diff --git a/package.json b/package.json index 0713614..92dfade 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "typeorm:db": "npm run build && npx typeorm -d dist/src/configs/dbConfig.js", "migration:generate": "npm run typeorm:db -- migration:generate", "migration:run": "npm run typeorm:db -- migration:run", + "migration:revert": "npm run typeorm:db -- migration:revert", "sync:db": "npm run typeorm:db schema:sync", "seed": "npm run build && node dist/src/scripts/seed-db.js" }, diff --git a/src/controllers/monthlyChecking.controller.ts b/src/controllers/monthlyChecking.controller.ts new file mode 100644 index 0000000..bb6ed6e --- /dev/null +++ b/src/controllers/monthlyChecking.controller.ts @@ -0,0 +1,95 @@ +import type { Request, Response } from 'express' +import { type ApiResponse } from '../types' +import type MonthlyCheckIn from '../entities/checkin.entity' + +import { + addFeedbackByMentor, + addMonthlyCheckInByMentee, + fetchMonthlyCheckIns +} from '../services/monthlyChecking.service' + +export const postMonthlyCheckIn = async ( + req: Request, + res: Response +): Promise>> => { + try { + const { + menteeId, + title, + generalUpdatesAndFeedback, + progressTowardsGoals, + mediaContentLinks + } = req.body + + const newCheckIn = await addMonthlyCheckInByMentee( + menteeId, + title, + generalUpdatesAndFeedback, + progressTowardsGoals, + mediaContentLinks + ) + + return res + .status(201) + .json({ checkIn: newCheckIn, message: 'Check-in added successfully' }) + } catch (err) { + if (err instanceof Error) { + console.error('Error executing query', err) + return res + .status(500) + .json({ error: 'Internal server error', message: err.message }) + } + throw err + } +} + +export const getMonthlyCheckIns = async ( + req: Request, + res: Response +): Promise>> => { + try { + const { menteeId } = req.params + + const { statusCode, checkIns, message } = await fetchMonthlyCheckIns( + menteeId + ) + + return res.status(statusCode).json({ checkIns, message }) + } catch (err) { + if (err instanceof Error) { + console.error('Error executing query', err) + return res + .status(500) + .json({ error: 'Internal server error', message: err.message }) + } + throw err + } +} + +export const addFeedbackMonthlyCheckIn = async ( + req: Request, + res: Response +): Promise => { + try { + const { checkInId, menteeId, mentorFeedback, isCheckedByMentor } = req.body + + const newMentorFeedbackCheckIn = await addFeedbackByMentor( + menteeId, + checkInId, + mentorFeedback, + isCheckedByMentor + ) + + res.status(201).json({ + feedbackCheckIn: newMentorFeedbackCheckIn + }) + } catch (err) { + if (err instanceof Error) { + console.error('Error executing query', err) + res + .status(500) + .json({ error: 'Internal server error', message: err.message }) + } + throw err + } +} diff --git a/src/entities/checkin.entity.ts b/src/entities/checkin.entity.ts new file mode 100644 index 0000000..31693ad --- /dev/null +++ b/src/entities/checkin.entity.ts @@ -0,0 +1,63 @@ +import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm' +import BaseEntity from './baseEntity' +import Mentee from './mentee.entity' + +@Entity('monthly-check-in') +class MonthlyCheckIn extends BaseEntity { + @Column({ type: 'text' }) + title: string + + @Column({ type: 'text' }) + generalUpdatesAndFeedback: string + + @Column({ type: 'text' }) + progressTowardsGoals: string + + @Column({ type: 'json' }) + mediaContentLinks: string[] + + @Column({ type: 'text', nullable: true }) + mentorFeedback: string + + @Column({ type: 'boolean', default: false }) + isCheckedByMentor: boolean + + @Column({ type: 'timestamp', nullable: true }) + mentorCheckedDate: Date + + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + checkInDate: Date + + @ManyToOne(() => Mentee, (mentee) => mentee.checkIns) + @JoinColumn({ name: 'menteeId' }) + mentee: Mentee + + constructor( + title: string, + generalUpdatesAndFeedback: string, + progressTowardsGoals: string, + mediaContentLinks: string[], + mentorFeedback: string, + isCheckedByMentor: boolean, + mentorCheckedDate: Date, + checkInDate: Date, + mentee: Mentee + ) { + super() + this.title = title + this.generalUpdatesAndFeedback = generalUpdatesAndFeedback + this.progressTowardsGoals = progressTowardsGoals + this.mediaContentLinks = mediaContentLinks + this.mentorFeedback = mentorFeedback + this.isCheckedByMentor = isCheckedByMentor + this.mentorCheckedDate = mentorCheckedDate + this.checkInDate = checkInDate + this.mentee = mentee + } + + validate(): boolean { + return this.mediaContentLinks.length >= 3 + } +} + +export default MonthlyCheckIn diff --git a/src/entities/mentee.entity.ts b/src/entities/mentee.entity.ts index d2d4813..dbfbf5b 100644 --- a/src/entities/mentee.entity.ts +++ b/src/entities/mentee.entity.ts @@ -1,9 +1,10 @@ -import { Column, Entity, ManyToOne } from 'typeorm' +import { Column, Entity, ManyToOne, OneToMany } from 'typeorm' import Mentor from './mentor.entity' import profileEntity from './profile.entity' import { MenteeApplicationStatus, StatusUpdatedBy } from '../enums' import BaseEntity from './baseEntity' import { UUID } from 'typeorm/driver/mongodb/bson.typings' +import MonthlyCheckIn from './checkin.entity' @Entity('mentee') class Mentee extends BaseEntity { @@ -35,17 +36,22 @@ class Mentee extends BaseEntity { @ManyToOne(() => Mentor, (mentor) => mentor.mentees) mentor: Mentor + @OneToMany(() => MonthlyCheckIn, (checkIn) => checkIn.mentee) + checkIns?: MonthlyCheckIn[] + constructor( state: MenteeApplicationStatus, application: Record, profile: profileEntity, - mentor: Mentor + mentor: Mentor, + checkIns?: MonthlyCheckIn[] ) { super() this.state = state || MenteeApplicationStatus.PENDING this.application = application this.profile = profile this.mentor = mentor + this.checkIns = checkIns } } diff --git a/src/migrations/1722051742722-RemoveUniqueConstraintFromProfileUuid.ts b/src/migrations/1722051742722-RemoveUniqueConstraintFromProfileUuid.ts index 936370d..03384d7 100644 --- a/src/migrations/1722051742722-RemoveUniqueConstraintFromProfileUuid.ts +++ b/src/migrations/1722051742722-RemoveUniqueConstraintFromProfileUuid.ts @@ -6,14 +6,25 @@ export class RemoveUniqueConstraintFromProfileUuid1722051742722 name = 'RemoveUniqueConstraintFromProfileUuid1722051742722' public async up(queryRunner: QueryRunner): Promise { + // Check if the constraint exists before attempting to drop it + const constraintExists = await queryRunner.query(` + SELECT 1 + FROM information_schema.table_constraints + WHERE constraint_name = 'REL_f671cf2220d1bd0621a1a5e92e' + AND table_name = 'mentee' + `) + + if (constraintExists.length > 0) { + await queryRunner.query( + `ALTER TABLE "mentee" DROP CONSTRAINT "REL_f671cf2220d1bd0621a1a5e92e"` + ) + } + await queryRunner.query( `ALTER TABLE "mentee" DROP CONSTRAINT "FK_f671cf2220d1bd0621a1a5e92e7"` ) await queryRunner.query( - `ALTER TABLE "mentee" DROP CONSTRAINT "REL_f671cf2220d1bd0621a1a5e92e"` - ) - await queryRunner.query( - `ALTER TABLE "mentee" ADD CONSTRAINT "FK_f671cf2220d1bd0621a1a5e92e7" FOREIGN KEY ("profileUuid") REFERENCES "profile"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION` + `ALTER TABLE "mentee" ADD CONSTRAINT "FK_f671cf2220d1bd0621a1a5e92e7" FOREIGN KEY ("profileUuid") REFERENCES "profile"("uuid") ON DELETE CASCADE ON UPDATE NO ACTION` ) } diff --git a/src/migrations/1727197270336-monthly-checking-tags.ts b/src/migrations/1727197270336-monthly-checking-tags.ts new file mode 100644 index 0000000..2a1009e --- /dev/null +++ b/src/migrations/1727197270336-monthly-checking-tags.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class MonthlyCheckingTags1727197270336 implements MigrationInterface { + name = 'MonthlyCheckingTags1727197270336' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "monthly-check-in" ADD "tags" json`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "monthly-check-in" DROP COLUMN "tags"`); + } + +} diff --git a/src/migrations/1727636762101-monthlychecking.ts b/src/migrations/1727636762101-monthlychecking.ts new file mode 100644 index 0000000..030e5fb --- /dev/null +++ b/src/migrations/1727636762101-monthlychecking.ts @@ -0,0 +1,13 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm' + +export class Monthlychecking1727636762101 implements MigrationInterface { + name = 'Monthlychecking1727636762101' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "monthly-check-in" DROP COLUMN "tags"`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "monthly-check-in" ADD "tags" json`) + } +} diff --git a/src/routes/mentee/mentee.route.ts b/src/routes/mentee/mentee.route.ts index 24d255b..bf6e731 100644 --- a/src/routes/mentee/mentee.route.ts +++ b/src/routes/mentee/mentee.route.ts @@ -9,9 +9,16 @@ import { } from '../../controllers/mentee.controller' import { requestBodyValidator } from '../../middlewares/requestValidator' import { + addFeedbackMonthlyCheckInSchema, menteeApplicationSchema, + postMonthlyCheckInSchema, updateMenteeStatusSchema } from '../../schemas/mentee-routes.schemas' +import { + addFeedbackMonthlyCheckIn, + getMonthlyCheckIns, + postMonthlyCheckIn +} from '../../controllers/monthlyChecking.controller' const menteeRouter = express.Router() @@ -29,4 +36,18 @@ menteeRouter.put( ) menteeRouter.put('/revoke-application', requireAuth, revokeApplication) +menteeRouter.post( + '/checkin', + [requireAuth, requestBodyValidator(postMonthlyCheckInSchema)], + postMonthlyCheckIn +) + +menteeRouter.get('/checkin/:menteeId', requireAuth, getMonthlyCheckIns) + +menteeRouter.put( + '/checking/feedback', + [requireAuth, requestBodyValidator(addFeedbackMonthlyCheckInSchema)], + addFeedbackMonthlyCheckIn +) + export default menteeRouter diff --git a/src/schemas/mentee-routes.schemas.ts b/src/schemas/mentee-routes.schemas.ts index 0f6d287..4069319 100644 --- a/src/schemas/mentee-routes.schemas.ts +++ b/src/schemas/mentee-routes.schemas.ts @@ -9,3 +9,37 @@ export const menteeApplicationSchema = z.object({ export const updateMenteeStatusSchema = z.object({ state: z.nativeEnum(MenteeApplicationStatus) }) + +export const postMonthlyCheckInSchema = z.object({ + menteeId: z.string(), + title: z.enum([ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ]), + generalUpdatesAndFeedback: z + .string() + .min(1, 'Please provide a valid feedback'), + progressTowardsGoals: z + .string() + .min(1, 'Please provide a valid progress report'), + mediaContentLinks: z + .array(z.string()) + .min(1, 'Please provide at least 1 media content links') +}) + +export const addFeedbackMonthlyCheckInSchema = z.object({ + menteeId: z.string(), + checkInId: z.string(), + mentorFeedback: z.string().optional(), + isCheckedByMentor: z.boolean() +}) diff --git a/src/services/monthlyChecking.service.ts b/src/services/monthlyChecking.service.ts new file mode 100644 index 0000000..6fe1fc8 --- /dev/null +++ b/src/services/monthlyChecking.service.ts @@ -0,0 +1,131 @@ +import { dataSource } from '../configs/dbConfig' +import MonthlyCheckIn from '../entities/checkin.entity' +import Mentee from '../entities/mentee.entity' +import { type MonthlyCheckInResponse } from '../types' + +export const addFeedbackByMentor = async ( + menteeId: string, + checkInId: string, + mentorfeedback: string, + isCheckedByMentor: boolean +): Promise<{ + statusCode: number + message: string +}> => { + try { + const menteeRepository = dataSource.getRepository(Mentee) + const checkInRepository = dataSource.getRepository(MonthlyCheckIn) + + const mentee = await menteeRepository.findOne({ + where: { uuid: menteeId } + }) + + if (!mentee) { + return { statusCode: 404, message: 'Mentee not found' } + } + + const checkIn = await checkInRepository.findOne({ + where: { uuid: checkInId, mentee: { uuid: menteeId } } + }) + + if (!checkIn) { + return { statusCode: 404, message: 'Check-in not found' } + } + + checkIn.mentorFeedback = mentorfeedback + checkIn.isCheckedByMentor = isCheckedByMentor + checkIn.mentorCheckedDate = new Date() + + await checkInRepository.save(checkIn) + + return { statusCode: 200, message: 'feedback added' } + } catch (err) { + console.error('Error in addFeedbackToMonthlyCheckIn', err) + return { statusCode: 500, message: 'Internal server error' } + } +} + +export const addMonthlyCheckInByMentee = async ( + menteeId: string, + title: string, + generalUpdatesAndFeedback: string, + progressTowardsGoals: string, + mediaContentLinks: string[] +): Promise<{ + statusCode: number + message: string +}> => { + try { + const menteeRepository = dataSource.getRepository(Mentee) + const checkInRepository = dataSource.getRepository(MonthlyCheckIn) + + const mentee = await menteeRepository.findOne({ + where: { + uuid: menteeId + } + }) + + if (!mentee) { + return { statusCode: 404, message: 'Mentee not found' } + } + + const newCheckIn = checkInRepository.create({ + title, + generalUpdatesAndFeedback, + progressTowardsGoals, + mediaContentLinks, + checkInDate: new Date(), + mentee + }) + + await checkInRepository.save(newCheckIn) + + return { statusCode: 200, message: 'monthly checking inserted' } + } catch (err) { + console.error('Error in addMonthlyCheckIn', err) + throw new Error('Error in addMonthlyCheckIn') + } +} + +export const fetchMonthlyCheckIns = async ( + menteeId: string +): Promise<{ + statusCode: number + checkIns: MonthlyCheckInResponse[] + message: string +}> => { + try { + const checkInRepository = dataSource.getRepository(MonthlyCheckIn) + + const mentee = await dataSource.getRepository(Mentee).findOne({ + where: { uuid: menteeId } + }) + + if (!mentee) { + return { statusCode: 404, checkIns: [], message: 'Mentee not found' } + } + + const checkIns = await checkInRepository.find({ + where: { mentee: { uuid: menteeId } }, + relations: ['mentee'], + order: { checkInDate: 'DESC' } + }) + + if (checkIns.length === 0 || !checkIns) { + return { + statusCode: 404, + checkIns: [], + message: 'No check-ins found' + } + } + + return { + statusCode: 200, + checkIns, + message: 'Check-ins found' + } + } catch (err) { + console.error('Error in fetchMonthlyCheckIns', err) + return { statusCode: 500, checkIns: [], message: 'Internal server error' } + } +} diff --git a/src/types.ts b/src/types.ts index b5e27a7..8c39f18 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import type Mentee from './entities/mentee.entity' + export interface ApiResponse { statusCode: number message?: string @@ -32,3 +34,16 @@ export interface LinkedInProfile { picture: string email: string } + +export interface MonthlyCheckInResponse { + uuid: string + title: string + generalUpdatesAndFeedback: string + progressTowardsGoals: string + mediaContentLinks: string[] + mentorFeedback: string | null + isCheckedByMentor: boolean + mentorCheckedDate: Date | null + checkInDate: Date + mentee: Mentee +}