85 lines
3.5 KiB
TypeScript
85 lines
3.5 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { FileText, AlertCircle, Clock, CheckCircle2 } from "lucide-react";
|
|
import { getRecordView, getRVScreen } from "../api/viewService";
|
|
import { StatsSkeleton } from "./Skeleton";
|
|
import { ALL_INVOICES_RV_SCREEN_UUID, RECORD_VIEW_IDS } from "../config";
|
|
|
|
const ALL_INVOICES_VIEW = ALL_INVOICES_RV_SCREEN_UUID;
|
|
|
|
interface Stats {
|
|
total: number;
|
|
pendingClarification: number;
|
|
financeReview: number;
|
|
approved: number;
|
|
}
|
|
|
|
export default function DashboardStats() {
|
|
const [stats, setStats] = useState<Stats | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
async function fetchStats() {
|
|
try {
|
|
const rvScreen = await getRVScreen(ALL_INVOICES_VIEW);
|
|
const id = rvScreen.rv_template_uid;
|
|
const rvUid = RECORD_VIEW_IDS.ALL_INVOICES;
|
|
|
|
// Step 1: get exact total from pagination
|
|
const countRes = await getRecordView(id, rvUid, { page: 1, limit: 1 });
|
|
const total = countRes.pagination?.total_count ?? 0;
|
|
|
|
let pendingClarification = 0, financeReview = 0, approved = 0;
|
|
|
|
if (total > 0) {
|
|
// Step 2: fetch all rows in one call using exact total as limit
|
|
const allRes = await getRecordView(id, rvUid, { page: 1, limit: total });
|
|
for (const row of allRes.data ?? []) {
|
|
const state = String(row.current_state_name ?? "");
|
|
if (state === "Awaiting Clarification") pendingClarification++;
|
|
else if (state === "Finance Review") financeReview++;
|
|
else if (state === "Approved") approved++;
|
|
}
|
|
}
|
|
|
|
setStats({ total, pendingClarification, financeReview, approved });
|
|
} catch {
|
|
setStats({ total: 0, pendingClarification: 0, financeReview: 0, approved: 0 });
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
fetchStats();
|
|
}, []);
|
|
|
|
if (loading) return <StatsSkeleton />;
|
|
if (!stats) return null;
|
|
|
|
const cards = [
|
|
{ label: "Total Invoices", value: stats.total, icon: FileText, color: "text-slate-500 dark:text-zinc-400", bg: "bg-slate-100 dark:bg-zinc-800" },
|
|
{ label: "Pending Clarification", value: stats.pendingClarification, icon: AlertCircle, color: "text-amber-600 dark:text-amber-400", bg: "bg-amber-50 dark:bg-amber-950/40" },
|
|
{ label: "Finance Review", value: stats.financeReview, icon: Clock, color: "text-indigo-600 dark:text-indigo-400", bg: "bg-indigo-50 dark:bg-indigo-950/40" },
|
|
{ label: "Approved", value: stats.approved, icon: CheckCircle2, color: "text-emerald-600 dark:text-emerald-400", bg: "bg-emerald-50 dark:bg-emerald-950/40" },
|
|
];
|
|
|
|
return (
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-5">
|
|
{cards.map((card) => {
|
|
const Icon = card.icon;
|
|
return (
|
|
<div key={card.label} className="bg-white dark:bg-zinc-900 rounded-2xl border border-slate-200/80 dark:border-zinc-700/60 px-5 py-4 flex items-center gap-4">
|
|
<div className={`w-9 h-9 rounded-xl flex items-center justify-center shrink-0 ${card.bg}`}>
|
|
<Icon size={16} className={card.color} />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<div className="text-[22px] font-bold tabular-nums text-slate-900 dark:text-zinc-50 leading-none mb-0.5">
|
|
{card.value}
|
|
</div>
|
|
<div className="text-[11px] text-slate-400 dark:text-zinc-500 truncate">{card.label}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|