274 lines
9.4 KiB
TypeScript
274 lines
9.4 KiB
TypeScript
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<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();
|
|
|
|
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;
|