Implemented systems for editing training reports.

This commit is contained in:
2026-04-05 01:37:31 -04:00
parent ab9e6a6ebc
commit f0507beb88
6 changed files with 634 additions and 49 deletions

View File

@@ -1,12 +1,84 @@
import { CourseAttendee, CourseEventDetails } from "@app/shared/types/course";
import { getAllCourses, getCourseEventAttendees, getCourseEventDetails, getCourseEventRoles, getCourseEvents, insertCourseEvent, updateCourseEvent } from "../services/db/CourseSerivce";
import { getAllCourses, getCourseEventAttendees, getCourseEventDetails, getCourseEventRoles, getCourseEvents, insertCourseEvent, editCourseEventReport } from "../services/db/CourseSerivce";
import { Request, Response, Router } from "express";
import { requireLogin, requireMemberState, requireRole } from "../middleware/auth";
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<number>();
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();
@@ -69,6 +141,11 @@ er.get('/', async (req: Request, res: Response) => {
}
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);
@@ -145,21 +222,34 @@ er.post('/', async (req: Request, res: Response) => {
}
})
er.put('/:id', [requireLogin, requireMemberState(MemberState.Member), requireRole("dev")], async (req: Request, res: Response) => {
er.put('/:id', async (req: Request, res: Response) => {
const editorID: number = req.user.id;
const reportId = Number(req.params.id);
try {
let data: CourseEventDetails = req.body;
data.event_date = new Date(data.event_date);
const validation = validateTrainingReportPayload(req.body);
if (validation.valid === false) {
return res.status(400).json({
message: 'Invalid training report payload',
errors: validation.message,
});
}
await updateCourseEvent(reportId, data);
const syncOutcome = await syncQualificationsForCourseEvent(reportId, editorID);
const parsed = validation.value;
let data: CourseEventDetails = {
...req.body,
...parsed,
id: reportId,
event_date: parsed.event_date,
};
audit.course('report_edited', { actorId: editorID, targetId: reportId }, { qualificationSync: syncOutcome });
logger.info('app', 'Training report edited', { user: editorID, report: reportId, qualificationSync: syncOutcome })
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',
@@ -170,6 +260,11 @@ er.put('/:id', [requireLogin, requireMemberState(MemberState.Member), requireRol
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)
}
})