added policy system and self LOA management

This commit is contained in:
2025-12-11 20:28:49 -05:00
parent bcde81093d
commit 710b24e5a7
8 changed files with 170 additions and 23 deletions

View File

@@ -53,6 +53,18 @@ router.get("/me", async (req: Request, res: Response) => {
}
})
//get my LOA history
router.get("/history", async (req: Request, res: Response) => {
const user = req.user.id;
try {
const result = await getUserLOA(user);
res.status(200).json(result)
} catch (error) {
console.error(error);
res.status(500).send(error);
}
})
router.get('/all', async (req, res) => {
try {
const result = await getAllLOA();
@@ -128,7 +140,7 @@ router.get('/policy', async (req: Request, res: Response) => {
if (output.ok) {
const out = await output.json();
res.status(200).json(out.markdown);
res.status(200).json(out.html);
} else {
console.log("BAD");
res.sendStatus(500);

View File

@@ -32,7 +32,23 @@ export async function getAllLOA(page = 1, pageSize = 20): Promise<LOARequest[]>
}
export async function getUserLOA(userId: number): Promise<LOARequest[]> {
const result: LOARequest[] = await pool.query("SELECT * FROM leave_of_absences WHERE member_id = ?", [userId])
const result: LOARequest[] = await pool.query(`
SELECT loa.*, members.name, t.name AS type_name
FROM leave_of_absences AS loa
LEFT JOIN members ON loa.member_id = members.id
LEFT JOIN leave_of_absences_types AS t ON loa.type_id = t.id
WHERE member_id = ?
ORDER BY
CASE
WHEN loa.closed IS NULL
AND NOW() > COALESCE(loa.extended_till, loa.end_date) THEN 1
WHEN loa.closed IS NULL
AND NOW() BETWEEN loa.start_date AND COALESCE(loa.extended_till, loa.end_date) THEN 2
WHEN loa.closed IS NULL AND NOW() < loa.start_date THEN 3
WHEN loa.closed IS NOT NULL THEN 4
END,
loa.start_date DESC
`, [userId])
return result;
}
@@ -67,7 +83,7 @@ export async function closeLOA(id: number, closer: number) {
export async function getLOAbyID(id: number): Promise<LOARequest> {
let res = await pool.query(`SELECT * FROM leave_of_absences WHERE id = ?`, [id]);
console.log(res);
if (res.length == 1)
if (res.length != 1)
throw new Error(`LOA with id ${id} not found`);
return res[0];
}

View File

@@ -72,6 +72,23 @@ export function getAllLOAs(): Promise<LOARequest[]> {
});
}
export function getMyLOAs(): Promise<LOARequest[]> {
return fetch(`${addr}/loa/history`, {
method: "GET",
credentials: 'include',
headers: {
"Content-Type": "application/json",
},
}).then((res) => {
if (res.ok) {
return res.json();
} else {
return [];
}
});
}
export async function getLoaTypes(): Promise<LOAType[]> {
const res = await fetch(`${addr}/loa/types`, {
method: "GET",

View File

@@ -165,4 +165,76 @@
body {
@apply bg-background text-foreground;
}
}
/* Root container */
.ListRendererV2-container {
font-family: var(--font-sans, system-ui), sans-serif;
color: var(--foreground);
line-height: 1.45;
max-width: 760px;
margin: 0 auto;
font-size: 0.9rem;
}
/* Headers */
.ListRendererV2-container h4 {
margin: 0.9rem 0 0.4rem 0;
font-weight: 600;
line-height: 1.35;
font-size: 1.05rem;
color: var(--foreground);
/* PURE WHITE */
}
.ListRendererV2-container h5 {
margin: 0.9rem 0 0.4rem 0;
font-weight: 600;
line-height: 1.35;
font-size: 0.95rem;
color: var(--foreground);
/* Still white (change to muted if desired) */
}
/* Lists */
.ListRendererV2-container ul {
list-style-type: disc;
margin-left: 1.1rem;
margin-bottom: 0.6rem;
padding-left: 0.6rem;
color: var(--muted-foreground);
/* dim text */
}
/* Nested lists */
.ListRendererV2-container ul ul {
list-style-type: circle;
margin-left: 0.9rem;
}
/* List items */
.ListRendererV2-container li {
margin: 0.15rem 0;
padding-left: 0.1rem;
color: var(--muted-foreground);
}
/* Bullet color */
.ListRendererV2-container li::marker {
color: var(--muted-foreground);
}
/* Inline elements */
.ListRendererV2-container li p,
.ListRendererV2-container li span,
.ListRendererV2-container p {
display: inline;
margin: 0;
padding: 0;
color: var(--muted-foreground);
}
/* Top-level spacing */
.ListRendererV2-container>ul>li {
margin-top: 0.3rem;
}

View File

@@ -90,7 +90,9 @@ onMounted(async () => {
}
try {
if (!props.adminMode) {
policyString.value = await getLoaPolicy();
let policy = await getLoaPolicy() as any;
policyString.value = policy;
policyRef.value.innerHTML = policyString.value;
}
} catch (error) {
console.error(error);
@@ -101,6 +103,8 @@ onMounted(async () => {
resetForm({ values: { member_id: currentMember.value?.member_id } });
});
const policyRef = ref<HTMLElement>(null);
const defaultPlaceholder = today(getLocalTimeZone())
const minEndDate = computed(() => {
@@ -123,14 +127,10 @@ const maxEndDate = computed(() => {
<template>
<div class="flex flex-row-reverse gap-6 mx-auto w-full" :class="!adminMode ? 'max-w-5xl' : 'max-w-5xl'">
<div v-if="!adminMode" class="flex-1 flex space-x-4 rounded-md border p-4">
<div class="flex-2 space-y-1">
<p class="text-sm font-medium leading-none">
LOA Policy
</p>
<p class="text-sm text-muted-foreground">
Policy goes here.
</p>
<div v-if="!adminMode" class="flex-1 flex flex-col space-x-4 rounded-md border p-4">
<p class="scroll-m-20 text-2xl font-semibold tracking-tight">LOA Policy</p>
<div ref="policyRef">
<!-- LOA policy gets loaded here -->
</div>
</div>
<div class="flex-1 flex flex-col gap-5">
@@ -200,7 +200,7 @@ const maxEndDate = computed(() => {
<Popover>
<PopoverTrigger as-child>
<Button variant="outline" :class="cn(
'w-[280px] justify-start text-left font-normal',
'w-full justify-start text-left font-normal',
!field.value && 'text-muted-foreground',
)">
<CalendarIcon class="mr-2 h-4 w-4" />
@@ -228,7 +228,7 @@ const maxEndDate = computed(() => {
<Popover>
<PopoverTrigger as-child>
<Button variant="outline" :class="cn(
'w-[280px] justify-start text-left font-normal',
'w-full justify-start text-left font-normal',
!field.value && 'text-muted-foreground',
)">
<CalendarIcon class="mr-2 h-4 w-4" />

View File

@@ -16,7 +16,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Ellipsis } from "lucide-vue-next";
import { cancelLOA, extendLOA, getAllLOAs } from "@/api/loa";
import { cancelLOA, extendLOA, getAllLOAs, getMyLOAs } from "@/api/loa";
import { onMounted, ref, computed } from "vue";
import { LOARequest } from "@shared/types/loa";
import Dialog from "../ui/dialog/Dialog.vue";
@@ -31,7 +31,11 @@ import {
CalendarDate,
getLocalTimeZone,
} from "@internationalized/date"
import { el } from "@fullcalendar/core/internal-common";
const props = defineProps<{
adminMode?: boolean
}>()
const LOAList = ref<LOARequest[]>([]);
@@ -40,9 +44,11 @@ onMounted(async () => {
});
async function loadLOAs() {
// const unsort = await getAllLOAs();
// LOAList.value = sortByStartDate(unsort);
LOAList.value = await getAllLOAs();
if (props.adminMode) {
LOAList.value = await getAllLOAs();
} else {
LOAList.value = await getMyLOAs();
}
}
function formatDate(date: Date): string {
@@ -76,7 +82,7 @@ function sortByStartDate(loas: LOARequest[]): LOARequest[] {
}
async function cancelAndReload(id: number) {
await cancelLOA(id, true);
await cancelLOA(id, props.adminMode);
await loadLOAs();
}
@@ -160,11 +166,17 @@ async function commitExtend() {
<Ellipsis></Ellipsis>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem v-if="!post.closed" @click="isExtending = true; targetLOA = post">
<DropdownMenuItem v-if="!post.closed && props.adminMode"
@click="isExtending = true; targetLOA = post">
Extend
</DropdownMenuItem>
<DropdownMenuItem :variant="'destructive'" @click="cancelAndReload(post.id)">End
<DropdownMenuItem v-if="!post.closed" :variant="'destructive'"
@click="cancelAndReload(post.id)">End
</DropdownMenuItem>
<!-- Fallback: no actions available -->
<p v-if="post.closed || (!props.adminMode && post.closed)" class="p-2 text-center text-sm">
No actions
</p>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>

View File

@@ -33,6 +33,6 @@ const showLOADialog = ref(false);
<Button @click="showLOADialog = true">Post LOA</Button>
</div>
<h1>LOA Log</h1>
<LoaList></LoaList>
<LoaList :admin-mode="true"></LoaList>
</div>
</template>

View File

@@ -2,6 +2,8 @@
import LoaForm from '@/components/loa/loaForm.vue';
import { useUserStore } from '@/stores/user';
import { Member } from '@/api/member';
import LoaList from '@/components/loa/loaList.vue';
import { ref } from 'vue';
const userStore = useUserStore();
const user = userStore.user;
@@ -13,8 +15,24 @@ const memberFull: Member = {
status: null,
status_date: null,
};
const mode = ref<'submit' | 'view'>('submit')
</script>
<template>
<LoaForm class="m-10" :member="memberFull"></LoaForm>
<div class="max-w-5xl mx-auto flex w-full flex-col mt-4 mb-10">
<div class="mb-8">
<p class="scroll-m-20 text-2xl font-semibold tracking-tight">Leave of Absence</p>
<div class="pt-3">
<div class="flex w-min *:px-10 pt-2 border-b *:w-full *:text-center *:pb-1 *:cursor-pointer">
<label :class="mode === 'submit' ? 'border-b-3 border-foreground' : 'mb-[2px]'"
@click="mode = 'submit'">Submit</label>
<label :class="mode === 'view' ? 'border-b-3 border-foreground' : 'mb-[2px]'"
@click="mode = 'view'">History</label>
</div>
</div>
</div>
<LoaForm v-if="mode === 'submit'" :member="memberFull"></LoaForm>
<LoaList v-if="mode === 'view'" :admin-mode="false"></LoaList>
</div>
</template>