Files
HMDesign/packages/org-chart/src/data/content-selectors.ts
T

80 lines
3.6 KiB
TypeScript

import { normalizeOrgChartDataset } from './normalize.js';
import type { Employee, OrgChartDataset, OrgChartSummary, Organization, WorkStatus } from './types.js';
/** Internal content selectors: these do not expand the package's public data API. */
function uniqueEmployees(data: OrgChartDataset): Employee[] {
const seen = new Set<string>();
return data.employees.filter(employee => {
if (seen.has(employee.id)) return false;
seen.add(employee.id);
return true;
});
}
/** Literal unique IDs in their primary organization, including leaders and all statuses.
* Inclusive organization counts stay in Summary; they must never be summed as a subtotal.
* Map keys safely retain opaque organization IDs and position names such as __proto__.
*/
export function selectOrganizationPositionCounts(data: OrgChartDataset): ReadonlyMap<string, ReadonlyMap<string, number>> {
const counts = new Map<string, Map<string, number>>();
for (const organization of data.organizations) counts.set(organization.id, new Map());
for (const employee of uniqueEmployees(data)) {
const row = counts.get(employee.organizationId);
if (row) row.set(employee.position, (row.get(employee.position) ?? 0) + 1);
}
return counts;
}
export function selectAffiliationSummary(data: OrgChartDataset, summary: OrgChartSummary) {
// Include all declared affiliations, including zero, without recounting employees.
return data.affiliations.map(affiliation => ({
affiliation,
count: summary.raw.byAffiliation[affiliation.id] ?? 0,
}));
}
export interface MajorOrganizationTotal {
organization: Organization;
/** 'direct' for a root's own members (e.g. its leader); 'inclusive' for a major organization. */
scope: 'direct' | 'inclusive';
count: number;
}
/** Compact totals for the read-only composition: each root's direct members, then
* each major (root child) organization including its cells/teams. Primary
* membership is single, so the rows are disjoint and add up to the raw total. */
export function selectMajorOrganizationTotals(data: OrgChartDataset, summary: OrgChartSummary): MajorOrganizationTotal[] {
const rows: MajorOrganizationTotal[] = [];
for (const root of normalizeOrgChartDataset(data).roots) {
const counts = summary.raw.byOrganization[root.organization.id];
if (!root.children.length) {
rows.push({ organization: root.organization, scope: 'inclusive', count: counts?.inclusive ?? 0 });
continue;
}
if (counts?.direct) rows.push({ organization: root.organization, scope: 'direct', count: counts.direct });
for (const child of root.children) {
rows.push({ organization: child.organization, scope: 'inclusive', count: summary.raw.byOrganization[child.organization.id]?.inclusive ?? 0 });
}
}
return rows;
}
export interface StatusContentGroup {
status: Extract<WorkStatus, 'dispatched' | 'onLeave'>;
employees: readonly Employee[];
count: number;
}
export function selectStatusGroups(data: OrgChartDataset, summary: OrgChartSummary): StatusContentGroup[] {
const employees = uniqueEmployees(data);
const groups: StatusContentGroup[] = [];
if (data.presentation?.groups.showDispatched !== false) {
groups.push({ status: 'dispatched', employees: employees.filter(employee => employee.workStatus === 'dispatched'), count: summary.raw.byStatus.dispatched });
}
if (data.presentation?.groups.showOnLeave !== false) {
groups.push({ status: 'onLeave', employees: employees.filter(employee => employee.workStatus === 'onLeave'), count: summary.raw.byStatus.onLeave });
}
// Cards retain original Employee references. A second display never changes Summary.
return groups;
}