import { ReactNode, createContext, useCallback, useContext } from 'react'; import { environment } from '../environments/environment'; import { HttpError, useHttpService } from './HttpService'; import type { SaveTransport } from '../utils/saveOperation'; import type { PatientNoteDraft } from '../components/PatientNotes/patientNoteDraftStore'; const API_URL = environment.apiUrl; export interface PatientNoteDraftToken { incarnation: string; version: number; } export type PatientNoteDraftTransport = SaveTransport & { organizationId?: string }; export interface StoredPatientNoteDraft extends PatientNoteDraftToken { patientId: string; authorPractitionerId: string; draft: PatientNoteDraft; createdAtMs: number; updatedAtMs: number; } export type PatientNoteDraftSaveOutcome = | { status: 'saved'; incarnation: string; version: number; updatedAtMs: number } | { status: 'conflict'; current?: StoredPatientNoteDraft } | { status: 'failed'; tooLarge: boolean }; export type PatientNoteDraftCleanupOutcome = | { status: 'removed' | 'already-absent' | 'conflict' | 'unknown' } | { status: 'preserved'; current: PatientNoteDraftToken & { draft: PatientNoteDraft } }; export interface PatientNoteDraftApi { getPatientNoteDraft: (patientId: string, transport?: PatientNoteDraftTransport) => Promise; savePatientNoteDraft: (input: { patientId: string; draft: PatientNoteDraft; version: number; incarnation?: string }, transport?: PatientNoteDraftTransport) => Promise; deletePatientNoteDraft: (patientId: string, token: PatientNoteDraftToken, transport?: PatientNoteDraftTransport) => Promise; } const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; const record = (value: unknown): value is Record => Boolean(value && typeof value === 'object' && !Array.isArray(value)); const tokenOf = (value: unknown): value is PatientNoteDraftToken => record(value) && typeof value.incarnation === 'string' && UUID.test(value.incarnation) && Number.isSafeInteger(value.version) && Number(value.version) > 0; const visibility = (value: unknown) => record(value) && ['', 'me-only', 'all-staff', 'roles'].includes(String(value.mode)) && Array.isArray(value.categoryLabels) && value.categoryLabels.every(label => typeof label === 'string'); const draftOf = (value: unknown): value is PatientNoteDraft => record(value) && typeof value.newBody === 'string' && typeof value.editBody === 'string' && (value.editingId === undefined || typeof value.editingId === 'string') && visibility(value.newVisibility) && visibility(value.editVisibility); const PatientNoteDraftContext = createContext(undefined); export const usePatientNoteDraftService = (): PatientNoteDraftApi => { const ctx = useContext(PatientNoteDraftContext); if (!ctx) throw new Error('usePatientNoteDraftService must be used within a PatientNoteDraftProvider'); return ctx; }; export const useOptionalPatientNoteDraftService = (): PatientNoteDraftApi | undefined => useContext(PatientNoteDraftContext); export const PatientNoteDraftProvider = ({ children }: { children: ReactNode }) => { const { get, put, delete: del } = useHttpService(); const getPatientNoteDraft = useCallback(async (patientId: string, transport?: PatientNoteDraftTransport): Promise => { try { const result = await get(`${API_URL}/patient-note-draft?patient=${encodeURIComponent(patientId)}`, transport); transport?.admit(); if (!tokenOf(result) || !record(result) || result.patientId !== patientId || typeof result.authorPractitionerId !== 'string' || !draftOf(result.draft) || typeof result.createdAtMs !== 'number' || !Number.isFinite(result.createdAtMs) || typeof result.updatedAtMs !== 'number' || !Number.isFinite(result.updatedAtMs)) throw new Error('The private note draft could not be read.'); return result as unknown as StoredPatientNoteDraft; } catch (error) { transport?.admit(); if (error instanceof HttpError && error.status === 404) return null; throw error; } }, [get]); const savePatientNoteDraft = useCallback(async (input: { patientId: string; draft: PatientNoteDraft; version: number; incarnation?: string }, transport?: PatientNoteDraftTransport): Promise => { try { const result = await put(`${API_URL}/patient-note-draft`, input, transport); transport?.admit(); if (!tokenOf(result) || !record(result) || typeof result.updatedAtMs !== 'number' || !Number.isFinite(result.updatedAtMs)) return { status: 'failed', tooLarge: false }; return { status: 'saved', incarnation: result.incarnation, version: result.version, updatedAtMs: result.updatedAtMs as number }; } catch (error) { transport?.admit(); if (error instanceof HttpError && error.status === 409) return { status: 'conflict' }; return { status: 'failed', tooLarge: error instanceof HttpError && error.status === 413 }; } }, [put]); const deletePatientNoteDraft = useCallback(async (patientId: string, token: PatientNoteDraftToken, transport?: PatientNoteDraftTransport): Promise => { if (!tokenOf(token)) return { status: 'unknown' }; try { const query = new URLSearchParams({ patient: patientId, incarnation: token.incarnation, version: String(token.version) }); const result = await del(`${API_URL}/patient-note-draft?${query}`, transport); transport?.admit(); if (!record(result)) return { status: 'unknown' }; if (result.status === 'removed' || result.status === 'already-absent') return { status: result.status }; if (result.status === 'preserved' && tokenOf(result.current) && record(result.current) && draftOf(result.current.draft)) { return { status: 'preserved', current: { incarnation: result.current.incarnation, version: result.current.version, draft: result.current.draft } }; } return { status: 'unknown' }; } catch (error) { transport?.admit(); return { status: error instanceof HttpError && error.status === 409 ? 'conflict' : 'unknown' }; } }, [del]); return {children}; };