diff --git a/api/src/routes/course.ts b/api/src/routes/course.ts index 22cd023..38bd053 100644 --- a/api/src/routes/course.ts +++ b/api/src/routes/course.ts @@ -26,8 +26,23 @@ courseRouter.get('/roles', async (req, res) => { }) eventRouter.get('/', async (req: Request, res: Response) => { + const allowedSorts = new Map([ + ["ascending", "ASC"], + ["descending", "DESC"] + ]); + + 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); + try { - let events = await getCourseEvents(); + let events = await getCourseEvents(sortDir, search); 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 43d58b1..6b43c32 100644 --- a/api/src/services/CourseSerivce.ts +++ b/api/src/services/CourseSerivce.ts @@ -107,7 +107,18 @@ export async function insertCourseEvent(event: CourseEventDetails): Promise { +export async function getCourseEvents(sortDir: string, search: string = ""): Promise { + + let params = []; + let searchString = ""; + if (search !== "") { + searchString = `WHERE (C.name LIKE ? OR + C.short_name LIKE ? OR + M.name LIKE ?) `; + const p = `%${search}%`; + params.push(p, p, p); + } + const sql = `SELECT E.id AS event_id, E.course_id, @@ -120,8 +131,12 @@ export async function getCourseEvents(): Promise { 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); + ON E.created_by = M.id + ${searchString} + ORDER BY E.event_date ${sortDir};`; + console.log(sql) + console.log(params) + let events: CourseEventSummary[] = await pool.query(sql, params); return events; } diff --git a/ui/src/api/trainingReport.ts b/ui/src/api/trainingReport.ts index 349e9cb..a7a04b3 100644 --- a/ui/src/api/trainingReport.ts +++ b/ui/src/api/trainingReport.ts @@ -3,8 +3,8 @@ import { Course, CourseAttendeeRole, CourseEventDetails, CourseEventSummary } fr //@ts-ignore const addr = import.meta.env.VITE_APIHOST; -export async function getTrainingReports(): Promise { - const res = await fetch(`${addr}/courseEvent`); +export async function getTrainingReports(sortMode: string, search: string): Promise { + const res = await fetch(`${addr}/courseEvent?sort=${sortMode}&search=${search}`); if (res.ok) { return await res.json() as Promise; diff --git a/ui/src/components/ui/select/Select.vue b/ui/src/components/ui/select/Select.vue new file mode 100644 index 0000000..42202c8 --- /dev/null +++ b/ui/src/components/ui/select/Select.vue @@ -0,0 +1,26 @@ + + + diff --git a/ui/src/components/ui/select/SelectContent.vue b/ui/src/components/ui/select/SelectContent.vue new file mode 100644 index 0000000..11ab155 --- /dev/null +++ b/ui/src/components/ui/select/SelectContent.vue @@ -0,0 +1,81 @@ + + + diff --git a/ui/src/components/ui/select/SelectGroup.vue b/ui/src/components/ui/select/SelectGroup.vue new file mode 100644 index 0000000..eecc39b --- /dev/null +++ b/ui/src/components/ui/select/SelectGroup.vue @@ -0,0 +1,14 @@ + + + diff --git a/ui/src/components/ui/select/SelectItem.vue b/ui/src/components/ui/select/SelectItem.vue new file mode 100644 index 0000000..6cebe5b --- /dev/null +++ b/ui/src/components/ui/select/SelectItem.vue @@ -0,0 +1,49 @@ + + + diff --git a/ui/src/components/ui/select/SelectItemText.vue b/ui/src/components/ui/select/SelectItemText.vue new file mode 100644 index 0000000..eb87548 --- /dev/null +++ b/ui/src/components/ui/select/SelectItemText.vue @@ -0,0 +1,14 @@ + + + diff --git a/ui/src/components/ui/select/SelectLabel.vue b/ui/src/components/ui/select/SelectLabel.vue new file mode 100644 index 0000000..1e3c63a --- /dev/null +++ b/ui/src/components/ui/select/SelectLabel.vue @@ -0,0 +1,20 @@ + + + diff --git a/ui/src/components/ui/select/SelectScrollDownButton.vue b/ui/src/components/ui/select/SelectScrollDownButton.vue new file mode 100644 index 0000000..6932136 --- /dev/null +++ b/ui/src/components/ui/select/SelectScrollDownButton.vue @@ -0,0 +1,30 @@ + + + diff --git a/ui/src/components/ui/select/SelectScrollUpButton.vue b/ui/src/components/ui/select/SelectScrollUpButton.vue new file mode 100644 index 0000000..c7a493f --- /dev/null +++ b/ui/src/components/ui/select/SelectScrollUpButton.vue @@ -0,0 +1,30 @@ + + + diff --git a/ui/src/components/ui/select/SelectSeparator.vue b/ui/src/components/ui/select/SelectSeparator.vue new file mode 100644 index 0000000..34f2f59 --- /dev/null +++ b/ui/src/components/ui/select/SelectSeparator.vue @@ -0,0 +1,21 @@ + + + diff --git a/ui/src/components/ui/select/SelectTrigger.vue b/ui/src/components/ui/select/SelectTrigger.vue new file mode 100644 index 0000000..a86506b --- /dev/null +++ b/ui/src/components/ui/select/SelectTrigger.vue @@ -0,0 +1,37 @@ + + + diff --git a/ui/src/components/ui/select/SelectValue.vue b/ui/src/components/ui/select/SelectValue.vue new file mode 100644 index 0000000..db9807f --- /dev/null +++ b/ui/src/components/ui/select/SelectValue.vue @@ -0,0 +1,15 @@ + + + diff --git a/ui/src/components/ui/select/index.js b/ui/src/components/ui/select/index.js new file mode 100644 index 0000000..d911c4e --- /dev/null +++ b/ui/src/components/ui/select/index.js @@ -0,0 +1,11 @@ +export { default as Select } from "./Select.vue"; +export { default as SelectContent } from "./SelectContent.vue"; +export { default as SelectGroup } from "./SelectGroup.vue"; +export { default as SelectItem } from "./SelectItem.vue"; +export { default as SelectItemText } from "./SelectItemText.vue"; +export { default as SelectLabel } from "./SelectLabel.vue"; +export { default as SelectScrollDownButton } from "./SelectScrollDownButton.vue"; +export { default as SelectScrollUpButton } from "./SelectScrollUpButton.vue"; +export { default as SelectSeparator } from "./SelectSeparator.vue"; +export { default as SelectTrigger } from "./SelectTrigger.vue"; +export { default as SelectValue } from "./SelectValue.vue"; diff --git a/ui/src/pages/TrainingReport.vue b/ui/src/pages/TrainingReport.vue index 1f82826..b4b062b 100644 --- a/ui/src/pages/TrainingReport.vue +++ b/ui/src/pages/TrainingReport.vue @@ -10,11 +10,17 @@ import { TableHeader, TableRow, } from '@/components/ui/table' -import { Plus, X } from 'lucide-vue-next'; +import { ArrowUpDown, Funnel, Plus, Search, 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'; import { useRoute, useRouter } from 'vue-router'; +import Select from '@/components/ui/select/Select.vue'; +import SelectTrigger from '@/components/ui/select/SelectTrigger.vue'; +import SelectValue from '@/components/ui/select/SelectValue.vue'; +import SelectContent from '@/components/ui/select/SelectContent.vue'; +import SelectItem from '@/components/ui/select/SelectItem.vue'; +import Input from '@/components/ui/input/Input.vue'; enum sidePanelState { view, create, closed }; @@ -60,8 +66,24 @@ async function closeTrainingReport() { focusedTrainingReport.value = null; } +const sortMode = ref("descending"); +const searchString = ref(""); +let debounceTimer: ReturnType | null = null; + +watch(searchString, (newValue) => { + if (debounceTimer) clearTimeout(debounceTimer); + + debounceTimer = setTimeout(() => { + loadTrainingReports(); + }, 300); // 300ms debounce +}); + +watch(() => sortMode.value, async (newSortMode) => { + loadTrainingReports(); +}) + async function loadTrainingReports() { - trainingReports.value = await getTrainingReports(); + trainingReports.value = await getTrainingReports(sortMode.value, searchString.value); } onMounted(async () => { @@ -69,7 +91,6 @@ onMounted(async () => { if (route.params.id) viewTrainingReport(Number(route.params.id)) loaded.value = true; - console.log("load") }) @@ -77,12 +98,41 @@ onMounted(async () => {
-
+

Training Reports

+ +
+ +
+
+
+ + +
+
+ + +
+
+
@@ -120,12 +170,13 @@ onMounted(async () => {
-

{{ focusedTrainingReport.course_name }}

+

{{ focusedTrainingReport.course_name }} +

{{ focusedTrainingReport.event_date }}

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

@@ -143,7 +194,9 @@ onMounted(async () => {

{{ person.attendee_name }}

{{ person.role.name }}

{{ person.remarks == "" ? '--' + :class="person.remarks == '' ? 'text-muted-foreground' : ''"> + {{ person.remarks == "" ? + '--' : person.remarks }}

@@ -168,7 +221,9 @@ onMounted(async () => { :model-value="person.passed_qual" class="pointer-events-none ml-1">

{{ person.remarks == "" ? '--' + :class="person.remarks == '' ? 'text-muted-foreground' : ''"> + {{ person.remarks == "" ? + '--' : person.remarks }}

@@ -195,7 +250,9 @@ onMounted(async () => {

{{ person.remarks == "" ? '--' + :class="person.remarks == '' ? 'text-muted-foreground' : ''"> + {{ person.remarks == "" ? + '--' : person.remarks }}

@@ -219,7 +276,8 @@ onMounted(async () => {
+ @submit="(newID) => { router.push(`/trainingReport/${newID}`); loadTrainingReports() }"> +