{task.isMilestone ? (
) : isCritical ? (
) : null}
{task.name || task.identification || task.globalId.slice(0, 12)}
{task.predefinedType && (
{task.predefinedType}
)}
{start && (
<>
Start
{start}
>
)}
{finish && (
<>
Finish
{finish}
>
)}
{duration && (
<>
Duration
{duration}
>
)}
{completion !== undefined && (
<>
Complete
{Math.round(completion)}%
>
)}
{scheduleLabels.length > 0 && (
<>
Schedule
{scheduleLabels.join(', ')}
>
)}
);
}
/**
* Find all tasks whose products include the selected entity.
*
* Federation-aware: prefer `productGlobalIds` whenever we know the entity's
* globalId (and the task carries globalIds of its own) — local expressIds can
* collide across federated models, so matching by globalId first is the safe
* default. Fall back to `productExpressIds` only for schedules that never
* recorded globalIds (legacy / headless extraction paths).
*/
function findControllingTasks(
data: ScheduleExtraction | null,
selectedExpressId: number | null,
selectedGlobalId: string | null | undefined,
): ScheduleTaskInfo[] {
if (!data || data.tasks.length === 0) return [];
if (selectedExpressId === null && !selectedGlobalId) return [];
const out: ScheduleTaskInfo[] = [];
for (const task of data.tasks) {
const taskHasGlobalIds = task.productGlobalIds.some(Boolean);
if (selectedGlobalId && taskHasGlobalIds) {
if (task.productGlobalIds.includes(selectedGlobalId)) out.push(task);
// When globalIds are the authoritative side, do NOT also match on
// expressId — a collision across models would produce a false positive.
continue;
}
if (selectedExpressId !== null && selectedExpressId > 0
&& task.productExpressIds.includes(selectedExpressId)) {
out.push(task);
}
}
return out;
}
function buildScheduleNameLookup(data: ScheduleExtraction | null): Map