dashboard cleanup

This commit is contained in:
2026-03-15 21:17:54 -05:00
parent a43374fe77
commit ac0c6e4365

View File

@@ -1,7 +1,7 @@
import { permissions } from "@mrp/shared"; import { permissions } from "@mrp/shared";
import type { DemandPlanningRollupDto } from "@mrp/shared/dist/sales/types.js"; import type { DemandPlanningRollupDto } from "@mrp/shared/dist/sales/types.js";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Link } from "react-router-dom"; import type { ReactNode } from "react";
import { useAuth } from "../../auth/AuthProvider"; import { useAuth } from "../../auth/AuthProvider";
import { ApiError, api } from "../../lib/api"; import { ApiError, api } from "../../lib/api";
@@ -37,14 +37,74 @@ function sumNumber(values: number[]) {
return values.reduce((total, value) => total + value, 0); 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() { export function DashboardPage() {
const { token, user } = useAuth(); const { token, user } = useAuth();
const [snapshot, setSnapshot] = useState<DashboardSnapshot | null>(null); const [snapshot, setSnapshot] = useState<DashboardSnapshot | null>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const canWriteManufacturing = hasPermission(user?.permissions, permissions.manufacturingWrite);
const canWriteProjects = hasPermission(user?.permissions, permissions.projectsWrite);
const canReadPlanning = hasPermission(user?.permissions, permissions.ganttRead);
useEffect(() => { useEffect(() => {
if (!token || !user) { if (!token || !user) {
@@ -76,9 +136,9 @@ export function DashboardPage() {
canReadManufacturing ? api.getWorkOrders(authToken) : Promise.resolve(null), canReadManufacturing ? api.getWorkOrders(authToken) : Promise.resolve(null),
canReadSales ? api.getQuotes(authToken) : Promise.resolve(null), canReadSales ? api.getQuotes(authToken) : Promise.resolve(null),
canReadSales ? api.getSalesOrders(authToken) : Promise.resolve(null), canReadSales ? api.getSalesOrders(authToken) : Promise.resolve(null),
canReadShipping ? api.getShipments(authToken) : Promise.resolve(null), canReadShipping ? api.getShipments(authToken) : Promise.resolve(null),
canReadProjects ? api.getProjects(authToken) : Promise.resolve(null), canReadProjects ? api.getProjects(authToken) : Promise.resolve(null),
canReadSales ? api.getDemandPlanningRollup(authToken) : Promise.resolve(null), canReadSales ? api.getDemandPlanningRollup(authToken) : Promise.resolve(null),
]); ]);
if (!isMounted) { if (!isMounted) {
@@ -136,14 +196,14 @@ export function DashboardPage() {
const planningRollup = snapshot?.planningRollup; const planningRollup = snapshot?.planningRollup;
const customerCount = customers.length; const customerCount = customers.length;
const resellerCount = customers.filter((customer) => customer.isReseller).length;
const activeCustomerCount = customers.filter((customer) => customer.lifecycleStage === "ACTIVE").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 strategicCustomerCount = customers.filter((customer) => customer.strategicAccount).length;
const vendorCount = vendors.length; const vendorCount = vendors.length;
const itemCount = items.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 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 obsoleteItemCount = items.filter((item) => item.status === "OBSOLETE").length;
const warehouseCount = warehouses.length; const warehouseCount = warehouses.length;
const locationCount = sumNumber(warehouses.map((warehouse) => warehouse.locationCount)); const locationCount = sumNumber(warehouses.map((warehouse) => warehouse.locationCount));
@@ -151,16 +211,19 @@ export function DashboardPage() {
const purchaseOrderCount = purchaseOrders.length; const purchaseOrderCount = purchaseOrders.length;
const openPurchaseOrderCount = purchaseOrders.filter((order) => order.status !== "CLOSED").length; const openPurchaseOrderCount = purchaseOrders.filter((order) => order.status !== "CLOSED").length;
const issuedPurchaseOrderCount = purchaseOrders.filter((order) => order.status === "ISSUED" || order.status === "APPROVED").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 purchaseOrderValue = sumNumber(purchaseOrders.map((order) => order.total));
const workOrderCount = workOrders.length; const workOrderCount = workOrders.length;
const activeWorkOrderCount = workOrders.filter((workOrder) => workOrder.status === "RELEASED" || workOrder.status === "IN_PROGRESS" || workOrder.status === "ON_HOLD").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 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 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 quoteCount = quotes.length;
const orderCount = orders.length; const orderCount = orders.length;
const draftQuoteCount = quotes.filter((quote) => quote.status === "DRAFT").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 issuedOrderCount = orders.filter((order) => order.status === "ISSUED" || order.status === "APPROVED").length;
const quoteValue = sumNumber(quotes.map((quote) => quote.total)); const quoteValue = sumNumber(quotes.map((quote) => quote.total));
const orderValue = sumNumber(orders.map((order) => order.total)); const orderValue = sumNumber(orders.map((order) => order.total));
@@ -180,309 +243,350 @@ export function DashboardPage() {
return new Date(project.dueDate).getTime() < Date.now(); return new Date(project.dueDate).getTime() < Date.now();
}).length; }).length;
const shortageItemCount = planningRollup?.summary.uncoveredItemCount ?? 0; const shortageItemCount = planningRollup?.summary.uncoveredItemCount ?? 0;
const buyRecommendationCount = planningRollup?.summary.purchaseRecommendationCount ?? 0; const buyRecommendationCount = planningRollup?.summary.purchaseRecommendationCount ?? 0;
const buildRecommendationCount = planningRollup?.summary.buildRecommendationCount ?? 0; const buildRecommendationCount = planningRollup?.summary.buildRecommendationCount ?? 0;
const totalUncoveredQuantity = planningRollup?.summary.totalUncoveredQuantity ?? 0; const totalUncoveredQuantity = planningRollup?.summary.totalUncoveredQuantity ?? 0;
const planningItemCount = planningRollup?.summary.itemCount ?? 0;
const metricCards = [ const metricCards = [
{ {
label: "CRM Accounts", label: "Accounts",
value: snapshot?.customers !== null ? `${customerCount}` : "No access", value: snapshot?.customers !== null ? `${customerCount + vendorCount}` : "No access",
tone: "border-emerald-400/30 bg-emerald-500/12 text-emerald-700 dark:text-emerald-300", secondary: snapshot?.customers !== null ? `${activeCustomerCount} active customers` : "",
tone: "bg-emerald-500",
}, },
{ {
label: "Inventory Footprint", label: "Inventory",
value: snapshot?.items !== null ? `${itemCount}` : "No access", value: snapshot?.items !== null ? `${itemCount}` : "No access",
tone: "border-sky-400/30 bg-sky-500/12 text-sky-700 dark:text-sky-300", secondary: snapshot?.items !== null ? `${assemblyCount} buildable items` : "",
tone: "bg-sky-500",
}, },
{ {
label: "Purchasing Queue", label: "Open Supply",
value: snapshot?.purchaseOrders !== null ? `${openPurchaseOrderCount}` : "No access", value: snapshot?.purchaseOrders !== null || snapshot?.workOrders !== null ? `${openPurchaseOrderCount + activeWorkOrderCount}` : "No access",
tone: "border-teal-400/30 bg-teal-500/12 text-teal-700 dark:text-teal-300", secondary: snapshot?.purchaseOrders !== null ? `${openPurchaseOrderCount} PO | ${activeWorkOrderCount} WO` : "",
tone: "bg-teal-500",
}, },
{ {
label: "Manufacturing Load", label: "Commercial",
value: snapshot?.workOrders !== null ? `${activeWorkOrderCount}` : "No access",
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", value: snapshot?.quotes !== null || snapshot?.orders !== null ? formatCurrency(quoteValue + orderValue) : "No access",
tone: "border-amber-400/30 bg-amber-500/12 text-amber-700 dark:text-amber-300", secondary: snapshot?.orders !== null ? `${orderCount} orders live` : "",
tone: "bg-amber-500",
}, },
{ {
label: "Shipping Queue", label: "Projects",
value: snapshot?.shipments !== null ? `${activeShipmentCount}` : "No access",
tone: "border-brand/30 bg-brand/10 text-brand",
},
{
label: "Project Load",
value: snapshot?.projects !== null ? `${activeProjectCount}` : "No access", value: snapshot?.projects !== null ? `${activeProjectCount}` : "No access",
tone: "border-violet-400/30 bg-violet-500/12 text-violet-700 dark:text-violet-300", secondary: snapshot?.projects !== null ? `${atRiskProjectCount} at risk` : "",
tone: "bg-violet-500",
}, },
{ {
label: "Material Readiness", label: "Readiness",
value: planningRollup ? `${shortageItemCount}` : "No access", value: planningRollup ? `${shortageItemCount}` : "No access",
tone: "border-rose-400/30 bg-rose-500/12 text-rose-700 dark:text-rose-300", secondary: planningRollup ? `${totalUncoveredQuantity} units uncovered` : "",
}, tone: "bg-rose-500",
];
const modulePanels = [
{
title: "CRM",
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",
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: "Purchasing",
metrics: [
{ label: "Open POs", value: snapshot?.purchaseOrders !== null ? `${openPurchaseOrderCount}` : "No access" },
{ label: "Issued", value: snapshot?.purchaseOrders !== null ? `${issuedPurchaseOrderCount}` : "No access" },
{ label: "Committed", value: snapshot?.purchaseOrders !== null ? formatCurrency(purchaseOrderValue) : "No access" },
],
links: [
{ label: "Open purchase orders", to: "/purchasing/orders" },
],
},
{
title: "Manufacturing",
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",
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",
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",
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" }] : []),
],
},
{
title: "Planning",
metrics: [
{ label: "At risk projects", value: canReadPlanning ? `${atRiskProjectCount}` : "No access" },
{ label: "Shortage items", value: canReadPlanning && planningRollup ? `${shortageItemCount}` : "No access" },
{ label: "Build / buy", value: canReadPlanning && planningRollup ? `${buildRecommendationCount} / ${buyRecommendationCount}` : "No access" },
],
links: canReadPlanning ? [{ label: "Open gantt", to: "/planning/gantt" }] : [],
}, },
]; ];
return ( return (
<div className="space-y-4"> <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} {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-7"> <section className="grid gap-3 xl:grid-cols-6">
{metricCards.map((card) => ( {metricCards.map((card) => (
<article key={card.label} className="rounded-[18px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel"> <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> <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="mt-2 text-xl font-extrabold text-text">{isLoading ? "Loading..." : card.value}</div>
<div className="text-xl font-extrabold text-text">{card.value}</div> <div className="mt-2 flex items-center gap-3">
<span className={`rounded-lg px-2 py-1 text-xs font-semibold ${card.tone}`}>Live</span> <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> </div>
{card.secondary ? <div className="mt-2 text-xs text-muted">{card.secondary}</div> : null}
</article> </article>
))} ))}
</section> </section>
<section className="grid gap-3 xl:grid-cols-2 2xl:grid-cols-7"> <section className="grid gap-3 xl:grid-cols-[1.2fr_0.8fr]">
{modulePanels.map((panel) => ( <DashboardCard eyebrow="Commercial Surface" title="Revenue and document mix">
<article key={panel.title} className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5"> <div className="mt-4 grid gap-3 sm:grid-cols-2">
<h4 className="text-lg font-bold text-text">{panel.title}</h4> <div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="mt-4 grid gap-2"> <div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Quotes</div>
{panel.metrics.map((metric) => ( <div className="mt-2 text-2xl font-bold text-text">{snapshot?.quotes !== null ? formatCurrency(quoteValue) : "No access"}</div>
<div key={metric.label} className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> <div className="mt-3 space-y-2">
<span className="text-muted">{metric.label}</span> <div className="flex items-center justify-between text-xs text-muted">
<span className="font-semibold text-text">{metric.value}</span> <span>Draft</span>
<span>{draftQuoteCount}</span>
</div> </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>
<div className="mt-5 flex flex-wrap gap-2"> <div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
{panel.links.map((link) => ( <div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Orders</div>
<Link key={link.to} to={link.to} className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text"> <div className="mt-2 text-2xl font-bold text-text">{snapshot?.orders !== null ? formatCurrency(orderValue) : "No access"}</div>
{link.label} <div className="mt-3 grid gap-3 sm:grid-cols-2">
</Link> <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>
</article> </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>
<section className="grid gap-3 xl:grid-cols-6"> <section className="grid gap-3 xl:grid-cols-3">
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5"> <DashboardCard eyebrow="Inventory and Supply" title="Stock posture">
<h4 className="text-lg font-bold text-text">Planning</h4> <div className="mt-4 grid gap-3 sm:grid-cols-2">
<div className="mt-4 grid gap-2"> <div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> <div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Item mix</div>
<span className="text-muted">Shortage items</span> <div className="mt-3 space-y-3">
<span className="font-semibold text-text">{planningRollup ? `${shortageItemCount}` : "No access"}</span> <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>
<div className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> <div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<span className="text-muted">Build recommendations</span> <div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Storage surface</div>
<span className="font-semibold text-text">{planningRollup ? `${buildRecommendationCount}` : "No access"}</span> <div className="mt-3 grid gap-3">
</div> <div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<div className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> <div className="text-xs text-muted">Warehouses</div>
<span className="text-muted">Buy recommendations</span> <div className="mt-1 text-lg font-bold text-text">{warehouseCount}</div>
<span className="font-semibold text-text">{planningRollup ? `${buyRecommendationCount}` : "No access"}</span> </div>
</div> <div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<div className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> <div className="text-xs text-muted">Locations</div>
<span className="text-muted">Uncovered qty</span> <div className="mt-1 text-lg font-bold text-text">{locationCount}</div>
<span className="font-semibold text-text">{planningRollup ? `${totalUncoveredQuantity}` : "No access"}</span> </div>
</div>
</div> </div>
</div> </div>
</article> </DashboardCard>
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5"> <DashboardCard eyebrow="Supply Execution" title="Purchasing and manufacturing flow">
<h4 className="text-lg font-bold text-text">Inventory</h4> <div className="mt-4 rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="mt-4 grid gap-2"> <div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Open workload split</div>
<div className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> <div className="mt-3">
<span className="text-muted">Obsolete items</span> <StackedBar
<span className="font-semibold text-text">{snapshot?.items !== null ? `${obsoleteItemCount}` : "No access"}</span> 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>
<div className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> <div className="mt-4 grid gap-3 sm:grid-cols-2">
<span className="text-muted">Warehouse count</span> <div className="rounded-[16px] border border-line/70 bg-surface/80 px-3 py-3">
<span className="font-semibold text-text">{snapshot?.warehouses !== null ? `${warehouseCount}` : "No access"}</span> <div className="text-xs text-muted">Open PO queue</div>
</div> <div className="mt-1 text-lg font-bold text-text">{openPurchaseOrderCount}</div>
<div className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> <div className="mt-1 text-xs text-muted">{formatCurrency(purchaseOrderValue)} committed</div>
<span className="text-muted">Stock locations</span> </div>
<span className="font-semibold text-text">{snapshot?.warehouses !== null ? `${locationCount}` : "No access"}</span> <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>
</div> </div>
</article> </DashboardCard>
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5"> <DashboardCard eyebrow="Readiness" title="Planning pressure">
<h4 className="text-lg font-bold text-text">Sales</h4> <div className="mt-4 space-y-3">
<div className="mt-4 grid gap-2"> <div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<div className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> <div className="flex items-center justify-between text-sm">
<span className="text-muted">Issued orders</span> <span className="text-muted">Shortage items</span>
<span className="font-semibold text-text">{snapshot?.orders !== null ? `${issuedOrderCount}` : "No access"}</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>
<div className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> <div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<span className="text-muted">Draft quotes</span> <div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Build vs buy</div>
<span className="font-semibold text-text">{snapshot?.quotes !== null ? `${draftQuoteCount}` : "No access"}</span> <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>
<div className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> <div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<span className="text-muted">Order backlog</span> <div className="text-xs text-muted">Uncovered quantity</div>
<span className="font-semibold text-text">{snapshot?.orders !== null ? formatCurrency(orderValue) : "No access"}</span> <div className="mt-1 text-lg font-bold text-text">{planningRollup ? totalUncoveredQuantity : "No access"}</div>
</div> </div>
</div> </div>
</article> </DashboardCard>
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5"> </section>
<h4 className="text-lg font-bold text-text">Purchasing</h4> <section className="grid gap-3 xl:grid-cols-[0.95fr_1.05fr]">
<div className="mt-4 grid gap-2"> <DashboardCard eyebrow="Programs" title="Project and shipment execution">
<div className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> <div className="mt-4 grid gap-3 sm:grid-cols-2">
<span className="text-muted">Total purchase orders</span> <div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<span className="font-semibold text-text">{snapshot?.purchaseOrders !== null ? `${purchaseOrderCount}` : "No access"}</span> <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>
<div className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> <div className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<span className="text-muted">Open queue</span> <div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Shipping</div>
<span className="font-semibold text-text">{snapshot?.purchaseOrders !== null ? `${openPurchaseOrderCount}` : "No access"}</span> <div className="mt-3">
</div> <StackedBar
<div className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> segments={[
<span className="text-muted">Committed value</span> { value: activeShipmentCount, tone: "bg-brand" },
<span className="font-semibold text-text">{snapshot?.purchaseOrders !== null ? formatCurrency(purchaseOrderValue) : "No access"}</span> { 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>
</div> </div>
</article> </DashboardCard>
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5"> <DashboardCard eyebrow="Operations Mix" title="Cross-module volume">
<h4 className="text-lg font-bold text-text">Manufacturing</h4> <div className="mt-4 space-y-3">
<div className="mt-4 grid gap-2"> {[
<div className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> { label: "Customers", value: customerCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-emerald-500" },
<span className="text-muted">Total work orders</span> { label: "Inventory items", value: itemCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-sky-500" },
<span className="font-semibold text-text">{snapshot?.workOrders !== null ? `${workOrderCount}` : "No access"}</span> { label: "Sales orders", value: orderCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-amber-500" },
</div> { label: "Purchase orders", value: purchaseOrderCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-teal-500" },
<div className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> { label: "Work orders", value: workOrderCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-indigo-500" },
<span className="text-muted">Active queue</span> { label: "Shipments", value: shipmentCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-brand" },
<span className="font-semibold text-text">{snapshot?.workOrders !== null ? `${activeWorkOrderCount}` : "No access"}</span> { label: "Projects", value: projectCount, total: Math.max(customerCount, vendorCount, itemCount, orderCount, purchaseOrderCount, workOrderCount, shipmentCount, projectCount, 1), tone: "bg-violet-500" },
</div> ].map((row) => (
<div className="flex items-center justify-between rounded-xl border border-line/70 bg-page/60 px-2 py-2 text-sm"> <div key={row.label} className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<span className="text-muted">Overdue</span> <div className="flex items-center justify-between text-sm">
<span className="font-semibold text-text">{snapshot?.workOrders !== null ? `${overdueWorkOrderCount}` : "No access"}</span> <span className="text-muted">{row.label}</span>
</div> <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> </div>
</article> {snapshot ? <div className="mt-4 text-xs text-muted">Refreshed {new Date(snapshot.refreshedAt).toLocaleString()}</div> : null}
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5"> </DashboardCard>
<h4 className="text-lg font-bold text-text">Projects</h4>
<div className="mt-4 grid gap-2">
<div className="flex items-center justify-between rounded-xl 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-xl 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-xl 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-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<h4 className="text-lg font-bold text-text">Shipping</h4>
<div className="mt-4 grid gap-2">
<div className="flex items-center justify-between rounded-xl 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-xl 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-xl 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> </section>
</div> </div>
); );