live dashboard
This commit is contained in:
@@ -1,22 +1,259 @@
|
||||
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;
|
||||
quotes: Awaited<ReturnType<typeof api.getQuotes>> | null;
|
||||
orders: Awaited<ReturnType<typeof api.getSalesOrders>> | null;
|
||||
shipments: Awaited<ReturnType<typeof api.getShipments>> | 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);
|
||||
|
||||
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 canReadSales = hasPermission(user.permissions, permissions.salesRead);
|
||||
const canReadShipping = hasPermission(user.permissions, permissions.shippingRead);
|
||||
|
||||
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),
|
||||
canReadSales ? api.getQuotes(authToken) : Promise.resolve(null),
|
||||
canReadSales ? api.getSalesOrders(authToken) : Promise.resolve(null),
|
||||
canReadShipping ? api.getShipments(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,
|
||||
quotes: results[4].status === "fulfilled" ? results[4].value : null,
|
||||
orders: results[5].status === "fulfilled" ? results[5].value : null,
|
||||
shipments: results[6].status === "fulfilled" ? results[6].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 quotes = snapshot?.quotes ?? [];
|
||||
const orders = snapshot?.orders ?? [];
|
||||
const shipments = snapshot?.shipments ?? [];
|
||||
|
||||
const accessibleModules = [
|
||||
snapshot?.customers !== null || snapshot?.vendors !== null,
|
||||
snapshot?.items !== null || snapshot?.warehouses !== null,
|
||||
snapshot?.quotes !== null || snapshot?.orders !== null,
|
||||
snapshot?.shipments !== 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 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 lastActivityAt = [
|
||||
...customers.map((customer) => customer.updatedAt),
|
||||
...vendors.map((vendor) => vendor.updatedAt),
|
||||
...items.map((item) => item.updatedAt),
|
||||
...warehouses.map((warehouse) => warehouse.updatedAt),
|
||||
...quotes.map((quote) => quote.updatedAt),
|
||||
...orders.map((order) => order.updatedAt),
|
||||
...shipments.map((shipment) => shipment.updatedAt),
|
||||
]
|
||||
.sort()
|
||||
.at(-1) ?? null;
|
||||
|
||||
const metricCards = [
|
||||
{ label: "CRM Scope", value: "Complete", detail: "Customers, vendors, hierarchy, contacts, attachments", tone: "border-emerald-400/30 bg-emerald-500/12 text-emerald-700 dark:text-emerald-300" },
|
||||
{ label: "Inventory", value: "Live", detail: "Items, BOMs, warehouses, stock, transactions", tone: "border-sky-400/30 bg-sky-500/12 text-sky-700 dark:text-sky-300" },
|
||||
{ label: "Sales", value: "Active", detail: "Quotes, orders, totals, reseller pricing defaults", tone: "border-amber-400/30 bg-amber-500/12 text-amber-700 dark:text-amber-300" },
|
||||
{ label: "Shipping", value: "Online", detail: "Shipments, status flow, packing-slip PDFs", tone: "border-brand/30 bg-brand/10 text-brand" },
|
||||
{
|
||||
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: "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",
|
||||
},
|
||||
];
|
||||
|
||||
const modulePanels = [
|
||||
{
|
||||
title: "Commercial",
|
||||
eyebrow: "Revenue Flow",
|
||||
summary: "Quotes and sales orders now support item pricing, discount, tax, freight, and conversion workflows.",
|
||||
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: "Quote -> SO", value: "Enabled" },
|
||||
{ label: "Totals Logic", value: "Live" },
|
||||
{ label: "Customer Lookup", value: "Searchable" },
|
||||
{ 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: "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" },
|
||||
@@ -24,40 +261,29 @@ export function DashboardPage() {
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Operations",
|
||||
eyebrow: "Stock + Ship",
|
||||
summary: "Inventory and shipping now share a usable operational path from item master through shipment paperwork.",
|
||||
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: "On-Hand", value: "Tracked" },
|
||||
{ label: "Shipments", value: "Linked" },
|
||||
{ label: "Packing Slips", value: "Ready" },
|
||||
{ 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 inventory", to: "/inventory/items" },
|
||||
{ label: "Open shipments", to: "/shipping/shipments" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Planning",
|
||||
eyebrow: "Next Layer",
|
||||
summary: "The dashboard is now intended as a modular command surface for future purchasing, manufacturing, and execution metrics.",
|
||||
metrics: [
|
||||
{ label: "PO Module", value: "Next" },
|
||||
{ label: "Gantt", value: "Preview" },
|
||||
{ label: "Audit", value: "Pending" },
|
||||
],
|
||||
links: [
|
||||
{ label: "Open gantt", to: "/planning/gantt" },
|
||||
{ label: "Company settings", to: "/settings/company" },
|
||||
{ label: "Open packing flow", to: "/sales/orders" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const futureModules = [
|
||||
"Purchasing queue and supplier receipts",
|
||||
"Shipment labels and bills of lading",
|
||||
"Manufacturing schedule and bottleneck metrics",
|
||||
"Audit and system-health diagnostics",
|
||||
"Purchase-order queue and supplier receipts",
|
||||
"Stock transfers, allocations, and cycle counts",
|
||||
"Shipping labels, bills of lading, and delivery exceptions",
|
||||
"Manufacturing schedule, work orders, and bottleneck metrics",
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -67,12 +293,35 @@ export function DashboardPage() {
|
||||
<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">
|
||||
<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 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 dashboard is now the primary landing surface for the platform. It is intended to stay modular as purchasing, manufacturing, shipping, and audit metrics are added in future slices.
|
||||
This landing page now reads directly from live CRM, inventory, sales, and shipping data. It is intentionally modular so future purchasing,
|
||||
manufacturing, and audit slices can slot into the same command surface without a redesign.
|
||||
</p>
|
||||
<div className="mt-6 flex flex-wrap gap-3">
|
||||
<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>
|
||||
@@ -83,6 +332,7 @@ export function DashboardPage() {
|
||||
Open inventory
|
||||
</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">
|
||||
@@ -109,7 +359,7 @@ export function DashboardPage() {
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
<section className="grid gap-3 xl:grid-cols-3">
|
||||
<section className="grid gap-3 xl:grid-cols-2 2xl:grid-cols-4">
|
||||
{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>
|
||||
@@ -133,6 +383,62 @@ export function DashboardPage() {
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
<section className="grid gap-3 xl:grid-cols-3">
|
||||
<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">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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user