Wrapped up discharge form close #159
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(
|
||||||
@@ -105,4 +106,14 @@ export async function getPromotionsOnDay(day: Date): Promise<PromotionDetails[]>
|
|||||||
let batchPromotion = await pool.query(sql, [dayString]) as PromotionDetails[];
|
let batchPromotion = await pool.query(sql, [dayString]) as 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
13
api/src/services/db/unitService.ts
Normal file
13
api/src/services/db/unitService.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
shared/schemas/dischargeSchema.ts
Normal file
10
shared/schemas/dischargeSchema.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -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
|
||||||
@@ -13,14 +14,14 @@ export async function getMembers(): Promise<Member[]> {
|
|||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMembersFiltered(params: {
|
export async function getMembersFiltered(params: {
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
unitId?: string;
|
unitId?: string;
|
||||||
} = {}): Promise<PaginatedMembers> {
|
} = {}): Promise<PaginatedMembers> {
|
||||||
|
|
||||||
// Construct the query string dynamically
|
// Construct the query string dynamically
|
||||||
const query = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
if (params.page) query.append('page', params.page.toString());
|
if (params.page) query.append('page', params.page.toString());
|
||||||
@@ -114,4 +115,24 @@ export async function getFullMembers(ids: number[]): Promise<MemberCardDetails[]
|
|||||||
throw new Error("Failed to fetch settings");
|
throw new Error("Failed to fetch settings");
|
||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user