Added "approved by" system

This commit is contained in:
2026-01-16 16:26:20 -05:00
parent 1c1358f9d0
commit 19eb2be252
4 changed files with 56 additions and 23 deletions

View File

@@ -16,10 +16,11 @@ ur.use(requireLogin)
ur.post('/', [requireRole(["17th Command", "17th Administrator", "17th HQ"]), requireMemberState(MemberState.Member)], async (req: express.Request, res: express.Response) => { ur.post('/', [requireRole(["17th Command", "17th Administrator", "17th HQ"]), requireMemberState(MemberState.Member)], async (req: express.Request, res: express.Response) => {
try { try {
const change = req.body.promotions as BatchPromotionMember[]; const change = req.body.promotions as BatchPromotionMember[];
const approver = req.body.approver as number;
const author = req.user.id; const author = req.user.id;
if (!change) res.sendStatus(400); if (!change) res.sendStatus(400);
await batchInsertMemberRank(change, author); await batchInsertMemberRank(change, author, approver);
logger.info('app', 'Promotion batch submitted', { author: author }) logger.info('app', 'Promotion batch submitted', { author: author })
res.sendStatus(201); res.sendStatus(201);
} catch (error) { } catch (error) {

View File

@@ -36,11 +36,11 @@ export async function insertMemberRank(member_id: number, rank_id: number, date?
} }
export async function batchInsertMemberRank(promos: BatchPromotionMember[], author: number) { export async function batchInsertMemberRank(promos: BatchPromotionMember[], author: number, approver: number) {
try { try {
var con = await pool.getConnection(); var con = await pool.getConnection();
promos.forEach(p => { promos.forEach(p => {
con.query(`CALL sp_update_member_rank(?, ?, ?, ?, ?, ?)`, [p.member_id, p.rank_id, author, author, "Rank Change", toDateIgnoreZone(new Date(p.start_date))]) con.query(`CALL sp_update_member_rank(?, ?, ?, ?, ?, ?)`, [p.member_id, p.rank_id, author, approver, "Rank Change", toDateIgnoreZone(new Date(p.start_date))])
}); });
con.commit(); con.commit();

View File

@@ -10,7 +10,7 @@ export const batchPromotionMemberSchema = z.object({
export const batchPromotionSchema = z.object({ export const batchPromotionSchema = z.object({
promotions: z.array(batchPromotionMemberSchema, { message: "At least one promotion is required" }).nonempty({ message: "At least one promotion is required" }), promotions: z.array(batchPromotionMemberSchema, { message: "At least one promotion is required" }).nonempty({ message: "At least one promotion is required" }),
approver: z.number({ invalid_type_error: "Must select a member" }).int().positive()
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
// optional: check for duplicate member_ids // optional: check for duplicate member_ids

View File

@@ -26,7 +26,7 @@ import { error } from 'console';
import Input from '../ui/input/Input.vue'; import Input from '../ui/input/Input.vue';
import Field from '../ui/field/Field.vue'; import Field from '../ui/field/Field.vue';
const { handleSubmit, errors, values, resetForm, setFieldValue } = useForm({ const { handleSubmit, errors, values, resetForm, setFieldValue, submitCount } = useForm({
validationSchema: toTypedSchema(batchPromotionSchema), validationSchema: toTypedSchema(batchPromotionSchema),
validateOnMount: false, validateOnMount: false,
}) })
@@ -123,21 +123,15 @@ function setAllToday() {
<template> <template>
<div class="w-xl"> <div class="w-xl">
<form v-if="!formSubmitted" id="trainingForm" @submit.prevent="submitForm" <form v-if="!formSubmitted" id="trainingForm" @submit.prevent="submitForm"
class="w-full min-w-0 flex flex-col gap-6"> class="w-full min-w-0 flex flex-col gap-4">
<VeeFieldArray name="promotions" v-slot="{ fields, push, remove }">
<FieldSet class="w-full min-w-0">
<div class="flex flex-col gap-2">
<div> <div>
<FieldLegend class="scroll-m-20 text-2xl font-semibold tracking-tight"> <FieldLegend class="scroll-m-20 text-2xl font-semibold tracking-tight">
Promotion Form Promotion Form
</FieldLegend> </FieldLegend>
<div class="h-6">
<p v-if="errors.promotions && typeof errors.promotions === 'string'"
class="text-sm text-red-500">
{{ errors.promotions }}
</p>
</div>
</div> </div>
<VeeFieldArray name="promotions" v-slot="{ fields, push, remove }">
<FieldSet class="w-full min-w-0">
<div class="flex flex-col gap-2">
<!-- TABLE SHELL --> <!-- TABLE SHELL -->
<div class=""> <div class="">
<FieldGroup class=""> <FieldGroup class="">
@@ -168,8 +162,7 @@ function setAllToday() {
:display-value="id => :display-value="id =>
memberById.get(id)?.displayName || memberById.get(id)?.displayName ||
memberById.get(id)?.username memberById.get(id)?.username
" @input="memberSearch = $event.target.value" " @input="memberSearch = $event.target.value" />
/>
</ComboboxAnchor> </ComboboxAnchor>
<ComboboxList> <ComboboxList>
<ComboboxEmpty>No results</ComboboxEmpty> <ComboboxEmpty>No results</ComboboxEmpty>
@@ -250,9 +243,48 @@ function setAllToday() {
</div> </div>
</FieldSet> </FieldSet>
</VeeFieldArray> </VeeFieldArray>
<div class="flex justify-end items-center gap-5"> <div class="flex justify-between items-start">
<VeeField name="approver" v-slot="{ field, errors }">
<div class="flex flex-col min-w-0 gap-2">
<p>Approved By</p>
<Combobox :model-value="field.value" @update:model-value="field.onChange" :ignore-filter="true">
<ComboboxAnchor>
<ComboboxInput class="w-full pl-3" placeholder="Search members" :display-value="id =>
memberById.get(id)?.displayName ||
memberById.get(id)?.username
" @input="memberSearch = $event.target.value" />
</ComboboxAnchor>
<ComboboxList>
<ComboboxEmpty>No results</ComboboxEmpty>
<ComboboxGroup>
<div class="max-h-80 overflow-y-auto min-w-[12rem] scrollbar-themed">
<ComboboxItem v-for="member in filteredMembers" :key="member.id"
:value="member.id">
{{ member.displayName || member.username }}
<ComboboxItemIndicator>
<Check />
</ComboboxItemIndicator>
</ComboboxItem>
</div>
</ComboboxGroup>
</ComboboxList>
</Combobox>
<div class="h-5">
<FieldError v-if="errors.length" :errors="errors" />
</div>
</div>
</VeeField>
<div class="flex flex-col items-end gap-2">
<div class="h-6" />
<Button type="submit" class="w-min">Submit</Button>
<p v-if="submitError" class="text-destructive">{{ submitError }}</p> <p v-if="submitError" class="text-destructive">{{ submitError }}</p>
<Button type="submit">Submit</Button> <div v-else class="h-6 flex justify-end">
<p v-if="submitCount > 0 && errors.promotions && typeof errors.promotions === 'string'"
class="text-sm text-red-500">
{{ errors.promotions }}
</p>
</div>
</div>
</div> </div>
</form> </form>
<div v-else> <div v-else>