import React, { useMemo } from 'react'; import type { CarePlanReadStatus } from './useEncounterCarePlan'; import type { CarePlan, Goal, Observation, Task } from 'fhir/r4'; import { Check, Loader2, TriangleAlert, Lock } from 'lucide-react'; import type { NoteDraftStatus } from './useEncounterClinicalNote'; import { describeActivity } from '../../../utils/planActivity'; import { goalProgress } from '../../../utils/goalProgress'; import { carePlanAuthorship } from '../../../utils/carePlanAuthorship'; import type { PlanSharing } from '../../../utils/carePlanTeam'; import { EmptyState } from '../../../components/ui/empty-state'; // The care plan, written from the visit. // // A THIRD TAB BESIDE THE TWO NOTES, and the drawing says why: the tabset already means "what I am // writing right now", and both notes in it already behave the way a plan needs to - a draft as you type, // finished later, with a reminder if the visit ends first. This INHERITS that vocabulary rather than // reimplementing it, which is why the status words below are the note's own `NoteDraftStatus`. // // The plan outlives the encounter, unlike either note. That is said by where it appears afterwards, not // by making it awkward to write here. /** * The same words the clinical note uses, for the same reason it uses them: a draft that failed to save * must never be quiet. The clinician has been taught to stop thinking about saving, so an unreported * failure is worse than no autosave at all. */ const DRAFT_LABEL: Record = { idle: null, 'draft-saving': Saving draft…, 'draft-saved': Draft · saved as you type, 'draft-failed': Draft NOT saved — this plan only exists in this browser., 'draft-conflict': This plan was changed in another window. Autosave is off — copy your changes before leaving., }; // Once given to the patient the plan is no longer a draft, so autosave is saving the plan itself. const RELEASED_LABEL: Record = { ...DRAFT_LABEL, 'draft-saving': Saving…, 'draft-saved': Saved as you type, 'draft-failed': Change NOT saved — it only exists in this browser., }; export interface CarePlanEditorProps { plan?: CarePlan; /** Defaults to 'ready': a caller that passes no status has a read that answered. */ readStatus?: CarePlanReadStatus; activities: Task[]; goals: Goal[]; /** Every reading for this patient. Progress is derived from these, never stored. */ observations: Observation[]; currentOrganizationId?: string; draftStatus?: NoteDraftStatus; onAddInstruction?: () => void; onAddMeasurement?: () => void; onAddGoal?: () => void; onAttachGoal?: () => void; onGiveToPatient?: () => void; /** Which collaboration team is running this plan, when one is. */ sharing?: PlanSharing; onShareWithTeam?: () => void; } export const CarePlanEditor: React.FC = ({ plan, activities, goals, observations, currentOrganizationId, draftStatus = 'idle', readStatus = 'ready', onAddInstruction, onAddMeasurement, onAddGoal, onAttachGoal, onGiveToPatient, sharing, onShareWithTeam, }) => { const authorship = useMemo( () => carePlanAuthorship(plan, { currentOrganizationId }), [plan, currentOrganizationId], ); // Read-only is decided ONCE, here, and every control below reads it. The alternative - each control // deciding for itself - is how one of them stays live on somebody else's plan. // Only a plan whose latest read answered is edited; not on the strength of an older read. const readOnly = !authorship.mayEdit || readStatus !== 'ready'; if (!plan && readStatus === 'loading') { return
Loading the care plan...
; } // No Start button here: a plan may exist that this read could not see. A refusal hides a plan read // earlier too. if (readStatus === 'refused' || (!plan && readStatus === 'failed')) { return (
{readStatus === 'refused' ? "You do not have access to this patient's care plan." : 'The care plan could not be loaded, so it is not known whether this patient has one. Reopen the visit to try again.'}
); } if (!plan) { return (
No care plan for this visit yet. {onAddInstruction && (
)}
); } return (
{readStatus === 'failed' && (
The care plan could not be refreshed, so this may be out of date. Changes are off until it loads; reopen the visit to try again.
)} {/* Says it is not yours BEFORE anything is typed, rather than refusing at save - which would already have cost the work. */} {authorship.isFromAnotherPractice && (
{authorship.readOnlyReason}
)}
For {plan.title || plan.description || 'this plan'}
{/* WHICH TEAM IS RUNNING IT, and deliberately not "who can see it". A practitioner on the team already holds full-clinical access to the patient through the patient-approved referral Consent, patient-wide rather than plan-scoped - so the attachment says whose work this is and hands nobody a key. Wording it as permission would be a promise the attachment does not make. */} {(sharing || (!readOnly && onShareWithTeam)) && (
Shared with {sharing ? {sharing.label} : no collaboration team} {!readOnly && onShareWithTeam && ( )}
)}

What the patient does

{activities.length === 0 ? ( ) : (
    {activities.map((activity) => { const described = describeActivity(activity); return (
  • {activity.description}
    {described.label} {described.detail} {described.stopsWhenGoalReached && ' · Stops when the goal is reached'}
  • ); })}
)} {!readOnly && (onAddInstruction || onAddMeasurement) && (
{onAddInstruction && ( )} {onAddMeasurement && ( )}
)}

What it is working towards

{goals.length === 0 ? ( ) : (
    {goals.map((goal) => { // Derived here, at read time, from the observations that exist. Nothing on the // goal stores a number - a stored one is a frozen snapshot that goes wrong the // moment anything else writes a reading. const progress = goalProgress(goal, observations); return (
  • {goal.description?.text} {progress && ( <> {' '} {progress.startValue} → {progress.targetValue} )}
    {progress ? `${progress.direction === 'down' ? 'Lower' : 'Higher'} is better` : 'No direction set — this goal cannot show progress yet'}
  • ); })}
)} {/* Each control needs its OWN handler, not just write access. A button whose onClick is undefined renders live and does nothing when pressed, which is worse than an absent one - it is the same dead-end this editor exists to avoid, wearing the clothes of a working feature. "Attach an existing goal" was exactly that until it was wired. */} {!readOnly && (onAddGoal || onAttachGoal) && (
{onAddGoal && ( )} {onAttachGoal && ( )}
)}
{(plan.status === 'draft' ? DRAFT_LABEL : RELEASED_LABEL)[draftStatus]}
{!readOnly && plan.status === 'draft' && onGiveToPatient && ( )}
); }; export default CarePlanEditor;