Finished list render system
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { BatchPromotion } from '@shared/schemas/promotionSchema';
|
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
|
// @ts-ignore
|
||||||
const addr = import.meta.env.VITE_APIHOST;
|
const addr = import.meta.env.VITE_APIHOST;
|
||||||
@@ -16,7 +17,6 @@ export async function getAllRanks(): Promise<Rank[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Placeholder: submit a rank change
|
|
||||||
export async function submitRankChange(promo: BatchPromotion) {
|
export async function submitRankChange(promo: BatchPromotion) {
|
||||||
const res = await fetch(`${addr}/memberRanks`, {
|
const res = await fetch(`${addr}/memberRanks`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -33,3 +33,29 @@ export async function submitRankChange(promo: BatchPromotion) {
|
|||||||
throw new Error("Failed to submit rank change: Server error");
|
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">
|
<script setup lang="ts">
|
||||||
import { pagination } from '@shared/types/pagination';
|
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 expanded = ref<number | null>(null);
|
||||||
const hoverID = ref<number | null>(null);
|
const hoverID = ref<number | null>(null);
|
||||||
@@ -14,127 +52,59 @@ const pageSizeOptions = [10, 15, 30]
|
|||||||
function setPageSize(size: number) {
|
function setPageSize(size: number) {
|
||||||
pageSize.value = size
|
pageSize.value = size
|
||||||
pageNum.value = 1;
|
pageNum.value = 1;
|
||||||
// loadLOAs();
|
loadHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPage(pagenum: number) {
|
function setPage(pagenum: number) {
|
||||||
pageNum.value = pagenum;
|
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>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<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">
|
<div class="max-w-7xl w-full mx-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Member</TableHead>
|
<TableHead>Date</TableHead>
|
||||||
<TableHead>Type</TableHead>
|
<TableHead></TableHead>
|
||||||
<TableHead>Start</TableHead>
|
|
||||||
<TableHead>End</TableHead>
|
|
||||||
<!-- <TableHead class="w-[500px]">Reason</TableHead> -->
|
|
||||||
<TableHead>Posted on</TableHead>
|
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<template v-for="post in LOAList" :key="post.id">
|
<template v-for="(batch, index) in batchList" :key="index">
|
||||||
<TableRow class="hover:bg-muted/50 cursor-pointer" @click="expanded = post.id"
|
<TableRow class="hover:bg-muted/50 cursor-pointer" @click="expanded = index"
|
||||||
@mouseenter="hoverID = post.id" @mouseleave="hoverID = null" :class="{
|
@mouseenter="hoverID = index" @mouseleave="hoverID = null" :class="{
|
||||||
'border-b-0': expanded === post.id,
|
'border-b-0': expanded === index,
|
||||||
'bg-muted/50': hoverID === post.id
|
'bg-muted/50': hoverID === index
|
||||||
}">
|
}">
|
||||||
<TableCell class="font-medium">
|
<TableCell class="font-medium">
|
||||||
<MemberCard :member-id="post.member_id"></MemberCard>
|
{{ formatDate(new Date(batch.entry_day)) }}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{{ post.type_name }}</TableCell>
|
<TableCell class="text-right">
|
||||||
<TableCell>{{ formatDate(post.start_date) }}</TableCell>
|
<Button v-if="expanded === index" @click.stop="expanded = null" size="icon"
|
||||||
<TableCell>{{ post.extended_till ? formatDate(post.extended_till) :
|
variant="ghost">
|
||||||
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">
|
|
||||||
<ChevronUp class="size-6" />
|
<ChevronUp class="size-6" />
|
||||||
</Button>
|
</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" />
|
<ChevronDown class="size-6" />
|
||||||
</Button>
|
</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow v-if="expanded === post.id" @mouseenter="hoverID = post.id"
|
<TableRow v-if="expanded === index" @mouseenter="hoverID = index" @mouseleave="hoverID = null"
|
||||||
@mouseleave="hoverID = null" :class="{ 'bg-muted/50 border-t-0': hoverID === post.id }">
|
:class="{ 'bg-muted/50 border-t-0': hoverID === index }">
|
||||||
<TableCell :colspan="8" class="p-0">
|
<TableCell :colspan="8" class="p-0">
|
||||||
<div class="w-full p-3 mb-6 space-y-3">
|
<div class="w-full p-2 mb-6 space-y-3">
|
||||||
<div class="flex justify-between items-start gap-4">
|
<PromotionListDay :date="batch.entry_day"></PromotionListDay>
|
||||||
<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>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -142,6 +112,9 @@ function setPage(pagenum: number) {
|
|||||||
</template>
|
</template>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
<div v-if="loading">
|
||||||
|
<Spinner class="size-7"></Spinner>
|
||||||
|
</div>
|
||||||
<div class="mt-5 flex justify-between">
|
<div class="mt-5 flex justify-between">
|
||||||
<div></div>
|
<div></div>
|
||||||
<Pagination v-slot="{ page }" :items-per-page="pageData?.pageSize || 10" :total="pageData?.total || 10"
|
<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 -->
|
<!-- LEFT COLUMN -->
|
||||||
<div class="flex-1 border-r pr-8">
|
<div class="flex-1 border-r pr-8">
|
||||||
<!-- <PromotionList></PromotionList> -->
|
<PromotionList></PromotionList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- RIGHT COLUMN -->
|
<!-- RIGHT COLUMN -->
|
||||||
|
|||||||
Reference in New Issue
Block a user