p2p/src/components/DashboardStats.tsx

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>
);
}