mirror of
https://github.com/temetro/temetro.git
synced 2026-07-26 11:58:14 +00:00
0d2494d67a
Expose temetro's own records as a read-only FHIR R4 server at /fhir, authenticated with per-clinic API keys (tmf_… bearer tokens, SHA-256 hashed, shown once). Serves Patient, Observation (labs + vitals), AllergyIntolerance, Condition, MedicationRequest, Encounter and Appointment as text-only CodeableConcepts (temetro stores free-text clinical values); CapabilityStatement at /fhir/metadata (unauth). Searchset Bundles with _count/_offset pagination and self/next/prev links; every request is org-scoped and written to the activity log. Keys are created/revoked under Settings → Integrations (owner/admin). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
110 lines
3.3 KiB
TypeScript
110 lines
3.3 KiB
TypeScript
// Client for the backend integrations API (HL7/FHIR labs, e-prescribing,
|
|
// insurance claims). Config is owner/admin-only to write; status is readable by
|
|
// any member so pages can gate their on-page actions.
|
|
|
|
import { apiFetch } from "@/lib/api-client";
|
|
|
|
export type IntegrationType = "fhir" | "eprescribe" | "claims";
|
|
export type IntegrationStatus = "unconfigured" | "connected" | "error";
|
|
|
|
export type IntegrationConfig = {
|
|
type: IntegrationType;
|
|
endpoint: string;
|
|
enabled: boolean;
|
|
status: IntegrationStatus;
|
|
hasCredentials: boolean;
|
|
lastSyncAt: string | null;
|
|
};
|
|
|
|
export function listIntegrations(): Promise<IntegrationConfig[]> {
|
|
return apiFetch<IntegrationConfig[]>("/api/integrations");
|
|
}
|
|
|
|
export function saveIntegration(
|
|
type: IntegrationType,
|
|
input: { endpoint?: string; enabled?: boolean; credentials?: string },
|
|
): Promise<IntegrationConfig> {
|
|
return apiFetch<IntegrationConfig>(`/api/integrations/${type}`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
export function testIntegration(
|
|
type: IntegrationType,
|
|
): Promise<{ ok: boolean; message: string }> {
|
|
return apiFetch(`/api/integrations/${type}/test`, { method: "POST" });
|
|
}
|
|
|
|
// Pull a patient's lab results from the FHIR server.
|
|
export function syncFhirLabs(fileNumber: string): Promise<{ imported: number }> {
|
|
return apiFetch("/api/integrations/fhir/sync", {
|
|
method: "POST",
|
|
body: JSON.stringify({ fileNumber }),
|
|
});
|
|
}
|
|
|
|
// Transmit a prescription to a pharmacy (NCPDP SCRIPT NewRx).
|
|
export function sendEprescription(
|
|
rxId: string,
|
|
): Promise<{ messageId: string; status: string }> {
|
|
return apiFetch("/api/integrations/eprescribe/send", {
|
|
method: "POST",
|
|
body: JSON.stringify({ rxId }),
|
|
});
|
|
}
|
|
|
|
// Submit an insurance claim for an invoice (X12 837P) and read the remittance.
|
|
export function submitInsuranceClaim(
|
|
invoiceId: string,
|
|
): Promise<{ claimStatus: string; paidAmount: number; submitted: boolean }> {
|
|
return apiFetch("/api/integrations/claims/submit", {
|
|
method: "POST",
|
|
body: JSON.stringify({ invoiceId }),
|
|
});
|
|
}
|
|
|
|
// --- FHIR server API keys (owner/admin only) --------------------------------
|
|
|
|
export type FhirApiKey = {
|
|
id: string;
|
|
name: string;
|
|
createdAt: string;
|
|
lastUsedAt: string | null;
|
|
revoked: boolean;
|
|
};
|
|
|
|
// A freshly created key includes the one-time plaintext secret; it is never
|
|
// returned again.
|
|
export type CreatedFhirApiKey = FhirApiKey & { secret: string };
|
|
|
|
export function listFhirKeys(): Promise<FhirApiKey[]> {
|
|
return apiFetch<FhirApiKey[]>("/api/integrations/fhir-server/keys");
|
|
}
|
|
|
|
export function createFhirKey(name: string): Promise<CreatedFhirApiKey> {
|
|
return apiFetch<CreatedFhirApiKey>("/api/integrations/fhir-server/keys", {
|
|
method: "POST",
|
|
body: JSON.stringify({ name }),
|
|
});
|
|
}
|
|
|
|
export function revokeFhirKey(id: string): Promise<{ revoked: boolean }> {
|
|
return apiFetch(`/api/integrations/fhir-server/keys/${id}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
// Convenience hook-style fetch reused by the on-page sections: returns the
|
|
// config for one type (or null while loading/absent).
|
|
export async function getIntegration(
|
|
type: IntegrationType,
|
|
): Promise<IntegrationConfig | null> {
|
|
try {
|
|
const all = await listIntegrations();
|
|
return all.find((c) => c.type === type) ?? null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|