Finished list render system
This commit is contained in:
@@ -1,35 +1,61 @@
|
||||
import { BatchPromotion } from '@shared/schemas/promotionSchema';
|
||||
import { Rank } from '@shared/types/rank'
|
||||
import { PagedData } from '@shared/types/pagination';
|
||||
import { PromotionSummary, Rank } from '@shared/types/rank'
|
||||
|
||||
// @ts-ignore
|
||||
const addr = import.meta.env.VITE_APIHOST;
|
||||
|
||||
export async function getAllRanks(): Promise<Rank[]> {
|
||||
const res = await fetch(`${addr}/ranks`, {
|
||||
credentials: 'include'
|
||||
})
|
||||
const res = await fetch(`${addr}/ranks`, {
|
||||
credentials: 'include'
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
return res.json()
|
||||
} else {
|
||||
console.error("Something went wrong approving the application")
|
||||
}
|
||||
if (res.ok) {
|
||||
return res.json()
|
||||
} else {
|
||||
console.error("Something went wrong approving the application")
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder: submit a rank change
|
||||
export async function submitRankChange(promo: BatchPromotion) {
|
||||
const res = await fetch(`${addr}/memberRanks`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(promo),
|
||||
})
|
||||
const res = await fetch(`${addr}/memberRanks`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(promo),
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
return
|
||||
} else {
|
||||
throw new Error("Failed to submit rank change: Server error");
|
||||
}
|
||||
if (res.ok) {
|
||||
return
|
||||
} else {
|
||||
throw new Error("Failed to submit rank change: Server error");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPromoHistory(page?: number, pageSize?: number): Promise<PagedData<PromotionSummary>> {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (page !== undefined) {
|
||||
params.set("page", page.toString());
|
||||
}
|
||||
|
||||
if (pageSize !== undefined) {
|
||||
params.set("pageSize", pageSize.toString());
|
||||
}
|
||||
|
||||
return fetch(`${addr}/memberRanks?${params}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: 'include',
|
||||
}).then((res) => {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { pagination } from '@shared/types/pagination';
|
||||
import { ref } from 'vue';
|
||||
import { PromotionSummary } from '@shared/types/rank';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from '@/components/ui/pagination'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { ChevronDown, ChevronUp } from 'lucide-vue-next';
|
||||
import Button from '../ui/button/Button.vue';
|
||||
import { getPromoHistory } from '@/api/rank';
|
||||
import Spinner from '../ui/spinner/Spinner.vue';
|
||||
import PromotionListDay from './promotionListDay.vue';
|
||||
|
||||
|
||||
const loading = ref(true);
|
||||
const batchList = ref<PromotionSummary[]>();
|
||||
|
||||
onMounted(async () => {
|
||||
await loadHistory();
|
||||
loading.value = false;
|
||||
})
|
||||
|
||||
async function loadHistory() {
|
||||
let d = await getPromoHistory(pageNum.value, pageSize.value);
|
||||
batchList.value = d.data;
|
||||
pageData.value = d.pagination;
|
||||
}
|
||||
|
||||
const expanded = ref<number | null>(null);
|
||||
const hoverID = ref<number | null>(null);
|
||||
@@ -14,127 +52,59 @@ const pageSizeOptions = [10, 15, 30]
|
||||
function setPageSize(size: number) {
|
||||
pageSize.value = size
|
||||
pageNum.value = 1;
|
||||
// loadLOAs();
|
||||
loadHistory();
|
||||
}
|
||||
|
||||
function setPage(pagenum: number) {
|
||||
pageNum.value = pagenum;
|
||||
// loadLOAs();
|
||||
loadHistory();
|
||||
}
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
if (!date) return "";
|
||||
date = typeof date === 'string' ? new Date(date) : date;
|
||||
return date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<Dialog :open="isExtending" @update:open="(val) => isExtending = val">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Extend {{ targetLOA.name }}'s Leave of Absence </DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="flex gap-5">
|
||||
<Calendar v-model="extendTo" class="rounded-md border shadow-sm w-min" layout="month-and-year"
|
||||
:min-value="toCalendarDate(targetEnd)"
|
||||
:max-value="toCalendarDate(targetEnd).add({ years: 1 })" />
|
||||
<div class="flex flex-col w-full gap-3 px-2">
|
||||
<p>Quick Options</p>
|
||||
<Button variant="outline" @click="extendTo = toCalendarDate(targetEnd).add({ days: 7 })">1
|
||||
Week</Button>
|
||||
<Button variant="outline" @click="extendTo = toCalendarDate(targetEnd).add({ months: 1 })">1
|
||||
Month</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-4">
|
||||
<Button variant="outline" @click="isExtending = false">Cancel</Button>
|
||||
<Button @click="commitExtend">Extend</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div class="max-w-7xl w-full mx-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Member</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Start</TableHead>
|
||||
<TableHead>End</TableHead>
|
||||
<!-- <TableHead class="w-[500px]">Reason</TableHead> -->
|
||||
<TableHead>Posted on</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<template v-for="post in LOAList" :key="post.id">
|
||||
<TableRow class="hover:bg-muted/50 cursor-pointer" @click="expanded = post.id"
|
||||
@mouseenter="hoverID = post.id" @mouseleave="hoverID = null" :class="{
|
||||
'border-b-0': expanded === post.id,
|
||||
'bg-muted/50': hoverID === post.id
|
||||
<template v-for="(batch, index) in batchList" :key="index">
|
||||
<TableRow class="hover:bg-muted/50 cursor-pointer" @click="expanded = index"
|
||||
@mouseenter="hoverID = index" @mouseleave="hoverID = null" :class="{
|
||||
'border-b-0': expanded === index,
|
||||
'bg-muted/50': hoverID === index
|
||||
}">
|
||||
<TableCell class="font-medium">
|
||||
<MemberCard :member-id="post.member_id"></MemberCard>
|
||||
{{ formatDate(new Date(batch.entry_day)) }}
|
||||
</TableCell>
|
||||
<TableCell>{{ post.type_name }}</TableCell>
|
||||
<TableCell>{{ formatDate(post.start_date) }}</TableCell>
|
||||
<TableCell>{{ post.extended_till ? formatDate(post.extended_till) :
|
||||
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-400">Upcoming</Badge>
|
||||
<Badge v-else-if="loaStatus(post) === 'Active'" class="bg-green-500">Active</Badge>
|
||||
<Badge v-else-if="loaStatus(post) === 'Overdue'" class="bg-yellow-400">Overdue</Badge>
|
||||
<Badge v-else class="bg-gray-400">Ended</Badge>
|
||||
</TableCell>
|
||||
<TableCell @click.stop="" class="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger class="cursor-pointer">
|
||||
<Button variant="ghost">
|
||||
<Ellipsis class="size-6"></Ellipsis>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem v-if="!post.closed && props.adminMode"
|
||||
@click="isExtending = true; targetLOA = post">
|
||||
Extend
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem v-if="!post.closed" :variant="'destructive'"
|
||||
@click="cancelAndReload(post.id)">{{ loaStatus(post) === 'Upcoming' ?
|
||||
'Cancel' :
|
||||
'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>
|
||||
<TableCell>
|
||||
<Button v-if="expanded === post.id" @click.stop="expanded = null" variant="ghost">
|
||||
<TableCell class="text-right">
|
||||
<Button v-if="expanded === index" @click.stop="expanded = null" size="icon"
|
||||
variant="ghost">
|
||||
<ChevronUp class="size-6" />
|
||||
</Button>
|
||||
<Button v-else @click.stop="expanded = post.id" variant="ghost">
|
||||
<Button v-else @click.stop="expanded = index" size="icon" variant="ghost">
|
||||
<ChevronDown class="size-6" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-if="expanded === post.id" @mouseenter="hoverID = post.id"
|
||||
@mouseleave="hoverID = null" :class="{ 'bg-muted/50 border-t-0': hoverID === post.id }">
|
||||
<TableRow v-if="expanded === index" @mouseenter="hoverID = index" @mouseleave="hoverID = null"
|
||||
:class="{ 'bg-muted/50 border-t-0': hoverID === index }">
|
||||
<TableCell :colspan="8" class="p-0">
|
||||
<div class="w-full p-3 mb-6 space-y-3">
|
||||
<div class="flex justify-between items-start gap-4">
|
||||
<div class="flex-1">
|
||||
<!-- Title -->
|
||||
<p class="text-md font-semibold text-foreground">
|
||||
Reason
|
||||
</p>
|
||||
|
||||
<!-- Content -->
|
||||
<p
|
||||
class="mt-1 text-md whitespace-pre-wrap leading-relaxed text-muted-foreground">
|
||||
{{ post.reason }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full p-2 mb-6 space-y-3">
|
||||
<PromotionListDay :date="batch.entry_day"></PromotionListDay>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -142,6 +112,9 @@ function setPage(pagenum: number) {
|
||||
</template>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div v-if="loading">
|
||||
<Spinner class="size-7"></Spinner>
|
||||
</div>
|
||||
<div class="mt-5 flex justify-between">
|
||||
<div></div>
|
||||
<Pagination v-slot="{ page }" :items-per-page="pageData?.pageSize || 10" :total="pageData?.total || 10"
|
||||
|
||||
17
ui/src/components/promotions/promotionListDay.vue
Normal file
17
ui/src/components/promotions/promotionListDay.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
|
||||
defineProps<{
|
||||
date: Date
|
||||
}>()
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
Hello
|
||||
</template>
|
||||
@@ -9,7 +9,7 @@ import PromotionList from "@/components/promotions/promotionList.vue";
|
||||
|
||||
<!-- LEFT COLUMN -->
|
||||
<div class="flex-1 border-r pr-8">
|
||||
<!-- <PromotionList></PromotionList> -->
|
||||
<PromotionList></PromotionList>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT COLUMN -->
|
||||
|
||||
Reference in New Issue
Block a user