import type { DiagnosticReport } from "fhir/r4"; /** * What was added to a report after it was signed. * * The server writes this exact URL and the plugin reads it back, so the literal has to match byte for * byte -- the same contract every other Akashic extension the client reads carries. * * A SIGNED REPORT IS NEVER EDITED, so these sit beside the signed words rather than inside them. A * reader who saw the report before an addendum was written saw something still true; the addendum is * what changed since, which is why the sequence matters and the original does not move. */ export const ADDENDUM_EXTENSION_URL = "https://akashichealthrecords.com/fhir/StructureDefinition/report-addendum"; /** Every status a report can hold once somebody has signed it. */ export const SIGNED_REPORT_STATUSES = ["final", "amended", "corrected", "appended"] as const; export const isSignedReport = (report: DiagnosticReport | undefined): boolean => (SIGNED_REPORT_STATUSES as readonly string[]).includes(report?.status ?? ""); /** Not ids: what a reference holds when it was built from values that were never there. */ const NOT_AN_ID = new Set(["", "undefined", "null"]); /** * Who wrote or signed something, as a screen shows them: the name the reference carries, its id when it carries * none, and nobody when the reference is not one. A report once stored its reader as "undefined/undefined", and the * order printed it as a name. */ export const readerOf = (ref: { reference?: string; display?: string } | undefined): string | undefined => { const name = ref?.display?.trim(); if (name) return name; const [type, id, ...rest] = (ref?.reference ?? "").split("/"); if (rest.length || NOT_AN_ID.has(type ?? "") || NOT_AN_ID.has(id ?? "")) return undefined; return id; }; export interface ReportAddendum { text?: string; at?: string; /** The author as a screen shows them: a name when the reference carries one, its id otherwise. */ by?: string; } /** The same addendum as the server hands it back, where the author is still a FHIR reference. */ export interface WireAddendum { text?: string; at?: string; by?: { reference?: string; display?: string }; } export const normaliseAddendum = (addendum: WireAddendum): ReportAddendum => ({ text: addendum.text, at: addendum.at, by: readerOf(addendum.by), }); /** In the order they were written, which is the order somebody came to understand the study. */ export const readAddenda = (report: DiagnosticReport | undefined): ReportAddendum[] => (report?.extension ?? []) .filter((e) => e.url === ADDENDUM_EXTENSION_URL) .map((found) => { const part = (url: string) => (found.extension ?? []).find((e) => e.url === url); return { text: part("text")?.valueString, at: part("at")?.valueDateTime, by: readerOf(part("by")?.valueReference), }; }); export interface SignedReportChanges { mayDelete: boolean; maySave: boolean; /** Said on the page when Save is held back. */ why?: string; } /** * What the generic report editor may do to a report that has been signed. * * A signed report is never deleted and never edited in place: the one sanctioned change is a CORRECTION, * its status moved to corrected or amended with a reason, which is the lab-versions path. Anything else * goes beside it as an addendum. */ export const signedReportChanges = ( loaded: DiagnosticReport | undefined, nextStatus: string | undefined, ): SignedReportChanges => { if (!isSignedReport(loaded)) return { mayDelete: true, maySave: true }; const correcting = nextStatus === "corrected" || nextStatus === "amended"; return correcting ? { mayDelete: false, maySave: true } : { mayDelete: false, maySave: false, why: "This report is signed, so it is not edited here. Correct it by setting its status to Corrected or Amended with a reason, or add an addendum beside it.", }; };