eduverse-backend / src /modules /office-hours /controllers /office-hours.controller.ts
tarekdx3's picture
feat: add database test script for office hour slots retrieval
ed9da92
Raw
History Blame Contribute Delete
13.1 kB
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';
@ApiTags('🕐 Office Hours')
@ApiBearerAuth('JWT-auth')
@Controller('api/office-hours')
@UseGuards(JwtAuthGuard, RolesGuard)
export class OfficeHoursController {
constructor(private readonly officeHoursService: OfficeHoursService) {}
// ── Slots ──
@Get('slots')
@Roles(
RoleName.STUDENT,
RoleName.INSTRUCTOR,
RoleName.TA,
RoleName.ADMIN,
RoleName.IT_ADMIN,
)
@ApiOperation({
summary: 'List all office hour slots',
description:
'Retrieves all office hour slots with instructor details. ' +
'Access roles: ALL. Uses table: `office_hour_slots`.',
})
@ApiResponse({ status: 200, description: 'Slots returned' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async getSlots(
@Query('instructorId') instructorId?: string,
@Query('dayOfWeek') dayOfWeek?: string,
@Query('page') page?: string,
@Query('limit') limit?: string,
) {
return this.officeHoursService.getSlots({
instructorId: instructorId ? Number(instructorId) : undefined,
dayOfWeek,
page: page ? Number(page) : undefined,
limit: limit ? Number(limit) : undefined,
});
}
@Get()
@Roles(
RoleName.STUDENT,
RoleName.INSTRUCTOR,
RoleName.TA,
RoleName.ADMIN,
RoleName.IT_ADMIN,
)
@ApiOperation({
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.',
})
@ApiResponse({ status: 200, description: 'Office hours returned' })
async getOfficeHours(
@Query('instructorId') instructorId?: string,
@Query('dayOfWeek') dayOfWeek?: string,
@Query('page') page?: string,
@Query('limit') limit?: string,
) {
return this.officeHoursService.getSlots({
instructorId: instructorId ? Number(instructorId) : undefined,
dayOfWeek,
page: page ? Number(page) : undefined,
limit: limit ? Number(limit) : undefined,
});
}
@Get('my-slots')
@Roles(RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN, RoleName.IT_ADMIN)
@ApiOperation({
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`.',
})
@ApiResponse({ status: 200, description: "Instructor's slots returned" })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async getMySlots(@Request() req) {
return this.officeHoursService.getMySlots(req.user.userId);
}
@Get('available')
@Roles(
RoleName.STUDENT,
RoleName.INSTRUCTOR,
RoleName.TA,
RoleName.ADMIN,
RoleName.IT_ADMIN,
)
@ApiOperation({
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`.',
})
@ApiResponse({ status: 200, description: 'Available slots returned' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async getAvailableSlots(@Query('instructorId') instructorId?: string) {
return this.officeHoursService.getAvailableSlots(
instructorId ? Number(instructorId) : undefined,
);
}
@Post('slots')
@Roles(RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN)
@ApiOperation({
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`.',
})
@ApiBody({ type: CreateSlotDto })
@ApiResponse({ status: 201, description: 'Slot created' })
@ApiResponse({ status: 400, description: 'Validation error' })
async createSlot(@Body() dto: CreateSlotDto, @Request() 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);
}
@Post()
@Roles(RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN)
@ApiOperation({
summary: 'Create an office hour (compatibility route)',
description:
'Compatibility alias for creating slots via /api/office-hours.',
})
@ApiBody({ type: CreateSlotDto })
@ApiResponse({ status: 201, description: 'Office hour created' })
async createOfficeHour(@Body() dto: CreateSlotDto, @Request() 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);
}
@Put('slots/:id')
@Roles(RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN)
@ApiOperation({
summary: 'Update an office hour slot',
description:
'Updates an existing office hour slot. ' +
'Access roles: INSTRUCTOR, ADMIN, IT_ADMIN. Uses table: `office_hour_slots`.',
})
@ApiParam({ name: 'id', type: Number, description: 'Slot ID' })
@ApiBody({ type: UpdateSlotDto })
@ApiResponse({ status: 200, description: 'Slot updated' })
@ApiResponse({ status: 404, description: 'Slot not found' })
async updateSlot(
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateSlotDto,
@Request() req,
) {
return this.officeHoursService.updateSlot(id, dto, req.user.userId);
}
@Put(':id')
@Roles(RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN)
@ApiOperation({
summary: 'Update an office hour (compatibility route)',
description:
'Compatibility alias for updating slots via /api/office-hours/:id.',
})
@ApiParam({ name: 'id', type: Number, description: 'Slot ID' })
@ApiBody({ type: UpdateSlotDto })
@ApiResponse({ status: 200, description: 'Office hour updated' })
async updateOfficeHour(
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateSlotDto,
@Request() req,
) {
return this.officeHoursService.updateSlot(id, dto, req.user.userId);
}
@Delete('slots/:id')
@Roles(RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN)
@ApiOperation({
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`.',
})
@ApiParam({ name: 'id', type: Number, description: 'Slot ID' })
@ApiResponse({ status: 200, description: 'Slot deleted' })
@ApiResponse({ status: 404, description: 'Slot not found' })
async deleteSlot(@Param('id', ParseIntPipe) id: number) {
return this.officeHoursService.deleteSlot(id);
}
@Delete(':id')
@Roles(RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN)
@ApiOperation({
summary: 'Delete an office hour (compatibility route)',
description:
'Compatibility alias for deleting slots via /api/office-hours/:id.',
})
@ApiParam({ name: 'id', type: Number, description: 'Slot ID' })
@ApiResponse({ status: 200, description: 'Office hour deleted' })
async deleteOfficeHour(@Param('id', ParseIntPipe) id: number) {
return this.officeHoursService.deleteSlot(id);
}
// ── Appointments ──
@Get('appointments')
@Roles(RoleName.INSTRUCTOR, RoleName.ADMIN, RoleName.IT_ADMIN)
@ApiOperation({
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`.',
})
@ApiResponse({ status: 200, description: 'Appointments returned' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async getAppointments(@Request() req, @Query('slotId') slotId?: string) {
return this.officeHoursService.getAppointments(
req.user.userId,
slotId ? Number(slotId) : undefined,
);
}
@Get('my-appointments')
@Roles(
RoleName.STUDENT,
RoleName.INSTRUCTOR,
RoleName.TA,
RoleName.ADMIN,
RoleName.IT_ADMIN,
)
@ApiOperation({
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`.',
})
@ApiResponse({ status: 200, description: "Student's appointments returned" })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async getMyAppointments(@Request() req) {
return this.officeHoursService.getMyAppointments(req.user.userId);
}
@Get(':id')
@Roles(
RoleName.STUDENT,
RoleName.INSTRUCTOR,
RoleName.TA,
RoleName.ADMIN,
RoleName.IT_ADMIN,
)
@ApiOperation({
summary: 'Get office hour slot by ID',
description: 'Retrieves one office hour slot by slot ID.',
})
@ApiParam({ name: 'id', type: Number, description: 'Slot ID' })
@ApiResponse({ status: 200, description: 'Slot returned' })
@ApiResponse({ status: 404, description: 'Slot not found' })
async getSlotById(@Param('id', ParseIntPipe) id: number) {
return this.officeHoursService.getSlotById(id);
}
@Post('appointments')
@Roles(
RoleName.STUDENT,
RoleName.INSTRUCTOR,
RoleName.TA,
RoleName.ADMIN,
RoleName.IT_ADMIN,
)
@ApiOperation({
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`.',
})
@ApiBody({ type: BookAppointmentDto })
@ApiResponse({ status: 201, description: 'Appointment booked' })
@ApiResponse({ status: 400, description: 'Slot is full or not active' })
@ApiResponse({ status: 404, description: 'Slot not found' })
async bookAppointment(@Body() dto: BookAppointmentDto, @Request() req) {
return this.officeHoursService.bookAppointment(dto, req.user.userId);
}
@Patch('appointments/:id')
@Roles(
RoleName.STUDENT,
RoleName.INSTRUCTOR,
RoleName.ADMIN,
RoleName.IT_ADMIN,
)
@ApiOperation({
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`.',
})
@ApiParam({ name: 'id', type: Number, description: 'Appointment ID' })
@ApiBody({ type: UpdateAppointmentDto })
@ApiResponse({ status: 200, description: 'Appointment updated' })
@ApiResponse({ status: 404, description: 'Appointment not found' })
async updateAppointment(
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateAppointmentDto,
@Request() req,
) {
return this.officeHoursService.updateAppointment(id, dto, req.user.userId);
}
@Delete('appointments/:id')
@Roles(
RoleName.STUDENT,
RoleName.INSTRUCTOR,
RoleName.ADMIN,
RoleName.IT_ADMIN,
)
@ApiOperation({
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`.',
})
@ApiParam({ name: 'id', type: Number, description: 'Appointment ID' })
@ApiResponse({ status: 200, description: 'Appointment cancelled' })
@ApiResponse({ status: 404, description: 'Appointment not found' })
async cancelAppointment(
@Param('id', ParseIntPipe) id: number,
@Request() 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 [];
}
}