From e59113716205ca56e42b5261e409d522215de951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 4 Jun 2026 15:28:35 +0200 Subject: [PATCH] fix(dashboard): confirm before discarding a partly-filled quick-entry sheet An accidental overlay tap, Escape, or mobile swipe-down on a quick-entry sheet silently dropped any typed measurement, mood, or intake. Detect unsaved input at dismiss time and ask before closing; an explicit Cancel or a successful save still closes straight away. --- messages/de.json | 6 +++ messages/en.json | 6 +++ messages/es.json | 6 +++ messages/fr.json | 6 +++ messages/it.json | 6 +++ messages/pl.json | 6 +++ src/app/page.tsx | 96 ++++++++++++++++++++++++++++++++++++++++++++++-- 7 files changed, 129 insertions(+), 3 deletions(-) diff --git a/messages/de.json b/messages/de.json index 6153962cb..4d10544f1 100644 --- a/messages/de.json +++ b/messages/de.json @@ -278,6 +278,12 @@ "emptyDescription": "Lege zuerst ein Medikament mit Zeitfenster an, damit du die Einnahme protokollieren kannst.", "emptyCta": "Medikament anlegen" }, + "quickEntryDiscard": { + "title": "Eintrag verwerfen?", + "description": "Du hast nicht gespeicherte Eingaben. Formular schließen und das Getippte verwerfen?", + "confirm": "Verwerfen", + "cancel": "Weiter bearbeiten" + }, "compliance7d": "Einnahmetreue (7 Tage)", "mood": "Stimmung", "customizeTitle": "Dashboard-Layout", diff --git a/messages/en.json b/messages/en.json index 32190a699..6f7eee95c 100644 --- a/messages/en.json +++ b/messages/en.json @@ -278,6 +278,12 @@ "emptyDescription": "Create a medication with a schedule first, then come back here to log the intake.", "emptyCta": "Add medication" }, + "quickEntryDiscard": { + "title": "Discard this entry?", + "description": "You have unsaved input. Close the form and lose what you typed?", + "confirm": "Discard", + "cancel": "Keep editing" + }, "compliance7d": "Compliance (7 days)", "mood": "Mood", "customizeTitle": "Dashboard layout", diff --git a/messages/es.json b/messages/es.json index 95992ac86..31fbcc8a1 100644 --- a/messages/es.json +++ b/messages/es.json @@ -278,6 +278,12 @@ "emptyDescription": "Crea primero un medicamento con horario, después vuelve aquí para registrar la toma.", "emptyCta": "Añadir medicamento" }, + "quickEntryDiscard": { + "title": "¿Descartar esta entrada?", + "description": "Tienes datos sin guardar. ¿Cerrar el formulario y perder lo que escribiste?", + "confirm": "Descartar", + "cancel": "Seguir editando" + }, "compliance7d": "Cumplimiento (7 días)", "mood": "Estado de ánimo", "customizeTitle": "Diseño del panel", diff --git a/messages/fr.json b/messages/fr.json index bfa7b47a5..76808860d 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -278,6 +278,12 @@ "emptyDescription": "Crée d’abord un médicament avec un horaire, puis reviens ici pour saisir la prise.", "emptyCta": "Ajouter un médicament" }, + "quickEntryDiscard": { + "title": "Abandonner cette saisie ?", + "description": "Vous avez des données non enregistrées. Fermer le formulaire et perdre ce que vous avez saisi ?", + "confirm": "Abandonner", + "cancel": "Continuer la saisie" + }, "compliance7d": "Observance (7 jours)", "mood": "Humeur", "customizeTitle": "Mise en page du tableau", diff --git a/messages/it.json b/messages/it.json index d417705f0..55e954b18 100644 --- a/messages/it.json +++ b/messages/it.json @@ -278,6 +278,12 @@ "emptyDescription": "Crea prima un farmaco con un orario, poi torna qui per registrarne l’assunzione.", "emptyCta": "Aggiungi farmaco" }, + "quickEntryDiscard": { + "title": "Scartare questa voce?", + "description": "Hai dati non salvati. Chiudere il modulo e perdere ciò che hai digitato?", + "confirm": "Scarta", + "cancel": "Continua a modificare" + }, "compliance7d": "Aderenza (7 giorni)", "mood": "Umore", "customizeTitle": "Layout della dashboard", diff --git a/messages/pl.json b/messages/pl.json index 2a32f7986..4cf5a126a 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -278,6 +278,12 @@ "emptyDescription": "Najpierw dodaj lek z harmonogramem, a potem wróć tutaj, aby zarejestrować jego przyjęcie.", "emptyCta": "Dodaj lek" }, + "quickEntryDiscard": { + "title": "Odrzucić ten wpis?", + "description": "Masz niezapisane dane. Zamknąć formularz i utracić to, co wpisano?", + "confirm": "Odrzuć", + "cancel": "Kontynuuj edycję" + }, "compliance7d": "Przestrzeganie (7 dni)", "mood": "Nastrój", "customizeTitle": "Układ panelu", diff --git a/src/app/page.tsx b/src/app/page.tsx index f9f989b65..4b19333b6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -32,6 +32,16 @@ import { EmptyState } from "@/components/ui/empty-state"; import { ChartSkeleton } from "@/components/charts/chart-skeleton"; import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic"; import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { DropdownMenu, DropdownMenuContent, @@ -59,6 +69,37 @@ const DASHBOARD_QUERY_OPTS = { refetchOnWindowFocus: false, } as const; +/** + * v1.11.3 F3 — guard the quick-entry sheets against an accidental + * dismiss (overlay tap, Esc, mobile swipe-down) discarding a partly + * filled form. The forms live behind a stable `ResponsiveSheet` body + * slot and don't expose their dirty state, so we read it from the DOM + * at dismiss time: any visible text/number/date input or textarea that + * carries a value, or any checked checkbox/radio that isn't a default, + * counts as unsaved input. Cheap, synchronous, and runs only on the + * one mounted sheet body. + */ +function sheetBodyHasUnsavedInput(): boolean { + if (typeof document === "undefined") return false; + const body = document.querySelector( + '[data-slot="responsive-sheet-body"]', + ); + if (!body) return false; + const fields = body.querySelectorAll< + HTMLInputElement | HTMLTextAreaElement + >("input, textarea"); + for (const field of fields) { + if (field.disabled || field.type === "hidden") continue; + if (field.type === "checkbox" || field.type === "radio") { + const box = field as HTMLInputElement; + if (box.checked !== box.defaultChecked) return true; + continue; + } + if (field.value.trim() !== "") return true; + } + return false; +} + const MoodChart = dynamic( () => import("@/components/charts/mood-chart").then((mod) => ({ @@ -215,6 +256,22 @@ export default function DashboardPage() { // mobile soft keyboard. const [medicationIntakeFooterEl, setMedicationIntakeFooterEl] = useState(null); + // v1.11.3 F3 — when an open quick-entry sheet is dismissed with + // unsaved input, hold the close in this flag and surface a confirm + // instead of nulling the dialog outright. Cleared once the user + // confirms (close) or keeps editing (dismiss the confirm). + const [confirmDiscardOpen, setConfirmDiscardOpen] = useState(false); + + // Intercept a sheet dismiss: close immediately when the form is + // clean, otherwise keep the sheet open and ask before discarding. + function handleQuickEntryOpenChange(open: boolean) { + if (open) return; + if (sheetBodyHasUnsavedInput()) { + setConfirmDiscardOpen(true); + return; + } + setQuickEntryDialog(null); + } // v1.4.29 M5 — the three inline dashboard queries default to a // 0-ms `staleTime`, so a tab-focus-and-return triggered a refetch @@ -738,7 +795,7 @@ export default function DashboardPage() { {/* Quick Entry Sheets — bottom-sheet on ` !open && setQuickEntryDialog(null)} + onOpenChange={handleQuickEntryOpenChange} title={t("measurements.addMeasurement")} footer={
@@ -752,7 +809,7 @@ export default function DashboardPage() { !open && setQuickEntryDialog(null)} + onOpenChange={handleQuickEntryOpenChange} title={t("mood.addEntry")} footer={
} > @@ -768,7 +825,7 @@ export default function DashboardPage() { stays reachable above the soft keyboard on mobile. */} !open && setQuickEntryDialog(null)} + onOpenChange={handleQuickEntryOpenChange} title={t("dashboard.medicationIntakeQuickAdd.sheetTitle")} description={t("dashboard.medicationIntakeQuickAdd.sheetDescription")} footer={ @@ -782,6 +839,39 @@ export default function DashboardPage() { /> + {/* v1.11.3 F3 — confirm before discarding a partly-filled + quick-entry form when the sheet is dismissed by an overlay + tap, Escape, or a mobile swipe-down. Keeps the sheet open + until the user decides. */} + + + + + {t("dashboard.quickEntryDiscard.title")} + + + {t("dashboard.quickEntryDiscard.description")} + + + + + {t("dashboard.quickEntryDiscard.cancel")} + + { + setConfirmDiscardOpen(false); + setQuickEntryDialog(null); + }} + > + {t("dashboard.quickEntryDiscard.confirm")} + + + + + {(() => { type TrendEntry = { id: string; order: number; node: React.ReactNode }; const trendCards: TrendEntry[] = [];