// --------------------------------------------------------------------------- // KpiCard — pre-built KPI metric card wired to AnyDB aggregate // --------------------------------------------------------------------------- // PRE-INSTALLED. CodeWriterAgent just uses: // import { KpiCard } from "@/components/KpiCard" // // // // --------------------------------------------------------------------------- import { useMemo } from "react"; import { useAggregate, useCollection } from "@/hooks/use-anydb"; import { Card, CardContent } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { LucideIcon } from "lucide-react"; interface KpiCardProps { collection: string; metric: "count" | "sum" | "avg" | "min" | "max"; field?: string; filter?: Record; label: string; icon?: LucideIcon; format?: "number" | "currency" | "percent"; prefix?: string; suffix?: string; trend?: number; // e.g. 12.5 for +12.5% className?: string; } export function KpiCard({ collection, metric, field, filter = {}, label, icon: Icon, format = "number", prefix = "", suffix = "", trend, className = "", }: KpiCardProps) { const pipeline = useMemo(() => { const stages: any[] = []; if (Object.keys(filter).length > 0) { stages.push({ $match: filter }); } switch (metric) { case "count": stages.push({ $count: "value" }); break; case "sum": stages.push({ $group: { _id: null, value: { $sum: `$${field}` } } }); break; case "avg": stages.push({ $group: { _id: null, value: { $avg: `$${field}` } } }); break; case "min": stages.push({ $group: { _id: null, value: { $min: `$${field}` } } }); break; case "max": stages.push({ $group: { _id: null, value: { $max: `$${field}` } } }); break; } return stages; }, [metric, field, JSON.stringify(filter)]); // For count, we can use the simpler useCollection total const useSimpleCount = metric === "count" && Object.keys(filter).length === 0; const { total: simpleTotal, loading: simpleLoading } = useCollection(collection, { limit: 1, autoFetch: useSimpleCount, }); const { data: aggData, loading: aggLoading } = useAggregate(collection, pipeline); const loading = useSimpleCount ? simpleLoading : aggLoading; const rawValue = useSimpleCount ? simpleTotal : aggData?.[0]?.value ?? 0; const formatValue = (val: number): string => { switch (format) { case "currency": return `$${(val / 100).toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`; case "percent": return `${val.toFixed(1)}%`; default: return val >= 1000 ? `${(val / 1000).toFixed(1)}K` : val.toLocaleString(); } }; return (

{label}

{loading ? ( ) : (

{prefix}{formatValue(rawValue)}{suffix}

)} {trend !== undefined && !loading && (

= 0 ? "text-emerald-600" : "text-red-500"}`}> {trend >= 0 ? "↑" : "↓"} {Math.abs(trend)}%

)}
{Icon && (
)}
); }