import { normalizeOrgChartDataset } from './normalize.js'; import type { ApprovedAggregationPolicy, Employee, OrgChartDataset, OrgChartSummary, SummaryCounts, WorkStatus } from './types.js'; const statuses: WorkStatus[] = ['active', 'dispatched', 'onLeave']; function supportedPolicy(policy: ApprovedAggregationPolicy): boolean { const validStatuses = (values: unknown): values is WorkStatus[] => Array.isArray(values) && values.every(value => statuses.includes(value)) && new Set(values).size === values.length; return !!policy && typeof policy.policyVersion === 'string' && !!policy.policyVersion.trim() && !/[\u0000-\u001f<>]/.test(policy.policyVersion) && validStatuses(policy.includedStatuses) && validStatuses(policy.activeStatuses) && policy.activeStatuses.every(status => policy.includedStatuses.includes(status)) && policy.includeLeaders === true && policy.membershipMode === 'primaryOnly'; } function count(data: OrgChartDataset, employees: readonly Employee[]): SummaryCounts { const counts: SummaryCounts = { total: 0, byOrganization: Object.create(null) as SummaryCounts['byOrganization'], byPosition: Object.create(null) as SummaryCounts['byPosition'], byAffiliation: Object.create(null) as SummaryCounts['byAffiliation'], byStatus: { active: 0, dispatched: 0, onLeave: 0 }, }; for (const org of data.organizations) counts.byOrganization[org.id] = { direct: 0, inclusive: 0 }; for (const affiliation of data.affiliations) counts.byAffiliation[affiliation.id] = 0; const seen = new Set(); for (const employee of employees) { if (seen.has(employee.id)) continue; seen.add(employee.id); counts.total += 1; counts.byOrganization[employee.organizationId]!.direct += 1; counts.byPosition[employee.position] = (counts.byPosition[employee.position] ?? 0) + 1; counts.byAffiliation[employee.affiliationId] = (counts.byAffiliation[employee.affiliationId] ?? 0) + 1; counts.byStatus[employee.workStatus] += 1; } // Single primary membership makes child totals disjoint; compute a postorder iteratively. const stack = [...normalizeOrgChartDataset(data).roots]; const traversal = []; while (stack.length) { const node = stack.pop()!; traversal.push(node); stack.push(...node.children); } for (let index = traversal.length - 1; index >= 0; index--) { const node = traversal[index]!; counts.byOrganization[node.organization.id]!.inclusive = counts.byOrganization[node.organization.id]!.direct + node.children.reduce((sum, child) => sum + counts.byOrganization[child.organization.id]!.inclusive, 0); } return counts; } /** Raw counts are literal unique employee IDs, independent of presentation or business totals. */ export function calculateSummary(data: OrgChartDataset, policy?: ApprovedAggregationPolicy): OrgChartSummary { const raw = count(data, data.employees); if (!policy) return { raw, aggregation: { status: 'unavailable', reason: 'policyRequired' } }; if (!supportedPolicy(policy)) return { raw, aggregation: { status: 'unavailable', reason: 'unsupportedPolicy' } }; const selected = data.employees.filter(employee => policy.includedStatuses.includes(employee.workStatus)); return { raw, aggregation: { status: 'ready', policyVersion: policy.policyVersion, counts: count(data, selected) } }; }