mirror of
https://github.com/temetro/temetro.git
synced 2026-08-31 12:58:08 +00:00
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:
@@ -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>;
|
||||
Reference in New Issue
Block a user