import type { DecisionApproachDetailEntry } from "../../app/(app)/create/types"; import decisionApproachesMessages from "../../messages/en/create/customRule/decisionApproaches.json"; export type DecisionApproachKeyResourceItem = { id: string; label: string; }; function uniquePreserveOrder(values: readonly string[]): string[] { const seen = new Set(); const out: string[] = []; for (const value of values) { if (seen.has(value)) continue; seen.add(value); out.push(value); } return out; } function isKeyResourceItem(value: unknown): value is DecisionApproachKeyResourceItem { if (!value || typeof value !== "object") return false; const row = value as { id?: unknown; label?: unknown }; return typeof row.id === "string" && typeof row.label === "string"; } /** * Sidebar “key resource” checkboxes on the decision-approaches step. * These labels are also offered as Applicable Scope chips on every approach. */ export function decisionApproachKeyResourceItems(): DecisionApproachKeyResourceItem[] { const items = ( decisionApproachesMessages as { messageBox?: { items?: unknown }; } ).messageBox?.items; if (!Array.isArray(items)) return []; return items.filter(isKeyResourceItem); } export function decisionApproachKeyResourceLabels(): string[] { return decisionApproachKeyResourceItems().map((item) => item.label); } export function selectedKeyResourceLabelsFromCheckedIds( checkedIds: readonly string[], ): string[] { const idSet = new Set(checkedIds); return decisionApproachKeyResourceItems() .filter((item) => idSet.has(item.id)) .map((item) => item.label); } export function decisionApproachKeyResourceIdsFromLabels( labels: readonly string[], ): string[] { const labelSet = new Set(labels); return decisionApproachKeyResourceItems() .filter((item) => labelSet.has(item.label)) .map((item) => item.id); } /** * Sidebar checkboxes follow the open approach while its modal is up. * With no modal they show key resources assigned to any selected approach, * plus extra reminder checks that are not painted onto chips. */ export function decisionApproachKeyResourceCheckboxIds(params: { detailsById: Record | undefined; selectedApproachIds: readonly string[]; reminderIds?: readonly string[]; openDraft?: DecisionApproachDetailEntry | null; }): string[] { if (params.openDraft) { return decisionApproachKeyResourceIdsFromLabels( params.openDraft.selectedApplicableScope, ); } const assignedLabels: string[] = []; for (const id of params.selectedApproachIds) { const entry = params.detailsById?.[id]; if (!entry) continue; assignedLabels.push(...entry.selectedApplicableScope); } return uniquePreserveOrder([ ...decisionApproachKeyResourceIdsFromLabels(assignedLabels), ...(params.reminderIds ?? []), ]); } export function stringArraysEqual( a: readonly string[], b: readonly string[], ): boolean { if (a.length !== b.length) return false; return a.every((value, index) => value === b[index]); } /** * Offer the four sidebar labels as Applicable Scope chips without persisting * them onto `applicableScope` (unchecked keys must not publish as defaults). */ export function withDecisionApproachKeyResourceScopes( scopes: readonly string[], ): string[] { return uniquePreserveOrder([ ...scopes, ...decisionApproachKeyResourceLabels(), ]); } /** * Keep non-key chip selections, then select the key-resource labels whose * sidebar boxes are checked. */ export function applyDecisionApproachKeyResources( entry: DecisionApproachDetailEntry, selectedKeyResourceLabels: readonly string[], ): DecisionApproachDetailEntry { const keyLabels = decisionApproachKeyResourceLabels(); const keySet = new Set(keyLabels); const selectedSet = new Set(selectedKeyResourceLabels); return { ...entry, selectedApplicableScope: uniquePreserveOrder([ ...entry.selectedApplicableScope.filter((scope) => !keySet.has(scope)), ...keyLabels.filter((label) => selectedSet.has(label)), ]), }; } export function withoutDecisionApproachKeyResourceLabels( scopes: readonly string[], ): string[] { const keySet = new Set(decisionApproachKeyResourceLabels()); return scopes.filter((scope) => !keySet.has(scope)); } /** * Publish selected original scopes (or all originals when none are picked) * plus any checked key-resource labels. Checking a key resource must not drop * unselected default scopes. */ export function decisionApproachScopeForPublish( applicableScope: readonly unknown[], selectedApplicableScope: readonly unknown[], ): string[] { const asStrings = (values: readonly unknown[]): string[] => values.filter((value): value is string => typeof value === "string"); const keySet = new Set(decisionApproachKeyResourceLabels()); const original = withoutDecisionApproachKeyResourceLabels( asStrings(applicableScope), ); const selected = asStrings(selectedApplicableScope); const selectedOther = withoutDecisionApproachKeyResourceLabels(selected); const selectedKeys = selected.filter((scope) => keySet.has(scope)); return uniquePreserveOrder([ ...(selectedOther.length > 0 ? selectedOther : original), ...selectedKeys, ]); } export function syncDecisionApproachKeyResourceDetails( detailsById: Record | undefined, extraIds: readonly string[], selectedKeyResourceLabels: readonly string[], seed: (_id: string) => DecisionApproachDetailEntry, ): Record { const next: Record = {}; const ids = new Set([...Object.keys(detailsById ?? {}), ...extraIds]); for (const id of ids) { next[id] = applyDecisionApproachKeyResources( detailsById?.[id] ?? seed(id), selectedKeyResourceLabels, ); } return next; }