import { useCallback, useRef } from 'react'; import type { DocumentReference } from 'fhir/r4'; import { environment } from '../environments/environment'; import type { SaveTransport } from '../utils/saveOperation'; const API_URL = environment.fhirFoundationApi; export const FILING_SYSTEM = 'https://akashichealthrecords.com/fhir/NamingSystem/filing-operation'; const STORE = 'akashic.documentFilings'; export type FilingTarget = | { kind: 'create'; purpose: 'practice-document' } | { kind: 'create'; purpose: 'document'; patientId: string } | { kind: 'amendment'; documentId: string; baseVersion: string }; export type FilingUpload = { attemptId: string; slotId: string; observed?: 'not-observed' | 'uploaded' | 'held' | 'not-issued' | 'unknown' }; export type FilingState = { filingId: string; state: 'prepared' | 'abandoned' | 'sealed' | 'filed' | 'superseded'; evidence?: 'owed' | 'settled'; uploadCustody: boolean; uploads: FilingUpload[] }; export type FilingOwner = { viewer: string; practice: string }; type Remembered = { filingId: string } & FilingOwner; type Get = (url: string, options?: object) => Promise; type Post = (url: string, body: unknown, options?: object) => Promise; type Send = (uploadUrl: string, file: File, transport?: Partial, headers?: Record) => Promise; const readStore = (): Remembered[] => { try { const parsed = JSON.parse(sessionStorage.getItem(STORE) ?? '[]'); return Array.isArray(parsed) ? parsed : []; } catch { return []; } }; const writeStore = (rows: Remembered[]) => { try { sessionStorage.setItem(STORE, JSON.stringify(rows)); } catch { /* the server list still finds it */ } }; /** Kept before the first request about it, so a closed form or a reload still finds the filing by its id. */ export const rememberFiling = (row: Remembered) => { const rows = readStore(); if (!rows.some((each) => each.filingId === row.filingId)) writeStore([...rows, row]); }; export const forgetFiling = (filingId: string) => writeStore(readStore().filter((each) => each.filingId !== filingId)); export const rememberedFilings = (viewer: string, practice: string): string[] => readStore().filter((each) => each.viewer === viewer && each.practice === practice).map((each) => each.filingId); const states = new Set(['prepared', 'abandoned', 'sealed', 'filed', 'superseded']); const observations = new Set(['not-observed', 'uploaded', 'held', 'not-issued', 'unknown']); /** * Unreadable or foreign recovery answers cannot settle or clear the filing that was asked about. The server always * answers custody; a reply without it says nothing about held uploads, so it is unreadable rather than taken as none. */ const readState = (value: unknown, asked?: string): FilingState => { if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Document filing could not be read'); const row = value as Record; const valid = typeof row.filingId === 'string' && row.filingId.length > 0 && (!asked || row.filingId === asked) && typeof row.state === 'string' && states.has(row.state) && (row.evidence === undefined || row.evidence === 'owed' || row.evidence === 'settled') && typeof row.uploadCustody === 'boolean' && Array.isArray(row.uploads) && row.uploads.every((upload) => upload && typeof upload === 'object' && typeof upload.attemptId === 'string' && upload.attemptId.length > 0 && typeof upload.slotId === 'string' && upload.slotId.length > 0 && (upload.observed === undefined || observations.has(upload.observed))); if (!valid) throw new Error('Document filing could not be read'); return value as FilingState; }; /** The caller's document filings on the server: prepare, issue an upload and send it, check, list, resume, abandon. */ export const filingApi = (get: Get, post: Post, send: Send) => { const base = `${API_URL}/DocumentReference/filings`; return { prepare: (filingId: string, target: FilingTarget, transport?: Partial) => post(`${base}/${filingId}`, target, { ...transport }), issue: (filingId: string, body: { attemptId: string; slotId: string; contentType: string; filename: string }, transport?: Partial) => post<{ attemptId: string; key: string; uploadUrl: string; headers: Record }>(`${base}/${filingId}/uploads`, body, { ...transport }), send, check: async (filingId: string, transport?: Partial) => readState(await get(`${base}/${filingId}`, { ...transport }), filingId), list: async (transport?: Partial) => { const answer = await get<{ filings: FilingState[] }>(base, { ...transport }); if (!Array.isArray(answer?.filings)) throw new Error('Document filings could not be read'); return answer.filings.map((row) => readState(row)); }, abandon: async (filingId: string, transport?: Partial) => readState(await post(`${base}/${filingId}/abandon`, undefined, { ...transport }), filingId), resume: async (filingId: string, transport?: Partial) => readState(await post(`${base}/${filingId}/resume`, undefined, { ...transport }), filingId), }; }; export type FilingApi = ReturnType; /** The viewer and practice a remembered filing belongs to; empty until both are known. */ export const filingOwnerOf = (auth: { currentUser?: unknown; wpOrganization?: { current?: { id?: string } | null } }): FilingOwner => { const fhir = (auth.currentUser as { fhir?: { resourceType?: string; id?: string } } | null | undefined)?.fhir; return { viewer: fhir?.id ? `${fhir.resourceType}/${fhir.id}` : '', practice: auth.wpOrganization?.current?.id ?? '' }; }; /** The document under its filing: the server alone names the filing a record was filed through. */ export const underFiling = (document: DocumentReference, filingId?: string): DocumentReference => (filingId ? { ...document, identifier: [...(document.identifier ?? []).filter((each) => each.system !== FILING_SYSTEM), { system: FILING_SYSTEM, value: filingId }] } : document); /** * One filing per editable opening. Its id is minted when the opening starts and remembered before anything is sent; * the filing is prepared on the first file picked, and every pick is a new attempt in its slot under that filing. A * save sends the filing only once something was issued under it. */ export const useDocumentFiling = (target: FilingTarget | undefined, opening: unknown, api: FilingApi | undefined, owner: FilingOwner) => { const ownerKey = JSON.stringify([owner.viewer, owner.practice]); const state = useRef<{ opening: unknown; ownerKey: string; filingId: string; prepared?: Promise; issued: boolean }>(); if (!state.current || state.current.opening !== opening || state.current.ownerKey !== ownerKey) state.current = { opening, ownerKey, filingId: crypto.randomUUID(), issued: false }; const upload = useCallback(async (file: File, slotId: string, transport?: Partial): Promise<{ key: string }> => { const current = state.current!; if (!target || !api) throw new Error('Nothing to file this document under'); transport?.admit?.(); rememberFiling({ filingId: current.filingId, viewer: owner.viewer, practice: owner.practice }); current.prepared ??= api.prepare(current.filingId, target, transport).catch((error) => { current.prepared = undefined; throw error; }); await current.prepared; transport?.admit?.(); const issued = await api.issue(current.filingId, { attemptId: crypto.randomUUID(), slotId, contentType: file.type, filename: file.name }, transport); current.issued = true; await api.send(issued.uploadUrl, file, transport, issued.headers); return { key: issued.key }; }, [api, owner.practice, owner.viewer, target]); /** The filing this opening's save goes under, if anything was issued under it. */ const filingId = useCallback(() => (state.current?.issued ? state.current.filingId : undefined), []); return { upload, filingId }; };