mirror of
https://github.com/temetro/temetro.git
synced 2026-08-27 19:06:52 +00:00
feat: clinic→wallet record-update push
A clinician can push an updated record to a wallet-linked patient (permanent share). The snapshot is signed with the clinic Ed25519 key and sealed to the wallet's X25519 key — derived from its Ed25519 wallet number via the birational map, verified byte-for-byte against the wallet's own derivation. Stored pending, delivered over the /wallet relay live and on the wallet's next authenticated connect (offline catch-up). The patient approves/denies in-app; the wallet signs its decision, the backend verifies it, and the record is replaced only on approval. Wallet pins the clinic key (TOFU) and warns on change. Backend: walletRecordUpdates table + service, ed25519PubToX25519Hex helper, POST /api/patients/wallet/push, GET .../link/:fileNumber|updates|updates/:id, wallet:update-request / wallet:update-response relay events. Frontend: "Push to wallet" dialog with live status, wallet-link gating on the patient sheet, "Sent updates" list under Settings → Signing, walletPush / walletUpdatesList locale namespaces across all five languages. Bumps to v0.5.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,53 @@
|
||||
"generic": "حدث خطأ ما. يُرجى المحاولة مرة أخرى."
|
||||
}
|
||||
},
|
||||
"walletUpdatesList": {
|
||||
"title": "التحديثات المُرسَلة إلى المحافظ",
|
||||
"description": "تحديثات السجلات التي أرسلتها إلى محافظ المرضى، وما إذا كان المريض قد وافق عليها.",
|
||||
"none": "لم ترسل أي تحديثات إلى المحافظ بعد."
|
||||
},
|
||||
"walletPush": {
|
||||
"action": "إرسال إلى المحفظة",
|
||||
"title": "إرسال التحديث إلى المحفظة",
|
||||
"subtitle": "أرسل سجل {{name}} المُحدَّث إلى تطبيق المحفظة الخاص به. يجب أن يوافق عليه على هاتفه قبل أن يحل محل النسخة الموجودة على جهازه.",
|
||||
"sectionsLabel": "ما الذي تغيّر",
|
||||
"sections": {
|
||||
"demographics": "البيانات الديموغرافية",
|
||||
"problems": "المشكلات",
|
||||
"medications": "الأدوية",
|
||||
"allergies": "الحساسية",
|
||||
"labs": "التحاليل",
|
||||
"vitals": "العلامات الحيوية",
|
||||
"visits": "الزيارات"
|
||||
},
|
||||
"noteLabel": "ملاحظة (اختياري)",
|
||||
"notePlaceholder": "ملاحظة قصيرة عن هذا التحديث…",
|
||||
"notice": "يُرسِل هذا السجل الحالي كاملاً، موقَّعًا بمفتاح عيادتك. ولا يحل محل النسخة الموجودة على جهاز المريض إلا بعد موافقته.",
|
||||
"cancel": "إلغاء",
|
||||
"send": "إرسال التحديث",
|
||||
"done": "تم",
|
||||
"errors": {
|
||||
"generic": "تعذّر إرسال التحديث. يُرجى المحاولة مرة أخرى."
|
||||
},
|
||||
"status": {
|
||||
"pending": {
|
||||
"title": "بانتظار المريض",
|
||||
"body": "تم إرسال التحديث. سيصل إلى هاتف المريض الآن، أو في المرة التالية التي يفتح فيها التطبيق."
|
||||
},
|
||||
"delivered": {
|
||||
"title": "تم التسليم إلى الهاتف",
|
||||
"body": "استلم المريض التحديث ويلزمه الموافقة عليه على جهازه."
|
||||
},
|
||||
"approved": {
|
||||
"title": "وافق المريض",
|
||||
"body": "وافق المريض على التحديث، وسجلّه على الجهاز محدَّث الآن."
|
||||
},
|
||||
"denied": {
|
||||
"title": "رفض المريض",
|
||||
"body": "رفض المريض هذا التحديث، لذا بقي سجلّه على الجهاز دون تغيير."
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"appName": "temetro",
|
||||
"email": "البريد الإلكتروني",
|
||||
|
||||
@@ -43,6 +43,53 @@
|
||||
"generic": "Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut."
|
||||
}
|
||||
},
|
||||
"walletUpdatesList": {
|
||||
"title": "An Wallets gesendete Updates",
|
||||
"description": "Aktenupdates, die Sie an Patienten-Wallets gesendet haben, und ob der Patient sie bestätigt hat.",
|
||||
"none": "Sie haben noch keine Wallet-Updates gesendet."
|
||||
},
|
||||
"walletPush": {
|
||||
"action": "An Wallet senden",
|
||||
"title": "Update an Wallet senden",
|
||||
"subtitle": "Senden Sie {{name}}s aktualisierte Akte an die Wallet-App. Der Patient muss sie auf dem Telefon bestätigen, bevor sie die Kopie auf dem Gerät ersetzt.",
|
||||
"sectionsLabel": "Was sich geändert hat",
|
||||
"sections": {
|
||||
"demographics": "Stammdaten",
|
||||
"problems": "Probleme",
|
||||
"medications": "Medikamente",
|
||||
"allergies": "Allergien",
|
||||
"labs": "Labor",
|
||||
"vitals": "Vitalwerte",
|
||||
"visits": "Besuche"
|
||||
},
|
||||
"noteLabel": "Notiz (optional)",
|
||||
"notePlaceholder": "Eine kurze Notiz zu diesem Update…",
|
||||
"notice": "Dies sendet die vollständige aktuelle Akte, signiert mit Ihrem Klinikschlüssel. Sie ersetzt die Kopie auf dem Gerät des Patienten erst nach dessen Zustimmung.",
|
||||
"cancel": "Abbrechen",
|
||||
"send": "Update senden",
|
||||
"done": "Fertig",
|
||||
"errors": {
|
||||
"generic": "Update konnte nicht gesendet werden. Bitte versuchen Sie es erneut."
|
||||
},
|
||||
"status": {
|
||||
"pending": {
|
||||
"title": "Warten auf den Patienten",
|
||||
"body": "Das Update wurde gesendet. Es erreicht das Telefon des Patienten jetzt oder beim nächsten Öffnen der App."
|
||||
},
|
||||
"delivered": {
|
||||
"title": "An das Telefon zugestellt",
|
||||
"body": "Der Patient hat das Update erhalten und muss es auf seinem Gerät bestätigen."
|
||||
},
|
||||
"approved": {
|
||||
"title": "Vom Patienten bestätigt",
|
||||
"body": "Der Patient hat das Update bestätigt; seine Akte auf dem Gerät ist nun aktuell."
|
||||
},
|
||||
"denied": {
|
||||
"title": "Vom Patienten abgelehnt",
|
||||
"body": "Der Patient hat dieses Update abgelehnt; seine Akte auf dem Gerät bleibt unverändert."
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"appName": "temetro",
|
||||
"email": "E-Mail",
|
||||
|
||||
@@ -43,6 +43,53 @@
|
||||
"generic": "Something went wrong. Please try again."
|
||||
}
|
||||
},
|
||||
"walletUpdatesList": {
|
||||
"title": "Updates sent to wallets",
|
||||
"description": "Record updates you've pushed to patient wallets, and whether the patient has approved them.",
|
||||
"none": "You haven't pushed any wallet updates yet."
|
||||
},
|
||||
"walletPush": {
|
||||
"action": "Push to wallet",
|
||||
"title": "Push update to wallet",
|
||||
"subtitle": "Send {{name}}'s updated record to their wallet app. They must approve it on their phone before it replaces the copy on their device.",
|
||||
"sectionsLabel": "What changed",
|
||||
"sections": {
|
||||
"demographics": "Demographics",
|
||||
"problems": "Problems",
|
||||
"medications": "Medications",
|
||||
"allergies": "Allergies",
|
||||
"labs": "Labs",
|
||||
"vitals": "Vitals",
|
||||
"visits": "Visits"
|
||||
},
|
||||
"noteLabel": "Note (optional)",
|
||||
"notePlaceholder": "A short note about this update…",
|
||||
"notice": "This sends the full current record, signed with your clinic key. It replaces the patient's on-device copy only after they approve it.",
|
||||
"cancel": "Cancel",
|
||||
"send": "Send update",
|
||||
"done": "Done",
|
||||
"errors": {
|
||||
"generic": "Couldn't send the update. Please try again."
|
||||
},
|
||||
"status": {
|
||||
"pending": {
|
||||
"title": "Waiting for the patient",
|
||||
"body": "The update was sent. It will arrive on the patient's phone now, or the next time they open the app."
|
||||
},
|
||||
"delivered": {
|
||||
"title": "Delivered to the phone",
|
||||
"body": "The patient has received the update and needs to approve it on their device."
|
||||
},
|
||||
"approved": {
|
||||
"title": "Approved by the patient",
|
||||
"body": "The patient approved the update and their on-device record is now up to date."
|
||||
},
|
||||
"denied": {
|
||||
"title": "Declined by the patient",
|
||||
"body": "The patient declined this update, so their on-device record is unchanged."
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"appName": "temetro",
|
||||
"email": "Email",
|
||||
|
||||
@@ -43,6 +43,53 @@
|
||||
"generic": "Une erreur s'est produite. Veuillez réessayer."
|
||||
}
|
||||
},
|
||||
"walletUpdatesList": {
|
||||
"title": "Mises à jour envoyées aux portefeuilles",
|
||||
"description": "Les mises à jour de dossiers que vous avez envoyées aux portefeuilles des patients, et si le patient les a approuvées.",
|
||||
"none": "Vous n'avez encore envoyé aucune mise à jour de portefeuille."
|
||||
},
|
||||
"walletPush": {
|
||||
"action": "Envoyer au portefeuille",
|
||||
"title": "Envoyer la mise à jour au portefeuille",
|
||||
"subtitle": "Envoyez le dossier mis à jour de {{name}} vers son application portefeuille. Le patient doit l'approuver sur son téléphone avant qu'elle ne remplace la copie sur son appareil.",
|
||||
"sectionsLabel": "Ce qui a changé",
|
||||
"sections": {
|
||||
"demographics": "Données démographiques",
|
||||
"problems": "Problèmes",
|
||||
"medications": "Médicaments",
|
||||
"allergies": "Allergies",
|
||||
"labs": "Analyses",
|
||||
"vitals": "Signes vitaux",
|
||||
"visits": "Visites"
|
||||
},
|
||||
"noteLabel": "Note (facultatif)",
|
||||
"notePlaceholder": "Une courte note sur cette mise à jour…",
|
||||
"notice": "Ceci envoie le dossier actuel complet, signé avec la clé de votre clinique. Il ne remplace la copie sur l'appareil du patient qu'après son approbation.",
|
||||
"cancel": "Annuler",
|
||||
"send": "Envoyer la mise à jour",
|
||||
"done": "Terminé",
|
||||
"errors": {
|
||||
"generic": "Impossible d'envoyer la mise à jour. Veuillez réessayer."
|
||||
},
|
||||
"status": {
|
||||
"pending": {
|
||||
"title": "En attente du patient",
|
||||
"body": "La mise à jour a été envoyée. Elle arrivera sur le téléphone du patient maintenant, ou à la prochaine ouverture de l'application."
|
||||
},
|
||||
"delivered": {
|
||||
"title": "Reçue sur le téléphone",
|
||||
"body": "Le patient a reçu la mise à jour et doit l'approuver sur son appareil."
|
||||
},
|
||||
"approved": {
|
||||
"title": "Approuvée par le patient",
|
||||
"body": "Le patient a approuvé la mise à jour ; son dossier sur l'appareil est maintenant à jour."
|
||||
},
|
||||
"denied": {
|
||||
"title": "Refusée par le patient",
|
||||
"body": "Le patient a refusé cette mise à jour ; son dossier sur l'appareil est inchangé."
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"appName": "temetro",
|
||||
"email": "E-mail",
|
||||
|
||||
@@ -43,6 +43,53 @@
|
||||
"generic": "Wax baa qaldamay. Fadlan isku day mar kale."
|
||||
}
|
||||
},
|
||||
"walletUpdatesList": {
|
||||
"title": "Cusboonaysiimaha loo diray walletyada",
|
||||
"description": "Cusboonaysiimaha diiwaanka ee aad u dirtay walletyada bukaannada, iyo haddii bukaanku ansixiyay.",
|
||||
"none": "Weli ma aadan dirin wax cusboonaysiin walletka ah."
|
||||
},
|
||||
"walletPush": {
|
||||
"action": "U dir walletka",
|
||||
"title": "U dir cusboonaysiinta walletka",
|
||||
"subtitle": "U dir diiwaanka la cusboonaysiiyay ee {{name}} abka walletkooda. Waa inay taleefankooda ku ansixiyaan ka hor inta uusan bedelin nuqulka qalabkooda ku jira.",
|
||||
"sectionsLabel": "Waxa isbedelay",
|
||||
"sections": {
|
||||
"demographics": "Xogta shakhsi",
|
||||
"problems": "Dhibaatooyinka",
|
||||
"medications": "Daawooyinka",
|
||||
"allergies": "Xasaasiyadda",
|
||||
"labs": "Baaritaannada",
|
||||
"vitals": "Calaamadaha muhiimka ah",
|
||||
"visits": "Booqashooyinka"
|
||||
},
|
||||
"noteLabel": "Qoraal (ikhtiyaari)",
|
||||
"notePlaceholder": "Qoraal gaaban oo ku saabsan cusboonaysiintan…",
|
||||
"notice": "Tan waxay dirtaa diiwaanka hadda oo dhan, oo lagu saxeexay furaha rugtaada. Waxay bedeshaa nuqulka qalabka bukaanka oo keliya ka dib marka ay ansixiyaan.",
|
||||
"cancel": "Jooji",
|
||||
"send": "Dir cusboonaysiinta",
|
||||
"done": "Dhammaystiran",
|
||||
"errors": {
|
||||
"generic": "Lama diri karin cusboonaysiinta. Fadlan isku day mar kale."
|
||||
},
|
||||
"status": {
|
||||
"pending": {
|
||||
"title": "Sugaya bukaanka",
|
||||
"body": "Cusboonaysiintii waa la diray. Waxay ku iman doontaa taleefanka bukaanka hadda, ama marka xigta ee ay furaan abka."
|
||||
},
|
||||
"delivered": {
|
||||
"title": "La gaarsiiyay taleefanka",
|
||||
"body": "Bukaanku wuu helay cusboonaysiinta wuxuuna u baahan yahay inuu ku ansixiyo qalabkiisa."
|
||||
},
|
||||
"approved": {
|
||||
"title": "Bukaanku wuu ansixiyay",
|
||||
"body": "Bukaanku wuu ansixiyay cusboonaysiinta, diiwaanka qalabkooduna hadda waa cusub yahay."
|
||||
},
|
||||
"denied": {
|
||||
"title": "Bukaanku wuu diiday",
|
||||
"body": "Bukaanku wuu diiday cusboonaysiintan, sidaas darteed diiwaanka qalabkoodu isma bedelin."
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"appName": "temetro",
|
||||
"email": "Iimayl",
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Client for the clinic→wallet record-update push. When a clinician edits a
|
||||
// wallet-linked patient, they can push the updated record to the patient's app;
|
||||
// it stays pending until the patient approves it on their phone.
|
||||
|
||||
import { apiFetch } from "@/lib/api-client";
|
||||
|
||||
export type WalletUpdateStatus = "pending" | "delivered" | "approved" | "denied";
|
||||
|
||||
export type WalletUpdate = {
|
||||
id: string;
|
||||
fileNumber: string;
|
||||
walletNumber: string;
|
||||
status: WalletUpdateStatus;
|
||||
changes: string[];
|
||||
createdAt: string;
|
||||
deliveredAt: string | null;
|
||||
resolvedAt: string | null;
|
||||
};
|
||||
|
||||
// Resolve the wallet a patient is linked to. Rejects (404) when not wallet-backed.
|
||||
export function getWalletLink(
|
||||
fileNumber: string,
|
||||
): Promise<{ walletNumber: string }> {
|
||||
return apiFetch<{ walletNumber: string }>(
|
||||
`/api/patients/wallet/link/${encodeURIComponent(fileNumber)}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function pushWalletUpdate(input: {
|
||||
fileNumber: string;
|
||||
changes: string[];
|
||||
}): Promise<WalletUpdate> {
|
||||
return apiFetch<WalletUpdate>("/api/patients/wallet/push", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function listWalletUpdates(): Promise<WalletUpdate[]> {
|
||||
return apiFetch<WalletUpdate[]>("/api/patients/wallet/updates");
|
||||
}
|
||||
|
||||
export function getWalletUpdate(id: string): Promise<WalletUpdate> {
|
||||
return apiFetch<WalletUpdate>(
|
||||
`/api/patients/wallet/updates/${encodeURIComponent(id)}`,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user