import React, { useState } from 'react'; import type { CodeableConcept, Goal } from 'fhir/r4'; import { X } from 'lucide-react'; import { Dialog, DialogClose, DialogContent, DialogHeader, DialogTitle } from '../../../../components/ui/dialog'; import { deriveGoalDirection, getGoalStartValue, type GoalDirection, } from '../../../../utils/goalProgress'; import { DEFAULT_TRACKABLE_MEASURE, findMeasureForGoal, findTrackableMeasure, TRACKABLE_MEASURES, } from '../../../../utils/trackableMeasures'; import { formatFhirCodeableConcept } from '../../../../utils/fhirCodeableConcept'; import { describeWriteFailure } from '../../../../utils/fetchFailureMessage'; import { isRecordChanged } from '../../../../utils/recordChanged'; import { outcomeUnknown } from '../../../../utils/saveOperation'; // Sent, with the answer lost: it may have been saved, so it is not offered again from here. export const GOAL_SET_UNCONFIRMED = 'We could not confirm the goal was set. Close this and check your goals before setting it again.'; export const GOAL_EDIT_UNCONFIRMED = 'We could not confirm your change was saved. Close this and open the goal again to see it as it is now.'; // The goal modal, opened over the goals list by the patient and by staff alike. Four questions and no // more, in the reader's voice: the patient's copy says "you", a clinician's says "they". // // WHAT IS BEING MEASURED IS THE UNIT FIELD, and that is why there is no fifth question. The drawing's // boxes read "350 lb" and "200 lb" - a number and a unit - so the unit control carries the coding with // it. Without one the goal would have no measure at all, no reading could ever match it, and the bar // would sit at nought forever with nothing saying why. // // DIRECTION IS ASKED, NEVER INFERRED. "350 to 200 is downward and 8,000 steps is upward; without it a // bar cannot say whether a number is progress." Reading it off whether the target sits below the start // is wrong the first time somebody enters the two the wrong way round. export interface GoalAnswers { description: string; measure: CodeableConcept; startValue: number; targetValue: number; unit: string; direction: GoalDirection; } interface Props { onSet: (input: GoalAnswers) => Promise | void; onCancel: () => void; /** The goal being changed, when this is an edit rather than a new one. */ goal?: Goal; reader?: 'patient' | 'staff'; } // A goal written before the measure list existed keeps its own measure rather than being reopened as // the default and silently rewritten to pounds on save. One that has no measure is asked for one. const AS_RECORDED = 'as-recorded'; const UNCHOSEN = ''; const field = 'mt-1 w-full rounded border border-border p-2 text-sm'; const label = 'block text-xs text-muted-foreground'; export const SetYourOwnGoal: React.FC = ({ onSet, onCancel, goal, reader = 'patient' }) => { const staff = reader === 'staff'; const target = goal?.target?.[0]; // Matched on the code and the unit together - weight in pounds and in kilograms share a coding. const existingMeasure = findMeasureForGoal(target?.measure, target?.detailQuantity?.unit); const recordedMeasure = !existingMeasure && target?.measure ? target.measure : undefined; const existingStart = getGoalStartValue(goal); const [description, setDescription] = useState(goal?.description?.text ?? ''); const [measureKey, setMeasureKey] = useState( existingMeasure?.key ?? (recordedMeasure ? AS_RECORDED : goal ? UNCHOSEN : DEFAULT_TRACKABLE_MEASURE.key), ); const [startValue, setStartValue] = useState( existingStart === undefined ? '' : String(existingStart), ); const [targetValue, setTargetValue] = useState( target?.detailQuantity?.value === undefined ? '' : String(target.detailQuantity.value), ); const [busy, setBusy] = useState(false); const [error, setError] = useState(); // Someone else changed this goal since the sheet opened: nothing more is saved from it. Reopened, it starts from // the goal as it is now. const [changedElsewhere, setChangedElsewhere] = useState(false); const recorded = measureKey === AS_RECORDED && recordedMeasure; const measure = findTrackableMeasure(measureKey) ?? DEFAULT_TRACKABLE_MEASURE; const unit = recorded ? target?.detailQuantity?.unit ?? '' : measure.unit; // Refused rather than saved as a goal that cannot draw: an equal start and target is a zero range. const unusable = !description.trim() || measureKey === UNCHOSEN || startValue.trim() === '' || targetValue.trim() === '' || !Number.isFinite(Number(startValue)) || !Number.isFinite(Number(targetValue)) || Number(startValue) === Number(targetValue); const submit = async (event: React.FormEvent) => { event.preventDefault(); const direction = deriveGoalDirection(Number(startValue), Number(targetValue)); if (unusable || busy || changedElsewhere || !direction) return; setBusy(true); setError(undefined); try { await onSet({ description: description.trim(), measure: recorded ? recordedMeasure : measure.code, startValue: Number(startValue), targetValue: Number(targetValue), unit, direction, }); } catch (failure) { if (isRecordChanged(failure)) { setChangedElsewhere(true); setError(`${failure.message} Close this and open the goal again to edit it as it is now.`); return; } if (outcomeUnknown(failure)) { setChangedElsewhere(true); setError(goal ? GOAL_EDIT_UNCONFIRMED : GOAL_SET_UNCONFIRMED); return; } // The modal stays open with the answers in it, so a failed save can be tried again. setError(describeWriteFailure(failure, 'The goal could not be saved')); } finally { setBusy(false); } }; const planReference = (goal?.addresses ?? []).find((reference) => reference.reference?.startsWith('CarePlan/')); const patientSetIt = goal?.expressedBy?.reference?.startsWith('Patient/') ?? false; const planLine = planReference ? `On ${planReference.display?.trim() ? `the ${planReference.display.trim()} plan` : 'a care plan'}` : !staff ? 'Not attached — this is your own goal' : patientSetIt ? 'Not attached — this is the patient’s own goal' : 'Not attached to a care plan'; return ( { if (!open) onCancel(); }}> {/* The action sits in the header: four fields fit, and the header is where the built modal already puts it. The X closes, so there is no Cancel beside it. */} {goal ? 'Edit goal' : 'Set a goal'}
{error &&

{error}

}
setDescription(event.target.value)} />
setStartValue(event.target.value)} />
setTargetValue(event.target.value)} />
{/* Stated rather than offered: a plan is attached from the plan, by the practice that wrote it. */}
Attach it to a care plan

{planLine}

); }; export default SetYourOwnGoal;