Integrated new member state into manage members page
Implemented suspend/unsuspend
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ export type Member = {
|
||||
status: string | null;
|
||||
status_date: string | null;
|
||||
loa_until?: Date;
|
||||
member_state?: MemberState;
|
||||
};
|
||||
|
||||
export interface MemberLight {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user