import { PRIMARY_FIELD, type RecordThing } from './recordThingSearch'; import type { ProposalKind } from './recordProposals'; /** * What a clinician adds most, and how a filed item becomes an entry in it. * * Drawn in `mockups/practitioner/encounter.html` as the first group under the search box: * * What you add most * Metformin 500 mg tablet prescription * Type 2 diabetes mellitus diagnosis * * IT IS PER PRACTITIONER AND IT IS HELD ON THE SERVER, which is Spencer's ruling of 2026-09-04 over the * cheaper per-browser version. The case this whole surface is built around is a practitioner writing up * in the evening - often not at the desk they saw the patient from - and a list kept in one browser is * blank exactly then. * * ORDERED BY COUNT, NOT BY RECENCY, because the heading is "what you add most" rather than "what you * added last". A drug prescribed twenty times sits above one prescribed yesterday; recency only breaks * ties, so a new favourite still climbs. */ /** One thing this clinician has filed, with how often. */ export interface RecordThingTally extends RecordThing { count: number; /** ISO instant of the most recent time it was filed. Breaks ties only. */ lastAddedAt: string; } /** * How many are shown. * * The deck draws five, and the same rule governs it as the kind list beside it: *"offer six with two * wrong and a clinician stops reading it."* A long list of favourites is the flat catalogue again. */ export const MOST_ADDED_SHOWN = 5; /** Stable within a clinician's tally: one entry per coding per kind. */ export const tallyKey = (thing: Pick): string => `${thing.kind}:${thing.id}`; /** * The rows shown under "What you add most". * * EMPTY IS A REAL ANSWER AND IS RETURNED AS ONE. On day one a clinician has filed nothing, and the * group is drawn as absent rather than as a heading over nothing - the same trade already ruled on the * kind list, where specialty seeds the order until recents accumulate. */ export const mostAdded = ( tallies: readonly RecordThingTally[], cap: number = MOST_ADDED_SHOWN, ): RecordThing[] => [...tallies] .sort((a, b) => { if (a.count !== b.count) return b.count - a.count; // Newer first among equals, and a missing timestamp sorts last rather than throwing the // comparison - an entry written before this field existed must not outrank a real one. return (b.lastAddedAt ?? '').localeCompare(a.lastAddedAt ?? ''); }) .slice(0, cap) .map(({ count: _count, lastAddedAt: _lastAddedAt, ...thing }) => thing); /** * The thing that was just filed, read back out of what was filed. * * DERIVED FROM THE SAME MAP THE SEARCH SEEDS WITH, so the two can never disagree about which field IS * the thing. An item filed from the search records exactly what was picked; one confirmed from a note * records whatever coding the clinician chose in that row. * * NOTHING IS RECORDED WITHOUT A CODE. A display with no coding cannot be offered back as a favourite - * picking it later would file words rather than a coded item, which is the failure the coded search * exists to prevent. It would also collide with every other uncoded entry of the same name. */ export const thingFromFiledValues = ( kind: ProposalKind, values: Record, ): RecordThing | undefined => { const field = PRIMARY_FIELD[kind]; if (!field) return undefined; const held = values[field]; // A list field holds its value in an array; a single one does not. Both reach here. const picked = Array.isArray(held) ? held[0] : held; if (!picked || typeof picked !== 'object') return undefined; const { code, system, display } = picked as { code?: string; system?: string; display?: string }; if (!code || !display?.trim()) return undefined; return { id: code, label: display.trim(), kind, code, ...(system ? { system } : {}) }; };