import React from 'react'; import { Timing, TimingRepeat } from 'fhir/r4'; import { formatDate } from './dateUtils'; interface FhirTimingDisplayProps { timing?: Timing | Timing[] | null; className?: string; multiple?: boolean; } const PERIOD_UNIT_LABELS: Record = { s: 'second', min: 'minute', h: 'hour', d: 'day', wk: 'week', mo: 'month', a: 'year', }; const DAY_OF_WEEK_LABELS: Record = { mon: 'Mon', tue: 'Tue', wed: 'Wed', thu: 'Thu', fri: 'Fri', sat: 'Sat', sun: 'Sun', }; const pluralize = (unit: string, count: number): string => (count === 1 ? unit : `${unit}s`); const summarizeFrequency = (repeat: TimingRepeat): string => { const { frequency, frequencyMax, period, periodUnit } = repeat; if (!frequency || !period || !periodUnit) { return ''; } const unitLabel = PERIOD_UNIT_LABELS[periodUnit] || periodUnit; const freqText = frequencyMax && frequencyMax !== frequency ? `${frequency}-${frequencyMax} times` : `${frequency} ${pluralize('time', frequency)}`; const periodText = period === 1 ? unitLabel : `${period} ${pluralize(unitLabel, period)}`; return `${freqText} per ${periodText}`; }; const summarizeRepeat = (repeat: TimingRepeat | undefined): string => { if (!repeat) { return ''; } const parts: string[] = []; const frequencyText = summarizeFrequency(repeat); if (frequencyText) { parts.push(frequencyText); } if (repeat.dayOfWeek && repeat.dayOfWeek.length > 0) { const days = repeat.dayOfWeek.map(day => DAY_OF_WEEK_LABELS[day] || day).join(', '); parts.push(`on ${days}`); } if (repeat.timeOfDay && repeat.timeOfDay.length > 0) { parts.push(`at ${repeat.timeOfDay.join(', ')}`); } const boundsPeriod = repeat.boundsPeriod; if (boundsPeriod && (boundsPeriod.start || boundsPeriod.end)) { const start = boundsPeriod.start ? formatDate(boundsPeriod.start) : ''; const end = boundsPeriod.end ? formatDate(boundsPeriod.end) : ''; if (start && end) { parts.push(`from ${start} to ${end}`); } else if (start) { parts.push(`from ${start}`); } else if (end) { parts.push(`until ${end}`); } } return parts.join(', '); }; /** What summarizeRepeat leaves out and a schedule can still say: how many times, when in the day, how long. */ const repeatDetail = (repeat: TimingRepeat | undefined): string[] => { if (!repeat) return []; const parts: string[] = []; if (repeat.count !== undefined) { parts.push(repeat.countMax !== undefined && repeat.countMax !== repeat.count ? `${repeat.count}-${repeat.countMax} times in all` : `${repeat.count} ${pluralize('time', repeat.count)} in all`); } else if (repeat.countMax !== undefined) { parts.push(`up to ${repeat.countMax} times in all`); } if (repeat.duration !== undefined) { const span = repeat.durationMax !== undefined && repeat.durationMax !== repeat.duration ? `${repeat.duration}-${repeat.durationMax}` : `${repeat.duration}`; const unit = PERIOD_UNIT_LABELS[repeat.durationUnit ?? '']; parts.push(`each lasting ${span} ${unit ? pluralize(unit, repeat.durationMax ?? repeat.duration) : repeat.durationUnit ?? ''}`.trim()); } if (repeat.periodMax !== undefined) { const unit = PERIOD_UNIT_LABELS[repeat.periodUnit ?? '']; parts.push(`at most every ${repeat.periodMax} ${unit ? pluralize(unit, repeat.periodMax) : repeat.periodUnit ?? ''}`.trim()); } if (repeat.when?.length) parts.push(`when: ${repeat.when.join(', ')}`); if (repeat.offset !== undefined) parts.push(`${repeat.offset} min offset`); if (repeat.boundsDuration?.value !== undefined) parts.push(`for ${repeat.boundsDuration.value} ${repeat.boundsDuration.unit ?? repeat.boundsDuration.code ?? ''}`.trim()); if (repeat.boundsRange) { const low = repeat.boundsRange.low?.value; const high = repeat.boundsRange.high?.value; parts.push(`for ${low ?? '?'}-${high ?? '?'} ${repeat.boundsRange.high?.unit ?? repeat.boundsRange.low?.unit ?? ''}`.trim()); } return parts; }; /** * A schedule's name, its repeat and its events together: everything a review has to show of it. Events are given * as recorded, time and zone included, so two times on the same day never read alike. */ export const describeFhirTimingFully = (timing: Timing | undefined | null): string => { if (!timing) return ''; const code = timing.code; const codeText = code?.text || code?.coding?.[0]?.display || code?.coding?.[0]?.code; const events = timing.event?.length ? `on ${timing.event.join(', ')}` : ''; return [codeText, summarizeRepeat(timing.repeat), ...repeatDetail(timing.repeat), events].filter(Boolean).join(' ยท '); }; export const formatFhirTiming = (timing: Timing | undefined | null): string => { try { if (!timing) { return ''; } const code = timing.code; const codeText = code?.text || code?.coding?.[0]?.display || code?.coding?.[0]?.code; if (codeText) { return codeText; } return summarizeRepeat(timing.repeat); } catch (error) { console.error('Error formatting FHIR Timing:', error); return ''; } }; export const FhirTimingDisplay: React.FC = ({ timing, className, multiple = false }) => { try { if (!timing || (Array.isArray(timing) && timing.length === 0)) { return null; } if (multiple && Array.isArray(timing)) { const summaries = timing .map(item => formatFhirTiming(item)) .filter(text => text); if (summaries.length === 0) { return null; } return (
{summaries.map((text, index) => (
{text}
))}
); } const singleTiming = Array.isArray(timing) ? timing[0] : timing; const formatted = formatFhirTiming(singleTiming); if (!formatted) { return null; } return {formatted}; } catch (error) { console.error('Error rendering FhirTimingDisplay:', error); return null; } };