// The packet editor's rules, kept pure so they can be tested without the page. The server re-checks every one. import type { Questionnaire } from 'fhir/r4'; import { MAX_PACKET_MEMBERS, MAX_PACKET_TITLE, type PacketInput, type PacketTrigger, type QuestionnairePacket, } from '../services/QuestionnairePacketService'; export const emptyPacketDraft = (): PacketInput => ({ title: '', status: 'draft', members: [], trigger: 'off', priority: 'routine', }); export const draftOfPacket = (packet: QuestionnairePacket): PacketInput => ({ title: packet.title, status: packet.status, members: packet.members.map((member) => member.questionnaireId), trigger: packet.trigger, priority: packet.priority, }); /** Moves one member by `delta` places; an index or target off either end leaves the order unchanged. */ export const moveMember = (members: string[], index: number, delta: number): string[] => { const target = index + delta; if (index < 0 || index >= members.length || target < 0 || target >= members.length) return members; const next = [...members]; [next[index], next[target]] = [next[target], next[index]]; return next; }; /** Questionnaires that can still be added: in the clinical library, not retired, not already a member. */ export const addableQuestionnaires = (library: Questionnaire[], members: string[]): Questionnaire[] => library.filter((questionnaire) => questionnaire.id && (questionnaire.status === 'active' || questionnaire.status === 'draft') && !members.includes(questionnaire.id)); export type MemberStanding = 'active' | 'draft' | 'retired' | 'missing'; /** A member absent from the clinical library is missing to this author, never assumed fine. */ export const memberStanding = (library: Questionnaire[], questionnaireId: string): MemberStanding => { const status = library.find((questionnaire) => questionnaire.id === questionnaireId)?.status; if (status === 'active' || status === 'draft') return status; return status ? 'retired' : 'missing'; }; /** The first thing stopping a save, in the author's words, or undefined when the draft can be sent. */ export const packetDraftProblem = (draft: PacketInput, library: Questionnaire[]): string | undefined => { const title = draft.title.trim(); if (!title) return 'Name the packet.'; if (title.length > MAX_PACKET_TITLE) return `Keep the name to ${MAX_PACKET_TITLE} characters.`; if (draft.members.length === 0) return 'Add at least one questionnaire.'; if (draft.members.length > MAX_PACKET_MEMBERS) return `A packet holds up to ${MAX_PACKET_MEMBERS} questionnaires.`; const standings = draft.members.map((id) => memberStanding(library, id)); if (standings.includes('missing')) return 'Remove the questionnaire that is no longer in your library.'; if (standings.includes('retired')) return 'Remove the retired questionnaire.'; if (draft.status === 'active' && standings.includes('draft')) { return 'An active packet can hold only active questionnaires. Activate the draft or keep the packet as a draft.'; } return undefined; }; /** Whether the draft differs from what was opened, by the fields the server stores. */ export const packetDraftChanged = (draft: PacketInput, opened: PacketInput): boolean => draft.title.trim() !== opened.title.trim() || draft.status !== opened.status || draft.trigger !== opened.trigger || (draft.trigger !== 'off' && draft.priority !== opened.priority) || draft.members.join('\n') !== opened.members.join('\n'); export const triggerSummary = (trigger: PacketTrigger): string => { switch (trigger) { case 'all-appointments': return 'Assigned automatically for every appointment'; case 'new-patient-appointments': return 'Assigned automatically for new patient appointments'; default: return 'Not assigned automatically'; } };