Wrapped up discharge form close #159

This commit is contained in:
2026-01-28 15:39:45 -05:00
parent c646254616
commit 22eaba6f90
8 changed files with 111 additions and 25 deletions

View File

@@ -5,12 +5,16 @@ 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 } 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 { 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 { Performance } from 'perf_hooks'; import { Performance } from 'perf_hooks';
import { logger } from '../services/logging/logger'; 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 //get all users
router.get('/', [requireLogin, requireMemberState(MemberState.Member)], async (req, res) => { router.get('/', [requireLogin, requireMemberState(MemberState.Member)], async (req, res) => {
@@ -232,5 +236,32 @@ router.put('/:id/displayname', async (req, res) => {
return res.status(501); 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; export const memberRouter = router;

View File

@@ -3,6 +3,7 @@ import pool from "../../db";
import { Member, MemberCardDetails, MemberLight, memberSettings, MemberState, PaginatedMembers } from '@app/shared/types/member' import { Member, MemberCardDetails, MemberLight, memberSettings, MemberState, PaginatedMembers } from '@app/shared/types/member'
import { logger } from "../logging/logger"; import { logger } from "../logging/logger";
import { memberCache } from "../../routes/auth"; import { memberCache } from "../../routes/auth";
import * as mariadb from 'mariadb';
export async function getFilteredMembers( export async function getFilteredMembers(
page: number = 1, page: number = 1,
@@ -98,12 +99,12 @@ export async function getUserData(userID: number): Promise<Member> {
return res[0] ?? null; 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 { try {
const sql = `UPDATE members const sql = `UPDATE members
SET state = ? SET state = ?
WHERE id = ?;`; WHERE id = ?;`;
return await pool.query(sql, [state, userID]); return await con.query(sql, [state, userID]);
} catch (error) { } catch (error) {
logger.error('app', 'Error setting user state', error); logger.error('app', 'Error setting user state', error);
} finally { } finally {

View File

@@ -3,6 +3,7 @@ import { PromotionDetails, PromotionSummary } from "@app/shared/types/rank"
import pool from "../../db"; import pool from "../../db";
import { PagedData } from "@app/shared/types/pagination"; import { PagedData } from "@app/shared/types/pagination";
import { toDate, toDateIgnoreZone, toDateTime } from "@app/shared/utils/time"; import { toDate, toDateIgnoreZone, toDateTime } from "@app/shared/utils/time";
import * as mariadb from 'mariadb';
export async function getAllRanks() { export async function getAllRanks() {
const rows = await pool.query( const rows = await pool.query(
@@ -106,3 +107,13 @@ export async function getPromotionsOnDay(day: Date): Promise<PromotionDetails[]>
return batchPromotion; return batchPromotion;
} }
export async function cancelLatestRank(userID: number, con: mariadb.Pool | mariadb.Connection = pool): Promise<boolean> {
try {
let sql = `CALL sp_end_member_rank(?,NOW())`;
con.query(sql, [userID]);
return true;
} catch (error) {
throw error;
}
}

View File

@@ -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<boolean> {
try {
let sql = `CALL sp_end_member_unit(?,NOW())`;
con.query(sql, [userID]);
return true;
} catch (error) {
throw error;
}
}

View File

@@ -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<typeof dischargeSchema> & {
userID: number;
};

View File

@@ -1,3 +1,4 @@
import { Discharge } from "@shared/schemas/dischargeSchema";
import { memberSettings, Member, MemberLight, MemberCardDetails, PaginatedMembers } from "@shared/types/member"; import { memberSettings, Member, MemberLight, MemberCardDetails, PaginatedMembers } from "@shared/types/member";
// @ts-ignore // @ts-ignore
@@ -115,3 +116,23 @@ export async function getFullMembers(ids: number[]): Promise<MemberCardDetails[]
} }
return response.json(); return response.json();
} }
/**
* Requests for the given member to be discharged
* @param data discharge packet
* @returns true on success
*/
export async function dischargeMember(data: Discharge): Promise<boolean> {
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;
}

View File

@@ -18,8 +18,9 @@ import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import MemberCard from './MemberCard.vue' import MemberCard from './MemberCard.vue'
import { Member } from '@shared/types/member' 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<{ const props = defineProps<{
open: boolean open: boolean
member: Member | null member: Member | null
@@ -27,18 +28,17 @@ const props = defineProps<{
const emit = defineEmits(['update:open', 'discharged']) const emit = defineEmits(['update:open', 'discharged'])
// 2. Discharge-specific schema const formSchema = toTypedSchema(dischargeSchema);
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"),
}))
function onSubmit(values: any) { async function onSubmit(values: z.infer<typeof dischargeSchema>) {
const data: Discharge = { userID: props.member.member_id, reason: values.reason }
console.log('Discharging member:', props.member?.member_id) 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 // Notify parent to refresh/close
emit('discharged', { memberId: props.member?.member_id, ...values }) emit('discharged', { data })
emit('update:open', false) emit('update:open', false)
} }
</script> </script>
@@ -62,19 +62,19 @@ function onSubmit(values: any) {
<VeeField v-slot="{ componentField, errors }" name="reason"> <VeeField v-slot="{ componentField, errors }" name="reason">
<Field :data-invalid="!!errors.length"> <Field :data-invalid="!!errors.length">
<FieldLabel>Reason for Discharge</FieldLabel> <FieldLabel>Reason for Discharge</FieldLabel>
<Textarea placeholder="Retirement, inactivity, etc. " <Textarea placeholder="Retirement, inactivity, etc. " v-bind="componentField"
v-bind="componentField" class="resize-none" /> class="resize-none" />
<FieldError v-if="errors.length" :errors="errors" /> <FieldError v-if="errors.length" :errors="errors" />
</Field> </Field>
</VeeField> </VeeField>
<VeeField v-slot="{ componentField, errors }" name="effectiveDate"> <!-- <VeeField v-slot="{ componentField, errors }" name="effectiveDate">
<Field :data-invalid="!!errors.length"> <Field :data-invalid="!!errors.length">
<FieldLabel>Effective Date</FieldLabel> <FieldLabel>Effective Date</FieldLabel>
<Input type="date" v-bind="componentField" /> <Input type="date" v-bind="componentField" />
<FieldError v-if="errors.length" :errors="errors" /> <FieldError v-if="errors.length" :errors="errors" />
</Field> </Field>
</VeeField> </VeeField> -->
</form> </form>
</Form> </Form>

View File

@@ -140,8 +140,7 @@ function openDischargeModal(member) {
} }
function handleDischargeSuccess(data) { function handleDischargeSuccess(data) {
// Refresh your list or show a toast fetchMembers();
console.log('Member removed from list:', data.memberId)
} }
</script> </script>