import { getCompanyColorPreset } from '../company-color-presets.js'; import { isSafeImageUrl, type OrgChartDataset, type OrganizationTreeNode, type ValidationIssue } from '../data/index.js'; import type { OrgChartTopNode } from './types.js'; export function resolveExpandedCompanies(data: OrgChartDataset, roots: OrganizationTreeNode[], fallback: string) { const companies = new Map(); const issues: ValidationIssue[] = []; const visit = (node: OrganizationTreeNode, inherited?: string) => { const org = node.organization; const leader = data.employees.find(employee => employee.id === org.leaderId); const company = org.companyId ?? inherited ?? (leader && getCompanyColorPreset(leader.affiliationId) ? leader.affiliationId : fallback); if (!getCompanyColorPreset(company)) issues.push({ code: 'COMPANY_PRESET', path: `organizations.${org.id}.companyId`, message: 'Auto company requires a registered color preset.' }); if (inherited && company !== inherited) issues.push({ code: 'COMPANY_HIERARCHY', path: `organizations.${org.id}.companyId`, message: 'A child cannot belong to another company tree.' }); companies.set(org.id, company); node.children.forEach(child => visit(child, company)); }; roots.forEach(root => visit(root)); return { companies, issues, companyIds: [...new Set(roots.map(root => companies.get(root.organization.id)!))] }; } export function validateTopNodes(data: OrgChartDataset, roots: OrganizationTreeNode[], companies: ReadonlyMap, configs: Readonly>): ValidationIssue[] { const issues: ValidationIssue[] = []; for (const [id, config] of Object.entries(configs)) { const path = `companyTopNodes.${id}`; if (!getCompanyColorPreset(id) || !config || typeof config !== 'object' || Array.isArray(config)) { issues.push({ code: 'TOP_NODE_CONFIG', path, message: 'Expected a registered company and top node object.' }); continue; } for (const [key, value] of Object.entries(config)) { const valid = key === 'display' ? ['card', 'titleOnly', 'hidden'].includes(value as string) : ['employeeId', 'profileImage'].includes(key) && value === null ? true : ['employeeId', 'profileImage', 'title', 'name', 'organizationName'].includes(key) && typeof value === 'string' && !!value.trim() && !/[<>\u0000-\u001f]/u.test(value); if (!valid || (key === 'profileImage' && typeof value === 'string' && !isSafeImageUrl(value))) issues.push({ code: 'TOP_NODE_CONFIG', path: `${path}.${key}`, message: 'Invalid top node presentation value.' }); } const companyRoots = roots.filter(root => companies.get(root.organization.id) === id); if (config.employeeId && companyRoots.length && !companyRoots.some(root => data.employees.some(employee => employee.id === config.employeeId && employee.organizationId === root.organization.id))) { issues.push({ code: 'TOP_NODE_REFERENCE', path, message: 'Top employee must belong to a root of this company.' }); } } return issues; }