mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
feat(dashboard): open the day with a verdict
The dashboard now leads with a daily band: the greeting, the one item that matters right now — an overdue or upcoming dose, a very high blood-pressure reading, weight drifting from its range, a run of short nights, a quiet stretch, a score drop, or the briefing's key finding — with a single action, today's dose tally beside the next due time, and the health score as a ring on the right. The verdict resolves from a fixed nine-rung ladder on the client with the thresholds shared from the nudge engine through a dependency-free module, the blood-pressure rung uses fixed crisis floors rather than personal target ranges, and a cached next-due in the past never renders as overdue. The band reserves its exact footprint from the first paint, swaps in with the tiles in one pass, respects the hour-format preference, and can be turned off in the dashboard settings; the greeting moves out of the page header into the band.
This commit is contained in:
1 parent
ad33fa46ed
commit
ea29e9cf1b
20 files changed
+1451
-94
No files matched your search
@@ -1,5 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import {
|
||||
DEFAULT_DASHBOARD_LAYOUT,
|
||||
@@ -269,6 +271,76 @@ describe("<DashboardLayoutSection> — iOS-only id skip (v1.7.0)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Dashboard hero (Tagesüberblick) visibility switch — one toggle above
|
||||
* the widget list, persisted as `heroVisible` on the layout blob through
|
||||
* the SAME PUT mutation the widget rows use (the Save button flushes the
|
||||
* draft). SSR smoke assertions + source pins, matching the rest of this
|
||||
* suite (no DOM runtime for state mutation).
|
||||
*/
|
||||
describe("<DashboardLayoutSection> — hero visibility switch", () => {
|
||||
it("renders one hero switch above the widget list", () => {
|
||||
const html = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
const switches = html.match(/data-slot="hero-visible-switch"/g) ?? [];
|
||||
expect(switches).toHaveLength(1);
|
||||
// Above the list: the hero switch markup precedes the first widget row.
|
||||
expect(html.indexOf('data-slot="hero-visible-switch"')).toBeLessThan(
|
||||
html.indexOf('data-slot="widget-row"'),
|
||||
);
|
||||
});
|
||||
|
||||
it("labels the switch with the localised copy + description", () => {
|
||||
const de = render(<DashboardLayoutSection id="dashboard-layout" />, "de");
|
||||
expect(de).toContain("Tagesüberblick");
|
||||
expect(de).toContain(
|
||||
"Zeigt Score, Tagesfokus und Dosen-Status ganz oben auf dem Dashboard.",
|
||||
);
|
||||
const en = render(<DashboardLayoutSection id="dashboard-layout" />);
|
||||
expect(en).toContain("Daily overview");
|
||||
});
|
||||
|
||||
it("reflects heroVisible: default layout → checked, false → unchecked", () => {
|
||||
const checkedHtml = render(
|
||||
<DashboardLayoutSection id="dashboard-layout" />,
|
||||
);
|
||||
const checked = checkedHtml.match(
|
||||
/<button[^>]*data-slot="hero-visible-switch"[^>]*>/,
|
||||
);
|
||||
expect(checked).not.toBeNull();
|
||||
expect(checked![0]).toContain('data-state="checked"');
|
||||
|
||||
queryState.layout = { ...DEFAULT_DASHBOARD_LAYOUT, heroVisible: false };
|
||||
const uncheckedHtml = render(
|
||||
<DashboardLayoutSection id="dashboard-layout" />,
|
||||
);
|
||||
const unchecked = uncheckedHtml.match(
|
||||
/<button[^>]*data-slot="hero-visible-switch"[^>]*>/,
|
||||
);
|
||||
expect(unchecked).not.toBeNull();
|
||||
expect(unchecked![0]).toContain('data-state="unchecked"');
|
||||
});
|
||||
|
||||
it("persists through the existing PUT draft flow (source pins)", () => {
|
||||
const src = readFileSync(
|
||||
join(
|
||||
process.cwd(),
|
||||
"src/components/settings/dashboard-layout-section.tsx",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
// The toggle writes the flag onto the layout draft…
|
||||
expect(src).toMatch(
|
||||
/setDraft\(\{\s*\.\.\.layout,\s*heroVisible:\s*value\s*\}\)/,
|
||||
);
|
||||
// …which the Save button flushes via the one save mutation
|
||||
// (`apiPut("/api/dashboard/widgets", …)`) every other control uses.
|
||||
expect(src).toMatch(/onCheckedChange=\{\(v\) => setHeroVisible\(v\)\}/);
|
||||
expect(src).toMatch(
|
||||
/apiPut<DashboardLayout>\("\/api\/dashboard\/widgets", next\)/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.11.2 HIGH-1 — the 8 B5 ids are WRITABLE (in DASHBOARD_WIDGET_IDS so the
|
||||
* iOS pin PUT validates them) but have NO web render path, so the web
|
||||
|
||||
@@ -153,6 +153,8 @@ export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
// tooltip. One hint paragraph is rendered once at the bottom of the list
|
||||
// and referenced by every drag handle in this section.
|
||||
const dragHintId = useId();
|
||||
// Label ↔ switch hookup for the hero (Tagesüberblick) toggle below.
|
||||
const heroToggleId = useId();
|
||||
|
||||
// v1.4.47 W4 — sensors: pointer for mouse/touch, keyboard for Tab + Space
|
||||
// + arrow-key reordering. The KeyboardSensor still works for users who
|
||||
@@ -243,6 +245,16 @@ export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
setDraft({ ...layout, comparisonBaseline: value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard hero (daily verdict) visibility. Rides the layout blob
|
||||
* (`heroVisible`, default on) and persists through the same PUT the
|
||||
* widget toggles use — the Save button flushes it.
|
||||
*/
|
||||
function setHeroVisible(value: boolean) {
|
||||
if (!layout) return;
|
||||
setDraft({ ...layout, heroVisible: value });
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.4.48 M6a — the arrow buttons now delegate to `reorderWidgets()`
|
||||
* so the swap-and-renumber logic lives in exactly one place. The
|
||||
@@ -377,6 +389,33 @@ export function DashboardLayoutSection({ id }: { id: string }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hero (Tagesüberblick) visibility — one switch above the widget
|
||||
list because the band sits above every widget on the dashboard.
|
||||
Persists as `heroVisible` on the layout blob through the same
|
||||
PUT mutation the rows below use (Save flushes the draft). */}
|
||||
{layout && (
|
||||
<div className="border-border bg-background/30 flex min-h-12 items-center justify-between gap-3 rounded-md border px-3 py-2">
|
||||
<div className="min-w-0">
|
||||
<label
|
||||
htmlFor={heroToggleId}
|
||||
className="text-foreground text-sm font-medium"
|
||||
>
|
||||
{t("dashboard.heroToggleLabel")}
|
||||
</label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("dashboard.heroToggleDescription")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id={heroToggleId}
|
||||
checked={layout.heroVisible !== false}
|
||||
onCheckedChange={(v) => setHeroVisible(v)}
|
||||
disabled={saveMutation.isPending}
|
||||
data-slot="hero-visible-switch"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading || !layout ? (
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-sm">
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
|
||||
Reference in new issue
Block a user