import type { AllocationWithDetails, ColumnDef } from "@nexus/shared"; import type { CollapsedAllocationGroups } from "./allocationGroupState.js"; import { formatDate } from "~/lib/format.js"; import { AllocationRow } from "./AllocationRow.js"; type ProjectSubGroup = { projectId: string; projectName: string; projectCode: string; allocations: AllocationWithDetails[]; typicalHoursPerDay: number; earliestStart: Date; latestEnd: Date; }; export type AllocGroup = { resourceId: string; resourceName: string; eid: string; chapter: string | null; allocations: AllocationWithDetails[]; projectSubGroups: ProjectSubGroup[]; }; /** Fragment wrapper for grouped rows — avoids unnecessary DOM nodes */ function GroupRows({ children }: { children: React.ReactNode }) { return <>{children}; } type AllocationGroupedBodyProps = { groups: AllocGroup[]; collapsedGroups: CollapsedAllocationGroups; expandedSubGroups: Set; visibleColumns: ColumnDef[]; selectedIds: Set; isAllSelected: (ids: string[]) => boolean; isIndeterminate?: (ids: string[]) => boolean; onToggleSelection: (id: string) => void; onToggleAllSelection: (ids: string[]) => void; onToggleGroup: (resourceId: string) => void; onToggleSubGroup: (resourceId: string, projectId: string) => void; onEdit: (alloc: AllocationWithDetails) => void; onRequestDelete: (alloc: AllocationWithDetails) => void; deleteDisabled: boolean; formatPeriod: (alloc: AllocationWithDetails) => string; }; export function AllocationGroupedBody({ groups, collapsedGroups, expandedSubGroups, visibleColumns, selectedIds, isAllSelected, onToggleSelection, onToggleAllSelection, onToggleGroup, onToggleSubGroup, onEdit, onRequestDelete, deleteDisabled, formatPeriod, }: AllocationGroupedBodyProps) { return ( <> {groups.map((group) => { const isCollapsed = collapsedGroups === "all" || collapsedGroups.has(group.resourceId); const groupAllocIds = group.allocations.map((a) => a.id); const allGroupSelected = isAllSelected(groupAllocIds); const groupIndeterminate = !allGroupSelected && groupAllocIds.some((id) => selectedIds.has(id)); return ( {/* Group header */} onToggleGroup(group.resourceId)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onToggleGroup(group.resourceId); } }} tabIndex={0} role="button" aria-expanded={!isCollapsed} > e.stopPropagation()}> { if (el) el.indeterminate = groupIndeterminate; }} onChange={() => onToggleAllSelection(groupAllocIds)} className="rounded border-gray-300 dark:border-gray-600" />
{isCollapsed ? "▸" : "▾"} {group.resourceName} {group.eid && ( {group.eid} )} {group.chapter && ( · {group.chapter} )} {group.allocations.length}
{/* Project sub-groups within person */} {!isCollapsed && group.projectSubGroups.map((subGroup) => { const subKey = `${group.resourceId}::${subGroup.projectId}`; const isSubExpanded = expandedSubGroups.has(subKey); // Single allocation for this project — render directly, no sub-group header if (subGroup.allocations.length === 1) { const alloc = subGroup.allocations[0]!; return ( onToggleSelection(alloc.id)} onEdit={() => onEdit(alloc)} onRequestDelete={() => onRequestDelete(alloc)} deleteDisabled={deleteDisabled} formatPeriod={formatPeriod} /> ); } // Multiple allocations — show collapsible project sub-group const subAllocIds = subGroup.allocations.map((a) => a.id); const allSubSelected = isAllSelected(subAllocIds); const subIndeterminate = !allSubSelected && subAllocIds.some((id) => selectedIds.has(id)); return ( onToggleSubGroup(group.resourceId, subGroup.projectId)} tabIndex={0} role="button" aria-expanded={isSubExpanded} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onToggleSubGroup(group.resourceId, subGroup.projectId); } }} > e.stopPropagation()}> { if (el) el.indeterminate = subIndeterminate; }} onChange={() => onToggleAllSelection(subAllocIds)} className="rounded border-gray-300 dark:border-gray-600" />
{isSubExpanded ? "▾" : "▸"} {subGroup.projectCode} {subGroup.projectName} {formatDate(subGroup.earliestStart)} → {formatDate(subGroup.latestEnd)} {subGroup.typicalHoursPerDay}h/day {subGroup.allocations.length}
{isSubExpanded && subGroup.allocations.map((alloc, idx) => ( onToggleSelection(alloc.id)} onEdit={() => onEdit(alloc)} onRequestDelete={() => onRequestDelete(alloc)} deleteDisabled={deleteDisabled} formatPeriod={formatPeriod} /> ))}
); })}
); })} ); }