diff --git a/frontend/components/appointments/add-appointment-dialog.tsx b/frontend/components/appointments/add-appointment-dialog.tsx index 5a7f25e..a479cd2 100644 --- a/frontend/components/appointments/add-appointment-dialog.tsx +++ b/frontend/components/appointments/add-appointment-dialog.tsx @@ -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; }) { const { t } = useTranslation(); const [patients, setPatients] = useState([]); @@ -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 ( - { - onOpenChange(o); - if (!o) reset(); - }} - open={open} - > - + + {t("appointments.dialog.title")} {t("appointments.dialog.description")} + {sync.linked && } + {step === "wallet" ? ( + handleOpenChange(false)} + patientName={selected?.name ?? ""} + summary={walletSummary} + sync={sync} + /> + ) : (
@@ -320,6 +348,7 @@ export function AddAppointmentDialog({ + )}
); diff --git a/frontend/components/chat/patient-form-dialog.tsx b/frontend/components/chat/patient-form-dialog.tsx index 22cf884..c7614ac 100644 --- a/frontend/components/chat/patient-form-dialog.tsx +++ b/frontend/components/chat/patient-form-dialog.tsx @@ -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 ?? "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 ( - - + + {isReview @@ -419,8 +441,17 @@ export function PatientFormDialog({ }) : t("patientForm.createDescription")} + {sync.linked && } + {step === "wallet" ? ( + handleOpenChange(false)} + patientName={name.trim()} + summary={t("walletSync.summary.demographics")} + sync={sync} + /> + ) : (
+ )}
); diff --git a/frontend/components/invoices/invoice-form-dialog.tsx b/frontend/components/invoices/invoice-form-dialog.tsx index c221777..77c4d10 100644 --- a/frontend/components/invoices/invoice-form-dialog.tsx +++ b/frontend/components/invoices/invoice-form-dialog.tsx @@ -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([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 ( - - + + {mode === "edit" @@ -274,8 +301,17 @@ export function InvoiceFormDialog({ {t("invoices.dialog.description")} + {sync.linked && } + {step === "wallet" ? ( + handleOpenChange(false)} + patientName={activePatient?.name ?? ""} + summary={walletSummary} + sync={sync} + /> + ) : (
@@ -478,6 +514,7 @@ export function InvoiceFormDialog({ + )}
); diff --git a/frontend/components/patients/scribe-dialog.tsx b/frontend/components/patients/scribe-dialog.tsx index ec09ae2..c06153e 100644 --- a/frontend/components/patients/scribe-dialog.tsx +++ b/frontend/components/patients/scribe-dialog.tsx @@ -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(null); const [veilNote, setVeilNote] = useState(null); const [error, setError] = useState(null); + const [walletStep, setWalletStep] = useState(false); + + const sync = useWalletSync(patient.fileNumber); const mediaRef = useRef(null); const chunksRef = useRef([]); @@ -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({ {t("scribe.subtitle", { name: patient.name })} + {sync.linked && ( + + )} + {walletStep ? ( + handleOpenChange(false)} + patientName={patient.name} + summary={t("walletSync.summary.note")} + sync={sync} + /> + ) : ( + <> {phase === "review" && draft ? (
@@ -430,6 +457,8 @@ export function ScribeDialog({ )} + + )}
); diff --git a/frontend/components/prescriptions/add-prescription-dialog.tsx b/frontend/components/prescriptions/add-prescription-dialog.tsx index 379aabb..8a3965c 100644 --- a/frontend/components/prescriptions/add-prescription-dialog.tsx +++ b/frontend/components/prescriptions/add-prescription-dialog.tsx @@ -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; }) { const { t } = useTranslation(); const [patients, setPatients] = useState([]); @@ -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 ( - { - onOpenChange(o); - if (!o) reset(); - }} - open={open} - > - + + {t("prescriptions.dialog.title")} {t("prescriptions.dialog.description")} + {sync.linked && } + {step === "wallet" ? ( + handleOpenChange(false)} + patientName={selected?.name ?? ""} + summary={walletSummary} + sync={sync} + /> + ) : (
@@ -616,6 +644,7 @@ export function AddPrescriptionDialog({ + )}
); diff --git a/frontend/components/wallet/use-wallet-sync.ts b/frontend/components/wallet/use-wallet-sync.ts new file mode 100644 index 0000000..07c7849 --- /dev/null +++ b/frontend/components/wallet/use-wallet-sync.ts @@ -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("idle"); + const [update, setUpdate] = useState(null); + const [error, setError] = useState(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; diff --git a/frontend/components/wallet/wallet-sync-step.tsx b/frontend/components/wallet/wallet-sync-step.tsx new file mode 100644 index 0000000..08c4d99 --- /dev/null +++ b/frontend/components/wallet/wallet-sync-step.tsx @@ -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 ( +
+ {steps.map((s, i) => { + const done = i < activeIndex; + const active = i === activeIndex; + return ( +
+ + {done ? : i + 1} + + + {s.label} + + {i < steps.length - 1 && ( + + )} +
+ ); + })} +
+ ); +} + +// 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 ( + <> + + {state === "idle" || state === "error" ? ( +
+
+

+ {t("walletSync.prompt", { name: patientName })} +

+

+ {t("walletSync.promptBody")} +

+
+
+ + {t("walletSync.changesLabel")} + +
+ {summary} +
+
+ {error && ( +

+ {error === "generic" ? t("walletSync.errors.generic") : error} +

+ )} +
+ ) : ( +
+ {status === "approved" ? ( +
+ +
+ ) : status === "denied" ? ( +
+ +
+ ) : ( + + )} +
+

+ {t(`walletPush.status.${status}.title`)} +

+

+ {t(`walletPush.status.${status}.body`)} +

+
+
+ )} +
+ + + {state === "idle" || state === "error" ? ( + <> + + + + ) : ( + + )} + + + ); +} diff --git a/frontend/lib/i18n/locales/ar/translation.json b/frontend/lib/i18n/locales/ar/translation.json index fc07afe..8121610 100644 --- a/frontend/lib/i18n/locales/ar/translation.json +++ b/frontend/lib/i18n/locales/ar/translation.json @@ -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": "ملاحظة سريرية جديدة" + } } } diff --git a/frontend/lib/i18n/locales/de/translation.json b/frontend/lib/i18n/locales/de/translation.json index e794cee..e92defc 100644 --- a/frontend/lib/i18n/locales/de/translation.json +++ b/frontend/lib/i18n/locales/de/translation.json @@ -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" + } } } diff --git a/frontend/lib/i18n/locales/en/translation.json b/frontend/lib/i18n/locales/en/translation.json index 6c4da58..92be106 100644 --- a/frontend/lib/i18n/locales/en/translation.json +++ b/frontend/lib/i18n/locales/en/translation.json @@ -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" + } } } diff --git a/frontend/lib/i18n/locales/fr/translation.json b/frontend/lib/i18n/locales/fr/translation.json index d5b30bf..abb2d70 100644 --- a/frontend/lib/i18n/locales/fr/translation.json +++ b/frontend/lib/i18n/locales/fr/translation.json @@ -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" + } } } diff --git a/frontend/lib/i18n/locales/so/translation.json b/frontend/lib/i18n/locales/so/translation.json index aed36b4..2e21e8a 100644 --- a/frontend/lib/i18n/locales/so/translation.json +++ b/frontend/lib/i18n/locales/so/translation.json @@ -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" + } } }