import { CourseAttendee, CourseEventDetails } from "@app/shared/types/course"; import { getAllCourses, getCourseEventAttendees, getCourseEventDetails, getCourseEventRoles, getCourseEvents, insertCourseEvent, editCourseEventReport } from "../services/db/CourseSerivce"; import { Request, Response, Router } from "express"; import { requireLogin, requireMemberState } from "../middleware/auth"; import { MemberState } from "@app/shared/types/member"; import { logger } from "../services/logging/logger"; import { audit } from "../services/logging/auditLog"; import { syncQualificationsForCourseEvent } from "../services/db/qualificationService"; function validateTrainingReportPayload(payload: any): { valid: true; value: any } | { valid: false; message: string } { if (!payload || typeof payload !== "object") { return { valid: false, message: "Payload must be an object" }; } if (!Number.isInteger(payload.course_id)) { return { valid: false, message: "Must select a training" }; } const eventDate = new Date(payload.event_date); if (isNaN(eventDate.getTime())) { return { valid: false, message: "Must be a valid date" }; } const attendees = Array.isArray(payload.attendees) ? payload.attendees : []; const trainerRole = 1; const traineeRole = 2; const seenAttendees = new Set(); let hasTrainer = false; let hasTrainee = false; for (let i = 0; i < attendees.length; i++) { const attendee = attendees[i]; if (!attendee || typeof attendee !== "object") { return { valid: false, message: `Attendee at index ${i} is invalid` }; } if (!Number.isInteger(attendee.attendee_id) || attendee.attendee_id <= 0) { return { valid: false, message: "Must select a member" }; } if (!Number.isInteger(attendee.attendee_role_id) || attendee.attendee_role_id <= 0) { return { valid: false, message: "Must select a role" }; } if (typeof attendee.passed_bookwork !== "boolean" || typeof attendee.passed_qual !== "boolean") { return { valid: false, message: "Attendee pass fields must be boolean" }; } if (typeof attendee.remarks !== "string") { return { valid: false, message: "Attendee remarks must be a string" }; } if (seenAttendees.has(attendee.attendee_id)) { return { valid: false, message: "Cannot have duplicate attendee." }; } seenAttendees.add(attendee.attendee_id); hasTrainer = hasTrainer || attendee.attendee_role_id === trainerRole; hasTrainee = hasTrainee || attendee.attendee_role_id === traineeRole; } if (!hasTrainer) { return { valid: false, message: "At least one Primary Trainer is required." }; } if (!hasTrainee) { return { valid: false, message: "At least one Trainee is required." }; } return { valid: true, value: { ...payload, attendees, event_date: eventDate, remarks: payload.remarks ?? null, }, }; } const cr = Router(); const er = Router(); cr.use(requireLogin) er.use(requireLogin) cr.use(requireMemberState(MemberState.Member)) er.use(requireMemberState(MemberState.Member)) cr.get('/', async (req, res) => { try { const courses = await getAllCourses(); res.status(200).json(courses); } catch (error) { logger.error( 'app', 'Failed to fetch courses', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, } ); res.status(500).json('failed to fetch courses'); } }) cr.get('/roles', async (req, res) => { try { const roles = await getCourseEventRoles(); res.status(200).json(roles); } catch (error) { logger.error( 'app', 'Failed to fetch course roles', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, } ); res.status(500).json('failed to fetch course roles'); } }) //get event list er.get('/', async (req: Request, res: Response) => { try { const allowedSorts = new Map([ ["ascending", "ASC"], ["descending", "DESC"] ]); const page = Number(req.query.page) || undefined; const pageSize = Number(req.query.pageSize) || undefined; const sort = String(req.query.sort || "").toLowerCase(); const search = String(req.query.search || "").toLowerCase(); if (!allowedSorts.has(sort)) { return res.status(400).json({ message: `Invalid sort direction '${req.query.sort}'. Allowed values are 'ascending' or 'descending'.` }); } const sortDir = allowedSorts.get(sort); if (!sortDir) { return res.status(400).json({ message: `Invalid sort direction '${req.query.sort}'. Allowed values are 'ascending' or 'descending'.` }); } let events = await getCourseEvents(sortDir, search, page, pageSize); res.status(200).json(events); } catch (error) { logger.error( 'app', 'Failed to fetch course events', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, } ); res.status(500).json(error); } }); er.get('/:id', async (req: Request, res: Response) => { try { let out = await getCourseEventDetails(Number(req.params.id)); res.status(200).json(out); } catch (error) { logger.error( 'app', 'Failed to fetch course event', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, } ); res.status(500).json(error); } }); er.get('/attendees/:id', async (req: Request, res: Response) => { try { const attendees: CourseAttendee[] = await getCourseEventAttendees(Number(req.params.id)); res.status(200).json(attendees); } catch (error) { logger.error( 'app', 'Failed to fetch course event attendees', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, } ); res.status(500).json("failed to fetch attendees"); } }) er.post('/', async (req: Request, res: Response) => { const posterID: number = req.user.id; try { let data: CourseEventDetails = req.body; data.created_by = posterID; data.event_date = new Date(data.event_date); const id = await insertCourseEvent(data); const syncOutcome = await syncQualificationsForCourseEvent(id, posterID); audit.course('report_created', { actorId: posterID, targetId: id }, { qualificationSync: syncOutcome }); logger.info('app', 'Training report posted', { user: posterID, report: id, qualificationSync: syncOutcome }) res.status(201).json(id); } catch (error) { logger.error( 'app', 'Failed to post training report', { user: posterID, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, } ); res.status(500).json("failed to post training\n" + error) } }) er.put('/:id', async (req: Request, res: Response) => { const editorID: number = req.user.id; const reportId = Number(req.params.id); try { const validation = validateTrainingReportPayload(req.body); if (validation.valid === false) { return res.status(400).json({ message: 'Invalid training report payload', errors: validation.message, }); } const parsed = validation.value; let data: CourseEventDetails = { ...req.body, ...parsed, id: reportId, event_date: parsed.event_date, }; const actorRoles = (req.user.roles || []).map((role) => role.name); const result = await editCourseEventReport(reportId, data, editorID, actorRoles); logger.info('app', 'Training report edited', { user: editorID, report: reportId, qualificationSync: result.syncOutcome }) res.sendStatus(200); } catch (error) { const status = typeof (error as any)?.status === 'number' ? (error as any).status : 500; logger.error( 'app', 'Failed to edit training report', { user: editorID, report: reportId, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, } ); if (status !== 500) { return res.status(status).json(error instanceof Error ? error.message : String(error)); } res.status(500).json("failed to edit training\n" + error) } }) export const courseRouter = cr; export const eventRouter = er;