import { useEffect, useRef, useState } from 'react';
import type { Resource } from 'fhir/r4';
import {
type AnchorResolveResult,
type AnchorTemporal,
useAuthServiceContext,
useCareViewAnchorServiceContext,
usePatientServiceContext,
} from '../../services';
import { useAnchorResults } from '../../hooks/useAnchorResults';
import { SnapshotChart, Spinner } from '../PatientSnapshot/SnapshotChart';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { type ExportAuthority, ExportLegalRecordDialog } from './ExportLegalRecordDialog';
import { TamperedBanner } from './TamperedBanner';
import { describeFetchFailure } from '../../utils/fetchFailureMessage';
import { onViewerChange, viewerEpoch } from '../../utils/writeThroughHold';
/** Stable empty references so the pre-resolve render doesn't churn `useAnchorResults`. */
const NO_RESOURCES: Resource[] = [];
const NO_REFS: string[] = [];
interface ExportOpening {
token: number;
anchorId: string;
edition: string;
authority: ExportAuthority;
}
function sameAuthority(a: ExportAuthority, b: ExportAuthority): boolean {
return (
a.viewerId === b.viewerId &&
a.practiceId === b.practiceId &&
a.patientId === b.patientId &&
a.actingAsId === b.actingAsId &&
a.sessionId === b.sessionId &&
a.viewerEpoch === b.viewerEpoch
);
}
interface IntegrityBannerProps {
hashVerified: boolean;
unresolvedRefs: string[];
}
/** Format an ISO instant for display, falling back to the raw value / a neutral phrase. */
function formatInstant(iso?: string): string {
if (!iso) return 'the time of care';
const d = new Date(iso);
return Number.isNaN(d.getTime()) ? iso : d.toLocaleString();
}
/**
* The court-grade integrity notice for a resolved anchor. GREEN when the server re-hash matched,
* RED when it did not (contents don't match the stored hash), plus an AMBER line when some pinned
* versions couldn't be retrieved. Read-only - carries no actions. "Verified" speaks only to the
* stored record and its hash; when the record stands in time is the temporal notice's to say.
*/
function IntegrityBanner({ hashVerified, unresolvedRefs }: IntegrityBannerProps) {
return (
{hashVerified ? (
Verified — this record matches its stored hash.
) : (
)}
{unresolvedRefs.length > 0 && (
{unresolvedRefs.length} pinned item(s) could not be retrieved.
{unresolvedRefs.map((ref) => (
{ref}
))}
)}
);
}
const QUALIFICATION_WORDS: Record = {
'boundary-tie': 'Some records changed at the same instant as the start and are not included.',
'subject-differs-at-boundary': 'Some records belonged to another patient at the start and are not included.',
'history-order-unproved': "Some records' history could not be placed in order and are not included.",
};
/**
* When the record stands, said apart from "Verified". A reconstruction names the visit's start and that which records
* existed then is not proven; an unreadable record claims no time; an anchor that records none says nothing.
*/
export function TemporalNotice({ temporal }: { temporal?: AnchorTemporal }) {
if (!temporal || temporal.state === 'not-recorded') return null;
if (temporal.state === 'unreadable') {
return (
When this record stands could not be read, so no time is claimed for it.
Reconstructed as the record stood at the visit's start, {formatInstant(boundary)}.
Which records existed then is not proven.
{limits.map((line) =>
{line}
)}
{basisStatus === 'unavailable' &&
The agreements in force at the start were not kept with it.
}
);
}
/** Small metadata card describing the anchor (its date, encounter, source). */
function AnchorMeta({ anchor }: { anchor: AnchorResolveResult['anchor'] }) {
return (
Care-view anchor
Anchor date: {formatInstant(anchor.date)}
{anchor.encounter &&
Encounter: {anchor.encounter}
}
{anchor.source &&
Source: {anchor.source}
}
{typeof anchor.resourceCount === 'number' && (
{anchor.resourceCount} pinned item(s)
)}
);
}
interface AnchorViewProps {
anchorId: string;
}
/**
* Route-level, READ-ONLY view of a single care-view anchor. On mount it asks the server to
* resolve the anchor (vread each pinned version + re-verify the hash), then renders the resolved
* record through the shared `SnapshotChart` under an integrity banner. There are ZERO
* create/edit/archive/delete affordances anywhere in this component.
*/
export function AnchorView({ anchorId }: AnchorViewProps) {
const { resolveCareViewAnchor } = useCareViewAnchorServiceContext();
const { currentUser, currentOrganization, actingAs, actingAsSessionRef } = useAuthServiceContext();
const { patientState } = usePatientServiceContext();
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [opening, setOpening] = useState(null);
const openingRef = useRef(null);
const nextOpeningRef = useRef(0);
const viewerId = currentUser?.fhir?.id;
const practiceId = currentOrganization?.id;
const patientId = patientState?.id;
const actingAsId = actingAs?.relatedPersonId;
const edition = result ? `${result.anchor.id ?? anchorId}:${result.anchor.hash ?? ''}` : '';
// What this render shows, written as it runs. An attempt still awaiting holds the functions of the render it
// started in, so they read this rather than their own closure: a later render's patient is what they see.
const renderedRef = useRef({ anchorId, edition, viewerId, practiceId, patientId, actingAsId });
renderedRef.current = { anchorId, edition, viewerId, practiceId, patientId, actingAsId };
// Ref and epoch are read when asked, not copied at render: ids alone can return after a switch.
const liveAuthority = (): ExportAuthority => {
const shown = renderedRef.current;
return {
viewerId: shown.viewerId,
practiceId: shown.practiceId,
patientId: shown.patientId,
actingAsId: shown.actingAsId,
sessionId: actingAsSessionRef?.current,
viewerEpoch: viewerEpoch(),
};
};
const opensForShown = (candidate: ExportOpening): boolean =>
candidate.anchorId === renderedRef.current.anchorId
&& candidate.edition === renderedRef.current.edition
&& sameAuthority(candidate.authority, liveAuthority());
// Retired by the render that no longer shows its identity, before any effect runs; the same ids returning later
// do not revive it.
if (openingRef.current && !opensForShown(openingRef.current)) openingRef.current = null;
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
setResult(null);
resolveCareViewAnchor(anchorId)
.then((res) => {
if (cancelled) return;
setResult(res);
setLoading(false);
})
.catch((err: unknown) => {
if (cancelled) return;
setError(describeFetchFailure(err, 'Could not load this care-view anchor'));
setLoading(false);
});
return () => {
cancelled = true;
};
}, [anchorId, resolveCareViewAnchor]);
// A viewer, practice or anchor change kills the opening before its child can answer.
useEffect(() => {
openingRef.current = null;
setOpening(null);
}, [anchorId, edition, viewerId, practiceId, patientId, actingAsId]);
// Practice/viewer retirement is synchronous, so a retired token cannot revive when its ids return.
useEffect(() => onViewerChange(() => {
if (!openingRef.current) return;
openingRef.current = null;
setOpening(null);
}), []);
useEffect(() => () => {
openingRef.current = null;
}, []);
// Hooks must run unconditionally - feed empty refs until the resolve returns.
const mapped = useAnchorResults(
result?.resolvedResources ?? NO_RESOURCES,
result?.unresolvedRefs ?? NO_REFS,
);
if (loading) {
return (
);
}
if (error || !result) {
return (
Couldn't load this care-view anchor.
{error &&
{error}
}
);
}
const openExport = () => {
const token = nextOpeningRef.current + 1;
nextOpeningRef.current = token;
const next: ExportOpening = {
token,
anchorId,
edition,
authority: liveAuthority(),
};
openingRef.current = next;
setOpening(next);
};
const closeExport = (token: number) => {
if (openingRef.current?.token !== token) return;
openingRef.current = null;
setOpening(null);
};
const isOpeningCurrent = (token: number) => {
const current = openingRef.current;
return Boolean(current && current.token === token && opensForShown(current));
};
// Only the opening the ref still holds is shown: a retired one is never mounted again.
const activeOpening = opening && openingRef.current?.token === opening.token ? opening : null;
return (
>
}
emptyLabel="in this anchor"
/>
{activeOpening && (
)}