feat(modules): unify Vorsorge, Illness and Labs with the medication pattern

- add a wrench beside each module's add button linking to the module's own
  settings page; rename every add button to a plain "hinzufügen"
- give each module a settings page: view (cards/list) + manual reorder; Labs
  also gets a sort-order choice (by last reading) and biomarker CRUD/reorder
- collapse the Illness "abgeschlossen" list by default
- rebuild the Vorsorge card on the medication card's building blocks: a green
  "Jetzt messen" action, the interval as a chip beside the name, and an
  in-interval progress signal; drop the kebab "anpassen" and stray actions
- persist per-module view/order/sort client-side; add reverse-coverage-clean
  strings across all six locales
This commit is contained in:
Marc-André Bombeck committed 2026-06-18 21:36:58 +02:00
1 parent ae3091df9e
commit 267dcad13d
22 files changed
+1834 -273

No files matched your search

@@ -0,0 +1,69 @@
"use client";
import { LayoutGrid, List as ListIcon } from "lucide-react";
import { useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
import type { ModuleListView } from "@/lib/module-list-prefs";
/**
* v1.18.6 (W8 / MOD-03) — icon-only card/list view toggle shared by the
* Vorsorge / Illness / Labs settings pages. Mirrors the medication module's
* `MedicationViewToggle` grammar (two ghost segments, the active one filled,
* `aria-pressed` per segment) but switches between a card grid and a compact
* list rather than cards-vs-table, so the label set is `moduleList.view*`.
*/
interface ModuleViewToggleProps {
view: ModuleListView;
onChange: (view: ModuleListView) => void;
}
export function ModuleViewToggle({ view, onChange }: ModuleViewToggleProps) {
const { t } = useTranslations();
const segments = [
{
value: "cards" as const,
label: t("moduleList.viewCards"),
icon: LayoutGrid,
},
{
value: "list" as const,
label: t("moduleList.viewList"),
icon: ListIcon,
},
];
return (
<div
role="group"
aria-label={t("moduleList.viewToggleLabel")}
className="border-border bg-background/30 inline-flex shrink-0 items-center rounded-md border p-0.5"
>
{segments.map(({ value, label, icon: Icon }) => {
const active = view === value;
return (
<button
key={value}
type="button"
onClick={() => {
if (!active) onChange(value);
}}
aria-pressed={active}
aria-label={label}
title={label}
data-slot={`module-view-${value}`}
className={cn(
"focus-visible:ring-ring inline-flex size-10 items-center justify-center rounded-[5px] transition-colors focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none sm:size-8",
active
? "bg-secondary text-secondary-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground",
)}
>
<Icon className="size-4" aria-hidden="true" />
</button>
);
})}
</div>
);
}