import { applyVisualOverrides, type TimelineVisualOverrides, } from "./allocationVisualState.js"; import type { TimelineAssignmentEntry, TimelineDemandEntry, useTimelineContext, } from "./TimelineContext.js"; import { PROJECT_HEADER_HEIGHT, ROW_HEIGHT, SUB_LANE_HEIGHT } from "./timelineConstants.js"; import { getProjectRowMetricsKey } from "./timelineProjectMetrics.js"; export type TimelineProjectGroup = NonNullable["projectGroups"]>[number]; export type OpenDemandRowLayout = { visibleOpenDemands: TimelineDemandEntry[]; laneMap: Map; laneCount: number; rowHeight: number; }; export type ProjectFlatRow = | { type: "header"; key: string; project: TimelineProjectGroup; } | { type: "open-demand"; key: string; projectId: string; openDemandCount: number; layout: OpenDemandRowLayout; } | { type: "resource"; key: string; project: TimelineProjectGroup; resource: TimelineProjectGroup["resourceRows"][number]["resource"]; allocs: TimelineAssignmentEntry[]; metricsKey: string; }; export function estimateProjectRowHeight(row: ProjectFlatRow | undefined) { if (!row) return ROW_HEIGHT; if (row.type === "header") return PROJECT_HEADER_HEIGHT; if (row.type === "open-demand") return row.layout.rowHeight; return ROW_HEIGHT; } export function buildProjectFlatRows( visualProjectGroups: TimelineProjectGroup[], openDemandsByProject: Map, optimisticAllocations: TimelineVisualOverrides, ): ProjectFlatRow[] { const rows: ProjectFlatRow[] = []; for (const project of visualProjectGroups) { rows.push({ type: "header", key: `header-${project.id}`, project }); const openDemands = openDemandsByProject.get(project.id) ?? []; if (openDemands.length > 0) { rows.push({ type: "open-demand", key: `open-demand-${project.id}`, projectId: project.id, openDemandCount: openDemands.length, layout: buildOpenDemandRowLayout(openDemands, optimisticAllocations), }); } for (const { resource, allocs } of project.resourceRows) { rows.push({ type: "resource", key: `${project.id}-${resource.id}`, project, resource, allocs, metricsKey: getProjectRowMetricsKey(project.id, resource.id), }); } } return rows; } function assignDemandLanes(demands: TimelineDemandEntry[]): Map { const laneMap = new Map(); const laneEnds: Date[] = []; const sorted = [...demands].sort( (a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime(), ); for (const demand of sorted) { const start = new Date(demand.startDate); let assigned = -1; for (let i = 0; i < laneEnds.length; i++) { if (laneEnds[i]! < start) { assigned = i; laneEnds[i] = new Date(demand.endDate); break; } } if (assigned === -1) { assigned = laneEnds.length; laneEnds.push(new Date(demand.endDate)); } laneMap.set(demand.id, assigned); } return laneMap; } function buildOpenDemandRowLayout( openDemands: TimelineDemandEntry[], optimisticAllocations: TimelineVisualOverrides, ): OpenDemandRowLayout { const visibleOpenDemands = applyVisualOverrides(openDemands, optimisticAllocations); const laneMap = assignDemandLanes(visibleOpenDemands); const laneCount = laneMap.size > 0 ? Math.max(...laneMap.values()) + 1 : 1; return { visibleOpenDemands, laneMap, laneCount, rowHeight: Math.max(ROW_HEIGHT, laneCount * SUB_LANE_HEIGHT + 16), }; }