Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dac4de236b | |||
| 5e1351d033 | |||
| 5f6c17361b | |||
| eca4a75a6e | |||
| 9196a86570 | |||
| 05e6030626 | |||
| d01881f0af | |||
| 905a975327 | |||
| 5659f053ba | |||
| 52b0e3e86d | |||
| e949e32189 | |||
| e9fadc724e |
@@ -56,9 +56,13 @@ router.get("/me", async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
//get my LOA history
|
//get my LOA history
|
||||||
router.get("/history", async (req: Request, res: Response) => {
|
router.get("/history", async (req: Request, res: Response) => {
|
||||||
const user = req.user.id;
|
|
||||||
try {
|
try {
|
||||||
const result = await getUserLOA(user);
|
const user = req.user.id;
|
||||||
|
|
||||||
|
const page = Number(req.query.page) || undefined;
|
||||||
|
const pageSize = Number(req.query.pageSize) || undefined;
|
||||||
|
|
||||||
|
const result = await getUserLOA(user, page, pageSize);
|
||||||
res.status(200).json(result)
|
res.status(200).json(result)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export async function getApplicationByID(appID: number): Promise<ApplicationRow>
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getApplicationList(page: number = 1, pageSize: number = 25): Promise<ApplicationListRow[]> {
|
export async function getApplicationList(page: number = 1, pageSize: number = 25): Promise<ApplicationListRow[]> {
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
const sql = `SELECT
|
const sql = `SELECT
|
||||||
member.name AS member_name,
|
member.name AS member_name,
|
||||||
app.id,
|
app.id,
|
||||||
@@ -44,7 +46,7 @@ export async function getApplicationList(page: number = 1, pageSize: number = 25
|
|||||||
ORDER BY app.submitted_at DESC
|
ORDER BY app.submitted_at DESC
|
||||||
LIMIT ? OFFSET ?;`
|
LIMIT ? OFFSET ?;`
|
||||||
|
|
||||||
const rows: ApplicationListRow[] = await pool.query(sql, [pageSize, page]);
|
const rows: ApplicationListRow[] = await pool.query(sql, [pageSize, offset]);
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ export async function getAllLOA(page = 1, pageSize = 10): Promise<PagedData<LOAR
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserLOA(userId: number): Promise<LOARequest[]> {
|
export async function getUserLOA(userId: number, page = 1, pageSize = 10): Promise<PagedData<LOARequest>> {
|
||||||
|
|
||||||
|
const offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
const result: LOARequest[] = await pool.query(`
|
const result: LOARequest[] = await pool.query(`
|
||||||
SELECT loa.*, members.name, t.name AS type_name
|
SELECT loa.*, members.name, t.name AS type_name
|
||||||
FROM leave_of_absences AS loa
|
FROM leave_of_absences AS loa
|
||||||
@@ -53,8 +56,12 @@ export async function getUserLOA(userId: number): Promise<LOARequest[]> {
|
|||||||
WHEN loa.closed IS NOT NULL THEN 4
|
WHEN loa.closed IS NOT NULL THEN 4
|
||||||
END,
|
END,
|
||||||
loa.start_date DESC
|
loa.start_date DESC
|
||||||
`, [userId])
|
LIMIT ? OFFSET ?;`, [userId, pageSize, offset])
|
||||||
return result;
|
|
||||||
|
let loaCount = Number((await pool.query(`SELECT COUNT(*) as count FROM leave_of_absences WHERE member_id = ?;`, [userId]))[0].count);
|
||||||
|
let pageCount = loaCount / pageSize;
|
||||||
|
let output: PagedData<LOARequest> = { data: result, pagination: { page: page, pageSize: pageSize, total: loaCount, totalPages: pageCount } }
|
||||||
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserActiveLOA(userId: number): Promise<LOARequest[]> {
|
export async function getUserActiveLOA(userId: number): Promise<LOARequest[]> {
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ const environment = import.meta.env.VITE_ENVIRONMENT;
|
|||||||
<p>This is a development build of the application. Some features will be unavailable or unstable.</p>
|
<p>This is a development build of the application. Some features will be unavailable or unstable.</p>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
<Alert v-if="userStore.user?.LOAData?.[0]" class="m-2 mx-auto w-5xl" variant="info">
|
<Alert v-if="userStore.user?.LOAs?.[0]" class="m-2 mx-auto w-5xl" variant="info">
|
||||||
<AlertDescription class="flex flex-row items-center text-nowrap gap-5 mx-auto">
|
<AlertDescription class="flex flex-row items-center text-nowrap gap-5 mx-auto">
|
||||||
<p>You are on LOA until <strong>{{ formatDate(userStore.user?.LOAData?.[0].end_date) }}</strong></p>
|
<p>You are on LOA until <strong>{{ formatDate(userStore.user?.LOAs?.[0].extended_till || userStore.user?.LOAs?.[0].end_date) }}</strong></p>
|
||||||
<Button variant="secondary" @click="async () => { await cancelLOA(userStore.user?.LOAData?.[0].id); userStore.loadUser(); }">End
|
<Button variant="secondary" @click="async () => { await cancelLOA(userStore.user.LOAs?.[0].id); userStore.loadUser(); }">End
|
||||||
LOA</Button>
|
LOA</Button>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ export async function adminSubmitLOA(request: LOARequest): Promise<{ id?: number
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return res.json();
|
return
|
||||||
} else {
|
} else {
|
||||||
return { error: "Failed to submit LOA" };
|
throw new Error("Failed to submit LOA");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +85,18 @@ export async function getAllLOAs(page?: number, pageSize?: number): Promise<Page
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMyLOAs(): Promise<LOARequest[]> {
|
export function getMyLOAs(page?: number, pageSize?: number): Promise<PagedData<LOARequest>> {
|
||||||
return fetch(`${addr}/loa/history`, {
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (page !== undefined) {
|
||||||
|
params.set("page", page.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageSize !== undefined) {
|
||||||
|
params.set("pageSize", pageSize.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(`${addr}/loa/history?${params}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function onSubmit(values: { text: string }, { resetForm }: { resetForm: () => vo
|
|||||||
<!-- Button below, right-aligned -->
|
<!-- Button below, right-aligned -->
|
||||||
<div class="mt-2 flex justify-end gap-2">
|
<div class="mt-2 flex justify-end gap-2">
|
||||||
<Button v-if="adminMode" type="submit" @click="submitMode = 'internal'" variant="outline">Post (Internal)</Button>
|
<Button v-if="adminMode" type="submit" @click="submitMode = 'internal'" variant="outline">Post (Internal)</Button>
|
||||||
<Button type="submit" @click="submitMode = 'public'">Post (Public)</Button>
|
<Button type="submit" @click="submitMode = 'public'">{{ adminMode ? 'Post (Public)' : 'Post' }}</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const regexB = /^https?:\/\/steamcommunity\.com\/profiles\/\d+\/?$/;
|
|||||||
const formSchema = toTypedSchema(z.object({
|
const formSchema = toTypedSchema(z.object({
|
||||||
dob: z.string().refine(v => v, { message: "A date of birth is required." }),
|
dob: z.string().refine(v => v, { message: "A date of birth is required." }),
|
||||||
name: z.string().nonempty(),
|
name: z.string().nonempty(),
|
||||||
playtime: z.coerce.number({ invalid_type_error: "Must be a number", }).min(0, "Cannot be less than 0"),
|
playtime: z.preprocess((v) => (v === "" ? undefined : String(v)),z.string({ required_error: "Required" }).regex(/^\d+(\.\d+)?$/, "Must be a number").transform(Number).refine((n) => n >= 0, "Cannot be less than 0")),
|
||||||
hobbies: z.string().nonempty(),
|
hobbies: z.string().nonempty(),
|
||||||
military: z.boolean(),
|
military: z.boolean(),
|
||||||
communities: z.string().nonempty(),
|
communities: z.string().nonempty(),
|
||||||
|
|||||||
@@ -134,6 +134,22 @@ const maxEndDate = computed(() => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const memberFilter = ref('');
|
||||||
|
|
||||||
|
const filteredMembers = computed(() => {
|
||||||
|
const q = memberFilter?.value?.toLowerCase() ?? ""
|
||||||
|
const results: Member[] = []
|
||||||
|
|
||||||
|
for (const m of members.value ?? []) {
|
||||||
|
if (!q || (m.displayName || m.member_name).toLowerCase().includes(q)) {
|
||||||
|
results.push(m)
|
||||||
|
if (results.length >= 50) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -158,12 +174,13 @@ const maxEndDate = computed(() => {
|
|||||||
:display-value="(id) => {
|
:display-value="(id) => {
|
||||||
const m = members.find(mem => mem.member_id === id)
|
const m = members.find(mem => mem.member_id === id)
|
||||||
return m ? m.displayName || m.member_name : ''
|
return m ? m.displayName || m.member_name : ''
|
||||||
}" />
|
}" @input="memberFilter = $event.target.value" />
|
||||||
</ComboboxAnchor>
|
</ComboboxAnchor>
|
||||||
<ComboboxList class="*:w-64">
|
<ComboboxList class="*:w-64">
|
||||||
<ComboboxEmpty class="text-muted-foreground w-full">No results</ComboboxEmpty>
|
<ComboboxEmpty class="text-muted-foreground w-full">No results</ComboboxEmpty>
|
||||||
<ComboboxGroup>
|
<ComboboxGroup>
|
||||||
<template v-for="member in members" :key="member.member_id">
|
<div class="max-h-80 overflow-y-scroll scrollbar-themed min-w-3xs">
|
||||||
|
<template v-for="member in filteredMembers" :key="member.member_id">
|
||||||
<ComboboxItem :value="member.member_id"
|
<ComboboxItem :value="member.member_id"
|
||||||
class="data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative cursor-pointer select-none px-2 py-1.5 w-full">
|
class="data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative cursor-pointer select-none px-2 py-1.5 w-full">
|
||||||
{{ member.displayName || member.member_name }}
|
{{ member.displayName || member.member_name }}
|
||||||
@@ -173,6 +190,7 @@ const maxEndDate = computed(() => {
|
|||||||
</ComboboxItemIndicator>
|
</ComboboxItemIndicator>
|
||||||
</ComboboxItem>
|
</ComboboxItem>
|
||||||
</template>
|
</template>
|
||||||
|
</div>
|
||||||
</ComboboxGroup>
|
</ComboboxGroup>
|
||||||
</ComboboxList>
|
</ComboboxList>
|
||||||
</Combobox>
|
</Combobox>
|
||||||
@@ -286,8 +304,10 @@ const maxEndDate = computed(() => {
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p class="max-w-md text-muted-foreground">
|
<p class="max-w-md text-muted-foreground">
|
||||||
Your Leave of Absence request has been submitted successfully.
|
{{ adminMode ? 'You have successfully submitted a Leave of Absence on behalf of another member.' :
|
||||||
It will take effect on your selected start date.
|
`Your Leave
|
||||||
|
of Absence request has been submitted successfully.
|
||||||
|
It will take effect on your selected start date.` }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ async function loadLOAs() {
|
|||||||
LOAList.value = result.data;
|
LOAList.value = result.data;
|
||||||
pageData.value = result.pagination;
|
pageData.value = result.pagination;
|
||||||
} else {
|
} else {
|
||||||
LOAList.value = await getMyLOAs();
|
let result = await getMyLOAs(pageNum.value, pageSize.value);
|
||||||
|
LOAList.value = result.data;
|
||||||
|
pageData.value = result.pagination;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ async function handleDeny(id) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!loading" class="w-full h-20">
|
<div v-if="!loading" class="w-full">
|
||||||
<div v-if="unauthorized" class="flex justify-center w-full my-10">
|
<div v-if="unauthorized" class="flex justify-center w-full my-10">
|
||||||
You do not have permission to view this application.
|
You do not have permission to view this application.
|
||||||
</div>
|
</div>
|
||||||
@@ -134,7 +134,7 @@ async function handleDeny(id) {
|
|||||||
<!-- Application header -->
|
<!-- Application header -->
|
||||||
<div>
|
<div>
|
||||||
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight">{{ member_name }}</h3>
|
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight">{{ member_name }}</h3>
|
||||||
<p class="text-muted-foreground">Submitted: {{ submitDate.toLocaleString("en-US", {
|
<p class="text-muted-foreground">Submitted: {{ submitDate?.toLocaleString("en-US", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "long",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
|||||||
Reference in New Issue
Block a user