Spaces:
Sleeping
Sleeping
| import { | |
| Controller, | |
| Get, | |
| Post, | |
| Put, | |
| Patch, | |
| Delete, | |
| Param, | |
| Body, | |
| Query, | |
| UseGuards, | |
| ParseIntPipe, | |
| Request, | |
| } from '@nestjs/common'; | |
| import { | |
| ApiTags, | |
| ApiBearerAuth, | |
| ApiOperation, | |
| ApiResponse, | |
| ApiParam, | |
| ApiBody, | |
| } from '@nestjs/swagger'; | |
| import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; | |
| import { RolesGuard } from '../../auth/guards/roles.guard'; | |
| import { Roles } from '../../../common/decorators/roles.decorator'; | |
| import { RoleName } from '../../auth/entities/role.entity'; | |
| import { OfficeHoursService } from '../services/office-hours.service'; | |
| import { CreateSlotDto } from '../dto/create-slot.dto'; | |
| import { UpdateSlotDto } from '../dto/update-slot.dto'; | |
| import { BookAppointmentDto } from '../dto/book-appointment.dto'; | |
| import { UpdateAppointmentDto } from '../dto/update-appointment.dto'; | |
| ('🕐 Office Hours') | |
| ('JWT-auth') | |
| ('api/office-hours') | |
| (JwtAuthGuard, RolesGuard) | |
| export class OfficeHoursController { | |
| constructor(private readonly officeHoursService: OfficeHoursService) {} | |
| // ── Slots ── | |
| ('slots') | |
| ( | |
| RoleName.STUDENT, | |
| RoleName.INSTRUCTOR, | |
| RoleName.TA, | |
| RoleName.ADMIN, | |
| RoleName.IT_ADMIN, | |
| ) | |
| ({ | |
| summary: 'List all office hour slots', | |
| description: | |
| 'Retrieves all office hour slots with instructor details. ' + | |
| 'Access roles: ALL. Uses table: `office_hour_slots`.', | |
| }) | |
| ({ status: 200, description: 'Slots returned' }) | |
| ({ status: 401, description: 'Unauthorized' }) | |
| async getSlots( | |
| ('instructorId') instructorId?: string, | |
| ('dayOfWeek') dayOfWeek?: string, | |
| ('page') page?: string, | |
| ('limit') limit?: string, | |
| ) { | |
| return this.officeHoursService.getSlots({ | |
| instructorId: instructorId ? Number(instructorId) : undefined, | |
| dayOfWeek, | |
| page: page ? Number(page) : undefined, | |
| limit: limit ? Number(limit) : undefined, | |
| }); | |
| } | |
| () | |
| ( | |
| RoleName.STUDENT, | |
| RoleName.INSTRUCTOR, | |
| RoleName.TA, | |
| RoleName.ADMIN, | |
| RoleName.IT_ADMIN, | |
| ) | |
| ({ | |
| summary: 'List office hours (compatibility route)', | |
| description: | |
| 'Compatibility alias for list slots. Returns paginated office hour slots for admin/front-end clients using /api/office-hours.', | |
| }) | |
| ({ status: 200, description: 'Office hours returned' }) | |
| async getOfficeHours( | |
| ('instructorId') instructorId?: string, | |
| ('dayOfWeek') dayOfWeek?: string, | |
| ('page') page?: string, | |
| ('limit') limit?: string, | |
| ) { | |
| return this.officeHoursService.getSlots({ | |
| instructorId: instructorId ? Number(instructorId) : undefined, | |
| dayOfWeek, | |
| page: page ? Number(page) : undefined, | |
| limit: limit ? Number(limit) : undefined, | |
| }); | |
| } | |
| ('my-slots') | |
| (RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| ({ | |
| summary: "Get instructor's own office hour slots", | |
| description: | |
| 'Retrieves all office hour slots created by the current instructor. ' + | |
| 'Access roles: INSTRUCTOR, TA, ADMIN, IT_ADMIN. Uses table: `office_hour_slots`.', | |
| }) | |
| ({ status: 200, description: "Instructor's slots returned" }) | |
| ({ status: 401, description: 'Unauthorized' }) | |
| async getMySlots(() req) { | |
| return this.officeHoursService.getMySlots(req.user.userId); | |
| } | |
| ('available') | |
| ( | |
| RoleName.STUDENT, | |
| RoleName.INSTRUCTOR, | |
| RoleName.TA, | |
| RoleName.ADMIN, | |
| RoleName.IT_ADMIN, | |
| ) | |
| ({ | |
| summary: 'Get available office hour slots for booking', | |
| description: | |
| 'Retrieves all active office hour slots that students can book. ' + | |
| 'Access roles: ALL. Uses table: `office_hour_slots`.', | |
| }) | |
| ({ status: 200, description: 'Available slots returned' }) | |
| ({ status: 401, description: 'Unauthorized' }) | |
| async getAvailableSlots(('instructorId') instructorId?: string) { | |
| return this.officeHoursService.getAvailableSlots( | |
| instructorId ? Number(instructorId) : undefined, | |
| ); | |
| } | |
| ('slots') | |
| (RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| ({ | |
| summary: 'Create an office hour slot', | |
| description: | |
| 'Creates a new weekly office hour slot for the current instructor. ' + | |
| 'Access roles: INSTRUCTOR, ADMIN, IT_ADMIN. Uses table: `office_hour_slots`.', | |
| }) | |
| ({ type: CreateSlotDto }) | |
| ({ status: 201, description: 'Slot created' }) | |
| ({ status: 400, description: 'Validation error' }) | |
| async createSlot(() dto: CreateSlotDto, () req) { | |
| const callerUserId = req.user.userId; | |
| const roles = this.extractRoles(req.user); | |
| const isAdmin = | |
| roles.includes(RoleName.ADMIN) || roles.includes(RoleName.IT_ADMIN); | |
| const targetInstructorId = | |
| isAdmin && dto.instructorId ? dto.instructorId : callerUserId; | |
| return this.officeHoursService.createSlot(dto, targetInstructorId); | |
| } | |
| () | |
| (RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| ({ | |
| summary: 'Create an office hour (compatibility route)', | |
| description: | |
| 'Compatibility alias for creating slots via /api/office-hours.', | |
| }) | |
| ({ type: CreateSlotDto }) | |
| ({ status: 201, description: 'Office hour created' }) | |
| async createOfficeHour(() dto: CreateSlotDto, () req) { | |
| const callerUserId = req.user.userId; | |
| const roles = this.extractRoles(req.user); | |
| const isAdmin = | |
| roles.includes(RoleName.ADMIN) || roles.includes(RoleName.IT_ADMIN); | |
| const targetInstructorId = | |
| isAdmin && dto.instructorId ? dto.instructorId : callerUserId; | |
| return this.officeHoursService.createSlot(dto, targetInstructorId); | |
| } | |
| ('slots/:id') | |
| (RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| ({ | |
| summary: 'Update an office hour slot', | |
| description: | |
| 'Updates an existing office hour slot. ' + | |
| 'Access roles: INSTRUCTOR, ADMIN, IT_ADMIN. Uses table: `office_hour_slots`.', | |
| }) | |
| ({ name: 'id', type: Number, description: 'Slot ID' }) | |
| ({ type: UpdateSlotDto }) | |
| ({ status: 200, description: 'Slot updated' }) | |
| ({ status: 404, description: 'Slot not found' }) | |
| async updateSlot( | |
| ('id', ParseIntPipe) id: number, | |
| () dto: UpdateSlotDto, | |
| () req, | |
| ) { | |
| return this.officeHoursService.updateSlot(id, dto, req.user.userId); | |
| } | |
| (':id') | |
| (RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| ({ | |
| summary: 'Update an office hour (compatibility route)', | |
| description: | |
| 'Compatibility alias for updating slots via /api/office-hours/:id.', | |
| }) | |
| ({ name: 'id', type: Number, description: 'Slot ID' }) | |
| ({ type: UpdateSlotDto }) | |
| ({ status: 200, description: 'Office hour updated' }) | |
| async updateOfficeHour( | |
| ('id', ParseIntPipe) id: number, | |
| () dto: UpdateSlotDto, | |
| () req, | |
| ) { | |
| return this.officeHoursService.updateSlot(id, dto, req.user.userId); | |
| } | |
| ('slots/:id') | |
| (RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| ({ | |
| summary: 'Delete an office hour slot', | |
| description: | |
| 'Deletes a slot and cancels all future appointments for it. ' + | |
| 'Access roles: INSTRUCTOR, ADMIN, IT_ADMIN. Uses tables: `office_hour_slots`, `office_hour_appointments`.', | |
| }) | |
| ({ name: 'id', type: Number, description: 'Slot ID' }) | |
| ({ status: 200, description: 'Slot deleted' }) | |
| ({ status: 404, description: 'Slot not found' }) | |
| async deleteSlot(('id', ParseIntPipe) id: number) { | |
| return this.officeHoursService.deleteSlot(id); | |
| } | |
| (':id') | |
| (RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| ({ | |
| summary: 'Delete an office hour (compatibility route)', | |
| description: | |
| 'Compatibility alias for deleting slots via /api/office-hours/:id.', | |
| }) | |
| ({ name: 'id', type: Number, description: 'Slot ID' }) | |
| ({ status: 200, description: 'Office hour deleted' }) | |
| async deleteOfficeHour(('id', ParseIntPipe) id: number) { | |
| return this.officeHoursService.deleteSlot(id); | |
| } | |
| // ── Appointments ── | |
| ('appointments') | |
| (RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN) | |
| ({ | |
| summary: 'List all appointments', | |
| description: | |
| 'Retrieves all appointments. For instructors, returns appointments for their slots. ' + | |
| 'Access roles: INSTRUCTOR, ADMIN, IT_ADMIN. Uses tables: `office_hour_appointments`, `office_hour_slots`.', | |
| }) | |
| ({ status: 200, description: 'Appointments returned' }) | |
| ({ status: 401, description: 'Unauthorized' }) | |
| async getAppointments(() req, ('slotId') slotId?: string) { | |
| return this.officeHoursService.getAppointments( | |
| req.user.userId, | |
| slotId ? Number(slotId) : undefined, | |
| ); | |
| } | |
| ('my-appointments') | |
| ( | |
| RoleName.STUDENT, | |
| RoleName.INSTRUCTOR, | |
| RoleName.TA, | |
| RoleName.ADMIN, | |
| RoleName.IT_ADMIN, | |
| ) | |
| ({ | |
| summary: "Get student's own appointments", | |
| description: | |
| 'Retrieves all appointments the current user has booked. ' + | |
| 'Access roles: ALL. Uses tables: `office_hour_appointments`, `office_hour_slots`.', | |
| }) | |
| ({ status: 200, description: "Student's appointments returned" }) | |
| ({ status: 401, description: 'Unauthorized' }) | |
| async getMyAppointments(() req) { | |
| return this.officeHoursService.getMyAppointments(req.user.userId); | |
| } | |
| (':id') | |
| ( | |
| RoleName.STUDENT, | |
| RoleName.INSTRUCTOR, | |
| RoleName.TA, | |
| RoleName.ADMIN, | |
| RoleName.IT_ADMIN, | |
| ) | |
| ({ | |
| summary: 'Get office hour slot by ID', | |
| description: 'Retrieves one office hour slot by slot ID.', | |
| }) | |
| ({ name: 'id', type: Number, description: 'Slot ID' }) | |
| ({ status: 200, description: 'Slot returned' }) | |
| ({ status: 404, description: 'Slot not found' }) | |
| async getSlotById(('id', ParseIntPipe) id: number) { | |
| return this.officeHoursService.getSlotById(id); | |
| } | |
| ('appointments') | |
| ( | |
| RoleName.STUDENT, | |
| RoleName.INSTRUCTOR, | |
| RoleName.TA, | |
| RoleName.ADMIN, | |
| RoleName.IT_ADMIN, | |
| ) | |
| ({ | |
| summary: 'Book an appointment', | |
| description: | |
| 'Books an appointment for an available office hour slot. Validates slot is active and not full. ' + | |
| 'Access roles: ALL (primarily STUDENT). Uses tables: `office_hour_appointments`, `office_hour_slots`.', | |
| }) | |
| ({ type: BookAppointmentDto }) | |
| ({ status: 201, description: 'Appointment booked' }) | |
| ({ status: 400, description: 'Slot is full or not active' }) | |
| ({ status: 404, description: 'Slot not found' }) | |
| async bookAppointment(() dto: BookAppointmentDto, () req) { | |
| return this.officeHoursService.bookAppointment(dto, req.user.userId); | |
| } | |
| ('appointments/:id') | |
| ( | |
| RoleName.STUDENT, | |
| RoleName.INSTRUCTOR, | |
| RoleName.ADMIN, | |
| RoleName.IT_ADMIN, | |
| ) | |
| ({ | |
| summary: 'Update an appointment', | |
| description: | |
| 'Updates status or notes of an appointment (confirm, cancel, complete, no_show). ' + | |
| 'Access roles: STUDENT, INSTRUCTOR, ADMIN, IT_ADMIN. Uses table: `office_hour_appointments`.', | |
| }) | |
| ({ name: 'id', type: Number, description: 'Appointment ID' }) | |
| ({ type: UpdateAppointmentDto }) | |
| ({ status: 200, description: 'Appointment updated' }) | |
| ({ status: 404, description: 'Appointment not found' }) | |
| async updateAppointment( | |
| ('id', ParseIntPipe) id: number, | |
| () dto: UpdateAppointmentDto, | |
| () req, | |
| ) { | |
| return this.officeHoursService.updateAppointment(id, dto, req.user.userId); | |
| } | |
| ('appointments/:id') | |
| ( | |
| RoleName.STUDENT, | |
| RoleName.INSTRUCTOR, | |
| RoleName.ADMIN, | |
| RoleName.IT_ADMIN, | |
| ) | |
| ({ | |
| summary: 'Cancel an appointment', | |
| description: | |
| 'Cancels an appointment and records who cancelled it. ' + | |
| 'Access roles: STUDENT, INSTRUCTOR, ADMIN, IT_ADMIN. Uses table: `office_hour_appointments`.', | |
| }) | |
| ({ name: 'id', type: Number, description: 'Appointment ID' }) | |
| ({ status: 200, description: 'Appointment cancelled' }) | |
| ({ status: 404, description: 'Appointment not found' }) | |
| async cancelAppointment( | |
| ('id', ParseIntPipe) id: number, | |
| () req, | |
| ) { | |
| return this.officeHoursService.cancelAppointment(id, req.user.userId); | |
| } | |
| private extractRoles(user: any): string[] { | |
| if (Array.isArray(user.roles)) { | |
| return user.roles.map((r: any) => | |
| typeof r === 'string' ? r : r.roleName || r.name, | |
| ); | |
| } | |
| return []; | |
| } | |
| } | |