import { permissions } from "@mrp/shared"; import type { ProjectMilestoneStatus, WorkOrderSummaryDto } from "@mrp/shared"; import type { ProjectDetailDto } from "@mrp/shared/dist/projects/types.js"; import type { SalesOrderPlanningDto } from "@mrp/shared/dist/sales/types.js"; import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { useAuth } from "../../auth/AuthProvider"; import { FileAttachmentsPanel } from "../../components/FileAttachmentsPanel"; import { api, ApiError } from "../../lib/api"; import { projectMilestoneStatusPalette } from "./config"; import { ProjectPriorityBadge } from "./ProjectPriorityBadge"; import { ProjectStatusBadge } from "./ProjectStatusBadge"; function formatCurrency(value: number | null) { return value === null ? "Not linked" : `$${value.toFixed(2)}`; } export function ProjectDetailPage() { const { token, user } = useAuth(); const { projectId } = useParams(); const [project, setProject] = useState(null); const [workOrders, setWorkOrders] = useState([]); const [planning, setPlanning] = useState(null); const [status, setStatus] = useState("Loading project..."); const [updatingMilestoneId, setUpdatingMilestoneId] = useState(null); const canManage = user?.permissions.includes(permissions.projectsWrite) ?? false; useEffect(() => { if (!token || !projectId) { return; } api.getProject(token, projectId) .then(async (nextProject) => { setProject(nextProject); setStatus("Project loaded."); const [nextPlanning, nextWorkOrders] = await Promise.all([ nextProject.salesOrderId ? api.getSalesOrderPlanning(token, nextProject.salesOrderId).catch(() => null) : Promise.resolve(null), api.getWorkOrders(token, { projectId: nextProject.id }), ]); setPlanning(nextPlanning); setWorkOrders(nextWorkOrders); }) .catch((error: unknown) => { const message = error instanceof ApiError ? error.message : "Unable to load project."; setStatus(message); }); }, [projectId, token]); if (!project) { return
{status}
; } 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 topBuildRecommendation = planning?.items.find((item) => item.recommendedBuildQuantity > 0) ?? null; const topPurchaseRecommendation = planning?.items.find((item) => item.recommendedPurchaseQuantity > 0) ?? null; const completionPercent = project.rollups.milestoneCount > 0 ? Math.round((project.rollups.completedMilestoneCount / project.rollups.milestoneCount) * 100) : 0; const readinessScore = project.cockpit.risk.readinessScore; const riskTone = project.cockpit.risk.riskLevel === "LOW" ? "text-emerald-600 dark:text-emerald-300" : project.cockpit.risk.riskLevel === "MEDIUM" ? "text-amber-600 dark:text-amber-300" : "text-rose-600 dark:text-rose-300"; async function updateMilestoneStatus(milestoneId: string, nextStatus: ProjectMilestoneStatus) { if (!token || !project) { return; } setUpdatingMilestoneId(milestoneId); setStatus("Updating milestone status..."); try { const nextProject = await api.updateProjectMilestoneStatus(token, project.id, milestoneId, { status: nextStatus }); setProject(nextProject); setStatus("Milestone status updated."); } catch (error: unknown) { const message = error instanceof ApiError ? error.message : "Unable to update milestone status."; setStatus(message); } finally { setUpdatingMilestoneId(null); } } function milestoneQuickActions(currentStatus: ProjectMilestoneStatus) { if (currentStatus === "PLANNED") { return [ { status: "IN_PROGRESS" as const, label: "Start" }, { status: "BLOCKED" as const, label: "Block" }, { status: "COMPLETE" as const, label: "Complete" }, ]; } if (currentStatus === "IN_PROGRESS") { return [ { status: "BLOCKED" as const, label: "Block" }, { status: "COMPLETE" as const, label: "Complete" }, { status: "PLANNED" as const, label: "Reset" }, ]; } if (currentStatus === "BLOCKED") { return [ { status: "IN_PROGRESS" as const, label: "Resume" }, { status: "COMPLETE" as const, label: "Complete" }, { status: "PLANNED" as const, label: "Reset" }, ]; } return [{ status: "IN_PROGRESS" as const, label: "Reopen" }]; } return (

Project

{project.projectNumber}

{project.name}

Back to projects {canManage ? Edit project : null}

Customer

{project.customerName}

Owner

{project.ownerName || "Unassigned"}

Due Date

{project.dueDate ? new Date(project.dueDate).toLocaleDateString() : "Not set"}

Created

{new Date(project.createdAt).toLocaleDateString()}

Milestones

{project.rollups.completedMilestoneCount}/{project.rollups.milestoneCount}
{project.rollups.openMilestoneCount} open

Overdue Milestones

{project.rollups.overdueMilestoneCount}

Linked Work Orders

{project.rollups.workOrderCount}
{project.rollups.activeWorkOrderCount} active

Overdue Work Orders

{project.rollups.overdueWorkOrderCount}
{project.rollups.completedWorkOrderCount} complete

Project Cockpit

Cross-functional execution view

Commercial, supply, execution, purchasing, and delivery signals for this program in one place.

Milestone Progress
{completionPercent}%

Commercial

{formatCurrency(project.cockpit.commercial.activeDocumentTotal)}
{project.cockpit.commercial.activeDocumentNumber ? `${project.cockpit.commercial.activeDocumentNumber} - ${project.cockpit.commercial.activeDocumentStatus}` : "Link a quote or sales order"}

Supply

{planning ? planning.summary.uncoveredItemCount : project.cockpit.risk.shortageItemCount} shortage items
{planning ? `Build ${planning.summary.totalBuildQuantity} - Buy ${planning.summary.totalPurchaseQuantity}` : `Uncovered qty ${project.cockpit.risk.totalUncoveredQuantity}`}

Execution

{project.rollups.activeWorkOrderCount} active work orders
{nextWorkOrder ? `${nextWorkOrder.workOrderNumber} due ${nextWorkOrder.dueDate ? new Date(nextWorkOrder.dueDate).toLocaleDateString() : "unscheduled"}` : "No active work order due date"}

Delivery

{project.cockpit.delivery.shipmentStatus ? project.cockpit.delivery.shipmentStatus.replaceAll("_", " ") : "Not linked"}
{project.cockpit.delivery.shipmentNumber ? `${project.cockpit.delivery.shipmentNumber} - ${project.cockpit.delivery.packageCount} package(s)` : "Link a shipment to track delivery"}

Purchasing Coverage

{project.cockpit.purchasing.totalReceivedQuantity}/{project.cockpit.purchasing.totalOrderedQuantity} received
{project.cockpit.purchasing.linkedPurchaseOrderCount} linked PO(s) - {project.cockpit.purchasing.totalOutstandingQuantity} outstanding

Readiness Score

{readinessScore}%
{project.cockpit.risk.riskLevel} risk - {project.cockpit.risk.shortageItemCount} shortage item(s)

Material Spend

${project.cockpit.purchasing.linkedLineValue.toFixed(2)}
{project.cockpit.purchasing.vendorCount} vendor(s) across {project.cockpit.purchasing.linkedLineCount} linked line(s)

Booked Revenue

{formatCurrency(project.cockpit.costs.bookedRevenue)}
Quoted baseline {formatCurrency(project.cockpit.costs.quotedRevenue)}

Purchase Commitment

${project.cockpit.costs.linkedPurchaseCommitment.toFixed(2)}
Linked PO line value already committed

Planned Material Cost

${project.cockpit.costs.plannedMaterialCost.toFixed(2)}
Issued so far ${project.cockpit.costs.issuedMaterialCost.toFixed(2)}

Build Load

{project.cockpit.costs.completedBuildQuantity}/{project.cockpit.costs.buildQuantity}
{project.cockpit.costs.plannedOperationHours.toFixed(1)} planned operation hours

Next Checkpoints

Milestone
{nextMilestone ? nextMilestone.title : "All milestones complete"}
{nextMilestone ? `${nextMilestone.status.replace("_", " ")} - ${nextMilestone.dueDate ? new Date(nextMilestone.dueDate).toLocaleDateString() : "No due date"}` : "No open milestone remains."}
Work Order
{nextWorkOrder ? nextWorkOrder.workOrderNumber : "No active work orders"}
{nextWorkOrder ? `${nextWorkOrder.itemSku} - ${nextWorkOrder.completedQuantity}/${nextWorkOrder.quantity} complete` : "Launch or link a work order to populate execution checkpoints."}

Material Watchlist

{materialExceptionItems.length === 0 ?
No current build/buy exception items from linked sales-order planning.
:
{materialExceptionItems.map((item) => (
{item.itemSku}
{item.itemName}
Build {item.recommendedBuildQuantity} - Buy {item.recommendedPurchaseQuantity} - Uncovered {item.uncoveredQuantity}
))}
}

Actionable Cockpit

Turn current exceptions into purchasing, manufacturing, and planning follow-through.

Open workbench

Build Follow-Through

{topBuildRecommendation ? topBuildRecommendation.itemSku : "No build recommendation"}
{topBuildRecommendation ? `Recommended build qty ${topBuildRecommendation.recommendedBuildQuantity}` : "Planning does not currently recommend a new build."}
{topBuildRecommendation && project.salesOrderId ? ( Launch work order ) : null}

Buy Follow-Through

{topPurchaseRecommendation ? topPurchaseRecommendation.itemSku : "No buy recommendation"}
{topPurchaseRecommendation ? `Recommended buy qty ${topPurchaseRecommendation.recommendedPurchaseQuantity}` : "Planning does not currently recommend a new purchase."}
{topPurchaseRecommendation && project.salesOrderId ? ( Launch purchase order ) : null}
New project work order {project.salesOrderId ? ( Open sales order ) : null} Review purchasing

Linked Purchasing

Purchase orders and receipts tied back to the project sales order.

{project.salesOrderId ? Open purchasing : null}
{project.cockpit.purchasing.purchaseOrders.length === 0 ?
No linked purchase orders are tied to this project yet.
:
{project.cockpit.purchasing.purchaseOrders.slice(0, 5).map((purchaseOrder) => (
{purchaseOrder.documentNumber}
{purchaseOrder.vendorName} - {purchaseOrder.status.replaceAll("_", " ")}
${purchaseOrder.linkedLineValue.toFixed(2)} linked value
{purchaseOrder.totalReceivedQuantity}/{purchaseOrder.totalOrderedQuantity} received
))}
}

Readiness Drivers

Risk posture
{project.cockpit.risk.riskLevel}
{project.cockpit.risk.outstandingPurchaseOrderCount} PO(s) still waiting on receipts.
Blocked milestones: {project.cockpit.risk.blockedMilestoneCount}
Overdue execution items: {project.cockpit.risk.overdueMilestoneCount + project.cockpit.risk.overdueWorkOrderCount}
Uncovered material quantity: {project.cockpit.risk.totalUncoveredQuantity}

Vendor Exposure

{project.cockpit.purchasing.vendors.length === 0 ?
No supplier exposure exists until purchasing is linked.
:
{project.cockpit.purchasing.vendors.slice(0, 4).map((vendor) => (
{vendor.vendorName}
{vendor.orderCount} linked order(s)
${vendor.linkedLineValue.toFixed(2)}
{vendor.outstandingQuantity} outstanding qty
))}
}

Recent Receipts

{project.cockpit.purchasing.recentReceipts.length === 0 ?
No purchase receipts have been posted against linked project supply.
:
{project.cockpit.purchasing.recentReceipts.map((receipt) => (
{receipt.receiptNumber}
{receipt.vendorName} - {receipt.purchaseOrderNumber}
{new Date(receipt.receivedAt).toLocaleDateString()}
{receipt.totalQuantity} units received
))}
}

Customer Linkage

Account
{project.customerName}
Email
{project.customerEmail}
Phone
{project.customerPhone}

Program Notes

{project.notes || "No project notes recorded."}

Commercial + Delivery Links

Quote
{project.salesQuoteNumber ? {project.salesQuoteNumber} : "Not linked"}
Sales Order
{project.salesOrderNumber ? {project.salesOrderNumber} : "Not linked"}
Shipment
{project.shipmentNumber ? {project.shipmentNumber} : "Not linked"}

Milestones

Track project checkpoints, blockers, and completion progress.

{canManage ? Edit milestones : null}
{project.milestones.length === 0 ?
No milestones are defined for this project yet.
:
{project.milestones.map((milestone) => (
{milestone.title}
{milestone.status.replace("_", " ")}Due {milestone.dueDate ? new Date(milestone.dueDate).toLocaleDateString() : "not scheduled"}{milestone.completedAt ? Completed {new Date(milestone.completedAt).toLocaleDateString()} : null}
{milestone.notes ?
{milestone.notes}
: null}
{canManage ?
{milestoneQuickActions(milestone.status).map((action) => ())}
: null}
))}
}
{planning ? (

Material Readiness

Build Qty

{planning.summary.totalBuildQuantity}

Buy Qty

{planning.summary.totalPurchaseQuantity}

Uncovered Qty

{planning.summary.totalUncoveredQuantity}

Shortage Items

{planning.summary.uncoveredItemCount}
{planning.items.filter((item) => item.recommendedBuildQuantity > 0 || item.recommendedPurchaseQuantity > 0 || item.uncoveredQuantity > 0).slice(0, 8).map((item) => (
{item.itemSku}
{item.itemName}
Build {item.recommendedBuildQuantity} - Buy {item.recommendedPurchaseQuantity} - Uncovered {item.uncoveredQuantity}
))}
) : null}

Manufacturing Links

Work orders already linked to this project.

{canManage ? New work order : null}
{workOrders.length === 0 ?
No work orders are linked to this project yet.
:
{workOrders.map((workOrder) => (
{workOrder.workOrderNumber}
{workOrder.itemSku} - {workOrder.completedQuantity}/{workOrder.quantity} complete
{workOrder.status.replace("_", " ")}
))}
}

Activity Timeline

Chronological project, milestone, purchasing, manufacturing, sales, and shipping history.

{project.timeline.length === 0 ? (
No timeline activity is available for this project yet.
) : (
{project.timeline.map((entry) => (
{entry.sourceType}
{entry.href ? {entry.title} : entry.title}
{entry.detail}
{new Date(entry.createdAt).toLocaleString()}
{entry.actorName || "System"}
))}
)}
{status}
); }