diff --git a/api/src/routes/members.ts b/api/src/routes/members.ts index e21dc5f..0d47c0f 100644 --- a/api/src/routes/members.ts +++ b/api/src/routes/members.ts @@ -5,12 +5,16 @@ 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, getFilteredMembers } from '../services/db/memberService'; +import { getAllMembersLite, getMemberSettings, getMembersFull, getMembersLite, getUserData, getUserState, setUserSettings, getFilteredMembers, setUserState } from '../services/db/memberService'; import { getUserRoles } from '../services/db/rolesService'; import { memberSettings, MemberState, myData } from '@app/shared/types/member'; +import { Discharge } from '@app/shared/schemas/dischargeSchema'; import { Performance } from 'perf_hooks'; import { logger } from '../services/logging/logger'; +import { memberCache } from './auth'; +import { cancelLatestRank } from '../services/db/rankService'; +import { cancelLatestUnit } from '../services/db/unitService'; //get all users router.get('/', [requireLogin, requireMemberState(MemberState.Member)], async (req, res) => { @@ -232,5 +236,32 @@ router.put('/:id/displayname', async (req, res) => { return res.status(501); }); +//discharge member +router.post('/discharge', [requireLogin, requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req: Request, res: Response) => { + try { + var con = await pool.getConnection(); + + con.beginTransaction(); + + var data: Discharge = req.body; + setUserState(data.userID, MemberState.Retired, con); + cancelLatestRank(data.userID, con); + cancelLatestUnit(data.userID, con); + con.commit(); + memberCache.Invalidate(data.userID); + + res.sendStatus(200); + } catch (error) { + logger.error('app', 'Failed to discharge user', { + data: data, + caller: req.user.id, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }) + } finally { + if (con) + con.release(); + } +}); export const memberRouter = router; diff --git a/api/src/services/db/memberService.ts b/api/src/services/db/memberService.ts index 7a31c23..29f8737 100644 --- a/api/src/services/db/memberService.ts +++ b/api/src/services/db/memberService.ts @@ -3,6 +3,7 @@ import pool from "../../db"; import { Member, MemberCardDetails, MemberLight, memberSettings, MemberState, PaginatedMembers } from '@app/shared/types/member' import { logger } from "../logging/logger"; import { memberCache } from "../../routes/auth"; +import * as mariadb from 'mariadb'; export async function getFilteredMembers( page: number = 1, @@ -98,12 +99,12 @@ export async function getUserData(userID: number): Promise { return res[0] ?? null; } -export async function setUserState(userID: number, state: MemberState) { +export async function setUserState(userID: number, state: MemberState, con: mariadb.Pool | mariadb.Connection = pool) { try { const sql = `UPDATE members SET state = ? WHERE id = ?;`; - return await pool.query(sql, [state, userID]); + return await con.query(sql, [state, userID]); } catch (error) { logger.error('app', 'Error setting user state', error); } finally { diff --git a/api/src/services/db/rankService.ts b/api/src/services/db/rankService.ts index 4bffdde..427a088 100644 --- a/api/src/services/db/rankService.ts +++ b/api/src/services/db/rankService.ts @@ -3,6 +3,7 @@ import { PromotionDetails, PromotionSummary } from "@app/shared/types/rank" import pool from "../../db"; import { PagedData } from "@app/shared/types/pagination"; import { toDate, toDateIgnoreZone, toDateTime } from "@app/shared/utils/time"; +import * as mariadb from 'mariadb'; export async function getAllRanks() { const rows = await pool.query( @@ -105,4 +106,14 @@ export async function getPromotionsOnDay(day: Date): Promise let batchPromotion = await pool.query(sql, [dayString]) as PromotionDetails[]; return batchPromotion; +} + +export async function cancelLatestRank(userID: number, con: mariadb.Pool | mariadb.Connection = pool): Promise { + try { + let sql = `CALL sp_end_member_rank(?,NOW())`; + con.query(sql, [userID]); + return true; + } catch (error) { + throw error; + } } \ No newline at end of file diff --git a/api/src/services/db/unitService.ts b/api/src/services/db/unitService.ts new file mode 100644 index 0000000..9d1b1f0 --- /dev/null +++ b/api/src/services/db/unitService.ts @@ -0,0 +1,13 @@ +import pool from "../../db"; +import * as mariadb from 'mariadb'; + + +export async function cancelLatestUnit(userID: number, con: mariadb.Pool | mariadb.Connection = pool): Promise { + try { + let sql = `CALL sp_end_member_unit(?,NOW())`; + con.query(sql, [userID]); + return true; + } catch (error) { + throw error; + } +} \ No newline at end of file diff --git a/shared/schemas/dischargeSchema.ts b/shared/schemas/dischargeSchema.ts new file mode 100644 index 0000000..6b44e01 --- /dev/null +++ b/shared/schemas/dischargeSchema.ts @@ -0,0 +1,10 @@ +import z from "zod"; + +export const dischargeSchema = z.object({ + reason: z.string().min(1, "Please provide a valid reason for discharge").max(200), + // effectiveDate: z.string().min(1, "Date is required"), +}) + +export type Discharge = z.infer & { + userID: number; +}; \ No newline at end of file diff --git a/ui/src/api/member.ts b/ui/src/api/member.ts index cef1c21..9d06a5b 100644 --- a/ui/src/api/member.ts +++ b/ui/src/api/member.ts @@ -1,3 +1,4 @@ +import { Discharge } from "@shared/schemas/dischargeSchema"; import { memberSettings, Member, MemberLight, MemberCardDetails, PaginatedMembers } from "@shared/types/member"; // @ts-ignore @@ -13,14 +14,14 @@ export async function getMembers(): Promise { return response.json(); } -export async function getMembersFiltered(params: { - page?: number; - pageSize?: number; - search?: string; - status?: string; - unitId?: string; +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()); @@ -114,4 +115,24 @@ export async function getFullMembers(ids: number[]): Promise { + const response = await fetch(`${addr}/members/discharge`, { + credentials: 'include', + method: 'POST', + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data) + }); + if (!response.ok) { + throw new Error("Failed to discharge member"); + } + return true; } \ No newline at end of file diff --git a/ui/src/components/members/DischargeMember.vue b/ui/src/components/members/DischargeMember.vue index ba3849d..62f0e9a 100644 --- a/ui/src/components/members/DischargeMember.vue +++ b/ui/src/components/members/DischargeMember.vue @@ -18,8 +18,9 @@ import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import MemberCard from './MemberCard.vue' import { Member } from '@shared/types/member' +import { Discharge, dischargeSchema } from '@shared/schemas/dischargeSchema'; +import { dischargeMember } from '@/api/member' -// 1. Props for control and data const props = defineProps<{ open: boolean member: Member | null @@ -27,18 +28,17 @@ const props = defineProps<{ const emit = defineEmits(['update:open', 'discharged']) -// 2. Discharge-specific schema -const formSchema = toTypedSchema(z.object({ - reason: z.string().min(1, "Please provide a valid reason for discharge").max(200), - effectiveDate: z.string().min(1, "Date is required"), -})) +const formSchema = toTypedSchema(dischargeSchema); -function onSubmit(values: any) { +async function onSubmit(values: z.infer) { + const data: Discharge = { userID: props.member.member_id, reason: values.reason } console.log('Discharging member:', props.member?.member_id) - console.log('Discharge Data:', values) + console.log('Discharge Data:', data) + + await dischargeMember(data); // Notify parent to refresh/close - emit('discharged', { memberId: props.member?.member_id, ...values }) + emit('discharged', { data }) emit('update:open', false) } @@ -62,19 +62,19 @@ function onSubmit(values: any) { Reason for Discharge -