Files
milsim-site-v4/ui/src/components/trainingReport/trainingReportForm.vue

298 lines
15 KiB
Vue

<script setup lang="ts">
import { trainingReportSchema, courseEventAttendeeSchema } from '@shared/schemas/trainingReportSchema'
import { Course, CourseAttendee, CourseAttendeeRole, CourseEventDetails } from '@shared/types/course'
import { useForm, useFieldArray, FieldArray as VeeFieldArray, ErrorMessage, Field as VeeField } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { computed, onMounted, ref, watch } from 'vue'
import { getAllAttendeeRoles, getAllTrainings, postTrainingReport } from '@/api/trainingReport'
import { getMembers, Member } from '@/api/member'
import FieldGroup from '../ui/field/FieldGroup.vue'
import Field from '../ui/field/Field.vue'
import FieldLabel from '../ui/field/FieldLabel.vue'
import FieldError from '../ui/field/FieldError.vue'
import Button from '../ui/button/Button.vue'
import Textarea from '../ui/textarea/Textarea.vue'
import { Plus, X } from 'lucide-vue-next';
import FieldSet from '../ui/field/FieldSet.vue'
import FieldLegend from '../ui/field/FieldLegend.vue'
import FieldDescription from '../ui/field/FieldDescription.vue'
import Checkbox from '../ui/checkbox/Checkbox.vue'
const { handleSubmit, resetForm, errors, values, setFieldValue } = useForm({
validationSchema: toTypedSchema(trainingReportSchema),
initialValues: {
course_id: null,
event_date: "",
remarks: "",
attendees: [],
}
})
// watch(errors, (newErrors) => {
// console.log(newErrors)
// }, { deep: true })
// watch(values, (newErrors) => {
// console.log(newErrors.attendees)
// }, { 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);
function toMySQLDateTime(date: Date): string {
return date
.toISOString() // 2025-11-19T00:00:00.000Z
.slice(0, 23) // keep milliseconds → 2025-11-19T00:00:00.000
.replace("T", " ") + "000"; // becomes → 2025-11-19 00:00:00.000000
}
function onSubmit(vals) {
try {
const clean: CourseEventDetails = {
...vals,
event_date: toMySQLDateTime(new Date(vals.event_date)),
}
postTrainingReport(clean).then((newID) => {
emit("submit", newID);
});
} catch (err) {
console.error("There was an error submitting the training report", err);
}
}
const { remove, push, fields } = useFieldArray('attendees');
const selectedCourse = computed<Course | undefined>(() => { return trainings.value?.find(c => c.id == values.course_id) })
const trainings = ref<Course[] | null>(null);
const members = ref<Member[] | null>(null);
const eventRoles = ref<CourseAttendeeRole[] | null>(null);
const emit = defineEmits(['submit'])
onMounted(async () => {
trainings.value = await getAllTrainings();
members.value = await getMembers();
eventRoles.value = await getAllAttendeeRoles();
})
</script>
<template>
<form id="trainingForm" @submit.prevent="submitForm" class="flex flex-col gap-5">
<div class="flex gap-5">
<div class="flex-1">
<FieldGroup>
<VeeField v-slot="{ field, errors }" name="course_id">
<Field :data-invalid="!!errors.length">
<FieldLabel class="scroll-m-20 text-lg tracking-tight">Training Course</FieldLabel>
<select v-bind="field"
class="h-9 border rounded p-2 w-auto focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] bg-background outline-none">
<option value="" disabled>Select a course</option>
<option v-for="course in trainings" :key="course.id" :value="course.id">
{{ course.name }}
</option>
</select>
<div class="h-4">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</Field>
</VeeField>
</FieldGroup>
</div>
<div class="w-[150px]">
<FieldGroup>
<VeeField v-slot="{ field, errors }" name="event_date">
<Field :data-invalid="!!errors.length">
<FieldLabel class="scroll-m-20 text-lg tracking-tight">Event Date</FieldLabel>
<input type="date" v-bind="field"
class="h-9 border rounded p-2 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] bg-background outline-none" />
<div class="h-4">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</Field>
</VeeField>
</FieldGroup>
</div>
</div>
<VeeFieldArray name="attendees" v-slot="{ fields, push, remove }">
<FieldSet class="gap-4">
<FieldLegend class="scroll-m-20 text-lg tracking-tight">Attendees</FieldLegend>
<FieldDescription>Add members who attended this session.</FieldDescription>
<FieldGroup class="gap-4">
<!-- Column Headers -->
<div class="relative">
<div
class="grid grid-cols-[180px_155px_65px_41px_1fr_auto] gap-3 font-medium text-sm text-muted-foreground">
<div>Member</div>
<div>Role</div>
<!-- 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>
<!-- Attendee Rows -->
<template v-for="(field, index) in fields" :key="field.key">
<div class="grid grid-cols-[180px_160px_50px_50px_1fr_auto] gap-3 items-center">
<!-- Member Select -->
<VeeField :name="`attendees[${index}].attendee_id`" v-slot="{ field: f, errors: e }">
<div>
<select v-bind="f"
class="border rounded p-2 w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] bg-background outline-none">
<option value="">Select member...</option>
<option v-for="m in members" :key="m.member_id" :value="m.member_id">
{{ m.member_name }}
</option>
</select>
<div class="h-4">
<FieldError v-if="e.length" :errors="e" />
</div>
</div>
</VeeField>
<!-- Role Select -->
<VeeField :name="`attendees[${index}].attendee_role_id`" v-slot="{ field: f, errors: e }">
<div>
<select v-bind="f"
class="border rounded p-2 w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] bg-background outline-none">
<option value="">Select role...</option>
<option v-for="r in eventRoles" :key="r.id" :value="r.id">
{{ r.name }}
</option>
</select>
<div class="h-4">
<FieldError v-if="e.length" :errors="e" />
</div>
</div>
</VeeField>
<!-- Bookwork Checkbox -->
<VeeField v-slot="{ field }" :name="`attendees[${index}].passed_bookwork`" type="checkbox"
:value="false" :unchecked-value="true">
<div class="flex flex-col items-center">
<div class="relative inline-flex items-center group">
<Checkbox :disabled="!selectedCourse?.hasBookwork"
:name="`attendees[${index}].passed_bookwork`" :model-value="!field.checked"
@update:model-value="field['onUpdate:modelValue']">
</Checkbox>
<!-- Tooltip bubble -->
<div v-if="!selectedCourse?.hasBookwork" 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
text-popover-foreground shadow-md border border-border
opacity-0 translate-y-1
group-hover:opacity-100 group-hover:translate-y-0
transition-opacity transition-transform duration-150">
This course does not have bookwork
</div>
</div>
<div class="h-4">
</div>
</div>
</VeeField>
<!-- Qual Checkbox -->
<VeeField v-slot="{ field }" :name="`attendees[${index}].passed_qual`" type="checkbox"
:value="false" :unchecked-value="true">
<div class="flex flex-col items-center">
<div class="relative inline-flex items-center group">
<Checkbox :disabled="!selectedCourse?.hasQual"
:name="`attendees[${index}].passed_qual`" :model-value="!field.checked"
@update:model-value="field['onUpdate:modelValue']"></Checkbox>
<!-- Tooltip bubble -->
<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
text-popover-foreground shadow-md border border-border
opacity-0 translate-y-1
group-hover:opacity-100 group-hover:translate-y-0
transition-opacity transition-transform duration-150">
This course does not have a qualification
</div>
</div>
<div class="h-4">
</div>
</div>
</VeeField>
<!-- Remarks -->
<VeeField :name="`attendees[${index}].remarks`" v-slot="{ field: f, errors: e }">
<div class="flex flex-col">
<textarea v-bind="f"
class="h-[38px] resize-none border rounded p-2 w-full focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] bg-background outline-none"
placeholder="Optional remarks"></textarea>
<div class="h-4">
<FieldError v-if="e.length" :errors="e" />
</div>
</div>
</VeeField>
<div>
<!-- Remove button -->
<Button type="button" variant="ghost" size="sm" @click="remove(index)"
class="self-center">
<X :size="10" />
</Button>
<div class="h-4">
</div>
</div>
</div>
</template>
</FieldGroup>
<Button type="button" size="sm" variant="outline"
@click="push({ attendee_id: null, attendee_role_id: null, passed_bookwork: false, passed_qual: false, remarks: '' })">
<Plus class="mr-1 h-4 w-4" />
Add Attendee
</Button>
</FieldSet>
</VeeFieldArray>
<FieldGroup class="pt-3">
<VeeField v-slot="{ field, errors }" name="remarks">
<Field :data-invalid="!!errors.length">
<FieldLabel class="scroll-m-20 text-lg tracking-tight">Remarks</FieldLabel>
<Textarea v-bind="field" placeholder="Any remarks about this training event..."
autocomplete="off" />
<FieldError v-if="errors.length" :errors="errors" />
</Field>
</VeeField>
</FieldGroup>
<div class="flex gap-3 justify-end">
<Button type="button" variant="outline" @click="resetForm">Reset</Button>
<Button type="submit" form="trainingForm">Submit</Button>
</div>
</form>
</template>