From b333f287ed51d0c67e6c335480efe081dce70249 Mon Sep 17 00:00:00 2001
From: nfracchia-pw
Date: Sat, 9 Aug 2025 18:59:42 -0600
Subject: [PATCH 1/7] feat(mentorship): mentor dashboard with create session
functionality
---
.../backend/src/mentee/mentee.controller.ts | 56 +++++
packages/backend/src/mentee/mentee.service.ts | 171 ++++++++++++++
.../src/mentor/entities/mentor.entity.ts | 4 +
packages/backend/src/mentor/mentor.service.ts | 2 +
.../features/mentorship/api/mentorship-api.ts | 29 ++-
.../common/modals/create-session-modal.tsx | 210 ++++++++++++++++++
.../components/mentee-dashboard.tsx | 188 +++++-----------
.../components/mentor-dashboard.tsx | 11 +-
.../src/features/mentorship/store/index.ts | 102 ++++++++-
.../src/features/mentorship/types/index.ts | 108 ++++++++-
10 files changed, 741 insertions(+), 140 deletions(-)
create mode 100644 packages/frontend/src/features/mentorship/components/common/modals/create-session-modal.tsx
diff --git a/packages/backend/src/mentee/mentee.controller.ts b/packages/backend/src/mentee/mentee.controller.ts
index b537a4d..8fe6a62 100644
--- a/packages/backend/src/mentee/mentee.controller.ts
+++ b/packages/backend/src/mentee/mentee.controller.ts
@@ -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()
+ async getMyDashboard(@GetUser() user: JwtPayload) {
+ return await 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()
+ 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()
+ 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()
+ 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..2c9d065 100644
--- a/packages/backend/src/mentee/mentee.service.ts
+++ b/packages/backend/src/mentee/mentee.service.ts
@@ -356,6 +356,177 @@ export class MenteeService {
}
}
+ async getMyDashboard(menteeId: number) {
+ try {
+ // Current approved match (mentor)
+ const currentMatch = await this.prisma.matchedMentorMentee.findFirst({
+ where: { menteeId, status: 'APPROVED' },
+ include: {
+ mentor: {
+ select: {
+ first_name: true,
+ last_name: true,
+ email: true,
+ profession: true,
+ company_name: true,
+ picture_upload_link: true,
+ },
+ },
+ },
+ orderBy: { createdAt: 'desc' },
+ });
+
+ // Sessions attended
+ const sessionsAttended = await this.prisma.mentorshipSessions.count({
+ where: { menteeId, dateEnd: { lt: new Date() } },
+ });
+
+ // First match date as mentorship started
+ const firstMatch = await this.prisma.matchedMentorMentee.findFirst({
+ where: { menteeId },
+ orderBy: { createdAt: 'asc' },
+ select: { createdAt: true },
+ });
+
+ // Next upcoming session
+ const nextSession = await this.prisma.mentorshipSessions.findFirst({
+ where: { menteeId, dateStart: { gt: new Date() } },
+ orderBy: { dateStart: 'asc' },
+ select: { dateStart: true },
+ });
+
+ const mentor = currentMatch
+ ? {
+ 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: nextSession?.dateStart?.toISOString() ?? null,
+ };
+ } 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..c5d67ab 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',
diff --git a/packages/backend/src/mentor/mentor.service.ts b/packages/backend/src/mentor/mentor.service.ts
index 8e592da..18dba52 100644
--- a/packages/backend/src/mentor/mentor.service.ts
+++ b/packages/backend/src/mentor/mentor.service.ts
@@ -632,6 +632,7 @@ export class MentorService {
include: {
mentee: {
select: {
+ id: true,
first_name: true,
last_name: true,
email: true,
@@ -647,6 +648,7 @@ 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',
diff --git a/packages/frontend/src/features/mentorship/api/mentorship-api.ts b/packages/frontend/src/features/mentorship/api/mentorship-api.ts
index 2846279..c219c3c 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,24 @@ 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
+ ),
+
+ // 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')
};
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 (
+
+ );
+}
diff --git a/packages/frontend/src/features/mentorship/components/mentee-dashboard.tsx b/packages/frontend/src/features/mentorship/components/mentee-dashboard.tsx
index f8785d1..9ee0ad4 100644
--- a/packages/frontend/src/features/mentorship/components/mentee-dashboard.tsx
+++ b/packages/frontend/src/features/mentorship/components/mentee-dashboard.tsx
@@ -13,138 +13,50 @@ import { MessageSquare, Calendar } from 'lucide-react';
import { NotesCard } from './common/notes-card';
import { ScrollArea } from '@/shared/components/ui/scroll-area';
import { NoteEditor } from './common/note-editor';
-import { useState } from 'react';
-
+import { useEffect, useMemo, useState } from 'react';
+import { useMentorshipStore } from '../store';
import { PastMentorsModal } from './common/modals/past-mentors-modal';
-const notes = [
- {
- title: 'Session 1',
- content:
- 'Discussed career goals and created a development plan. Focused on improving system design skills and architectural patterns.',
- date: new Date()
- },
- {
- title: 'Session 2',
- content:
- 'Reviewed recent project challenges. Mentor suggested implementing design patterns and improving code documentation practices.',
- date: new Date()
- },
- {
- title: 'Session 3',
- content:
- 'Deep dive into microservices architecture. Explored service communication patterns and discussed eventual consistency.',
- date: new Date()
- },
- {
- title: 'Session 4',
- content:
- 'Code review session focusing on performance optimization. Identified areas for improvement in database query patterns.',
- date: new Date()
- },
- {
- title: 'Session 5',
- content:
- 'Worked on leadership skills and team communication. Discussed strategies for leading technical discussions effectively.',
- date: new Date()
- },
- {
- title: 'Session 6',
- content:
- 'Explored advanced TypeScript patterns. Covered generic types, utility types, and best practices for type-safe development.',
- date: new Date()
- },
- {
- title: 'Session 7',
- content:
- 'Discussion about career progression and industry trends. Mapped out learning path for cloud-native development skills.',
- date: new Date()
- },
- {
- title: 'Session 8',
- content:
- 'Technical deep dive into React performance optimization. Covered memo, useMemo, useCallback, and component structure.',
- date: new Date()
- },
- {
- title: 'Session 9',
- content:
- 'Reviewed system design case study. Created architecture diagrams and discussed scalability considerations in detail.',
- date: new Date()
- },
- {
- title: 'Session 10',
- content:
- 'Focused on soft skills development. Discussed techniques for effective communication in cross-functional team settings.',
- date: new Date()
- }
-];
-
-const pastMentors = [
- {
- id: 1,
- firstName: 'John',
- lastName: 'Doe',
- profession: 'Software Engineer',
- company: 'Microsoft',
- expertise: 'Software Engineer',
- email: 'john.doe@example.com',
- avatarUrl: 'https://github.com/shadcn.png'
- },
- {
- id: 2,
- firstName: 'Jane',
- lastName: 'Doe',
- profession: 'Software Engineer',
- company: 'Microsoft',
- expertise: 'Software Engineer',
- email: 'jane.doe@example.com',
- avatarUrl: 'https://github.com/shadcn.png'
- },
- {
- id: 3,
- firstName: 'John',
- lastName: 'Doe',
- profession: 'Software Engineer',
- company: 'Microsoft',
- expertise: 'Software Engineer',
- email: 'john.doe@example.com',
- avatarUrl: 'https://github.com/shadcn.png'
- },
- {
- id: 4,
- firstName: 'John',
- lastName: 'Doe',
- profession: 'Software Engineer',
- company: 'Microsoft',
- expertise: 'Software Engineer',
- email: 'john.doe@example.com',
- avatarUrl: 'https://github.com/shadcn.png'
- },
- {
- id: 5,
- firstName: 'John',
- lastName: 'Doe',
- profession: 'Software Engineer',
- company: 'Microsoft',
- expertise: 'Software Engineer',
- email: 'john.doe@example.com',
- avatarUrl: 'https://github.com/shadcn.png'
- }
-];
-
const MenteeDashboard = () => {
const { user } = useUserStore();
const [isPastMentorsOpen, setIsPastMentorsOpen] = useState(false);
-
- const mentor = {
- fullName: user?.firstName + ' ' + user?.lastName,
- profession: user?.profession,
- company: 'Microsoft',
- expertise: 'Software Engineer',
- email: user?.email,
- avatarUrl: 'https://github.com/shadcn.png'
- };
+ const {
+ menteeDashboard,
+ menteeNotes,
+ pastMentors,
+ getMenteeDashboard,
+ getMenteeNotes,
+ getMenteePastMentors
+ } = useMentorshipStore();
+
+ useEffect(() => {
+ // Load mentee dashboard data on mount
+ getMenteeDashboard().catch(() => {});
+ getMenteeNotes().catch(() => {});
+ getMenteePastMentors().catch(() => {});
+ }, [getMenteeDashboard, getMenteeNotes, getMenteePastMentors]);
+
+ const mentor = useMemo(() => {
+ const m = menteeDashboard?.mentor;
+ if (m) {
+ return {
+ fullName: `${m.firstName} ${m.lastName}`,
+ profession: m.profession,
+ company: m.company,
+ expertise: m.expertise,
+ email: m.email,
+ avatarUrl: m.avatarUrl
+ };
+ }
+ return {
+ fullName: `${user?.firstName ?? ''} ${user?.lastName ?? ''}`,
+ profession: user?.profession,
+ company: undefined,
+ expertise: undefined,
+ email: user?.email,
+ avatarUrl: undefined
+ };
+ }, [menteeDashboard, user]);
return (
@@ -227,17 +139,27 @@ const MenteeDashboard = () => {
@@ -254,12 +176,12 @@ const MenteeDashboard = () => {
- {notes.map((note) => (
+ {menteeNotes.map((note) => (
{}}
/>
))}
diff --git a/packages/frontend/src/features/mentorship/components/mentor-dashboard.tsx b/packages/frontend/src/features/mentorship/components/mentor-dashboard.tsx
index db7b4a3..55c1b2c 100644
--- a/packages/frontend/src/features/mentorship/components/mentor-dashboard.tsx
+++ b/packages/frontend/src/features/mentorship/components/mentor-dashboard.tsx
@@ -11,6 +11,7 @@ import { menteesColumns } from './table/mentees-column';
import MentorCard from './common/mentor-card';
import { useState, useEffect } from 'react';
import { MentorModal } from './common/modals/mentor-modal';
+import { CreateSessionModal } from './common/modals/create-session-modal';
import { useMentorshipStore } from '../store';
import { MenteeStatus } from '../types';
@@ -41,6 +42,7 @@ const MentorDashboard = () => {
} = useMentorshipStore();
const [isModalOpen, setIsModalOpen] = useState(false);
+ const [isCreateSessionOpen, setIsCreateSessionOpen] = useState(false);
const [selectedProfile, setSelectedProfile] = useState(
null
);
@@ -140,7 +142,10 @@ const MentorDashboard = () => {
Hey, {user?.firstName}!👋
-
diff --git a/packages/frontend/src/features/mentorship/store/index.ts b/packages/frontend/src/features/mentorship/store/index.ts
index b73de78..9ad8f28 100644
--- a/packages/frontend/src/features/mentorship/store/index.ts
+++ b/packages/frontend/src/features/mentorship/store/index.ts
@@ -12,7 +12,13 @@ import {
MyMentorDashboard,
MentorStatistics,
MentorUpcomingSessions,
- MyMentees
+ MyMentees,
+ MenteeDashboard,
+ MenteeNote,
+ PastMentor,
+ MenteeUpcomingSession,
+ CreateMentorshipSessionDto,
+ MentorshipSession
} from '../types';
interface MentorshipState {
@@ -31,9 +37,17 @@ interface MentorshipState {
mentorStatistics: MentorStatistics | null;
mentorUpcomingSessions: MentorUpcomingSessions[];
mentorMentees: MyMentees[];
+ // Mentee dashboard state
+ menteeDashboard: MenteeDashboard | null;
+ menteeNotes: MenteeNote[];
+ pastMentors: PastMentor[];
+ menteeUpcomingSession: MenteeUpcomingSession | null;
// Actions
createMentor: (mentorData: FormData) => Promise;
createMentee: (menteeData: FormData) => Promise;
+ createMentorshipSession: (
+ payload: CreateMentorshipSessionDto
+ ) => Promise;
fetchInterests: () => Promise;
getMentor: (mentorId: number) => Promise;
getPendingApplications: () => Promise;
@@ -45,6 +59,11 @@ interface MentorshipState {
getMyStatistics: () => Promise;
getMyUpcomingSessions: () => Promise;
getMyMentees: () => Promise;
+ // Mentee dashboard actions
+ getMenteeDashboard: () => Promise;
+ getMenteeNotes: () => Promise;
+ getMenteePastMentors: () => Promise;
+ getMenteeUpcomingSession: () => Promise;
}
export const useMentorshipStore = create()(
@@ -59,6 +78,11 @@ export const useMentorshipStore = create()(
mentorStatistics: null,
mentorUpcomingSessions: [],
mentorMentees: [],
+ // Mentee
+ menteeDashboard: null,
+ menteeNotes: [],
+ pastMentors: [],
+ menteeUpcomingSession: null,
createMentor: async (mentorData) => {
set({ isLoading: true, error: null });
@@ -94,6 +118,21 @@ export const useMentorshipStore = create()(
}
},
+ createMentorshipSession: async (payload) => {
+ set({ isLoading: true, error: null });
+ try {
+ const response = await mentorshipApi.createMentorshipSession(payload);
+ set({ isLoading: false });
+ return response.data;
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : 'An error occurred',
+ isLoading: false
+ });
+ throw error;
+ }
+ },
+
getMentor: async (mentorId: number) => {
const response = await mentorshipApi.getMentor(mentorId);
set({ mentor: response.data });
@@ -197,6 +236,67 @@ export const useMentorshipStore = create()(
}
},
+ // Mentee dashboard actions
+ getMenteeDashboard: async () => {
+ set({ isLoading: true, error: null });
+ try {
+ const response = await mentorshipApi.getMenteeDashboard();
+ set({ menteeDashboard: response.data, isLoading: false });
+ return response.data;
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : 'An error occurred',
+ isLoading: false
+ });
+ throw error;
+ }
+ },
+
+ getMenteeNotes: async () => {
+ set({ isLoading: true, error: null });
+ try {
+ const response = await mentorshipApi.getMenteeNotes();
+ set({ menteeNotes: response.data, isLoading: false });
+ return response.data;
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : 'An error occurred',
+ isLoading: false
+ });
+ throw error;
+ }
+ },
+
+ getMenteePastMentors: async () => {
+ set({ isLoading: true, error: null });
+ try {
+ const response = await mentorshipApi.getMenteePastMentors();
+ set({ pastMentors: response.data, isLoading: false });
+ return response.data;
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : 'An error occurred',
+ isLoading: false
+ });
+ throw error;
+ }
+ },
+
+ getMenteeUpcomingSession: async () => {
+ set({ isLoading: true, error: null });
+ try {
+ const response = await mentorshipApi.getMenteeUpcomingSession();
+ set({ menteeUpcomingSession: response.data, isLoading: false });
+ return response.data;
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : 'An error occurred',
+ isLoading: false
+ });
+ throw error;
+ }
+ },
+
fetchInterests: async () => {
set({ isLoading: true, error: null });
try {
diff --git a/packages/frontend/src/features/mentorship/types/index.ts b/packages/frontend/src/features/mentorship/types/index.ts
index fec6b45..e81beb3 100644
--- a/packages/frontend/src/features/mentorship/types/index.ts
+++ b/packages/frontend/src/features/mentorship/types/index.ts
@@ -66,7 +66,7 @@ export type RatingsGroup = {
export interface MentorshipAdmin {
id: number;
- mentorApplicationId?: number; // Add this field
+ mentorApplicationId?: number;
identity: {
avatar: string;
firstName: string;
@@ -89,7 +89,7 @@ export interface MentorshipAdmin {
export type Mentee = {
id: number;
- menteeApplicationId?: number; // Add this field
+ menteeApplicationId?: number;
identity: {
avatar: string;
firstName: string;
@@ -102,7 +102,7 @@ export type Mentee = {
email: string;
reason: string;
experience?: string;
- resume?: string; // Add this field
+ resume?: string;
};
// New types for mentor dashboard
@@ -126,6 +126,7 @@ export interface MentorUpcomingSessions {
export interface MyMentees {
id: number;
+ menteeUserId?: number;
avatar?: string;
mentee: string;
email: string;
@@ -138,3 +139,104 @@ export interface MyMentorDashboard {
upcomingSessions: MentorUpcomingSessions[];
mentees: MyMentees[];
}
+
+export interface MenteeNote {
+ title: string;
+ content: string;
+ date: string;
+}
+
+export interface PastMentor {
+ id: number;
+ firstName: string;
+ lastName: string;
+ profession?: string;
+ company?: string;
+ expertise?: string;
+ email: string;
+ avatarUrl?: string;
+}
+
+export interface MenteeUpcomingSession {
+ id: number;
+ dateStart: string;
+ dateEnd: string;
+ link: string;
+ mentor: {
+ firstName: string;
+ lastName: string;
+ email: string;
+ avatarUrl?: string;
+ profession?: string;
+ };
+}
+
+export interface MenteeDashboard {
+ mentor: {
+ firstName: string;
+ lastName: string;
+ email: string;
+ profession?: string;
+ company?: string;
+ expertise?: string;
+ avatarUrl?: string;
+ } | null;
+ mentorshipStarted: string | null;
+ sessionsAttended: number;
+ nextSession: string | null;
+}
+
+// DTO for creating mentorship session (MENTOR only)
+export interface CreateMentorshipSessionDto {
+ menteeId: number;
+ link: string;
+ dateStart: string; // ISO string
+ dateEnd: string; // ISO string
+ description?: string;
+}
+
+// Response types for mentorship sessions
+export interface MentorshipSessionDescription {
+ id: number;
+ sessionId: number;
+ description: string;
+}
+
+export interface MentorshipSessionMenteeNotes {
+ id: number;
+ sessionId: number;
+ notes: string;
+}
+
+export interface MentorshipSessionRating {
+ id: number;
+ sessionId: number;
+ rate: number; // 1-5
+ comment?: string;
+ createdAt: string; // ISO string
+ session?: MentorshipSession;
+}
+
+export interface MentorshipSessionUser {
+ id: number;
+ firstName: string;
+ lastName: string;
+ email: string;
+}
+
+export interface MentorshipSession {
+ id: number;
+ mentorId: number;
+ menteeId: number;
+ link?: string;
+ dateStart: string; // ISO string
+ dateEnd: string; // ISO string
+ createdAt: string; // ISO string
+ updatedAt: string; // ISO string
+ mentor?: MentorshipSessionUser;
+ mentee?: MentorshipSessionUser;
+ description?: MentorshipSessionDescription;
+ menteeNotes?: MentorshipSessionMenteeNotes;
+ mentorRating?: MentorshipSessionRating;
+ menteeRating?: MentorshipSessionRating;
+}
From 65cd191a8e5cd42ceb652cd0d0d8eb1988057f40 Mon Sep 17 00:00:00 2001
From: nfracchia-pw
Date: Sat, 9 Aug 2025 19:54:32 -0600
Subject: [PATCH 2/7] fix(mentee-modal): fixed functionality to start
conversation and view resume
---
.../src/mentor/entities/mentor.entity.ts | 9 ++++++
packages/backend/src/mentor/mentor.service.ts | 9 ++++++
.../common/modals/admin-mentee-modal.tsx | 28 +++++++++++++++--
.../components/mentor-dashboard.tsx | 30 +++++++++----------
.../src/features/mentorship/types/index.ts | 3 ++
5 files changed, 62 insertions(+), 17 deletions(-)
diff --git a/packages/backend/src/mentor/entities/mentor.entity.ts b/packages/backend/src/mentor/entities/mentor.entity.ts
index c5d67ab..047374a 100644
--- a/packages/backend/src/mentor/entities/mentor.entity.ts
+++ b/packages/backend/src/mentor/entities/mentor.entity.ts
@@ -162,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 18dba52..e167245 100644
--- a/packages/backend/src/mentor/mentor.service.ts
+++ b/packages/backend/src/mentor/mentor.service.ts
@@ -637,6 +637,11 @@ export class MentorService {
last_name: true,
email: true,
profession: true,
+ mentee: {
+ select: {
+ resume: true,
+ },
+ },
},
},
},
@@ -653,6 +658,10 @@ export class MentorService {
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/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 f914882..2983df1 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,10 +55,17 @@ 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.'
+ });
}
};
@@ -182,7 +190,24 @@ export const AdminMenteeModalCard = ({
-
+ {
+ const userId = data.menteeUserId;
+ if (userId) {
+ setIsModalOpen(false);
+ router.push(`/messages/${userId}`);
+ } else {
+ showAlert({
+ type: 'warning',
+ title: 'Messaging unavailable',
+ description:
+ 'We could not determine the mentee user account to start a chat.'
+ });
+ }
+ }}
+ >
Message
@@ -190,7 +215,6 @@ export const AdminMenteeModalCard = ({
className="h-10 flex-1 gap-2 px-0"
onClick={handleViewResume}
variant="outline"
- disabled={!resume}
>
View Resume
diff --git a/packages/frontend/src/features/mentorship/components/mentor-dashboard.tsx b/packages/frontend/src/features/mentorship/components/mentor-dashboard.tsx
index 55c1b2c..2c5918f 100644
--- a/packages/frontend/src/features/mentorship/components/mentor-dashboard.tsx
+++ b/packages/frontend/src/features/mentorship/components/mentor-dashboard.tsx
@@ -7,7 +7,7 @@ import { MentorshipSection } from './common/mentorship-section';
import { MentorshipIcons } from './icons';
import { DataTable } from './table/data-table';
import { sessionsColumns } from './table/sessions-columns';
-import { menteesColumns } from './table/mentees-column';
+import { createMenteeColumns } from './table/mentees-column';
import MentorCard from './common/mentor-card';
import { useState, useEffect } from 'react';
import { MentorModal } from './common/modals/mentor-modal';
@@ -102,7 +102,8 @@ const MentorDashboard = () => {
// Transform mentees data for the table
const menteesTableData = mentorMentees.map((mentee, index) => ({
id: mentee.id,
- menteeApplicationId: index,
+ menteeApplicationId: mentee.menteeApplicationId,
+ menteeUserId: mentee.menteeUserId,
identity: {
avatar: mentee.avatar || '/profile/default-avatar.png',
firstName: mentee.mentee.split(' ')[0] || '',
@@ -115,7 +116,7 @@ const MentorDashboard = () => {
email: mentee.email,
reason: '',
experience: '',
- resume: ''
+ resume: mentee.resume || ''
}));
if (isLoading) {
@@ -238,19 +239,18 @@ const MentorDashboard = () => {
{
+ try {
+ await getMyMentees();
+ await getMyStatistics();
+ } catch (e) {
+ console.error(
+ 'Failed to refresh mentees/stats after status update',
+ e
+ );
+ }
+ })}
data={menteesTableData}
- onRowClick={(rowData) => {
- setSelectedProfile({
- firstName: rowData.identity.firstName,
- lastName: rowData.identity.lastName,
- profession: rowData.profession,
- email: rowData.identity.email,
- avatarUrl: rowData.identity.avatar,
- isMentor: false
- });
- setIsModalOpen(true);
- }}
/>
diff --git a/packages/frontend/src/features/mentorship/types/index.ts b/packages/frontend/src/features/mentorship/types/index.ts
index e81beb3..e83b5a1 100644
--- a/packages/frontend/src/features/mentorship/types/index.ts
+++ b/packages/frontend/src/features/mentorship/types/index.ts
@@ -90,6 +90,7 @@ export interface MentorshipAdmin {
export type Mentee = {
id: number;
menteeApplicationId?: number;
+ menteeUserId?: number;
identity: {
avatar: string;
firstName: string;
@@ -132,6 +133,8 @@ export interface MyMentees {
email: string;
profession?: string;
status: string;
+ resume?: string;
+ menteeApplicationId?: number;
}
export interface MyMentorDashboard {
From 4fbb6b05ec88fb6b60f34b8ef8ebe2f053dc3792 Mon Sep 17 00:00:00 2001
From: nfracchia-pw
Date: Sat, 9 Aug 2025 20:03:20 -0600
Subject: [PATCH 3/7] fix(mentee-modal): fixed functionality to update status
---
.../src/features/mentorship/api/mentorship-api.ts | 4 ++++
.../components/common/modals/admin-mentee-modal.tsx | 9 +++------
2 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/packages/frontend/src/features/mentorship/api/mentorship-api.ts b/packages/frontend/src/features/mentorship/api/mentorship-api.ts
index c219c3c..3471362 100644
--- a/packages/frontend/src/features/mentorship/api/mentorship-api.ts
+++ b/packages/frontend/src/features/mentorship/api/mentorship-api.ts
@@ -73,6 +73,10 @@ export const mentorshipApi = {
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'),
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 2983df1..1655ded 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
@@ -70,14 +70,12 @@ export const AdminMenteeModalCard = ({
};
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',
@@ -85,7 +83,6 @@ export const AdminMenteeModalCard = ({
description: `Mentee status has been updated to ${selectedStatus}.`
});
- // Refresh the data
onStatusUpdate?.();
setIsModalOpen(false);
} catch (error) {
From f9f77f4a80d78d19d6068bd944278b07dfa0b186 Mon Sep 17 00:00:00 2001
From: nfracchia-pw
Date: Sat, 9 Aug 2025 20:12:48 -0600
Subject: [PATCH 4/7] fix(mentee-modal): removed matching button from mentee
modal in mentors dashboard
---
.../components/common/modals/admin-mentee-modal.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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 1655ded..8ac2636 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
@@ -257,14 +257,14 @@ export const AdminMenteeModalCard = ({
- {isApproved ? (
+ {/* {isApproved ? (
console.log('match with mentor')}
>
Match with a Mentor
- ) : null}
+ ) : null} */}
From 4aaa82aa5f8c82df7755fcf08e645727ffad3577 Mon Sep 17 00:00:00 2001
From: nfracchia-pw
Date: Sun, 10 Aug 2025 12:44:23 -0600
Subject: [PATCH 5/7] chore(mentor-dashboard): removed button view profile
---
.../src/features/mentorship/components/mentor-dashboard.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/frontend/src/features/mentorship/components/mentor-dashboard.tsx b/packages/frontend/src/features/mentorship/components/mentor-dashboard.tsx
index 2c5918f..2eaee5a 100644
--- a/packages/frontend/src/features/mentorship/components/mentor-dashboard.tsx
+++ b/packages/frontend/src/features/mentorship/components/mentor-dashboard.tsx
@@ -149,13 +149,13 @@ const MentorDashboard = () => {
>
Create a Session
-
View Profile
-
+ */}
setIsModalOpen(false)}
From 2d4a8eb9fb1c462917f5bfed4e17794b7166da30 Mon Sep 17 00:00:00 2001
From: nfracchia-pw
Date: Fri, 12 Sep 2025 19:58:32 -0600
Subject: [PATCH 6/7] feat(dashboards): integration with api
---
.../backend/src/mentee/mentee.controller.ts | 39 ++++---
packages/backend/src/mentee/mentee.service.ts | 103 ++++++++++++++++--
packages/backend/src/users/users.service.ts | 1 -
.../src/features/mentorship/store/index.ts | 2 +
.../features/user-profile/hooks/useRole.tsx | 9 +-
5 files changed, 120 insertions(+), 34 deletions(-)
diff --git a/packages/backend/src/mentee/mentee.controller.ts b/packages/backend/src/mentee/mentee.controller.ts
index 8fe6a62..45da0a6 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,
@@ -181,17 +181,16 @@ export class MenteeController {
}
@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()
- async getMyDashboard(@GetUser() user: JwtPayload) {
- return await this.menteeService.getMyDashboard(user.sub);
+ // @Roles('MENTEE')
+ // @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: MENTEE'})
+ // @ApiBearerAuth('JWT')
+ getMyDashboard() {
+ console.log('MENTEE DASHBOARD');
+ return 'MENTEE DASHBOARD';
+ // console.log('USER ACCESSING: ', user);
+ // return this.menteeService.getMyDashboard(user.sub);
}
@Get('my-past-mentors')
@@ -203,7 +202,7 @@ export class MenteeController {
description:
'Returns mentors previously matched with the mentee. REQUIRED ROLES: USER | MENTEE | ADMIN',
})
- @ApiBearerAuth()
+ @ApiBearerAuth('JWT')
async getMyPastMentors(@GetUser() user: JwtPayload) {
return await this.menteeService.getMyPastMentors(user.sub);
}
@@ -217,7 +216,7 @@ export class MenteeController {
description:
'Returns notes the mentee took for mentorship sessions. REQUIRED ROLES: USER | MENTEE | ADMIN',
})
- @ApiBearerAuth()
+ @ApiBearerAuth('JWT')
async getMyNotes(@GetUser() user: JwtPayload) {
return await this.menteeService.getMyNotes(user.sub);
}
@@ -231,7 +230,7 @@ export class MenteeController {
description:
'Returns the next upcoming session for the mentee. REQUIRED ROLES: USER | MENTEE | ADMIN',
})
- @ApiBearerAuth()
+ @ApiBearerAuth('JWT')
async getMyUpcomingSession(@GetUser() user: JwtPayload) {
return await this.menteeService.getMyUpcomingSession(user.sub);
}
diff --git a/packages/backend/src/mentee/mentee.service.ts b/packages/backend/src/mentee/mentee.service.ts
index 2c9d065..3361c2e 100644
--- a/packages/backend/src/mentee/mentee.service.ts
+++ b/packages/backend/src/mentee/mentee.service.ts
@@ -357,8 +357,95 @@ export class MenteeService {
}
async getMyDashboard(menteeId: number) {
+ console.log('MENTEE ACCESSING SERVICE - MY DASHBOARD: ', menteeId);
try {
- // Current approved match (mentor)
+ // 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: {
+ first_name: true,
+ last_name: true,
+ email: true,
+ profession: true,
+ company_name: true,
+ picture_upload_link: true,
+ },
+ });
+
+ const mentor = mentorUser
+ ? {
+ 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: {
@@ -376,25 +463,16 @@ export class MenteeService {
orderBy: { createdAt: 'desc' },
});
- // Sessions attended
const sessionsAttended = await this.prisma.mentorshipSessions.count({
where: { menteeId, dateEnd: { lt: new Date() } },
});
- // First match date as mentorship started
const firstMatch = await this.prisma.matchedMentorMentee.findFirst({
where: { menteeId },
orderBy: { createdAt: 'asc' },
select: { createdAt: true },
});
- // Next upcoming session
- const nextSession = await this.prisma.mentorshipSessions.findFirst({
- where: { menteeId, dateStart: { gt: new Date() } },
- orderBy: { dateStart: 'asc' },
- select: { dateStart: true },
- });
-
const mentor = currentMatch
? {
firstName: currentMatch.mentor.first_name,
@@ -411,8 +489,9 @@ export class MenteeService {
mentor,
mentorshipStarted: firstMatch?.createdAt?.toISOString() ?? null,
sessionsAttended,
- nextSession: nextSession?.dateStart?.toISOString() ?? null,
- };
+ nextSession: null,
+ nextSessionLink: null,
+ } as any;
} catch (error) {
throw new InternalServerErrorException(
`Error building mentee dashboard: ${error.message}`,
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/store/index.ts b/packages/frontend/src/features/mentorship/store/index.ts
index 9ad8f28..98d485b 100644
--- a/packages/frontend/src/features/mentorship/store/index.ts
+++ b/packages/frontend/src/features/mentorship/store/index.ts
@@ -238,9 +238,11 @@ export const useMentorshipStore = create()(
// Mentee dashboard actions
getMenteeDashboard: async () => {
+ console.log('MENTEE DASHBOARD GET');
set({ isLoading: true, error: null });
try {
const response = await mentorshipApi.getMenteeDashboard();
+ console.log('MENTEE DASHBOARD', response.data);
set({ menteeDashboard: response.data, isLoading: false });
return response.data;
} catch (error) {
diff --git a/packages/frontend/src/features/user-profile/hooks/useRole.tsx b/packages/frontend/src/features/user-profile/hooks/useRole.tsx
index d29861b..3b6ef1f 100644
--- a/packages/frontend/src/features/user-profile/hooks/useRole.tsx
+++ b/packages/frontend/src/features/user-profile/hooks/useRole.tsx
@@ -102,8 +102,15 @@ export const useRole = () => {
if (requiredRole === 'ADMIN') return role === 'ADMIN';
if (requiredRole === 'MENTOR')
return role === 'ADMIN' || role === 'MENTOR';
+ if (requiredRole === 'MENTEE')
+ return role === 'ADMIN' || role === 'MENTOR' || role === 'MENTEE';
if (requiredRole === 'USER')
- return role === 'ADMIN' || role === 'MENTOR' || role === 'USER';
+ return (
+ role === 'ADMIN' ||
+ role === 'MENTOR' ||
+ role === 'MENTEE' ||
+ role === 'USER'
+ );
return false;
},
From f5d634474343d75402eaabc98839e7745a691bc4 Mon Sep 17 00:00:00 2001
From: nfracchia-pw
Date: Fri, 19 Sep 2025 19:46:43 -0600
Subject: [PATCH 7/7] feat(mentee): missed mentee dashboard integration
---
.../backend/src/auth/guards/roles.guard.ts | 17 ++++-
.../backend/src/mentee/mentee.controller.ts | 21 ++++---
packages/backend/src/mentee/mentee.service.ts | 4 ++
.../features/mentorship/api/mentorship-api.ts | 16 ++++-
.../components/common/mentorship-form.tsx | 4 +-
.../components/mentee-dashboard.tsx | 63 +++++++++++++++----
.../src/features/mentorship/store/index.ts | 54 ++++++++++++++++
.../src/features/mentorship/types/index.ts | 2 +
8 files changed, 153 insertions(+), 28 deletions(-)
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 45da0a6..0b312a1 100644
--- a/packages/backend/src/mentee/mentee.controller.ts
+++ b/packages/backend/src/mentee/mentee.controller.ts
@@ -181,16 +181,17 @@ export class MenteeController {
}
@Get('my-dashboard')
- // @Roles('MENTEE')
- // @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: MENTEE'})
- // @ApiBearerAuth('JWT')
- getMyDashboard() {
- console.log('MENTEE DASHBOARD');
- return 'MENTEE DASHBOARD';
- // console.log('USER ACCESSING: ', user);
- // return this.menteeService.getMyDashboard(user.sub);
+ @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')
diff --git a/packages/backend/src/mentee/mentee.service.ts b/packages/backend/src/mentee/mentee.service.ts
index 3361c2e..0d6c15f 100644
--- a/packages/backend/src/mentee/mentee.service.ts
+++ b/packages/backend/src/mentee/mentee.service.ts
@@ -408,6 +408,7 @@ export class MenteeService {
: await this.prisma.users.findUnique({
where: { id: mentorId! },
select: {
+ id: true,
first_name: true,
last_name: true,
email: true,
@@ -419,6 +420,7 @@ export class MenteeService {
const mentor = mentorUser
? {
+ id: (mentorUser as any).id,
firstName: mentorUser.first_name,
lastName: mentorUser.last_name,
email: mentorUser.email,
@@ -451,6 +453,7 @@ export class MenteeService {
include: {
mentor: {
select: {
+ id: true,
first_name: true,
last_name: true,
email: true,
@@ -475,6 +478,7 @@ export class MenteeService {
const mentor = currentMatch
? {
+ id: currentMatch.mentor.id,
firstName: currentMatch.mentor.first_name,
lastName: currentMatch.mentor.last_name,
email: currentMatch.mentor.email,
diff --git a/packages/frontend/src/features/mentorship/api/mentorship-api.ts b/packages/frontend/src/features/mentorship/api/mentorship-api.ts
index 3471362..2e499f7 100644
--- a/packages/frontend/src/features/mentorship/api/mentorship-api.ts
+++ b/packages/frontend/src/features/mentorship/api/mentorship-api.ts
@@ -87,5 +87,19 @@ export const mentorshipApi = {
apiMethods.get('/mentees/my-past-mentors'),
getMenteeUpcomingSession: () =>
- apiMethods.get('/mentees/my-upcoming-session')
+ 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 addd078..7a84965 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/mentee-dashboard.tsx b/packages/frontend/src/features/mentorship/components/mentee-dashboard.tsx
index 9ee0ad4..124913d 100644
--- a/packages/frontend/src/features/mentorship/components/mentee-dashboard.tsx
+++ b/packages/frontend/src/features/mentorship/components/mentee-dashboard.tsx
@@ -24,9 +24,12 @@ const MenteeDashboard = () => {
menteeDashboard,
menteeNotes,
pastMentors,
+ menteeUpcomingSession,
getMenteeDashboard,
getMenteeNotes,
- getMenteePastMentors
+ getMenteePastMentors,
+ getMenteeUpcomingSession,
+ createMenteeNote
} = useMentorshipStore();
useEffect(() => {
@@ -34,7 +37,13 @@ const MenteeDashboard = () => {
getMenteeDashboard().catch(() => {});
getMenteeNotes().catch(() => {});
getMenteePastMentors().catch(() => {});
- }, [getMenteeDashboard, getMenteeNotes, getMenteePastMentors]);
+ getMenteeUpcomingSession().catch(() => {});
+ }, [
+ getMenteeDashboard,
+ getMenteeNotes,
+ getMenteePastMentors,
+ getMenteeUpcomingSession
+ ]);
const mentor = useMemo(() => {
const m = menteeDashboard?.mentor;
@@ -97,6 +106,14 @@ const MenteeDashboard = () => {
{mentor?.profession}
+ {mentor?.email && (
+
+ Email:{' '}
+
+ {mentor.email}
+
+
+ )}
Company:{' '}
@@ -111,12 +128,16 @@ const MenteeDashboard = () => {
-
- Email:{' '}
-
- {mentor?.email}
-
-
+ {menteeDashboard?.nextSession && (
+
+ Next Session:{' '}
+
+ {new Date(
+ menteeDashboard.nextSession
+ ).toLocaleString()}
+
+
+ )}
@@ -125,14 +146,25 @@ const MenteeDashboard = () => {
Message
-
-
- Book a Session
+
+
+
+ Book a Session
+
@@ -196,7 +228,14 @@ const MenteeDashboard = () => {
- {}} />
+ {
+ const sessionId = menteeUpcomingSession?.id;
+ if (!sessionId) return;
+ const note = title ? `${title}: ${content}` : content;
+ await createMenteeNote(sessionId, note);
+ }}
+ />
diff --git a/packages/frontend/src/features/mentorship/store/index.ts b/packages/frontend/src/features/mentorship/store/index.ts
index 98d485b..ba2f906 100644
--- a/packages/frontend/src/features/mentorship/store/index.ts
+++ b/packages/frontend/src/features/mentorship/store/index.ts
@@ -48,6 +48,10 @@ interface MentorshipState {
createMentorshipSession: (
payload: CreateMentorshipSessionDto
) => Promise;
+ // Mentee notes actions
+ createMenteeNote: (sessionId: number, note: string) => Promise;
+ updateMenteeNote: (id: number, note: string) => Promise;
+ deleteMenteeNote: (id: number) => Promise;
fetchInterests: () => Promise;
getMentor: (mentorId: number) => Promise;
getPendingApplications: () => Promise;
@@ -83,6 +87,56 @@ export const useMentorshipStore = create()(
menteeNotes: [],
pastMentors: [],
menteeUpcomingSession: null,
+ createMenteeNote: async (sessionId, note) => {
+ set({ isLoading: true, error: null });
+ try {
+ await mentorshipApi.createMenteeNote(sessionId, note);
+ await mentorshipApi
+ .getMenteeNotes()
+ .then((res) => set({ menteeNotes: res.data }));
+ set({ isLoading: false });
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : 'An error occurred',
+ isLoading: false
+ });
+ throw error;
+ }
+ },
+
+ updateMenteeNote: async (id, note) => {
+ set({ isLoading: true, error: null });
+ try {
+ await mentorshipApi.updateMenteeNote(id, note);
+ await mentorshipApi
+ .getMenteeNotes()
+ .then((res) => set({ menteeNotes: res.data }));
+ set({ isLoading: false });
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : 'An error occurred',
+ isLoading: false
+ });
+ throw error;
+ }
+ },
+
+ deleteMenteeNote: async (id) => {
+ set({ isLoading: true, error: null });
+ try {
+ await mentorshipApi.deleteMenteeNote(id);
+ await mentorshipApi
+ .getMenteeNotes()
+ .then((res) => set({ menteeNotes: res.data }));
+ set({ isLoading: false });
+ } catch (error) {
+ set({
+ error: error instanceof Error ? error.message : 'An error occurred',
+ isLoading: false
+ });
+ throw error;
+ }
+ },
createMentor: async (mentorData) => {
set({ isLoading: true, error: null });
diff --git a/packages/frontend/src/features/mentorship/types/index.ts b/packages/frontend/src/features/mentorship/types/index.ts
index e83b5a1..bd7ea9e 100644
--- a/packages/frontend/src/features/mentorship/types/index.ts
+++ b/packages/frontend/src/features/mentorship/types/index.ts
@@ -176,6 +176,7 @@ export interface MenteeUpcomingSession {
export interface MenteeDashboard {
mentor: {
+ id?: number;
firstName: string;
lastName: string;
email: string;
@@ -187,6 +188,7 @@ export interface MenteeDashboard {
mentorshipStarted: string | null;
sessionsAttended: number;
nextSession: string | null;
+ nextSessionLink?: string | null;
}
// DTO for creating mentorship session (MENTOR only)