Files
vberp/src/routes/backoffice/+page.svelte
2025-07-20 23:46:17 +07:00

243 lines
7.3 KiB
Svelte

<script lang="ts">
import { onMount } from "svelte";
import { supabase } from "$lib/supabaseClient";
import HeroIcon from "../../components/HeroIcon.svelte";
import { quintOut } from "svelte/easing";
import { tweened } from "svelte/motion";
import type { Tweened } from "svelte/motion";
type StatKey =
| "vb_purchase_orders"
| "vb_issues"
| "vb_villas"
| "vb_timesheet"
| "vb_inventory"
| "vb_projects"
| "vb_vendor"
| "vb_employee"
| "vb_transport"
| "vb_dinning"
| "vb_feedback";
// This object will hold our `tweened` store instances
const tweenedStats: Record<StatKey, Tweened<number>> = {
vb_purchase_orders: tweened(0),
vb_issues: tweened(0),
vb_villas: tweened(0),
vb_timesheet: tweened(0),
vb_inventory: tweened(0),
vb_projects: tweened(0),
vb_vendor: tweened(0),
vb_employee: tweened(0),
vb_transport: tweened(0),
vb_dinning: tweened(0),
vb_feedback: tweened(0),
};
// This object will hold the *values* from the tweened stores, and will be reactive
let stats: Record<StatKey, number> = {
vb_purchase_orders: 0,
vb_issues: 0,
vb_villas: 0,
vb_timesheet: 0,
vb_inventory: 0,
vb_projects: 0,
vb_vendor: 0,
vb_employee: 0,
vb_transport: 0,
vb_dinning: 0,
vb_feedback: 0,
};
let isLoading = true;
const items: {
label: string;
key: StatKey;
color: string;
icon: string;
bgColor: string;
}[] = [
{
label: "Total Issues",
key: "vb_issues",
color: "text-red-600",
icon: "exclamation-triangle",
bgColor: "bg-red-100",
},
{
label: "Total Projects",
key: "vb_projects",
color: "text-purple-600",
icon: "folder",
bgColor: "bg-purple-100",
},
{
label: "Total POs",
key: "vb_purchase_orders",
color: "text-blue-600",
icon: "document",
bgColor: "bg-blue-100",
},
{
label: "Total Villas",
key: "vb_villas",
color: "text-green-600",
icon: "home",
bgColor: "bg-green-100",
},
{
label: "Total Timesheets",
key: "vb_timesheet",
color: "text-gray-800",
icon: "clock",
bgColor: "bg-gray-100",
},
{
label: "Total Inventories",
key: "vb_inventory",
color: "text-yellow-600",
icon: "cube",
bgColor: "bg-yellow-100",
},
{
label: "Total Vendors",
key: "vb_vendor",
color: "text-orange-600",
icon: "building-storefront",
bgColor: "bg-orange-100",
},
{
label: "Total Employees",
key: "vb_employee",
color: "text-teal-600",
icon: "user-group",
bgColor: "bg-teal-100",
},
{
label: "Total Transport",
key: "vb_transport",
color: "text-indigo-600",
icon: "truck",
bgColor: "bg-indigo-100",
},
{
label: "Total Dining",
key: "vb_dinning",
color: "text-pink-600",
icon: "utensils",
bgColor: "bg-pink-100",
},
{
label: "Total Feedback",
key: "vb_feedback",
color: "text-gray-600",
icon: "comments",
bgColor: "bg-gray-100",
},
];
onMount(async () => {
const fetchCount = async (table: string) => {
const { count, error } = await supabase
.from(table)
.select("*", { count: "exact", head: true });
if (error) {
console.error(
`Error fetching count for ${table}:`,
error.message,
);
return 0;
}
return count || 0;
};
const promises = items.map(async (item) => {
const count = await fetchCount(item.key);
// Set the tweened store's value
tweenedStats[item.key].set(count, {
duration: 1000,
easing: quintOut,
});
// Subscribe to the tweened store and update the 'stats' variable
// Using a reactive statement `$: stats[item.key] = $tweenedStats[item.key]`
// for each item in the loop is not ideal.
// A better way is to update 'stats' directly within the subscription,
// or just bind to `tweenedStats[item.key]` in the template.
// For simplicity and direct reactivity:
tweenedStats[item.key].subscribe((val) => {
stats = { ...stats, [item.key]: Math.round(val) };
});
});
await Promise.all(promises);
isLoading = false;
console.log("Dashboard data fetched successfully and animated!");
});
</script>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1
class="text-3xl font-extrabold text-gray-900 mb-8 text-center sm:text-left"
>
Dashboard Overview
</h1>
<div
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
>
{#each items as item}
<div
class="bg-white p-6 rounded-xl shadow-lg border border-gray-100
flex flex-col items-center justify-center text-center
transform transition-all duration-300 ease-in-out
hover:scale-105 hover:shadow-xl hover:border-blue-300
{isLoading ? 'animate-pulse-light' : ''}"
>
<div
class="relative w-14 h-14 rounded-full {item.bgColor}
flex items-center justify-center mb-4 transition-all duration-300 ease-in-out
group-hover:scale-110"
>
<HeroIcon name={item.icon} colorClass={item.color} />
<div
class="absolute inset-0 rounded-full ring-2 ring-transparent transition-all duration-300 ease-in-out
group-hover:ring-blue-200"
></div>
</div>
<h2
class="text-md font-semibold text-gray-700 mb-2 whitespace-nowrap"
>
{item.label}
</h2>
<p class={`text-3xl font-extrabold ${item.color}`}>
{#if isLoading}
<div class="h-8 bg-gray-200 rounded w-24 mx-auto"></div>
{:else}
{stats[item.key]}
{/if}
</p>
</div>
{/each}
</div>
</div>
<style>
/* Custom animations for a smoother feel */
@keyframes pulse-light {
0%,
100% {
background-color: #ffffff; /* white */
}
50% {
background-color: #f3f4f6; /* gray-100 */
}
}
.animate-pulse-light {
animation: pulse-light 2s infinite ease-in-out;
}
</style>