import React, { useEffect, useState } from 'react'; import { useFieldArray, useFormContext } from 'react-hook-form'; import type { FhirResource } from 'fhir/r4'; import clsx from 'clsx'; import { Plus, Trash2 } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; import { Button } from '../ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; import { IValueSet, useAllergyIntoleranceServiceContext, useAuthServiceContext, useConditionServiceContext, useObservationServiceContext, useQuestionnaireResponseServiceContext, useValueSetServiceContext, } from '../../services'; import { useFhirAllergyIntolerances, useFhirConditions, useFhirFamilyMemberHistories, useFhirFamilyMemberHistoryBroadcast, useFhirObservations, useFhirQuestionnaireResponses, } from '../../hooks'; import { AgeFields, CheckField, Choice, CodeSearch, CodedChoice, NotesField, PeriodFields, RangeFields, ReferencesField, TextField, } from './familyHistoryFields'; import './FhirFamilyMemberHistory.css'; interface FhirFamilyMemberHistoryProps { elevated?: boolean; disabled?: boolean; ignoreBroadcast?: boolean; /** Off inside a modal, so a linked record never opens a window over a window. */ enableInlineActions?: boolean; } // Patient words for FHIR codes whose value-set displays are jargon (08-26 ruling). const STATUS_WORDS: Record = { completed: 'All of it', partial: 'Some of it', 'health-unknown': 'None of it', 'entered-in-error': 'Added by mistake', }; const ABSENT_REASON_WORDS: Record = { 'subject-unknown': 'Who they are is not known', withheld: 'Kept private', 'unable-to-obtain': 'Could not find out', deferred: 'To be added later', }; const COMMON_RELATIVES = [ 'MTH', 'FTH', 'SIS', 'BRO', 'DAU', 'SON', 'MGRMTH', 'MGRFTH', 'PGRMTH', 'PGRFTH', 'MAUNT', 'PAUNT', 'MUNCLE', 'PUNCLE', 'COUSN', 'HSIS', 'HBRO', ]; const BIRTH_CHOICES = [ { value: 'bornDate', label: 'Date of birth' }, { value: 'bornPeriod', label: 'Roughly when they were born' }, { value: 'ageAge', label: 'Their age' }, { value: 'ageRange', label: 'An age range' }, ]; const DEATH_CHOICES = [ { value: 'age', label: 'At an age' }, { value: 'range', label: 'Within an age range' }, { value: 'date', label: 'On a date' }, ]; const ONSET_CHOICES = [ { value: 'age', label: 'At an age' }, { value: 'range', label: 'Within an age range' }, { value: 'period', label: 'Between dates' }, ]; const REFERENCE_KINDS = [ { key: 'Condition', label: 'A condition' }, { key: 'Observation', label: 'A test result or reading' }, { key: 'AllergyIntolerance', label: 'An allergy' }, { key: 'QuestionnaireResponse', label: 'A form you filled in' }, ]; export const FhirFamilyMemberHistory: React.FC = ({ elevated = true, disabled = false, ignoreBroadcast = false, enableInlineActions = true }) => { const { watch, control, setValue, getValues } = useFormContext(); const { userRole, USER_ROLES } = useAuthServiceContext(); // Staff only: the patient's approved form has no view/create beside a link. const staff = !!userRole && ![USER_ROLES.PATIENT, USER_ROLES.RELATED_PERSON].includes(userRole); const { fetchValueSet, valueSetUrls } = useValueSetServiceContext(); const { getConditionDisplay } = useConditionServiceContext(); const { getObservationDisplay } = useObservationServiceContext(); const { getAllergyIntoleranceDisplay } = useAllergyIntoleranceServiceContext(); const { getQuestionnaireResponseDisplay } = useQuestionnaireResponseServiceContext(); const [historyStatus, setHistoryStatus] = useState([]); const [historyAbsentReason, setHistoryAbsentReason] = useState([]); const [familyMember, setFamilyMember] = useState([]); const [administrativeGender, setAdministrativeGender] = useState([]); useFhirFamilyMemberHistories(); useFhirFamilyMemberHistoryBroadcast({ ignore: ignoreBroadcast }); const { conditionsList } = useFhirConditions(); const { observationsList } = useFhirObservations(); const { allergyIntolerancesList } = useFhirAllergyIntolerances(); const { questionnaireResponsesList } = useFhirQuestionnaireResponses(); const conditionFieldArray = useFieldArray({ control, name: 'condition' }); const status = watch('status'); const bornType = watch('bornType'); const ageType = watch('ageType'); const deceasedBoolean = watch('deceasedBoolean'); const deceasedType = watch('deceasedType'); const recorded = watch('date'); const birthMode = bornType === 'date' ? 'bornDate' : bornType === 'period' ? 'bornPeriod' : ageType === 'age' ? 'ageAge' : ageType === 'range' ? 'ageRange' : ''; useEffect(() => { if (ageType === 'age' && !getValues('ageAge.unit')) { setValue('ageAge.unit', 'years'); } }, [ageType, getValues, setValue]); useEffect(() => { const load = async () => { try { const [statuses, absentReasons, relatives, genders] = await Promise.all([ fetchValueSet(valueSetUrls.historyStatus), fetchValueSet(valueSetUrls.historyAbsentReason), fetchValueSet(valueSetUrls.familyMember), fetchValueSet(valueSetUrls.administrativeGender), ]); setHistoryStatus(statuses); setHistoryAbsentReason(absentReasons); setFamilyMember(relatives); setAdministrativeGender(genders); } catch (err) { console.error('Error loading value sets:', err); } }; load(); }, [fetchValueSet, valueSetUrls]); const chooseBirthMode = (mode: string) => { setValue('bornType', mode === 'bornDate' ? 'date' : mode === 'bornPeriod' ? 'period' : '', { shouldDirty: true }); setValue('ageType', mode === 'ageAge' ? 'age' : mode === 'ageRange' ? 'range' : '', { shouldDirty: true }); }; const addCondition = () => conditionFieldArray.append({ code: null, outcome: null, onsetType: '', onsetRange: { low: { value: 0 }, high: { value: 100 } }, note: [], }); const listFor = (kind: string): FhirResource[] => { switch (kind) { case 'Condition': return conditionsList; case 'Observation': return observationsList; case 'AllergyIntolerance': return allergyIntolerancesList; case 'QuestionnaireResponse': return questionnaireResponsesList; default: return []; } }; const describe = (resource: FhirResource): string => { switch (resource.resourceType) { case 'Condition': return getConditionDisplay(resource); case 'Observation': return getObservationDisplay(resource); case 'AllergyIntolerance': return getAllergyIntoleranceDisplay(resource); case 'QuestionnaireResponse': return getQuestionnaireResponseDisplay(resource); default: return resource.id ?? resource.resourceType; } }; return ( Details {status && status !== 'completed' && ( )} {birthMode === 'bornDate' && ( )} {birthMode === 'bornPeriod' && ( )} {birthMode === 'ageAge' && ( )} {birthMode === 'ageRange' && ( )} {ageType && ( )} {deceasedBoolean && ( <> setValue('deceasedType', value, { shouldDirty: true })} groups={[{ options: DEATH_CHOICES }]} disabled={disabled}/> {deceasedType === 'age' && } {deceasedType === 'range' && } {deceasedType === 'date' && } )} {recorded && (

Last updated {new Date(recorded).toLocaleString()}

)} Conditions Why this is on file Notes

Their conditions

{conditionFieldArray.fields.length === 0 && (

No conditions added yet.

)} {conditionFieldArray.fields.map((condition, index) => { const onsetType = watch(`condition.${index}.onsetType`); return (

Condition {index + 1}

setValue(`condition.${index}.onsetType`, value, { shouldDirty: true })} groups={[{ options: ONSET_CHOICES }]} disabled={disabled}/> {onsetType === 'age' && } {onsetType === 'range' && } {onsetType === 'period' && }
); })}
); };