added policy system and self LOA management
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user