57 lines
3.3 KiB
TypeScript
57 lines
3.3 KiB
TypeScript
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<string>();
|
|
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) } };
|
|
}
|