"use client"; import { CheckCircle2, CircleDashed, Copy, KeyRound, Trash2, XCircle } from "lucide-react"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { FieldLabel, SettingsCard, SettingsSection, } from "@/components/settings/settings-parts"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { API_BASE_URL } from "@/lib/api-client"; import { createFhirKey, type FhirApiKey, type IntegrationConfig, type IntegrationType, listFhirKeys, listIntegrations, revokeFhirKey, saveIntegration, testIntegration, } from "@/lib/integrations"; import { notify } from "@/lib/toast"; const TYPES: IntegrationType[] = ["fhir", "eprescribe", "claims"]; function StatusBadge({ config }: { config: IntegrationConfig }) { const { t } = useTranslation(); if (config.status === "connected") { return ( {t("settings.integrations.status.connected")} ); } if (config.status === "error") { return ( {t("settings.integrations.status.error")} ); } return ( {t("settings.integrations.status.unconfigured")} ); } function IntegrationCard({ type, initial, }: { type: IntegrationType; initial: IntegrationConfig; }) { const { t } = useTranslation(); const [endpoint, setEndpoint] = useState(initial.endpoint); const [enabled, setEnabled] = useState(initial.enabled); // Empty = leave the stored secret untouched; typing replaces it. const [credentials, setCredentials] = useState(""); const [config, setConfig] = useState(initial); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); const dirty = endpoint !== config.endpoint || enabled !== config.enabled || credentials.length > 0; const save = async () => { setSaving(true); try { const saved = await saveIntegration(type, { endpoint, enabled, ...(credentials ? { credentials } : {}), }); setConfig(saved); setEndpoint(saved.endpoint); setEnabled(saved.enabled); setCredentials(""); notify.success( t("settings.integrations.savedTitle"), t(`settings.integrations.${type}.title`), ); } catch { notify.error( t("settings.integrations.saveFailedTitle"), t("settings.integrations.saveFailedBody"), ); } finally { setSaving(false); } }; const test = async () => { setTesting(true); try { const result = await testIntegration(type); if (result.ok) { notify.success(t("settings.integrations.testOk"), result.message); } else { notify.error(t("settings.integrations.testFailed"), result.message); } } catch { notify.error( t("settings.integrations.testFailed"), t("settings.integrations.testError"), ); } finally { setTesting(false); } }; return ( } description={t(`settings.integrations.${type}.description`)} title={t(`settings.integrations.${type}.title`)} >
{t("settings.integrations.endpoint")} setEndpoint(e.target.value)} placeholder={t(`settings.integrations.${type}.endpointPlaceholder`)} value={endpoint} />
{t("settings.integrations.credentials")} setCredentials(e.target.value)} placeholder={ config.hasCredentials ? t("settings.integrations.credentialsSet") : t(`settings.integrations.${type}.credentialsPlaceholder`) } type="password" value={credentials} />

{t(`settings.integrations.${type}.credentialsHint`)}

{config.lastSyncAt ? ( {t("settings.integrations.lastSync", { when: new Date(config.lastSyncAt).toLocaleString(), })} ) : null}
); } // The read-only FHIR R4 server. Unlike the integration cards above (which make // temetro a FHIR *client*), this exposes temetro's own records over `/fhir` to // external systems, authenticated with per-clinic API keys. Owner/admin only: // the component self-gates by hiding when the keys fetch is forbidden. function FhirServerCard() { const { t } = useTranslation(); const [keys, setKeys] = useState(null); const [allowed, setAllowed] = useState(true); const [name, setName] = useState(""); const [creating, setCreating] = useState(false); const [freshSecret, setFreshSecret] = useState(null); const [confirmRevoke, setConfirmRevoke] = useState(null); const baseUrl = `${API_BASE_URL}/fhir`; useEffect(() => { let active = true; listFhirKeys() .then((rows) => active && setKeys(rows)) .catch(() => { if (active) { setAllowed(false); setKeys([]); } }); return () => { active = false; }; }, []); const create = async () => { if (!name.trim() || creating) return; setCreating(true); try { const created = await createFhirKey(name.trim()); setFreshSecret(created.secret); setKeys((prev) => [created, ...(prev ?? [])]); setName(""); } catch { notify.error( t("settings.integrations.fhirServer.createFailed"), t("settings.integrations.fhirServer.createFailedBody"), ); } finally { setCreating(false); } }; const revoke = async (id: string) => { try { await revokeFhirKey(id); setKeys((prev) => (prev ?? []).map((k) => (k.id === id ? { ...k, revoked: true } : k)), ); } catch { notify.error( t("settings.integrations.fhirServer.revokeFailed"), t("settings.integrations.fhirServer.revokeFailedBody"), ); } finally { setConfirmRevoke(null); } }; const copy = async (text: string, label: string) => { try { await navigator.clipboard.writeText(text); notify.success(label, ""); } catch { // Clipboard blocked — no-op; the value is visible for manual copy. } }; if (!allowed) return null; return (
{t("settings.integrations.fhirServer.baseUrl")}

{t("settings.integrations.fhirServer.baseUrlHint")}

{freshSecret ? (

{t("settings.integrations.fhirServer.secretTitle")}

{t("settings.integrations.fhirServer.secretHint")}

{freshSecret}
) : null}
{t("settings.integrations.fhirServer.newKey")}
setName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && create()} placeholder={t("settings.integrations.fhirServer.newKeyPlaceholder")} value={name} />
{keys && keys.length > 0 ? (
    {keys.map((k) => (
  • {k.name}

    {k.lastUsedAt ? t("settings.integrations.fhirServer.lastUsed", { when: new Date(k.lastUsedAt).toLocaleString(), }) : t("settings.integrations.fhirServer.neverUsed")}

    {k.revoked ? ( {t("settings.integrations.fhirServer.revoked")} ) : confirmRevoke === k.id ? (
    ) : ( )}
  • ))}
) : (

{t("settings.integrations.fhirServer.noKeys")}

)}
); } export function IntegrationsPanel() { const { t } = useTranslation(); const [configs, setConfigs] = useState(null); useEffect(() => { let active = true; listIntegrations() .then((rows) => active && setConfigs(rows)) .catch(() => active && setConfigs([])); return () => { active = false; }; }, []); if (configs === null) { return (

{t("settings.integrations.loading")}

); } return (

{t("settings.integrations.intro")}

{TYPES.map((type) => { const initial = configs.find((c) => c.type === type) ?? ({ type, endpoint: "", enabled: false, status: "unconfigured", hasCredentials: false, lastSyncAt: null, } satisfies IntegrationConfig); return ; })}
); }