implemented pagination for training reports
This commit is contained in:
@@ -33,23 +33,26 @@ cr.get('/roles', async (req, res) => {
|
||||
})
|
||||
|
||||
er.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(sortDir, search);
|
||||
const allowedSorts = new Map([
|
||||
["ascending", "ASC"],
|
||||
["descending", "DESC"]
|
||||
]);
|
||||
|
||||
const page = Number(req.query.page) || undefined;
|
||||
const pageSize = Number(req.query.pageSize) || undefined;
|
||||
|
||||
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);
|
||||
|
||||
let events = await getCourseEvents(sortDir, search, page, pageSize);
|
||||
res.status(200).json(events);
|
||||
} catch (error) {
|
||||
console.error('failed to fetch reports', error);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import pool from "../db"
|
||||
import { Course, CourseAttendee, CourseAttendeeRole, CourseEventDetails, CourseEventSummary, RawAttendeeRow } from "@app/shared/types/course"
|
||||
import { PagedData } from "@app/shared/types/pagination";
|
||||
import { toDateTime } from "@app/shared/utils/time";
|
||||
export async function getAllCourses(): Promise<Course[]> {
|
||||
const sql = "SELECT * FROM courses WHERE deleted = false ORDER BY name ASC;"
|
||||
@@ -107,7 +108,8 @@ export async function insertCourseEvent(event: CourseEventDetails): Promise<numb
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCourseEvents(sortDir: string, search: string = ""): Promise<CourseEventSummary[]> {
|
||||
export async function getCourseEvents(sortDir: string, search: string = "", page = 1, pageSize = 10): Promise<PagedData<CourseEventSummary>> {
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
let params = [];
|
||||
let searchString = "";
|
||||
@@ -133,11 +135,23 @@ export async function getCourseEvents(sortDir: string, search: string = ""): Pro
|
||||
LEFT JOIN members AS M
|
||||
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;
|
||||
ORDER BY E.event_date ${sortDir}
|
||||
LIMIT ? OFFSET ?;`;
|
||||
|
||||
let countSQL = `SELECT COUNT(*) AS count
|
||||
FROM course_events AS E
|
||||
LEFT JOIN courses AS C
|
||||
ON E.course_id = C.id
|
||||
LEFT JOIN members AS M
|
||||
ON E.created_by = M.id ${searchString};`
|
||||
let recordCount = Number((await pool.query(countSQL, [...params]))[0].count);
|
||||
let pageCount = recordCount / pageSize;
|
||||
|
||||
let events: CourseEventSummary[] = await pool.query(sql, [...params, pageSize, offset]);
|
||||
|
||||
let output: PagedData<CourseEventSummary> = { data: events, pagination: { page: page, pageSize: pageSize, total: recordCount, totalPages: pageCount } }
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export async function getCourseEventRoles(): Promise<CourseAttendeeRole[]> {
|
||||
|
||||
@@ -1,15 +1,34 @@
|
||||
import { Course, CourseAttendeeRole, CourseEventDetails, CourseEventSummary } from '@shared/types/course'
|
||||
import { PagedData } from '@shared/types/pagination';
|
||||
|
||||
//@ts-ignore
|
||||
const addr = import.meta.env.VITE_APIHOST;
|
||||
|
||||
export async function getTrainingReports(sortMode: string, search: string): Promise<CourseEventSummary[]> {
|
||||
const res = await fetch(`${addr}/courseEvent?sort=${sortMode}&search=${search}`, {
|
||||
export async function getTrainingReports(sortMode?: string, search?: string, page?: number, pageSize?: number): Promise<PagedData<CourseEventSummary>> {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (page !== undefined) {
|
||||
params.set("page", page.toString());
|
||||
}
|
||||
|
||||
if (pageSize !== undefined) {
|
||||
params.set("pageSize", pageSize.toString());
|
||||
}
|
||||
|
||||
if (sortMode !== undefined) {
|
||||
params.set("sort", sortMode.toString());
|
||||
}
|
||||
|
||||
if (search !== undefined || search !== "") {
|
||||
params.set("search", search.toString());
|
||||
}
|
||||
|
||||
const res = await fetch(`${addr}/courseEvent?${params}`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
return await res.json() as Promise<CourseEventSummary[]>;
|
||||
return await res.json() as Promise<PagedData<CourseEventSummary>>;
|
||||
} else {
|
||||
console.error("Something went wrong");
|
||||
throw new Error("Failed to load training reports");
|
||||
@@ -31,7 +50,7 @@ export async function getTrainingReport(id: number): Promise<CourseEventDetails>
|
||||
|
||||
export async function getAllTrainings(): Promise<Course[]> {
|
||||
const res = await fetch(`${addr}/course`, {
|
||||
credentials: 'include',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
|
||||
@@ -23,6 +23,15 @@ import SelectItem from '@/components/ui/select/SelectItem.vue';
|
||||
import Input from '@/components/ui/input/Input.vue';
|
||||
import MemberCard from '@/components/members/MemberCard.vue';
|
||||
import Spinner from '@/components/ui/spinner/Spinner.vue';
|
||||
import { pagination } from '@shared/types/pagination';
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from '@/components/ui/pagination'
|
||||
|
||||
enum sidePanelState { view, create, closed };
|
||||
|
||||
@@ -87,17 +96,36 @@ watch(() => sortMode.value, async (newSortMode) => {
|
||||
})
|
||||
|
||||
async function loadTrainingReports() {
|
||||
trainingReports.value = await getTrainingReports(sortMode.value, searchString.value);
|
||||
let data = await getTrainingReports(sortMode.value, searchString.value, pageNum.value, pageSize.value);
|
||||
trainingReports.value = data.data;
|
||||
pageData.value = data.pagination;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loadTrainingReports();
|
||||
await loadTrainingReports();
|
||||
if (route.params.id)
|
||||
viewTrainingReport(Number(route.params.id))
|
||||
loaded.value = true;
|
||||
})
|
||||
|
||||
const TRLoaded = ref(false);
|
||||
|
||||
const pageNum = ref<number>(1);
|
||||
const pageData = ref<pagination>();
|
||||
|
||||
const pageSize = ref<number>(15)
|
||||
const pageSizeOptions = [10, 15, 30]
|
||||
|
||||
function setPageSize(size: number) {
|
||||
pageSize.value = size
|
||||
pageNum.value = 1;
|
||||
loadTrainingReports();
|
||||
}
|
||||
|
||||
function setPage(pagenum: number) {
|
||||
pageNum.value = pagenum;
|
||||
loadTrainingReports();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -118,7 +146,7 @@ const TRLoaded = ref(false);
|
||||
<div class="flex flex-row gap-5">
|
||||
<div>
|
||||
<label class="text-muted-foreground">Search</label>
|
||||
<Input v-model="searchString"></Input>
|
||||
<Input v-model="searchString" placeholder="Search..."></Input>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<label class="text-muted-foreground">Sort By</label>
|
||||
@@ -169,14 +197,44 @@ const TRLoaded = ref(false);
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-between" :class="sidePanel !== sidePanelState.closed ? 'flex-col items-center' : 'items-center justify-between'"
|
||||
>
|
||||
<div></div>
|
||||
<Pagination v-slot="{ page }" :items-per-page="pageData?.pageSize || 10" :total="pageData?.total || 10"
|
||||
:default-page="2" :page="pageNum" @update:page="setPage">
|
||||
<PaginationContent v-slot="{ items }">
|
||||
<PaginationPrevious />
|
||||
<template v-for="(item, index) in items" :key="index">
|
||||
<PaginationItem v-if="item.type === 'page'" :value="item.value"
|
||||
:is-active="item.value === page">
|
||||
{{ item.value }}
|
||||
</PaginationItem>
|
||||
</template>
|
||||
<PaginationEllipsis :index="4" />
|
||||
<PaginationNext />
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
<div class="flex items-center gap-3 text-sm" :class="sidePanel !== sidePanelState.closed ? 'mt-3' : ''" >
|
||||
<p class="text-muted-foreground text-nowrap">Per page:</p>
|
||||
|
||||
<button v-for="size in pageSizeOptions" :key="size" @click="setPageSize(size)"
|
||||
class="px-2 py-1 rounded transition-colors" :class="{
|
||||
'bg-muted font-semibold': pageSize === size,
|
||||
'hover:bg-muted/50': pageSize !== size
|
||||
}">
|
||||
{{ size }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- view training report section -->
|
||||
<div v-if="focusedTrainingReport != null && sidePanel == sidePanelState.view" class="pl-9 my-3 border-l w-3/5">
|
||||
<div class="flex justify-between">
|
||||
<div class="flex justify-between items-center">
|
||||
<p class="scroll-m-20 text-2xl font-semibold tracking-tight">Training Report Details</p>
|
||||
<button @click="closeTrainingReport" class="cursor-pointer">
|
||||
<X></X>
|
||||
</button>
|
||||
<Button @click="closeTrainingReport" class="cursor-pointer" variant="ghost" size="icon">
|
||||
<X class="size-6"></X>
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="TRLoaded" 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">
|
||||
@@ -189,7 +247,7 @@ const TRLoaded = ref(false);
|
||||
:member-id="focusedTrainingReport.created_by" />
|
||||
<p v-else>{{ focusedTrainingReport.created_by_name === null ? "Unknown User" :
|
||||
focusedTrainingReport.created_by_name
|
||||
}}</p>
|
||||
}}</p>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -294,13 +352,13 @@ const TRLoaded = ref(false);
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="sidePanel == sidePanelState.create" class="pl-7 border-l w-3/5 max-w-5xl">
|
||||
<div class="flex justify-between my-3">
|
||||
<div class="flex justify-between items-center my-3">
|
||||
<div class="flex pl-2 gap-5">
|
||||
<p class="scroll-m-20 text-2xl font-semibold tracking-tight">New Training Report</p>
|
||||
</div>
|
||||
<button @click="closeTrainingReport" class="cursor-pointer">
|
||||
<X></X>
|
||||
</button>
|
||||
<Button @click="closeTrainingReport" class="cursor-pointer" variant="ghost" size="icon">
|
||||
<X class="size-6"></X>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="overflow-y-auto max-h-[70vh] mt-5 scrollbar-themed">
|
||||
<TrainingReportForm class="w-full pl-2"
|
||||
|
||||
Reference in New Issue
Block a user