diff --git a/packages/backend/src/auth/guards/roles.guard.ts b/packages/backend/src/auth/guards/roles.guard.ts index a3253bc..1db2e50 100644 --- a/packages/backend/src/auth/guards/roles.guard.ts +++ b/packages/backend/src/auth/guards/roles.guard.ts @@ -17,8 +17,21 @@ export class RolesGuard implements CanActivate { } const { user } = context.switchToHttp().getRequest(); - return requiredRoles.some((role) => - user?.roles?.includes(users_roles[role]), + const rawUserRoles = user?.roles ?? user?.role; + + // Normalize to an array of uppercased strings for robust comparison + const normalizedUserRoles: string[] = Array.isArray(rawUserRoles) + ? rawUserRoles.map((r: any) => String(r).toUpperCase()) + : rawUserRoles + ? [String(rawUserRoles).toUpperCase()] + : []; + + const normalizedRequiredRoles = requiredRoles.map((r) => + String(users_roles[r]).toUpperCase(), + ); + + return normalizedRequiredRoles.some((req) => + normalizedUserRoles.includes(req), ); } } diff --git a/packages/backend/src/mentee/mentee.controller.ts b/packages/backend/src/mentee/mentee.controller.ts index b537a4d..0b312a1 100644 --- a/packages/backend/src/mentee/mentee.controller.ts +++ b/packages/backend/src/mentee/mentee.controller.ts @@ -51,7 +51,7 @@ export class MenteeController { description: 'Fetch any mentee applications. \n\n REQUIRED ROLES: **ADMIN**', }) - @ApiBearerAuth() + @ApiBearerAuth('JWT') findAll(@Body() filters: FilterMenteeDto) { return this.menteeService.findAll(filters); } @@ -69,7 +69,7 @@ export class MenteeController { description: 'There is no mentee application for this user (USER ID: #`:id` )', }) - @ApiBearerAuth() + @ApiBearerAuth('JWT') findMyApplication(@GetUser() user: JwtPayload) { return this.menteeService.findOneByUserId(user.sub); } @@ -88,7 +88,7 @@ export class MenteeController { description: 'There is no mentee application with ID: `:menteeApplicationId`. Make sure you are not using a user ID', }) - @ApiBearerAuth() + @ApiBearerAuth('JWT') findOne(@Param('menteeApplicationId') menteeApplicationId: string) { return this.menteeService.findOneById(+menteeApplicationId); } @@ -108,7 +108,7 @@ export class MenteeController { }) @ApiBadRequestResponse({ description: 'Mentee already exists' }) @ApiNotFoundResponse({ description: 'The associated user does not exists' }) - @ApiBearerAuth() + @ApiBearerAuth('JWT') async create( @GetUser() user: JwtPayload, @Body() createMenteeDto: CreateMenteeDto, @@ -136,7 +136,7 @@ export class MenteeController { @ApiNotFoundResponse({ description: 'Mentee application for user with ID: `user_id` not found', }) - @ApiBearerAuth() + @ApiBearerAuth('JWT') update( @GetUser() user: JwtPayload, @Body() updateMenteeDto: UpdateMenteeDto, @@ -169,7 +169,7 @@ export class MenteeController { @ApiBadRequestResponse({ description: 'Error updating mentee application status: `[ERROR MESSAGE]`', }) - @ApiBearerAuth() + @ApiBearerAuth('JWT') updateStatus( @Param('menteeApplicationId') menteeApplicationId: string, @Body() updateMenteeStatusDto: UpdateMenteeStatusDto, @@ -180,6 +180,62 @@ export class MenteeController { return this.menteeService.updateStatus(+menteeApplicationId, updatedMentee); } + @Get('my-dashboard') + @Roles('USER', 'MENTEE', 'ADMIN') + @ApiOkResponse({ description: 'Mentee dashboard payload' }) + @ApiInternalServerErrorResponse({ description: 'Internal server error' }) + @ApiOperation({ + summary: 'Get mentee dashboard', + description: + 'Returns current mentor profile, statistics, and next session info for the logged mentee. REQUIRED ROLES: USER | MENTEE | ADMIN', + }) + @ApiBearerAuth('JWT') + getMyDashboard(@GetUser() user: JwtPayload) { + return this.menteeService.getMyDashboard(user.sub); + } + + @Get('my-past-mentors') + @Roles('USER', 'MENTEE', 'ADMIN') + @ApiOkResponse({ description: 'List of past mentors' }) + @ApiInternalServerErrorResponse({ description: 'Internal server error' }) + @ApiOperation({ + summary: 'Get past mentors', + description: + 'Returns mentors previously matched with the mentee. REQUIRED ROLES: USER | MENTEE | ADMIN', + }) + @ApiBearerAuth('JWT') + async getMyPastMentors(@GetUser() user: JwtPayload) { + return await this.menteeService.getMyPastMentors(user.sub); + } + + @Get('my-notes') + @Roles('USER', 'MENTEE', 'ADMIN') + @ApiOkResponse({ description: 'List of mentee notes across sessions' }) + @ApiInternalServerErrorResponse({ description: 'Internal server error' }) + @ApiOperation({ + summary: 'Get mentee notes', + description: + 'Returns notes the mentee took for mentorship sessions. REQUIRED ROLES: USER | MENTEE | ADMIN', + }) + @ApiBearerAuth('JWT') + async getMyNotes(@GetUser() user: JwtPayload) { + return await this.menteeService.getMyNotes(user.sub); + } + + @Get('my-upcoming-session') + @Roles('USER', 'MENTEE', 'ADMIN') + @ApiOkResponse({ description: 'Next upcoming session for mentee' }) + @ApiInternalServerErrorResponse({ description: 'Internal server error' }) + @ApiOperation({ + summary: 'Get mentee upcoming session', + description: + 'Returns the next upcoming session for the mentee. REQUIRED ROLES: USER | MENTEE | ADMIN', + }) + @ApiBearerAuth('JWT') + async getMyUpcomingSession(@GetUser() user: JwtPayload) { + return await this.menteeService.getMyUpcomingSession(user.sub); + } + // TO-DO: REMOVE MENTEE INFORMATION? OR CHANGE STATUS TO "REJECTED" OR "DELETED" TO KEEP THE INFORMATION? @Delete(':id') diff --git a/packages/backend/src/mentee/mentee.service.ts b/packages/backend/src/mentee/mentee.service.ts index 8cffc2f..0d6c15f 100644 --- a/packages/backend/src/mentee/mentee.service.ts +++ b/packages/backend/src/mentee/mentee.service.ts @@ -356,6 +356,260 @@ export class MenteeService { } } + async getMyDashboard(menteeId: number) { + console.log('MENTEE ACCESSING SERVICE - MY DASHBOARD: ', menteeId); + try { + // 1) Find next upcoming session for this mentee (soonest by dateStart) + const nextSession = await this.prisma.mentorshipSessions.findFirst({ + where: { menteeId, dateStart: { gt: new Date() } }, + orderBy: { dateStart: 'asc' }, + include: { + mentor: { + select: { + id: true, + first_name: true, + last_name: true, + email: true, + profession: true, + company_name: true, + picture_upload_link: true, + }, + }, + mentee: { select: { id: true } }, + }, + }); + + // If we have an upcoming session, tie dashboard to that mentor + if (nextSession) { + const mentorId = nextSession.mentor?.id; + + // 4) First session with this mentor as mentorshipStarted + const firstSessionWithMentor = + await this.prisma.mentorshipSessions.findFirst({ + where: { menteeId, mentorId }, + orderBy: { dateStart: 'asc' }, + select: { dateStart: true }, + }); + + // 5) Sessions attended with this mentor from first session until now + const sessionsAttended = await this.prisma.mentorshipSessions.count({ + where: { + menteeId, + mentorId, + dateEnd: { lt: new Date() }, + ...(firstSessionWithMentor?.dateStart + ? { dateStart: { gte: firstSessionWithMentor.dateStart } } + : {}), + }, + }); + + const mentorUser = nextSession.mentor + ? nextSession.mentor + : await this.prisma.users.findUnique({ + where: { id: mentorId! }, + select: { + id: true, + first_name: true, + last_name: true, + email: true, + profession: true, + company_name: true, + picture_upload_link: true, + }, + }); + + const mentor = mentorUser + ? { + id: (mentorUser as any).id, + firstName: mentorUser.first_name, + lastName: mentorUser.last_name, + email: mentorUser.email, + profession: mentorUser.profession ?? undefined, + company: mentorUser.company_name ?? undefined, + expertise: mentorUser.profession ?? undefined, + avatarUrl: mentorUser.picture_upload_link ?? undefined, + } + : null; + + const menteeDashboard = { + mentor, + mentorshipStarted: + firstSessionWithMentor?.dateStart?.toISOString() ?? null, + sessionsAttended, + // Keep existing field used by frontend + nextSession: nextSession.dateStart?.toISOString() ?? null, + // Add link as requested by API spec while keeping compatibility + nextSessionLink: nextSession.link ?? null, + } as any; + + console.log('MENTEE DASHBOARD', menteeDashboard); + + return menteeDashboard; + } + + // Fallback: no upcoming session. Preserve previous behavior based on current approved match. + const currentMatch = await this.prisma.matchedMentorMentee.findFirst({ + where: { menteeId, status: 'APPROVED' }, + include: { + mentor: { + select: { + id: true, + first_name: true, + last_name: true, + email: true, + profession: true, + company_name: true, + picture_upload_link: true, + }, + }, + }, + orderBy: { createdAt: 'desc' }, + }); + + const sessionsAttended = await this.prisma.mentorshipSessions.count({ + where: { menteeId, dateEnd: { lt: new Date() } }, + }); + + const firstMatch = await this.prisma.matchedMentorMentee.findFirst({ + where: { menteeId }, + orderBy: { createdAt: 'asc' }, + select: { createdAt: true }, + }); + + const mentor = currentMatch + ? { + id: currentMatch.mentor.id, + firstName: currentMatch.mentor.first_name, + lastName: currentMatch.mentor.last_name, + email: currentMatch.mentor.email, + profession: currentMatch.mentor.profession ?? undefined, + company: currentMatch.mentor.company_name ?? undefined, + expertise: currentMatch.mentor.profession ?? undefined, + avatarUrl: currentMatch.mentor.picture_upload_link ?? undefined, + } + : null; + + return { + mentor, + mentorshipStarted: firstMatch?.createdAt?.toISOString() ?? null, + sessionsAttended, + nextSession: null, + nextSessionLink: null, + } as any; + } catch (error) { + throw new InternalServerErrorException( + `Error building mentee dashboard: ${error.message}`, + ); + } + } + + async getMyPastMentors(menteeId: number) { + try { + const matches = await this.prisma.matchedMentorMentee.findMany({ + where: { menteeId }, + include: { + mentor: { + select: { + id: true, + first_name: true, + last_name: true, + email: true, + profession: true, + company_name: true, + picture_upload_link: true, + }, + }, + }, + orderBy: { createdAt: 'desc' }, + take: 20, + }); + + return matches.map((m) => ({ + id: m.mentor.id, + firstName: m.mentor.first_name, + lastName: m.mentor.last_name, + profession: m.mentor.profession ?? 'Not specified', + company: m.mentor.company_name ?? undefined, + expertise: m.mentor.profession ?? undefined, + email: m.mentor.email, + avatarUrl: m.mentor.picture_upload_link ?? undefined, + })); + } catch (error) { + throw new InternalServerErrorException( + `Error getting past mentors: ${error.message}`, + ); + } + } + + async getMyNotes(menteeId: number) { + try { + // Get recent sessions with mentee notes + const sessions = await this.prisma.mentorshipSessions.findMany({ + where: { menteeId }, + include: { + menteeNotes: true, + }, + orderBy: { dateEnd: 'desc' }, + take: 50, + }); + + // Flatten notes + const notes = sessions.flatMap((s, idx) => + s.menteeNotes.map((n, noteIdx) => ({ + title: `Session ${idx + 1}${noteIdx > 0 ? ` - Note ${noteIdx + 1}` : ''}`, + content: n.notes, + date: s.dateEnd?.toISOString() ?? s.dateStart?.toISOString(), + })), + ); + + return notes; + } catch (error) { + throw new InternalServerErrorException( + `Error getting mentee notes: ${error.message}`, + ); + } + } + + async getMyUpcomingSession(menteeId: number) { + try { + const session = await this.prisma.mentorshipSessions.findFirst({ + where: { menteeId, dateStart: { gt: new Date() } }, + orderBy: { dateStart: 'asc' }, + include: { + mentor: { + select: { + first_name: true, + last_name: true, + email: true, + picture_upload_link: true, + profession: true, + }, + }, + }, + }); + + if (!session) return null; + + return { + id: session.id, + dateStart: session.dateStart.toISOString(), + dateEnd: session.dateEnd.toISOString(), + link: session.link ?? '', + mentor: { + firstName: session.mentor.first_name, + lastName: session.mentor.last_name, + email: session.mentor.email, + avatarUrl: session.mentor.picture_upload_link ?? undefined, + profession: session.mentor.profession ?? undefined, + }, + }; + } catch (error) { + throw new InternalServerErrorException( + `Error getting upcoming session: ${error.message}`, + ); + } + } + remove(id: number) { // TO-DO: REMOVE MENTEE INFORMATION? OR CHANGE STATUS TO "REJECTED" OR "DELETED" TO KEEP THE INFORMATION? return `This action removes a #${id} mentee`; diff --git a/packages/backend/src/mentor/entities/mentor.entity.ts b/packages/backend/src/mentor/entities/mentor.entity.ts index 7576c0c..047374a 100644 --- a/packages/backend/src/mentor/entities/mentor.entity.ts +++ b/packages/backend/src/mentor/entities/mentor.entity.ts @@ -136,6 +136,10 @@ export class MyMentees { @IsNumber() id?: number; + @ApiProperty({ description: 'User ID of the mentee', example: '25' }) + @IsNumber() + menteeUserId?: number; + @ApiProperty({ description: 'Full name of the mentee', example: 'Jane Smith', @@ -158,6 +162,15 @@ export class MyMentees { @IsString() profession?: string; + @ApiProperty({ + description: 'Resume path or URL from mentees table', + example: 'uploads/resumes/abc123.pdf', + required: false, + }) + @IsOptional() + @IsString() + resume?: string; + @ApiProperty({ description: 'Status of the matching', example: 'APPROVED', diff --git a/packages/backend/src/mentor/mentor.service.ts b/packages/backend/src/mentor/mentor.service.ts index 8e592da..e167245 100644 --- a/packages/backend/src/mentor/mentor.service.ts +++ b/packages/backend/src/mentor/mentor.service.ts @@ -632,10 +632,16 @@ export class MentorService { include: { mentee: { select: { + id: true, first_name: true, last_name: true, email: true, profession: true, + mentee: { + select: { + resume: true, + }, + }, }, }, }, @@ -647,10 +653,15 @@ export class MentorService { // Transform the data to match the MyMentees entity const transformedMentees = mentees.map((matching) => ({ id: matching.id, + menteeUserId: matching.mentee.id, mentee: `${matching.mentee.first_name} ${matching.mentee.last_name}`, email: matching.mentee.email, profession: matching.mentee.profession || 'Not specified', status: matching.status, + resume: + (Array.isArray(matching.mentee.mentee) && matching.mentee.mentee[0] + ? matching.mentee.mentee[0].resume + : undefined) || undefined, })); return transformedMentees; diff --git a/packages/backend/src/users/users.service.ts b/packages/backend/src/users/users.service.ts index 884ea2b..9e946c6 100644 --- a/packages/backend/src/users/users.service.ts +++ b/packages/backend/src/users/users.service.ts @@ -78,7 +78,6 @@ export class UsersService { // User Retrieval Methods async getUserById(userIdNumber: string): Promise { - console.log('| - - - - - - - > USER ID:', userIdNumber); const user = await this.prisma.users.findFirst({ where: { id: Number(userIdNumber), diff --git a/packages/frontend/src/features/mentorship/api/mentorship-api.ts b/packages/frontend/src/features/mentorship/api/mentorship-api.ts index 2846279..2e499f7 100644 --- a/packages/frontend/src/features/mentorship/api/mentorship-api.ts +++ b/packages/frontend/src/features/mentorship/api/mentorship-api.ts @@ -10,7 +10,13 @@ import { MyMentorDashboard, MentorStatistics, MentorUpcomingSessions, - MyMentees + MyMentees, + MenteeDashboard, + MenteeNote, + PastMentor, + MenteeUpcomingSession, + CreateMentorshipSessionDto, + MentorshipSession } from '../types'; export const mentorshipApi = { @@ -58,5 +64,42 @@ export const mentorshipApi = { getMyUpcomingSessions: () => apiMethods.get('/mentors/my-upcoming-sessions'), - getMyMentees: () => apiMethods.get('/mentors/my-mentees') + getMyMentees: () => apiMethods.get('/mentors/my-mentees'), + + // Create mentorship session (mentor only) + createMentorshipSession: (body: CreateMentorshipSessionDto) => + apiMethods.post( + '/mentorship-sessions/mentorship-session', + body + ), + + // Update matching status (mentor only) + updateMatchingStatus: (matchingId: number, status: string) => + apiMethods.patch(`/mentorship-sessions/matching/${matchingId}`, { status }), + + // Mentee dashboard endpoints + getMenteeDashboard: () => + apiMethods.get('/mentees/my-dashboard'), + + getMenteeNotes: () => apiMethods.get('/mentees/my-notes'), + + getMenteePastMentors: () => + apiMethods.get('/mentees/my-past-mentors'), + + getMenteeUpcomingSession: () => + apiMethods.get( + '/mentees/my-upcoming-session' + ), + // Mentee notes CRUD + createMenteeNote: (sessionId: number, note: string) => + apiMethods.post('/mentorship-sessions/mentee-notes', { + sessionId, + note + }), + + updateMenteeNote: (id: number, note: string) => + apiMethods.put(`/mentorship-sessions/mentee-notes/${id}`, { note }), + + deleteMenteeNote: (id: number) => + apiMethods.delete(`/mentorship-sessions/mentee-notes/${id}`) }; diff --git a/packages/frontend/src/features/mentorship/components/common/mentorship-form.tsx b/packages/frontend/src/features/mentorship/components/common/mentorship-form.tsx index dbf1cab..02b6c06 100644 --- a/packages/frontend/src/features/mentorship/components/common/mentorship-form.tsx +++ b/packages/frontend/src/features/mentorship/components/common/mentorship-form.tsx @@ -109,9 +109,7 @@ export const MentorshipForm = ({ title, description }: MentorshipFormProps) => { } if (data.interests) { - data.interests.forEach((interest) => { - formData.append('interests[]', String(interest)); - }); + formData.append('interests', JSON.stringify(data.interests)); } if (data.experience_details) { const experienceParam = isMentor ? 'experience_details' : 'reason'; diff --git a/packages/frontend/src/features/mentorship/components/common/modals/admin-mentee-modal.tsx b/packages/frontend/src/features/mentorship/components/common/modals/admin-mentee-modal.tsx index b2a0829..2fab6ad 100644 --- a/packages/frontend/src/features/mentorship/components/common/modals/admin-mentee-modal.tsx +++ b/packages/frontend/src/features/mentorship/components/common/modals/admin-mentee-modal.tsx @@ -17,6 +17,7 @@ import { DialogTrigger } from '@/shared/components/ui/dialog'; import { useState } from 'react'; +import { useRouter } from 'next/navigation'; import { PdfPreviewModal } from '@/shared/components/pdf/pdf-preview-modal'; import { Select, @@ -54,22 +55,27 @@ export const AdminMenteeModalCard = ({ ); const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); const { showAlert } = useAlertDialog(); + const router = useRouter(); const handleViewResume = () => { if (resume) { setIsResumeModalOpen(true); + } else { + showAlert({ + type: 'warning', + title: 'Resume not available', + description: 'This mentee has not uploaded a resume yet.' + }); } }; const handleStatusUpdate = async () => { - if (selectedStatus === status || !menteeApplicationId) return; + // Must change and have a valid matching id (data.id) + if (selectedStatus === status || !data?.id) return; setIsUpdatingStatus(true); try { - await mentorshipApi.updateMenteeStatus( - menteeApplicationId, - selectedStatus - ); + await mentorshipApi.updateMatchingStatus(data.id, selectedStatus); showAlert({ type: 'success', @@ -77,7 +83,6 @@ export const AdminMenteeModalCard = ({ description: `Mentee status has been updated to ${selectedStatus}.` }); - // Refresh the data onStatusUpdate?.(); setIsModalOpen(false); } catch (error) { @@ -182,7 +187,24 @@ export const AdminMenteeModalCard = ({
- @@ -190,7 +212,6 @@ export const AdminMenteeModalCard = ({ className="h-10 flex-1 gap-2 px-0" onClick={handleViewResume} variant="outline" - disabled={!resume} > View Resume @@ -236,14 +257,14 @@ export const AdminMenteeModalCard = ({
- {isApproved ? ( + {/* {isApproved ? ( - ) : null} + ) : null} */} diff --git a/packages/frontend/src/features/mentorship/components/common/modals/create-session-modal.tsx b/packages/frontend/src/features/mentorship/components/common/modals/create-session-modal.tsx new file mode 100644 index 0000000..4ac6029 --- /dev/null +++ b/packages/frontend/src/features/mentorship/components/common/modals/create-session-modal.tsx @@ -0,0 +1,210 @@ +import { Button } from '@/shared/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle +} from '@/shared/components/ui/dialog'; +import { ScrollArea } from '@/shared/components/ui/scroll-area'; +import { useState } from 'react'; +import { useMentorshipStore } from '../../../store'; +import { CreateMentorshipSessionDto } from '../../../types'; + +interface CreateSessionModalProps { + isOpen: boolean; + setOpen: (open: boolean) => void; +} + +export function CreateSessionModal({ + isOpen, + setOpen +}: CreateSessionModalProps) { + const { mentorMentees, createMentorshipSession, getMyUpcomingSessions } = + useMentorshipStore(); + + const approvedMentees = mentorMentees.filter((m) => m.status === 'APPROVED'); + + const [form, setForm] = useState<{ + menteeId: number | ''; + link: string; + dateStart: string; + dateEnd: string; + description: string; + }>({ menteeId: '', link: '', dateStart: '', dateEnd: '', description: '' }); + + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const handleChange = ( + e: React.ChangeEvent< + HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement + > + ) => { + const { name, value } = e.target; + setForm((prev) => ({ + ...prev, + [name]: name === 'menteeId' ? (value === '' ? '' : Number(value)) : value + })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (!form.menteeId || !form.link || !form.dateStart || !form.dateEnd) { + setError('Please fill all required fields.'); + return; + } + + // Ensure selected mentee has an APPROVED matching + // const isApproved = approvedMentees.some((m) => m.id === form.menteeId); + // if (!isApproved) { + // setError('You can only schedule sessions with APPROVED mentees.'); + // return; + // } + + const payload: CreateMentorshipSessionDto = { + menteeId: Number(form.menteeId), + link: form.link, + dateStart: new Date(form.dateStart).toISOString(), + dateEnd: new Date(form.dateEnd).toISOString(), + description: form.description || undefined + }; + + try { + setSubmitting(true); + await createMentorshipSession(payload); + // Optionally refresh upcoming sessions list + await getMyUpcomingSessions(); + setOpen(false); + setForm({ + menteeId: '', + link: '', + dateStart: '', + dateEnd: '', + description: '' + }); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create session'); + } finally { + setSubmitting(false); + } + }; + + return ( + + + + + Create Mentorship Session + + + Schedule a new mentorship session with a mentee. + + + + +
+
+ + + {approvedMentees.length === 0 && ( +

+ You currently have no approved mentees to schedule with. +

+ )} +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ +