mirror of
https://github.com/temetro/temetro.git
synced 2026-07-26 20:08:14 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62a5f39683 | |||
| 6127a0ac42 | |||
| 352ca65838 | |||
| 44d653ffbd | |||
| 76da310766 |
@@ -7,6 +7,30 @@ for how releases are cut and published.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.14.2] — 2026-07-15
|
||||
|
||||
### Fixed
|
||||
- **Docker images build on ARM64 again.** Next 16's default Turbopack production build has no native
|
||||
bindings for `linux/arm64` in the Alpine image, so `docker compose up --build` failed with
|
||||
"Turbopack is not supported on this platform". The frontend now builds with Webpack
|
||||
(`next build --webpack`), which builds on every architecture (`frontend/package.json`).
|
||||
|
||||
## [0.14.1] — 2026-07-15
|
||||
|
||||
### Fixed
|
||||
- **"Send to wallet" now works from every dialog, not just the patient sheet.** The wallet step in
|
||||
the appointment, invoice, prescription, patient-edit, and scribe dialogs was gated on a wallet-link
|
||||
check that resolved asynchronously; if the clinician saved before it resolved (or it briefly
|
||||
failed), the dialog silently closed without pushing. The dialogs now await the link check before
|
||||
deciding, and an empty change summary can no longer be sent
|
||||
(`frontend/components/wallet/use-wallet-sync.ts`, `wallet-sync-step.tsx`).
|
||||
|
||||
### Changed
|
||||
- **Settings sections composed from `CardFrame` primitives.** Each settings section now builds on
|
||||
`CardFrameHeader`/`CardFrameTitle`/`CardFrameDescription` + a new `CardFramePanel` body, and the
|
||||
per-row `SettingsCard` renders a real `Card`, giving a consistent framed surface
|
||||
(`frontend/components/ui/card.tsx`, `frontend/components/settings/settings-parts.tsx`).
|
||||
|
||||
## [0.14.0] — 2026-07-13
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "temetro-backend",
|
||||
"version": "0.14.0",
|
||||
"version": "0.14.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "temetro backend — Express + Postgres API with Better Auth (email/password, organizations) and org-scoped patient records.",
|
||||
|
||||
@@ -195,7 +195,7 @@ export function AddAppointmentDialog({
|
||||
t("appointments.dialog.addedTitle"),
|
||||
`${selected.name} · ${time}`,
|
||||
);
|
||||
if (sync.linked) {
|
||||
if (await sync.ensureLinked()) {
|
||||
setWalletSummary(
|
||||
t("walletSync.summary.appointment", { date: keyOf(date), time }),
|
||||
);
|
||||
|
||||
@@ -396,7 +396,7 @@ export function PatientFormDialog({
|
||||
t("patientForm.updatedTitle"),
|
||||
t("patientForm.updatedBody", { name: saved.name }),
|
||||
);
|
||||
if (sync.linked) {
|
||||
if (await sync.ensureLinked()) {
|
||||
setStep("wallet");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ export function InvoiceFormDialog({
|
||||
})
|
||||
: await createInvoice(payload);
|
||||
onSaved(saved);
|
||||
if (sync.linked) {
|
||||
if (await sync.ensureLinked()) {
|
||||
setWalletSummary(
|
||||
mode === "edit"
|
||||
? t("walletSync.summary.invoiceUpdated", { number: saved.number })
|
||||
|
||||
@@ -233,7 +233,7 @@ export function ScribeDialog({
|
||||
const updated = await saveNote(patient.fileNumber, draft);
|
||||
notify.success(t("scribe.saved.title"), patient.name);
|
||||
onSaved(updated);
|
||||
if (sync.linked) {
|
||||
if (await sync.ensureLinked()) {
|
||||
setPhase("review");
|
||||
setWalletStep(true);
|
||||
} else {
|
||||
|
||||
@@ -347,7 +347,7 @@ export function AddPrescriptionDialog({
|
||||
t("prescriptions.dialog.addedTitle"),
|
||||
`${medication.trim()} · ${selected.name}`,
|
||||
);
|
||||
if (sync.linked) {
|
||||
if (await sync.ensureLinked()) {
|
||||
setWalletSummary(
|
||||
t("walletSync.summary.prescription", { drug: medication.trim() }),
|
||||
);
|
||||
|
||||
@@ -176,7 +176,7 @@ export function SigningPanel() {
|
||||
description={t("settings.network.description")}
|
||||
title={t("settings.network.title")}
|
||||
>
|
||||
<SettingsCard className="flex items-center justify-between gap-4 p-5">
|
||||
<SettingsCard className="flex flex-row items-center justify-between gap-4 p-5">
|
||||
<div className="min-w-0 space-y-0.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-medium">
|
||||
@@ -251,7 +251,7 @@ export function SigningPanel() {
|
||||
description={t("settings.signing.backupDescription")}
|
||||
title={t("settings.signing.backupTitle")}
|
||||
>
|
||||
<SettingsCard className="flex items-center justify-between gap-4 p-4">
|
||||
<SettingsCard className="flex flex-row items-center justify-between gap-4 p-4">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">
|
||||
{t("settings.signing.backupLabel")}
|
||||
|
||||
@@ -5,12 +5,13 @@ import { useState } from "react";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Card,
|
||||
CardFrame,
|
||||
CardFrameAction,
|
||||
CardFrameDescription,
|
||||
CardFrameHeader,
|
||||
CardFramePanel,
|
||||
CardFrameTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
@@ -43,7 +44,7 @@ export function SettingsFrame({
|
||||
) : null}
|
||||
{action ? <CardFrameAction>{action}</CardFrameAction> : null}
|
||||
</CardFrameHeader>
|
||||
<div className={cn("p-5", bodyClassName)}>{children}</div>
|
||||
<CardFramePanel className={bodyClassName}>{children}</CardFramePanel>
|
||||
</CardFrame>
|
||||
);
|
||||
}
|
||||
@@ -74,6 +75,10 @@ export function SettingsSection({
|
||||
);
|
||||
}
|
||||
|
||||
// A card surface used inside settings panels. Rendering a real COSS `Card` (with
|
||||
// `data-slot="card"`) keeps settings cards consistent with the rest of the app
|
||||
// and lets them pick up the frame's card treatment. `Card` is `flex flex-col`,
|
||||
// so row layouts must pass `flex-row` in their className.
|
||||
export function SettingsCard({
|
||||
className,
|
||||
children,
|
||||
@@ -81,11 +86,7 @@ export function SettingsCard({
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("rounded-2xl border border-border bg-card/30", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <Card className={className}>{children}</Card>;
|
||||
}
|
||||
|
||||
export function ToggleRow({
|
||||
@@ -103,7 +104,7 @@ export function ToggleRow({
|
||||
onCheckedChange?: (checked: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<SettingsCard className="flex items-center justify-between gap-4 px-4 py-3.5">
|
||||
<SettingsCard className="flex flex-row items-center justify-between gap-4 px-4 py-3.5">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">{title}</p>
|
||||
{description ? (
|
||||
|
||||
@@ -375,7 +375,7 @@ export function ProfilePanel() {
|
||||
description={t("settings.profile.dangerZoneDescription")}
|
||||
title={t("settings.profile.dangerZone")}
|
||||
>
|
||||
<SettingsCard className="flex items-center justify-between gap-4 p-4">
|
||||
<SettingsCard className="flex flex-row items-center justify-between gap-4 p-4">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">
|
||||
{t("settings.profile.deleteAccount")}
|
||||
|
||||
@@ -136,6 +136,26 @@ export function CardFrameFooter({
|
||||
});
|
||||
}
|
||||
|
||||
// The padded body of a frame: content that sits between the frame header and
|
||||
// footer. Mirrors the other CardFrame* subcomponents (useRender + data-slot) so
|
||||
// a frame can be composed entirely from primitives instead of a raw <div>.
|
||||
export function CardFramePanel({
|
||||
className,
|
||||
render,
|
||||
...props
|
||||
}: useRender.ComponentProps<"div">): React.ReactElement {
|
||||
const defaultProps = {
|
||||
className: cn("p-5", className),
|
||||
"data-slot": "card-frame-panel",
|
||||
};
|
||||
|
||||
return useRender({
|
||||
defaultTagName: "div",
|
||||
props: mergeProps<"div">(defaultProps, props),
|
||||
render,
|
||||
});
|
||||
}
|
||||
|
||||
export function CardHeader({
|
||||
className,
|
||||
render,
|
||||
|
||||
@@ -74,13 +74,43 @@ export function useWalletSync(fileNumber: string | null | undefined) {
|
||||
};
|
||||
}, [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 });
|
||||
const created = await pushWalletUpdate({ fileNumber, changes: clean });
|
||||
setUpdate(created);
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : "generic");
|
||||
@@ -96,7 +126,7 @@ export function useWalletSync(fileNumber: string | null | undefined) {
|
||||
setError(null);
|
||||
}, []);
|
||||
|
||||
return { linked, checking, state, update, error, push, reset };
|
||||
return { linked, checking, state, update, error, ensureLinked, push, reset };
|
||||
}
|
||||
|
||||
export type UseWalletSync = ReturnType<typeof useWalletSync>;
|
||||
|
||||
@@ -60,6 +60,9 @@ export function WalletSyncStep({
|
||||
const { t } = useTranslation();
|
||||
const { state, update, error, push } = sync;
|
||||
const status = update?.status ?? "pending";
|
||||
// The summary is the human-readable change the patient approves. Guard against
|
||||
// an empty one (the backend 400s on empty changes) with a translated fallback.
|
||||
const changeSummary = summary.trim() || t("walletSync.summaryFallback");
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -79,7 +82,7 @@ export function WalletSyncStep({
|
||||
{t("walletSync.changesLabel")}
|
||||
</span>
|
||||
<div className="rounded-lg border bg-muted/50 px-3 py-2 text-foreground text-sm">
|
||||
{summary}
|
||||
{changeSummary}
|
||||
</div>
|
||||
</div>
|
||||
{error && (
|
||||
@@ -119,7 +122,7 @@ export function WalletSyncStep({
|
||||
<Button onClick={onDone} type="button" variant="outline">
|
||||
{t("walletSync.skip")}
|
||||
</Button>
|
||||
<Button onClick={() => push([summary])} type="button">
|
||||
<Button onClick={() => push([changeSummary])} type="button">
|
||||
<Send className="size-4" />
|
||||
{t("walletSync.send")}
|
||||
</Button>
|
||||
|
||||
@@ -2257,6 +2257,7 @@
|
||||
"prescription": "وصفة جديدة: {{drug}}",
|
||||
"demographics": "تم تحديث البيانات الديموغرافية",
|
||||
"note": "ملاحظة سريرية جديدة"
|
||||
}
|
||||
},
|
||||
"summaryFallback": "تم تحديث السجل"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2237,6 +2237,7 @@
|
||||
"prescription": "Neues Rezept: {{drug}}",
|
||||
"demographics": "Stammdaten aktualisiert",
|
||||
"note": "Neue klinische Notiz"
|
||||
}
|
||||
},
|
||||
"summaryFallback": "Datensatz aktualisiert"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2237,6 +2237,7 @@
|
||||
"prescription": "New prescription: {{drug}}",
|
||||
"demographics": "Demographics updated",
|
||||
"note": "New clinical note"
|
||||
}
|
||||
},
|
||||
"summaryFallback": "Record updated"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2237,6 +2237,7 @@
|
||||
"prescription": "Nouvelle ordonnance : {{drug}}",
|
||||
"demographics": "Données démographiques mises à jour",
|
||||
"note": "Nouvelle note clinique"
|
||||
}
|
||||
},
|
||||
"summaryFallback": "Dossier mis à jour"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2237,6 +2237,7 @@
|
||||
"prescription": "Rijeeto cusub: {{drug}}",
|
||||
"demographics": "Xogta bukaanka la cusboonaysiiyay",
|
||||
"note": "Qoraal caafimaad cusub"
|
||||
}
|
||||
},
|
||||
"summaryFallback": "Diiwaanka waa la cusboonaysiiyay"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.14.0",
|
||||
"version": "0.14.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"build": "next build --webpack",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
"check-locales": "node scripts/check-locales.mjs"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "temetro",
|
||||
"version": "0.14.0",
|
||||
"version": "0.14.2",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"shadcn": "^4.11.0"
|
||||
|
||||
Reference in New Issue
Block a user