Added "approved by" system
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user