import React from 'react'; import { useFormContext, useFormState } from 'react-hook-form'; import { FhirValueSetSearch } from '../../../components'; import UcumUnitSearch from '../../../components/UcumUnitSearch/UcumUnitSearch'; import { readPair, BLOOD_PRESSURE } from '../../../utils/pairedObservation'; import { instantsAtWallTime, isRealDay, toLocalDateTimeInput, utcOffsetLabel, wallTimeOf } from '../../../utils/dateUtils'; import { useValueSetServiceContext } from '../../../services'; import type { ComposedField } from '../../../utils/composeRecordForm'; import { NoteTermOffer } from './NoteTermOffer'; /** * One field in the condensed record form: a label, and the value under it. * * A CODED FIELD IS A SEARCH, NEVER A TEXT BOX. Spencer, 2026-09-04: *"it needs to be coded. We should * have a search select option where they can select the coded prescription."* Those rows render the * product's own `FhirValueSetSearch` against the value set the catalogue derived for that field, so what * is filed carries a real coding rather than words that merely look like one. * * EVERYTHING ELSE IS THE SAME PLAIN ROW, which is the drawing: * `mockups/practitioner/encounter.html` draws Quantity to dispense, Days supply and Earliest fill date * as identical boxes, and that uniformity is what lets a ten-field prescription fit on a screen a * clinician reads between patients. * * AMBER ON THE EDGE, NEVER RED, and the mockup's own stylesheet says why: *"A red one reads as an error * the clinician has already made, and they have not made one yet - they simply have not reached it."* */ interface Props { composed: ComposedField; disabled?: boolean; /** The note gave no time, so this one started at now. */ startedAtNow?: boolean; /** What the note gave for this time when it was not a date and time. */ unreadTime?: string; } export const RecordFieldRow: React.FC = ({ composed, disabled, startedAtNow, unreadTime }) => { const { field, band } = composed; const { register, watch, setValue, getFieldState } = useFormContext(); // Answered by the clinician in this proposal's form (typed, cleared, Now, or the repeated hour): the started-at-now // explanation was about the untouched default, so it goes once the time is theirs. const timeAnswered = getFieldState(field.name, useFormState({ name: field.name })).isTouched; const { valueSetUrls } = useValueSetServiceContext(); // Required AND still empty. A required field the note filled sits in the first band and is not // owed - marking it here would ask for something already on screen. const owed = band === 'still-needed'; const id = `record-field-${field.name}`; if (field.shape === 'codeableConcept' || field.shape === 'code') { /* THE REAL SEARCH, bound to the value set derived for this field. It writes a value-set object into the form, which is exactly what this kind's own `transformToFhirX` reads - so a coded field goes onto the record with its code and its system, not as a display string. A field with no value set never reaches here: the catalogue marks it `unsearchable` and `fieldsForKind` does not offer it, because the only thing left to render would be the text box this row exists to replace. */ return ( <> ); } if (field.shape === 'quantity') { // What the note supplied, as the row holds it - read so the row can say when it is something // this form cannot file rather than letting it look answered. const reading = (watch(`${field.name}.value`) ?? '') as unknown; const written = typeof reading === 'string' ? reading.trim() : reading; /* A PAIRED READING IS EDITED AS THE PAIR IT IS. The note says "128/78 mmHg" and no single box can hold that. The real observation form already edits `Observation.component`, and the general doc says a proposal opens THE REAL FORM condensed - so the condensed row offers the same two halves rather than refusing a shape the product records everywhere else. THE HALVES ARE THE ONES THE PRODUCT ALREADY DECLARES, not ones invented here: the vitals form builds a blood pressure as a panel of systolic and diastolic, and `pairedObservation` says the same thing in the shape this row needs, with a guard comparing the two. */ const pair = readPair(written); const unfileableReading = !pair && typeof written === 'string' && written !== '' && !Number.isFinite(Number(written)); /* A MEASUREMENT IS A NUMBER AND A UNIT, AND ONE BOX CAN ONLY CARRY THE NUMBER. This row used to be the same plain text input as everything else, so a temperature confirmed from the note filed as the bare number 38.4 - which is a fever in Celsius and hypothermia in Fahrenheit, and the record said neither. The transform was never at fault: `transformToFhirQuantity` reads a unit when it is given one and omits it when it is not, and nothing on this screen could give it one. An allergy's onset age and a condition's abatement age landed the same way, with no answer to years or months. THE UNIT IS THE PRODUCT'S OWN UCUM SEARCH, for the same reason a coded field is a search rather than a box: what is filed has to carry a real code and system, and free text beside a number is not a unit. It writes the value-set object the transform already reads. THE ROW KEEPS ITS DRAWN ANATOMY - one label, the value under it - because the deck's uniformity is what lets a ten-field item fit on a screen a clinician reads between patients. The unit sits beside the number inside that row rather than becoming a second field. */ return (
{pair && ( <> {BLOOD_PRESSURE.halves.map((half, index) => ( { // Written back as the pair, because that is what the filing // reads and what the row is showing. Keeping two separate // fields here would be a second place for the same reading. const next = event.target.value; const other = index === 0 ? pair.diastolic : pair.systolic; setValue( `${field.name}.value`, index === 0 ? `${next}/${other}` : `${other}/${next}`, ); }} className="record-quantity__value block w-full min-w-0 rounded-[5px] border border-input bg-background px-2.5 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring" /> ))} )}
{/* THE REFUSAL NAMES WHAT WAS REFUSED, rather than letting the row look answered. A reading the note supplied that this form cannot file - a blood pressure, which is a PAIR and lives in `component` - would otherwise sit in "From your note" reading "128/78 mmHg", arm Save on the three required fields, and file an observation with no value. A form that cannot record the reading is a defect; a form that cannot record it and says nothing is a clinical one, because the practitioner walks away believing the pressure is in the record. One denial rule everywhere: a refusal renders as a denial naming what is denied, never as a failure or empty data. */} {unfileableReading && (

This is not one number, so it cannot be recorded here. Use the full form, or enter a single value with its unit.

)}
); } if (field.shape === 'dateTime' && field.dayAllowed) { // A record's own date: the day, and its time only if somebody knows it. A day alone is filed as that day, never // as a midnight or a now nobody gave; with a time it is one moment, and which one when the clocks repeat an hour. const held = watch(field.name); const day = isRealDay(held) ? held : undefined; const shown = day ? '' : wallTimeOf(held); const dateShown = day ?? shown.slice(0, 10); const timeShown = shown.slice(11); const moments = shown ? instantsAtWallTime(shown) : []; const pinned = shown && held !== shown ? new Date(held as string).getTime() : undefined; const clock = shown.slice(11, 16); const box = (warn: boolean) => [ 'block w-full min-w-0 rounded-[5px] border bg-background px-2.5 py-2 text-sm text-foreground', 'focus:outline-none focus:ring-2 focus:ring-ring', warn ? 'border-warning bg-amber-soft' : 'border-input', ].join(' '); return (
{ const next = event.target.value; setValue(field.name, next && timeShown ? `${next}T${timeShown}` : next, { shouldDirty: true, shouldTouch: true }); }} className={box(owed)} />
{ const next = event.target.value; setValue(field.name, next ? `${dateShown}T${next}` : dateShown, { shouldDirty: true, shouldTouch: true }); }} className={box(false)} />
{moments.length === 2 && (

{clock} happens twice here as the clocks go back. Which one?

{moments.map((moment, index) => ( ))}
)} {shown && moments.length === 0 && (

{clock} does not happen on this day here: the clocks go forward. Enter another time, or none.

)} {unreadTime !== undefined && (

The note gave {unreadTime ? `"${unreadTime}"` : 'a value'}, which is not a date. Enter the date, and the time if known.

)}
); } if (field.shape === 'dateTime') { // When it was taken: the held moment at its local time, with Now, and which one when the clocks repeat an hour. const held = watch(field.name); const shown = wallTimeOf(held); const moments = shown ? instantsAtWallTime(shown) : []; const pinned = shown && held !== shown ? new Date(held as string).getTime() : undefined; const clock = shown.slice(11, 16); return (
setValue(field.name, event.target.value, { shouldDirty: true, shouldTouch: true })} className={[ 'block w-full min-w-0 rounded-[5px] border bg-background px-2.5 py-2 text-sm text-foreground', 'focus:outline-none focus:ring-2 focus:ring-ring', owed ? 'border-warning bg-amber-soft' : 'border-input', ].join(' ')} />
{moments.length === 2 && (

{clock} happens twice here as the clocks go back. Which one?

{moments.map((moment, index) => ( ))}
)} {shown && moments.length === 0 && (

{clock} does not happen on this day here: the clocks go forward. Enter another time.

)} {unreadTime !== undefined && (

The note gave {unreadTime ? `"${unreadTime}"` : 'a value'}, which is not a date and time.{' '} {field.name === 'effectiveDateTime' ? 'Enter when it was taken.' : 'Enter the date and time.'}

)} {startedAtNow && !timeAnswered && (

The note did not say when; this started at now.

)}
); } if (field.shape === 'choice') { /* ONE OF ITS ANSWERS, PICKED BY THE CLINICIAN, AND NONE UNTIL THEN. Each answer is the value it files - a vaccine's source is `true` or `false` - so it is held as that boolean, never a string or an unticked box that would read as `false`. Nothing is picked at first: the note may not answer it (`noteMayFill`). */ const held = watch(field.name); return (
{field.label} {(field.choices ?? []).map((choice) => ( ))}
); } if (field.shape === 'unfileable') { /* SHOWN, AND NOT TYPEABLE. These are the shapes a line of text cannot honestly become - a Reference needs the id of a resource that exists, and a Period or a Range needs structure a single box does not have. They are on screen so a clinician can READ what the note said about one, and they are never sent. WALKED AND FIXED, 2026-09-04: they used to render as ordinary inputs, so the long tail offered boxes you could type into that discarded what you typed on save. Reading is the whole reason they are here; accepting input never was. */ const held = watch(field.name); const shown = typeof held === 'string' ? held : ''; return (
{field.label}

{shown || 'Recorded on the full form, not here'}

); } return (
); }; export default RecordFieldRow;