import React, { useState } from 'react'; import type { Patient } from 'fhir/r4'; import { CarePlanEditor } from './CarePlanEditor'; import { useEncounterCarePlan } from './useEncounterCarePlan'; import type { PlanRecurrence } from '../../../utils/carePlanAuthoring'; import { deriveGoalDirection } from '../../../utils/goalProgress'; import { DEFAULT_TRACKABLE_MEASURE, findTrackableMeasure, TRACKABLE_MEASURES, } from '../../../utils/trackableMeasures'; import { EmptyState } from '../../../components/ui/empty-state'; import { useSaveScope } from '../../../utils/saveOperation'; // The care plan tab's contents: the hook that holds the plan, the forms that add to it, and the panel // that draws it. // // SPLIT FROM THE EDITOR ON PURPOSE. `CarePlanEditor` takes everything as props and calls no service, so // a test can render it with a plan from another practice and assert directly that it offers nothing it // cannot do. This file is the only part needing a live service context. // // EVERY ADD ASKS FOR WHAT IT NEEDS, and that is not ceremony. A goal created with placeholder numbers // would have a start equal to its target, which `goalProgress` refuses to draw - so the practitioner // would get a goal that silently shows nothing and no clue why. A control that creates a resource // nobody can use is worse than one that is not there. type OpenForm = 'none' | 'plan' | 'instruction' | 'measurement' | 'goal' | 'attach-goal' | 'team'; const field = 'mt-1 w-full rounded border border-border p-1.5 text-sm'; // `listsOtherPractices`: off where the page already lists every plan beside the builder. export const EncounterCarePlanPanel: React.FC<{ patient?: Patient; listsOtherPractices?: boolean }> = ({ patient, listsOtherPractices = true }) => { const carePlan = useEncounterCarePlan(patient); const [open, setOpen] = useState('none'); const [busy, setBusy] = useState(false); const [text, setText] = useState(''); // WHAT IS MEASURED AND ITS UNIT ARE ONE ANSWER, taken from the shared catalogue rather than typed. // A goal and the measurement feeding it must carry the SAME coding or the goal silently never moves, // and a free-text unit beside a fixed code was the shape that let those two drift apart. const [measureKey, setMeasureKey] = useState(DEFAULT_TRACKABLE_MEASURE.key); const [frequency, setFrequency] = useState('weekly'); const [startValue, setStartValue] = useState(''); const [targetValue, setTargetValue] = useState(''); const [teamId, setTeamId] = useState(''); const [attachGoalId, setAttachGoalId] = useState(''); // A change that may have saved unconfirmed holds every change until a read shows it or the clinician releases it. const held = Boolean(carePlan.unconfirmed); const locked = busy || held; const reset = () => { setOpen('none'); setText(''); setStartValue(''); setTargetValue(''); }; // What is typed belongs to the patient and the person signed in: another of either empties every form and // frees the panel, so nothing typed for one is added for the next. const panelScope = useSaveScope(patient?.id ? `${patient.id}|${carePlan.currentOrganizationId ?? ''}` : undefined, () => { reset(); setTeamId(''); setAttachGoalId(''); setMeasureKey(DEFAULT_TRACKABLE_MEASURE.key); setFrequency('weekly'); setBusy(false); }); // One at a time. Each Add is one decision the clinician made, and letting a second start before the // first has come back would reorder the plan under them. // A change that was not saved keeps its form and choice open, so trying again is a decision. const submit = async (operation: () => Promise) => { if (locked) return; const mine = panelScope.start(); setBusy(true); try { if (await operation() && mine.belongs()) reset(); } finally { if (mine.belongs()) setBusy(false); mine.retire(); } }; const reloadPlan = async () => { if (busy) return; const mine = panelScope.start(); setBusy(true); try { await carePlan.reload(); } finally { if (mine.belongs()) setBusy(false); mine.retire(); } }; if (!patient) { return

No patient.

; } const measure = findTrackableMeasure(measureKey) ?? DEFAULT_TRACKABLE_MEASURE; const nothingTyped = !text.trim(); const goalNumbersUnusable = !Number.isFinite(Number(startValue)) || !Number.isFinite(Number(targetValue)) || startValue.trim() === '' || targetValue.trim() === '' || Number(startValue) === Number(targetValue); return (
{carePlan.unconfirmed && (
{carePlan.unconfirmed} {carePlan.mayRetry && ( )}
)} setOpen(carePlan.plan ? 'instruction' : 'plan'), onAddMeasurement: () => setOpen('measurement'), onAddGoal: () => setOpen('goal'), })} {...(!held && carePlan.attachableGoals.length > 0 ? { onAttachGoal: () => { setAttachGoalId(carePlan.attachableGoals[0]?.id ?? ''); setOpen('attach-goal'); }, } : {})} {...(carePlan.sharing ? { sharing: carePlan.sharing } : {})} {...(held ? {} : { onShareWithTeam: () => { setTeamId(carePlan.sharing?.teamId ?? ''); setOpen('team'); }, onGiveToPatient: () => void submit(() => carePlan.giveToPatient()), })} /> {open === 'attach-goal' && (
{ e.preventDefault(); void submit(() => carePlan.attachGoal(attachGoalId)); }}> {/* SAID BECAUSE IT IS EASY TO ASSUME THE OPPOSITE. Attaching adds this plan to the goal; it does not move the goal, take it over, or change who set it. A goal the patient set stays theirs and keeps saying so on their goals page. */}

This adds the plan to the goal. It does not change who set it, and the goal keeps any other plan it belongs to.

)} {open === 'team' && (
{ e.preventDefault(); void submit(() => carePlan.shareWithTeam(teamId || undefined)); }}> {/* SAID HERE, WHERE THE DECISION IS MADE. A practitioner reasonably reads "share" as "grant access", and it is not: everyone on the team already reads this patient's record through the referral consent the patient approved. Leaving that unsaid would let somebody attach a plan believing it opened a door, or refuse to believing it opened one. */}

This records whose work the plan is. It does not change who can read the patient's record — the team already can.

{carePlan.teams.length === 0 ? ( ) : ( )}
)} {open === 'plan' && (
{ e.preventDefault(); void submit(() => carePlan.startPlan(text.trim())); }}> setText(e.target.value)} />
)} {open === 'instruction' && (
{ e.preventDefault(); void submit(() => carePlan.addInstruction({ description: text.trim() })); }}> setText(e.target.value)} /> {/* Says what it will become, because "standing" is a real behaviour rather than a default: it will not be something the patient ticks off. */}

A standing instruction — not something the patient ticks off.

)} {open === 'measurement' && (
{ e.preventDefault(); void submit(() => carePlan.addMeasurement({ description: text.trim(), measure: measure.code, unit: measure.unit, frequency, })); }}> setText(e.target.value)} />
)} {open === 'goal' && (
{ e.preventDefault(); const direction = deriveGoalDirection(Number(startValue), Number(targetValue)); if (!direction) return; void submit(() => carePlan.addGoal({ description: text.trim(), measure: measure.code, startValue: Number(startValue), targetValue: Number(targetValue), unit: measure.unit, direction, })); }}> setText(e.target.value)} /> {/* On a phone the measure takes its own row, so its name is not cut off under the chevron. */}
{/* Refused rather than saved as a goal that cannot draw: an equal start and target is a zero range, which `goalProgress` declines, so it would render as a goal that shows nothing with no clue why. */}
)} {/* Read-only and never what Add opens: another practice's plan does not stand in for this one's. */} {listsOtherPractices && carePlan.otherPracticePlans.map((other) => ( ))}
); }; export default EncounterCarePlanPanel;