Files
mrp/client/src/modules/dashboard/DashboardPage.tsx
2026-03-15 11:12:58 -05:00

571 lines
30 KiB
TypeScript

import { permissions } from "@mrp/shared";
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
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;
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;
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 formatDateTime(value: string | null) {
if (!value) {
return "No recent activity";
}
return new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
}).format(new Date(value));
}
function sumNumber(values: number[]) {
return values.reduce((total, value) => total + value, 0);
}
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);
const canWriteManufacturing = hasPermission(user?.permissions, permissions.manufacturingWrite);
const canWriteProjects = hasPermission(user?.permissions, permissions.projectsWrite);
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 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),
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),
]);
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,
workOrders: results[4].status === "fulfilled" ? results[4].value : null,
quotes: results[5].status === "fulfilled" ? results[5].value : null,
orders: results[6].status === "fulfilled" ? results[6].value : null,
shipments: results[7].status === "fulfilled" ? results[7].value : null,
projects: results[8].status === "fulfilled" ? results[8].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 workOrders = snapshot?.workOrders ?? [];
const quotes = snapshot?.quotes ?? [];
const orders = snapshot?.orders ?? [];
const shipments = snapshot?.shipments ?? [];
const projects = snapshot?.projects ?? [];
const accessibleModules = [
snapshot?.customers !== null || snapshot?.vendors !== null,
snapshot?.items !== null || snapshot?.warehouses !== null,
snapshot?.workOrders !== null,
snapshot?.quotes !== null || snapshot?.orders !== null,
snapshot?.shipments !== null,
snapshot?.projects !== null,
].filter(Boolean).length;
const customerCount = customers.length;
const resellerCount = customers.filter((customer) => customer.isReseller).length;
const activeCustomerCount = customers.filter((customer) => customer.lifecycleStage === "ACTIVE").length;
const strategicCustomerCount = customers.filter((customer) => customer.strategicAccount).length;
const vendorCount = vendors.length;
const itemCount = items.length;
const assemblyCount = items.filter((item) => item.type === "ASSEMBLY" || item.type === "MANUFACTURED").length;
const activeItemCount = items.filter((item) => item.status === "ACTIVE").length;
const obsoleteItemCount = items.filter((item) => item.status === "OBSOLETE").length;
const warehouseCount = warehouses.length;
const locationCount = sumNumber(warehouses.map((warehouse) => warehouse.locationCount));
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 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 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 lastActivityAt = [
...customers.map((customer) => customer.updatedAt),
...vendors.map((vendor) => vendor.updatedAt),
...items.map((item) => item.updatedAt),
...warehouses.map((warehouse) => warehouse.updatedAt),
...workOrders.map((workOrder) => workOrder.updatedAt),
...quotes.map((quote) => quote.updatedAt),
...orders.map((order) => order.updatedAt),
...shipments.map((shipment) => shipment.updatedAt),
...projects.map((project) => project.updatedAt),
]
.sort()
.at(-1) ?? null;
const metricCards = [
{
label: "CRM Accounts",
value: snapshot?.customers !== null ? `${customerCount}` : "No access",
detail:
snapshot?.customers !== null
? `${vendorCount} vendors, ${resellerCount} resellers, ${activeCustomerCount} active`
: "CRM metrics are permission-gated.",
tone: "border-emerald-400/30 bg-emerald-500/12 text-emerald-700 dark:text-emerald-300",
},
{
label: "Inventory Footprint",
value: snapshot?.items !== null ? `${itemCount}` : "No access",
detail:
snapshot?.items !== null
? `${assemblyCount} buildable items across ${warehouseCount} warehouses`
: "Inventory metrics are permission-gated.",
tone: "border-sky-400/30 bg-sky-500/12 text-sky-700 dark:text-sky-300",
},
{
label: "Manufacturing Load",
value: snapshot?.workOrders !== null ? `${activeWorkOrderCount}` : "No access",
detail:
snapshot?.workOrders !== null
? `${releasedWorkOrderCount} released and ${overdueWorkOrderCount} overdue`
: "Manufacturing metrics are permission-gated.",
tone: "border-indigo-400/30 bg-indigo-500/12 text-indigo-700 dark:text-indigo-300",
},
{
label: "Commercial Value",
value: snapshot?.quotes !== null || snapshot?.orders !== null ? formatCurrency(quoteValue + orderValue) : "No access",
detail:
snapshot?.quotes !== null || snapshot?.orders !== null
? `${quoteCount} quotes and ${orderCount} orders in the pipeline`
: "Sales metrics are permission-gated.",
tone: "border-amber-400/30 bg-amber-500/12 text-amber-700 dark:text-amber-300",
},
{
label: "Shipping Queue",
value: snapshot?.shipments !== null ? `${activeShipmentCount}` : "No access",
detail:
snapshot?.shipments !== null
? `${inTransitCount} in transit, ${deliveredCount} delivered`
: "Shipping metrics are permission-gated.",
tone: "border-brand/30 bg-brand/10 text-brand",
},
{
label: "Project Load",
value: snapshot?.projects !== null ? `${activeProjectCount}` : "No access",
detail:
snapshot?.projects !== null
? `${atRiskProjectCount} at risk and ${overdueProjectCount} overdue`
: "Project metrics are permission-gated.",
tone: "border-violet-400/30 bg-violet-500/12 text-violet-700 dark:text-violet-300",
},
];
const modulePanels = [
{
title: "CRM",
eyebrow: "Account Health",
summary:
snapshot?.customers !== null
? "Live account counts, reseller coverage, and strategic-account concentration from the current CRM records."
: "CRM read permission is required to surface customer and vendor metrics here.",
metrics: [
{ label: "Customers", value: snapshot?.customers !== null ? `${customerCount}` : "No access" },
{ label: "Strategic", value: snapshot?.customers !== null ? `${strategicCustomerCount}` : "No access" },
{ label: "Vendors", value: snapshot?.vendors !== null ? `${vendorCount}` : "No access" },
],
links: [
{ label: "Open customers", to: "/crm/customers" },
{ label: "Open vendors", to: "/crm/vendors" },
],
},
{
title: "Inventory",
eyebrow: "Master + Stock",
summary:
snapshot?.items !== null
? "Item master, BOM-capable parts, and warehouse footprint are now feeding the dashboard directly."
: "Inventory read permission is required to surface item and warehouse metrics here.",
metrics: [
{ label: "Active items", value: snapshot?.items !== null ? `${activeItemCount}` : "No access" },
{ label: "Assemblies", value: snapshot?.items !== null ? `${assemblyCount}` : "No access" },
{ label: "Locations", value: snapshot?.warehouses !== null ? `${locationCount}` : "No access" },
],
links: [
{ label: "Open inventory", to: "/inventory/items" },
{ label: "Open warehouses", to: "/inventory/warehouses" },
],
},
{
title: "Manufacturing",
eyebrow: "Execution Load",
summary:
snapshot?.workOrders !== null
? "Work orders, released load, and overdue build pressure are now visible from the dashboard."
: "Manufacturing read permission is required to surface work-order metrics here.",
metrics: [
{ label: "Open work", value: snapshot?.workOrders !== null ? `${activeWorkOrderCount}` : "No access" },
{ label: "Released", value: snapshot?.workOrders !== null ? `${releasedWorkOrderCount}` : "No access" },
{ label: "Overdue", value: snapshot?.workOrders !== null ? `${overdueWorkOrderCount}` : "No access" },
],
links: [
{ label: "Open work orders", to: "/manufacturing/work-orders" },
...(canWriteManufacturing ? [{ label: "New work order", to: "/manufacturing/work-orders/new" }] : []),
],
},
{
title: "Sales",
eyebrow: "Revenue Flow",
summary:
snapshot?.quotes !== null || snapshot?.orders !== null
? "Quotes and sales orders now contribute real commercial value, open-document counts, and pipeline visibility."
: "Sales read permission is required to surface commercial metrics here.",
metrics: [
{ label: "Quote value", value: snapshot?.quotes !== null ? formatCurrency(quoteValue) : "No access" },
{ label: "Order value", value: snapshot?.orders !== null ? formatCurrency(orderValue) : "No access" },
{ label: "Draft quotes", value: snapshot?.quotes !== null ? `${draftQuoteCount}` : "No access" },
],
links: [
{ label: "Open quotes", to: "/sales/quotes" },
{ label: "Open sales orders", to: "/sales/orders" },
],
},
{
title: "Shipping",
eyebrow: "Execution Queue",
summary:
snapshot?.shipments !== null
? "Shipment records, in-transit volume, and completed deliveries are now visible from the landing page."
: "Shipping read permission is required to surface shipment metrics here.",
metrics: [
{ label: "Open shipments", value: snapshot?.shipments !== null ? `${activeShipmentCount}` : "No access" },
{ label: "In transit", value: snapshot?.shipments !== null ? `${inTransitCount}` : "No access" },
{ label: "Delivered", value: snapshot?.shipments !== null ? `${deliveredCount}` : "No access" },
],
links: [
{ label: "Open shipments", to: "/shipping/shipments" },
{ label: "Open packing flow", to: "/sales/orders" },
],
},
{
title: "Projects",
eyebrow: "Program Control",
summary:
snapshot?.projects !== null
? "Project records now tie customers, commercial documents, shipment context, and delivery ownership into one operational surface."
: "Project read permission is required to surface program metrics here.",
metrics: [
{ label: "Active", value: snapshot?.projects !== null ? `${activeProjectCount}` : "No access" },
{ label: "At risk", value: snapshot?.projects !== null ? `${atRiskProjectCount}` : "No access" },
{ label: "Overdue", value: snapshot?.projects !== null ? `${overdueProjectCount}` : "No access" },
],
links: [
{ label: "Open projects", to: "/projects" },
...(canWriteProjects ? [{ label: "New project", to: "/projects/new" }] : []),
],
},
];
const futureModules = [
"Vendor invoice attachments and supplier exception queues",
"Stock transfers, allocations, and cycle counts",
"Planning timeline, milestones, and dependency views",
"Audit trails, diagnostics, and system health checks",
];
return (
<div className="space-y-4">
<section className="overflow-hidden rounded-[30px] border border-line/70 bg-surface/90 shadow-panel backdrop-blur">
<div className="grid gap-0 xl:grid-cols-[1.35fr_0.65fr]">
<div className="relative overflow-hidden p-5 2xl:p-6">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(24,90,219,0.16),_transparent_44%),linear-gradient(135deg,rgba(255,255,255,0.06),transparent)]" />
<div className="relative">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Dashboard</p>
<h3 className="mt-2 max-w-3xl text-2xl font-extrabold text-text">Operational command surface for metrics, movement, and next actions.</h3>
</div>
<div className="rounded-2xl border border-line/70 bg-surface/80 px-2 py-2 text-right">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-muted">Last Refresh</p>
<p className="mt-1 text-sm font-semibold text-text">{snapshot ? formatDateTime(snapshot.refreshedAt) : "Waiting"}</p>
</div>
</div>
<p className="mt-3 max-w-3xl text-sm leading-6 text-muted">
This landing page now reads directly from live CRM, inventory, manufacturing, sales, shipping, and project data. It is intentionally
modular so future purchasing, planning, and audit slices can slot into the same command surface without a redesign.
</p>
<div className="mt-5 grid gap-2 sm:grid-cols-3">
<div className="rounded-2xl border border-line/70 bg-surface/80 px-2 py-2">
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-muted">Modules Live</p>
<p className="mt-1 text-lg font-extrabold text-text">{accessibleModules}</p>
</div>
<div className="rounded-2xl border border-line/70 bg-surface/80 px-2 py-2">
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-muted">Recent Activity</p>
<p className="mt-1 text-sm font-semibold text-text">{formatDateTime(lastActivityAt)}</p>
</div>
<div className="rounded-2xl border border-line/70 bg-surface/80 px-2 py-2">
<p className="text-[11px] font-semibold uppercase tracking-[0.2em] text-muted">Loading State</p>
<p className="mt-1 text-sm font-semibold text-text">{isLoading ? "Refreshing data" : "Live snapshot loaded"}</p>
</div>
</div>
<div className="mt-5 flex flex-wrap gap-3">
<Link className="rounded-2xl bg-brand px-3 py-2 text-sm font-semibold text-white" to="/sales/orders">
Open sales orders
</Link>
<Link className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text" to="/shipping/shipments">
Open shipments
</Link>
<Link className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text" to="/inventory/items">
Open inventory
</Link>
<Link className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text" to="/projects">
Open projects
</Link>
<Link className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text" to="/manufacturing/work-orders">
Open manufacturing
</Link>
</div>
{error ? <div className="mt-4 rounded-2xl border border-amber-400/30 bg-amber-500/12 px-2 py-2 text-sm text-amber-700 dark:text-amber-300">{error}</div> : null}
</div>
</div>
<div className="border-l border-line/70 bg-page/40 p-5 2xl:p-6">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Upgrade Path</p>
<div className="mt-4 space-y-3">
{futureModules.map((item) => (
<div key={item} className="rounded-2xl border border-line/70 bg-surface/80 px-2 py-2 text-sm text-text">
{item}
</div>
))}
</div>
</div>
</div>
</section>
<section className="grid gap-3 xl:grid-cols-6">
{metricCards.map((card) => (
<article key={card.label} className="rounded-[24px] 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 flex items-center justify-between gap-3">
<div className="text-xl font-extrabold text-text">{card.value}</div>
<span className={`rounded-full px-2 py-1 text-xs font-semibold ${card.tone}`}>Live</span>
</div>
<p className="mt-2 text-sm text-muted">{card.detail}</p>
</article>
))}
</section>
<section className="grid gap-3 xl:grid-cols-2 2xl:grid-cols-6">
{modulePanels.map((panel) => (
<article key={panel.title} className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">{panel.eyebrow}</p>
<h4 className="mt-2 text-lg font-bold text-text">{panel.title}</h4>
<p className="mt-3 text-sm leading-6 text-muted">{panel.summary}</p>
<div className="mt-5 grid gap-2">
{panel.metrics.map((metric) => (
<div key={metric.label} className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">{metric.label}</span>
<span className="font-semibold text-text">{metric.value}</span>
</div>
))}
</div>
<div className="mt-5 flex flex-wrap gap-2">
{panel.links.map((link) => (
<Link key={link.to} to={link.to} className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
{link.label}
</Link>
))}
</div>
</article>
))}
</section>
<section className="grid gap-3 xl:grid-cols-5">
<article className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory Watch</p>
<h4 className="mt-2 text-lg font-bold text-text">Master data pressure points</h4>
<div className="mt-4 grid gap-2">
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">Obsolete items</span>
<span className="font-semibold text-text">{snapshot?.items !== null ? `${obsoleteItemCount}` : "No access"}</span>
</div>
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">Warehouse count</span>
<span className="font-semibold text-text">{snapshot?.warehouses !== null ? `${warehouseCount}` : "No access"}</span>
</div>
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">Stock locations</span>
<span className="font-semibold text-text">{snapshot?.warehouses !== null ? `${locationCount}` : "No access"}</span>
</div>
</div>
</article>
<article className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Sales Watch</p>
<h4 className="mt-2 text-lg font-bold text-text">Commercial flow snapshot</h4>
<div className="mt-4 grid gap-2">
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">Issued orders</span>
<span className="font-semibold text-text">{snapshot?.orders !== null ? `${issuedOrderCount}` : "No access"}</span>
</div>
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">Draft quotes</span>
<span className="font-semibold text-text">{snapshot?.quotes !== null ? `${draftQuoteCount}` : "No access"}</span>
</div>
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">Order backlog</span>
<span className="font-semibold text-text">{snapshot?.orders !== null ? formatCurrency(orderValue) : "No access"}</span>
</div>
</div>
</article>
<article className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Manufacturing Watch</p>
<h4 className="mt-2 text-lg font-bold text-text">Build execution and due-date pressure</h4>
<div className="mt-4 grid gap-2">
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">Total work orders</span>
<span className="font-semibold text-text">{snapshot?.workOrders !== null ? `${workOrderCount}` : "No access"}</span>
</div>
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">Active queue</span>
<span className="font-semibold text-text">{snapshot?.workOrders !== null ? `${activeWorkOrderCount}` : "No access"}</span>
</div>
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">Overdue</span>
<span className="font-semibold text-text">{snapshot?.workOrders !== null ? `${overdueWorkOrderCount}` : "No access"}</span>
</div>
</div>
</article>
<article className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Project Watch</p>
<h4 className="mt-2 text-lg font-bold text-text">Program status and delivery pressure</h4>
<div className="mt-4 grid gap-2">
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">Total projects</span>
<span className="font-semibold text-text">{snapshot?.projects !== null ? `${projectCount}` : "No access"}</span>
</div>
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">At risk</span>
<span className="font-semibold text-text">{snapshot?.projects !== null ? `${atRiskProjectCount}` : "No access"}</span>
</div>
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">Overdue</span>
<span className="font-semibold text-text">{snapshot?.projects !== null ? `${overdueProjectCount}` : "No access"}</span>
</div>
</div>
</article>
<article className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Shipping Watch</p>
<h4 className="mt-2 text-lg font-bold text-text">Execution and delivery status</h4>
<div className="mt-4 grid gap-2">
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">Total shipments</span>
<span className="font-semibold text-text">{snapshot?.shipments !== null ? `${shipmentCount}` : "No access"}</span>
</div>
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">Open queue</span>
<span className="font-semibold text-text">{snapshot?.shipments !== null ? `${activeShipmentCount}` : "No access"}</span>
</div>
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">Delivered</span>
<span className="font-semibold text-text">{snapshot?.shipments !== null ? `${deliveredCount}` : "No access"}</span>
</div>
</div>
</article>
</section>
</div>
);
}