This commit is contained in:
2026-03-17 07:40:12 -05:00
parent c1f6386e7d
commit 7993f16a76
4 changed files with 158 additions and 9 deletions

View File

@@ -6,6 +6,7 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh
### Added ### Added
- Project cockpit section on project detail pages for commercial, supply, execution, and delivery rollups
- Project milestones with status, due dates, notes, and edit-time sequencing inside the project workflow - Project milestones with status, due dates, notes, and edit-time sequencing inside the project workflow
- Project-side milestone and work-order rollups surfaced on project list and detail pages - Project-side milestone and work-order rollups surfaced on project list and detail pages
- Inventory SKU master builder with family-level sequence codes, branch-aware taxonomy management, and generated SKU previews on the item form - Inventory SKU master builder with family-level sequence codes, branch-aware taxonomy management, and generated SKU previews on the item form

View File

@@ -91,7 +91,7 @@ Navigation direction:
## Projects Direction ## Projects Direction
Projects are now the long-running program and delivery layer for cross-module execution. The current slice ships project records with customer linkage, owner assignment, priority, due dates, milestones, project-side milestone/work-order rollups, notes, commercial document links, shipment links, attachments, and dashboard visibility. Projects are now the long-running program and delivery layer for cross-module execution. The current slice ships project records with customer linkage, owner assignment, priority, due dates, milestones, project-side milestone/work-order rollups, cockpit-style commercial/supply/execution/delivery visibility, notes, commercial document links, shipment links, attachments, and dashboard visibility.
Current interactions: Current interactions:

View File

@@ -34,6 +34,7 @@ This file tracks roadmap phases, slices, and major foundations that have already
- Logistics attachments directly on shipment records - Logistics attachments directly on shipment records
- Projects foundation with customer, quote, sales-order, shipment, owner, due-date, notes, and attachment linkage - Projects foundation with customer, quote, sales-order, shipment, owner, due-date, notes, and attachment linkage
- Project milestones and project-side milestone/work-order rollups - Project milestones and project-side milestone/work-order rollups
- Project cockpit section on detail pages for commercial, supply, execution, and delivery visibility
- Project list/detail/create/edit workflows and dashboard program widgets - Project list/detail/create/edit workflows and dashboard program widgets
- Manufacturing foundation with work orders, project linkage, material issue posting, completion posting, and work-order attachments - Manufacturing foundation with work orders, project linkage, material issue posting, completion posting, and work-order attachments
- Manufacturing stations, item routing templates, and automatic work-order operation planning for gantt scheduling - Manufacturing stations, item routing templates, and automatic work-order operation planning for gantt scheduling

View File

@@ -1,6 +1,8 @@
import { permissions } from "@mrp/shared"; import { permissions } from "@mrp/shared";
import type { ProjectDetailDto } from "@mrp/shared/dist/projects/types.js"; import type { ProjectDetailDto } from "@mrp/shared/dist/projects/types.js";
import type { SalesOrderPlanningDto } from "@mrp/shared/dist/sales/types.js"; import type { SalesOrderPlanningDto } from "@mrp/shared/dist/sales/types.js";
import type { SalesDocumentDetailDto } from "@mrp/shared/dist/sales/types.js";
import type { ShipmentDetailDto } from "@mrp/shared/dist/shipping/types.js";
import type { WorkOrderSummaryDto } from "@mrp/shared"; import type { WorkOrderSummaryDto } from "@mrp/shared";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom"; import { Link, useParams } from "react-router-dom";
@@ -18,6 +20,9 @@ export function ProjectDetailPage() {
const [project, setProject] = useState<ProjectDetailDto | null>(null); const [project, setProject] = useState<ProjectDetailDto | null>(null);
const [workOrders, setWorkOrders] = useState<WorkOrderSummaryDto[]>([]); const [workOrders, setWorkOrders] = useState<WorkOrderSummaryDto[]>([]);
const [planning, setPlanning] = useState<SalesOrderPlanningDto | null>(null); const [planning, setPlanning] = useState<SalesOrderPlanningDto | null>(null);
const [quote, setQuote] = useState<SalesDocumentDetailDto | null>(null);
const [salesOrder, setSalesOrder] = useState<SalesDocumentDetailDto | null>(null);
const [shipment, setShipment] = useState<ShipmentDetailDto | null>(null);
const [status, setStatus] = useState("Loading project..."); const [status, setStatus] = useState("Loading project...");
const canManage = user?.permissions.includes(permissions.projectsWrite) ?? false; const canManage = user?.permissions.includes(permissions.projectsWrite) ?? false;
@@ -28,17 +33,23 @@ export function ProjectDetailPage() {
} }
api.getProject(token, projectId) api.getProject(token, projectId)
.then((nextProject) => { .then(async (nextProject) => {
setProject(nextProject); setProject(nextProject);
setStatus("Project loaded."); setStatus("Project loaded.");
if (nextProject.salesOrderId) { const [nextPlanning, nextWorkOrders, nextQuote, nextSalesOrder, nextShipment] = await Promise.all([
api.getSalesOrderPlanning(token, nextProject.salesOrderId).then(setPlanning).catch(() => setPlanning(null)); nextProject.salesOrderId ? api.getSalesOrderPlanning(token, nextProject.salesOrderId).catch(() => null) : Promise.resolve(null),
} else { api.getWorkOrders(token, { projectId: nextProject.id }),
setPlanning(null); nextProject.salesQuoteId ? api.getQuote(token, nextProject.salesQuoteId).catch(() => null) : Promise.resolve(null),
} nextProject.salesOrderId ? api.getSalesOrder(token, nextProject.salesOrderId).catch(() => null) : Promise.resolve(null),
return api.getWorkOrders(token, { projectId: nextProject.id }); nextProject.shipmentId ? api.getShipment(token, nextProject.shipmentId).catch(() => null) : Promise.resolve(null),
]);
setPlanning(nextPlanning);
setWorkOrders(nextWorkOrders);
setQuote(nextQuote);
setSalesOrder(nextSalesOrder);
setShipment(nextShipment);
}) })
.then((nextWorkOrders) => setWorkOrders(nextWorkOrders))
.catch((error: unknown) => { .catch((error: unknown) => {
const message = error instanceof ApiError ? error.message : "Unable to load project."; const message = error instanceof ApiError ? error.message : "Unable to load project.";
setStatus(message); setStatus(message);
@@ -49,6 +60,48 @@ export function ProjectDetailPage() {
return <div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel">{status}</div>; return <div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel">{status}</div>;
} }
const sortedMilestones = [...project.milestones].sort((left, right) => {
if (left.status === "COMPLETE" && right.status !== "COMPLETE") {
return 1;
}
if (left.status !== "COMPLETE" && right.status === "COMPLETE") {
return -1;
}
if (left.dueDate && right.dueDate) {
return new Date(left.dueDate).getTime() - new Date(right.dueDate).getTime();
}
if (left.dueDate) {
return -1;
}
if (right.dueDate) {
return 1;
}
return left.sortOrder - right.sortOrder;
});
const nextMilestone = sortedMilestones.find((milestone) => milestone.status !== "COMPLETE") ?? null;
const activeWorkOrders = workOrders.filter((workOrder) => workOrder.status === "RELEASED" || workOrder.status === "IN_PROGRESS" || workOrder.status === "ON_HOLD");
const nextWorkOrder = [...activeWorkOrders]
.sort((left, right) => {
if (left.dueDate && right.dueDate) {
return new Date(left.dueDate).getTime() - new Date(right.dueDate).getTime();
}
if (left.dueDate) {
return -1;
}
if (right.dueDate) {
return 1;
}
return left.workOrderNumber.localeCompare(right.workOrderNumber);
})[0] ?? null;
const materialExceptionItems = planning
? planning.items.filter((item) => item.uncoveredQuantity > 0 || item.recommendedBuildQuantity > 0 || item.recommendedPurchaseQuantity > 0).slice(0, 5)
: [];
const completionPercent = project.rollups.milestoneCount > 0
? Math.round((project.rollups.completedMilestoneCount / project.rollups.milestoneCount) * 100)
: 0;
return ( return (
<section className="space-y-4"> <section className="space-y-4">
<div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5"> <div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
@@ -95,6 +148,100 @@ export function ProjectDetailPage() {
<div className="mt-1 text-xs text-muted">{project.rollups.completedWorkOrderCount} complete</div> <div className="mt-1 text-xs text-muted">{project.rollups.completedWorkOrderCount} complete</div>
</article> </article>
</section> </section>
<section className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Project Cockpit</p>
<h4 className="mt-2 text-lg font-bold text-text">Cross-functional execution view</h4>
<p className="mt-2 text-sm text-muted">Commercial, supply, execution, and delivery signals for this program in one place.</p>
</div>
<div className="rounded-2xl border border-line/70 bg-page/60 px-3 py-2 text-right">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Milestone Progress</div>
<div className="mt-1 text-2xl font-bold text-text">{completionPercent}%</div>
</div>
</div>
<div className="mt-5 grid gap-3 xl:grid-cols-4">
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Commercial</p>
<div className="mt-2 text-base font-bold text-text">
{salesOrder ? `$${salesOrder.total.toFixed(2)}` : quote ? `$${quote.total.toFixed(2)}` : "Not linked"}
</div>
<div className="mt-1 text-xs text-muted">
{salesOrder ? `${salesOrder.documentNumber} · ${salesOrder.status}` : quote ? `${quote.documentNumber} · ${quote.status}` : "Link a quote or sales order"}
</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Supply</p>
<div className="mt-2 text-base font-bold text-text">{planning ? planning.summary.uncoveredItemCount : 0} shortage items</div>
<div className="mt-1 text-xs text-muted">
{planning ? `Build ${planning.summary.totalBuildQuantity} · Buy ${planning.summary.totalPurchaseQuantity}` : "No sales-order planning linked"}
</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Execution</p>
<div className="mt-2 text-base font-bold text-text">{project.rollups.activeWorkOrderCount} active work orders</div>
<div className="mt-1 text-xs text-muted">
{nextWorkOrder ? `${nextWorkOrder.workOrderNumber} due ${nextWorkOrder.dueDate ? new Date(nextWorkOrder.dueDate).toLocaleDateString() : "unscheduled"}` : "No active work order due date"}
</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Delivery</p>
<div className="mt-2 text-base font-bold text-text">{shipment ? shipment.status.replace("_", " ") : "Not linked"}</div>
<div className="mt-1 text-xs text-muted">
{shipment ? `${shipment.shipmentNumber} · ${shipment.packageCount} package(s)` : "Link a shipment to track delivery"}
</div>
</article>
</div>
<div className="mt-5 grid gap-3 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Next Checkpoints</p>
<div className="mt-4 space-y-3">
<div className="rounded-[16px] border border-line/70 bg-surface/80 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Milestone</div>
<div className="mt-2 font-semibold text-text">{nextMilestone ? nextMilestone.title : "All milestones complete"}</div>
<div className="mt-1 text-xs text-muted">
{nextMilestone
? `${nextMilestone.status.replace("_", " ")} · ${nextMilestone.dueDate ? new Date(nextMilestone.dueDate).toLocaleDateString() : "No due date"}`
: "No open milestone remains."}
</div>
</div>
<div className="rounded-[16px] border border-line/70 bg-surface/80 p-3">
<div className="text-xs font-semibold uppercase tracking-[0.16em] text-muted">Work Order</div>
<div className="mt-2 font-semibold text-text">{nextWorkOrder ? nextWorkOrder.workOrderNumber : "No active work orders"}</div>
<div className="mt-1 text-xs text-muted">
{nextWorkOrder
? `${nextWorkOrder.itemSku} · ${nextWorkOrder.completedQuantity}/${nextWorkOrder.quantity} complete`
: "Launch or link a work order to populate execution checkpoints."}
</div>
</div>
</div>
</article>
<article className="rounded-[18px] border border-line/70 bg-page/60 p-3">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Material Watchlist</p>
{materialExceptionItems.length === 0 ? (
<div className="mt-4 rounded-[16px] border border-dashed border-line/70 bg-surface/80 px-3 py-4 text-sm text-muted">
No current build/buy exception items from linked sales-order planning.
</div>
) : (
<div className="mt-4 space-y-3">
{materialExceptionItems.map((item) => (
<div key={item.itemId} className="rounded-[16px] border border-line/70 bg-surface/80 p-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<div className="font-semibold text-text">{item.itemSku}</div>
<div className="mt-1 text-xs text-muted">{item.itemName}</div>
</div>
<div className="text-xs text-muted">
Build {item.recommendedBuildQuantity} · Buy {item.recommendedPurchaseQuantity} · Uncovered {item.uncoveredQuantity}
</div>
</div>
</div>
))}
</div>
)}
</article>
</div>
</section>
<div className="grid gap-3 xl:grid-cols-[minmax(0,1.05fr)_minmax(320px,0.95fr)]"> <div className="grid gap-3 xl:grid-cols-[minmax(0,1.05fr)_minmax(320px,0.95fr)]">
<article className="rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5"> <article className="rounded-[20px] 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">Customer Linkage</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Customer Linkage</p>