This commit is contained in:
2026-03-15 10:13:53 -05:00
parent 552d4e2844
commit 6644ba2932
30 changed files with 1768 additions and 64 deletions

View File

@@ -13,6 +13,7 @@ interface DashboardSnapshot {
quotes: Awaited<ReturnType<typeof api.getQuotes>> | null;
orders: Awaited<ReturnType<typeof api.getSalesOrders>> | null;
shipments: Awaited<ReturnType<typeof api.getShipments>> | null;
projects: Awaited<ReturnType<typeof api.getProjects>> | null;
refreshedAt: string;
}
@@ -50,6 +51,7 @@ export function DashboardPage() {
const [snapshot, setSnapshot] = useState<DashboardSnapshot | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const canWriteProjects = hasPermission(user?.permissions, permissions.projectsWrite);
useEffect(() => {
if (!token || !user) {
@@ -67,6 +69,7 @@ export function DashboardPage() {
const canReadInventory = hasPermission(user.permissions, permissions.inventoryRead);
const canReadSales = hasPermission(user.permissions, permissions.salesRead);
const canReadShipping = hasPermission(user.permissions, permissions.shippingRead);
const canReadProjects = hasPermission(user.permissions, permissions.projectsRead);
async function loadSnapshot() {
const results = await Promise.allSettled([
@@ -77,6 +80,7 @@ export function DashboardPage() {
canReadSales ? api.getQuotes(authToken) : Promise.resolve(null),
canReadSales ? api.getSalesOrders(authToken) : Promise.resolve(null),
canReadShipping ? api.getShipments(authToken) : Promise.resolve(null),
canReadProjects ? api.getProjects(authToken) : Promise.resolve(null),
]);
if (!isMounted) {
@@ -97,6 +101,7 @@ export function DashboardPage() {
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,
projects: results[7].status === "fulfilled" ? results[7].value : null,
refreshedAt: new Date().toISOString(),
});
setIsLoading(false);
@@ -124,12 +129,14 @@ export function DashboardPage() {
const quotes = snapshot?.quotes ?? [];
const orders = snapshot?.orders ?? [];
const shipments = snapshot?.shipments ?? [];
const projects = snapshot?.projects ?? [];
const accessibleModules = [
snapshot?.customers !== null || snapshot?.vendors !== null,
snapshot?.items !== null || snapshot?.warehouses !== null,
snapshot?.quotes !== null || snapshot?.orders !== null,
snapshot?.shipments !== null,
snapshot?.projects !== null,
].filter(Boolean).length;
const customerCount = customers.length;
@@ -157,6 +164,17 @@ export function DashboardPage() {
const inTransitCount = shipments.filter((shipment) => shipment.status === "SHIPPED").length;
const deliveredCount = shipments.filter((shipment) => shipment.status === "DELIVERED").length;
const projectCount = projects.length;
const activeProjectCount = projects.filter((project) => project.status === "ACTIVE").length;
const atRiskProjectCount = projects.filter((project) => project.status === "AT_RISK").length;
const overdueProjectCount = projects.filter((project) => {
if (!project.dueDate || project.status === "COMPLETE") {
return false;
}
return new Date(project.dueDate).getTime() < Date.now();
}).length;
const lastActivityAt = [
...customers.map((customer) => customer.updatedAt),
...vendors.map((vendor) => vendor.updatedAt),
@@ -165,6 +183,7 @@ export function DashboardPage() {
...quotes.map((quote) => quote.updatedAt),
...orders.map((order) => order.updatedAt),
...shipments.map((shipment) => shipment.updatedAt),
...projects.map((project) => project.updatedAt),
]
.sort()
.at(-1) ?? null;
@@ -206,6 +225,15 @@ export function DashboardPage() {
: "Shipping metrics are permission-gated.",
tone: "border-brand/30 bg-brand/10 text-brand",
},
{
label: "Project Load",
value: snapshot?.projects !== null ? `${activeProjectCount}` : "No access",
detail:
snapshot?.projects !== null
? `${atRiskProjectCount} at risk and ${overdueProjectCount} overdue`
: "Project metrics are permission-gated.",
tone: "border-violet-400/30 bg-violet-500/12 text-violet-700 dark:text-violet-300",
},
];
const modulePanels = [
@@ -277,13 +305,31 @@ export function DashboardPage() {
{ label: "Open packing flow", to: "/sales/orders" },
],
},
{
title: "Projects",
eyebrow: "Program Control",
summary:
snapshot?.projects !== null
? "Project records now tie customers, commercial documents, shipment context, and delivery ownership into one operational surface."
: "Project read permission is required to surface program metrics here.",
metrics: [
{ label: "Active", value: snapshot?.projects !== null ? `${activeProjectCount}` : "No access" },
{ label: "At risk", value: snapshot?.projects !== null ? `${atRiskProjectCount}` : "No access" },
{ label: "Overdue", value: snapshot?.projects !== null ? `${overdueProjectCount}` : "No access" },
],
links: [
{ label: "Open projects", to: "/projects" },
...(canWriteProjects ? [{ label: "New project", to: "/projects/new" }] : []),
],
},
];
const futureModules = [
"Purchase-order queue and supplier receipts",
"Vendor invoice attachments and supplier exception queues",
"Stock transfers, allocations, and cycle counts",
"Shipping labels, bills of lading, and delivery exceptions",
"Manufacturing schedule, work orders, and bottleneck metrics",
"Manufacturing work orders, routings, and bottleneck metrics",
"Planning timeline, milestones, and dependency views",
"Audit trails, diagnostics, and system health checks",
];
return (
@@ -304,8 +350,8 @@ export function DashboardPage() {
</div>
</div>
<p className="mt-3 max-w-3xl text-sm leading-6 text-muted">
This landing page now reads directly from live CRM, inventory, 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.
This landing page now reads directly from live CRM, inventory, sales, shipping, and project 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-5 grid gap-2 sm:grid-cols-3">
<div className="rounded-2xl border border-line/70 bg-surface/80 px-2 py-2">
@@ -331,6 +377,9 @@ export function DashboardPage() {
<Link className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text" to="/inventory/items">
Open inventory
</Link>
<Link className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text" to="/projects">
Open projects
</Link>
</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>
@@ -347,7 +396,7 @@ export function DashboardPage() {
</div>
</div>
</section>
<section className="grid gap-3 xl:grid-cols-4">
<section className="grid gap-3 xl:grid-cols-5">
{metricCards.map((card) => (
<article key={card.label} className="rounded-[24px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{card.label}</p>
@@ -359,7 +408,7 @@ export function DashboardPage() {
</article>
))}
</section>
<section className="grid gap-3 xl:grid-cols-2 2xl:grid-cols-4">
<section className="grid gap-3 xl:grid-cols-2 2xl:grid-cols-5">
{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>
@@ -383,7 +432,7 @@ export function DashboardPage() {
</article>
))}
</section>
<section className="grid gap-3 xl:grid-cols-3">
<section className="grid gap-3 xl:grid-cols-4">
<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>
@@ -420,6 +469,24 @@ export function DashboardPage() {
</div>
</div>
</article>
<article className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Project Watch</p>
<h4 className="mt-2 text-lg font-bold text-text">Program status and delivery pressure</h4>
<div className="mt-4 grid gap-2">
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">Total projects</span>
<span className="font-semibold text-text">{snapshot?.projects !== null ? `${projectCount}` : "No access"}</span>
</div>
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">At risk</span>
<span className="font-semibold text-text">{snapshot?.projects !== null ? `${atRiskProjectCount}` : "No access"}</span>
</div>
<div className="flex items-center justify-between rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm">
<span className="text-muted">Overdue</span>
<span className="font-semibold text-text">{snapshot?.projects !== null ? `${overdueProjectCount}` : "No access"}</span>
</div>
</div>
</article>
<article className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Shipping Watch</p>
<h4 className="mt-2 text-lg font-bold text-text">Execution and delivery status</h4>