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 pool from '../db';
|
||||||
import { requireLogin, requireMemberState, requireRole } from '../middleware/auth';
|
import { requireLogin, requireMemberState, requireRole } from '../middleware/auth';
|
||||||
import { getUserActiveLOA } from '../services/db/loaService';
|
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 { getUserRoles } from '../services/db/rolesService';
|
||||||
import { memberSettings, MemberState, myData } from '@app/shared/types/member';
|
import { memberSettings, MemberState, myData } from '@app/shared/types/member';
|
||||||
import { Discharge } from '@app/shared/schemas/dischargeSchema';
|
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),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
stack: error instanceof Error ? error.stack : undefined,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
})
|
})
|
||||||
|
res.sendStatus(500);
|
||||||
} finally {
|
} finally {
|
||||||
if (con)
|
if (con)
|
||||||
con.release();
|
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;
|
export const memberRouter = router;
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export async function getFilteredMembers(
|
|||||||
status: row.status,
|
status: row.status,
|
||||||
status_date: row.status_date,
|
status_date: row.status_date,
|
||||||
loa_until: row.loa_until ? new Date(row.loa_until) : undefined,
|
loa_until: row.loa_until ? new Date(row.loa_until) : undefined,
|
||||||
|
member_state: row.member_state
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -99,26 +100,28 @@ export async function getUserData(userID: number): Promise<Member> {
|
|||||||
return res[0] ?? null;
|
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;
|
const isInternalConn = !externalCon;
|
||||||
if(isInternalConn)
|
if (isInternalConn)
|
||||||
var con = await pool.getConnection();
|
var con = await pool.getConnection();
|
||||||
else
|
else
|
||||||
var con = externalCon;
|
var con = externalCon;
|
||||||
// const con = (externalCon || await pool.getConnection()) as mariadb.PoolConnection;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isInternalConn) await con.beginTransaction();
|
if (isInternalConn) await con.beginTransaction();
|
||||||
|
|
||||||
await endLatestMemberState(userID, con);
|
if (endPrevious)
|
||||||
|
await endLatestMemberState(userID, con);
|
||||||
|
|
||||||
const sql = `UPDATE members SET state = ? WHERE id = ?;`;
|
const sql = `UPDATE members SET state = ? WHERE id = ?;`;
|
||||||
await con.query(sql, [state, userID]);
|
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)
|
(member_id, state_id, reason, created_by_id, start_date, end_date)
|
||||||
VALUES (?, ?, ?, ?, NOW(), NULL);`;
|
VALUES (?, ?, ?, ?, NOW(), NULL);`;
|
||||||
await con.query(insertHistorySql, [userID, state, reason, creatorID]);
|
await con.query(insertHistorySql, [userID, state, reason, creatorID]);
|
||||||
|
}
|
||||||
|
|
||||||
if (isInternalConn) await con.commit();
|
if (isInternalConn) await con.commit();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -259,4 +262,23 @@ export async function endLatestMemberState(memberID: number, con: mariadb.Pool |
|
|||||||
}
|
}
|
||||||
// let res = await pool.query(sql, [memberID]);
|
// let res = await pool.query(sql, [memberID]);
|
||||||
// console.log(res);
|
// 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: string | null;
|
||||||
status_date: string | null;
|
status_date: string | null;
|
||||||
loa_until?: Date;
|
loa_until?: Date;
|
||||||
|
member_state?: MemberState;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface MemberLight {
|
export interface MemberLight {
|
||||||
|
|||||||
@@ -135,4 +135,26 @@ export async function dischargeMember(data: Discharge): Promise<boolean> {
|
|||||||
throw new Error("Failed to discharge member");
|
throw new Error("Failed to discharge member");
|
||||||
}
|
}
|
||||||
return true;
|
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'
|
} from '@/components/ui/pagination'
|
||||||
|
|
||||||
// API & Types
|
// API & Types
|
||||||
import { getMembersFiltered } from "@/api/member";
|
import { getMembersFiltered, suspendMember, unsuspendMember } from "@/api/member";
|
||||||
import { getUnits } from "@/api/units";
|
import { getUnits } from "@/api/units";
|
||||||
import type { Member } from "@shared/types/member";
|
import type { Member } from "@shared/types/member";
|
||||||
import { MemberState } from "@shared/types/member";
|
import { MemberState } from "@shared/types/member";
|
||||||
@@ -145,8 +145,12 @@
|
|||||||
isDischargeOpen.value = true
|
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) {
|
function handleDischargeSuccess(data) {
|
||||||
@@ -222,7 +226,7 @@
|
|||||||
<TableHead class="w-[200px]">Member</TableHead>
|
<TableHead class="w-[200px]">Member</TableHead>
|
||||||
<TableHead>Rank</TableHead>
|
<TableHead>Rank</TableHead>
|
||||||
<TableHead>Unit</TableHead>
|
<TableHead>Unit</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>State</TableHead>
|
||||||
<TableHead></TableHead>
|
<TableHead></TableHead>
|
||||||
<TableHead class="text-right">Actions</TableHead>
|
<TableHead class="text-right">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -238,7 +242,7 @@
|
|||||||
<TableCell>{{ member.rank }}</TableCell>
|
<TableCell>{{ member.rank }}</TableCell>
|
||||||
<TableCell>{{ member.unit }}</TableCell>
|
<TableCell>{{ member.unit }}</TableCell>
|
||||||
<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>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge v-if="member.loa_until" variant="secondary"
|
<Badge v-if="member.loa_until" variant="secondary"
|
||||||
@@ -259,10 +263,14 @@
|
|||||||
class="text-destructive focus:bg-destructive focus:text-destructive-foreground font-medium">
|
class="text-destructive focus:bg-destructive focus:text-destructive-foreground font-medium">
|
||||||
Discharge Member
|
Discharge Member
|
||||||
</DropdownMenuItem>
|
</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">
|
class="text-destructive focus:bg-destructive focus:text-destructive-foreground font-medium">
|
||||||
Suspend Member
|
Suspend Member
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem v-else @click="onUnsuspend(member)"
|
||||||
|
class="text-destructive focus:bg-destructive focus:text-destructive-foreground font-medium">
|
||||||
|
Remove Suspension
|
||||||
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
Reference in New Issue
Block a user