Merge pull request 'Update ui/src/components/trainingReport/trainingReportForm.vue' (#36) from training-report-cbox-reset into main

Reviewed-on: #36
This commit was merged in pull request #36.
This commit is contained in:
2025-11-28 16:00:26 -06:00
6 changed files with 68 additions and 37 deletions

View File

@@ -76,6 +76,7 @@ eventRouter.post('/', async (req: Request, res: Response) => {
console.log(); console.log();
let data: CourseEventDetails = req.body; let data: CourseEventDetails = req.body;
data.created_by = posterID; data.created_by = posterID;
data.event_date = new Date(data.event_date);
const id = await insertCourseEvent(data); const id = await insertCourseEvent(data);
res.status(201).json(id); res.status(201).json(id);
} catch (error) { } catch (error) {

View File

@@ -1,8 +1,8 @@
import pool from "../db" import pool from "../db"
import { Course, CourseAttendee, CourseAttendeeRole, CourseEventDetails, CourseEventSummary, RawAttendeeRow } from "@app/shared/types/course" import { Course, CourseAttendee, CourseAttendeeRole, CourseEventDetails, CourseEventSummary, RawAttendeeRow } from "@app/shared/types/course"
import { toDateTime } from "@app/shared/utils/time";
export async function getAllCourses(): Promise<Course[]> { export async function getAllCourses(): Promise<Course[]> {
const sql = "SELECT * FROM courses WHERE deleted = false;" const sql = "SELECT * FROM courses WHERE deleted = false ORDER BY name ASC;"
const res: Course[] = await pool.query(sql); const res: Course[] = await pool.query(sql);
@@ -21,8 +21,8 @@ function buildAttendee(row: RawAttendeeRow): CourseAttendee {
passed_qual: !!row.passed_qual, passed_qual: !!row.passed_qual,
attendee_id: row.attendee_id, attendee_id: row.attendee_id,
course_event_id: row.course_event_id, course_event_id: row.course_event_id,
created_at: row.created_at, created_at: new Date(row.created_at),
updated_at: row.updated_at, updated_at: new Date(row.updated_at),
remarks: row.remarks, remarks: row.remarks,
attendee_role_id: row.attendee_role_id, attendee_role_id: row.attendee_role_id,
attendee_name: row.attendee_name, attendee_name: row.attendee_name,
@@ -32,8 +32,8 @@ function buildAttendee(row: RawAttendeeRow): CourseAttendee {
name: row.role_name, name: row.role_name,
description: row.role_description, description: row.role_description,
deleted: !!row.role_deleted, deleted: !!row.role_deleted,
created_at: row.role_created_at, created_at: new Date(row.role_created_at),
updated_at: row.role_updated_at, updated_at: new Date(row.role_updated_at),
} }
: null : null
}; };
@@ -83,7 +83,7 @@ export async function insertCourseEvent(event: CourseEventDetails): Promise<numb
const con = await pool.getConnection(); const con = await pool.getConnection();
try { try {
await con.beginTransaction(); await con.beginTransaction();
const res = await con.query("INSERT INTO course_events (course_id, event_date, remarks, created_by) VALUES (?, ?, ?, ?);", [event.course_id, event.event_date, event.remarks, event.created_by]); const res = await con.query("INSERT INTO course_events (course_id, event_date, remarks, created_by) VALUES (?, ?, ?, ?);", [event.course_id, toDateTime(event.event_date), event.remarks, event.created_by]);
var eventID: number = res.insertId; var eventID: number = res.insertId;
for (const attendee of event.attendees) { for (const attendee of event.attendees) {

View File

@@ -5,8 +5,8 @@ export interface Course {
category: string; category: string;
description?: string | null; description?: string | null;
image_url?: string | null; image_url?: string | null;
created_at: string; created_at: Date;
updated_at: string; updated_at: Date;
deleted?: number | boolean; deleted?: number | boolean;
prereq_id?: number | null; prereq_id?: number | null;
hasBookwork: boolean; hasBookwork: boolean;
@@ -17,12 +17,12 @@ export interface CourseEventDetails {
id: number | null; // PK id: number | null; // PK
course_id: number | null; // FK → courses.id course_id: number | null; // FK → courses.id
event_type: number | null; // FK → event_types.id event_type: number | null; // FK → event_types.id
event_date: string; // datetime (not nullable) event_date: Date; // datetime (not nullable)
guilded_event_id: number | null; guilded_event_id: number | null;
created_at: string; // datetime created_at: Date; // datetime
updated_at: string; // datetime updated_at: Date; // datetime
deleted: boolean | null; // tinyint(4), nullable deleted: boolean | null; // tinyint(4), nullable
report_url: string | null; // varchar(2048) report_url: string | null; // varchar(2048)
@@ -44,8 +44,8 @@ export interface CourseAttendee {
course_event_id: number; // PK course_event_id: number; // PK
attendee_role_id: number | null; attendee_role_id: number | null;
role: CourseAttendeeRole | null; role: CourseAttendeeRole | null;
created_at: string; // datetime → ISO string created_at: Date; // datetime → ISO string
updated_at: string; // datetime → ISO string updated_at: Date; // datetime → ISO string
remarks: string | null; remarks: string | null;
attendee_name: string | null; attendee_name: string | null;
@@ -55,8 +55,8 @@ export interface CourseAttendeeRole {
id: number; // PK, auto-increment id: number; // PK, auto-increment
name: string | null; // varchar(50), unique, nullable name: string | null; // varchar(50), unique, nullable
description: string | null; // text description: string | null; // text
created_at: string | null; // datetime (nullable) created_at: Date | null; // datetime (nullable)
updated_at: string | null; // datetime (nullable) updated_at: Date | null; // datetime (nullable)
deleted: boolean; // tinyint(4) deleted: boolean; // tinyint(4)
} }

View File

@@ -1,4 +1,5 @@
export function toDateTime(date: Date): string { export function toDateTime(date: Date): string {
console.log(date);
// This produces a CST-local time because server runs in CST // This produces a CST-local time because server runs in CST
const year = date.getFullYear(); const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0"); const month = (date.getMonth() + 1).toString().padStart(2, "0");

View File

@@ -6,6 +6,7 @@ import { toTypedSchema } from '@vee-validate/zod'
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { getAllAttendeeRoles, getAllTrainings, postTrainingReport } from '@/api/trainingReport' import { getAllAttendeeRoles, getAllTrainings, postTrainingReport } from '@/api/trainingReport'
import { getMembers, Member } from '@/api/member' import { getMembers, Member } from '@/api/member'
import FieldGroup from '../ui/field/FieldGroup.vue' import FieldGroup from '../ui/field/FieldGroup.vue'
import Field from '../ui/field/Field.vue' import Field from '../ui/field/Field.vue'
import FieldLabel from '../ui/field/FieldLabel.vue' import FieldLabel from '../ui/field/FieldLabel.vue'
@@ -18,7 +19,8 @@ import FieldLegend from '../ui/field/FieldLegend.vue'
import FieldDescription from '../ui/field/FieldDescription.vue' import FieldDescription from '../ui/field/FieldDescription.vue'
import Checkbox from '../ui/checkbox/Checkbox.vue' import Checkbox from '../ui/checkbox/Checkbox.vue'
const { handleSubmit, resetForm, errors, values } = useForm({
const { handleSubmit, resetForm, errors, values, setFieldValue } = useForm({
validationSchema: toTypedSchema(trainingReportSchema), validationSchema: toTypedSchema(trainingReportSchema),
initialValues: { initialValues: {
course_id: null, course_id: null,
@@ -28,13 +30,22 @@ const { handleSubmit, resetForm, errors, values } = useForm({
} }
}) })
watch(errors, (newErrors) => { // watch(errors, (newErrors) => {
console.log(newErrors) // console.log(newErrors)
}, { deep: true }) // }, { deep: true })
watch(values, (newErrors) => { // watch(values, (newErrors) => {
console.log(newErrors.attendees) // console.log(newErrors.attendees)
}, { deep: true }) // }, { deep: true })
watch(() => values.course_id, (newCourseId, oldCourseId) => {
if (!oldCourseId) return;
values.attendees.forEach((a, index) => {
setFieldValue(`attendees[${index}].passed_bookwork`, false);
setFieldValue(`attendees[${index}].passed_qual`, false);
});
});
const submitForm = handleSubmit(onSubmit); const submitForm = handleSubmit(onSubmit);
@@ -50,13 +61,14 @@ function onSubmit(vals) {
try { try {
const clean: CourseEventDetails = { const clean: CourseEventDetails = {
...vals, ...vals,
event_date: toMySQLDateTime(new Date(vals.event_date)), event_date: new Date(vals.event_date),
} }
postTrainingReport(clean).then((newID) => { postTrainingReport(clean).then((newID) => {
emit("submit", newID); emit("submit", newID);
}); });
} catch (err) { } catch (err) {
console.log("There was an error submitting the training report", err); console.error("There was an error submitting the training report", err);
} }
} }
@@ -123,14 +135,29 @@ onMounted(async () => {
<FieldGroup class="gap-4"> <FieldGroup class="gap-4">
<!-- Column Headers --> <!-- Column Headers -->
<div <div class="relative">
class="grid grid-cols-[180px_155px_65px_41px_1fr_auto] gap-3 font-medium text-sm text-muted-foreground"> <div
<div>Member</div> class="grid grid-cols-[180px_155px_65px_41px_1fr_auto] gap-3 font-medium text-sm text-muted-foreground">
<div>Role</div>
<div>Bookwork</div> <div>Member</div>
<div>Qual</div> <div>Role</div>
<div>Remarks</div>
<div></div> <!-- empty for remove button --> <!-- Bookwork -->
<div class="text-center">Bookwork</div>
<!-- Qual -->
<div class="text-center">Qual</div>
<div>Remarks</div>
<div></div> <!-- empty for remove button -->
</div>
<!-- FLOATING SUPERHEADER -->
<div class="absolute left-[calc(180px+155px+65px/2)] -top-5
w-[106px] text-center text-xs font-medium text-muted-foreground
pointer-events-none">
Pass
</div>
</div> </div>
@@ -178,7 +205,8 @@ onMounted(async () => {
<div class="relative inline-flex items-center group"> <div class="relative inline-flex items-center group">
<Checkbox :disabled="!selectedCourse?.hasBookwork" <Checkbox :disabled="!selectedCourse?.hasBookwork"
:name="`attendees[${index}].passed_bookwork`" v-bind="field"> :name="`attendees[${index}].passed_bookwork`" :model-value="!field.checked"
@update:model-value="field['onUpdate:modelValue']">
</Checkbox> </Checkbox>
<!-- Tooltip bubble --> <!-- Tooltip bubble -->
<div v-if="!selectedCourse?.hasBookwork" class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 <div v-if="!selectedCourse?.hasBookwork" class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2
@@ -201,7 +229,8 @@ onMounted(async () => {
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div class="relative inline-flex items-center group"> <div class="relative inline-flex items-center group">
<Checkbox :disabled="!selectedCourse?.hasQual" <Checkbox :disabled="!selectedCourse?.hasQual"
:name="`attendees[${index}].passed_qual`" v-bind="field"></Checkbox> :name="`attendees[${index}].passed_qual`" :model-value="!field.checked"
@update:model-value="field['onUpdate:modelValue']"></Checkbox>
<!-- Tooltip bubble --> <!-- Tooltip bubble -->
<div v-if="!selectedCourse?.hasQual" class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 <div v-if="!selectedCourse?.hasQual" class="pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2
whitespace-nowrap rounded-md bg-popover px-2 py-1 text-xs whitespace-nowrap rounded-md bg-popover px-2 py-1 text-xs

View File

@@ -151,7 +151,7 @@ onMounted(async () => {
@click="router.push(`/trainingReport/${report.event_id}`)"> @click="router.push(`/trainingReport/${report.event_id}`)">
<TableCell class="font-medium">{{ report.course_name.length > 30 ? report.course_shortname : <TableCell class="font-medium">{{ report.course_name.length > 30 ? report.course_shortname :
report.course_name }}</TableCell> report.course_name }}</TableCell>
<TableCell>{{ report.date }}</TableCell> <TableCell>{{ report.date.split('T')[0] }}</TableCell>
<TableCell class="text-right">{{ report.created_by_name === null ? "Unknown User" : <TableCell class="text-right">{{ report.created_by_name === null ? "Unknown User" :
report.created_by_name report.created_by_name
}}</TableCell> }}</TableCell>
@@ -173,7 +173,7 @@ onMounted(async () => {
<p class="scroll-m-20 text-xl font-semibold tracking-tight">{{ focusedTrainingReport.course_name }} <p class="scroll-m-20 text-xl font-semibold tracking-tight">{{ focusedTrainingReport.course_name }}
</p> </p>
<div class="flex gap-10"> <div class="flex gap-10">
<p class="text-muted-foreground">{{ focusedTrainingReport.event_date }}</p> <p class="text-muted-foreground">{{ focusedTrainingReport.event_date.split('T')[0] }}</p>
<p class="">Created by {{ focusedTrainingReport.created_by_name === null ? "Unknown User" : <p class="">Created by {{ focusedTrainingReport.created_by_name === null ? "Unknown User" :
focusedTrainingReport.created_by_name focusedTrainingReport.created_by_name
}} }}