80 lines
3.6 KiB
TypeScript
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;
|
|
}
|