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) => {
|
router.get('/all', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = await getAllLOA();
|
const result = await getAllLOA();
|
||||||
@@ -128,7 +140,7 @@ router.get('/policy', async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
if (output.ok) {
|
if (output.ok) {
|
||||||
const out = await output.json();
|
const out = await output.json();
|
||||||
res.status(200).json(out.markdown);
|
res.status(200).json(out.html);
|
||||||
} else {
|
} else {
|
||||||
console.log("BAD");
|
console.log("BAD");
|
||||||
res.sendStatus(500);
|
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[]> {
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +83,7 @@ export async function closeLOA(id: number, closer: number) {
|
|||||||
export async function getLOAbyID(id: number): Promise<LOARequest> {
|
export async function getLOAbyID(id: number): Promise<LOARequest> {
|
||||||
let res = await pool.query(`SELECT * FROM leave_of_absences WHERE id = ?`, [id]);
|
let res = await pool.query(`SELECT * FROM leave_of_absences WHERE id = ?`, [id]);
|
||||||
console.log(res);
|
console.log(res);
|
||||||
if (res.length == 1)
|
if (res.length != 1)
|
||||||
throw new Error(`LOA with id ${id} not found`);
|
throw new Error(`LOA with id ${id} not found`);
|
||||||
return res[0];
|
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[]> {
|
export async function getLoaTypes(): Promise<LOAType[]> {
|
||||||
const res = await fetch(`${addr}/loa/types`, {
|
const res = await fetch(`${addr}/loa/types`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|||||||
@@ -165,4 +165,76 @@
|
|||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@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 {
|
try {
|
||||||
if (!props.adminMode) {
|
if (!props.adminMode) {
|
||||||
policyString.value = await getLoaPolicy();
|
let policy = await getLoaPolicy() as any;
|
||||||
|
policyString.value = policy;
|
||||||
|
policyRef.value.innerHTML = policyString.value;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -101,6 +103,8 @@ onMounted(async () => {
|
|||||||
resetForm({ values: { member_id: currentMember.value?.member_id } });
|
resetForm({ values: { member_id: currentMember.value?.member_id } });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const policyRef = ref<HTMLElement>(null);
|
||||||
|
|
||||||
const defaultPlaceholder = today(getLocalTimeZone())
|
const defaultPlaceholder = today(getLocalTimeZone())
|
||||||
|
|
||||||
const minEndDate = computed(() => {
|
const minEndDate = computed(() => {
|
||||||
@@ -123,14 +127,10 @@ const maxEndDate = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-row-reverse gap-6 mx-auto w-full" :class="!adminMode ? 'max-w-5xl' : 'max-w-5xl'">
|
<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 v-if="!adminMode" class="flex-1 flex flex-col space-x-4 rounded-md border p-4">
|
||||||
<div class="flex-2 space-y-1">
|
<p class="scroll-m-20 text-2xl font-semibold tracking-tight">LOA Policy</p>
|
||||||
<p class="text-sm font-medium leading-none">
|
<div ref="policyRef">
|
||||||
LOA Policy
|
<!-- LOA policy gets loaded here -->
|
||||||
</p>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
Policy goes here.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 flex flex-col gap-5">
|
<div class="flex-1 flex flex-col gap-5">
|
||||||
@@ -200,7 +200,7 @@ const maxEndDate = computed(() => {
|
|||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
<Button variant="outline" :class="cn(
|
<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',
|
!field.value && 'text-muted-foreground',
|
||||||
)">
|
)">
|
||||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||||
@@ -228,7 +228,7 @@ const maxEndDate = computed(() => {
|
|||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
<Button variant="outline" :class="cn(
|
<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',
|
!field.value && 'text-muted-foreground',
|
||||||
)">
|
)">
|
||||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { Ellipsis } from "lucide-vue-next";
|
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 { onMounted, ref, computed } from "vue";
|
||||||
import { LOARequest } from "@shared/types/loa";
|
import { LOARequest } from "@shared/types/loa";
|
||||||
import Dialog from "../ui/dialog/Dialog.vue";
|
import Dialog from "../ui/dialog/Dialog.vue";
|
||||||
@@ -31,7 +31,11 @@ import {
|
|||||||
CalendarDate,
|
CalendarDate,
|
||||||
getLocalTimeZone,
|
getLocalTimeZone,
|
||||||
} from "@internationalized/date"
|
} from "@internationalized/date"
|
||||||
|
import { el } from "@fullcalendar/core/internal-common";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
adminMode?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
const LOAList = ref<LOARequest[]>([]);
|
const LOAList = ref<LOARequest[]>([]);
|
||||||
|
|
||||||
@@ -40,9 +44,11 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function loadLOAs() {
|
async function loadLOAs() {
|
||||||
// const unsort = await getAllLOAs();
|
if (props.adminMode) {
|
||||||
// LOAList.value = sortByStartDate(unsort);
|
LOAList.value = await getAllLOAs();
|
||||||
LOAList.value = await getAllLOAs();
|
} else {
|
||||||
|
LOAList.value = await getMyLOAs();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(date: Date): string {
|
function formatDate(date: Date): string {
|
||||||
@@ -76,7 +82,7 @@ function sortByStartDate(loas: LOARequest[]): LOARequest[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function cancelAndReload(id: number) {
|
async function cancelAndReload(id: number) {
|
||||||
await cancelLOA(id, true);
|
await cancelLOA(id, props.adminMode);
|
||||||
await loadLOAs();
|
await loadLOAs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,11 +166,17 @@ async function commitExtend() {
|
|||||||
<Ellipsis></Ellipsis>
|
<Ellipsis></Ellipsis>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem v-if="!post.closed" @click="isExtending = true; targetLOA = post">
|
<DropdownMenuItem v-if="!post.closed && props.adminMode"
|
||||||
|
@click="isExtending = true; targetLOA = post">
|
||||||
Extend
|
Extend
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem :variant="'destructive'" @click="cancelAndReload(post.id)">End
|
<DropdownMenuItem v-if="!post.closed" :variant="'destructive'"
|
||||||
|
@click="cancelAndReload(post.id)">End
|
||||||
</DropdownMenuItem>
|
</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>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -33,6 +33,6 @@ const showLOADialog = ref(false);
|
|||||||
<Button @click="showLOADialog = true">Post LOA</Button>
|
<Button @click="showLOADialog = true">Post LOA</Button>
|
||||||
</div>
|
</div>
|
||||||
<h1>LOA Log</h1>
|
<h1>LOA Log</h1>
|
||||||
<LoaList></LoaList>
|
<LoaList :admin-mode="true"></LoaList>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
import LoaForm from '@/components/loa/loaForm.vue';
|
import LoaForm from '@/components/loa/loaForm.vue';
|
||||||
import { useUserStore } from '@/stores/user';
|
import { useUserStore } from '@/stores/user';
|
||||||
import { Member } from '@/api/member';
|
import { Member } from '@/api/member';
|
||||||
|
import LoaList from '@/components/loa/loaList.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const user = userStore.user;
|
const user = userStore.user;
|
||||||
@@ -13,8 +15,24 @@ const memberFull: Member = {
|
|||||||
status: null,
|
status: null,
|
||||||
status_date: null,
|
status_date: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mode = ref<'submit' | 'view'>('submit')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
</template>
|
||||||
Reference in New Issue
Block a user