mirror of
https://github.com/temetro/temetro.git
synced 2026-07-26 11:58:14 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 }),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() }),
|
||||
);
|
||||
|
||||
@@ -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<boolean> => {
|
||||
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<typeof useWalletSync>;
|
||||
|
||||
@@ -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")}
|
||||
</span>
|
||||
<div className="rounded-lg border bg-muted/50 px-3 py-2 text-foreground text-sm">
|
||||
{summary}
|
||||
{changeSummary}
|
||||
</div>
|
||||
</div>
|
||||
{error && (
|
||||
@@ -119,7 +122,7 @@ export function WalletSyncStep({
|
||||
<Button onClick={onDone} type="button" variant="outline">
|
||||
{t("walletSync.skip")}
|
||||
</Button>
|
||||
<Button onClick={() => push([summary])} type="button">
|
||||
<Button onClick={() => push([changeSummary])} type="button">
|
||||
<Send className="size-4" />
|
||||
{t("walletSync.send")}
|
||||
</Button>
|
||||
|
||||
@@ -2257,6 +2257,7 @@
|
||||
"prescription": "وصفة جديدة: {{drug}}",
|
||||
"demographics": "تم تحديث البيانات الديموغرافية",
|
||||
"note": "ملاحظة سريرية جديدة"
|
||||
}
|
||||
},
|
||||
"summaryFallback": "تم تحديث السجل"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2237,6 +2237,7 @@
|
||||
"prescription": "Neues Rezept: {{drug}}",
|
||||
"demographics": "Stammdaten aktualisiert",
|
||||
"note": "Neue klinische Notiz"
|
||||
}
|
||||
},
|
||||
"summaryFallback": "Datensatz aktualisiert"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2237,6 +2237,7 @@
|
||||
"prescription": "New prescription: {{drug}}",
|
||||
"demographics": "Demographics updated",
|
||||
"note": "New clinical note"
|
||||
}
|
||||
},
|
||||
"summaryFallback": "Record updated"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2237,6 +2237,7 @@
|
||||
"prescription": "Nouvelle ordonnance : {{drug}}",
|
||||
"demographics": "Données démographiques mises à jour",
|
||||
"note": "Nouvelle note clinique"
|
||||
}
|
||||
},
|
||||
"summaryFallback": "Dossier mis à jour"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2237,6 +2237,7 @@
|
||||
"prescription": "Rijeeto cusub: {{drug}}",
|
||||
"demographics": "Xogta bukaanka la cusboonaysiiyay",
|
||||
"note": "Qoraal caafimaad cusub"
|
||||
}
|
||||
},
|
||||
"summaryFallback": "Diiwaanka waa la cusboonaysiiyay"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user