/** * Client mirror of the backend's contiguous-range rules for priority configs * (see PriorityConfigsService.assertNoRangeCollision): ranges per type — per * currency for CURRENCY — run from 1 with no gaps and no overlaps, so the next * range always starts at the lowest uncovered wagon count. There is no upper * ceiling. The backend re-validates on submit AND on approval; this only * drives the form prefill. */ export type PriorityRuleType = "WAGON" | "CURRENCY" | "CUSTOMS"; export interface PriorityRangeRule { id?: unknown; type?: unknown; currency?: unknown; minWagonCount?: unknown; maxWagonCount?: unknown; } const PRIORITY_RULE_TYPES: PriorityRuleType[] = ["WAGON", "CURRENCY", "CUSTOMS"]; /** * Where the next range for `type` (+`currency`) must start, excluding * `excludeId` (the rule being edited). Null only when `type` is not yet a * known priority rule type — the chain itself is unbounded, so a next start * always exists. */ export function nextPriorityRangeStart( rules: PriorityRangeRule[], type: string, currency: string | null | undefined, excludeId?: string, ): number | null { if (!PRIORITY_RULE_TYPES.includes(type as PriorityRuleType)) return null; const scoped = rules .filter( (r) => String(r.type ?? "") === type && (excludeId === undefined || String(r.id ?? "") !== excludeId) && (type !== "CURRENCY" || String(r.currency ?? "") === String(currency ?? "")), ) .map((r) => ({ min: Number(r.minWagonCount ?? 0), max: Number(r.maxWagonCount ?? 0), })) .sort((a, b) => a.min - b.min); let next = 1; for (const r of scoped) { if (r.min > next) break; // gap before this rule — fill it first next = Math.max(next, r.max + 1); } return next; }