From 76da310766d3ab063304ecfb715fb21af5cbe9b4 Mon Sep 17 00:00:00 2001 From: Khalid Abdi Date: Tue, 14 Jul 2026 21:10:04 +0300 Subject: [PATCH] frontend: fix wallet push silently skipping in form dialogs The "send to wallet" step in the appointment/invoice/prescription/ patient-edit/scribe dialogs was gated on sync.linked, which resolves asynchronously via getWalletLink in an effect. A fast save (or a transient failure that collapsed it to false) left a wallet-linked patient looking unlinked, so the dialog took the else branch and pushed nothing. - Add ensureLinked() to useWalletSync: awaits getWalletLink at submit and returns the resolved status. Each of the 5 dialogs now awaits it before deciding whether to show the wallet step. - Harden push() to drop empty/whitespace changes (the backend 400s on an empty change set) and add a translated summaryFallback so the step never sends an empty summary. Co-Authored-By: Claude Opus 4.8 --- .../appointments/add-appointment-dialog.tsx | 2 +- .../components/chat/patient-form-dialog.tsx | 2 +- .../invoices/invoice-form-dialog.tsx | 2 +- .../components/patients/scribe-dialog.tsx | 2 +- .../prescriptions/add-prescription-dialog.tsx | 2 +- frontend/components/wallet/use-wallet-sync.ts | 34 +++++++++++++++++-- .../components/wallet/wallet-sync-step.tsx | 7 ++-- frontend/lib/i18n/locales/ar/translation.json | 3 +- frontend/lib/i18n/locales/de/translation.json | 3 +- frontend/lib/i18n/locales/en/translation.json | 3 +- frontend/lib/i18n/locales/fr/translation.json | 3 +- frontend/lib/i18n/locales/so/translation.json | 3 +- 12 files changed, 52 insertions(+), 14 deletions(-) diff --git a/frontend/components/appointments/add-appointment-dialog.tsx b/frontend/components/appointments/add-appointment-dialog.tsx index a479cd2..4512f12 100644 --- a/frontend/components/appointments/add-appointment-dialog.tsx +++ b/frontend/components/appointments/add-appointment-dialog.tsx @@ -195,7 +195,7 @@ export function AddAppointmentDialog({ t("appointments.dialog.addedTitle"), `${selected.name} · ${time}`, ); - if (sync.linked) { + if (await sync.ensureLinked()) { setWalletSummary( t("walletSync.summary.appointment", { date: keyOf(date), time }), ); diff --git a/frontend/components/chat/patient-form-dialog.tsx b/frontend/components/chat/patient-form-dialog.tsx index c7614ac..7c40b75 100644 --- a/frontend/components/chat/patient-form-dialog.tsx +++ b/frontend/components/chat/patient-form-dialog.tsx @@ -396,7 +396,7 @@ export function PatientFormDialog({ t("patientForm.updatedTitle"), t("patientForm.updatedBody", { name: saved.name }), ); - if (sync.linked) { + if (await sync.ensureLinked()) { setStep("wallet"); return; } diff --git a/frontend/components/invoices/invoice-form-dialog.tsx b/frontend/components/invoices/invoice-form-dialog.tsx index 68b2d64..d8b1a87 100644 --- a/frontend/components/invoices/invoice-form-dialog.tsx +++ b/frontend/components/invoices/invoice-form-dialog.tsx @@ -273,7 +273,7 @@ export function InvoiceFormDialog({ }) : await createInvoice(payload); onSaved(saved); - if (sync.linked) { + if (await sync.ensureLinked()) { setWalletSummary( mode === "edit" ? t("walletSync.summary.invoiceUpdated", { number: saved.number }) diff --git a/frontend/components/patients/scribe-dialog.tsx b/frontend/components/patients/scribe-dialog.tsx index c06153e..0a42280 100644 --- a/frontend/components/patients/scribe-dialog.tsx +++ b/frontend/components/patients/scribe-dialog.tsx @@ -233,7 +233,7 @@ export function ScribeDialog({ const updated = await saveNote(patient.fileNumber, draft); notify.success(t("scribe.saved.title"), patient.name); onSaved(updated); - if (sync.linked) { + if (await sync.ensureLinked()) { setPhase("review"); setWalletStep(true); } else { diff --git a/frontend/components/prescriptions/add-prescription-dialog.tsx b/frontend/components/prescriptions/add-prescription-dialog.tsx index c29754f..3b66835 100644 --- a/frontend/components/prescriptions/add-prescription-dialog.tsx +++ b/frontend/components/prescriptions/add-prescription-dialog.tsx @@ -347,7 +347,7 @@ export function AddPrescriptionDialog({ t("prescriptions.dialog.addedTitle"), `${medication.trim()} · ${selected.name}`, ); - if (sync.linked) { + if (await sync.ensureLinked()) { setWalletSummary( t("walletSync.summary.prescription", { drug: medication.trim() }), ); diff --git a/frontend/components/wallet/use-wallet-sync.ts b/frontend/components/wallet/use-wallet-sync.ts index 07c7849..80be67a 100644 --- a/frontend/components/wallet/use-wallet-sync.ts +++ b/frontend/components/wallet/use-wallet-sync.ts @@ -74,13 +74,43 @@ export function useWalletSync(fileNumber: string | null | undefined) { }; }, [state, update]); + // Authoritatively resolve link status at submit time. The `linked` state above + // is populated asynchronously by the effect, so a fast save (or a transient + // failure that collapsed it to false) can leave a wallet-backed patient looking + // unlinked. Callers await this before deciding whether to show the wallet step, + // so the decision is never made on an unresolved check. + const ensureLinked = useCallback(async (): Promise => { + if (!fileNumber) { + setLinked(false); + return false; + } + setChecking(true); + try { + await getWalletLink(fileNumber); + setLinked(true); + return true; + } catch { + setLinked(false); + return false; + } finally { + setChecking(false); + } + }, [fileNumber]); + const push = useCallback( async (changes: string[]) => { if (!fileNumber) return; + // Never push an empty/whitespace change set — the backend rejects it (400). + const clean = changes.map((c) => c.trim()).filter(Boolean); + if (clean.length === 0) { + setError("generic"); + setState("error"); + return; + } setError(null); setState("pending"); try { - const created = await pushWalletUpdate({ fileNumber, changes }); + const created = await pushWalletUpdate({ fileNumber, changes: clean }); setUpdate(created); } catch (err) { setError(err instanceof ApiError ? err.message : "generic"); @@ -96,7 +126,7 @@ export function useWalletSync(fileNumber: string | null | undefined) { setError(null); }, []); - return { linked, checking, state, update, error, push, reset }; + return { linked, checking, state, update, error, ensureLinked, push, reset }; } export type UseWalletSync = ReturnType; diff --git a/frontend/components/wallet/wallet-sync-step.tsx b/frontend/components/wallet/wallet-sync-step.tsx index 3548969..3c65b07 100644 --- a/frontend/components/wallet/wallet-sync-step.tsx +++ b/frontend/components/wallet/wallet-sync-step.tsx @@ -60,6 +60,9 @@ export function WalletSyncStep({ const { t } = useTranslation(); const { state, update, error, push } = sync; const status = update?.status ?? "pending"; + // The summary is the human-readable change the patient approves. Guard against + // an empty one (the backend 400s on empty changes) with a translated fallback. + const changeSummary = summary.trim() || t("walletSync.summaryFallback"); return ( <> @@ -79,7 +82,7 @@ export function WalletSyncStep({ {t("walletSync.changesLabel")}
- {summary} + {changeSummary}
{error && ( @@ -119,7 +122,7 @@ export function WalletSyncStep({ - diff --git a/frontend/lib/i18n/locales/ar/translation.json b/frontend/lib/i18n/locales/ar/translation.json index 83574c1..f172d94 100644 --- a/frontend/lib/i18n/locales/ar/translation.json +++ b/frontend/lib/i18n/locales/ar/translation.json @@ -2257,6 +2257,7 @@ "prescription": "وصفة جديدة: {{drug}}", "demographics": "تم تحديث البيانات الديموغرافية", "note": "ملاحظة سريرية جديدة" - } + }, + "summaryFallback": "تم تحديث السجل" } } diff --git a/frontend/lib/i18n/locales/de/translation.json b/frontend/lib/i18n/locales/de/translation.json index f136913..57b9782 100644 --- a/frontend/lib/i18n/locales/de/translation.json +++ b/frontend/lib/i18n/locales/de/translation.json @@ -2237,6 +2237,7 @@ "prescription": "Neues Rezept: {{drug}}", "demographics": "Stammdaten aktualisiert", "note": "Neue klinische Notiz" - } + }, + "summaryFallback": "Datensatz aktualisiert" } } diff --git a/frontend/lib/i18n/locales/en/translation.json b/frontend/lib/i18n/locales/en/translation.json index e61883c..4c4c8fa 100644 --- a/frontend/lib/i18n/locales/en/translation.json +++ b/frontend/lib/i18n/locales/en/translation.json @@ -2237,6 +2237,7 @@ "prescription": "New prescription: {{drug}}", "demographics": "Demographics updated", "note": "New clinical note" - } + }, + "summaryFallback": "Record updated" } } diff --git a/frontend/lib/i18n/locales/fr/translation.json b/frontend/lib/i18n/locales/fr/translation.json index 1f7f2f7..3761650 100644 --- a/frontend/lib/i18n/locales/fr/translation.json +++ b/frontend/lib/i18n/locales/fr/translation.json @@ -2237,6 +2237,7 @@ "prescription": "Nouvelle ordonnance : {{drug}}", "demographics": "Données démographiques mises à jour", "note": "Nouvelle note clinique" - } + }, + "summaryFallback": "Dossier mis à jour" } } diff --git a/frontend/lib/i18n/locales/so/translation.json b/frontend/lib/i18n/locales/so/translation.json index 755f2bc..c7b23ab 100644 --- a/frontend/lib/i18n/locales/so/translation.json +++ b/frontend/lib/i18n/locales/so/translation.json @@ -2237,6 +2237,7 @@ "prescription": "Rijeeto cusub: {{drug}}", "demographics": "Xogta bukaanka la cusboonaysiiyay", "note": "Qoraal caafimaad cusub" - } + }, + "summaryFallback": "Diiwaanka waa la cusboonaysiiyay" } }