finalized LOA systems
This commit is contained in:
@@ -12,7 +12,19 @@ router.post('/', async (req, res) => {
|
||||
//get all users
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT * FROM view_member_rank_status_all;');
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
v.*,
|
||||
CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM leave_of_absences l
|
||||
WHERE l.member_id = v.member_id
|
||||
AND l.deleted = 0
|
||||
AND UTC_TIMESTAMP() BETWEEN l.start_date AND l.end_date
|
||||
) THEN 1 ELSE 0
|
||||
END AS on_loa
|
||||
FROM view_member_rank_status_all v;`);
|
||||
return res.status(200).json(result);
|
||||
} catch (err) {
|
||||
console.error('Error fetching users:', err);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type LOARequest = {
|
||||
id: number;
|
||||
id?: number;
|
||||
name?: string;
|
||||
member_id: number;
|
||||
filed_date: string; // ISO 8601 string
|
||||
|
||||
@@ -5,6 +5,7 @@ export type Member = {
|
||||
rank_date: string | null;
|
||||
status: string | null;
|
||||
status_date: string | null;
|
||||
on_loa: boolean | null;
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Check, Search } from "lucide-vue-next"
|
||||
import { Combobox, ComboboxAnchor, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxItemIndicator, ComboboxList } from "@/components/ui/combobox"
|
||||
import { getRanks, Rank } from "@/api/rank"
|
||||
import { onMounted, ref } from "vue";
|
||||
import { Member, getMembers } from "@/api/member";
|
||||
import Button from "@/components/ui/button/Button.vue";
|
||||
@@ -18,28 +17,28 @@ import PopoverContent from "@/components/ui/popover/PopoverContent.vue";
|
||||
import { RangeCalendar } from "@/components/ui/range-calendar"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CalendarIcon } from "lucide-vue-next"
|
||||
import Input from "@/components/ui/input/Input.vue";
|
||||
import Textarea from "@/components/ui/textarea/Textarea.vue";
|
||||
import Separator from "@/components/ui/separator/Separator.vue";
|
||||
import { submitLOA } from "@/api/loa"; // <-- import the submit function
|
||||
import { LOARequest, submitLOA } from "@/api/loa"; // <-- import the submit function
|
||||
|
||||
const members = ref<Member[]>([])
|
||||
const currentMember = ref<Member | null>(null);
|
||||
|
||||
defineProps({
|
||||
adminMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const props = withDefaults(defineProps<{
|
||||
adminMode?: boolean;
|
||||
member?: Member | null;
|
||||
}>(), {
|
||||
adminMode: false,
|
||||
member: null,
|
||||
});
|
||||
|
||||
|
||||
const df = new DateFormatter("en-US", {
|
||||
dateStyle: "medium",
|
||||
})
|
||||
|
||||
const value = ref({
|
||||
start: new CalendarDate(2022, 1, 20),
|
||||
end: new CalendarDate(2022, 1, 20).add({ days: 20 }),
|
||||
// start: new CalendarDate(2022, 1, 20),
|
||||
// end: new CalendarDate(2022, 1, 20).add({ days: 20 }),
|
||||
}) as Ref<DateRange>
|
||||
|
||||
const reason = ref(""); // <-- reason for LOA
|
||||
@@ -48,6 +47,12 @@ const submitError = ref<string | null>(null);
|
||||
const submitSuccess = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.member) {
|
||||
currentMember.value = props.member;
|
||||
}
|
||||
if (props.adminMode) {
|
||||
members.value = await getMembers();
|
||||
}
|
||||
members.value = await getMembers();
|
||||
});
|
||||
|
||||
@@ -71,12 +76,12 @@ async function handleSubmit() {
|
||||
return;
|
||||
}
|
||||
|
||||
const req = {
|
||||
member_id,
|
||||
const req: LOARequest = {
|
||||
filed_date,
|
||||
start_date,
|
||||
end_date,
|
||||
reason: reason.value,
|
||||
member_id
|
||||
};
|
||||
|
||||
const result = await submitLOA(req);
|
||||
@@ -96,7 +101,7 @@ function toMariaDBDatetime(date: Date): string {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-row-reverse gap-6 mx-auto " :class="!adminMode ? 'max-w-5xl' : 'max-w-2xl'">
|
||||
<div class="flex flex-row-reverse gap-6 mx-auto " :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">
|
||||
|
||||
@@ -8,8 +8,16 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Ellipsis } from "lucide-vue-next";
|
||||
import { getAllLOAs, LOARequest } from "@/api/loa";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { onMounted, ref, computed } from "vue";
|
||||
|
||||
const LOAList = ref<LOARequest[]>([]);
|
||||
|
||||
@@ -25,6 +33,32 @@ function formatDate(dateStr: string): string {
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function loaStatus(loa: {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
deleted?: number;
|
||||
}): "Upcoming" | "Active" | "Expired" | "Cancelled" {
|
||||
if (loa.deleted) return "Cancelled";
|
||||
|
||||
const now = new Date();
|
||||
const start = new Date(loa.start_date);
|
||||
const end = new Date(loa.end_date);
|
||||
|
||||
if (now < start) return "Upcoming";
|
||||
if (now >= start && now <= end) return "Active";
|
||||
if (now > end) return "Expired";
|
||||
|
||||
return "Expired"; // fallback
|
||||
}
|
||||
|
||||
function sortByStartDate(loas: LOARequest[]): LOARequest[] {
|
||||
return [...loas].sort(
|
||||
(a, b) => new Date(b.start_date).getTime() - new Date(a.start_date).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
const sortedLoas = computed(() => sortByStartDate(LOAList.value));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -37,14 +71,11 @@ function formatDate(dateStr: string): string {
|
||||
<TableHead>End</TableHead>
|
||||
<TableHead>Reason</TableHead>
|
||||
<TableHead>Posted on</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-for="post in LOAList"
|
||||
:key="post.id"
|
||||
class="hover:bg-muted/50"
|
||||
>
|
||||
<TableRow v-for="post in sortedLoas" :key="post.id" class="hover:bg-muted/50">
|
||||
<TableCell class="font-medium">
|
||||
{{ post.name }}
|
||||
</TableCell>
|
||||
@@ -52,6 +83,23 @@ function formatDate(dateStr: string): string {
|
||||
<TableCell>{{ formatDate(post.end_date) }}</TableCell>
|
||||
<TableCell>{{ post.reason }}</TableCell>
|
||||
<TableCell>{{ formatDate(post.filed_date) }}</TableCell>
|
||||
<TableCell>
|
||||
<Badge v-if="loaStatus(post) === 'Upcoming'" class="bg-blue-500">Upcoming</Badge>
|
||||
<Badge v-else-if="loaStatus(post) === 'Active'" class="bg-green-500">Active</Badge>
|
||||
<Badge v-else-if="loaStatus(post) === 'Expired'" class="bg-gray-400">Expired</Badge>
|
||||
<Badge v-else class="bg-red-500">Cancelled</Badge>
|
||||
</TableCell>
|
||||
<TableCell @click.stop="console.log('hi')" class="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger class="cursor-pointer">
|
||||
<Ellipsis></Ellipsis>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem :variant="'destructive'">Cancel</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
@@ -1,12 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import LoaForm from '@/components/loa/loaForm.vue';
|
||||
import LoaList from '@/components/loa/loaList.vue';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import Button from '@/components/ui/button/Button.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const showLOADialog = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="showLOADialog" v-on:update:open="showLOADialog = false">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Post LOA</DialogTitle>
|
||||
<DialogDescription>
|
||||
Post an LOA on behalf of a member.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<LoaForm :admin-mode="true" class="my-5 w-full"></LoaForm>
|
||||
<!-- <DialogFooter>
|
||||
<Button variant="secondary" @click="showLOADialog = false">Cancel</Button>
|
||||
<Button>Apply</Button>
|
||||
</DialogFooter> -->
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div class="max-w-5xl mx-auto pt-10">
|
||||
<!-- <LoaForm class="m-10"></LoaForm> -->
|
||||
<div class="flex justify-end mb-4">
|
||||
<Button @click="showLOADialog = true">Post LOA</Button>
|
||||
</div>
|
||||
<h1>LOA Log</h1>
|
||||
<LoaList></LoaList>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import LoaForm from '@/components/loa/loaForm.vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { Member } from '@/api/member';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const user = userStore.user;
|
||||
const memberFull: Member = {
|
||||
member_id: user.id,
|
||||
member_name: user.name,
|
||||
rank: null,
|
||||
rank_date: null,
|
||||
status: null,
|
||||
status_date: null,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoaForm class="m-10"></LoaForm>
|
||||
<LoaForm class="m-10" :member="memberFull"></LoaForm>
|
||||
</template>
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import Badge from "@/components/ui/badge/Badge.vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { Member, getMembers } from "@/api/member";
|
||||
import { useRouter } from 'vue-router';
|
||||
@@ -48,23 +49,9 @@ const searchedMembers = computed(() => {
|
||||
return members.value.filter(member => member.member_name.toLowerCase().includes(searchVal.value.toLowerCase()));
|
||||
});
|
||||
|
||||
// page state systems
|
||||
const showLOADialog = ref(false);
|
||||
const LOAuserId = ref<number | null>(null);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="showLOADialog" v-on:update:open="showLOADialog = false">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>LOA Menu</DialogTitle>
|
||||
<DialogDescription>
|
||||
Something something flavor text.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<LoaForm :adminMode="true"></LoaForm>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<!-- table menu -->
|
||||
<div class="w-4xl mx-auto">
|
||||
@@ -89,9 +76,8 @@ const LOAuserId = ref<number | null>(null);
|
||||
</TableCell>
|
||||
<TableCell>{{ member.rank }}</TableCell>
|
||||
<TableCell>{{ member.status }}</TableCell>
|
||||
<TableCell>{{ member.status }}</TableCell>
|
||||
<TableCell>{{ member.status }}</TableCell>
|
||||
<TableCell @click.stop="console.log('hi')" class="text-right">
|
||||
<TableCell><Badge v-if="member.on_loa">On LOA</Badge></TableCell>
|
||||
<TableCell @click.stop="" class="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger class="cursor-pointer">
|
||||
<Ellipsis></Ellipsis>
|
||||
@@ -99,7 +85,6 @@ const LOAuserId = ref<number | null>(null);
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Change Rank</DropdownMenuItem>
|
||||
<DropdownMenuItem>Transfer</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="showLOADialog = true">LOA</DropdownMenuItem>
|
||||
<DropdownMenuItem :variant="'destructive'">Retire</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
Reference in New Issue
Block a user