From 9e469013c733748e45d161f12f7213b843062818 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Tue, 16 Dec 2025 00:23:59 -0500 Subject: [PATCH] implemented pagination for training reports --- api/src/routes/course.ts | 35 +++++++------ api/src/services/CourseSerivce.ts | 26 +++++++--- ui/src/api/trainingReport.ts | 27 ++++++++-- ui/src/pages/TrainingReport.vue | 82 ++++++++++++++++++++++++++----- 4 files changed, 132 insertions(+), 38 deletions(-) diff --git a/api/src/routes/course.ts b/api/src/routes/course.ts index f04ea08..7b10bdd 100644 --- a/api/src/routes/course.ts +++ b/api/src/routes/course.ts @@ -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); diff --git a/api/src/services/CourseSerivce.ts b/api/src/services/CourseSerivce.ts index f365da6..8b2f61df 100644 --- a/api/src/services/CourseSerivce.ts +++ b/api/src/services/CourseSerivce.ts @@ -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 { const sql = "SELECT * FROM courses WHERE deleted = false ORDER BY name ASC;" @@ -107,7 +108,8 @@ export async function insertCourseEvent(event: CourseEventDetails): Promise { +export async function getCourseEvents(sortDir: string, search: string = "", page = 1, pageSize = 10): Promise> { + 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 = { data: events, pagination: { page: page, pageSize: pageSize, total: recordCount, totalPages: pageCount } } + + return output; } export async function getCourseEventRoles(): Promise { diff --git a/ui/src/api/trainingReport.ts b/ui/src/api/trainingReport.ts index c1b8adc..a261b7b 100644 --- a/ui/src/api/trainingReport.ts +++ b/ui/src/api/trainingReport.ts @@ -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 { - const res = await fetch(`${addr}/courseEvent?sort=${sortMode}&search=${search}`, { +export async function getTrainingReports(sortMode?: string, search?: string, page?: number, pageSize?: number): Promise> { + 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; + return await res.json() as Promise>; } 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 export async function getAllTrainings(): Promise { const res = await fetch(`${addr}/course`, { - credentials: 'include', + credentials: 'include', }); if (res.ok) { diff --git a/ui/src/pages/TrainingReport.vue b/ui/src/pages/TrainingReport.vue index 4131ace..d321ef3 100644 --- a/ui/src/pages/TrainingReport.vue +++ b/ui/src/pages/TrainingReport.vue @@ -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(1); +const pageData = ref(); + +const pageSize = ref(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(); +}