import { useState, useEffect } from 'react'; import { SelectControl } from "../../../../components/ui/select-control"; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { InputControl } from '../../../../components/ui/input-control'; import { DismissibleAlert } from '../../../../components/ui/dismissible-alert'; import { Heading } from '../../../../components/ui/heading'; import { Flex, FlexBlock } from '../../../../components/ui/flex'; import { Spacer } from '../../../../components/ui/spacer'; import { Check } from 'lucide-react'; import type { Coverage } from 'fhir/r4'; import './CoveragePage.css'; import { AppHeader, AppSpinner } from '../../../../components'; import { Card, CardContent, CardHeader } from '../../../../components/ui/card.tsx'; import { useCoverageServiceContext, usePatientServiceContext, isUninsuredCoverage, type CoverageFormData, COVERAGE_TYPE_OPTIONS, RELATIONSHIP_OPTIONS, } from '../../../../services'; import { COVERAGE_STATUS_OPTIONS, fhirStatusFor, statusChoiceOf, statusDateConflict, type CoverageStatusChoice, } from '../coverageStatusChoice'; import { patientCacheGuard } from '../../../../utils/cacheOwner'; import { relayServerFailure } from '../../../../utils/fetchFailureMessage'; const EMPTY_FORM: CoverageFormData = { status: 'active', type: null, subscriberId: '', relationship: null, periodStart: '', periodEnd: '', payorName: '', groupNumber: '', planName: '', order: '1', network: '', }; const spacerMargin = '24px'; interface CoveragePageProps { // Embedded-in-modal: the coverage id ('new' to add) + a back-to-list handler. Absent = routed (useParams/navigate). coverageId?: string; onExit?: () => void; // Drawn inside the list's overlay: Cancel and Save sit at the foot, and the overlay carries the title. overlay?: boolean; // Replaces the exit after a save, so a host that stays mounted can refresh its list. onSaved?: (coverageId?: string) => void; } const CoveragePage = ({ coverageId: propCoverageId, onExit, overlay = false, onSaved }: CoveragePageProps = {}) => { const { coverageId: routeCoverageId } = useParams<{ coverageId: string }>(); const coverageId = propCoverageId ?? routeCoverageId; const navigate = useNavigate(); const [searchParams] = useSearchParams(); const isNew = !coverageId || coverageId === 'new'; // Back-to-list when embedded in a modal; navigate up when routed. const exit = () => { if (onExit) onExit(); else navigate('..', { relative: 'path' }); }; const { patientState } = usePatientServiceContext(); const { cachedForPatientId: coverageOwner, coverageList: rawCoverageList, fetchCoverage, createCoverage, updateCoverage, archiveCoverage, transformToFhirCoverage, transformFromFhirCoverage, setLastUpserted, setShouldReload, isCoverageExpired, } = useCoverageServiceContext(); // The cache outlives a change of patient: read only the patient on screen. const coverageGuard = patientCacheGuard(coverageOwner, patientState?.id); const coverageList = coverageGuard.list(rawCoverageList); // Only a plan in force holds a priority. A cancelled or lapsed Primary used to block adding a new // Primary - the overlay opened already warning "a coverage with this priority order already exists" // for a patient whose every plan had ended, and Save refused until they picked a lower slot. const holdsPriority = (c: Coverage) => c.status === 'active' && !isCoverageExpired?.(c); const [formData, setFormData] = useState(() => { const preselectedCode = searchParams.get('type'); const preselectedIsNoPayer = preselectedCode === 'UNINSURED' || preselectedCode === 'SELF'; let order = EMPTY_FORM.order; if (isNew && !preselectedIsNoPayer) { const usedOrders = new Set(coverageList.filter(holdsPriority).map(c => c.order ?? 0)); for (let i = 1; i <= 4; i++) { if (!usedOrders.has(i)) { order = String(i); break; } } } if (isNew && preselectedCode) { const preselectedType = COVERAGE_TYPE_OPTIONS.find(o => o.code === preselectedCode) ?? null; if (preselectedType) return { ...EMPTY_FORM, order, type: preselectedType }; } return { ...EMPTY_FORM, order }; }); const [loading, setLoading] = useState(!isNew); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(); const [duplicateOrderWarning, setDuplicateOrderWarning] = useState(false); useEffect(() => { if (isNew || !coverageId || !patientState) return; const load = async () => { setLoading(true); try { const coverage = await fetchCoverage(coverageId); const loaded = transformFromFhirCoverage(coverage); setFormData({ ...loaded, status: statusChoiceOf(coverage.status, coverage.period?.end) }); } catch (err) { console.error('Failed to load coverage:', err); setError('Could not load coverage. Try again.'); } finally { setLoading(false); } }; load(); }, [coverageId, isNew]); useEffect(() => { const typeCode = formData.type?.code; if (!formData.order || typeCode === 'UNINSURED' || typeCode === 'SELF') { setDuplicateOrderWarning(false); return; } const orderNum = parseInt(formData.order, 10); const isDuplicate = coverageList.some(c => { if (!isNew && c.id === coverageId) return false; return holdsPriority(c) && c.order === orderNum; }); setDuplicateOrderWarning(isDuplicate); }, [formData.order, formData.type?.code, coverageList, isNew, coverageId]); const set = (field: keyof CoverageFormData) => (value: string) => setFormData(prev => ({ ...prev, [field]: value })); const handleSubmit = async () => { if (!patientState) { setError('Patient information is not loaded.'); return; } if (duplicateOrderWarning) { setError('A coverage with this priority order already exists. Please select a different priority order.'); return; } if (!isNoPayer && !formData.payorName.trim()) { setError('Please enter the insurance company.'); return; } const statusChoice = formData.status as CoverageStatusChoice; const conflict = isNoPayer ? undefined : statusDateConflict(statusChoice, formData.periodEnd); if (conflict) { setError(conflict); return; } setSubmitting(true); setError(undefined); try { const fhirCoverage = transformToFhirCoverage({ ...formData, status: fhirStatusFor(statusChoice) }, patientState); let result; if (isNew) { // COVERAGE HAS EXACTLY TWO ANSWERS, SO RECORDING A PLAN RETIRES "NONE RIGHT NOW". // Marking uninsured already archives every active plan; the reverse did not. With Add // Insurance now reachable from an uninsured record, the two could otherwise stand at // once and the record would say both things. The direction that was missing is this one. // // Only for a real payer: saving a no-payer record IS the uninsured answer and must not // archive itself. if (!isNoPayer) { await Promise.all( coverageList .filter(c => c.status === 'active' && isUninsuredCoverage(c)) .map(c => archiveCoverage(c, 'no-longer-applies')), ); } result = await createCoverage(fhirCoverage); } else { result = await updateCoverage({ ...fhirCoverage, id: coverageId }); } setLastUpserted(result.id); setShouldReload(true); if (onSaved) onSaved(result.id); else exit(); } catch (err) { console.error('Failed to save coverage:', err); setError(relayServerFailure(err, 'Failed to save insurance. Please try again.')); } finally { setSubmitting(false); } }; const isNoPayer = formData.type?.code === 'UNINSURED' || formData.type?.code === 'SELF'; if (loading) return ; const typeOptions = [ { label: 'Select type', value: '' }, ...COVERAGE_TYPE_OPTIONS .filter(o => isNoPayer || (o.code !== 'UNINSURED' && o.code !== 'SELF')) .map(o => ({ label: o.display, value: o.code })), ]; const relationshipOptions = [ { label: 'Select relationship', value: '' }, ...RELATIONSHIP_OPTIONS.map(o => ({ label: o.display, value: o.code })), ]; const orderOptions = [ { label: 'Primary', value: '1' }, { label: 'Secondary', value: '2' }, { label: 'Tertiary', value: '3' }, { label: 'Other', value: '4' }, ]; return (
{!overlay && ( )} {error && !overlay && ( <> setError(undefined)}> {error} )} {duplicateOrderWarning && ( <> A coverage with this priority order already exists. Please select a different priority order before saving. )} {/* Insurance Company card */} Insurer { const opt = COVERAGE_TYPE_OPTIONS.find(o => o.code === code); setFormData(prev => ({ ...prev, type: opt ?? null, ...((opt?.code === 'SELF' || opt?.code === 'UNINSURED') && { payorName: '' }), })); }} /> {/* 🗣️ Spencer, 2026-08-15: *"I don't want patients selecting a list of insurers from a list an org creates. The patient should just have regular fields where they can add any insurance company along with their ID number. No different than any other new patient intake form, or a receptionist looking at their insurance ID card."* 🔻 THIS WAS A DROPDOWN OF ORGANIZATIONS ONE PRACTICE HAD CREATED, and it broke the org-agnostic model at its foundation. A patient belongs to no organization and attends several — so their insurer was unenterable the moment it was not on that practice's list ("No Insurance Providers available. Contact Admin"), and the same human's insurer became a different resource at every practice they attend. ⭐ A plain field is what the paper form asks and what the card in their hand says. */} {!isNoPayer && ( Insurance Company *} help="As it appears on the insurance card." value={formData.payorName} onChange={value => setFormData(prev => ({ ...prev, payorName: value ?? '' }))} /> )} {!isNoPayer && ( Coverage Details set('subscriberId')(val ?? '')} placeholder="e.g. XYZ123456789" /> { const opt = RELATIONSHIP_OPTIONS.find(o => o.code === code); setFormData(prev => ({ ...prev, relationship: opt ?? null })); }} /> set('periodStart')(val ?? '')} /> set('periodEnd')(val ?? '')} /> )} {!isNoPayer && ( <> Plan Information set('groupNumber')(val ?? '')} placeholder="e.g. 98765" /> set('planName')(val ?? '')} placeholder="e.g. Blue PPO 2500" /> set('network')(val ?? '')} placeholder="e.g. Blue PPO" /> )} {/* In the overlay the refusal sits beside Save, where the press was made. */} {overlay && error && ( <> setError(undefined)}> {error} )} {overlay && (
)}
); }; export default CoveragePage;