From 67562f56aa8490c5826340d4d8129b7b21de0be1 Mon Sep 17 00:00:00 2001 From: ajdj100 Date: Tue, 27 Jan 2026 10:01:41 -0500 Subject: [PATCH] Revived the page! He's baaaaack --- api/src/index.ts | 2 + api/src/routes/members.ts | 23 +- api/src/routes/units.ts | 29 +++ api/src/services/db/memberService.ts | 90 ++++++- shared/types/member.ts | 3 + shared/types/units.ts | 7 + ui/src/api/member.ts | 29 ++- ui/src/api/units.ts | 15 ++ ui/src/pages/memberList.vue | 366 ++++++++++++++++++++------- 9 files changed, 469 insertions(+), 95 deletions(-) create mode 100644 api/src/routes/units.ts create mode 100644 shared/types/units.ts create mode 100644 ui/src/api/units.ts diff --git a/api/src/index.ts b/api/src/index.ts index cfdc719..9a61817 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -102,6 +102,7 @@ import { roles, memberRoles } from './routes/roles'; import { courseRouter, eventRouter } from './routes/course'; import { calendarRouter } from './routes/calendar'; import { docsRouter } from './routes/docs'; +import { units } from './routes/units'; app.use('/application', applicationRouter); app.use('/ranks', ranks); @@ -115,6 +116,7 @@ app.use('/memberRoles', memberRoles) app.use('/course', courseRouter) app.use('/courseEvent', eventRouter) app.use('/calendar', calendarRouter) +app.use('/units', units) app.use('/docs', docsRouter) app.use('/', authRouter) diff --git a/api/src/routes/members.ts b/api/src/routes/members.ts index f7bdd15..e21dc5f 100644 --- a/api/src/routes/members.ts +++ b/api/src/routes/members.ts @@ -5,7 +5,7 @@ import { Request, Response } from 'express'; import pool from '../db'; import { requireLogin, requireMemberState, requireRole } from '../middleware/auth'; import { getUserActiveLOA } from '../services/db/loaService'; -import { getAllMembersLite, getMemberSettings, getMembersFull, getMembersLite, getUserData, getUserState, setUserSettings } from '../services/db/memberService'; +import { getAllMembersLite, getMemberSettings, getMembersFull, getMembersLite, getUserData, getUserState, setUserSettings, getFilteredMembers } from '../services/db/memberService'; import { getUserRoles } from '../services/db/rolesService'; import { memberSettings, MemberState, myData } from '@app/shared/types/member'; @@ -42,6 +42,27 @@ router.get('/', [requireLogin, requireMemberState(MemberState.Member)], async (r } }); +router.get('/filtered', [requireLogin, requireMemberState(MemberState.Member)], async (req, res) => { + try { + // Extract Query Parameters + const page = parseInt(req.query.page as string) || 1; + const pageSize = parseInt(req.query.pageSize as string) || 15; + const search = req.query.search as string | undefined; + const status = req.query.status as string | undefined; + const unitId = req.query.unitId as string | undefined; + + // Call the service function + const result = await getFilteredMembers(page, pageSize, search, status, unitId); + + return res.status(200).json(result); + } catch (error) { + logger.error('app', 'Failed to get filtered users', { + error: error instanceof Error ? error.message : String(error), + }); + return res.status(500).json({ error: 'Failed to fetch users' }); + } +}); + router.get('/me', [requireLogin], async (req: Request, res) => { if (!req.user) return res.sendStatus(401); diff --git a/api/src/routes/units.ts b/api/src/routes/units.ts new file mode 100644 index 0000000..e339a8a --- /dev/null +++ b/api/src/routes/units.ts @@ -0,0 +1,29 @@ +import express = require('express'); +const unitsRouter = express.Router(); + +import pool from '../db'; +import { requireLogin } from '../middleware/auth'; +import { logger } from '../services/logging/logger'; +import { Unit } from '@app/shared/types/units'; + +unitsRouter.use(requireLogin); + +//get all units +unitsRouter.get('/', async (req, res) => { + try { + const result: Unit[] = await pool.query('SELECT * FROM units WHERE active = 1;'); + res.json(result); + } catch (error) { + logger.error( + 'app', + 'Failed to get all units', + { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + } + ); + res.sendStatus(500); + } +}); + +export const units = unitsRouter; diff --git a/api/src/services/db/memberService.ts b/api/src/services/db/memberService.ts index a0776d3..7a31c23 100644 --- a/api/src/services/db/memberService.ts +++ b/api/src/services/db/memberService.ts @@ -1,9 +1,97 @@ import { Role } from "@app/shared/types/roles"; import pool from "../../db"; -import { Member, MemberCardDetails, MemberLight, memberSettings, MemberState } from '@app/shared/types/member' +import { Member, MemberCardDetails, MemberLight, memberSettings, MemberState, PaginatedMembers } from '@app/shared/types/member' import { logger } from "../logging/logger"; import { memberCache } from "../../routes/auth"; +export async function getFilteredMembers( + page: number = 1, + pageSize: number = 15, + search?: string, + status?: string, + unitId?: string +): Promise { + try { + const offset = (page - 1) * pageSize; + const whereClauses: string[] = []; + const params: any[] = []; + + if (status && status !== 'all') { + whereClauses.push(`m.state = ?`); + params.push(status); + } + + if (search) { + whereClauses.push(`v.member_name LIKE ?`); + params.push(`%${search}%`); + } + + if (unitId && unitId !== 'all') { + whereClauses.push(`v.unit = ?`); + params.push(unitId); + } + + const whereClause = whereClauses.length > 0 + ? ` WHERE ${whereClauses.join(' AND ')}` + : ''; + + // COUNT QUERY + const countQuery = `SELECT COUNT(*) as total FROM view_member_rank_unit_status_latest v INNER JOIN members m ON v.member_id = m.id ${whereClause}`; + const [countResults]: any[] = await pool.query(countQuery, params); + const total = Number(countResults?.total) || 0; + + // DATA QUERY + const dataQuery = ` + SELECT + v.*, + CASE + WHEN EXISTS ( + SELECT 1 FROM leave_of_absences l + WHERE l.member_id = v.member_id + AND l.deleted = 0 + AND UTC_TIMESTAMP() BETWEEN l.start_date AND l.end_date + ) THEN 1 ELSE 0 + END AS on_loa + FROM view_member_rank_unit_status_latest v + INNER JOIN members m ON v.member_id = m.id + ${whereClause} -- Added back correctly + ORDER BY v.member_name ASC + LIMIT ? OFFSET ? + `; + + const rows: any[] = await pool.query(dataQuery, [...params, pageSize, offset]); + + // Map rows to Member type + const members: Member[] = rows.map(row => ({ + member_id: Number(row.member_id), + member_name: row.member_name, + displayName: row.displayName, + rank: row.rank, + rank_date: row.rank_date, + unit: row.unit, + unit_date: row.unit_date, + status: row.status, + status_date: row.status_date, + loa_until: row.loa_until ? new Date(row.loa_until) : undefined, + })); + + return { + data: members, + pagination: { + page, + pageSize, + total, + totalPages: Math.ceil(total / pageSize), + }, + }; + } catch (error) { + logger.error('app', 'Error fetching filtered members', { + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } +} + export async function getUserData(userID: number): Promise { const sql = `SELECT * FROM view_member_rank_unit_status_latest WHERE member_id = ?`; const res: Member = await pool.query(sql, [userID]); diff --git a/shared/types/member.ts b/shared/types/member.ts index eea7f85..1cda2dd 100644 --- a/shared/types/member.ts +++ b/shared/types/member.ts @@ -1,10 +1,13 @@ import { LOARequest } from "./loa"; import { Role } from "./roles"; +import { PagedData } from "./pagination"; export interface memberSettings { displayName: string; } +export type PaginatedMembers = PagedData; + export enum MemberState { Guest = "guest", Applicant = "applicant", diff --git a/shared/types/units.ts b/shared/types/units.ts new file mode 100644 index 0000000..11866c6 --- /dev/null +++ b/shared/types/units.ts @@ -0,0 +1,7 @@ +export interface Unit { + id: number; + name: string; + description?: string; + active: boolean; + color?: string; +} \ No newline at end of file diff --git a/ui/src/api/member.ts b/ui/src/api/member.ts index b6aeb47..cef1c21 100644 --- a/ui/src/api/member.ts +++ b/ui/src/api/member.ts @@ -1,4 +1,4 @@ -import { memberSettings, Member, MemberLight, MemberCardDetails } from "@shared/types/member"; +import { memberSettings, Member, MemberLight, MemberCardDetails, PaginatedMembers } from "@shared/types/member"; // @ts-ignore const addr = import.meta.env.VITE_APIHOST; @@ -13,6 +13,33 @@ export async function getMembers(): Promise { return response.json(); } +export async function getMembersFiltered(params: { + page?: number; + pageSize?: number; + search?: string; + status?: string; + unitId?: string; +} = {}): Promise { + + // Construct the query string dynamically + const query = new URLSearchParams(); + if (params.page) query.append('page', params.page.toString()); + if (params.pageSize) query.append('pageSize', params.pageSize.toString()); + if (params.search) query.append('search', params.search); + if (params.status && params.status !== 'all') query.append('status', params.status); + if (params.unitId && params.unitId !== 'all') query.append('unitId', params.unitId); + + const response = await fetch(`${addr}/members/filtered?${query.toString()}`, { + credentials: 'include' + }); + + if (!response.ok) { + throw new Error("Failed to fetch members"); + } + + return response.json(); +} + export async function getMemberSettings(): Promise { const response = await fetch(`${addr}/members/settings`, { credentials: 'include' diff --git a/ui/src/api/units.ts b/ui/src/api/units.ts new file mode 100644 index 0000000..84b0d8c --- /dev/null +++ b/ui/src/api/units.ts @@ -0,0 +1,15 @@ +import { memberSettings, Member, MemberLight, MemberCardDetails } from "@shared/types/member"; +import { Unit } from "@shared/types/units"; + +// @ts-ignore +const addr = import.meta.env.VITE_APIHOST; + +export async function getUnits(): Promise { + const response = await fetch(`${addr}/units`, { + credentials: 'include' + }); + if (!response.ok) { + throw new Error("Failed to fetch units"); + } + return response.json(); +} \ No newline at end of file diff --git a/ui/src/pages/memberList.vue b/ui/src/pages/memberList.vue index 7e11e49..df2a858 100644 --- a/ui/src/pages/memberList.vue +++ b/ui/src/pages/memberList.vue @@ -1,117 +1,299 @@ \ No newline at end of file