import { v4 as uuidv4 } from 'uuid'; // Answer-option coding for questionnaire choice / open-choice questions. // // Rudy's original model (a6deac4c, 2025-04-24) is LOINC-first: a choice question is imported from the // LOINC library and LoincSearch.tsx (convertLoincQToItem) populates answerOption.value.{code,system, // display} directly -- which is WHY those fields are read-only in the builder. That model is correct // and stays. // // What it never supported is a MANUALLY authored dropdown ("Billing question" / "New patient"), which // has no code system at all -- while QuestionnaireService serializes choice options as valueCoding and // DROPS any option lacking a code (QuestionnaireService.tsx:526). So a manual option needs a code, and // this module derives one under a local Akashic CodeSystem. // // A local CodeSystem for an org-specific answer list is legitimate, portable FHIR -- a receiving system // sees a coded answer with an unambiguous system URI. It is NOT a SNOMED/LOINC-binding violation: that // convention governs *clinical* concepts, and "Billing question" has no SNOMED equivalent. // // See docs/_CLAUDE/2026-07-16-questionnaire-choice-answer-options-design.md. export const AKASHIC_ANSWER_CODE_SYSTEM = 'https://akashichealthrecords.com/fhir/CodeSystem/questionnaire-answer'; export const LOINC_SYSTEM = 'http://loinc.org'; /** * LOINC owns its answer lists. An imported option's label/code/system stay read-only in the builder so * a hand edit cannot corrupt standards-bound data. */ export const isLoincOwnedOption = (system?: string | null): boolean => system === LOINC_SYSTEM; /** lowercase; runs of non-alphanumerics collapse to a single '-'; leading/trailing '-' trimmed. */ export const slugifyAnswerCode = (value: string): string => (value ?? '') .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); /** * Derive a STABLE code for a manually authored option. * * Stability is the entire point. The caller MUST only invoke this when the option's code is empty, so * relabelling can never change an existing code -- a changed code orphans every QuestionnaireResponse * that referenced the old one. See the design doc's Risks table. * * `takenCodes` are the codes already used by the OTHER options in the SAME question, so two options * labelled alike get `billing-question` and `billing-question-2` rather than a duplicate. * * A label that slugs to nothing (all punctuation/emoji) falls back to a uuid: opaque, but stable and * unique, which is what matters. */ export const deriveAnswerOptionCode = (display: string, takenCodes: string[]): string => { const base = slugifyAnswerCode(display); if (base === '') { return uuidv4(); } if (!takenCodes.includes(base)) { return base; } let suffix = 2; while (takenCodes.includes(`${base}-${suffix}`)) { suffix += 1; } return `${base}-${suffix}`; };