Files
CapaKraken/apps/web/src/components/timeline/timelineProjectRows.ts
T

130 lines
3.6 KiB
TypeScript

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<ReturnType<typeof useTimelineContext>["projectGroups"]>[number];
export type OpenDemandRowLayout = {
visibleOpenDemands: TimelineDemandEntry[];
laneMap: Map<string, number>;
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<string, TimelineDemandEntry[]>,
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<string, number> {
const laneMap = new Map<string, number>();
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),
};
}