import type { MyHistoryEvent } from './collapseHistory'; import { resourceLabel } from './resourceLabel'; /** * WHAT SOMEBODY ACTUALLY LOOKED AT, rather than how many times the API was read. * * PURE - no React, no HTTP - so the rule is testable without rendering, like the rest of this folder. * * THE COUNT WAS OF THE WRONG NOUN, AND IT MISLED IN THE ONE DIRECTION THAT MATTERS HERE. An access row * expanded into one line per API read: seven entries that were two things - a profile read four times * and a task read twice, three of those within six seconds of each other. The collapsed row then said * "7 items", and a patient reading that concludes somebody went through seven parts of their record. * They opened two. * * This is an awareness tool whose credibility is the whole product. Overstating what a clinician looked * at is not a rounding error in it - it invents an intrusion that did not happen, and a patient who * checks one alarming row against the truth stops believing the quiet ones too. * * SO THE REPEAT BECOMES THE INFORMATION INSTEAD OF THE NOISE. "Your profile, 4 times, 2:11-2:14 PM" * says strictly more than four identical lines did, and it is true. * * A RANGE, AND MINUTES. Second-level precision is the system's granularity, not a person's; nobody has * ever needed to know two reads were three seconds apart. What a patient wants from a burst of reads is * how long somebody was in there, which is exactly what a range says and a list of stamps does not. * * ORDERED BY FIRST TOUCH, because the sequence is the signal this whole screen is built on. Sorting by * count would put the busiest thing first and destroy the order somebody moved through the record in. * * KNOWN AND NOT SOLVED HERE: "a task" and "your profile" name a KIND, not an instance. An access log * that cannot say WHICH task cannot fully answer the question a worried patient came to ask. Resolving * that needs the referenced resources, which this pure function has no way to fetch; it is a real gap * rather than a decision, and grouping does not make it worse. */ export interface AccessedThing { /** What was opened, in the patient's words - "Your profile", "A task". */ label: string; /** How many times it was read. 1 is normal and is deliberately not rendered as a count. */ times: number; /** First and last read of this thing. Equal when it was read once. */ from: string | undefined; to: string | undefined; } export const groupAccessDetail = (detail: readonly MyHistoryEvent[]): AccessedThing[] => { const byLabel = new Map(); for (const event of detail) { // Grouped on the LABEL rather than on `resourceType`, because the label is what the patient // reads. Two types that both render "a visit" are one thing on screen, and splitting them into // two rows saying the same words would look like a bug to the only person who sees it. const label = resourceLabel(event.resourceType); const existing = byLabel.get(label); if (!existing) { byLabel.set(label, { label, times: 1, from: event.recorded, to: event.recorded }); continue; } existing.times += 1; // Min and max rather than first and last: the feed is ordered, but a range that renders // backwards because one event arrived out of order is a visible defect, and clamping here is // cheaper than trusting the sort. An undefined stamp never becomes a bound. if (event.recorded && (!existing.from || event.recorded < existing.from)) existing.from = event.recorded; if (event.recorded && (!existing.to || event.recorded > existing.to)) existing.to = event.recorded; } return [...byLabel.values()]; }; /** * "twice", not "2 times" - a person reading their own record is not reading a log. * * ONE READ RENDERS NOTHING. "Your profile once" is noise: a thing in this list was looked at, and * saying so twice adds no information. The caller omits the element entirely rather than printing an * empty string, so there is no stray separator left behind. */ export const accessTimesLabel = (times: number): string | undefined => { if (times <= 1) return undefined; if (times === 2) return 'twice'; return `${times} times`; }; /** * HOW MANY THINGS, which is the noun the collapsed row was getting wrong. * * Also the fix for "1 items", which was a bare plural on a screen whose whole claim is that it can be * trusted about detail. */ export const accessThingsLabel = (things: number): string => (things === 1 ? '1 thing' : `${things} things`);