feat(comparison): comparison toggle persists baseline preference

Add a "Compare to" pulldown to the Settings → Dashboard section that
lets the user pick None / Last month / Last year. The preference rides
on the existing User.dashboardWidgetsJson blob — research §7 Q3 settled
on the JSON-pivot path so v1.4.16 ships migration-free.

The resolver defaults to "none" for legacy layouts and clamps unknown
values back to "none", so a stale client cannot poison the dashboard
with a baseline the renderer doesn't know how to draw. The PUT
endpoint accepts the new optional comparisonBaseline field; pre-B8
clients keep PUTing without it and see no behavioural change.

EN+DE i18n keys land for the toggle label, the three options, the
"unavailable" fallback, and the chart caption — Vormonat / Vorjahr per
Marc's brief.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 02:16:41 +02:00
1 parent 43204cbb86
commit 775df8cfe9
6 files changed
+223

No files matched your search

@@ -16,8 +16,17 @@ import { useTranslations } from "@/lib/i18n/context";
import {
type DashboardLayout,
type DashboardWidgetId,
type ComparisonBaseline,
COMPARISON_BASELINES,
DEFAULT_DASHBOARD_LAYOUT,
} from "@/lib/dashboard-layout";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const WIDGET_LABEL_KEYS: Record<DashboardWidgetId, string> = {
weight: "dashboard.weight",
@@ -115,6 +124,16 @@ export function DashboardLayoutSection({ id }: { id: string }) {
});
}
/**
* v1.4.16 phase B8 — comparison baseline picker. The toggle persists
* via the same `/api/dashboard/widgets` PUT the existing layout
* controls already use; saving rides through the same `Save` button.
*/
function setComparisonBaseline(value: ComparisonBaseline) {
if (!layout) return;
setDraft({ ...layout, comparisonBaseline: value });
}
function move(widgetId: DashboardWidgetId, delta: -1 | 1) {
if (!layout) return;
const sorted = [...layout.widgets].sort((a, b) => a.order - b.order);
@@ -157,6 +176,50 @@ export function DashboardLayoutSection({ id }: { id: string }) {
{t("dashboard.customizeSubtitle")}
</p>
{/* v1.4.16 phase B8 — comparison baseline picker. Lives at the top
of the section because it changes how every chart + tile below
renders, not just one. Mobile-friendly: full-width Select with
a 44px tap target on touch viewports per the Wave-C polish. */}
{layout && (
<div className="space-y-2">
<label
htmlFor="comparison-baseline"
className="text-foreground text-sm font-medium"
>
{t("comparison.toggleLabel")}
</label>
<Select
value={layout.comparisonBaseline ?? "none"}
onValueChange={(value) =>
setComparisonBaseline(value as ComparisonBaseline)
}
disabled={saveMutation.isPending}
>
<SelectTrigger
id="comparison-baseline"
className="min-h-11 w-full sm:w-72"
data-slot="comparison-baseline-trigger"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{COMPARISON_BASELINES.map((value) => (
<SelectItem
key={value}
value={value}
data-slot={`comparison-baseline-option-${value}`}
>
{t(`comparison.baseline.${value}`)}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-muted-foreground text-xs">
{t("comparison.toggleHint")}
</p>
</div>
)}
{isLoading || !layout ? (
<div className="text-muted-foreground flex items-center gap-2 text-sm">
<Loader2 className="h-4 w-4 animate-spin" />