export type OrgChartTenantSelection = { id: string; name: string; }; export type TenantFilterTarget = { id?: string; tenantId?: string; slug?: string; tenantSlug?: string; type?: string; parentId?: string | null; name?: string; tenantName?: string; }; export type HanmacFamilyUserTarget = { companyCode?: string; tenantSlug?: string; tenant?: TenantFilterTarget; joinedTenants?: TenantFilterTarget[]; metadata?: Record; }; type OrgChartPickerMessage = { type?: unknown; payload?: { selections?: Array<{ type?: unknown; id?: unknown; name?: unknown; }>; }; }; type OrgChartTenantPickerOptions = { includeInternal?: boolean; tenantId?: string; }; type OrgChartLoginOptions = { includeInternal?: boolean; returnTo?: string; }; function isSystemTenant(tenant: TenantFilterTarget) { const slug = tenant.slug?.trim().toLowerCase(); const type = tenant.type?.trim().toUpperCase(); return ( !tenant.id?.trim() || !tenant.slug?.trim() || type === "SYSTEM" || slug === "system" || slug === "global" ); } function isInTenantSubtree( tenant: T, rootTenantId: string, tenantById: Map, ) { if (!rootTenantId) { return false; } if (tenant.id === rootTenantId) { return true; } const visitedTenantIds = new Set(); let parentId = tenant.parentId ?? ""; while (parentId) { if (parentId === rootTenantId) { return true; } if (visitedTenantIds.has(parentId)) { return false; } visitedTenantIds.add(parentId); parentId = tenantById.get(parentId)?.parentId ?? ""; } return false; } function resolveHanmacFamilyTenantId( tenants: T[], hanmacFamilyTenantId?: string, ) { const envTenantId = hanmacFamilyTenantId?.trim(); if (envTenantId) return envTenantId; return ( tenants.find((tenant) => tenant.slug?.toLowerCase() === "hanmac-family") ?.id ?? "" ); } export function isHanmacFamilyTenant( tenant: T | undefined, tenants: T[], hanmacFamilyTenantId?: string, ) { if (!tenant || !tenant.id) return false; const rootTenantId = resolveHanmacFamilyTenantId( tenants, hanmacFamilyTenantId, ); if (!rootTenantId) return false; const tenantById = new Map( tenants .filter((item) => item.id?.trim()) .map((item) => [item.id as string, item]), ); const target = tenantById.get(tenant.id) ?? tenant; return isInTenantSubtree(target, rootTenantId, tenantById); } export function isHanmacFamilyUser( user: HanmacFamilyUserTarget, tenants: T[], hanmacFamilyTenantId?: string, ) { const metadataAppointments = Array.isArray( user.metadata?.additionalAppointments, ) ? user.metadata.additionalAppointments .map((appointment) => appointment as TenantFilterTarget) .filter( (appointment) => typeof appointment.tenantId === "string" || typeof appointment.id === "string" || typeof appointment.tenantSlug === "string" || typeof appointment.slug === "string", ) .map((appointment) => ({ id: appointment.id ?? appointment.tenantId, slug: appointment.slug ?? appointment.tenantSlug, parentId: appointment.parentId, type: appointment.type, name: appointment.name ?? appointment.tenantName, })) : []; const tenantBySlug = new Map( tenants .filter((tenant) => tenant.slug?.trim()) .map((tenant) => [tenant.slug?.toLowerCase() as string, tenant]), ); const tenantById = new Map( tenants .filter((tenant) => tenant.id?.trim()) .map((tenant) => [tenant.id as string, tenant]), ); const tenantCandidates = [ user.tenant, ...(user.joinedTenants ?? []), ...metadataAppointments, ...metadataAppointments.map((appointment) => tenantById.get(appointment.id ?? ""), ), tenantBySlug.get(user.tenantSlug?.toLowerCase() ?? ""), ]; return tenantCandidates.some((tenant) => isHanmacFamilyTenant(tenant, tenants, hanmacFamilyTenantId), ); } export function filterNonHanmacFamilyTenants( tenants: T[], hanmacFamilyTenantId?: string, ) { const rootTenantId = resolveHanmacFamilyTenantId( tenants, hanmacFamilyTenantId, ); const tenantById = new Map( tenants .filter((tenant) => tenant.id?.trim()) .map((tenant) => [tenant.id as string, tenant]), ); return tenants.filter( (tenant) => !isSystemTenant(tenant) && !isInTenantSubtree(tenant, rootTenantId, tenantById), ); } export function buildOrgChartTenantPickerUrl( baseUrl?: string, options: OrgChartTenantPickerOptions = {}, ) { const normalizedBase = (baseUrl ?? "").replace(/\/+$/, ""); const params = new URLSearchParams({ mode: "single", select: "tenant", width: "400", height: "600", }); const tenantId = options.tenantId?.trim(); if (tenantId) { params.set("tenantId", tenantId); } if (options.includeInternal) { params.set("includeInternal", "true"); } return `${normalizedBase}/embed/picker?${params.toString()}`; } export function buildAuthenticatedOrgChartTenantPickerUrl( baseUrl?: string, options: OrgChartTenantPickerOptions = {}, ) { const pickerUrl = buildOrgChartTenantPickerUrl("", { includeInternal: true, ...options, }); return buildAuthenticatedOrgChartUrl(baseUrl, { returnTo: pickerUrl }); } export function buildAuthenticatedOrgChartUrl( baseUrl?: string, options: OrgChartLoginOptions = { includeInternal: true }, ) { const normalizedBase = (baseUrl ?? "").replace(/\/+$/, ""); let returnTo = options.returnTo?.trim() || "/chart"; if (options.includeInternal && returnTo.startsWith("/chart")) { const [path, query = ""] = returnTo.split("?", 2); const params = new URLSearchParams(query); params.set("includeInternal", "true"); returnTo = `${path}?${params.toString()}`; } const params = new URLSearchParams({ auto: "1", returnTo, }); return `${normalizedBase}/login?${params.toString()}`; } export function parseOrgChartTenantSelection( message: unknown, ): OrgChartTenantSelection | null { const data = message as OrgChartPickerMessage; if (data?.type !== "orgfront:picker:confirm") { return null; } const selection = data.payload?.selections?.[0]; if ( selection?.type !== "tenant" || typeof selection.id !== "string" || typeof selection.name !== "string" || selection.id.trim() === "" ) { return null; } return { id: selection.id, name: selection.name, }; }