import { Controller, Post, Get, Delete, Param, Body, Query, UseGuards, Request, BadRequestException, HttpCode, HttpStatus, ParseIntPipe, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiParam, ApiQuery, ApiBody, } from '@nestjs/swagger'; import { EnrollmentsService } from '../services'; import { EnrollCourseDto } from '../dto/enroll-course.dto'; import { EnrollmentResponseDto } from '../dto/enrollment-response.dto'; import { AvailableCoursesFilterDto, AvailableCoursesDto, } from '../dto/available-courses.dto'; import { DropCourseDto } from '../dto/drop-course.dto'; import { AssignInstructorDto, InstructorAssignmentResponseDto, } from '../dto/assign-instructor.dto'; import { AssignTADto, TAAssignmentResponseDto } from '../dto/assign-ta.dto'; import { Roles } from '../../auth/roles.decorator'; import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; import { RolesGuard } from '../../auth/guards/roles.guard'; import { RoleName } from '../../auth/entities/role.entity'; @ApiTags('✅ Enrollments') @Controller('api/enrollments') @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth('JWT-auth') export class EnrollmentsController { constructor(private readonly enrollmentsService: EnrollmentsService) {} /** * GET /api/enrollments/periods * Get enrollment periods (derived from semesters with registration dates) */ @Get('periods') @Roles( RoleName.IT_ADMIN, RoleName.ADMIN, RoleName.INSTRUCTOR, RoleName.TA, RoleName.STUDENT, ) @ApiOperation({ summary: 'Get enrollment periods', description: 'Returns semesters with registration date ranges as enrollment periods.', }) @ApiQuery({ name: 'departmentId', required: false, type: Number, description: 'When set, course and enrollment counts are limited to this department.', }) @ApiResponse({ status: 200, description: 'List of enrollment periods' }) async getEnrollmentPeriods( @Query('departmentId') departmentId?: string, ): Promise { const id = departmentId ? parseInt(departmentId, 10) : undefined; return this.enrollmentsService.getEnrollmentPeriods( id !== undefined && Number.isFinite(id) ? id : undefined, ); } /** * GET /api/enrollments/my-courses * Get all courses enrolled by the authenticated student */ @Get('my-courses') @Roles(RoleName.STUDENT) @ApiOperation({ summary: 'Get my enrolled courses', description: ` ## Get Student's Enrolled Courses Retrieves all courses the authenticated student is enrolled in. ### Access Control - **Authentication Required**: ✅ Yes (Bearer Token) - **Roles Required**: STUDENT only ### Filtering Use \`semester\` query parameter to filter by specific semester. ### Response Includes - Course information - Section details - Enrollment status - Grades (if available) `, }) @ApiQuery({ name: 'semester', required: false, type: Number, description: 'Semester ID to filter', }) @ApiResponse({ status: 200, description: 'List of enrolled courses' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 403, description: 'Forbidden - Student role required', }) async getMyEnrollments( @Request() req, @Query('semester') semester?: number, ): Promise { const userId = req.user.userId || req.user.id; return this.enrollmentsService.getMyEnrollments(userId, semester); } /** * GET /api/enrollments/available * Get list of available courses for student enrollment */ @Get('available') @Roles(RoleName.STUDENT) @ApiOperation({ summary: 'Get available courses', description: ` ## Get Available Courses for Enrollment Retrieves courses the student can enroll in based on prerequisites and availability. ### Access Control - **Authentication Required**: ✅ Yes (Bearer Token) - **Roles Required**: STUDENT only ### Availability Criteria - Prerequisites completed - Section has available seats - No schedule conflicts - Within enrollment period ### Filtering Options Available through query parameters (see AvailableCoursesFilterDto). `, }) @ApiResponse({ status: 200, description: 'List of available courses' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 403, description: 'Forbidden - Student role required', }) async getAvailableCourses( @Request() req, @Query() filters: AvailableCoursesFilterDto, ): Promise { return this.enrollmentsService.getAvailableCourses( req.user.userId, filters, ); } /** * POST /api/enrollments/register * Enroll a student in a course section */ @Post('register') @Roles(RoleName.STUDENT) @ApiOperation({ summary: 'Enroll in a course', description: ` ## Register for Course Section Enrolls the authenticated student in a course section. ### Access Control - **Authentication Required**: ✅ Yes (Bearer Token) - **Roles Required**: STUDENT only ### Enrollment Process 1. Validates prerequisites are met 2. Checks section capacity 3. Checks for schedule conflicts 4. Creates enrollment record 5. Updates section enrollment count ### Notes - If section is full, student is added to waitlist - Enrollment may be pending approval `, }) @ApiBody({ type: EnrollCourseDto }) @ApiResponse({ status: 201, description: 'Successfully enrolled' }) @ApiResponse({ status: 400, description: 'Prerequisites not met or schedule conflict', }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 403, description: 'Forbidden - Student role required', }) @ApiResponse({ status: 409, description: 'Already enrolled in this course' }) async enrollCourse( @Request() req, @Body() enrollCourseDto: EnrollCourseDto, ): Promise { const userId = req.user.userId || req.user.id; if (!userId) { throw new BadRequestException( 'User ID not found in authentication token', ); } return this.enrollmentsService.enrollStudent(userId, enrollCourseDto); } /** * GET /api/enrollments/teaching * Get all courses taught by the authenticated instructor */ @Get('teaching') @Roles(RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN) @ApiOperation({ summary: 'Get teaching courses', description: 'Retrieves all course sections assigned to the authenticated instructor.', }) @ApiResponse({ status: 200, description: 'List of teaching courses' }) async getTeachingCourses(@Request() req): Promise { const userId = req.user.userId || req.user.id; return this.enrollmentsService.getTeachingCourses(userId); } /** * GET /api/enrollments/student/:userId * List enrollments for a student (admin / IT admin / department head) */ @Get('student/:userId') @Roles(RoleName.ADMIN, RoleName.IT_ADMIN, RoleName.DEPARTMENT_HEAD) @ApiOperation({ summary: 'Get enrollments for a student (staff)', description: 'Returns the same enrollment payload as the student “my courses” view, for roster and admin tools.', }) @ApiParam({ name: 'userId', description: 'Student user ID', type: Number }) @ApiQuery({ name: 'semester', required: false, type: Number, description: 'Optional semester ID to filter sections', }) @ApiResponse({ status: 200, description: 'List of enrollments' }) async getEnrollmentsForStudent( @Param('userId', ParseIntPipe) userId: number, @Query('semester') semester?: number, ): Promise { return this.enrollmentsService.getMyEnrollments(userId, semester); } /** * GET /api/enrollments/:id * Get enrollment details by ID */ @Get(':id') @ApiOperation({ summary: 'Get enrollment by ID', description: ` ## Get Enrollment Details Retrieves details of a specific enrollment. ### Access Control - **Authentication Required**: ✅ Yes (Bearer Token) - **Roles Required**: Any authenticated user (own enrollments) or ADMIN/INSTRUCTOR `, }) @ApiParam({ name: 'id', description: 'Enrollment ID', type: Number }) @ApiResponse({ status: 200, description: 'Enrollment details' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 404, description: 'Enrollment not found' }) async getEnrollmentById( @Param('id') id: number, ): Promise { return this.enrollmentsService.getEnrollmentById(id); } /** * DELETE /api/enrollments/:id * Drop/withdraw from a course enrollment */ @Delete(':id') @ApiOperation({ summary: 'Drop/withdraw from course', description: ` ## Drop Course Enrollment Withdraws from an enrolled course. ### Access Control - **Authentication Required**: ✅ Yes (Bearer Token) - **Roles Required**: STUDENT (own enrollment) or ADMIN ### Withdrawal Rules - Before deadline: Full withdrawal - After deadline: May incur penalties or show as "W" grade - Admins can override restrictions ### Notes Dropping may move waitlisted students into the section. `, }) @ApiParam({ name: 'id', description: 'Enrollment ID', type: Number }) @ApiBody({ type: DropCourseDto, required: false }) @ApiResponse({ status: 200, description: 'Successfully dropped' }) @ApiResponse({ status: 400, description: 'Drop deadline passed' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 404, description: 'Enrollment not found' }) async dropCourse( @Request() req, @Param('id') enrollmentId: number, @Body() dropCourseDto?: DropCourseDto, ): Promise { const staffRoles = [ RoleName.ADMIN, RoleName.IT_ADMIN, RoleName.DEPARTMENT_HEAD, ]; const isAdmin = req.user.roles?.some((r) => staffRoles.includes(r.roleName)); return this.enrollmentsService.dropCourse( enrollmentId, req.user.userId, !!isAdmin, ); } /** * GET /api/courses/:courseId/enrollments * Get all enrollments for a specific course (instructor/admin only) */ @Get('course/:courseId/list') @Roles(RoleName.INSTRUCTOR, RoleName.ADMIN) @ApiOperation({ summary: 'Get course enrollments', description: ` ## Get All Enrollments for Course Retrieves all enrollments across all sections of a course. ### Access Control - **Authentication Required**: ✅ Yes (Bearer Token) - **Roles Required**: INSTRUCTOR, ADMIN only `, }) @ApiParam({ name: 'courseId', description: 'Course ID', type: Number }) @ApiResponse({ status: 200, description: 'List of enrollments' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 403, description: 'Forbidden - Instructor or Admin required', }) async getCourseEnrollments( @Param('courseId') courseId: number, ): Promise { // This would list all sections for the course // Implementation depends on course structure return []; } /** * GET /api/enrollments/section/:sectionId/students/count * Get enrolled students count for a section (instructor/admin only) */ @Get('section/:sectionId/students/count') @Roles(RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN) @ApiOperation({ summary: 'Get enrolled students count for section', description: 'Returns the actual count of enrolled students (status=ENROLLED) for a section.', }) @ApiParam({ name: 'sectionId', description: 'Section ID', type: Number }) @ApiResponse({ status: 200, description: 'Student count retrieved' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 403, description: 'Forbidden - Instructor, TA, or Admin required', }) @ApiResponse({ status: 404, description: 'Section not found' }) async getSectionStudentsCount( @Request() req, @Param('sectionId', ParseIntPipe) sectionId: number, ): Promise<{ count: number }> { const userId = req.user.userId || req.user.id; const roles = this.extractRoles(req.user); return this.enrollmentsService.getSectionStudentsCount(sectionId, userId, roles); } /** * GET /api/sections/:sectionId/students * Get all enrolled students in a section (instructor/admin only) */ @Get('section/:sectionId/students') @Roles(RoleName.INSTRUCTOR, RoleName.TA, RoleName.ADMIN) @ApiOperation({ summary: 'Get section students', description: ` ## Get Enrolled Students in Section Retrieves all students enrolled in a specific section. ### Access Control - **Authentication Required**: ✅ Yes (Bearer Token) - **Roles Required**: INSTRUCTOR, ADMIN only ### Response Includes - Student information - Enrollment status - Enrollment date `, }) @ApiParam({ name: 'sectionId', description: 'Section ID', type: Number }) @ApiResponse({ status: 200, description: 'List of enrolled students' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 403, description: 'Forbidden - Instructor or Admin required', }) @ApiResponse({ status: 404, description: 'Section not found' }) async getSectionStudents( @Request() req, @Param('sectionId') sectionId: number, ): Promise { const userId = req.user.userId || req.user.id; const roles = this.extractRoles(req.user); return this.enrollmentsService.getSectionStudents(sectionId, userId, roles); } /** * GET /api/sections/:sectionId/waitlist * Get waitlist for a section (instructor/admin only) */ @Get('section/:sectionId/waitlist') @Roles(RoleName.INSTRUCTOR, RoleName.ADMIN) @ApiOperation({ summary: 'Get section waitlist', description: ` ## Get Section Waitlist Retrieves all students on the waitlist for a section. ### Access Control - **Authentication Required**: ✅ Yes (Bearer Token) - **Roles Required**: INSTRUCTOR, ADMIN only ### Response Students are ordered by waitlist position. `, }) @ApiParam({ name: 'sectionId', description: 'Section ID', type: Number }) @ApiResponse({ status: 200, description: 'Waitlist students' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 403, description: 'Forbidden - Instructor or Admin required', }) @ApiResponse({ status: 404, description: 'Section not found' }) async getSectionWaitlist( @Param('sectionId') sectionId: number, ): Promise { return this.enrollmentsService.getWaitlist(sectionId); } /** * PUT /api/enrollments/:id/status * Update enrollment status (admin only) */ @Post(':id/status') @Roles(RoleName.ADMIN) @ApiOperation({ summary: 'Update enrollment status', description: ` ## Update Enrollment Status Manually updates the status of an enrollment. ### Access Control - **Authentication Required**: ✅ Yes (Bearer Token) - **Roles Required**: ADMIN only ### Available Statuses - enrolled, waitlisted, dropped, completed, failed `, }) @ApiParam({ name: 'id', description: 'Enrollment ID', type: Number }) @ApiBody({ schema: { type: 'object', properties: { status: { type: 'string', example: 'enrolled' }, }, }, }) @ApiResponse({ status: 200, description: 'Status updated' }) @ApiResponse({ status: 400, description: 'Invalid status' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 403, description: 'Forbidden - Admin required' }) @ApiResponse({ status: 404, description: 'Enrollment not found' }) async updateEnrollmentStatus( @Param('id') enrollmentId: number, @Body() body: { status: string }, ): Promise { return this.enrollmentsService.updateEnrollmentStatus(enrollmentId, body.status as any); } // ─── Admin: Instructor Assignment Endpoints ─────────────────────────────── /** * POST /api/enrollments/sections/:sectionId/instructors * Assign an instructor to a course section (admin only) */ @Post('sections/:sectionId/instructors') @Roles(RoleName.ADMIN) @HttpCode(HttpStatus.CREATED) @ApiOperation({ summary: 'Assign instructor to section', description: ` ## Assign Instructor to Course Section Assigns a user with the Instructor role to a specific course section. ### Access Control - **Authentication Required**: ✅ Yes (Bearer Token) - **Roles Required**: ADMIN only ### Instructor Roles - \`primary\` – Main instructor (default) - \`co_instructor\` – Secondary instructor - \`guest\` – Guest lecturer `, }) @ApiParam({ name: 'sectionId', description: 'Course section ID', type: Number, }) @ApiBody({ type: AssignInstructorDto }) @ApiResponse({ status: 201, description: 'Instructor assigned successfully', type: InstructorAssignmentResponseDto, }) @ApiResponse({ status: 400, description: 'Invalid request body' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 403, description: 'Forbidden - Admin required' }) @ApiResponse({ status: 404, description: 'Section or user not found' }) @ApiResponse({ status: 409, description: 'Instructor already assigned to this section', }) async assignInstructor( @Param('sectionId', ParseIntPipe) sectionId: number, @Body() dto: AssignInstructorDto, ): Promise { return this.enrollmentsService.assignInstructor(sectionId, dto); } /** * DELETE /api/enrollments/sections/:sectionId/instructors/:assignmentId * Remove an instructor from a course section (admin only) */ @Delete('sections/:sectionId/instructors/:assignmentId') @Roles(RoleName.ADMIN) @HttpCode(HttpStatus.NO_CONTENT) @ApiOperation({ summary: 'Remove instructor from section', description: ` ## Remove Instructor Assignment Removes an instructor assignment from a course section. ### Access Control - **Authentication Required**: ✅ Yes (Bearer Token) - **Roles Required**: ADMIN only `, }) @ApiParam({ name: 'sectionId', description: 'Course section ID', type: Number, }) @ApiParam({ name: 'assignmentId', description: 'Instructor assignment ID', type: Number, }) @ApiResponse({ status: 204, description: 'Instructor removed successfully' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 403, description: 'Forbidden - Admin required' }) @ApiResponse({ status: 404, description: 'Section or assignment not found' }) async removeInstructor( @Param('sectionId', ParseIntPipe) sectionId: number, @Param('assignmentId', ParseIntPipe) assignmentId: number, ): Promise { return this.enrollmentsService.removeInstructor(sectionId, assignmentId); } /** * GET /api/enrollments/sections/:sectionId/instructors * List all instructors for a section (admin/instructor) */ @Get('sections/:sectionId/instructors') @Roles(RoleName.ADMIN, RoleName.INSTRUCTOR, RoleName.TA) @ApiOperation({ summary: 'Get instructors for a section', description: ` ## List Section Instructors Returns all instructors assigned to a course section. ### Access Control - **Authentication Required**: ✅ Yes (Bearer Token) - **Roles Required**: ADMIN, INSTRUCTOR `, }) @ApiParam({ name: 'sectionId', description: 'Course section ID', type: Number, }) @ApiResponse({ status: 200, description: 'List of instructor assignments', type: [InstructorAssignmentResponseDto], }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 403, description: 'Forbidden - Admin or Instructor required', }) @ApiResponse({ status: 404, description: 'Section not found' }) async getSectionInstructors( @Request() req, @Param('sectionId', ParseIntPipe) sectionId: number, ): Promise { const userId = req.user.userId || req.user.id; const roles = this.extractRoles(req.user); return this.enrollmentsService.getSectionInstructors( sectionId, userId, roles, ); } /** * GET /api/enrollments/section/:sectionId/instructor * Get the primary instructor summary for a section */ @Get('section/:sectionId/instructor') @Roles(RoleName.ADMIN, RoleName.INSTRUCTOR, RoleName.TA, RoleName.STUDENT) @ApiOperation({ summary: 'Get assigned instructor for a section', description: 'Returns the assigned instructor summary for a course section, including user ID, full name, and email.', }) @ApiParam({ name: 'sectionId', description: 'Course section ID', type: Number, }) @ApiResponse({ status: 200, description: 'Assigned instructor summary', schema: { type: 'object', properties: { instructorId: { type: 'number', example: 58 }, instructor: { type: 'object', properties: { userId: { type: 'number', example: 58 }, fullName: { type: 'string', example: 'Tarek Instructor' }, email: { type: 'string', example: 'tarek@example.com' }, }, }, }, }, }) async getSectionInstructorSummary( @Request() req, @Param('sectionId', ParseIntPipe) sectionId: number, ): Promise<{ instructorId: number | null; instructor: { userId: number; fullName: string; email: string } | null; }> { const userId = req.user.userId || req.user.id; const roles = this.extractRoles(req.user); return this.enrollmentsService.getSectionInstructorSummary( sectionId, userId, roles, ); } // ─── Admin: TA Assignment Endpoints ────────────────────────────────────── /** * POST /api/enrollments/sections/:sectionId/tas * Assign a Teaching Assistant to a course section (admin only) */ @Post('sections/:sectionId/tas') @Roles(RoleName.ADMIN) @HttpCode(HttpStatus.CREATED) @ApiOperation({ summary: 'Assign Teaching Assistant to section', description: ` ## Assign TA to Course Section Assigns a user with the Teaching Assistant role to a specific course section. ### Access Control - **Authentication Required**: ✅ Yes (Bearer Token) - **Roles Required**: ADMIN only ### Optional Fields - \`responsibilities\` – Free-text description of TA duties (e.g., "Grading labs, office hours Mon/Wed") `, }) @ApiParam({ name: 'sectionId', description: 'Course section ID', type: Number, }) @ApiBody({ type: AssignTADto }) @ApiResponse({ status: 201, description: 'TA assigned successfully', type: TAAssignmentResponseDto, }) @ApiResponse({ status: 400, description: 'Invalid request body' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 403, description: 'Forbidden - Admin required' }) @ApiResponse({ status: 404, description: 'Section or user not found' }) @ApiResponse({ status: 409, description: 'TA already assigned to this section', }) async assignTA( @Param('sectionId', ParseIntPipe) sectionId: number, @Body() dto: AssignTADto, ): Promise { return this.enrollmentsService.assignTA(sectionId, dto); } /** * DELETE /api/enrollments/sections/:sectionId/tas/:assignmentId * Remove a Teaching Assistant from a course section (admin only) */ @Delete('sections/:sectionId/tas/:assignmentId') @Roles(RoleName.ADMIN) @HttpCode(HttpStatus.NO_CONTENT) @ApiOperation({ summary: 'Remove Teaching Assistant from section', description: ` ## Remove TA Assignment Removes a Teaching Assistant assignment from a course section. ### Access Control - **Authentication Required**: ✅ Yes (Bearer Token) - **Roles Required**: ADMIN only `, }) @ApiParam({ name: 'sectionId', description: 'Course section ID', type: Number, }) @ApiParam({ name: 'assignmentId', description: 'TA assignment ID', type: Number, }) @ApiResponse({ status: 204, description: 'TA removed successfully' }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 403, description: 'Forbidden - Admin required' }) @ApiResponse({ status: 404, description: 'Section or assignment not found' }) async removeTA( @Param('sectionId', ParseIntPipe) sectionId: number, @Param('assignmentId', ParseIntPipe) assignmentId: number, ): Promise { return this.enrollmentsService.removeTA(sectionId, assignmentId); } /** * GET /api/enrollments/sections/:sectionId/tas * List all TAs for a section (admin/instructor) */ @Get('sections/:sectionId/tas') @Roles(RoleName.ADMIN, RoleName.INSTRUCTOR, RoleName.TA) @ApiOperation({ summary: 'Get Teaching Assistants for a section', description: ` ## List Section Teaching Assistants Returns all Teaching Assistants assigned to a course section. ### Access Control - **Authentication Required**: ✅ Yes (Bearer Token) - **Roles Required**: ADMIN, INSTRUCTOR `, }) @ApiParam({ name: 'sectionId', description: 'Course section ID', type: Number, }) @ApiResponse({ status: 200, description: 'List of TA assignments', type: [TAAssignmentResponseDto], }) @ApiResponse({ status: 401, description: 'Unauthorized' }) @ApiResponse({ status: 403, description: 'Forbidden - Admin or Instructor required', }) @ApiResponse({ status: 404, description: 'Section not found' }) async getSectionTAs( @Request() req, @Param('sectionId', ParseIntPipe) sectionId: number, ): Promise { const userId = req.user.userId || req.user.id; const roles = this.extractRoles(req.user); return this.enrollmentsService.getSectionTAs(sectionId, userId, roles); } /** * GET /api/enrollments/section/:sectionId/tas * Get simplified TA summaries for a section */ @Get('section/:sectionId/tas') @Roles(RoleName.ADMIN, RoleName.INSTRUCTOR, RoleName.TA, RoleName.STUDENT) @ApiOperation({ summary: 'Get assigned TAs for a section', description: 'Returns simplified Teaching Assistant summaries for a course section, including user ID, full name, and email.', }) @ApiParam({ name: 'sectionId', description: 'Course section ID', type: Number, }) @ApiResponse({ status: 200, description: 'Assigned TA summaries', schema: { type: 'array', items: { type: 'object', properties: { userId: { type: 'number', example: 60 }, fullName: { type: 'string', example: 'Tarek TA' }, email: { type: 'string', example: 'ta@example.com' }, }, }, }, }) async getSectionTASummaries( @Request() req, @Param('sectionId', ParseIntPipe) sectionId: number, ): Promise> { const userId = req.user.userId || req.user.id; const roles = this.extractRoles(req.user); return this.enrollmentsService.getSectionTASummaries( sectionId, userId, roles, ); } private extractRoles(user: any): string[] { if (!user.roles) return []; return user.roles.map((r: any) => r.roleName || r.name || r); } }