mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
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.
This commit is contained in:
1 parent
ad3bbd5507
commit
e591137162
7 files changed
+129
-3
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+93
-3
@@ -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<HTMLElement>(
|
||||
'[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<HTMLDivElement | null>(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 `<md`, centred Dialog on `md+`. */}
|
||||
<ResponsiveSheet
|
||||
open={quickEntryDialog === "measurement"}
|
||||
onOpenChange={(open) => !open && setQuickEntryDialog(null)}
|
||||
onOpenChange={handleQuickEntryOpenChange}
|
||||
title={t("measurements.addMeasurement")}
|
||||
footer={
|
||||
<div ref={setMeasurementFooterEl} className="flex w-full" />
|
||||
@@ -752,7 +809,7 @@ export default function DashboardPage() {
|
||||
</ResponsiveSheet>
|
||||
<ResponsiveSheet
|
||||
open={quickEntryDialog === "mood"}
|
||||
onOpenChange={(open) => !open && setQuickEntryDialog(null)}
|
||||
onOpenChange={handleQuickEntryOpenChange}
|
||||
title={t("mood.addEntry")}
|
||||
footer={<div ref={setMoodFooterEl} className="flex w-full" />}
|
||||
>
|
||||
@@ -768,7 +825,7 @@ export default function DashboardPage() {
|
||||
stays reachable above the soft keyboard on mobile. */}
|
||||
<ResponsiveSheet
|
||||
open={quickEntryDialog === "medicationIntake"}
|
||||
onOpenChange={(open) => !open && setQuickEntryDialog(null)}
|
||||
onOpenChange={handleQuickEntryOpenChange}
|
||||
title={t("dashboard.medicationIntakeQuickAdd.sheetTitle")}
|
||||
description={t("dashboard.medicationIntakeQuickAdd.sheetDescription")}
|
||||
footer={
|
||||
@@ -782,6 +839,39 @@ export default function DashboardPage() {
|
||||
/>
|
||||
</ResponsiveSheet>
|
||||
|
||||
{/* 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. */}
|
||||
<AlertDialog
|
||||
open={confirmDiscardOpen}
|
||||
onOpenChange={setConfirmDiscardOpen}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("dashboard.quickEntryDiscard.title")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("dashboard.quickEntryDiscard.description")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
{t("dashboard.quickEntryDiscard.cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
setConfirmDiscardOpen(false);
|
||||
setQuickEntryDialog(null);
|
||||
}}
|
||||
>
|
||||
{t("dashboard.quickEntryDiscard.confirm")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{(() => {
|
||||
type TrendEntry = { id: string; order: number; node: React.ReactNode };
|
||||
const trendCards: TrendEntry[] = [];
|
||||
|
||||
Reference in new issue
Block a user