import { Button } from "../../../components/ui/button"; import "./PersonalImports.css"; import { useCallback, useEffect, useRef, useState, type SetStateAction } from 'react'; import { useAuthServiceContext } from '../../../services'; import { authSessionId } from '../../../services/authStorage'; import { environment } from '../../../environments/environment'; import { HttpError } from '../../../services/HttpService'; import { usePersonalRecordsService, personalOperationId, type PersonalImport, type PersonalEventEvidence, type PersonalConnection, type PersonalCandidates, type PersonalAcceptance, type PersonalAcceptanceRequest, type PersonalImportRequest, type PersonalIdentityStop } from '../../../services/PersonalRecordsService'; import { grantedTypes, type RetrievableType } from '../../../services/RecordsConnectionsService'; import { useSaveScope, isSaveRetired, type SaveOperation } from '../../../utils/saveOperation'; import { relayServerFailure } from '../../../utils/fetchFailureMessage'; import type { RecordsCallScope } from '../../../services/RecordsService'; const callScope = (life: SaveOperation): RecordsCallScope => ({ admit: life.admit, signal: life.transport.signal, organizationId: '' }); const words = (value: unknown) => JSON.stringify(value, null, 2) ?? 'Unconfirmed'; const EventDelivery = ({ evidence }: { evidence?: PersonalEventEvidence }) => !evidence ? null :
{'events' in evidence ? <> {evidence.events.map((event, index) =>

{event.step.replace(/-/g, ' ')}: {event.state === 'owed' ? 'awaiting handoff' : `handed off via ${event.transport ?? 'unconfirmed destination'}; storage unconfirmed`}

)} {evidence.more &&

Additional event delivery states have not been returned.

} :

Event delivery status is unconfirmed.

}
; const HOLD_WORDS: Record['code'], string> = { 'identity-conflict': 'This read cannot go ahead: the person in the other system does not match your record here. Check this import operation before starting another.', 'facts-missing': 'This read cannot go ahead: the other system did not return enough about the person to check that it is you. Check this import operation before starting another.', 'reconnect-required': 'This read cannot go ahead until this system is connected again under Your connections. Check this import operation before starting another.', }; const factsWords = (facts: { birthDate: string; names: string[] }) => (facts.names.join(', ') || 'No name returned') + ' · born ' + (facts.birthDate || 'not returned'); type IdentityCapture = { request: PersonalImportRequest; opening: SaveOperation; stop: PersonalIdentityStop }; const PersonalImports = () => { const { currentUser, currentOrganization } = useAuthServiceContext(); const api = usePersonalRecordsService(); const owner = [currentUser?.fhir?.resourceType, currentUser?.fhir?.id, currentOrganization?.id, authSessionId(), environment.sessionPortal, window.location.origin].join('|'); const [imports, setImports] = useState([]); const [connections, setConnections] = useState([]); const [historyCursor, setHistoryCursor] = useState(); const [connectionCursor, setConnectionCursor] = useState(); const [connectionId, setConnectionId] = useState(''); const [types, setTypes] = useState([]); const [supersedes, setSupersedes] = useState(); const [pendingImport, showPendingImport] = useState(); const pendingImportNow = useRef(); const setPendingImport = useCallback((update: SetStateAction) => { const next = typeof update === 'function' ? update(pendingImportNow.current) : update; pendingImportNow.current = next; showPendingImport(next); }, []); const [pendingAcceptance, showPendingAcceptance] = useState<{ operationId: string; acceptanceId: string }>(); const pendingAcceptanceNow = useRef<{ operationId: string; acceptanceId: string }>(); const setPendingAcceptance = useCallback((update: SetStateAction<{ operationId: string; acceptanceId: string } | undefined>) => { const next = typeof update === 'function' ? update(pendingAcceptanceNow.current) : update; pendingAcceptanceNow.current = next; showPendingAcceptance(next); }, []); const [identity, showIdentity] = useState(); const identityNow = useRef(); const setIdentity = useCallback((next: IdentityCapture | undefined) => { identityNow.current = next; showIdentity(next); }, []); const [review, setReview] = useState(); const reviewNow = useRef(review); reviewNow.current = review; const [decisions, setDecisions] = useState>({}); const [reviewed, setReviewed] = useState(false); const [acceptances, setAcceptances] = useState([]); const [acceptanceCursor, setAcceptanceCursor] = useState(); const [original, setOriginal] = useState<{ operationId: string; body: unknown }>(); const [busy, setBusy] = useState(); const [error, setError] = useState(); const [notice, setNotice] = useState(); const serial = useRef(0); const clear = useCallback(() => { serial.current += 1; setImports([]); setConnections([]); setHistoryCursor(undefined); setConnectionCursor(undefined); setConnectionId(''); setTypes([]); setSupersedes(undefined); setPendingImport(undefined); setPendingAcceptance(undefined); setIdentity(undefined); setReview(undefined); setDecisions({}); setReviewed(false); setAcceptances([]); setAcceptanceCursor(undefined); setOriginal(undefined); setBusy(undefined); setError(undefined); setNotice(undefined); }, [setPendingImport, setPendingAcceptance, setIdentity]); const lives = useSaveScope(owner, clear); useEffect(clear, [owner, clear]); const perform = useCallback(async (label: string, work: (life: SaveOperation) => Promise) => { const mine = ++serial.current; const life = lives.start(); setBusy(label); setError(undefined); try { await work(life); } catch (failure) { if (!isSaveRetired(failure) && life.belongs() && mine === serial.current) setError(relayServerFailure(failure, 'This step is unconfirmed. Check its stored status before doing anything again.')); } finally { if (life.belongs() && mine === serial.current) setBusy(undefined); } }, [lives]); const install = (next: PersonalImport) => setImports(now => { const previous = now.find(row => row.operationId === next.operationId); if (previous && previous.rowVersion > next.rowVersion) return now; return [next, ...now.filter(row => row.operationId !== next.operationId)]; }); const history = useCallback((cursor?: string) => perform('Loading import history', async life => { const page = await life.step(() => api.imports(callScope(life), cursor)); if (!Array.isArray(page.imports)) throw new Error('Import history is unavailable'); setImports(now => { const byId = new Map((cursor ? now : []).map(row => [row.operationId, row])); for (const row of page.imports) { const previous = now.find(old => old.operationId === row.operationId); byId.set(row.operationId, previous && previous.rowVersion > row.rowVersion ? previous : row); } return [...byId.values()]; }); setHistoryCursor(page.cursor); }), [api, perform]); useEffect(() => { void history(); const life = lives.start(); void life.step(() => api.connections(callScope(life))).then(page => { if (life.belongs()) { setConnections(page.connections); setConnectionCursor(page.cursor); } }).catch(failure => { if (!isSaveRetired(failure) && life.belongs()) setError('Connections for import could not be loaded.'); }); return () => life.retire(); }, [api, history, lives, owner]); const inspectImport = (operationId: string) => perform('Checking import', async life => { const row = await life.step(() => api.importStatus(operationId, callScope(life))); install(row); if (['attached', 'saved', 'chart-pending', 'refused', 'superseded'].includes(row.state)) setPendingImport(now => now === operationId ? undefined : now); setNotice('Stored import status: ' + row.state + '. This check does not read the other system.'); }); const observeRead = async (life: SaveOperation, operationId: string) => { const row = await life.step(() => api.importStatus(operationId, callScope(life))); install(row); const ownsPending = pendingImportNow.current === operationId; if (['attached', 'saved', 'chart-pending', 'refused', 'superseded'].includes(row.state)) setPendingImport(now => now === operationId ? undefined : now); if (ownsPending) setSupersedes(undefined); setNotice('Read outcome observed. Retaining an original does not confirm chart entries or evidence delivery.'); }; const holdIdentity = (capture: IdentityCapture) => { if (!capture.opening.belongs() || pendingImportNow.current !== capture.request.operationId) return; setIdentity(capture); }; const start = () => { if (!connectionId || !types.length || pendingImport) return; const operationId = personalOperationId(); const request: PersonalImportRequest = { operationId, connectionId, types: [...types], ...(supersedes ? { supersedes: { operationId: supersedes.operationId, expectedVersion: supersedes.rowVersion } } : {}) }; setPendingImport(operationId); setIdentity(undefined); void perform('Reading selected records', async life => { const answer = await life.step(() => api.startImport(request, callScope(life)), true); if (answer.kind !== 'started') return holdIdentity({ request, opening: life, stop: answer }); await observeRead(life, operationId); }); }; const confirmIdentity = () => { const capture = identityNow.current; if (!capture || capture.stop.kind !== 'review' || !capture.opening.belongs() || pendingImportNow.current !== capture.request.operationId) return; const { review: shown, code } = capture.stop; const link = { evidenceDigest: shown.evidenceDigest, confirm: code === 'link-required' ? 'self' : 'self-name-reviewed' } as const; void perform('Confirming these records are yours', async life => { const answer = await life.step(() => api.startImport({ ...capture.request, link }, callScope(life)), true); if (identityNow.current !== capture || !capture.opening.belongs()) return; if (answer.kind !== 'started') { if (answer.requestDigest !== capture.stop.requestDigest) throw new Error('The identity answer no longer matches this read'); return holdIdentity({ ...capture, stop: answer }); } setIdentity(undefined); await observeRead(life, capture.request.operationId); }); }; const loadReview = (operationId: string, cursor?: string) => perform('Loading chart review', async life => { const [page, previous] = await life.step(() => Promise.all([api.candidates(operationId, callScope(life), cursor), api.acceptances(operationId, callScope(life))])); setReview(page); setDecisions({}); setReviewed(false); setOriginal(undefined); setAcceptances(previous.acceptances); setAcceptanceCursor(previous.cursor); }); const choose = (key: string, decision: string) => setDecisions(now => { const { [key]: _previous, ...rest } = now; return decision === 'accept' || decision === 'decline' ? { ...rest, [key]: decision } : rest; }); const settlePendingAcceptance = (stored: PersonalAcceptance) => { const pending = pendingAcceptanceNow.current; if (!pending || pending.operationId !== stored.operationId || pending.acceptanceId !== stored.acceptanceId || stored.items.some(item => item.phase === 'write-claimed')) return false; setPendingAcceptance(now => now?.operationId === stored.operationId && now.acceptanceId === stored.acceptanceId ? undefined : now); return true; }; const accept = () => { if (!review || !reviewed || pendingAcceptance) return; const items = review.candidates.flatMap(item => { const decision = decisions[item.sourceIdentity + '#' + item.sourceEdition]; return decision ? [{ sourceIdentity: item.sourceIdentity, sourceEdition: item.sourceEdition, proposalDigest: item.proposalDigest, decision, expectedLocalCopies: item.priorCopies.map(copy => ({ resource: copy.resource, version: copy.version })) }] : []; }); if (!items.length) return; const acceptanceId = personalOperationId(), operationId = review.operationId; const body: PersonalAcceptanceRequest = { acceptanceId, expectedImportVersion: review.importVersion, originalVersionId: review.originalVersionId, originalSha256: review.originalSha256, items }; setPendingAcceptance({ operationId, acceptanceId }); void perform('Recording chart decisions', async life => { try { await life.step(() => api.accept(operationId, body, callScope(life)), true); } catch (failure) { if (failure instanceof HttpError && [400, 403, 404, 409].includes(failure.status)) setPendingAcceptance(now => now?.operationId === operationId && now.acceptanceId === acceptanceId ? undefined : now); throw failure; } const stored = await life.step(() => api.acceptanceStatus(operationId, acceptanceId, callScope(life))); setAcceptances(now => [stored, ...now.filter(row => row.acceptanceId !== stored.acceptanceId)]); if (settlePendingAcceptance(stored) && reviewNow.current === review) { setDecisions({}); setReviewed(false); } setNotice('Stored decisions observed. Each item and its evidence handoff have their own outcome below.'); }); }; const checkAcceptance = (operationId: string, acceptanceId: string) => perform('Checking chart decisions', async life => { const stored = await life.step(() => api.acceptanceStatus(operationId, acceptanceId, callScope(life))); setAcceptances(now => [stored, ...now.filter(row => row.acceptanceId !== stored.acceptanceId)]); settlePendingAcceptance(stored); }); const readOriginal = (operationId: string) => perform('Reading retained original', async life => { const body = await life.step(() => api.original(operationId, callScope(life))); setOriginal({ operationId, body }); }); const selected = connections.find(row => row.connectionId === connectionId); const offeredTypes = selected ? grantedTypes(selected.grantedScopes) : []; return

Read your records from a connected system

Choose what to read, then review the retained original before adding chart entries. Importing medication requests records what the other system said; it does not prescribe, send an order, or settle diagnostic work.

{error &&

{error}

}{notice &&

{notice}

}{busy &&

{busy}…

} {connectionCursor && }
Record types to read{offeredTypes.map(type => )}
{supersedes &&

This new read explicitly supersedes {supersedes.operationId} at version {supersedes.rowVersion}. Its original and history remain retained.

} {identity?.stop.kind === 'review' && identity.request.operationId === pendingImport &&

Confirm these records are yours

The other system names this person: {factsWords(identity.stop.review.source)}

Your record here: {factsWords(identity.stop.review.local)}

{identity.stop.code === 'name-review' &&

The names differ. Confirm only if both are you.

}
} {identity?.stop.kind === 'hold' && identity.request.operationId === pendingImport &&

{HOLD_WORDS[identity.stop.code]}

} {pendingImport &&

Import operation {pendingImport} is not yet settled here.

}

Import history

{imports.length === 0 && !busy &&

No import history has been returned.

}
    {imports.map(row =>
  • {row.operationId} — {row.state}, version {row.rowVersion}

    {row.actor} · {row.at}

    Original: {row.original.status}{row.original.sha256 ? ' · SHA256 ' + row.original.sha256 : ''}

    Evidence handoff: {words(row.evidence)}. Handoff is not confirmation that evidence has been stored.

    {row.supersededBy &&

    Superseded by {row.supersededBy}; this history is retained.

    } {row.summary &&
    Read summary
    {words(row.summary)}
    } {row.original.status === 'attached' && <> } {!row.supersededBy && row.rowVersion > 0 && row.original.status === 'unconfirmed' && ['reading', 'read-unconfirmed', 'original-pending', 'original-unconfirmed'].includes(row.state) && }
  • )}
{historyCursor && } {original &&
Retained original for {original.operationId}
{words(original.body)}
} {review &&

Review chart candidates

Original version {review.originalVersionId} · SHA256 {review.originalSha256} · import version {review.importVersion} · mapping {review.mapping}

    {review.candidates.map(item =>
  • {item.kind}: {item.sourceIdentity} · edition {item.sourceEdition}

    {words(item.fields)}
    {item.unavailable &&

    Held: {item.unavailable}. The original remains available.

    }

    Existing local copies reviewed: {item.priorCopies.length ? item.priorCopies.map(copy => copy.resource + ' version ' + copy.version + (copy.sameEdition ? ' (same source edition)' : '')).join(', ') : 'none returned'}

  • )}
Items not offered for chart entry
{words(review.skipped)}
{review.cursor && }
} {pendingAcceptance && } {review &&

Chart decision history

    {acceptances.map(row =>
  • {row.acceptanceId} · original actor {row.actor} · {row.at} · receipt version {row.rowVersion}

      {row.items.map(item =>
    • {item.kind} {item.sourceIdentity} · {item.sourceEdition} · {item.decision}: {item.phase} {item.reason && · {item.reason}}{item.proof && · Proved {item.proof.resource}/_history/{item.proof.versionId}} {item.existing !== undefined &&
      Existing source copy: {words(item.existing)}
      }{item.evidence !== undefined &&
      Evidence handoff: {words(item.evidence)}; stored evidence is not confirmed here.
      }
    • )}
  • )}
{acceptanceCursor && }
}
; }; export default PersonalImports;