Integrated new member state into manage members page

Implemented suspend/unsuspend
This commit is contained in:
2026-02-08 13:54:23 -05:00
parent cf880ed124
commit 921e74f188
5 changed files with 104 additions and 13 deletions

View File

@@ -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;

View File

@@ -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<Member> {
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<MemberState> {
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),
});
}
}

View File

@@ -30,6 +30,7 @@ export type Member = {
status: string | null;
status_date: string | null;
loa_until?: Date;
member_state?: MemberState;
};
export interface MemberLight {

View File

@@ -135,4 +135,26 @@ export async function dischargeMember(data: Discharge): Promise<boolean> {
throw new Error("Failed to discharge member");
}
return true;
}
export async function suspendMember(memberID: number): Promise<boolean> {
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<boolean> {
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;
}

View File

@@ -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 @@
<TableHead class="w-[200px]">Member</TableHead>
<TableHead>Rank</TableHead>
<TableHead>Unit</TableHead>
<TableHead>Status</TableHead>
<TableHead>State</TableHead>
<TableHead></TableHead>
<TableHead class="text-right">Actions</TableHead>
</TableRow>
@@ -238,7 +242,7 @@
<TableCell>{{ member.rank }}</TableCell>
<TableCell>{{ member.unit }}</TableCell>
<TableCell>
<Badge variant="outline" class="capitalize font-normal">{{ member.status }}</Badge>
<Badge variant="outline" class="capitalize font-normal">{{ MemberState[member.member_state] }}</Badge>
</TableCell>
<TableCell>
<Badge v-if="member.loa_until" variant="secondary"
@@ -259,10 +263,14 @@
class="text-destructive focus:bg-destructive focus:text-destructive-foreground font-medium">
Discharge Member
</DropdownMenuItem>
<DropdownMenuItem @click="suspendMember(member)"
<DropdownMenuItem v-if="member.member_state !== MemberState.Suspended" @click="onSuspend(member)"
class="text-destructive focus:bg-destructive focus:text-destructive-foreground font-medium">
Suspend Member
</DropdownMenuItem>
<DropdownMenuItem v-else @click="onUnsuspend(member)"
class="text-destructive focus:bg-destructive focus:text-destructive-foreground font-medium">
Remove Suspension
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>