From ca152f7955b5e5db374d362861a073a8df85ef3c Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Sun, 16 Nov 2025 00:48:30 -0500 Subject: [PATCH 01/46] added service with base function to get course and event attendees --- api/src/index.js | 3 ++ api/src/routes/course.ts | 43 ++++++++++++++++++++++++++ api/src/services/CourseSerivce.ts | 51 +++++++++++++++++++++++++++++++ shared/tsconfig.json | 17 +++++++++++ shared/types/course.ts | 49 +++++++++++++++++++++++++++++ 5 files changed, 163 insertions(+) create mode 100644 api/src/routes/course.ts create mode 100644 api/src/services/CourseSerivce.ts create mode 100644 shared/tsconfig.json create mode 100644 shared/types/course.ts diff --git a/api/src/index.js b/api/src/index.js index cb7629c..59c1381 100644 --- a/api/src/index.js +++ b/api/src/index.js @@ -45,6 +45,7 @@ const loaHandler = require('./routes/loa') const { status, memberStatus } = require('./routes/statuses') const authRouter = require('./routes/auth') const { roles, memberRoles } = require('./routes/roles'); +const { courseRouter, eventRouter } = require('./routes/course'); const morgan = require('morgan'); app.use('/application', applicationsRouter); @@ -56,6 +57,8 @@ app.use('/status', status) app.use('/memberStatus', memberStatus) app.use('/roles', roles) app.use('/memberRoles', memberRoles) +app.use('/course', courseRouter) +app.use('/courseEvent', eventRouter) app.use('/', authRouter) app.get('/ping', (req, res) => { diff --git a/api/src/routes/course.ts b/api/src/routes/course.ts new file mode 100644 index 0000000..527abf9 --- /dev/null +++ b/api/src/routes/course.ts @@ -0,0 +1,43 @@ +import { CourseAttendee } from "@app/shared/types/course"; +import { getAllCourses, getCourseAttendees } from "../services/CourseSerivce"; +import { Request, Response, Router } from "express"; + +const courseRouter = Router(); +const eventRouter = Router(); + +courseRouter.get('/', async (req, res) => { + try { + const courses = await getAllCourses(); + res.status(200).json(courses); + } catch (err) { + console.error('failed to fetch courses', err); + res.status(500).json('failed to fetch courses\n' + err); + } +}) + +eventRouter.get('/attendees/:id', async (req: Request, res: Response) => { + try { + const attendees: CourseAttendee[] = await getCourseAttendees(Number(req.params.id)); + res.status(200).json(attendees); + } catch (err) { + console.error('failed to fetch attendees', err); + res.status(500).json("failed to fetch attendees\n" + err); + } +}) + +// //insert a new latest rank for a user +// ur.post('/', async (req, res) => { + +// try { +// const change = req.body?.change; +// await insertMemberRank(change.member_id, change.rank_id, change.date); + +// res.sendStatus(201); +// } catch (err) { +// console.error('Insert failed:', err); +// res.status(500).json({ error: 'Failed to update ranks' }); +// } +// }); + +module.exports.courseRouter = courseRouter; +module.exports.eventRouter = eventRouter; \ No newline at end of file diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/CourseSerivce.ts new file mode 100644 index 0000000..c24d9b9 --- /dev/null +++ b/api/src/services/CourseSerivce.ts @@ -0,0 +1,51 @@ +import pool from "../db" +import { Course, CourseAttendee, RawAttendeeRow } from "@app/shared/types/course" + +export async function getAllCourses(): Promise { + const sql = "SELECT * FROM courses WHERE deleted = false;" + + const res = await pool.query(sql); + + return res; +} + +function buildAttendee(row: RawAttendeeRow): CourseAttendee { + return { + passed: !!row.passed, + attendee_id: row.attendee_id, + course_event_id: row.course_event_id, + created_at: row.created_at, + updated_at: row.updated_at, + remarks: row.remarks, + attendee_role_id: row.attendee_role_id, + + role: row.role_id + ? { + id: row.role_id, + name: row.role_name, + description: row.role_description, + deleted: !!row.role_deleted, + created_at: row.role_created_at, + updated_at: row.role_updated_at, + } + : null + }; +} + +export async function getCourseAttendees(id: number): Promise { + const sql = `SELECT + ca.*, + ar.id AS role_id, + ar.name AS role_name, + ar.description AS role_description, + ar.deleted AS role_deleted, + ar.created_at AS role_created_at, + ar.updated_at AS role_updated_at + FROM course_attendees ca + LEFT JOIN course_attendee_roles ar ON ar.id = ca.attendee_role_id + WHERE ca.course_event_id = ?;`; + + const res:RawAttendeeRow[] = await pool.query(sql, [id]); + + return res.map((row) => buildAttendee(row)) +} \ No newline at end of file diff --git a/shared/tsconfig.json b/shared/tsconfig.json new file mode 100644 index 0000000..3f94d22 --- /dev/null +++ b/shared/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "isolatedModules": true, + "resolveJsonModule": true + }, + "include": ["./**/*.ts"] +} diff --git a/shared/types/course.ts b/shared/types/course.ts new file mode 100644 index 0000000..13f77f2 --- /dev/null +++ b/shared/types/course.ts @@ -0,0 +1,49 @@ +export interface Course { + id: number; + name: string; + short_name: string; + category: string; + description?: string | null; + image_url?: string | null; + created_at: string; + updated_at: string; + deleted?: number | boolean; + prereq_id?: number | null; +} + +export interface CourseAttendee { + passed: boolean; // tinyint(1) + attendee_id: number; // PK + course_event_id: number; // PK + attendee_role_id: number | null; + role: CourseAttendeeRole | null; + created_at: string; // datetime → ISO string + updated_at: string; // datetime → ISO string + remarks: string | null; +} + +export interface CourseAttendeeRole { + id: number; // PK, auto-increment + name: string | null; // varchar(50), unique, nullable + description: string | null; // text + created_at: string | null; // datetime (nullable) + updated_at: string | null; // datetime (nullable) + deleted: boolean; // tinyint(4) +} + +export interface RawAttendeeRow { + passed: number; + attendee_id: number; + course_event_id: number; + attendee_role_id: number | null; + created_at: string; + updated_at: string; + remarks: string | null; + + role_id: number | null; + role_name: string | null; + role_description: string | null; + role_deleted: number | null; + role_created_at: string | null; + role_updated_at: string | null; +} \ No newline at end of file From 0ff3fc58deb608c50d262f9867551a74adcbdf0f Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Sun, 16 Nov 2025 01:29:22 -0500 Subject: [PATCH 02/46] implemented getter for course event details --- api/src/routes/course.ts | 15 +++++++++++++-- api/src/services/CourseSerivce.ts | 12 ++++++++++-- shared/types/course.ts | 21 ++++++++++++++++++++- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/api/src/routes/course.ts b/api/src/routes/course.ts index 527abf9..7a61097 100644 --- a/api/src/routes/course.ts +++ b/api/src/routes/course.ts @@ -1,5 +1,5 @@ import { CourseAttendee } from "@app/shared/types/course"; -import { getAllCourses, getCourseAttendees } from "../services/CourseSerivce"; +import { getAllCourses, getCourseEventAttendees, getCourseEventDetails } from "../services/CourseSerivce"; import { Request, Response, Router } from "express"; const courseRouter = Router(); @@ -15,9 +15,20 @@ courseRouter.get('/', async (req, res) => { } }) +eventRouter.get('/:id', async (req: Request, res: Response) => { + try { + let out = await getCourseEventDetails(Number(req.params.id)); + console.log(out); + res.status(200).json(out); + } catch (error) { + console.error('failed to fetch report', error); + res.status(500).json(error); + } +}); + eventRouter.get('/attendees/:id', async (req: Request, res: Response) => { try { - const attendees: CourseAttendee[] = await getCourseAttendees(Number(req.params.id)); + const attendees: CourseAttendee[] = await getCourseEventAttendees(Number(req.params.id)); res.status(200).json(attendees); } catch (err) { console.error('failed to fetch attendees', err); diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/CourseSerivce.ts index c24d9b9..5d913a6 100644 --- a/api/src/services/CourseSerivce.ts +++ b/api/src/services/CourseSerivce.ts @@ -1,5 +1,5 @@ import pool from "../db" -import { Course, CourseAttendee, RawAttendeeRow } from "@app/shared/types/course" +import { Course, CourseAttendee, CourseEventDetails, RawAttendeeRow } from "@app/shared/types/course" export async function getAllCourses(): Promise { const sql = "SELECT * FROM courses WHERE deleted = false;" @@ -32,7 +32,7 @@ function buildAttendee(row: RawAttendeeRow): CourseAttendee { }; } -export async function getCourseAttendees(id: number): Promise { +export async function getCourseEventAttendees(id: number): Promise { const sql = `SELECT ca.*, ar.id AS role_id, @@ -48,4 +48,12 @@ export async function getCourseAttendees(id: number): Promise const res:RawAttendeeRow[] = await pool.query(sql, [id]); return res.map((row) => buildAttendee(row)) +} + +export async function getCourseEventDetails(id: number): Promise { + const sql = `SELECT * FROM course_events WHERE id = ?;`; + let rows: CourseEventDetails[] = await pool.query(sql, [id]); + let event = rows[0]; + event.attendees = await getCourseEventAttendees(id); + return event; } \ No newline at end of file diff --git a/shared/types/course.ts b/shared/types/course.ts index 13f77f2..5e36f6f 100644 --- a/shared/types/course.ts +++ b/shared/types/course.ts @@ -6,11 +6,30 @@ export interface Course { description?: string | null; image_url?: string | null; created_at: string; - updated_at: string; + updated_at: string; deleted?: number | boolean; prereq_id?: number | null; } +export interface CourseEventDetails { + id: number; // PK + course_id: number | null; // FK → courses.id + event_type: number | null; // FK → event_types.id + event_date: string; // datetime (not nullable) + + guilded_event_id: number | null; + + created_at: string; // datetime + updated_at: string; // datetime + + deleted: boolean | null; // tinyint(4), nullable + report_url: string | null; // varchar(2048) + remarks: string | null; // text + + attendees: CourseAttendee[] | null; +} + + export interface CourseAttendee { passed: boolean; // tinyint(1) attendee_id: number; // PK From 810a15d27955e2de1d8af42787f6a29762d14655 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Sun, 16 Nov 2025 10:10:09 -0500 Subject: [PATCH 03/46] added API support for posting training reports --- api/src/routes/course.ts | 16 ++++++++++++-- api/src/services/CourseSerivce.ts | 36 +++++++++++++++++++++++++++---- shared/types/course.ts | 2 +- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/api/src/routes/course.ts b/api/src/routes/course.ts index 7a61097..73955db 100644 --- a/api/src/routes/course.ts +++ b/api/src/routes/course.ts @@ -1,5 +1,5 @@ -import { CourseAttendee } from "@app/shared/types/course"; -import { getAllCourses, getCourseEventAttendees, getCourseEventDetails } from "../services/CourseSerivce"; +import { CourseAttendee, CourseEventDetails } from "@app/shared/types/course"; +import { getAllCourses, getCourseEventAttendees, getCourseEventDetails, insertCourseEvent } from "../services/CourseSerivce"; import { Request, Response, Router } from "express"; const courseRouter = Router(); @@ -36,6 +36,18 @@ eventRouter.get('/attendees/:id', async (req: Request, res: Response) => { } }) +eventRouter.post('/', async (req: Request, res: Response) => { + try { + console.log(); + const data: CourseEventDetails = req.body; + await insertCourseEvent(data); + res.sendStatus(201); + } catch (error) { + console.error('failed to post training', error); + res.status(500).json("failed to post training\n" + error) + } +}) + // //insert a new latest rank for a user // ur.post('/', async (req, res) => { diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/CourseSerivce.ts index 5d913a6..7fc063a 100644 --- a/api/src/services/CourseSerivce.ts +++ b/api/src/services/CourseSerivce.ts @@ -18,7 +18,7 @@ function buildAttendee(row: RawAttendeeRow): CourseAttendee { updated_at: row.updated_at, remarks: row.remarks, attendee_role_id: row.attendee_role_id, - + role: row.role_id ? { id: row.role_id, @@ -44,9 +44,9 @@ export async function getCourseEventAttendees(id: number): Promise buildAttendee(row)) } @@ -56,4 +56,32 @@ export async function getCourseEventDetails(id: number): Promise { + const con = await pool.getConnection(); + try { + await con.beginTransaction(); + const res = await con.execute("INSERT INTO course_events (course_id, event_date, remarks) VALUES (?, ?, ?);", [event.course_id, event.event_date, event.remarks]); + var eventID: number = res.insertId; + + for (const attendee of event.attendees) { + await con.execute(`INSERT INTO course_attendees ( + attendee_id, + course_event_id, + attendee_role_id, + passed, + remarks + ) + VALUES (?, ?, ?, ?, ?);`, [attendee.attendee_id, eventID, attendee.attendee_role_id, attendee.passed, attendee.remarks]); + + } + await con.commit(); + } catch (error) { + await con.rollback(); + throw error; + } finally { + await con.release(); + return eventID; + } } \ No newline at end of file diff --git a/shared/types/course.ts b/shared/types/course.ts index 5e36f6f..4f75ad5 100644 --- a/shared/types/course.ts +++ b/shared/types/course.ts @@ -12,7 +12,7 @@ export interface Course { } export interface CourseEventDetails { - id: number; // PK + id: number | null; // PK course_id: number | null; // FK → courses.id event_type: number | null; // FK → event_types.id event_date: string; // datetime (not nullable) From 4d0dea553ef8b611c08663b168fe2893a0e53361 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Sun, 16 Nov 2025 10:15:52 -0500 Subject: [PATCH 04/46] added support for getting all training reports --- api/src/routes/course.ts | 12 +++++++++++- api/src/services/CourseSerivce.ts | 6 ++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/api/src/routes/course.ts b/api/src/routes/course.ts index 73955db..5333ab0 100644 --- a/api/src/routes/course.ts +++ b/api/src/routes/course.ts @@ -1,5 +1,5 @@ import { CourseAttendee, CourseEventDetails } from "@app/shared/types/course"; -import { getAllCourses, getCourseEventAttendees, getCourseEventDetails, insertCourseEvent } from "../services/CourseSerivce"; +import { getAllCourses, getCourseEventAttendees, getCourseEventDetails, getCourseEvents, insertCourseEvent } from "../services/CourseSerivce"; import { Request, Response, Router } from "express"; const courseRouter = Router(); @@ -15,6 +15,16 @@ courseRouter.get('/', async (req, res) => { } }) +eventRouter.get('/', async (req: Request, res: Response) => { + try { + let events: CourseEventDetails[] = await getCourseEvents(); + res.status(200).json(events); + } catch (error) { + console.error('failed to fetch reports', error); + res.status(500).json(error); + } +}); + eventRouter.get('/:id', async (req: Request, res: Response) => { try { let out = await getCourseEventDetails(Number(req.params.id)); diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/CourseSerivce.ts index 7fc063a..dcc735d 100644 --- a/api/src/services/CourseSerivce.ts +++ b/api/src/services/CourseSerivce.ts @@ -84,4 +84,10 @@ export async function insertCourseEvent(event: CourseEventDetails): Promise { + const sql = "SELECT * FROM course_events;"; + let events: CourseEventDetails[] = await pool.query(sql); + return events; } \ No newline at end of file From f6dd3a77dc0cffe06e92ee144bd7a8d3c6ef8bea Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Sun, 16 Nov 2025 22:37:41 -0500 Subject: [PATCH 05/46] added support for short format training report and created_by field --- api/src/routes/course.ts | 2 +- api/src/services/CourseSerivce.ts | 9 +++++---- shared/types/course.ts | 12 +++++++++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/api/src/routes/course.ts b/api/src/routes/course.ts index 5333ab0..375982f 100644 --- a/api/src/routes/course.ts +++ b/api/src/routes/course.ts @@ -17,7 +17,7 @@ courseRouter.get('/', async (req, res) => { eventRouter.get('/', async (req: Request, res: Response) => { try { - let events: CourseEventDetails[] = await getCourseEvents(); + let events = await getCourseEvents(); res.status(200).json(events); } catch (error) { console.error('failed to fetch reports', error); diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/CourseSerivce.ts index dcc735d..f83132a 100644 --- a/api/src/services/CourseSerivce.ts +++ b/api/src/services/CourseSerivce.ts @@ -1,5 +1,5 @@ import pool from "../db" -import { Course, CourseAttendee, CourseEventDetails, RawAttendeeRow } from "@app/shared/types/course" +import { Course, CourseAttendee, CourseEventDetails, CourseEventSummary, RawAttendeeRow } from "@app/shared/types/course" export async function getAllCourses(): Promise { const sql = "SELECT * FROM courses WHERE deleted = false;" @@ -86,8 +86,9 @@ export async function insertCourseEvent(event: CourseEventDetails): Promise { - const sql = "SELECT * FROM course_events;"; - let events: CourseEventDetails[] = await pool.query(sql); +export async function getCourseEvents(): Promise { + const sql = "SELECT E.id, E.course_id, E.event_date, E.created_by, C.name FROM course_events as E LEFT JOIN courses AS C ON E.course_id = C.id;"; + let events: CourseEventSummary[] = await pool.query(sql); + console.log(events); return events; } \ No newline at end of file diff --git a/shared/types/course.ts b/shared/types/course.ts index 4f75ad5..cc3efee 100644 --- a/shared/types/course.ts +++ b/shared/types/course.ts @@ -25,8 +25,10 @@ export interface CourseEventDetails { deleted: boolean | null; // tinyint(4), nullable report_url: string | null; // varchar(2048) remarks: string | null; // text - + attendees: CourseAttendee[] | null; + + created_by: number | null; } @@ -65,4 +67,12 @@ export interface RawAttendeeRow { role_deleted: number | null; role_created_at: string | null; role_updated_at: string | null; +} + +export interface CourseEventSummary { + event_id: number; + course_id: number; + course_name: string; + date: string; + created_by: number; } \ No newline at end of file From dd07397c2d52ad0af7ad30beed40797aa30fe4f7 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Sun, 16 Nov 2025 22:49:59 -0500 Subject: [PATCH 06/46] switched vue project to proper tsconfig --- ui/{jsconfig.json => tsconfig.json} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename ui/{jsconfig.json => tsconfig.json} (76%) diff --git a/ui/jsconfig.json b/ui/tsconfig.json similarity index 76% rename from ui/jsconfig.json rename to ui/tsconfig.json index 225a4a4..eac02cb 100644 --- a/ui/jsconfig.json +++ b/ui/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "paths": { "@/*": ["./src/*"], - "@shared": ["../shared/*"] + "@shared/*": ["../shared/*"] } }, "exclude": ["node_modules", "dist"] From f49988fbaf8272caafb8b68da6972357e7e5d90f Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Sun, 16 Nov 2025 22:50:16 -0500 Subject: [PATCH 07/46] added zod dep to shared library --- shared/package-lock.json | 24 ++++++++++++++++++++++++ shared/package.json | 7 +++++-- 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 shared/package-lock.json diff --git a/shared/package-lock.json b/shared/package-lock.json new file mode 100644 index 0000000..e709653 --- /dev/null +++ b/shared/package-lock.json @@ -0,0 +1,24 @@ +{ + "name": "@app/shared", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@app/shared", + "version": "1.0.0", + "dependencies": { + "zod": "^4.1.12" + } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/shared/package.json b/shared/package.json index 374484e..17bc4d0 100644 --- a/shared/package.json +++ b/shared/package.json @@ -2,5 +2,8 @@ "name": "@app/shared", "version": "1.0.0", "main": "index.ts", - "type": "module" -} \ No newline at end of file + "type": "module", + "dependencies": { + "zod": "^4.1.12" + } +} From 631eae4412ec19f459810f92675bc0c81b3bde33 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Sun, 16 Nov 2025 22:51:42 -0500 Subject: [PATCH 08/46] added training report list to client --- api/src/services/CourseSerivce.ts | 2 +- shared/schemas/trainingReportSchema.ts | 20 +++++++++ ui/src/api/trainingReport.ts | 15 +++++++ ui/src/pages/TrainingReport.vue | 59 ++++++++++++++++++++++++++ ui/src/router/index.js | 1 + 5 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 shared/schemas/trainingReportSchema.ts create mode 100644 ui/src/api/trainingReport.ts create mode 100644 ui/src/pages/TrainingReport.vue diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/CourseSerivce.ts index f83132a..60dd48c 100644 --- a/api/src/services/CourseSerivce.ts +++ b/api/src/services/CourseSerivce.ts @@ -87,7 +87,7 @@ export async function insertCourseEvent(event: CourseEventDetails): Promise { - const sql = "SELECT E.id, E.course_id, E.event_date, E.created_by, C.name FROM course_events as E LEFT JOIN courses AS C ON E.course_id = C.id;"; + const sql = "SELECT E.id AS event_id, E.course_id, E.event_date AS date, E.created_by, C.name AS course_name FROM course_events as E LEFT JOIN courses AS C ON E.course_id = C.id;"; let events: CourseEventSummary[] = await pool.query(sql); console.log(events); return events; diff --git a/shared/schemas/trainingReportSchema.ts b/shared/schemas/trainingReportSchema.ts new file mode 100644 index 0000000..d153953 --- /dev/null +++ b/shared/schemas/trainingReportSchema.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; + +export const trainingReportSchema = z.object({ + id: z.number().int().positive().optional(), + course_id: z.number().int(), + event_date: z + .string() + .refine( + (val) => !isNaN(Date.parse(val)), + "event_date must be a valid ISO date string" + ), + remarks: z.string().nullable().optional(), +}) + +export const courseEventAttendeeSchema = z.object({ + attendee_id: z.number().int().positive(), + passed: z.boolean(), + remarks: z.string(), + attendee_role_id: z.number().int().positive() +}) diff --git a/ui/src/api/trainingReport.ts b/ui/src/api/trainingReport.ts new file mode 100644 index 0000000..3cbde6c --- /dev/null +++ b/ui/src/api/trainingReport.ts @@ -0,0 +1,15 @@ +import { CourseEventDetails, CourseEventSummary } from '@shared/types/course' + +//@ts-ignore +const addr = import.meta.env.VITE_APIHOST; + +export async function getTrainingReports(): Promise { + const res = await fetch(`${addr}/courseEvent`); + + if (res.ok) { + return await res.json() as Promise; + } else { + console.error("Something went wrong"); + throw new Error("Failed to load training reports"); + } +} \ No newline at end of file diff --git a/ui/src/pages/TrainingReport.vue b/ui/src/pages/TrainingReport.vue new file mode 100644 index 0000000..ab6dec9 --- /dev/null +++ b/ui/src/pages/TrainingReport.vue @@ -0,0 +1,59 @@ + + + diff --git a/ui/src/router/index.js b/ui/src/router/index.js index b688592..acfce65 100644 --- a/ui/src/router/index.js +++ b/ui/src/router/index.js @@ -16,6 +16,7 @@ const router = createRouter({ { path: '/loa', component: () => import('@/pages/SubmitLOA.vue'), meta: { requiresAuth: true, memberOnly: true } }, { path: '/transfer', component: () => import('@/pages/Transfer.vue'), meta: { requiresAuth: true, memberOnly: true } }, { path: '/calendar', component: () => import('@/pages/Calendar.vue'), meta: { requiresAuth: true, memberOnly: true } }, + { path: '/trainingReport', component: () => import('@/pages/TrainingReport.vue'), meta: { requiresAuth: true, memberOnly: true } }, // ADMIN / STAFF ROUTES { From 5387306d939fc877b7ebc784d2b0d8837ff86d74 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Sun, 16 Nov 2025 22:55:12 -0500 Subject: [PATCH 09/46] removed nuisance logging --- api/src/services/CourseSerivce.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/CourseSerivce.ts index 60dd48c..c07ccf1 100644 --- a/api/src/services/CourseSerivce.ts +++ b/api/src/services/CourseSerivce.ts @@ -89,6 +89,5 @@ export async function insertCourseEvent(event: CourseEventDetails): Promise { const sql = "SELECT E.id AS event_id, E.course_id, E.event_date AS date, E.created_by, C.name AS course_name FROM course_events as E LEFT JOIN courses AS C ON E.course_id = C.id;"; let events: CourseEventSummary[] = await pool.query(sql); - console.log(events); return events; } \ No newline at end of file From 1d35fe1cf5f508b1110fe0eecb0349b5546d8167 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Sun, 16 Nov 2025 23:40:06 -0500 Subject: [PATCH 10/46] added support for course name in course_event details --- api/src/services/CourseSerivce.ts | 3 +-- shared/types/course.ts | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/CourseSerivce.ts index c07ccf1..059dcfd 100644 --- a/api/src/services/CourseSerivce.ts +++ b/api/src/services/CourseSerivce.ts @@ -51,7 +51,7 @@ export async function getCourseEventAttendees(id: number): Promise { - const sql = `SELECT * FROM course_events WHERE id = ?;`; + const sql = `SELECT E.*, C.name AS course_name FROM course_events AS E LEFT JOIN courses AS C ON E.course_id = C.id WHERE E.id = ?;`; let rows: CourseEventDetails[] = await pool.query(sql, [id]); let event = rows[0]; event.attendees = await getCourseEventAttendees(id); @@ -74,7 +74,6 @@ export async function insertCourseEvent(event: CourseEventDetails): Promise Date: Mon, 17 Nov 2025 00:21:38 -0500 Subject: [PATCH 11/46] implemented most of the viewing training reports UI --- ui/src/api/trainingReport.ts | 11 ++++++ ui/src/pages/TrainingReport.vue | 70 ++++++++++++++++++++++++++++----- 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/ui/src/api/trainingReport.ts b/ui/src/api/trainingReport.ts index 3cbde6c..c2c57d9 100644 --- a/ui/src/api/trainingReport.ts +++ b/ui/src/api/trainingReport.ts @@ -12,4 +12,15 @@ export async function getTrainingReports(): Promise { console.error("Something went wrong"); throw new Error("Failed to load training reports"); } +} + +export async function getTrainingReport(id: number): Promise { + const res = await fetch(`${addr}/courseEvent/${id}`); + + if (res.ok) { + return await res.json() as Promise; + } else { + console.error("Something went wrong"); + throw new Error("Failed to load training reports"); + } } \ No newline at end of file diff --git a/ui/src/pages/TrainingReport.vue b/ui/src/pages/TrainingReport.vue index ab6dec9..15abf3f 100644 --- a/ui/src/pages/TrainingReport.vue +++ b/ui/src/pages/TrainingReport.vue @@ -1,9 +1,9 @@ From 750ee5f02c41fd562967f274763e4fbab99420c7 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Mon, 17 Nov 2025 11:57:03 -0500 Subject: [PATCH 12/46] removed nuisance print --- api/src/routes/course.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/api/src/routes/course.ts b/api/src/routes/course.ts index 375982f..ce7ce93 100644 --- a/api/src/routes/course.ts +++ b/api/src/routes/course.ts @@ -28,7 +28,6 @@ eventRouter.get('/', async (req: Request, res: Response) => { eventRouter.get('/:id', async (req: Request, res: Response) => { try { let out = await getCourseEventDetails(Number(req.params.id)); - console.log(out); res.status(200).json(out); } catch (error) { console.error('failed to fetch report', error); From 2eeb62cf3c80e3b6c57861fdf4dd9957528c43e6 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Mon, 17 Nov 2025 11:57:25 -0500 Subject: [PATCH 13/46] added member names to training reports --- api/src/services/CourseSerivce.ts | 4 +++- shared/types/course.ts | 4 ++++ ui/src/pages/TrainingReport.vue | 25 ++++++++++++++----------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/CourseSerivce.ts index 059dcfd..bc8d283 100644 --- a/api/src/services/CourseSerivce.ts +++ b/api/src/services/CourseSerivce.ts @@ -18,7 +18,7 @@ function buildAttendee(row: RawAttendeeRow): CourseAttendee { updated_at: row.updated_at, remarks: row.remarks, attendee_role_id: row.attendee_role_id, - + attendee_name: row.attendee_name, role: row.role_id ? { id: row.role_id, @@ -35,6 +35,7 @@ function buildAttendee(row: RawAttendeeRow): CourseAttendee { export async function getCourseEventAttendees(id: number): Promise { const sql = `SELECT ca.*, + mem.name AS attendee_name, ar.id AS role_id, ar.name AS role_name, ar.description AS role_description, @@ -43,6 +44,7 @@ export async function getCourseEventAttendees(id: number): Promise { + + \ No newline at end of file From 7a31c77c7ecc8faa91ae29f1377c4a2d77298ed6 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Thu, 20 Nov 2025 09:07:31 -0500 Subject: [PATCH 28/46] applied scroll behaviour to the form too --- ui/src/pages/TrainingReport.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/src/pages/TrainingReport.vue b/ui/src/pages/TrainingReport.vue index 2abf2a7..67fddc0 100644 --- a/ui/src/pages/TrainingReport.vue +++ b/ui/src/pages/TrainingReport.vue @@ -127,7 +127,9 @@ onMounted(async () => { - +
+ +
From 105b28d9a4e7e05564f6d5633404d555b842864b Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Thu, 20 Nov 2025 09:22:59 -0500 Subject: [PATCH 29/46] Integrated "created by" system --- api/src/routes/course.ts | 4 +++- api/src/services/memberService.ts | 13 ++++++++++++- ui/src/api/trainingReport.ts | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/api/src/routes/course.ts b/api/src/routes/course.ts index fcef0a4..22cd023 100644 --- a/api/src/routes/course.ts +++ b/api/src/routes/course.ts @@ -56,9 +56,11 @@ eventRouter.get('/attendees/:id', async (req: Request, res: Response) => { }) eventRouter.post('/', async (req: Request, res: Response) => { + const posterID: number = req.user.id; try { console.log(); - const data: CourseEventDetails = req.body; + let data: CourseEventDetails = req.body; + data.created_by = posterID; const id = await insertCourseEvent(data); res.status(201).json(id); } catch (error) { diff --git a/api/src/services/memberService.ts b/api/src/services/memberService.ts index 98045da..4563a6a 100644 --- a/api/src/services/memberService.ts +++ b/api/src/services/memberService.ts @@ -20,4 +20,15 @@ export async function setUserState(userID: number, state: MemberState) { SET state = ? WHERE id = ?;`; return await pool.query(sql, [state, userID]); -} \ No newline at end of file +} + +declare global { + namespace Express { + interface Request { + user: { + id: number; + name: string; + }; + } + } +} diff --git a/ui/src/api/trainingReport.ts b/ui/src/api/trainingReport.ts index 1c93232..349e9cb 100644 --- a/ui/src/api/trainingReport.ts +++ b/ui/src/api/trainingReport.ts @@ -54,6 +54,7 @@ export async function postTrainingReport(report: CourseEventDetails) { "Content-Type": "application/json", }, body: JSON.stringify(report), + credentials: 'include', }); if (!res.ok) { From aaec72af7ec9b2e16934a1ee2e8b0a956eb7fbfa Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Thu, 20 Nov 2025 10:06:01 -0500 Subject: [PATCH 30/46] Made all created by human readable --- api/src/services/CourseSerivce.ts | 27 ++++++++++++++++++++++++--- shared/types/course.ts | 2 ++ ui/src/pages/TrainingReport.vue | 15 ++++++++++----- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/CourseSerivce.ts index 7ba4daf..ea324e3 100644 --- a/api/src/services/CourseSerivce.ts +++ b/api/src/services/CourseSerivce.ts @@ -53,7 +53,17 @@ export async function getCourseEventAttendees(id: number): Promise { - const sql = `SELECT E.*, C.name AS course_name FROM course_events AS E LEFT JOIN courses AS C ON E.course_id = C.id WHERE E.id = ?;`; + const sql = `SELECT + E.*, + M.name AS created_by_name, + C.name AS course_name + FROM course_events AS E + LEFT JOIN courses AS C + ON E.course_id = C.id + LEFT JOIN members AS M + ON E.created_by = M.id + WHERE E.id = ?; + `; let rows: CourseEventDetails[] = await pool.query(sql, [id]); let event = rows[0]; event.attendees = await getCourseEventAttendees(id); @@ -65,7 +75,7 @@ export async function insertCourseEvent(event: CourseEventDetails): Promise { - const sql = "SELECT E.id AS event_id, E.course_id, E.event_date AS date, E.created_by, C.name AS course_name FROM course_events as E LEFT JOIN courses AS C ON E.course_id = C.id;"; + const sql = `SELECT + E.id AS event_id, + E.course_id, + E.event_date AS date, + E.created_by, + C.name AS course_name, + M.name AS created_by_name + FROM course_events AS E + LEFT JOIN courses AS C + ON E.course_id = C.id + LEFT JOIN members AS M + ON E.created_by = M.id;`; let events: CourseEventSummary[] = await pool.query(sql); return events; } diff --git a/shared/types/course.ts b/shared/types/course.ts index 50c7315..c16ec41 100644 --- a/shared/types/course.ts +++ b/shared/types/course.ts @@ -29,6 +29,7 @@ export interface CourseEventDetails { attendees: CourseAttendee[] | null; created_by: number | null; + created_by_name: string | null; course_name: string | null; } @@ -80,4 +81,5 @@ export interface CourseEventSummary { course_name: string; date: string; created_by: number; + created_by_name: string; } \ No newline at end of file diff --git a/ui/src/pages/TrainingReport.vue b/ui/src/pages/TrainingReport.vue index 67fddc0..1211956 100644 --- a/ui/src/pages/TrainingReport.vue +++ b/ui/src/pages/TrainingReport.vue @@ -60,7 +60,7 @@ onMounted(async () => {
- + Training @@ -76,7 +76,8 @@ onMounted(async () => { @click="viewTrainingReport(report.event_id);"> {{ report.course_name }} {{ report.date }} - {{ report.created_by === null ? "Unknown User" : report.created_by + {{ report.created_by_name === null ? "Unknown User" : + report.created_by_name }} @@ -89,6 +90,9 @@ onMounted(async () => {

{{ focusedTrainingReport.course_name }}

{{ focusedTrainingReport.event_date }}

+

Created by {{ focusedTrainingReport.created_by_name === null ? "Unknown User" : + focusedTrainingReport.created_by_name + }}

@@ -92,7 +96,7 @@ onMounted(async () => {

{{ focusedTrainingReport.event_date }}

Created by {{ focusedTrainingReport.created_by_name === null ? "Unknown User" : focusedTrainingReport.created_by_name - }}

+ }}

- +
From d9e4c1d6ff93987c649cce90ab6f22cdc13b3513 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Thu, 20 Nov 2025 14:27:34 -0500 Subject: [PATCH 32/46] added support for optional checkboxes --- api/src/services/CourseSerivce.ts | 17 +++++++++++++---- shared/types/course.ts | 9 +++++++-- .../trainingReport/trainingReportForm.vue | 19 +++++++++++++++---- ui/src/pages/TrainingReport.vue | 15 +++++++++++---- 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/CourseSerivce.ts index ea324e3..5537059 100644 --- a/api/src/services/CourseSerivce.ts +++ b/api/src/services/CourseSerivce.ts @@ -4,14 +4,21 @@ import { Course, CourseAttendee, CourseAttendeeRole, CourseEventDetails, CourseE export async function getAllCourses(): Promise { const sql = "SELECT * FROM courses WHERE deleted = false;" - const res = await pool.query(sql); + const res: Course[] = await pool.query(sql); return res; } +export async function getCourseByID(id: number): Promise { + const sql = "SELECT * FROM courses WHERE id = ?;" + const res: Course[] = await pool.query(sql, [id]); + return res[0]; +} + function buildAttendee(row: RawAttendeeRow): CourseAttendee { return { - passed: !!row.passed, + passed_bookwork: !!row.passed_bookwork, + passed_qual: !!row.passed_qual, attendee_id: row.attendee_id, course_event_id: row.course_event_id, created_at: row.created_at, @@ -67,6 +74,7 @@ export async function getCourseEventDetails(id: number): Promise(() => {return trainings.value?.find(c => c.id == values.course_id)}) + const trainings = ref(null); const members = ref(null); const eventRoles = ref(null); @@ -108,7 +110,8 @@ onMounted(async () => { class="grid grid-cols-[180px_140px_50px_1fr_auto] gap-3 font-medium text-sm text-muted-foreground px-1">
Member
Role
-
Passed
+
Bookwork
+
Qual
Remarks
@@ -146,7 +149,7 @@ onMounted(async () => { - + + + + diff --git a/ui/src/pages/TrainingReport.vue b/ui/src/pages/TrainingReport.vue index 1809e86..69f42c6 100644 --- a/ui/src/pages/TrainingReport.vue +++ b/ui/src/pages/TrainingReport.vue @@ -13,6 +13,7 @@ import { import { X } from 'lucide-vue-next'; import Button from '@/components/ui/button/Button.vue'; import TrainingReportForm from '@/components/trainingReport/trainingReportForm.vue'; +import Checkbox from '@/components/ui/checkbox/Checkbox.vue'; enum sidePanelState { view, create, closed }; @@ -113,10 +114,15 @@ onMounted(async () => {
- -
+
+

{{ person.attendee_name }}

-

{{ person.passed ? "Passed" : "Failed" }}

+ + + + +

{{ person.remarks }}

@@ -136,7 +142,8 @@ onMounted(async () => {
- +
From 91b915fbcf7015dfbf17d536d3446a76fbce35e5 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Thu, 20 Nov 2025 14:55:43 -0500 Subject: [PATCH 33/46] fixed schema validation to support multi checkbox --- api/src/services/CourseSerivce.ts | 2 +- shared/schemas/trainingReportSchema.ts | 3 +- .../trainingReport/trainingReportForm.vue | 48 +++++++++++++------ 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/CourseSerivce.ts index 5537059..9b23afa 100644 --- a/api/src/services/CourseSerivce.ts +++ b/api/src/services/CourseSerivce.ts @@ -95,7 +95,7 @@ export async function insertCourseEvent(event: CourseEventDetails): Promise { + console.warn("Validation errors:", e); +}, { deep: true }); + + const submitForm = handleSubmit(onSubmit); function toMySQLDateTime(date: Date): string { @@ -55,7 +63,7 @@ function onSubmit(vals) { const { remove, push, fields } = useFieldArray('attendees'); -const selectedCourse = computed(() => {return trainings.value?.find(c => c.id == values.course_id)}) +const selectedCourse = computed(() => { return trainings.value?.find(c => c.id == values.course_id) }) const trainings = ref(null); const members = ref(null); @@ -70,14 +78,15 @@ onMounted(async () => { })