added searching and sorting system
This commit is contained in:
@@ -26,8 +26,23 @@ courseRouter.get('/roles', async (req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
eventRouter.get('/', async (req: Request, res: Response) => {
|
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 {
|
try {
|
||||||
let events = await getCourseEvents();
|
let events = await getCourseEvents(sortDir, search);
|
||||||
res.status(200).json(events);
|
res.status(200).json(events);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('failed to fetch reports', error);
|
console.error('failed to fetch reports', error);
|
||||||
|
|||||||
@@ -107,7 +107,18 @@ export async function insertCourseEvent(event: CourseEventDetails): Promise<numb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCourseEvents(): Promise<CourseEventSummary[]> {
|
export async function getCourseEvents(sortDir: string, search: string = ""): Promise<CourseEventSummary[]> {
|
||||||
|
|
||||||
|
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
|
const sql = `SELECT
|
||||||
E.id AS event_id,
|
E.id AS event_id,
|
||||||
E.course_id,
|
E.course_id,
|
||||||
@@ -120,8 +131,12 @@ export async function getCourseEvents(): Promise<CourseEventSummary[]> {
|
|||||||
LEFT JOIN courses AS C
|
LEFT JOIN courses AS C
|
||||||
ON E.course_id = C.id
|
ON E.course_id = C.id
|
||||||
LEFT JOIN members AS M
|
LEFT JOIN members AS M
|
||||||
ON E.created_by = M.id;`;
|
ON E.created_by = M.id
|
||||||
let events: CourseEventSummary[] = await pool.query(sql);
|
${searchString}
|
||||||
|
ORDER BY E.event_date ${sortDir};`;
|
||||||
|
console.log(sql)
|
||||||
|
console.log(params)
|
||||||
|
let events: CourseEventSummary[] = await pool.query(sql, params);
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { Course, CourseAttendeeRole, CourseEventDetails, CourseEventSummary } fr
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const addr = import.meta.env.VITE_APIHOST;
|
const addr = import.meta.env.VITE_APIHOST;
|
||||||
|
|
||||||
export async function getTrainingReports(): Promise<CourseEventSummary[]> {
|
export async function getTrainingReports(sortMode: string, search: string): Promise<CourseEventSummary[]> {
|
||||||
const res = await fetch(`${addr}/courseEvent`);
|
const res = await fetch(`${addr}/courseEvent?sort=${sortMode}&search=${search}`);
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return await res.json() as Promise<CourseEventSummary[]>;
|
return await res.json() as Promise<CourseEventSummary[]>;
|
||||||
|
|||||||
26
ui/src/components/ui/select/Select.vue
Normal file
26
ui/src/components/ui/select/Select.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<script setup>
|
||||||
|
import { SelectRoot, useForwardPropsEmits } from "reka-ui";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
open: { type: Boolean, required: false },
|
||||||
|
defaultOpen: { type: Boolean, required: false },
|
||||||
|
defaultValue: { type: null, required: false },
|
||||||
|
modelValue: { type: null, required: false },
|
||||||
|
by: { type: [String, Function], required: false },
|
||||||
|
dir: { type: String, required: false },
|
||||||
|
multiple: { type: Boolean, required: false },
|
||||||
|
autocomplete: { type: String, required: false },
|
||||||
|
disabled: { type: Boolean, required: false },
|
||||||
|
name: { type: String, required: false },
|
||||||
|
required: { type: Boolean, required: false },
|
||||||
|
});
|
||||||
|
const emits = defineEmits(["update:modelValue", "update:open"]);
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectRoot v-slot="slotProps" data-slot="select" v-bind="forwarded">
|
||||||
|
<slot v-bind="slotProps" />
|
||||||
|
</SelectRoot>
|
||||||
|
</template>
|
||||||
81
ui/src/components/ui/select/SelectContent.vue
Normal file
81
ui/src/components/ui/select/SelectContent.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactiveOmit } from "@vueuse/core";
|
||||||
|
import {
|
||||||
|
SelectContent,
|
||||||
|
SelectPortal,
|
||||||
|
SelectViewport,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { SelectScrollDownButton, SelectScrollUpButton } from ".";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
forceMount: { type: Boolean, required: false },
|
||||||
|
position: { type: String, required: false, default: "popper" },
|
||||||
|
bodyLock: { type: Boolean, required: false },
|
||||||
|
side: { type: null, required: false },
|
||||||
|
sideOffset: { type: Number, required: false },
|
||||||
|
sideFlip: { type: Boolean, required: false },
|
||||||
|
align: { type: null, required: false },
|
||||||
|
alignOffset: { type: Number, required: false },
|
||||||
|
alignFlip: { type: Boolean, required: false },
|
||||||
|
avoidCollisions: { type: Boolean, required: false },
|
||||||
|
collisionBoundary: { type: null, required: false },
|
||||||
|
collisionPadding: { type: [Number, Object], required: false },
|
||||||
|
arrowPadding: { type: Number, required: false },
|
||||||
|
sticky: { type: String, required: false },
|
||||||
|
hideWhenDetached: { type: Boolean, required: false },
|
||||||
|
positionStrategy: { type: String, required: false },
|
||||||
|
updatePositionStrategy: { type: String, required: false },
|
||||||
|
disableUpdateOnLayoutShift: { type: Boolean, required: false },
|
||||||
|
prioritizePosition: { type: Boolean, required: false },
|
||||||
|
reference: { type: null, required: false },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
const emits = defineEmits([
|
||||||
|
"closeAutoFocus",
|
||||||
|
"escapeKeyDown",
|
||||||
|
"pointerDownOutside",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class");
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectPortal>
|
||||||
|
<SelectContent
|
||||||
|
data-slot="select-content"
|
||||||
|
v-bind="{ ...$attrs, ...forwarded }"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
||||||
|
position === 'popper' &&
|
||||||
|
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectViewport
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'p-1',
|
||||||
|
position === 'popper' &&
|
||||||
|
'h-[var(--reka-select-trigger-height)] w-full min-w-[var(--reka-select-trigger-width)] scroll-my-1',
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</SelectViewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectContent>
|
||||||
|
</SelectPortal>
|
||||||
|
</template>
|
||||||
14
ui/src/components/ui/select/SelectGroup.vue
Normal file
14
ui/src/components/ui/select/SelectGroup.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup>
|
||||||
|
import { SelectGroup } from "reka-ui";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectGroup data-slot="select-group" v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</SelectGroup>
|
||||||
|
</template>
|
||||||
49
ui/src/components/ui/select/SelectItem.vue
Normal file
49
ui/src/components/ui/select/SelectItem.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactiveOmit } from "@vueuse/core";
|
||||||
|
import { Check } from "lucide-vue-next";
|
||||||
|
import {
|
||||||
|
SelectItem,
|
||||||
|
SelectItemIndicator,
|
||||||
|
SelectItemText,
|
||||||
|
useForwardProps,
|
||||||
|
} from "reka-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
value: { type: null, required: true },
|
||||||
|
disabled: { type: Boolean, required: false },
|
||||||
|
textValue: { type: String, required: false },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class");
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectItem
|
||||||
|
data-slot="select-item"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'focus:bg-accent focus:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span class="absolute right-2 flex size-3.5 items-center justify-center">
|
||||||
|
<SelectItemIndicator>
|
||||||
|
<slot name="indicator-icon">
|
||||||
|
<Check class="size-4" />
|
||||||
|
</slot>
|
||||||
|
</SelectItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SelectItemText>
|
||||||
|
<slot />
|
||||||
|
</SelectItemText>
|
||||||
|
</SelectItem>
|
||||||
|
</template>
|
||||||
14
ui/src/components/ui/select/SelectItemText.vue
Normal file
14
ui/src/components/ui/select/SelectItemText.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup>
|
||||||
|
import { SelectItemText } from "reka-ui";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectItemText data-slot="select-item-text" v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</SelectItemText>
|
||||||
|
</template>
|
||||||
20
ui/src/components/ui/select/SelectLabel.vue
Normal file
20
ui/src/components/ui/select/SelectLabel.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script setup>
|
||||||
|
import { SelectLabel } from "reka-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
for: { type: String, required: false },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectLabel
|
||||||
|
data-slot="select-label"
|
||||||
|
:class="cn('text-muted-foreground px-2 py-1.5 text-xs', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</SelectLabel>
|
||||||
|
</template>
|
||||||
30
ui/src/components/ui/select/SelectScrollDownButton.vue
Normal file
30
ui/src/components/ui/select/SelectScrollDownButton.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactiveOmit } from "@vueuse/core";
|
||||||
|
import { ChevronDown } from "lucide-vue-next";
|
||||||
|
import { SelectScrollDownButton, useForwardProps } from "reka-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class");
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="
|
||||||
|
cn('flex cursor-default items-center justify-center py-1', props.class)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<ChevronDown class="size-4" />
|
||||||
|
</slot>
|
||||||
|
</SelectScrollDownButton>
|
||||||
|
</template>
|
||||||
30
ui/src/components/ui/select/SelectScrollUpButton.vue
Normal file
30
ui/src/components/ui/select/SelectScrollUpButton.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactiveOmit } from "@vueuse/core";
|
||||||
|
import { ChevronUp } from "lucide-vue-next";
|
||||||
|
import { SelectScrollUpButton, useForwardProps } from "reka-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class");
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectScrollUpButton
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="
|
||||||
|
cn('flex cursor-default items-center justify-center py-1', props.class)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<ChevronUp class="size-4" />
|
||||||
|
</slot>
|
||||||
|
</SelectScrollUpButton>
|
||||||
|
</template>
|
||||||
21
ui/src/components/ui/select/SelectSeparator.vue
Normal file
21
ui/src/components/ui/select/SelectSeparator.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactiveOmit } from "@vueuse/core";
|
||||||
|
import { SelectSeparator } from "reka-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectSeparator
|
||||||
|
data-slot="select-separator"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn('bg-border pointer-events-none -mx-1 my-1 h-px', props.class)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
37
ui/src/components/ui/select/SelectTrigger.vue
Normal file
37
ui/src/components/ui/select/SelectTrigger.vue
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactiveOmit } from "@vueuse/core";
|
||||||
|
import { ChevronDown } from "lucide-vue-next";
|
||||||
|
import { SelectIcon, SelectTrigger, useForwardProps } from "reka-ui";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
disabled: { type: Boolean, required: false },
|
||||||
|
reference: { type: null, required: false },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
size: { type: String, required: false, default: "default" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class", "size");
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectTrigger
|
||||||
|
data-slot="select-trigger"
|
||||||
|
:data-size="size"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<SelectIcon as-child>
|
||||||
|
<ChevronDown class="size-4 opacity-50" />
|
||||||
|
</SelectIcon>
|
||||||
|
</SelectTrigger>
|
||||||
|
</template>
|
||||||
15
ui/src/components/ui/select/SelectValue.vue
Normal file
15
ui/src/components/ui/select/SelectValue.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup>
|
||||||
|
import { SelectValue } from "reka-ui";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
placeholder: { type: String, required: false },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SelectValue data-slot="select-value" v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</SelectValue>
|
||||||
|
</template>
|
||||||
11
ui/src/components/ui/select/index.js
Normal file
11
ui/src/components/ui/select/index.js
Normal file
@@ -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";
|
||||||
@@ -10,11 +10,17 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} 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 Button from '@/components/ui/button/Button.vue';
|
||||||
import TrainingReportForm from '@/components/trainingReport/trainingReportForm.vue';
|
import TrainingReportForm from '@/components/trainingReport/trainingReportForm.vue';
|
||||||
import Checkbox from '@/components/ui/checkbox/Checkbox.vue';
|
import Checkbox from '@/components/ui/checkbox/Checkbox.vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
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 };
|
enum sidePanelState { view, create, closed };
|
||||||
|
|
||||||
@@ -60,8 +66,24 @@ async function closeTrainingReport() {
|
|||||||
focusedTrainingReport.value = null;
|
focusedTrainingReport.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortMode = ref<string>("descending");
|
||||||
|
const searchString = ref<string>("");
|
||||||
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
watch(searchString, (newValue) => {
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer);
|
||||||
|
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
loadTrainingReports();
|
||||||
|
}, 300); // 300ms debounce
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => sortMode.value, async (newSortMode) => {
|
||||||
|
loadTrainingReports();
|
||||||
|
})
|
||||||
|
|
||||||
async function loadTrainingReports() {
|
async function loadTrainingReports() {
|
||||||
trainingReports.value = await getTrainingReports();
|
trainingReports.value = await getTrainingReports(sortMode.value, searchString.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -69,7 +91,6 @@ onMounted(async () => {
|
|||||||
if (route.params.id)
|
if (route.params.id)
|
||||||
viewTrainingReport(Number(route.params.id))
|
viewTrainingReport(Number(route.params.id))
|
||||||
loaded.value = true;
|
loaded.value = true;
|
||||||
console.log("load")
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -77,12 +98,41 @@ onMounted(async () => {
|
|||||||
<div class="px-20 mx-auto max-w-[100rem] flex mt-5">
|
<div class="px-20 mx-auto max-w-[100rem] flex mt-5">
|
||||||
<!-- training report list -->
|
<!-- training report list -->
|
||||||
<div class="px-4 my-3" :class="sidePanel == sidePanelState.closed ? 'w-full' : 'w-2/5'">
|
<div class="px-4 my-3" :class="sidePanel == sidePanelState.closed ? 'w-full' : 'w-2/5'">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between mb-4">
|
||||||
<p class="scroll-m-20 text-2xl font-semibold tracking-tight">Training Reports</p>
|
<p class="scroll-m-20 text-2xl font-semibold tracking-tight">Training Reports</p>
|
||||||
<Button @click="router.push('/trainingReport/new')">
|
<Button @click="router.push('/trainingReport/new')">
|
||||||
<Plus></Plus> New Training Report
|
<Plus></Plus> New Training Report
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- search/filter -->
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<!-- <Search></Search>
|
||||||
|
<Funnel></Funnel> -->
|
||||||
|
<div></div>
|
||||||
|
<div class="flex flex-row gap-5">
|
||||||
|
<div>
|
||||||
|
<label class="text-muted-foreground">Search</label>
|
||||||
|
<Input v-model="searchString"></Input>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<label class="text-muted-foreground">Sort By</label>
|
||||||
|
<Select v-model="sortMode">
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Sort By" />
|
||||||
|
<!-- <ArrowUpDown></ArrowUpDown> -->
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ascending">
|
||||||
|
Date (Ascending)
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="descending">
|
||||||
|
Date (Descending)
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="overflow-y-auto max-h-[70vh] mt-5 scrollbar-themed">
|
<div class="overflow-y-auto max-h-[70vh] mt-5 scrollbar-themed">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader class="">
|
<TableHeader class="">
|
||||||
@@ -120,12 +170,13 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="max-h-[70vh] overflow-auto scrollbar-themed my-5">
|
<div class="max-h-[70vh] overflow-auto scrollbar-themed my-5">
|
||||||
<div class="flex flex-col mb-5 border rounded-lg bg-muted/70 p-2 py-3 px-4">
|
<div class="flex flex-col mb-5 border rounded-lg bg-muted/70 p-2 py-3 px-4">
|
||||||
<p class="scroll-m-20 text-xl font-semibold tracking-tight">{{ focusedTrainingReport.course_name }}</p>
|
<p class="scroll-m-20 text-xl font-semibold tracking-tight">{{ focusedTrainingReport.course_name }}
|
||||||
|
</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 }}</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
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -143,7 +194,9 @@ onMounted(async () => {
|
|||||||
<p>{{ person.attendee_name }}</p>
|
<p>{{ person.attendee_name }}</p>
|
||||||
<p class="">{{ person.role.name }}</p>
|
<p class="">{{ person.role.name }}</p>
|
||||||
<p class="col-span-2 text-right px-2"
|
<p class="col-span-2 text-right px-2"
|
||||||
:class="person.remarks == '' ? 'text-muted-foreground' : ''">{{ person.remarks == "" ? '--'
|
:class="person.remarks == '' ? 'text-muted-foreground' : ''">
|
||||||
|
{{ person.remarks == "" ?
|
||||||
|
'--'
|
||||||
: person.remarks }}</p>
|
: person.remarks }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,7 +221,9 @@ onMounted(async () => {
|
|||||||
:model-value="person.passed_qual" class="pointer-events-none ml-1">
|
:model-value="person.passed_qual" class="pointer-events-none ml-1">
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<p class="col-span-2 text-right px-2"
|
<p class="col-span-2 text-right px-2"
|
||||||
:class="person.remarks == '' ? 'text-muted-foreground' : ''">{{ person.remarks == "" ? '--'
|
:class="person.remarks == '' ? 'text-muted-foreground' : ''">
|
||||||
|
{{ person.remarks == "" ?
|
||||||
|
'--'
|
||||||
: person.remarks }}</p>
|
: person.remarks }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,7 +250,9 @@ onMounted(async () => {
|
|||||||
<div></div>
|
<div></div>
|
||||||
<div></div>
|
<div></div>
|
||||||
<p class="col-span-2 text-right px-2"
|
<p class="col-span-2 text-right px-2"
|
||||||
:class="person.remarks == '' ? 'text-muted-foreground' : ''">{{ person.remarks == "" ? '--'
|
:class="person.remarks == '' ? 'text-muted-foreground' : ''">
|
||||||
|
{{ person.remarks == "" ?
|
||||||
|
'--'
|
||||||
: person.remarks }}</p>
|
: person.remarks }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,7 +276,8 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="overflow-y-auto max-h-[70vh] mt-5 scrollbar-themed">
|
<div class="overflow-y-auto max-h-[70vh] mt-5 scrollbar-themed">
|
||||||
<TrainingReportForm class="w-full pl-2"
|
<TrainingReportForm class="w-full pl-2"
|
||||||
@submit="(newID) => { router.push(`/trainingReport/${newID}`); loadTrainingReports() }"></TrainingReportForm>
|
@submit="(newID) => { router.push(`/trainingReport/${newID}`); loadTrainingReports() }">
|
||||||
|
</TrainingReportForm>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user