243 lines
7.3 KiB
Svelte
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>
|