dashboard cleanup
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user