Files
codexium-odoo/client/src/modules/dashboard/DashboardPage.tsx
2026-03-16 14:38:00 -05:00

594 lines
30 KiB
TypeScript

import { permissions } from "@mrp/shared";
import type { DemandPlanningRollupDto } from "@mrp/shared/dist/sales/types.js";
import { useEffect, useState } from "react";
import type { ReactNode } from "react";
import { useAuth } from "../../auth/AuthProvider";
import { ApiError, api } from "../../lib/api";
interface DashboardSnapshot {
customers: Awaited<ReturnType<typeof api.getCustomers>> | null;
vendors: Awaited<ReturnType<typeof api.getVendors>> | null;
items: Awaited<ReturnType<typeof api.getInventoryItems>> | null;
warehouses: Awaited<ReturnType<typeof api.getWarehouses>> | null;
purchaseOrders: Awaited<ReturnType<typeof api.getPurchaseOrders>> | null;
workOrders: Awaited<ReturnType<typeof api.getWorkOrders>> | null;
quotes: Awaited<ReturnType<typeof api.getQuotes>> | null;
orders: Awaited<ReturnType<typeof api.getSalesOrders>> | null;
shipments: Awaited<ReturnType<typeof api.getShipments>> | null;
projects: Awaited<ReturnType<typeof api.getProjects>> | null;
planningRollup: DemandPlanningRollupDto | null;
refreshedAt: string;
}
function hasPermission(userPermissions: string[] | undefined, permission: string) {
return Boolean(userPermissions?.includes(permission));
}
function formatCurrency(value: number) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}).format(value);
}
function sumNumber(values: number[]) {
return values.reduce((total, value) => total + value, 0);
}
function formatPercent(value: number, total: number) {
if (total <= 0) {
return "0%";
}
return `${Math.round((value / total) * 100)}%`;
}
function ProgressBar({
value,
total,
tone,
}: {
value: number;
total: number;
tone: string;
}) {
const width = total > 0 ? Math.max(6, Math.round((value / total) * 100)) : 0;
return (
<div className="h-2 overflow-hidden rounded-full bg-page/80">
<div className={`h-full rounded-full ${tone}`} style={{ width: `${Math.min(width, 100)}%` }} />
</div>
);
}
function StackedBar({
segments,
}: {
segments: Array<{ value: number; tone: string }>;
}) {
const total = segments.reduce((sum, segment) => sum + segment.value, 0);
return (
<div className="flex h-3 overflow-hidden rounded-full bg-page/80">
{segments.map((segment, index) => {
const width = total > 0 ? (segment.value / total) * 100 : 0;
return <div key={`${segment.tone}-${index}`} className={segment.tone} style={{ width: `${width}%` }} />;
})}
</div>
);
}
function DashboardCard({
eyebrow,
title,
children,
className = "",
}: {
eyebrow: string;
title: string;
children: ReactNode;
className?: string;
}) {
return (
<article className={`rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5 ${className}`.trim()}>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">{eyebrow}</p>
<h3 className="mt-2 text-lg font-bold text-text">{title}</h3>
{children}
</article>
);
}
export function DashboardPage() {
const { token, user } = useAuth();
const [snapshot, setSnapshot] = useState<DashboardSnapshot | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!token || !user) {
setSnapshot(null);
setIsLoading(false);
return;
}
const authToken = token;
let isMounted = true;
setIsLoading(true);
setError(null);
const canReadCrm = hasPermission(user.permissions, permissions.crmRead);
const canReadInventory = hasPermission(user.permissions, permissions.inventoryRead);
const canReadPurchasing = hasPermission(user.permissions, permissions.purchasingRead);
const canReadManufacturing = hasPermission(user.permissions, permissions.manufacturingRead);
const canReadSales = hasPermission(user.permissions, permissions.salesRead);
const canReadShipping = hasPermission(user.permissions, permissions.shippingRead);
const canReadProjects = hasPermission(user.permissions, permissions.projectsRead);
async function loadSnapshot() {
const results = await Promise.allSettled([
canReadCrm ? api.getCustomers(authToken) : Promise.resolve(null),
canReadCrm ? api.getVendors(authToken) : Promise.resolve(null),
canReadInventory ? api.getInventoryItems(authToken) : Promise.resolve(null),
canReadInventory ? api.getWarehouses(authToken) : Promise.resolve(null),
canReadPurchasing ? api.getPurchaseOrders(authToken) : Promise.resolve(null),
canReadManufacturing ? api.getWorkOrders(authToken) : Promise.resolve(null),
canReadSales ? api.getQuotes(authToken) : Promise.resolve(null),
canReadSales ? api.getSalesOrders(authToken) : Promise.resolve(null),
canReadShipping ? api.getShipments(authToken) : Promise.resolve(null),
canReadProjects ? api.getProjects(authToken) : Promise.resolve(null),
canReadSales ? api.getDemandPlanningRollup(authToken) : Promise.resolve(null),
]);
if (!isMounted) {
return;
}
const firstRejected = results.find((result) => result.status === "rejected");
if (firstRejected?.status === "rejected") {
const reason = firstRejected.reason;
setError(reason instanceof ApiError ? reason.message : "Unable to load dashboard data.");
}
setSnapshot({
customers: results[0].status === "fulfilled" ? results[0].value : null,
vendors: results[1].status === "fulfilled" ? results[1].value : null,
items: results[2].status === "fulfilled" ? results[2].value : null,
warehouses: results[3].status === "fulfilled" ? results[3].value : null,
purchaseOrders: results[4].status === "fulfilled" ? results[4].value : null,
workOrders: results[5].status === "fulfilled" ? results[5].value : null,
quotes: results[6].status === "fulfilled" ? results[6].value : null,
orders: results[7].status === "fulfilled" ? results[7].value : null,
shipments: results[8].status === "fulfilled" ? results[8].value : null,
projects: results[9].status === "fulfilled" ? results[9].value : null,
planningRollup: results[10].status === "fulfilled" ? results[10].value : null,
refreshedAt: new Date().toISOString(),
});
setIsLoading(false);
}
loadSnapshot().catch((loadError) => {
if (!isMounted) {
return;
}
setError(loadError instanceof ApiError ? loadError.message : "Unable to load dashboard data.");
setSnapshot(null);
setIsLoading(false);
});
return () => {
isMounted = false;
};
}, [token, user]);
const customers = snapshot?.customers ?? [];
const vendors = snapshot?.vendors ?? [];
const items = snapshot?.items ?? [];
const warehouses = snapshot?.warehouses ?? [];
const purchaseOrders = snapshot?.purchaseOrders ?? [];
const workOrders = snapshot?.workOrders ?? [];
const quotes = snapshot?.quotes ?? [];
const orders = snapshot?.orders ?? [];
const shipments = snapshot?.shipments ?? [];
const projects = snapshot?.projects ?? [];
const planningRollup = snapshot?.planningRollup;
const customerCount = customers.length;
const activeCustomerCount = customers.filter((customer) => customer.lifecycleStage === "ACTIVE").length;
const resellerCount = customers.filter((customer) => customer.isReseller).length;
const strategicCustomerCount = customers.filter((customer) => customer.strategicAccount).length;
const vendorCount = vendors.length;
const itemCount = items.length;
const activeItemCount = items.filter((item) => item.status === "ACTIVE").length;
const assemblyCount = items.filter((item) => item.type === "ASSEMBLY" || item.type === "MANUFACTURED").length;
const obsoleteItemCount = items.filter((item) => item.status === "OBSOLETE").length;
const warehouseCount = warehouses.length;
const locationCount = sumNumber(warehouses.map((warehouse) => warehouse.locationCount));
const purchaseOrderCount = purchaseOrders.length;
const openPurchaseOrderCount = purchaseOrders.filter((order) => order.status !== "CLOSED").length;
const issuedPurchaseOrderCount = purchaseOrders.filter((order) => order.status === "ISSUED" || order.status === "APPROVED").length;
const closedPurchaseOrderCount = purchaseOrders.filter((order) => order.status === "CLOSED").length;
const purchaseOrderValue = sumNumber(purchaseOrders.map((order) => order.total));
const workOrderCount = workOrders.length;
const activeWorkOrderCount = workOrders.filter((workOrder) => workOrder.status === "RELEASED" || workOrder.status === "IN_PROGRESS" || workOrder.status === "ON_HOLD").length;
const releasedWorkOrderCount = workOrders.filter((workOrder) => workOrder.status === "RELEASED").length;
const inProgressWorkOrderCount = workOrders.filter((workOrder) => workOrder.status === "IN_PROGRESS").length;
const overdueWorkOrderCount = workOrders.filter((workOrder) => workOrder.dueDate && workOrder.status !== "COMPLETE" && workOrder.status !== "CANCELLED" && new Date(workOrder.dueDate).getTime() < Date.now()).length;
const quoteCount = quotes.length;
const orderCount = orders.length;
const draftQuoteCount = quotes.filter((quote) => quote.status === "DRAFT").length;
const approvedQuoteCount = quotes.filter((quote) => quote.status === "APPROVED").length;
const issuedOrderCount = orders.filter((order) => order.status === "ISSUED" || order.status === "APPROVED").length;
const quoteValue = sumNumber(quotes.map((quote) => quote.total));
const orderValue = sumNumber(orders.map((order) => order.total));
const shipmentCount = shipments.length;
const activeShipmentCount = shipments.filter((shipment) => shipment.status !== "DELIVERED").length;
const inTransitCount = shipments.filter((shipment) => shipment.status === "SHIPPED").length;
const deliveredCount = shipments.filter((shipment) => shipment.status === "DELIVERED").length;
const projectCount = projects.length;
const activeProjectCount = projects.filter((project) => project.status === "ACTIVE").length;
const atRiskProjectCount = projects.filter((project) => project.status === "AT_RISK").length;
const overdueProjectCount = projects.filter((project) => {
if (!project.dueDate || project.status === "COMPLETE") {
return false;
}
return new Date(project.dueDate).getTime() < Date.now();
}).length;
const shortageItemCount = planningRollup?.summary.uncoveredItemCount ?? 0;
const buyRecommendationCount = planningRollup?.summary.purchaseRecommendationCount ?? 0;
const buildRecommendationCount = planningRollup?.summary.buildRecommendationCount ?? 0;
const totalUncoveredQuantity = planningRollup?.summary.totalUncoveredQuantity ?? 0;
const planningItemCount = planningRollup?.summary.itemCount ?? 0;
const metricCards = [
{
label: "Accounts",
value: snapshot?.customers !== null ? `${customerCount + vendorCount}` : "No access",
secondary: snapshot?.customers !== null ? `${activeCustomerCount} active customers` : "",
tone: "bg-emerald-500",
},
{
label: "Inventory",
value: snapshot?.items !== null ? `${itemCount}` : "No access",
secondary: snapshot?.items !== null ? `${assemblyCount} buildable items` : "",
tone: "bg-sky-500",
},
{
label: "Open Supply",
value: snapshot?.purchaseOrders !== null || snapshot?.workOrders !== null ? `${openPurchaseOrderCount + activeWorkOrderCount}` : "No access",
secondary: snapshot?.purchaseOrders !== null ? `${openPurchaseOrderCount} PO | ${activeWorkOrderCount} WO` : "",
tone: "bg-teal-500",
},
{
label: "Commercial",
value: snapshot?.quotes !== null || snapshot?.orders !== null ? formatCurrency(quoteValue + orderValue) : "No access",
secondary: snapshot?.orders !== null ? `${orderCount} orders live` : "",
tone: "bg-amber-500",
},
{
label: "Projects",
value: snapshot?.projects !== null ? `${activeProjectCount}` : "No access",
secondary: snapshot?.projects !== null ? `${atRiskProjectCount} at risk` : "",
tone: "bg-violet-500",
},
{
label: "Readiness",
value: planningRollup ? `${shortageItemCount}` : "No access",
secondary: planningRollup ? `${totalUncoveredQuantity} units uncovered` : "",
tone: "bg-rose-500",
},
];
return (
<div className="space-y-4">
{error ? <div className="rounded-[18px] border border-amber-400/30 bg-amber-500/12 px-3 py-3 text-sm text-amber-700 dark:text-amber-300">{error}</div> : null}
<section className="grid gap-3 xl:grid-cols-6">
{metricCards.map((card) => (
<article key={card.label} className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{card.label}</p>
<div className="mt-2 text-xl font-extrabold text-text">{isLoading ? "Loading..." : card.value}</div>
<div className="mt-2 flex items-center gap-3">
<div className="h-2 flex-1 overflow-hidden rounded-full bg-page/80">
<div className={`h-full rounded-full ${card.tone}`} style={{ width: isLoading ? "35%" : "100%" }} />
</div>
<span className="text-xs text-muted">Live</span>
</div>
{card.secondary ? <div className="mt-2 text-xs text-muted">{card.secondary}</div> : null}
</article>
))}
</section>
<section className="grid gap-3 xl:grid-cols-[1.2fr_0.8fr]">
<DashboardCard eyebrow="Commercial Surface" title="Revenue and document mix">
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Quotes</div>
<div className="mt-2 text-2xl font-bold text-text">{snapshot?.quotes !== null ? formatCurrency(quoteValue) : "No access"}</div>
<div className="mt-3 space-y-2">
<div className="flex items-center justify-between text-xs text-muted">
<span>Draft</span>
<span>{draftQuoteCount}</span>
</div>
<ProgressBar value={draftQuoteCount} total={Math.max(quoteCount, 1)} tone="bg-amber-500" />
<div className="flex items-center justify-between text-xs text-muted">
<span>Approved</span>
<span>{approvedQuoteCount}</span>
</div>
<ProgressBar value={approvedQuoteCount} total={Math.max(quoteCount, 1)} tone="bg-emerald-500" />
</div>
</div>
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Orders</div>
<div className="mt-2 text-2xl font-bold text-text">{snapshot?.orders !== null ? formatCurrency(orderValue) : "No access"}</div>
<div className="mt-3 grid gap-3 sm:grid-cols-2">
<div>
<div className="text-xs text-muted">Issued / approved</div>
<div className="mt-1 text-lg font-semibold text-text">{issuedOrderCount}</div>
</div>
<div>
<div className="text-xs text-muted">Total orders</div>
<div className="mt-1 text-lg font-semibold text-text">{orderCount}</div>
</div>
</div>
<div className="mt-3">
<ProgressBar value={issuedOrderCount} total={Math.max(orderCount, 1)} tone="bg-brand" />
</div>
</div>
</div>
</DashboardCard>
<DashboardCard eyebrow="CRM Footprint" title="Customer and vendor balance">
<div className="mt-4 space-y-4">
<div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted">Active customers</span>
<span className="font-semibold text-text">{snapshot?.customers !== null ? formatPercent(activeCustomerCount, Math.max(customerCount, 1)) : "No access"}</span>
</div>
<div className="mt-2">
<ProgressBar value={activeCustomerCount} total={Math.max(customerCount, 1)} tone="bg-emerald-500" />
</div>
</div>
<div className="grid gap-3 sm:grid-cols-3">
<div className="rounded-[16px] border border-line/70 bg-page/60 px-3 py-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Customers</div>
<div className="mt-1 text-lg font-bold text-text">{customerCount}</div>
</div>
<div className="rounded-[16px] border border-line/70 bg-page/60 px-3 py-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Resellers</div>
<div className="mt-1 text-lg font-bold text-text">{resellerCount}</div>
</div>
<div className="rounded-[16px] border border-line/70 bg-page/60 px-3 py-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Vendors</div>
<div className="mt-1 text-lg font-bold text-text">{vendorCount}</div>
</div>
</div>
<div className="rounded-[16px] border border-line/70 bg-page/60 px-3 py-3">
<div className="flex items-center justify-between text-sm">
<span className="text-muted">Strategic accounts</span>
<span className="font-semibold text-text">{strategicCustomerCount}</span>
</div>
<div className="mt-2">
<ProgressBar value={strategicCustomerCount} total={Math.max(customerCount, 1)} tone="bg-violet-500" />
</div>
</div>
</div>
</DashboardCard>
</section>
<section className="grid gap-3 xl:grid-cols-3">
<DashboardCard eyebrow="Inventory and Supply" title="Stock posture">
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Item mix</div>
<div className="mt-3 space-y-3">
<div>
<div className="flex items-center justify-between text-xs text-muted">
<span>Active items</span>
<span>{activeItemCount}</span>
</div>
<div className="mt-2">
<ProgressBar value={activeItemCount} total={Math.max(itemCount, 1)} tone="bg-sky-500" />
</div>
</div>
<div>
<div className="flex items-center justify-between text-xs text-muted">
<span>Buildable items</span>
<span>{assemblyCount}</span>
</div>
<div className="mt-2">
<ProgressBar value={assemblyCount} total={Math.max(itemCount, 1)} tone="bg-indigo-500" />
</div>
</div>
<div>
<div className="flex items-center justify-between text-xs text-muted">
<span>Obsolete items</span>
<span>{obsoleteItemCount}</span>
</div>
<div className="mt-2">
<ProgressBar value={obsoleteItemCount} total={Math.max(itemCount, 1)} tone="bg-slate-500" />
</div>
</div>
</div>
</div>
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Storage surface</div>
<div className="mt-3 grid gap-3">
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<div className="text-xs text-muted">Warehouses</div>
<div className="mt-1 text-lg font-bold text-text">{warehouseCount}</div>
</div>
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<div className="text-xs text-muted">Locations</div>
<div className="mt-1 text-lg font-bold text-text">{locationCount}</div>
</div>
</div>
</div>
</div>
</DashboardCard>
<DashboardCard eyebrow="Supply Execution" title="Purchasing and manufacturing flow">
<div className="mt-4 rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Open workload split</div>
<div className="mt-3">
<StackedBar
segments={[
{ value: openPurchaseOrderCount, tone: "bg-teal-500" },
{ value: releasedWorkOrderCount, tone: "bg-indigo-500" },
{ value: inProgressWorkOrderCount, tone: "bg-amber-500" },
{ value: overdueWorkOrderCount, tone: "bg-rose-500" },
]}
/>
</div>
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<div className="text-xs text-muted">Open PO queue</div>
<div className="mt-1 text-lg font-bold text-text">{openPurchaseOrderCount}</div>
<div className="mt-1 text-xs text-muted">{formatCurrency(purchaseOrderValue)} committed</div>
</div>
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<div className="text-xs text-muted">Active work orders</div>
<div className="mt-1 text-lg font-bold text-text">{activeWorkOrderCount}</div>
<div className="mt-1 text-xs text-muted">{overdueWorkOrderCount} overdue</div>
</div>
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<div className="text-xs text-muted">Issued / approved POs</div>
<div className="mt-1 text-lg font-bold text-text">{issuedPurchaseOrderCount}</div>
</div>
<div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<div className="text-xs text-muted">Released WOs</div>
<div className="mt-1 text-lg font-bold text-text">{releasedWorkOrderCount}</div>
</div>
</div>
</div>
</DashboardCard>
<DashboardCard eyebrow="Readiness" title="Planning pressure">
<div className="mt-4 space-y-3">
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="flex items-center justify-between text-sm">
<span className="text-muted">Shortage items</span>
<span className="font-semibold text-text">{planningRollup ? shortageItemCount : "No access"}</span>
</div>
<div className="mt-2">
<ProgressBar value={shortageItemCount} total={Math.max(planningItemCount, 1)} tone="bg-rose-500" />
</div>
</div>
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Build vs buy</div>
<div className="mt-3">
<StackedBar
segments={[
{ value: buildRecommendationCount, tone: "bg-indigo-500" },
{ value: buyRecommendationCount, tone: "bg-teal-500" },
]}
/>
</div>
<div className="mt-3 grid gap-3 sm:grid-cols-2">
<div>
<div className="text-xs text-muted">Build recommendations</div>
<div className="mt-1 text-lg font-bold text-text">{planningRollup ? buildRecommendationCount : "No access"}</div>
</div>
<div>
<div className="text-xs text-muted">Buy recommendations</div>
<div className="mt-1 text-lg font-bold text-text">{planningRollup ? buyRecommendationCount : "No access"}</div>
</div>
</div>
</div>
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs text-muted">Uncovered quantity</div>
<div className="mt-1 text-lg font-bold text-text">{planningRollup ? totalUncoveredQuantity : "No access"}</div>
</div>
</div>
</DashboardCard>
</section>
<section className="grid gap-3 xl:grid-cols-[0.95fr_1.05fr]">
<DashboardCard eyebrow="Programs" title="Project and shipment execution">
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Projects</div>
<div className="mt-3">
<StackedBar
segments={[
{ value: activeProjectCount, tone: "bg-violet-500" },
{ value: atRiskProjectCount, tone: "bg-amber-500" },
{ value: overdueProjectCount, tone: "bg-rose-500" },
]}
/>
</div>
<div className="mt-4 grid gap-3 sm:grid-cols-3">
<div>
<div className="text-xs text-muted">Active</div>
<div className="mt-1 font-semibold text-text">{activeProjectCount}</div>
</div>
<div>
<div className="text-xs text-muted">At risk</div>
<div className="mt-1 font-semibold text-text">{atRiskProjectCount}</div>
</div>
<div>
<div className="text-xs text-muted">Overdue</div>
<div className="mt-1 font-semibold text-text">{overdueProjectCount}</div>
</div>
</div>
</div>
<div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Shipping</div>
<div className="mt-3">
<StackedBar
segments={[
{ value: activeShipmentCount, tone: "bg-brand" },
{ value: inTransitCount, tone: "bg-sky-500" },
{ value: deliveredCount, tone: "bg-emerald-500" },
]}
/>
</div>
<div className="mt-4 grid gap-3 sm:grid-cols-3">
<div>
<div className="text-xs text-muted">Open</div>
<div className="mt-1 font-semibold text-text">{activeShipmentCount}</div>
</div>
<div>
<div className="text-xs text-muted">In transit</div>
<div className="mt-1 font-semibold text-text">{inTransitCount}</div>
</div>
<div>
<div className="text-xs text-muted">Delivered</div>
<div className="mt-1 font-semibold text-text">{deliveredCount}</div>
</div>
</div>
</div>
</div>
</DashboardCard>
<DashboardCard eyebrow="Operations Mix" title="Cross-module volume">
<div className="mt-4 space-y-3">
{[
{ label: "Customers", value: customerCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-emerald-500" },
{ label: "Inventory items", value: itemCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-sky-500" },
{ label: "Sales orders", value: orderCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-amber-500" },
{ label: "Purchase orders", value: purchaseOrderCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-teal-500" },
{ label: "Work orders", value: workOrderCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-indigo-500" },
{ label: "Shipments", value: shipmentCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-brand" },
{ label: "Projects", value: projectCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-violet-500" },
].map((row) => (
<div key={row.label} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="flex items-center justify-between text-sm">
<span className="text-muted">{row.label}</span>
<span className="font-semibold text-text">{row.value}</span>
</div>
<div className="mt-2">
<ProgressBar value={row.value} total={row.total} tone={row.tone} />
</div>
</div>
))}
</div>
{snapshot ? <div className="mt-4 text-xs text-muted">Refreshed {new Date(snapshot.refreshedAt).toLocaleString()}</div> : null}
</DashboardCard>
</section>
</div>
);
}