2026-03-14 14:44:40 -05:00
import { Gantt } from "@svar-ui/react-gantt" ;
import "@svar-ui/react-gantt/style.css" ;
2026-03-15 16:40:25 -05:00
import type { DemandPlanningRollupDto , GanttTaskDto , PlanningExceptionDto , PlanningTimelineDto } from "@mrp/shared" ;
2026-03-17 21:12:27 -05:00
import { useEffect , useMemo , useState } from "react" ;
import { Link } from "react-router-dom" ;
2026-03-14 14:44:40 -05:00
import { useAuth } from "../../auth/AuthProvider" ;
2026-03-15 12:11:46 -05:00
import { ApiError , api } from "../../lib/api" ;
2026-03-14 15:47:29 -05:00
import { useTheme } from "../../theme/ThemeProvider" ;
2026-03-14 14:44:40 -05:00
2026-03-17 21:12:27 -05:00
type WorkbenchMode = "overview" | "gantt" | "heatmap" | "agenda" ;
type FocusRecord = {
id : string ;
title : string ;
kind : "PROJECT" | "WORK_ORDER" | "OPERATION" | "MILESTONE" ;
status : string ;
ownerLabel : string | null ;
start : string ;
end : string ;
progress : number ;
detailHref : string | null ;
parentId : string | null ;
} ;
type HeatmapCell = {
dateKey : string ;
count : number ;
lateCount : number ;
blockedCount : number ;
tasks : FocusRecord [ ] ;
} ;
const DAY_MS = 24 * 60 * 60 * 1000 ;
function formatDate ( value : string | null , options? : Intl.DateTimeFormatOptions ) {
2026-03-15 12:11:46 -05:00
if ( ! value ) {
return "Unscheduled" ;
}
2026-03-17 21:12:27 -05:00
return new Intl . DateTimeFormat ( "en-US" , options ? ? {
2026-03-15 12:11:46 -05:00
month : "short" ,
day : "numeric" ,
} ) . format ( new Date ( value ) ) ;
}
2026-03-17 21:12:27 -05:00
function startOfDay ( value : Date ) {
return new Date ( value . getFullYear ( ) , value . getMonth ( ) , value . getDate ( ) ) ;
}
function dateKey ( value : Date ) {
return value . toISOString ( ) . slice ( 0 , 10 ) ;
}
function parseFocusKind ( task : GanttTaskDto ) : FocusRecord [ "kind" ] {
if ( task . type === "project" ) {
return "PROJECT" ;
}
if ( task . type === "milestone" ) {
return "MILESTONE" ;
}
if ( task . id . startsWith ( "work-order-operation-" ) ) {
return "OPERATION" ;
}
return "WORK_ORDER" ;
}
function densityTone ( cell : HeatmapCell ) {
if ( cell . lateCount > 0 ) {
return "border-rose-400/60 bg-rose-500/25" ;
}
if ( cell . blockedCount > 0 ) {
return "border-amber-300/60 bg-amber-400/25" ;
}
if ( cell . count >= 4 ) {
return "border-brand/80 bg-brand/35" ;
}
if ( cell . count >= 2 ) {
return "border-brand/50 bg-brand/20" ;
}
if ( cell . count === 1 ) {
return "border-line/80 bg-page/80" ;
}
return "border-line/60 bg-surface/70" ;
}
function buildFocusRecords ( tasks : GanttTaskDto [ ] ) {
return tasks . map ( ( task ) = > ( {
id : task.id ,
title : task.text ,
kind : parseFocusKind ( task ) ,
status : task.status ? ? "PLANNED" ,
ownerLabel : task.ownerLabel ? ? null ,
start : task.start ,
end : task.end ,
progress : task.progress ,
detailHref : task.detailHref ? ? null ,
parentId : task.parentId ? ? null ,
} ) ) ;
}
2026-03-14 14:44:40 -05:00
export function GanttPage() {
const { token } = useAuth ( ) ;
2026-03-14 15:47:29 -05:00
const { mode } = useTheme ( ) ;
2026-03-15 12:11:46 -05:00
const [ timeline , setTimeline ] = useState < PlanningTimelineDto | null > ( null ) ;
2026-03-15 16:40:25 -05:00
const [ planningRollup , setPlanningRollup ] = useState < DemandPlanningRollupDto | null > ( null ) ;
2026-03-15 12:11:46 -05:00
const [ status , setStatus ] = useState ( "Loading live planning timeline..." ) ;
2026-03-17 21:12:27 -05:00
const [ workbenchMode , setWorkbenchMode ] = useState < WorkbenchMode > ( "overview" ) ;
const [ selectedFocusId , setSelectedFocusId ] = useState < string | null > ( null ) ;
const [ selectedHeatmapDate , setSelectedHeatmapDate ] = useState < string | null > ( null ) ;
2026-03-14 14:44:40 -05:00
useEffect ( ( ) = > {
if ( ! token ) {
return ;
}
2026-03-15 16:40:25 -05:00
Promise . all ( [ api . getPlanningTimeline ( token ) , api . getDemandPlanningRollup ( token ) ] )
. then ( ( [ data , rollup ] ) = > {
2026-03-15 12:11:46 -05:00
setTimeline ( data ) ;
2026-03-15 16:40:25 -05:00
setPlanningRollup ( rollup ) ;
2026-03-17 21:12:27 -05:00
setStatus ( "Planning workbench loaded." ) ;
2026-03-15 12:11:46 -05:00
} )
. catch ( ( error : unknown ) = > {
const message = error instanceof ApiError ? error . message : "Unable to load planning timeline." ;
setStatus ( message ) ;
} ) ;
2026-03-14 14:44:40 -05:00
} , [ token ] ) ;
2026-03-15 12:11:46 -05:00
const tasks = timeline ? . tasks ? ? [ ] ;
const links = timeline ? . links ? ? [ ] ;
const summary = timeline ? . summary ;
const exceptions = timeline ? . exceptions ? ? [ ] ;
2026-03-17 21:12:27 -05:00
const focusRecords = useMemo ( ( ) = > buildFocusRecords ( tasks ) , [ tasks ] ) ;
const focusById = useMemo ( ( ) = > new Map ( focusRecords . map ( ( record ) = > [ record . id , record ] ) ) , [ focusRecords ] ) ;
const selectedFocus = selectedFocusId ? focusById . get ( selectedFocusId ) ? ? null : focusRecords [ 0 ] ? ? null ;
const ganttCellHeight = 38 ;
const ganttScaleHeight = 54 ;
const ganttHeight = Math . max ( 520 , tasks . length * ganttCellHeight + ganttScaleHeight ) ;
const heatmap = useMemo ( ( ) = > {
const start = summary ? startOfDay ( new Date ( summary . horizonStart ) ) : startOfDay ( new Date ( ) ) ;
const cells = new Map < string , HeatmapCell > ( ) ;
for ( let index = 0 ; index < 84 ; index += 1 ) {
const nextDate = new Date ( start . getTime ( ) + index * DAY_MS ) ;
cells . set ( dateKey ( nextDate ) , { dateKey : dateKey ( nextDate ) , count : 0 , lateCount : 0 , blockedCount : 0 , tasks : [ ] } ) ;
}
for ( const record of focusRecords ) {
if ( record . kind === "PROJECT" ) {
continue ;
}
const startDate = startOfDay ( new Date ( record . start ) ) ;
const endDate = startOfDay ( new Date ( record . end ) ) ;
for ( let cursor = startDate . getTime ( ) ; cursor <= endDate . getTime ( ) ; cursor += DAY_MS ) {
const key = dateKey ( new Date ( cursor ) ) ;
const current = cells . get ( key ) ;
if ( ! current ) {
continue ;
}
current . count += 1 ;
if ( record . status === "AT_RISK" || record . status === "ON_HOLD" ) {
current . blockedCount += 1 ;
}
if ( new Date ( record . end ) . getTime ( ) < Date . now ( ) && record . status !== "COMPLETE" && record . status !== "CANCELLED" ) {
current . lateCount += 1 ;
}
current . tasks . push ( record ) ;
}
}
return [ . . . cells . values ( ) ] ;
} , [ focusRecords , summary ] ) ;
const selectedHeatmapCell = selectedHeatmapDate ? heatmap . find ( ( cell ) = > cell . dateKey === selectedHeatmapDate ) ? ? null : null ;
const agendaItems = useMemo (
( ) = > [ . . . focusRecords ]
. filter ( ( record ) = > record . kind !== "OPERATION" )
. sort ( ( left , right ) = > new Date ( left . end ) . getTime ( ) - new Date ( right . end ) . getTime ( ) )
. slice ( 0 , 18 ) ,
[ focusRecords ]
) ;
const modeOptions : Array < { value : WorkbenchMode ; label : string ; detail : string } > = [
{ value : "overview" , label : "Overview" , detail : "Dense planner board" } ,
{ value : "gantt" , label : "Timeline" , detail : "Classic gantt lens" } ,
{ value : "heatmap" , label : "Heatmap" , detail : "Load by day" } ,
{ value : "agenda" , label : "Agenda" , detail : "Upcoming due flow" } ,
] ;
2026-03-15 12:11:46 -05:00
2026-03-14 14:44:40 -05:00
return (
2026-03-15 12:11:46 -05:00
< 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-17 21:12:27 -05:00
< div className = "flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between" >
2026-03-15 12:11:46 -05:00
< div >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Planning < / p >
2026-03-17 21:12:27 -05:00
< h3 className = "mt-2 text-2xl font-bold text-text" > Planning Workbench < / h3 >
< p className = "mt-2 max-w-4xl text-sm text-muted" > A reactive planning surface for projects , work orders , operations , shortages , and schedule risk . Use it as the daily planner cockpit , not just a chart . < / p >
2026-03-15 12:11:46 -05:00
< / div >
2026-03-15 20:07:48 -05:00
< div className = "rounded-[18px] border border-line/70 bg-page/60 px-3 py-3 text-sm" >
2026-03-17 21:12:27 -05:00
< div className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Workbench Status < / div >
2026-03-15 12:11:46 -05:00
< div className = "mt-2 font-semibold text-text" > { status } < / div >
< / div >
< / div >
2026-03-17 21:12:27 -05:00
< div className = "mt-5 grid gap-3 xl:grid-cols-4" >
{ modeOptions . map ( ( option ) = > (
< button key = { option . value } type = "button" onClick = { ( ) = > setWorkbenchMode ( option . value ) } className = { ` rounded-[18px] border px-3 py-3 text-left transition ${ workbenchMode === option . value ? "border-brand bg-brand/10" : "border-line/70 bg-page/60 hover:border-brand/40" } ` } >
< div className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > { option . label } < / div >
< div className = "mt-2 text-sm font-semibold text-text" > { option . detail } < / div >
< / button >
) ) }
< / div >
2026-03-15 12:11:46 -05:00
< / div >
2026-03-17 21:12:27 -05:00
< section className = "grid gap-3 xl:grid-cols-8" >
< MetricCard label = "Active Projects" value = { summary ? . activeProjects ? ? 0 } / >
< MetricCard label = "At Risk" value = { summary ? . atRiskProjects ? ? 0 } / >
< MetricCard label = "Overdue Projects" value = { summary ? . overdueProjects ? ? 0 } / >
< MetricCard label = "Active Work" value = { summary ? . activeWorkOrders ? ? 0 } / >
< MetricCard label = "Overdue Work" value = { summary ? . overdueWorkOrders ? ? 0 } / >
< MetricCard label = "Unscheduled" value = { summary ? . unscheduledWorkOrders ? ? 0 } / >
< MetricCard label = "Shortage Items" value = { planningRollup ? . summary . uncoveredItemCount ? ? 0 } / >
< MetricCard label = "Build / Buy" value = { planningRollup ? ` ${ planningRollup . summary . totalBuildQuantity } / ${ planningRollup . summary . totalPurchaseQuantity } ` : "0 / 0" } / >
2026-03-15 12:11:46 -05:00
< / section >
2026-03-17 21:12:27 -05:00
< div className = "grid gap-3 xl:grid-cols-[320px_minmax(0,1fr)_360px]" >
2026-03-15 12:11:46 -05:00
< aside className = "space-y-3" >
2026-03-17 21:12:27 -05:00
< section className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel" >
< div className = "flex items-center justify-between gap-3" >
< div >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Exception Rail < / p >
< p className = "mt-2 text-sm text-muted" > Late , at - risk , and unscheduled items that require planner attention . < / p >
2026-03-15 12:11:46 -05:00
< / div >
2026-03-17 21:12:27 -05:00
< span className = "rounded-full border border-line/70 px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-muted" > { exceptions . length } < / span >
< / div >
{ exceptions . length === 0 ? (
< div className = "mt-5 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted" > No planning exceptions are active . < / div >
2026-03-15 12:11:46 -05:00
) : (
< div className = "mt-5 space-y-3" >
{ exceptions . map ( ( exception : PlanningExceptionDto ) = > (
2026-03-17 21:12:27 -05:00
< button key = { exception . id } type = "button" onClick = { ( ) = > setSelectedFocusId ( exception . id . startsWith ( "project-" ) ? exception.id : exception.id.replace ( "work-order-unscheduled-" , "work-order-" ) ) } className = "block w-full rounded-[18px] border border-line/70 bg-page/60 p-3 text-left transition hover:bg-page/80" >
2026-03-15 12:11:46 -05:00
< div className = "flex items-start justify-between gap-3" >
< div >
< div className = "text-xs font-semibold uppercase tracking-[0.16em] text-muted" > { exception . kind === "PROJECT" ? "Project" : "Work Order" } < / div >
< div className = "mt-1 font-semibold text-text" > { exception . title } < / div >
< div className = "mt-2 text-xs text-muted" > { exception . ownerLabel ? ? "No owner context" } < / div >
< / div >
2026-03-17 21:12:27 -05:00
< span className = "rounded-full border border-line/70 px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-muted" > { exception . status . replaceAll ( "_" , " " ) } < / span >
2026-03-15 12:11:46 -05:00
< / div >
< div className = "mt-3 text-xs text-muted" > Due : { formatDate ( exception . dueDate ) } < / div >
2026-03-17 21:12:27 -05:00
< / button >
2026-03-15 12:11:46 -05:00
) ) }
< / div >
) }
< / section >
2026-03-17 21:12:27 -05:00
< section className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel" >
2026-03-15 12:11:46 -05:00
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Planner Actions < / p >
2026-03-15 20:07:48 -05:00
< div className = "mt-4 space-y-2 rounded-[18px] border border-line/70 bg-page/60 p-3 text-sm" >
2026-03-17 21:12:27 -05:00
< div className = "flex items-center justify-between gap-3" > < span className = "text-muted" > Uncovered quantity < / span > < span className = "font-semibold text-text" > { planningRollup ? . summary . totalUncoveredQuantity ? ? 0 } < / span > < / div >
< div className = "flex items-center justify-between gap-3" > < span className = "text-muted" > Projects with linked demand < / span > < span className = "font-semibold text-text" > { planningRollup ? . summary . projectCount ? ? 0 } < / span > < / div >
2026-03-15 16:40:25 -05:00
< / div >
2026-03-15 12:11:46 -05:00
< div className = "mt-4 flex flex-wrap gap-2" >
2026-03-17 21:12:27 -05:00
< Link to = "/projects" className = "rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" > Open projects < / Link >
< Link to = "/manufacturing/work-orders" className = "rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" > Open work orders < / Link >
< Link to = "/purchasing/orders" className = "rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" > Open purchasing < / Link >
2026-03-15 12:11:46 -05:00
< / div >
< / section >
< / aside >
2026-03-17 21:12:27 -05:00
< div className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel" >
{ workbenchMode === "overview" ? < OverviewBoard focusRecords = { focusRecords } onSelect = { setSelectedFocusId } / > : null }
{ workbenchMode === "gantt" ? (
< div className = { ` gantt-theme overflow-x-auto overflow-y-visible rounded-[18px] border border-line/70 bg-page/60 p-4 ${ mode === "dark" ? "wx-willow-dark-theme" : "wx-willow-theme" } ` } >
< div className = "mb-4 flex flex-wrap items-center justify-between gap-3" >
< div >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Schedule Window < / p >
< p className = "mt-2 text-sm text-muted" > { summary ? ` ${ formatDate ( summary . horizonStart ) } through ${ formatDate ( summary . horizonEnd ) } ` : "Waiting for live schedule data." } < / p >
< / div >
< div className = "rounded-2xl border border-line/70 bg-surface/80 px-3 py-2 text-xs font-semibold uppercase tracking-[0.18em] text-muted" > { tasks . length } schedule rows < / div >
< / div >
< div style = { { height : ` ${ ganttHeight } px ` , minWidth : "100%" } } >
< Gantt
tasks = { tasks . map ( ( task : GanttTaskDto ) = > ( {
. . . task ,
start : new Date ( task . start ) ,
end : new Date ( task . end ) ,
parent : task.parentId ? ? undefined ,
} ) ) }
links = { links }
cellHeight = { ganttCellHeight }
scaleHeight = { ganttScaleHeight }
/ >
< / div >
< / div >
) : null }
{ workbenchMode === "heatmap" ? < HeatmapBoard heatmap = { heatmap } selectedDate = { selectedHeatmapDate } onSelectDate = { setSelectedHeatmapDate } / > : null }
{ workbenchMode === "agenda" ? < AgendaBoard records = { agendaItems } onSelect = { setSelectedFocusId } / > : null }
< / div >
< aside className = "space-y-3" >
< section className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel" >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Focus Drawer < / p >
{ selectedFocus ? (
< div className = "mt-4 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" > { selectedFocus . kind } < / div >
< div className = "mt-2 text-base font-bold text-text" > { selectedFocus . title } < / div >
< div className = "mt-2 text-xs text-muted" > { selectedFocus . ownerLabel ? ? "No context label" } < / div >
< / div >
< div className = "rounded-[18px] border border-line/70 bg-page/60 p-3 text-sm" >
< div className = "flex items-center justify-between gap-3" > < span className = "text-muted" > Status < / span > < span className = "font-semibold text-text" > { selectedFocus . status . replaceAll ( "_" , " " ) } < / span > < / div >
< div className = "mt-2 flex items-center justify-between gap-3" > < span className = "text-muted" > Window < / span > < span className = "font-semibold text-text" > { formatDate ( selectedFocus . start ) } - { formatDate ( selectedFocus . end ) } < / span > < / div >
< div className = "mt-2 flex items-center justify-between gap-3" > < span className = "text-muted" > Progress < / span > < span className = "font-semibold text-text" > { selectedFocus . progress } % < / span > < / div >
< / div >
< div className = "flex flex-wrap gap-2" >
{ selectedFocus . detailHref ? < Link to = { selectedFocus . detailHref } className = "rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white" > Open record < / Link > : null }
< button type = "button" onClick = { ( ) = > setWorkbenchMode ( "gantt" ) } className = "rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" > View in timeline < / button >
< button type = "button" onClick = { ( ) = > setWorkbenchMode ( "heatmap" ) } className = "rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text" > View load < / button >
< / div >
< / div >
) : (
< div className = "mt-5 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted" > Select a project or work order to inspect it . < / div >
) }
< / section >
< section className = "rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel" >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > { workbenchMode === "heatmap" ? "Selected Day" : "Upcoming Agenda" } < / p >
{ workbenchMode === "heatmap"
? ( selectedHeatmapCell ? < SelectedDayPanel cell = { selectedHeatmapCell } onSelect = { setSelectedFocusId } / > : < div className = "mt-5 rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted" > Select a day in the heatmap to inspect its load . < / div > )
: < AgendaBoard records = { agendaItems . slice ( 0 , 8 ) } onSelect = { setSelectedFocusId } compact / > }
< / section >
< / aside >
2026-03-14 14:44:40 -05:00
< / div >
< / section >
) ;
}
2026-03-15 20:07:48 -05:00
2026-03-17 21:12:27 -05:00
function MetricCard ( { label , value } : { label : string ; value : string | number } ) {
return (
< 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" > { label } < / p >
< div className = "mt-2 text-xl font-extrabold text-text" > { value } < / div >
< / article >
) ;
}
function OverviewBoard ( { focusRecords , onSelect } : { focusRecords : FocusRecord [ ] ; onSelect : ( id : string ) = > void } ) {
const projects = focusRecords . filter ( ( record ) = > record . kind === "PROJECT" ) . slice ( 0 , 6 ) ;
const operations = focusRecords . filter ( ( record ) = > record . kind === "OPERATION" ) . slice ( 0 , 10 ) ;
const workOrders = focusRecords . filter ( ( record ) = > record . kind === "WORK_ORDER" ) . slice ( 0 , 10 ) ;
return (
< div className = "space-y-4" >
< div className = "flex flex-wrap items-center justify-between gap-3" >
< div >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Overview < / p >
< p className = "mt-2 text-sm text-muted" > Scan project rollups , active work , and operation load without leaving the planner . < / p >
< / div >
< / div >
< div className = "grid gap-3 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]" >
< section className = "rounded-[18px] border border-line/70 bg-page/60 p-3" >
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Program Queue < / p >
< div className = "mt-3 space-y-3" >
{ projects . map ( ( record ) = > (
< button key = { record . id } type = "button" onClick = { ( ) = > onSelect ( record . id ) } className = "block w-full rounded-[16px] border border-line/70 bg-surface/80 p-3 text-left transition hover:bg-surface" >
< div className = "flex flex-wrap items-center justify-between gap-3" >
< div >
< div className = "font-semibold text-text" > { record . title } < / div >
< div className = "mt-1 text-xs text-muted" > { record . ownerLabel ? ? "No owner context" } < / div >
< / div >
< div className = "text-right text-xs text-muted" >
< div > { record . status . replaceAll ( "_" , " " ) } < / div >
< div > { record . progress } % progress < / div >
< / div >
< / div >
< / button >
) ) }
< / div >
< / section >
< section className = "rounded-[18px] border border-line/70 bg-page/60 p-3" >
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Operation Load < / p >
< div className = "mt-3 space-y-2" >
{ operations . map ( ( record ) = > (
< button key = { record . id } type = "button" onClick = { ( ) = > onSelect ( record . id ) } className = "flex w-full items-center justify-between gap-3 rounded-[16px] border border-line/70 bg-surface/80 px-3 py-2 text-left transition hover:bg-surface" >
< div >
< div className = "font-semibold text-text" > { record . title } < / div >
< div className = "mt-1 text-xs text-muted" > { record . ownerLabel ? ? "No parent work order" } < / div >
< / div >
< div className = "text-xs text-muted" > { formatDate ( record . start ) } - { formatDate ( record . end ) } < / div >
< / button >
) ) }
< / div >
< / section >
< / div >
< section className = "rounded-[18px] border border-line/70 bg-page/60 p-3" >
< p className = "text-xs font-semibold uppercase tracking-[0.18em] text-muted" > Active Work Orders < / p >
< div className = "mt-3 grid gap-3 xl:grid-cols-2" >
{ workOrders . map ( ( record ) = > (
< button key = { record . id } type = "button" onClick = { ( ) = > onSelect ( record . id ) } className = "rounded-[16px] border border-line/70 bg-surface/80 p-3 text-left transition hover:bg-surface" >
< div className = "font-semibold text-text" > { record . title } < / div >
< div className = "mt-2 flex items-center justify-between gap-3 text-xs text-muted" >
< span > { record . status . replaceAll ( "_" , " " ) } < / span >
< span > { record . progress } % < / span >
< / div >
< / button >
) ) }
< / div >
< / section >
< / div >
) ;
}
function HeatmapBoard ( { heatmap , selectedDate , onSelectDate } : { heatmap : HeatmapCell [ ] ; selectedDate : string | null ; onSelectDate : ( date : string ) = > void } ) {
const weeks = [ ] ;
for ( let index = 0 ; index < heatmap . length ; index += 7 ) {
weeks . push ( heatmap . slice ( index , index + 7 ) ) ;
}
return (
< div className = "space-y-4" >
< div >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Load Heatmap < / p >
< p className = "mt-2 text-sm text-muted" > Dense daily load scan for operations and work orders , with late and blocked pressure highlighted . < / p >
< / div >
< div className = "overflow-x-auto rounded-[18px] border border-line/70 bg-page/60 p-4" >
< div className = "flex gap-2" >
< div className = "flex flex-col gap-2 pt-7" >
{ [ "M" , "T" , "W" , "T" , "F" , "S" , "S" ] . map ( ( label ) = > < div key = { label } className = "h-9 text-xs font-semibold text-muted" > { label } < / div > ) }
< / div >
{ weeks . map ( ( week , weekIndex ) = > (
< div key = { weekIndex } className = "flex flex-col gap-2" >
< div className = "h-5 text-[11px] font-semibold uppercase tracking-[0.16em] text-muted" > { formatDate ( week [ 0 ] ? . dateKey ? ? null , { month : "short" } ) } < / div >
{ week . map ( ( cell ) = > (
< button
key = { cell . dateKey }
type = "button"
onClick = { ( ) = > onSelectDate ( cell . dateKey ) }
className = { ` h-9 w-9 rounded-md border text-[10px] font-semibold transition hover:scale-110 ${ densityTone ( cell ) } ${ selectedDate === cell . dateKey ? "ring-2 ring-brand" : "" } ` }
title = { ` ${ cell . dateKey } : ${ cell . count } scheduled ` }
>
{ new Date ( cell . dateKey ) . getDate ( ) }
< / button >
) ) }
< / div >
) ) }
< / div >
< / div >
< / div >
) ;
}
function AgendaBoard ( { records , onSelect , compact = false } : { records : FocusRecord [ ] ; onSelect : ( id : string ) = > void ; compact? : boolean } ) {
return (
< div className = { compact ? "mt-4 space-y-3" : "space-y-4" } >
{ ! compact ? (
< div >
< p className = "text-xs font-semibold uppercase tracking-[0.24em] text-muted" > Agenda < / p >
< p className = "mt-2 text-sm text-muted" > Upcoming projects , work orders , and milestones ordered by due date . < / p >
< / div >
) : null }
< div className = "space-y-2" >
{ records . map ( ( record ) = > (
< button key = { record . id } type = "button" onClick = { ( ) = > onSelect ( record . id ) } className = "flex w-full items-center justify-between gap-3 rounded-[16px] border border-line/70 bg-page/60 px-3 py-3 text-left transition hover:bg-page/80" >
< div >
< div className = "font-semibold text-text" > { record . title } < / div >
< div className = "mt-1 text-xs text-muted" > { record . kind } - { record . ownerLabel ? ? "No context" } < / div >
< / div >
< div className = "text-right text-xs text-muted" >
< div > { formatDate ( record . end ) } < / div >
< div > { record . status . replaceAll ( "_" , " " ) } < / div >
< / div >
< / button >
) ) }
< / div >
< / div >
) ;
}
function SelectedDayPanel ( { cell , onSelect } : { cell : HeatmapCell ; onSelect : ( id : string ) = > void } ) {
return (
< div className = "mt-4 space-y-3" >
< div className = "rounded-[18px] border border-line/70 bg-page/60 p-3" >
< div className = "text-sm font-semibold text-text" > { formatDate ( cell . dateKey , { weekday : "short" , month : "short" , day : "numeric" } ) } < / div >
< div className = "mt-2 flex items-center justify-between gap-3 text-xs text-muted" >
< span > { cell . count } scheduled < / span >
< span > { cell . lateCount } late < / span >
< / div >
< / div >
< div className = "space-y-2" >
{ cell . tasks . slice ( 0 , 8 ) . map ( ( task ) = > (
< button key = { task . id } type = "button" onClick = { ( ) = > onSelect ( task . id ) } className = "block w-full rounded-[16px] border border-line/70 bg-page/60 p-3 text-left transition hover:bg-page/80" >
< div className = "font-semibold text-text" > { task . title } < / div >
< div className = "mt-1 text-xs text-muted" > { task . status . replaceAll ( "_" , " " ) } - { task . ownerLabel ? ? "No context" } < / div >
< / button >
) ) }
< / div >
< / div >
) ;
}