mirror of
https://github.com/temetro/temetro.git
synced 2026-07-26 11:58:14 +00:00
e8f3ed9ffe
`npm run lint` reported 99 errors and 26 warnings across 65 files and had presumably been failing for a while — next.config.ts sets eslint.ignoreDuringBuilds, so the build never surfaced it. 68 were in vendored code: components/charts and components/ai-elements, pulled from upstream registries. Re-linting those reports upstream's style back at us, and "fixing" them means diverging and eating conflicts on every update — ai-elements already has exactly this carve-out on the TypeScript side via ignoreBuildErrors. Ignore both, plus the two registry files in components/ui (carousel publishes its api from an effect; the sidebar skeleton picks a random width). The other 31 were ours, nearly all react-hooks/set-state-in-effect on the same shape: an effect that re-seeds form state when a dialog opens or a selection changes. Moved to render-phase adjustment, which is both what React recommends and a real fix — the effect version paints one frame of the *previous* record's values before correcting itself. Two carried sharper bugs: the employee dialog could keep a typed password across a switch to another member, and use-wallet-sync could carry `linked` over to a newly-selected patient, briefly offering to push a record to someone else's wallet. The rest: useIsMobile and speech-support detection become useSyncExternalStore (correct on first paint, no mount flash); refs mirroring state are written in effects rather than during render; the care-team fetch moves into its effect behind a reload key, dropping an exhaustive-deps suppression. Two effects in chat-panel keep the rule disabled with a reason. Both are what effects are for: draining the queued-message buffer when the transport goes idle, and resolving ?thread from the URL — the latter mints an id with nanoid(), so moving it into render would just trade this error for a purity one. Also removes a dead /explore-era import and unused directives found on the way. lint now exits 0, with the ai-elements tsc carve-out unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
139 lines
4.3 KiB
TypeScript
139 lines
4.3 KiB
TypeScript
"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);
|
|
|
|
// Drop the previous patient's link state as soon as the selection changes,
|
|
// adjusted during render: `linked` gates the wallet step, so carrying it over
|
|
// for even one render would offer to push another patient's record.
|
|
const [prevFileNumber, setPrevFileNumber] = useState(fileNumber);
|
|
if (prevFileNumber !== fileNumber) {
|
|
setPrevFileNumber(fileNumber);
|
|
setLinked(false);
|
|
setChecking(Boolean(fileNumber));
|
|
}
|
|
|
|
// Resolve link status whenever the chosen patient changes. A 404 simply means
|
|
// "not wallet-backed", so failures collapse to `linked = false`.
|
|
useEffect(() => {
|
|
if (!fileNumber) return;
|
|
let active = 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]);
|
|
|
|
// 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: clean });
|
|
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, ensureLinked, push, reset };
|
|
}
|
|
|
|
export type UseWalletSync = ReturnType<typeof useWalletSync>;
|