From ef21967d12c43fa605d2398f7ec8e9cbd4253a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 28 May 2026 23:34:08 +0200 Subject: [PATCH] refactor(meds): lift IntakeImportDialog out of the list page and bundle the inline-compliance prefix The list page was carrying two large dialogs (`IntakeImportDialog` and `ApiEndpointDialog`) whose state slots had no list-card trigger wired to them. The slots were only ever set from inside the dialogs themselves, so they read as dead code on the list page even though the dialog bodies themselves were healthy. `IntakeImportDialog` lifts to `src/components/medications/intake-import-dialog.tsx` as the canonical mount the detail-page intake-history header will import. `ApiEndpointDialog` no longer mounts on the list page; its final v1.5.5 home is the detail-page Settings sub-row (lands in a later commit). The list page drops the two state slots, the inline dialog bodies, and the now-unused imports. `medicationDependentKeys` also gains the `compliance-chart-inline` prefix. The per-medication inline compliance chart used to mount under `queryKeys.medicationComplianceChart(id)` (which expands to `["compliance-chart-inline", id]`) but the bundle did not match the prefix, so an intake mutation left the chart up to 60 s stale. The prefix now lands in the bundle so every detail-page mutation evicts the tile in one tick. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/medications/page.tsx | 614 +----------------- .../medications/intake-import-dialog.tsx | 251 +++++++ src/lib/__tests__/query-keys.test.ts | 10 + src/lib/query-keys.ts | 14 + 4 files changed, 287 insertions(+), 602 deletions(-) create mode 100644 src/components/medications/intake-import-dialog.tsx diff --git a/src/app/medications/page.tsx b/src/app/medications/page.tsx index 3b1c9eeac..1a8fc720c 100644 --- a/src/app/medications/page.tsx +++ b/src/app/medications/page.tsx @@ -1,15 +1,11 @@ "use client"; import { useEffect, useState } from "react"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { useRouter, useSearchParams } from "next/navigation"; import { useAuth } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; -import { - invalidateKeys, - medicationDependentKeys, - queryKeys, -} from "@/lib/query-keys"; +import { queryKeys } from "@/lib/query-keys"; import { parseScheduleRecurrence } from "@/lib/medication-schedule"; import { MedicationWizardDialog } from "@/components/medications/wizard/MedicationWizardDialog"; import type { MedicationPayload } from "@/components/medications/wizard/wizard-payload"; @@ -17,38 +13,7 @@ import { MedicationCard } from "@/components/medications/medication-card"; import { Glp1MedicationCard } from "@/components/medications/glp1-medication-card"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/ui/empty-state"; -import { Switch } from "@/components/ui/switch"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Textarea } from "@/components/ui/textarea"; -import { - MoreHorizontal, - Loader2, - Plus, - Pill, - Upload, - Copy, - RefreshCw, - RotateCcw, -} from "lucide-react"; +import { Loader2, Pill, Plus } from "lucide-react"; interface Schedule { id: string; @@ -102,11 +67,6 @@ export default function MedicationsPage() { const shouldOpenFromUrl = searchParams?.get("new") === "1"; const [dialogOpen, setDialogOpen] = useState(shouldOpenFromUrl); const [editingMed, setEditingMed] = useState(null); - const [importMedId, setImportMedId] = useState(null); - const [apiMed, setApiMed] = useState<{ - id: string; - name: string; - } | null>(null); useEffect(() => { if (shouldOpenFromUrl) { @@ -286,14 +246,15 @@ export default function MedicationsPage() { )} - {/* API Endpoint Dialog */} - setApiMed(null)} /> - - {/* Import Dialog */} - setImportMedId(null)} - /> + {/* v1.5.5 — the per-card kebab triggers used to open inline + IntakeImportDialog / ApiEndpointDialog mounts here, but both + state slots were only ever set from inside the dialogs + themselves (never from a list-card prop) so the inline + mounts were dead code. v1.5.5 retires both on the list page: + the import dialog lives in the detail-page intake-history + header (see `src/components/medications/intake-import-dialog.tsx`), + the API endpoint moves to the detail-page Settings → Externe + Integration sub-row. */} {/* v1.5.4 — modal-wizard mount. The same component drives both create (no initial) and edit (hydrates from the medication's @@ -341,554 +302,3 @@ function medicationToPayload(med: Medication): MedicationPayload { }; } -function IntakeImportDialog({ - medicationId, - onClose, -}: { - medicationId: string | null; - onClose: () => void; -}) { - const { t } = useTranslations(); - const [jsonText, setJsonText] = useState(""); - const [importing, setImporting] = useState(false); - const [result, setResult] = useState(null); - const [resultType, setResultType] = useState<"success" | "error" | null>( - null, - ); - const [selectedFileName, setSelectedFileName] = useState(null); - const [fileInputKey, setFileInputKey] = useState(0); - const queryClient = useQueryClient(); - - function resetImportForm() { - setJsonText(""); - setResult(null); - setResultType(null); - setSelectedFileName(null); - setFileInputKey((prev) => prev + 1); - } - - function handleClose() { - resetImportForm(); - onClose(); - } - - async function handleFileSelect(e: React.ChangeEvent) { - const file = e.target.files?.[0]; - if (!file) return; - - setResult(null); - setResultType(null); - - try { - const content = await file.text(); - JSON.parse(content); - setJsonText(content); - setSelectedFileName(file.name); - setResult(t("medications.importFileLoaded", { name: file.name })); - setResultType("success"); - } catch { - setResult(t("medications.importInvalidJson")); - setResultType("error"); - } - } - - async function handleImport() { - if (!medicationId || !jsonText.trim()) return; - setImporting(true); - setResult(null); - setResultType(null); - - try { - let data = JSON.parse(jsonText.trim()); - // Support both array and object-with-array - if (!Array.isArray(data)) { - const arrKey = Object.keys(data).find((k) => Array.isArray(data[k])); - if (arrKey) data = data[arrKey]; - else throw new Error(t("medications.importNoArray")); - } - - const res = await fetch( - `/api/medications/${medicationId}/intake/import`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(data), - }, - ); - - const json = await res.json(); - if (res.ok) { - const d = json.data; - setResult( - t("medications.importResult", { imported: d.imported }) + - (d.skippedDuplicates > 0 - ? `, ${t("medications.importDuplicatesSkipped", { count: d.skippedDuplicates })}` - : "") + - (d.skippedInvalid > 0 - ? `, ${t("medications.importInvalidSkipped", { count: d.skippedInvalid })}` - : ""), - ); - setResultType("success"); - void invalidateKeys(queryClient, medicationDependentKeys); - } else { - setResult(json.error || t("medications.importFailed")); - setResultType("error"); - } - } catch (err) { - setResult( - err instanceof SyntaxError - ? t("medications.importInvalidFormat") - : (err as Error).message || t("medications.importFailed"), - ); - setResultType("error"); - } finally { - setImporting(false); - } - } - - return ( - handleClose()}> - - - {t("medications.importIntakes")} - -
-

- {t("medications.importDescription")} -

-
- - - {selectedFileName && ( -

- {t("medications.importSelected", { name: selectedFileName })} -

- )} -
-
-            {`[
-  {"datum": "2026-02-14", "uhrzeit": "10:27:43", "zaehler": 523},
-  {"datum": "2026-02-14", "uhrzeit": "23:33:42", "zaehler": 524}
-]`}
-          
-