import { useEffect, useState } from 'react'; import { requiredText, requiredTextWhen } from '../../utils/requiredText'; import { TextControl } from "../ui/text-control"; import { ComboboxControl } from "../ui/combobox-control"; import type { ComboboxControlOption } from "../ui/combobox-control"; import { Card, CardBody, CardHeader } from '../ui/wp-card'; import { Divider } from '../ui/divider'; import { DismissibleAlert } from '../ui/dismissible-alert'; import { Heading } from '../ui/heading'; import { Flex, FlexItem, FlexBlock } from '../ui/flex'; import { Spacer } from '../ui/spacer'; import { PlusCircle, Trash2, XCircle } from 'lucide-react'; import { Controller, useWatch, useFormContext } from 'react-hook-form'; import { Address } from 'fhir/r4'; import { IValueSet, useValueSetServiceContext } from '../../services'; import { sortValueSet } from "../../utils/valueSetUtils"; import { matchUspsState } from '../../utils/matchUspsState'; import FhirCode from '../FhirCode/FhirCode.tsx'; import FhirPeriod from '../FhirPeriod/FhirPeriod.tsx'; interface FHIRAddressProps { addresses: any; submitting?: boolean; } const FhirAddress = ({ addresses, submitting = false }: FHIRAddressProps) => { const [addressUse, setAddressUse] = useState([]); const [addressType, setAddressType] = useState([]); const [uspsState, setUspsState] = useState([]); /** * WHAT WAS TYPED INTO EACH STATE BOX, which the control itself will not tell the form. * * `ComboboxControl` holds the typed text internally and calls `onChange` only on a click, so the * form could not distinguish "nothing typed" from "typed something we could not resolve" - and it * reported both as "State is required" while the box plainly showed text. Keyed by address index * because the form can hold several addresses and each has its own box. */ const [typedState, setTypedState] = useState>({}); const { control, formState: { errors }, setValue, getValues } = useFormContext(); const { fetchValueSet, valueSetUrls } = useValueSetServiceContext(); const spacerMargin = '24px'; const allAddressFields = useWatch({ control, name: 'address', defaultValue: addresses.fields }); useEffect(() => { const loadValueSets = async () => { try { const [addressUseValueSet, addressTypeValueSet, uspsStateValueSet] = await Promise.all([ fetchValueSet(valueSetUrls.addressUse), fetchValueSet(valueSetUrls.addressType), fetchValueSet(valueSetUrls.uspsState) ]); setAddressUse(addressUseValueSet); setAddressType(addressTypeValueSet); setUspsState(sortValueSet(uspsStateValueSet)); } catch (error) { console.error('Failed to load value sets:', error); } }; loadValueSets(); }, []); const addAddress = () => { if (canAddAddress()) { addresses.append({ line: [''], city: '', state: '', postalCode: '', country: 'US', use: '', type: '' }); } }; const removeAddress = (index: number) => { addresses.remove(index); }; const addAddressLine = (addressIndex: number) => { if (canAddAddressLine(addressIndex)) { const currentLines = getValues(`address.${addressIndex}.line`) || []; setValue(`address.${addressIndex}.line`, [...currentLines, '']); } }; const removeAddressLine = (addressIndex: number, lineIndex: number) => { const currentLines = getValues(`address.${addressIndex}.line`) || []; if (lineIndex > -1 && lineIndex < currentLines.length) { const newLines = [...currentLines]; newLines.splice(lineIndex, 1); setValue(`address.${addressIndex}.line`, newLines); } }; const canAddAddress = (): boolean => { if (addresses.fields.length === 0) { return true; } const lastAddressIndex = addresses.fields.length - 1; const lastLines = getValues(`address.${lastAddressIndex}.line`) || []; const hasNonEmptyLine = lastLines.some((line: string) => line.trim() !== ''); const lastCity = getValues(`address.${lastAddressIndex}.city`) || ''; const lastState = getValues(`address.${lastAddressIndex}.state`) || ''; const lastPostalCode = getValues(`address.${lastAddressIndex}.postalCode`) || ''; const lastCountry = getValues(`address.${lastAddressIndex}.country`) || ''; return hasNonEmptyLine && lastCity.trim() !== '' && lastState && lastPostalCode.trim() !== '' && lastCountry.trim() !== ''; }; const canAddAddressLine = (addressIndex: number): boolean => { const lines = getValues(`address.${addressIndex}.line`) || []; if (lines.length === 0) { return true; } const lastLine = lines[lines.length - 1]; return lastLine.trim() !== ''; }; return ( <> {addresses.fields.length === 0 && ( <> Address No address added yet. )} {addresses.fields.map((address: Address, addressIndex: number) => { const addressLines: string[] = allAddressFields?.[addressIndex]?.line ?? ['']; return ( <> {addresses.fields.length > 0 && (

Address {addressIndex + 1}

)} {/* Address Lines */} {addressLines.map((_line: string, lineIndex: number) => (
( )} /> {lineIndex === addressLines.length - 1 && ( )} {addressLines.length > 1 && ( )} {errors?.address?.[addressIndex]?.line?.[lineIndex] && ( Address line {lineIndex + 1} is required )}
))} {/* Country and City */} ( )} /> {errors?.address?.[addressIndex]?.country && ( Country is required )} ( )} /> {/* `address`, singular. This read `errors?.addresses` - a field name that does not exist, since the Controller registers `address.N.city` - so the message could never render however the field was filled. Found in a browser walk: a City of one space reddened the box and said nothing, while Country, Postal Code and Address line 1 beside it all named their fault. */} {errors?.address?.[addressIndex]?.city && ( City is required )} {/* State and Postal Code */} { const selectedOption = uspsState.find(state => state.code === field.value); const transformedValue = selectedOption ? selectedOption.code : undefined; return ( <> ({ value: state.code, label: `${state.display} (${state.code})` })) ] as ComboboxControlOption[]} value={transformedValue} onChange={(next) => { // Clicking a suggestion is unchanged - control 1. setTypedState((current) => ({ ...current, [addressIndex]: '' })); field.onChange(next); }} onFilterValueChange={(text: string) => { setTypedState((current) => ({ ...current, [addressIndex]: text })); // TYPING "IL" NOW COUNTS. Exact matches only - // a prefix match would resolve "M" to whichever // of eight states sorts first and write it into // somebody's address, which is worse than the // defect being fixed. const match = matchUspsState(uspsState, text); if (match?.code) field.onChange(match.code); }} /> ); }} /> {/* THE MESSAGE MATCHES WHAT THEY ARE LOOKING AT. An empty box gets "required"; a box with unrecognised text in it gets told the text is the problem. Reporting the second as "required" is the contradiction this control was refused for - the user reads it beside the value it says is missing, and there is no way to reason out of that. */} {errors?.address?.[addressIndex]?.state && ( {(typedState[addressIndex] ?? '').trim().length > 0 ? 'That is not a state we recognise — pick one from the list.' : 'State is required'} )} ( )} /> {errors?.address?.[addressIndex]?.postalCode && ( Postal Code is required )} {/* Address Use */} {/* Address Type */} {/* Period */}
{addressIndex === addresses.fields.length - 1 && ( )} ); })} ); }; export default FhirAddress;