frontend: in-dialog wallet-sync stepper for record changes

When the selected patient has a linked wallet, create/edit dialogs now show
a two-step stepper: after saving, step 2 offers to push the change to the
patient's wallet (reusing pushWalletUpdate + approval polling). Added a shared
useWalletSync hook and DialogStepper/WalletSyncStep components, wired into the
appointment, invoice, prescription, patient-edit, and scribe dialogs. Falls
back to the old close-on-save when the patient has no wallet. Added walletSync.*
keys to all locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-07-09 20:52:36 +03:00
parent 35f07c508d
commit feffce6cbf
12 changed files with 546 additions and 32 deletions
@@ -23,6 +23,11 @@ import { Popover, PopoverPopup, PopoverTrigger } from "@/components/ui/popover";
import { listPatients, type Patient } from "@/lib/patients";
import { listProviders, type Provider } from "@/lib/staff";
import { notify } from "@/lib/toast";
import { useWalletSync } from "@/components/wallet/use-wallet-sync";
import {
DialogStepper,
WalletSyncStep,
} from "@/components/wallet/wallet-sync-step";
export type NewAppointment = {
fileNumber: string;
@@ -78,7 +83,7 @@ export function AddAppointmentDialog({
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
onAdd: (appt: NewAppointment) => void;
onAdd: (appt: NewAppointment) => void | Promise<void>;
}) {
const { t } = useTranslation();
const [patients, setPatients] = useState<Patient[]>([]);
@@ -90,6 +95,10 @@ export function AddAppointmentDialog({
const [type, setType] = useState(TYPES[0]);
const [provider, setProvider] = useState("");
const [providerQuery, setProviderQuery] = useState("");
const [step, setStep] = useState<"form" | "wallet">("form");
const [walletSummary, setWalletSummary] = useState("");
const sync = useWalletSync(selected?.fileNumber ?? null);
// Load patients + providers lazily when the dialog opens (for the searches).
useEffect(() => {
@@ -155,7 +164,16 @@ export function AddAppointmentDialog({
setProviderQuery("");
};
const submit = (event: FormEvent) => {
const handleOpenChange = (o: boolean) => {
onOpenChange(o);
if (!o) {
reset();
setStep("form");
sync.reset();
}
};
const submit = async (event: FormEvent) => {
event.preventDefault();
if (!selected) {
notify.error(
@@ -164,7 +182,7 @@ export function AddAppointmentDialog({
);
return;
}
onAdd({
await onAdd({
fileNumber: selected.fileNumber,
name: selected.name,
initials: selected.initials,
@@ -177,26 +195,36 @@ export function AddAppointmentDialog({
t("appointments.dialog.addedTitle"),
`${selected.name} · ${time}`,
);
reset();
onOpenChange(false);
if (sync.linked) {
setWalletSummary(
t("walletSync.summary.appointment", { date: keyOf(date), time }),
);
setStep("wallet");
} else {
reset();
onOpenChange(false);
}
};
return (
<Dialog
onOpenChange={(o) => {
onOpenChange(o);
if (!o) reset();
}}
open={open}
>
<DialogPopup className="sm:max-w-md">
<Dialog onOpenChange={handleOpenChange} open={open}>
<DialogPopup className="flex max-h-[85dvh] flex-col sm:max-w-md">
<DialogHeader>
<DialogTitle>{t("appointments.dialog.title")}</DialogTitle>
<DialogDescription>
{t("appointments.dialog.description")}
</DialogDescription>
{sync.linked && <DialogStepper step={step} />}
</DialogHeader>
{step === "wallet" ? (
<WalletSyncStep
onDone={() => handleOpenChange(false)}
patientName={selected?.name ?? ""}
summary={walletSummary}
sync={sync}
/>
) : (
<form className="contents" onSubmit={submit}>
<DialogPanel className="flex flex-col gap-4">
<Field label={t("appointments.dialog.patient")}>
@@ -320,6 +348,7 @@ export function AddAppointmentDialog({
</Button>
</DialogFooter>
</form>
)}
</DialogPopup>
</Dialog>
);
@@ -38,6 +38,11 @@ import { uploadAttachment } from "@/lib/attachments";
import { hasClinicalAccess, useActiveRole } from "@/lib/roles";
import { listProviders, type Provider } from "@/lib/staff";
import { notify } from "@/lib/toast";
import { useWalletSync } from "@/components/wallet/use-wallet-sync";
import {
DialogStepper,
WalletSyncStep,
} from "@/components/wallet/wallet-sync-step";
type PatientFormDialogProps = {
open: boolean;
@@ -233,6 +238,19 @@ export function PatientFormDialog({
const [fileNumber, setFileNumber] = useState(() =>
isEdit && patient ? patient.fileNumber : generateFileNumber()
);
const [step, setStep] = useState<"form" | "wallet">("form");
// Only edits to an existing (non-review) record can sync to a wallet — a newly
// created patient has no wallet, and review mode stages an import.
const sync = useWalletSync(isEdit && !isReview ? fileNumber : null);
const handleOpenChange = (o: boolean) => {
onOpenChange(o);
if (!o) {
setStep("form");
sync.reset();
}
};
const [name, setName] = useState(patient?.name ?? "");
const [age, setAge] = useState(patient ? String(patient.age) : "");
const [sex, setSex] = useState<Patient["sex"]>(patient?.sex ?? "F");
@@ -378,6 +396,10 @@ export function PatientFormDialog({
t("patientForm.updatedTitle"),
t("patientForm.updatedBody", { name: saved.name }),
);
if (sync.linked) {
setStep("wallet");
return;
}
} else {
onCreated?.(saved.fileNumber);
notify.success(
@@ -400,8 +422,8 @@ export function PatientFormDialog({
};
return (
<Dialog onOpenChange={onOpenChange} open={open}>
<DialogPopup className="max-h-[85dvh] sm:max-w-lg">
<Dialog onOpenChange={handleOpenChange} open={open}>
<DialogPopup className="flex max-h-[85dvh] flex-col sm:max-w-lg">
<DialogHeader>
<DialogTitle>
{isReview
@@ -419,8 +441,17 @@ export function PatientFormDialog({
})
: t("patientForm.createDescription")}
</DialogDescription>
{sync.linked && <DialogStepper step={step} />}
</DialogHeader>
{step === "wallet" ? (
<WalletSyncStep
onDone={() => handleOpenChange(false)}
patientName={name.trim()}
summary={t("walletSync.summary.demographics")}
sync={sync}
/>
) : (
<form className="contents" onSubmit={handleSubmit}>
<DialogPanel
scrollFade={false}
@@ -769,6 +800,7 @@ export function PatientFormDialog({
</Button>
</DialogFooter>
</form>
)}
</DialogPopup>
</Dialog>
);
@@ -35,6 +35,11 @@ import {
} from "@/lib/invoices";
import { listPatients, type Patient } from "@/lib/patients";
import { notify } from "@/lib/toast";
import { useWalletSync } from "@/components/wallet/use-wallet-sync";
import {
DialogStepper,
WalletSyncStep,
} from "@/components/wallet/wallet-sync-step";
const STATUSES: InvoiceStatus[] = ["draft", "sent", "paid", "void"];
@@ -152,6 +157,19 @@ export function InvoiceFormDialog({
const [notes, setNotes] = useState("");
const [lineItems, setLineItems] = useState<InvoiceLineItem[]>([emptyLine()]);
const [busy, setBusy] = useState(false);
const [step, setStep] = useState<"form" | "wallet">("form");
const [walletSummary, setWalletSummary] = useState("");
const activePatient = fixedPatient ?? selected;
const sync = useWalletSync(activePatient?.fileNumber ?? null);
const handleOpenChange = (next: boolean) => {
if (!next) {
setStep("form");
sync.reset();
}
onOpenChange(next);
};
// Seed the form when opening.
useEffect(() => {
@@ -254,7 +272,16 @@ export function InvoiceFormDialog({
})
: await createInvoice(payload);
onSaved(saved);
onOpenChange(false);
if (sync.linked) {
setWalletSummary(
mode === "edit"
? t("walletSync.summary.invoiceUpdated", { number: saved.number })
: t("walletSync.summary.invoiceCreated", { number: saved.number }),
);
setStep("wallet");
} else {
onOpenChange(false);
}
} catch {
notify.error(t("invoices.addFailedTitle"), t("invoices.addFailedBody"));
} finally {
@@ -263,8 +290,8 @@ export function InvoiceFormDialog({
};
return (
<Dialog onOpenChange={onOpenChange} open={open}>
<DialogPopup className="sm:max-w-lg">
<Dialog onOpenChange={handleOpenChange} open={open}>
<DialogPopup className="flex max-h-[85dvh] flex-col sm:max-w-lg">
<DialogHeader>
<DialogTitle>
{mode === "edit"
@@ -274,8 +301,17 @@ export function InvoiceFormDialog({
<DialogDescription>
{t("invoices.dialog.description")}
</DialogDescription>
{sync.linked && <DialogStepper step={step} />}
</DialogHeader>
{step === "wallet" ? (
<WalletSyncStep
onDone={() => handleOpenChange(false)}
patientName={activePatient?.name ?? ""}
summary={walletSummary}
sync={sync}
/>
) : (
<form className="contents" onSubmit={submit}>
<DialogPanel className="flex flex-col gap-4">
<Field label={t("invoices.dialog.patient")}>
@@ -478,6 +514,7 @@ export function InvoiceFormDialog({
</Button>
</DialogFooter>
</form>
)}
</DialogPopup>
</Dialog>
);
+30 -1
View File
@@ -29,6 +29,11 @@ import type { Encounter, Patient } from "@/lib/patients";
import { draftNote, saveNote, transcribeRecording } from "@/lib/scribe";
import { notify } from "@/lib/toast";
import { ApiError } from "@/lib/api-client";
import { useWalletSync } from "@/components/wallet/use-wallet-sync";
import {
DialogStepper,
WalletSyncStep,
} from "@/components/wallet/wallet-sync-step";
type Phase = "input" | "processing" | "review";
type InputTab = "record" | "paste";
@@ -76,6 +81,9 @@ export function ScribeDialog({
const [draft, setDraft] = useState<Encounter | null>(null);
const [veilNote, setVeilNote] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [walletStep, setWalletStep] = useState(false);
const sync = useWalletSync(patient.fileNumber);
const mediaRef = useRef<MediaRecorder | null>(null);
const chunksRef = useRef<Blob[]>([]);
@@ -105,6 +113,8 @@ export function ScribeDialog({
setDraft(null);
setVeilNote(null);
setError(null);
setWalletStep(false);
sync.reset();
chunksRef.current = [];
blobRef.current = null;
};
@@ -223,7 +233,12 @@ export function ScribeDialog({
const updated = await saveNote(patient.fileNumber, draft);
notify.success(t("scribe.saved.title"), patient.name);
onSaved(updated);
handleOpenChange(false);
if (sync.linked) {
setPhase("review");
setWalletStep(true);
} else {
handleOpenChange(false);
}
} catch (err) {
setError(
err instanceof ApiError ? err.message : t("scribe.errors.generic"),
@@ -245,8 +260,20 @@ export function ScribeDialog({
<DialogDescription>
{t("scribe.subtitle", { name: patient.name })}
</DialogDescription>
{sync.linked && (
<DialogStepper step={walletStep ? "wallet" : "form"} />
)}
</DialogHeader>
{walletStep ? (
<WalletSyncStep
onDone={() => handleOpenChange(false)}
patientName={patient.name}
summary={t("walletSync.summary.note")}
sync={sync}
/>
) : (
<>
<DialogPanel className="min-h-0 flex-1 overflow-y-auto">
{phase === "review" && draft ? (
<div className="flex flex-col gap-4">
@@ -430,6 +457,8 @@ export function ScribeDialog({
</>
)}
</DialogFooter>
</>
)}
</DialogPopup>
</Dialog>
);
@@ -29,6 +29,11 @@ import { type InventoryItem, listInventory } from "@/lib/inventory";
import { listPatients, type Patient } from "@/lib/patients";
import { notify } from "@/lib/toast";
import { cn } from "@/lib/utils";
import { useWalletSync } from "@/components/wallet/use-wallet-sync";
import {
DialogStepper,
WalletSyncStep,
} from "@/components/wallet/wallet-sync-step";
export type NewPrescription = {
fileNumber: string;
@@ -132,7 +137,7 @@ export function AddPrescriptionDialog({
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
onAdd: (rx: NewPrescription) => void;
onAdd: (rx: NewPrescription) => void | Promise<void>;
}) {
const { t } = useTranslation();
const [patients, setPatients] = useState<Patient[]>([]);
@@ -151,6 +156,19 @@ export function AddPrescriptionDialog({
const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState("");
const [notes, setNotes] = useState("");
const [step, setStep] = useState<"form" | "wallet">("form");
const [walletSummary, setWalletSummary] = useState("");
const sync = useWalletSync(selected?.fileNumber ?? null);
const handleOpenChange = (o: boolean) => {
onOpenChange(o);
if (!o) {
reset();
setStep("form");
sync.reset();
}
};
// Load patients + inventory lazily when the dialog opens (for the quick
// searches). Inventory backs the medication combobox; it still accepts free
@@ -279,7 +297,7 @@ export function AddPrescriptionDialog({
setNotes("");
};
const submit = (event: FormEvent) => {
const submit = async (event: FormEvent) => {
event.preventDefault();
if (!selected) {
notify.error(
@@ -312,7 +330,7 @@ export function AddPrescriptionDialog({
);
return;
}
onAdd({
await onAdd({
fileNumber: selected.fileNumber,
name: selected.name,
initials: selected.initials,
@@ -328,26 +346,36 @@ export function AddPrescriptionDialog({
t("prescriptions.dialog.addedTitle"),
`${medication.trim()} · ${selected.name}`,
);
reset();
onOpenChange(false);
if (sync.linked) {
setWalletSummary(
t("walletSync.summary.prescription", { drug: medication.trim() }),
);
setStep("wallet");
} else {
reset();
onOpenChange(false);
}
};
return (
<Dialog
onOpenChange={(o) => {
onOpenChange(o);
if (!o) reset();
}}
open={open}
>
<DialogPopup className="sm:max-w-md">
<Dialog onOpenChange={handleOpenChange} open={open}>
<DialogPopup className="flex max-h-[85dvh] flex-col sm:max-w-md">
<DialogHeader>
<DialogTitle>{t("prescriptions.dialog.title")}</DialogTitle>
<DialogDescription>
{t("prescriptions.dialog.description")}
</DialogDescription>
{sync.linked && <DialogStepper step={step} />}
</DialogHeader>
{step === "wallet" ? (
<WalletSyncStep
onDone={() => handleOpenChange(false)}
patientName={selected?.name ?? ""}
summary={walletSummary}
sync={sync}
/>
) : (
<form className="contents" onSubmit={submit}>
<DialogPanel className="flex flex-col gap-4">
<Field label={t("prescriptions.dialog.patient")}>
@@ -616,6 +644,7 @@ export function AddPrescriptionDialog({
</Button>
</DialogFooter>
</form>
)}
</DialogPopup>
</Dialog>
);
@@ -0,0 +1,102 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { ApiError } from "@/lib/api-client";
import {
getWalletLink,
getWalletUpdate,
pushWalletUpdate,
type WalletUpdate,
} from "@/lib/wallet-updates";
export type WalletSyncState =
| "idle"
| "pending"
| "approved"
| "denied"
| "error";
// Shared wallet-sync logic for create/edit dialogs. It resolves whether the
// selected patient has a linked wallet, then (after the primary save) can push
// the change to their phone and poll until they approve/deny it — the same
// mechanism as the standalone WalletPushDialog, lifted out for reuse.
export function useWalletSync(fileNumber: string | null | undefined) {
const [linked, setLinked] = useState(false);
const [checking, setChecking] = useState(false);
const [state, setState] = useState<WalletSyncState>("idle");
const [update, setUpdate] = useState<WalletUpdate | null>(null);
const [error, setError] = useState<string | null>(null);
// Resolve link status whenever the chosen patient changes. A 404 simply means
// "not wallet-backed", so failures collapse to `linked = false`.
useEffect(() => {
if (!fileNumber) {
setLinked(false);
return;
}
let active = true;
setChecking(true);
getWalletLink(fileNumber)
.then(() => {
if (active) setLinked(true);
})
.catch(() => {
if (active) setLinked(false);
})
.finally(() => {
if (active) setChecking(false);
});
return () => {
active = false;
};
}, [fileNumber]);
// Poll the pushed update until the patient approves/denies it.
useEffect(() => {
if (state !== "pending" || !update || update.resolvedAt) return;
let active = true;
const timer = setInterval(async () => {
try {
const fresh = await getWalletUpdate(update.id);
if (!active) return;
setUpdate(fresh);
if (fresh.status === "approved") setState("approved");
else if (fresh.status === "denied") setState("denied");
if (fresh.resolvedAt) clearInterval(timer);
} catch {
/* keep polling */
}
}, 3000);
return () => {
active = false;
clearInterval(timer);
};
}, [state, update]);
const push = useCallback(
async (changes: string[]) => {
if (!fileNumber) return;
setError(null);
setState("pending");
try {
const created = await pushWalletUpdate({ fileNumber, changes });
setUpdate(created);
} catch (err) {
setError(err instanceof ApiError ? err.message : "generic");
setState("error");
}
},
[fileNumber],
);
const reset = useCallback(() => {
setState("idle");
setUpdate(null);
setError(null);
}, []);
return { linked, checking, state, update, error, push, reset };
}
export type UseWalletSync = ReturnType<typeof useWalletSync>;
@@ -0,0 +1,151 @@
"use client";
import { Check, Loader2, Send, X } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { DialogFooter, DialogPanel } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import type { UseWalletSync } from "./use-wallet-sync";
// Two-step header shown at the top of a dialog when the selected patient has a
// linked wallet: "Details" → "Sync to wallet".
export function DialogStepper({ step }: { step: "form" | "wallet" }) {
const { t } = useTranslation();
const steps = [
{ key: "form", label: t("walletSync.step1") },
{ key: "wallet", label: t("walletSync.step2") },
] as const;
const activeIndex = step === "form" ? 0 : 1;
return (
<div className="mt-3 flex items-center gap-2">
{steps.map((s, i) => {
const done = i < activeIndex;
const active = i === activeIndex;
return (
<div className="flex flex-1 items-center gap-2" key={s.key}>
<span
className={cn(
"flex size-5 shrink-0 items-center justify-center rounded-full text-[11px] font-semibold transition-colors",
done && "bg-primary text-primary-foreground",
active && "bg-primary/15 text-primary ring-1 ring-primary/40",
!done && !active && "bg-muted text-muted-foreground",
)}
>
{done ? <Check className="size-3" /> : i + 1}
</span>
<span
className={cn(
"text-xs font-medium",
active || done ? "text-foreground" : "text-muted-foreground",
)}
>
{s.label}
</span>
{i < steps.length - 1 && (
<span
className={cn(
"ml-1 h-px flex-1",
done ? "bg-primary/40" : "bg-border",
)}
/>
)}
</div>
);
})}
</div>
);
}
// Step 2 body + footer: offer to push the just-saved change to the patient's
// wallet, then show the approval status. Rendered in place of the form's
// DialogPanel/DialogFooter, so it returns them as a fragment.
export function WalletSyncStep({
patientName,
summary,
sync,
onDone,
}: {
patientName: string;
summary: string;
sync: UseWalletSync;
onDone: () => void;
}) {
const { t } = useTranslation();
const { state, update, error, push } = sync;
const status = update?.status ?? "pending";
return (
<>
<DialogPanel className="min-h-0 flex-1 overflow-y-auto">
{state === "idle" || state === "error" ? (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<p className="font-medium text-foreground text-sm">
{t("walletSync.prompt", { name: patientName })}
</p>
<p className="text-muted-foreground text-sm">
{t("walletSync.promptBody")}
</p>
</div>
<div className="flex flex-col gap-1.5">
<span className="text-muted-foreground text-xs">
{t("walletSync.changesLabel")}
</span>
<div className="rounded-lg border bg-muted/50 px-3 py-2 text-foreground text-sm">
{summary}
</div>
</div>
{error && (
<p className="text-destructive text-sm" role="alert">
{error === "generic" ? t("walletSync.errors.generic") : error}
</p>
)}
</div>
) : (
<div className="flex flex-col items-center gap-4 py-6 text-center">
{status === "approved" ? (
<div className="flex size-12 items-center justify-center rounded-full bg-success/15 text-success">
<Check className="size-6" />
</div>
) : status === "denied" ? (
<div className="flex size-12 items-center justify-center rounded-full bg-destructive/15 text-destructive">
<X className="size-6" />
</div>
) : (
<Loader2 className="size-8 animate-spin text-muted-foreground" />
)}
<div className="flex flex-col gap-1">
<p className="font-medium text-foreground text-sm">
{t(`walletPush.status.${status}.title`)}
</p>
<p className="text-muted-foreground text-sm">
{t(`walletPush.status.${status}.body`)}
</p>
</div>
</div>
)}
</DialogPanel>
<DialogFooter>
{state === "idle" || state === "error" ? (
<>
<Button onClick={onDone} type="button" variant="outline">
{t("walletSync.skip")}
</Button>
<Button onClick={() => push([summary])} type="button">
<Send className="size-4" />
{t("walletSync.send")}
</Button>
</>
) : (
<Button onClick={onDone} type="button">
{t("walletSync.done")}
</Button>
)}
</DialogFooter>
</>
);
}
@@ -2216,5 +2216,26 @@
"lookupAnother": "بحث عن آخر",
"errorGeneric": "تعذّر العثور على سجلك. يرجى التحقّق من بياناتك أو سؤال مكتب الاستقبال."
}
},
"walletSync": {
"step1": "التفاصيل",
"step2": "المزامنة مع المحفظة",
"prompt": "إرسال إلى محفظة {{name}}؟",
"promptBody": "يستخدم هذا المريض تطبيق المحفظة. أرسل هذا التغيير لمزامنته مع هاتفه — سيوافق عليه على جهازه.",
"changesLabel": "ما الذي تغيّر",
"skip": "ليس الآن",
"send": "إرسال إلى المحفظة",
"done": "تم",
"errors": {
"generic": "تعذّر إرسال التحديث. يرجى المحاولة مرة أخرى."
},
"summary": {
"invoiceCreated": "فاتورة جديدة {{number}}",
"invoiceUpdated": "تم تحديث الفاتورة {{number}}",
"appointment": "موعد جديد في {{date}} الساعة {{time}}",
"prescription": "وصفة جديدة: {{drug}}",
"demographics": "تم تحديث البيانات الديموغرافية",
"note": "ملاحظة سريرية جديدة"
}
}
}
@@ -2196,5 +2196,26 @@
"lookupAnother": "Weitere nachschlagen",
"errorGeneric": "Ihre Akte konnte nicht gefunden werden. Bitte prüfen Sie Ihre Angaben oder wenden Sie sich an die Rezeption."
}
},
"walletSync": {
"step1": "Details",
"step2": "Mit Wallet abgleichen",
"prompt": "An {{name}}s Wallet senden?",
"promptBody": "Diese Person nutzt die Wallet-App. Senden Sie diese Änderung, damit sie auf das Gerät übertragen wird — die Bestätigung erfolgt auf dem Telefon.",
"changesLabel": "Was sich geändert hat",
"skip": "Jetzt nicht",
"send": "An Wallet senden",
"done": "Fertig",
"errors": {
"generic": "Aktualisierung konnte nicht gesendet werden. Bitte erneut versuchen."
},
"summary": {
"invoiceCreated": "Neue Rechnung {{number}}",
"invoiceUpdated": "Rechnung {{number}} aktualisiert",
"appointment": "Neuer Termin am {{date}} um {{time}}",
"prescription": "Neues Rezept: {{drug}}",
"demographics": "Stammdaten aktualisiert",
"note": "Neue klinische Notiz"
}
}
}
@@ -2196,5 +2196,26 @@
"lookupAnother": "Look up another",
"errorGeneric": "Couldn't find your record. Please check your details or ask the front desk."
}
},
"walletSync": {
"step1": "Details",
"step2": "Sync to wallet",
"prompt": "Send to {{name}}'s wallet?",
"promptBody": "This patient uses the wallet app. Send this change so it syncs to their phone — they approve it on their device.",
"changesLabel": "What changed",
"skip": "Not now",
"send": "Send to wallet",
"done": "Done",
"errors": {
"generic": "Couldn't send the update. Please try again."
},
"summary": {
"invoiceCreated": "New invoice {{number}}",
"invoiceUpdated": "Updated invoice {{number}}",
"appointment": "New appointment on {{date}} at {{time}}",
"prescription": "New prescription: {{drug}}",
"demographics": "Demographics updated",
"note": "New clinical note"
}
}
}
@@ -2196,5 +2196,26 @@
"lookupAnother": "Rechercher un autre",
"errorGeneric": "Impossible de trouver votre dossier. Veuillez vérifier vos informations ou demander à l'accueil."
}
},
"walletSync": {
"step1": "Détails",
"step2": "Synchroniser au portefeuille",
"prompt": "Envoyer au portefeuille de {{name}} ?",
"promptBody": "Ce patient utilise l'application portefeuille. Envoyez cette modification pour la synchroniser sur son téléphone — il l'approuvera sur son appareil.",
"changesLabel": "Ce qui a changé",
"skip": "Plus tard",
"send": "Envoyer au portefeuille",
"done": "Terminé",
"errors": {
"generic": "Impossible d'envoyer la mise à jour. Veuillez réessayer."
},
"summary": {
"invoiceCreated": "Nouvelle facture {{number}}",
"invoiceUpdated": "Facture {{number}} mise à jour",
"appointment": "Nouveau rendez-vous le {{date}} à {{time}}",
"prescription": "Nouvelle ordonnance : {{drug}}",
"demographics": "Données démographiques mises à jour",
"note": "Nouvelle note clinique"
}
}
}
@@ -2196,5 +2196,26 @@
"lookupAnother": "Raadi mid kale",
"errorGeneric": "Diiwaankaaga lama helin. Fadlan hubi faahfaahintaada ama ka codso miiska hore."
}
},
"walletSync": {
"step1": "Faahfaahin",
"step2": "La socodsii walletka",
"prompt": "Ma u dirtaa walletka {{name}}?",
"promptBody": "Bukaankan wuxuu isticmaalaa abka walletka. Dir isbeddelkan si loogu socodsiiyo taleefankooda — waxay ku ansixin doonaan aaladdooda.",
"changesLabel": "Wixii isbeddelay",
"skip": "Hadda maya",
"send": "U dir walletka",
"done": "Dhammaystiran",
"errors": {
"generic": "Cusboonaysiinta lama diri karin. Fadlan mar kale isku day."
},
"summary": {
"invoiceCreated": "Qaansheeg cusub {{number}}",
"invoiceUpdated": "Qaansheeg {{number}} la cusboonaysiiyay",
"appointment": "Ballan cusub {{date}} saacadda {{time}}",
"prescription": "Rijeeto cusub: {{drug}}",
"demographics": "Xogta bukaanka la cusboonaysiiyay",
"note": "Qoraal caafimaad cusub"
}
}
}