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