130 lines
3.6 KiB
TypeScript
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),
|
|
};
|
|
}
|