Training-Report #27
@@ -1,5 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const courseEventAttendeeSchema = z.object({
|
||||
attendee_id: z.number().int().positive(),
|
||||
passed: z.boolean(),
|
||||
remarks: z.string(),
|
||||
attendee_role_id: z.number().int().positive()
|
||||
})
|
||||
|
||||
export const trainingReportSchema = z.object({
|
||||
id: z.number().int().positive().optional(),
|
||||
course_id: z.number().int(),
|
||||
@@ -10,11 +17,6 @@ export const trainingReportSchema = z.object({
|
||||
"event_date must be a valid ISO date string"
|
||||
),
|
||||
remarks: z.string().nullable().optional(),
|
||||
attendees: z.array(courseEventAttendeeSchema).default([]),
|
||||
})
|
||||
|
||||
export const courseEventAttendeeSchema = z.object({
|
||||
attendee_id: z.number().int().positive(),
|
||||
passed: z.boolean(),
|
||||
remarks: z.string(),
|
||||
attendee_role_id: z.number().int().positive()
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CourseEventDetails, CourseEventSummary } from '@shared/types/course'
|
||||
import { Course, CourseEventDetails, CourseEventSummary } from '@shared/types/course'
|
||||
|
||||
//@ts-ignore
|
||||
const addr = import.meta.env.VITE_APIHOST;
|
||||
@@ -23,4 +23,15 @@ export async function getTrainingReport(id: number): Promise<CourseEventDetails>
|
||||
console.error("Something went wrong");
|
||||
throw new Error("Failed to load training reports");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllTrainings(): Promise<Course[]> {
|
||||
const res = await fetch(`${addr}/course`);
|
||||
|
||||
if (res.ok) {
|
||||
return await res.json() as Promise<Course[]>;
|
||||
} else {
|
||||
console.error("Something went wrong");
|
||||
throw new Error("Failed to load training list");
|
||||
}
|
||||
}
|
||||
84
ui/src/components/trainingReport/trainingReportForm.vue
Normal file
84
ui/src/components/trainingReport/trainingReportForm.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { trainingReportSchema, courseEventAttendeeSchema } from '@shared/schemas/trainingReportSchema'
|
||||
import { Course } from '@shared/types/course'
|
||||
import { useForm, useFieldArray } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from '@/components/ui/form'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getAllTrainings } from '@/api/trainingReport'
|
||||
import { Member } from '@/api/member'
|
||||
|
||||
console.log(trainingReportSchema instanceof z.ZodType)
|
||||
|
||||
const form = useForm({ validationSchema: toTypedSchema(trainingReportSchema) });
|
||||
|
||||
const { fields: attendeeFields, push, remove } = useFieldArray({
|
||||
name: 'attendees',
|
||||
})
|
||||
function onSubmit(vals) {
|
||||
console.log(vals);
|
||||
}
|
||||
|
||||
import z from 'zod'
|
||||
const schema = z.object({ x: z.string() })
|
||||
const typed = toTypedSchema(schema)
|
||||
console.log(typed)
|
||||
|
||||
|
||||
const trainings = ref<Course[] | null>(null);
|
||||
const members = ref<Member[] | null>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
trainings.value = await getAllTrainings();
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<form @submit="form.handleSubmit(onSubmit)">
|
||||
|
||||
<!-- Training report fields here -->
|
||||
|
||||
<div class="my-6">
|
||||
<h2 class="font-semibold text-lg mb-4">Attendees</h2>
|
||||
|
||||
<div v-for="(field, idx) in attendeeFields" :key="field.key" class="mb-4 p-4 border rounded">
|
||||
|
||||
<!-- attendee_id -->
|
||||
<FormField name="attendees[idx].attendee_id" v-slot="{ componentField }">
|
||||
<FormItem>
|
||||
<FormLabel>Attendee ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- passed -->
|
||||
<FormField name="attendees[idx].passed" v-slot="{ componentField }">
|
||||
<FormItem>
|
||||
<FormLabel>Passed</FormLabel>
|
||||
<FormControl>
|
||||
<Checkbox v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<Button variant="destructive" @click.prevent="remove(idx)">Remove</Button>
|
||||
</div>
|
||||
|
||||
<Button @click.prevent="push({ attendee_id: null, attendee_role_id: null, passed: false, remarks: null })">
|
||||
Add Attendee
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button type="submit">Submit Report</Button>
|
||||
</form>
|
||||
</template>
|
||||
@@ -1,21 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { getTrainingReport, getTrainingReports } from '@/api/trainingReport';
|
||||
import { trainingReportSchema, courseEventAttendeeSchema } from '@shared/schemas/trainingReportSchema'
|
||||
import { CourseAttendee, CourseEventDetails, CourseEventSummary } from '@shared/types/course';
|
||||
import { toTypedSchema } from '@vee-validate/zod';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { X } from 'lucide-vue-next';
|
||||
import Button from '@/components/ui/button/Button.vue';
|
||||
import TrainingReportForm from '@/components/trainingReport/trainingReportForm.vue';
|
||||
|
||||
enum sidePanelState { view, create, closed };
|
||||
|
||||
const trainingReports = ref<CourseEventSummary[] | null>(null);
|
||||
const loaded = ref(false);
|
||||
@@ -31,10 +30,18 @@ const focusedTrainingTrainers = computed<CourseAttendee[] | null>(() => {
|
||||
})
|
||||
async function viewTrainingReport(id: number) {
|
||||
focusedTrainingReport.value = await getTrainingReport(id);
|
||||
sidePanel.value = sidePanelState.view;
|
||||
}
|
||||
|
||||
async function closeTrainingReport() {
|
||||
focusedTrainingReport.value = null;
|
||||
sidePanel.value = sidePanelState.closed;
|
||||
}
|
||||
|
||||
const sidePanel = ref<sidePanelState>(sidePanelState.closed);
|
||||
|
||||
function createTrainingReport() {
|
||||
sidePanel.value = sidePanelState.create;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -47,8 +54,9 @@ onMounted(async () => {
|
||||
<div class="max-w-7xl mx-auto flex mt-5">
|
||||
<!-- training report list -->
|
||||
<div class="px-4" :class="focusedTrainingReport == null ? 'w-full' : 'w-1/2'">
|
||||
<div>
|
||||
<div class="flex justify-between">
|
||||
<p class="scroll-m-20 text-2xl font-semibold tracking-tight">Training Reports</p>
|
||||
<Button @click="createTrainingReport">New Training Report</Button>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -74,13 +82,13 @@ onMounted(async () => {
|
||||
</Table>
|
||||
</div>
|
||||
<!-- view training report section -->
|
||||
<div v-if="focusedTrainingReport != null" class="px-4 border-l w-1/2">
|
||||
<div v-if="sidePanel == sidePanelState.view" class="px-4 border-l w-1/2">
|
||||
<div class="flex justify-between my-3">
|
||||
<div class="flex gap-5">
|
||||
<p>{{ focusedTrainingReport.course_name }}</p>
|
||||
<p class="text-muted-foreground">{{ focusedTrainingReport.event_date }}</p>
|
||||
</div>
|
||||
<button @click="closeTrainingReport">
|
||||
<button @click="closeTrainingReport" class="cursor-pointer">
|
||||
<X></X>
|
||||
</button>
|
||||
</div>
|
||||
@@ -108,5 +116,8 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="sidePanel == sidePanelState.create" class="px-4 border-l w-1/2">
|
||||
<TrainingReportForm></TrainingReportForm>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -15,7 +15,8 @@ export default defineConfig({
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@shared': fileURLToPath(new URL('../shared', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
|
||||
Reference in New Issue
Block a user