diff --git a/api/src/routes/members.ts b/api/src/routes/members.ts index fd78f62..af12870 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, getFilteredMembers, setUserState } from '../services/db/memberService'; +import { getAllMembersLite, getMemberSettings, getMembersFull, getMembersLite, getUserData, getUserState, setUserSettings, getFilteredMembers, setUserState, getLastNonSuspendedState } 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'; @@ -259,10 +259,48 @@ router.post('/discharge', [requireLogin, requireMemberState(MemberState.Member), error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }) + res.sendStatus(500); } finally { if (con) con.release(); } }); +//suspend member +router.post('/suspend', [requireLogin, requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req: Request, res: Response) => { + let author = req.user.id; + let target = Number(req.query.target); + try { + await setUserState(target, MemberState.Suspended, "Member Suspended", author, null); + res.sendStatus(200); + } catch (error) { + logger.error('app', 'Failed to suspend user', { + target: target, + caller: req.user.id, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }) + res.sendStatus(500); + } +}) + +//unsuspend member +router.post('/unsuspend', [requireLogin, requireMemberState(MemberState.Member), requireRole("17th Administrator")], async (req: Request, res: Response) => { + let author = req.user.id; + let target = Number(req.query.target); + try { + let prevState = await getLastNonSuspendedState(target); + await setUserState(target, prevState, "Member Suspension Removed", author, null); + res.sendStatus(200); + } catch (error) { + logger.error('app', 'Failed to suspend user', { + target: target, + caller: req.user.id, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }) + res.sendStatus(500); + } +}) + export const memberRouter = router; diff --git a/api/src/services/db/memberService.ts b/api/src/services/db/memberService.ts index cf4afc7..69c564b 100644 --- a/api/src/services/db/memberService.ts +++ b/api/src/services/db/memberService.ts @@ -74,6 +74,7 @@ export async function getFilteredMembers( status: row.status, status_date: row.status_date, loa_until: row.loa_until ? new Date(row.loa_until) : undefined, + member_state: row.member_state })); return { @@ -99,26 +100,28 @@ export async function getUserData(userID: number): Promise { return res[0] ?? null; } -export async function setUserState(userID: number, state: MemberState, reason: string, creatorID: number, externalCon?: mariadb.PoolConnection) { +export async function setUserState(userID: number, state: MemberState, reason: string, creatorID: number, externalCon?: mariadb.PoolConnection, endPrevious: boolean = true, createHistory: boolean = true) { const isInternalConn = !externalCon; - if(isInternalConn) + if (isInternalConn) var con = await pool.getConnection(); - else + else var con = externalCon; - // const con = (externalCon || await pool.getConnection()) as mariadb.PoolConnection; try { if (isInternalConn) await con.beginTransaction(); - await endLatestMemberState(userID, con); + if (endPrevious) + await endLatestMemberState(userID, con); const sql = `UPDATE members SET state = ? WHERE id = ?;`; await con.query(sql, [state, userID]); - const insertHistorySql = `INSERT INTO member_state_history + if (createHistory) { + const insertHistorySql = `INSERT INTO member_state_history (member_id, state_id, reason, created_by_id, start_date, end_date) VALUES (?, ?, ?, ?, NOW(), NULL);`; - await con.query(insertHistorySql, [userID, state, reason, creatorID]); + await con.query(insertHistorySql, [userID, state, reason, creatorID]); + } if (isInternalConn) await con.commit(); } catch (error) { @@ -259,4 +262,23 @@ export async function endLatestMemberState(memberID: number, con: mariadb.Pool | } // let res = await pool.query(sql, [memberID]); // console.log(res); +} + +export async function getLastNonSuspendedState(memberID: number): Promise { + try { + const sql = `SELECT state_id + FROM member_state_history + WHERE member_id = ? + AND state_id != ? + ORDER BY start_date DESC, id DESC + LIMIT 1;` + const res = await pool.query(sql, [memberID, MemberState.Suspended]); + console.log(res as MemberState[]) + if (res.length) + return res[0].state_id as MemberState; + } catch (error) { + logger.error('app', 'Error ending latest member state', { + error: error instanceof Error ? error.message : String(error), + }); + } } \ No newline at end of file diff --git a/shared/types/member.ts b/shared/types/member.ts index 699f3b6..233968d 100644 --- a/shared/types/member.ts +++ b/shared/types/member.ts @@ -30,6 +30,7 @@ export type Member = { status: string | null; status_date: string | null; loa_until?: Date; + member_state?: MemberState; }; export interface MemberLight { diff --git a/ui/src/api/member.ts b/ui/src/api/member.ts index b5107c8..de9a754 100644 --- a/ui/src/api/member.ts +++ b/ui/src/api/member.ts @@ -135,4 +135,26 @@ export async function dischargeMember(data: Discharge): Promise { throw new Error("Failed to discharge member"); } return true; +} + +export async function suspendMember(memberID: number): Promise { + const response = await fetch(`${addr}/members/suspend?target=${memberID}`, { + credentials: 'include', + method: 'POST', + }); + if (!response.ok) { + throw new Error("Failed to discharge member"); + } + return true; +} + +export async function unsuspendMember(memberID: number): Promise { + const response = await fetch(`${addr}/members/unsuspend?target=${memberID}`, { + credentials: 'include', + method: 'POST', + }); + if (!response.ok) { + throw new Error("Failed to discharge member"); + } + return true; } \ No newline at end of file diff --git a/ui/src/pages/memberList.vue b/ui/src/pages/memberList.vue index cb54aaa..763d3e6 100644 --- a/ui/src/pages/memberList.vue +++ b/ui/src/pages/memberList.vue @@ -15,7 +15,7 @@ } from '@/components/ui/pagination' // API & Types - import { getMembersFiltered } from "@/api/member"; + import { getMembersFiltered, suspendMember, unsuspendMember } from "@/api/member"; import { getUnits } from "@/api/units"; import type { Member } from "@shared/types/member"; import { MemberState } from "@shared/types/member"; @@ -145,8 +145,12 @@ isDischargeOpen.value = true } - function suspendMember(member: Member) { + async function onSuspend(member: Member) { + await suspendMember(member.member_id); + } + async function onUnsuspend(member: Member) { + await unsuspendMember(member.member_id); } function handleDischargeSuccess(data) { @@ -222,7 +226,7 @@ Member Rank Unit - Status + State Actions @@ -238,7 +242,7 @@ {{ member.rank }} {{ member.unit }} - {{ member.status }} + {{ MemberState[member.member_state] }} Discharge Member - Suspend Member + + Remove Suspension +