2026-03-15 10:13:53 -05:00
import { permissions } from "@mrp/shared" ;
2026-03-17 19:13:54 -05:00
import type { WorkOrderSummaryDto } from "@mrp/shared" ;
2026-03-15 10:13:53 -05:00
import type { ProjectDetailDto } from "@mrp/shared/dist/projects/types.js" ;
2026-03-15 16:40:25 -05:00
import type { SalesOrderPlanningDto } from "@mrp/shared/dist/sales/types.js" ;
2026-03-15 10:13:53 -05:00
import { useEffect , useState } from "react" ;
import { Link , useParams } from "react-router-dom" ;
import { useAuth } from "../../auth/AuthProvider" ;
2026-03-17 19:13:54 -05:00
import { FileAttachmentsPanel } from "../../components/FileAttachmentsPanel" ;
2026-03-15 10:13:53 -05:00
import { api , ApiError } from "../../lib/api" ;
2026-03-17 07:34:08 -05:00
import { projectMilestoneStatusPalette } from "./config" ;
2026-03-15 10:13:53 -05:00
import { ProjectPriorityBadge } from "./ProjectPriorityBadge" ;
import { ProjectStatusBadge } from "./ProjectStatusBadge" ;
2026-03-17 19:13:54 -05:00
function formatCurrency ( value : number | null ) {
return value === null ? "Not linked" : ` $ ${ value . toFixed ( 2 ) } ` ;
}
2026-03-15 10:13:53 -05:00
export function ProjectDetailPage() {
const { token , user } = useAuth ( ) ;
const { projectId } = useParams ( ) ;
const [ project , setProject ] = useState < ProjectDetailDto | null > ( null ) ;
2026-03-15 11:30:10 -05:00
const [ workOrders , setWorkOrders ] = useState < WorkOrderSummaryDto [ ] > ( [ ] ) ;
2026-03-15 16:40:25 -05:00
const [ planning , setPlanning ] = useState < SalesOrderPlanningDto | null > ( null ) ;
2026-03-15 10:13:53 -05:00
const [ status , setStatus ] = useState ( "Loading project..." ) ;
const canManage = user ? . permissions . includes ( permissions . projectsWrite ) ? ? false ;
useEffect ( ( ) = > {
if ( ! token || ! projectId ) {
return ;
}
api . getProject ( token , projectId )
2026-03-17 07:40:12 -05:00
. then ( async ( nextProject ) = > {
2026-03-15 10:13:53 -05:00
setProject ( nextProject ) ;
setStatus ( "Project loaded." ) ;
2026-03-17 19:13:54 -05:00
const [ nextPlanning , nextWorkOrders ] = await Promise . all ( [
2026-03-17 07:40:12 -05:00
nextProject . salesOrderId ? api . getSalesOrderPlanning ( token , nextProject . salesOrderId ) . catch ( ( ) = > null ) : Promise . resolve ( null ) ,
api . getWorkOrders ( token , { projectId : nextProject.id } ) ,
] ) ;
setPlanning ( nextPlanning ) ;
setWorkOrders ( nextWorkOrders ) ;
2026-03-15 10:13:53 -05:00
} )
. catch ( ( error : unknown ) = > {
const message = error instanceof ApiError ? error . message : "Unable to load project." ;
setStatus ( message ) ;
} ) ;
} , [ projectId , token ] ) ;
if ( ! project ) {
2026-03-15 20:07:48 -05:00
return < div className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel" > { status } < / div > ;
2026-03-15 10:13:53 -05:00
}
2026-03-17 07:40:12 -05:00
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 ;
} ) ;
2026-03-17 19:13:54 -05:00
2026-03-17 07:40:12 -05:00
const nextMilestone = sortedMilestones . find ( ( milestone ) = > milestone . status !== "COMPLETE" ) ? ? null ;
2026-03-17 19:13:54 -05:00
const activeWorkOrders = workOrders . filter (
( workOrder ) = > workOrder . status === "RELEASED" || workOrder . status === "IN_PROGRESS" || workOrder . status === "ON_HOLD"
) ;
2026-03-17 07:40:12 -05:00
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 ;
}
2026-03-17 19:13:54 -05:00
2026-03-17 07:40:12 -05:00
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 ;
2026-03-17 19:13:54 -05:00
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" ;
2026-03-17 07:40:12 -05:00
2026-03-15 10:13:53 -05:00
return (
< section className = "space-y-4" >
2026-03-15 20:07:48 -05:00
< div className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-15 10:13:53 -05:00
< 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 < / p >
< h3 className = "mt-2 text-xl font-bold text-text" > { project . projectNumber } < / h3 >
< p className = "mt-1 text-sm text-text" > { project . name } < / p >
< div className = "mt-3 flex flex-wrap gap-2" >
< ProjectStatusBadge status = { project . status } / >
< ProjectPriorityBadge priority = { project . priority } / >
< / div >
< / div >
< div className = "flex flex-wrap gap-3" >
< Link to = "/projects" className = "inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" > Back to projects < / Link >
{ canManage ? < Link to = { ` /projects/ ${ project . id } /edit ` } className = "inline-flex items-center justify-center rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white" > Edit project < / Link > : null }
< / div >
< / div >
< / div >
< section className = "grid gap-3 xl:grid-cols-4" >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[18px] 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" > Customer < / p > < div className = "mt-2 text-base font-bold text-text" > { project . customerName } < / div > < / article >
< article className = "rounded-[18px] 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" > Owner < / p > < div className = "mt-2 text-base font-bold text-text" > { project . ownerName || "Unassigned" } < / div > < / article >
< article className = "rounded-[18px] 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" > Due Date < / p > < div className = "mt-2 text-base font-bold text-text" > { project . dueDate ? new Date ( project . dueDate ) . toLocaleDateString ( ) : "Not set" } < / div > < / article >
< article className = "rounded-[18px] 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" > Created < / p > < div className = "mt-2 text-base font-bold text-text" > { new Date ( project . createdAt ) . toLocaleDateString ( ) } < / div > < / article >
2026-03-15 10:13:53 -05:00
< / section >
2026-03-17 07:34:08 -05:00
< section className = "grid gap-3 xl:grid-cols-4" >
2026-03-17 19:13:54 -05:00
< article className = "rounded-[18px] 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" > Milestones < / p > < div className = "mt-2 text-base font-bold text-text" > { project . rollups . completedMilestoneCount } / { project . rollups . milestoneCount } < / div > < div className = "mt-1 text-xs text-muted" > { project . rollups . openMilestoneCount } open < / div > < / article >
< article className = "rounded-[18px] 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" > Overdue Milestones < / p > < div className = "mt-2 text-base font-bold text-text" > { project . rollups . overdueMilestoneCount } < / div > < / article >
< article className = "rounded-[18px] 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" > Linked Work Orders < / p > < div className = "mt-2 text-base font-bold text-text" > { project . rollups . workOrderCount } < / div > < div className = "mt-1 text-xs text-muted" > { project . rollups . activeWorkOrderCount } active < / div > < / article >
< article className = "rounded-[18px] 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" > Overdue Work Orders < / p > < div className = "mt-2 text-base font-bold text-text" > { project . rollups . overdueWorkOrderCount } < / div > < div className = "mt-1 text-xs text-muted" > { project . rollups . completedWorkOrderCount } complete < / div > < / article >
2026-03-17 07:34:08 -05:00
< / section >
2026-03-17 07:40:12 -05:00
< 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 >
2026-03-17 19:13:54 -05:00
< p className = "mt-2 text-sm text-muted" > Commercial , supply , execution , purchasing , and delivery signals for this program in one place . < / p >
2026-03-17 07:40:12 -05:00
< / 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" >
2026-03-17 19:13:54 -05:00
< 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" > { formatCurrency ( project . cockpit . commercial . activeDocumentTotal ) } < / div > < div className = "mt-1 text-xs text-muted" > { project . cockpit . commercial . activeDocumentNumber ? ` ${ project . cockpit . commercial . activeDocumentNumber } - ${ project . cockpit . commercial . activeDocumentStatus } ` : "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 : project.cockpit.risk.shortageItemCount } shortage items < / div > < div className = "mt-1 text-xs text-muted" > { planning ? ` Build ${ planning . summary . totalBuildQuantity } - Buy ${ planning . summary . totalPurchaseQuantity } ` : ` Uncovered qty ${ project . cockpit . risk . totalUncoveredQuantity } ` } < / 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" > { project . cockpit . delivery . shipmentStatus ? project . cockpit . delivery . shipmentStatus . replaceAll ( "_" , " " ) : "Not linked" } < / div > < div className = "mt-1 text-xs text-muted" > { project . cockpit . delivery . shipmentNumber ? ` ${ project . cockpit . delivery . shipmentNumber } - ${ project . cockpit . delivery . packageCount } package(s) ` : "Link a shipment to track delivery" } < / div > < / article >
< / div >
< div className = "mt-5 grid gap-3 xl:grid-cols-3" >
< 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" > Purchasing Coverage < / p > < div className = "mt-2 text-base font-bold text-text" > { project . cockpit . purchasing . totalReceivedQuantity } / { project . cockpit . purchasing . totalOrderedQuantity } received < / div > < div className = "mt-1 text-xs text-muted" > { project . cockpit . purchasing . linkedPurchaseOrderCount } linked PO ( s ) - { project . cockpit . purchasing . totalOutstandingQuantity } outstanding < / 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" > Readiness Score < / p > < div className = { ` mt-2 text-base font-bold ${ riskTone } ` } > { readinessScore } % < / div > < div className = "mt-1 text-xs text-muted" > { project . cockpit . risk . riskLevel } risk - { project . cockpit . risk . shortageItemCount } shortage item ( s ) < / 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 Spend < / p > < div className = "mt-2 text-base font-bold text-text" > $ { project . cockpit . purchasing . linkedLineValue . toFixed ( 2 ) } < / div > < div className = "mt-1 text-xs text-muted" > { project . cockpit . purchasing . vendorCount } vendor ( s ) across { project . cockpit . purchasing . linkedLineCount } linked line ( s ) < / div > < / article >
2026-03-17 07:40:12 -05:00
< / div >
2026-03-17 19:17:12 -05:00
< 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" > Booked Revenue < / p > < div className = "mt-2 text-base font-bold text-text" > { formatCurrency ( project . cockpit . costs . bookedRevenue ) } < / div > < div className = "mt-1 text-xs text-muted" > Quoted baseline { formatCurrency ( project . cockpit . costs . quotedRevenue ) } < / 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" > Purchase Commitment < / p > < div className = "mt-2 text-base font-bold text-text" > $ { project . cockpit . costs . linkedPurchaseCommitment . toFixed ( 2 ) } < / div > < div className = "mt-1 text-xs text-muted" > Linked PO line value already committed < / 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" > Planned Material Cost < / p > < div className = "mt-2 text-base font-bold text-text" > $ { project . cockpit . costs . plannedMaterialCost . toFixed ( 2 ) } < / div > < div className = "mt-1 text-xs text-muted" > Issued so far $ { project . cockpit . costs . issuedMaterialCost . toFixed ( 2 ) } < / 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" > Build Load < / p > < div className = "mt-2 text-base font-bold text-text" > { project . cockpit . costs . completedBuildQuantity } / { project . cockpit . costs . buildQuantity } < / div > < div className = "mt-1 text-xs text-muted" > { project . cockpit . costs . plannedOperationHours . toFixed ( 1 ) } planned operation hours < / div > < / article >
< / div >
2026-03-17 07:40:12 -05:00
< 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" >
2026-03-17 19:13:54 -05:00
< 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 >
2026-03-17 07:40:12 -05:00
< / 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 >
2026-03-17 19:13:54 -05:00
{ 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 > }
2026-03-17 07:40:12 -05:00
< / article >
< / div >
< / section >
2026-03-17 19:13:54 -05:00
< section className = "grid gap-3 xl:grid-cols-[minmax(0,1.15fr)_minmax(320px,0.85fr)]" >
< article className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
< div className = "flex items-center justify-between gap-3" > < div > < p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Linked Purchasing < / p > < p className = "mt-2 text-sm text-muted" > Purchase orders and receipts tied back to the project sales order . < / p > < / div > { project . salesOrderId ? < Link to = "/purchasing/orders" className = "inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" > Open purchasing < / Link > : null } < / div >
{ project . cockpit . purchasing . purchaseOrders . length === 0 ? < div className = "mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted" > No linked purchase orders are tied to this project yet . < / div > : < div className = "mt-6 space-y-3" > { project . cockpit . purchasing . purchaseOrders . slice ( 0 , 5 ) . map ( ( purchaseOrder ) = > ( < Link key = { purchaseOrder . id } to = { ` /purchasing/orders/ ${ purchaseOrder . id } ` } className = "block rounded-[18px] border border-line/70 bg-page/60 p-3 transition hover:bg-page/80" > < div className = "flex flex-wrap items-start justify-between gap-3" > < div > < div className = "font-semibold text-text" > { purchaseOrder . documentNumber } < / div > < div className = "mt-1 text-xs text-muted" > { purchaseOrder . vendorName } - { purchaseOrder . status . replaceAll ( "_" , " " ) } < / div > < / div > < div className = "text-right text-xs text-muted" > < div > $ { purchaseOrder . linkedLineValue . toFixed ( 2 ) } linked value < / div > < div > { purchaseOrder . totalReceivedQuantity } / { purchaseOrder . totalOrderedQuantity } received < / div > < / div > < / div > < / Link > ) ) } < / div > }
< / article >
< 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" > Readiness Drivers < / p >
< div className = "mt-5 space-y-3" >
< div className = "rounded-[18px] border border-line/70 bg-page/60 p-3" > < div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Risk posture < / div > < div className = { ` mt-2 text-lg font-bold ${ riskTone } ` } > { project . cockpit . risk . riskLevel } < / div > < div className = "mt-1 text-xs text-muted" > { project . cockpit . risk . outstandingPurchaseOrderCount } PO ( s ) still waiting on receipts . < / div > < / div >
< div className = "rounded-[18px] border border-line/70 bg-page/60 p-3 text-sm text-text" > Blocked milestones : < span className = "font-semibold" > { project . cockpit . risk . blockedMilestoneCount } < / span > < / div >
< div className = "rounded-[18px] border border-line/70 bg-page/60 p-3 text-sm text-text" > Overdue execution items : < span className = "font-semibold" > { project . cockpit . risk . overdueMilestoneCount + project . cockpit . risk . overdueWorkOrderCount } < / span > < / div >
< div className = "rounded-[18px] border border-line/70 bg-page/60 p-3 text-sm text-text" > Uncovered material quantity : < span className = "font-semibold" > { project . cockpit . risk . totalUncoveredQuantity } < / span > < / div >
< / div >
< / article >
< / section >
< section className = "grid gap-3 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]" >
< 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" > Vendor Exposure < / p >
{ project . cockpit . purchasing . vendors . length === 0 ? < div className = "mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted" > No supplier exposure exists until purchasing is linked . < / div > : < div className = "mt-5 space-y-3" > { project . cockpit . purchasing . vendors . slice ( 0 , 4 ) . map ( ( vendor ) = > ( < div key = { vendor . vendorId } className = "rounded-[18px] border border-line/70 bg-page/60 p-3" > < div className = "flex flex-wrap items-center justify-between gap-3" > < div > < div className = "font-semibold text-text" > { vendor . vendorName } < / div > < div className = "mt-1 text-xs text-muted" > { vendor . orderCount } linked order ( s ) < / div > < / div > < div className = "text-right text-xs text-muted" > < div > $ { vendor . linkedLineValue . toFixed ( 2 ) } < / div > < div > { vendor . outstandingQuantity } outstanding qty < / div > < / div > < / div > < / div > ) ) } < / div > }
< / article >
< 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" > Recent Receipts < / p >
{ project . cockpit . purchasing . recentReceipts . length === 0 ? < div className = "mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted" > No purchase receipts have been posted against linked project supply . < / div > : < div className = "mt-5 space-y-3" > { project . cockpit . purchasing . recentReceipts . map ( ( receipt ) = > ( < div key = { receipt . receiptId } className = "rounded-[18px] border border-line/70 bg-page/60 p-3" > < div className = "flex flex-wrap items-start justify-between gap-3" > < div > < div className = "font-semibold text-text" > { receipt . receiptNumber } < / div > < div className = "mt-1 text-xs text-muted" > { receipt . vendorName } - { receipt . purchaseOrderNumber } < / div > < / div > < div className = "text-right text-xs text-muted" > < div > { new Date ( receipt . receivedAt ) . toLocaleDateString ( ) } < / div > < div > { receipt . totalQuantity } units received < / div > < / div > < / div > < / div > ) ) } < / div > }
< / article >
< / section >
2026-03-15 10:13:53 -05:00
< div className = "grid gap-3 xl:grid-cols-[minmax(0,1.05fr)_minmax(320px,0.95fr)]" >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-15 10:13:53 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Customer Linkage < / p >
< dl className = "mt-5 grid gap-3" >
< div > < dt className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Account < / dt > < dd className = "mt-1 text-sm text-text" > < Link to = { ` /crm/customers/ ${ project . customerId } ` } className = "hover:text-brand" > { project . customerName } < / Link > < / dd > < / div >
< div > < dt className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Email < / dt > < dd className = "mt-1 text-sm text-text" > { project . customerEmail } < / dd > < / div >
< div > < dt className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Phone < / dt > < dd className = "mt-1 text-sm text-text" > { project . customerPhone } < / dd > < / div >
< / dl >
< / article >
2026-03-15 20:07:48 -05:00
< article className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-15 10:13:53 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Program Notes < / p >
< p className = "mt-3 whitespace-pre-line text-sm leading-6 text-text" > { project . notes || "No project notes recorded." } < / p >
< / article >
< / div >
2026-03-15 20:07:48 -05:00
< section className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-15 10:13:53 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Commercial + Delivery Links < / p >
< div className = "mt-5 grid gap-3 xl:grid-cols-3" >
2026-03-17 19:13:54 -05:00
< div className = "rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm" > < div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Quote < / div > < div className = "mt-2 font-semibold text-text" > { project . salesQuoteNumber ? < Link to = { ` /sales/quotes/ ${ project . salesQuoteId } ` } className = "hover:text-brand" > { project . salesQuoteNumber } < / Link > : "Not linked" } < / div > < / div >
< div className = "rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm" > < div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Sales Order < / div > < div className = "mt-2 font-semibold text-text" > { project . salesOrderNumber ? < Link to = { ` /sales/orders/ ${ project . salesOrderId } ` } className = "hover:text-brand" > { project . salesOrderNumber } < / Link > : "Not linked" } < / div > < / div >
< div className = "rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm" > < div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted" > Shipment < / div > < div className = "mt-2 font-semibold text-text" > { project . shipmentNumber ? < Link to = { ` /shipping/shipments/ ${ project . shipmentId } ` } className = "hover:text-brand" > { project . shipmentNumber } < / Link > : "Not linked" } < / div > < / div >
2026-03-15 10:13:53 -05:00
< / div >
< / section >
2026-03-17 07:34:08 -05:00
< section className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
< div className = "flex items-center justify-between gap-3" >
2026-03-17 19:13:54 -05:00
< div > < p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Milestones < / p > < p className = "mt-2 text-sm text-muted" > Track project checkpoints , blockers , and completion progress . < / p > < / div >
{ canManage ? < Link to = { ` /projects/ ${ project . id } /edit ` } className = "inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" > Edit milestones < / Link > : null }
2026-03-17 07:34:08 -05:00
< / div >
2026-03-17 19:13:54 -05:00
{ project . milestones . length === 0 ? < div className = "mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted" > No milestones are defined for this project yet . < / div > : < div className = "mt-6 space-y-3" > { project . milestones . map ( ( milestone ) = > ( < div key = { milestone . id } className = "rounded-[18px] border border-line/70 bg-page/60 p-3" > < div className = "flex flex-wrap items-start justify-between gap-3" > < div className = "min-w-0" > < div className = "font-semibold text-text" > { milestone . title } < / div > < div className = "mt-2 flex flex-wrap items-center gap-2" > < span className = { ` inline-flex rounded-full px-2 py-1 text-xs font-semibold uppercase tracking-[0.16em] ${ projectMilestoneStatusPalette [ milestone . status ] } ` } > { milestone . status . replace ( "_" , " " ) } < / span > < span className = "text-xs text-muted" > Due { milestone . dueDate ? new Date ( milestone . dueDate ) . toLocaleDateString ( ) : "not scheduled" } < / span > { milestone . completedAt ? < span className = "text-xs text-muted" > Completed { new Date ( milestone . completedAt ) . toLocaleDateString ( ) } < / span > : null } < / div > { milestone . notes ? < div className = "mt-3 whitespace-pre-line text-sm text-text" > { milestone . notes } < / div > : null } < / div > < / div > < / div > ) ) } < / div > }
2026-03-17 07:34:08 -05:00
< / section >
2026-03-15 16:40:25 -05:00
{ planning ? (
2026-03-15 20:07:48 -05:00
< section className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-15 16:40:25 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Material Readiness < / p >
< div className = "mt-5 grid gap-3 xl:grid-cols-4" >
2026-03-17 19:13:54 -05:00
< article className = "rounded-[18px] border border-line/70 bg-page/60 px-3 py-3" > < p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Build Qty < / p > < div className = "mt-2 text-base font-bold text-text" > { planning . summary . totalBuildQuantity } < / div > < / article >
< article className = "rounded-[18px] border border-line/70 bg-page/60 px-3 py-3" > < p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Buy Qty < / p > < div className = "mt-2 text-base font-bold text-text" > { planning . summary . totalPurchaseQuantity } < / div > < / article >
< article className = "rounded-[18px] border border-line/70 bg-page/60 px-3 py-3" > < p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Uncovered Qty < / p > < div className = "mt-2 text-base font-bold text-text" > { planning . summary . totalUncoveredQuantity } < / div > < / article >
< article className = "rounded-[18px] border border-line/70 bg-page/60 px-3 py-3" > < p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Shortage Items < / p > < div className = "mt-2 text-base font-bold text-text" > { planning . summary . uncoveredItemCount } < / div > < / article >
2026-03-15 16:40:25 -05:00
< / div >
< div className = "mt-5 space-y-3" >
2026-03-17 19:13:54 -05:00
{ planning . items . filter ( ( item ) = > item . recommendedBuildQuantity > 0 || item . recommendedPurchaseQuantity > 0 || item . uncoveredQuantity > 0 ) . slice ( 0 , 8 ) . map ( ( item ) = > (
< div key = { item . itemId } className = "rounded-[18px] border border-line/70 bg-page/60 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-sm text-muted" > Build { item . recommendedBuildQuantity } - Buy { item . recommendedPurchaseQuantity } - Uncovered { item . uncoveredQuantity } < / div > < / div > < / div >
) ) }
2026-03-15 16:40:25 -05:00
< / div >
< / section >
) : null }
2026-03-15 20:07:48 -05:00
< section className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5" >
2026-03-15 11:30:10 -05:00
< div className = "flex items-center justify-between gap-3" >
2026-03-17 19:13:54 -05:00
< div > < p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Manufacturing Links < / p > < p className = "mt-2 text-sm text-muted" > Work orders already linked to this project . < / p > < / div >
{ canManage ? < Link to = { ` /manufacturing/work-orders/new?projectId= ${ project . id } ` } className = "inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" > New work order < / Link > : null }
2026-03-15 11:30:10 -05:00
< / div >
2026-03-17 19:13:54 -05:00
{ workOrders . length === 0 ? < div className = "mt-6 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted" > No work orders are linked to this project yet . < / div > : < div className = "mt-6 space-y-3" > { workOrders . map ( ( workOrder ) = > ( < Link key = { workOrder . id } to = { ` /manufacturing/work-orders/ ${ workOrder . id } ` } className = "block rounded-[18px] border border-line/70 bg-page/60 p-3 transition hover:bg-page/80" > < div className = "flex flex-wrap items-center justify-between gap-3" > < div > < div className = "font-semibold text-text" > { workOrder . workOrderNumber } < / div > < div className = "mt-1 text-xs text-muted" > { workOrder . itemSku } - { workOrder . completedQuantity } / { workOrder . quantity } complete < / div > < / div > < div className = "text-sm font-semibold text-text" > { workOrder . status . replace ( "_" , " " ) } < / div > < / div > < / Link > ) ) } < / div > }
2026-03-15 11:30:10 -05:00
< / section >
2026-03-17 19:13:54 -05:00
< FileAttachmentsPanel ownerType = "PROJECT" ownerId = { project . id } eyebrow = "Project Documents" title = "Program file hub" description = "Store drawings, revision references, correspondence, and support files directly on the project record." emptyMessage = "No project files have been uploaded yet." / >
2026-03-15 10:13:53 -05:00
< div className = "rounded-2xl border border-line/70 bg-page/60 px-2 py-2 text-sm text-muted" > { status } < / div >
< / section >
) ;
}