mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
feat(charts): persist per-chart overlay prefs (default off; clean line)
Hook the overlay-controls dropdown into a TanStack Query mutation so
each chart's three toggle states (7-day trend / Trend arrow / Target
range) save per user and survive reloads. The prefs piggy-back on the
existing User.dashboardWidgetsJson blob with a `chartOverlayPrefs`
keyed by chart id, mirroring the v1.4.16 phase B8 comparison-baseline
pattern — no Prisma migration needed.
Wires up:
- GET /api/dashboard/widgets surfaces the resolved prefs in its
response so a fresh dashboard load knows which overlays to paint.
- PUT /api/dashboard/chart-overlay-prefs replaces a single chart's
prefs without round-tripping the whole layout.
- Existing PUT /api/dashboard/widgets preserves any chartOverlayPrefs
the client doesn't send, so saving widget visibility doesn't wipe
chart prefs.
- useChartOverlayPrefs hook reads + optimistically updates the cache.
- HealthChart, MoodChart, MedicationComplianceChart all consume the
hook, render the controls dropdown next to the range tabs, and gate
their three overlays behind the persisted toggles.
Default state for every chart, every flag: false. The clean line is
the new default; users opt into overlays they care about per chart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
75f340e82b
commit
d70a9b1ee9
17 files changed
+761
-132
No files matched your search
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* PUT /api/dashboard/chart-overlay-prefs — v1.4.18
|
||||
*
|
||||
* Persist a single chart's overlay-prefs (3 toggles) onto the existing
|
||||
* `User.dashboardWidgetsJson` blob. Marc rejected v1.4.16's always-on
|
||||
* chart overlays (gradient fill, personal-baseline reference line,
|
||||
* target-zone shading); the new pattern is per-chart switches that the
|
||||
* user opts into independently and that persist per user.
|
||||
*
|
||||
* Why a partial-update route instead of round-tripping the full layout:
|
||||
* the client only knows which chart it's flipping; making it re-send the
|
||||
* entire widget array would force every chart wrapper to read+write the
|
||||
* full layout state, which is wasteful and racy when multiple charts
|
||||
* are mutated simultaneously. The handler reads the current layout, merges
|
||||
* the new prefs for the supplied `chartKey`, and writes it back inside the
|
||||
* same request.
|
||||
*/
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { apiSuccess, apiError, safeJson } from "@/lib/api-response";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { prisma } from "@/lib/db";
|
||||
import {
|
||||
CHART_OVERLAY_KEYS,
|
||||
resolveDashboardLayout,
|
||||
serializeDashboardLayout,
|
||||
type DashboardLayout,
|
||||
} from "@/lib/dashboard-layout";
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
import { z } from "zod/v4";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
const prefsSchema = z.object({
|
||||
chartKey: z.enum(CHART_OVERLAY_KEYS),
|
||||
prefs: z.object({
|
||||
showTrendIndicator: z.boolean(),
|
||||
showTrendArrow: z.boolean(),
|
||||
showTargetRange: z.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAuth();
|
||||
|
||||
const { data: body, error: jsonError } = await safeJson(request);
|
||||
if (jsonError) return jsonError;
|
||||
|
||||
const parsed = prefsSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return apiError(parsed.error.issues[0].message, 422);
|
||||
}
|
||||
|
||||
// Read the existing layout, merge the new chart's prefs, and write
|
||||
// it back. Resolver normalises legacy / missing fields, so layouts
|
||||
// saved before v1.4.18 pick up the new field with default-empty
|
||||
// prefs without a one-off migration.
|
||||
const row = await prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
select: { dashboardWidgetsJson: true },
|
||||
});
|
||||
|
||||
const current = resolveDashboardLayout(row?.dashboardWidgetsJson);
|
||||
const next: DashboardLayout = {
|
||||
...current,
|
||||
chartOverlayPrefs: {
|
||||
...(current.chartOverlayPrefs ?? {}),
|
||||
[parsed.data.chartKey]: parsed.data.prefs,
|
||||
},
|
||||
};
|
||||
const normalized = serializeDashboardLayout(next);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
dashboardWidgetsJson: normalized as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
annotate({
|
||||
action: { name: "dashboard.chartOverlayPrefs.update" },
|
||||
meta: {
|
||||
chart_key: parsed.data.chartKey,
|
||||
// Track which toggles ended up on so we can audit drift later.
|
||||
flags_on: Object.entries(parsed.data.prefs)
|
||||
.filter(([, v]) => v)
|
||||
.map(([k]) => k)
|
||||
.join(","),
|
||||
},
|
||||
});
|
||||
|
||||
return apiSuccess({ saved: true });
|
||||
});
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
DEFAULT_DASHBOARD_LAYOUT,
|
||||
DASHBOARD_WIDGET_IDS,
|
||||
COMPARISON_BASELINES,
|
||||
CHART_OVERLAY_KEYS,
|
||||
type ChartOverlayPrefsMap,
|
||||
type DashboardLayout,
|
||||
} from "@/lib/dashboard-layout";
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
@@ -53,6 +55,19 @@ const layoutSchema = z.object({
|
||||
// on the layout blob per research §7 Q3 (no Prisma migration). Optional
|
||||
// so v1.4.15 clients that don't know the field can still PUT.
|
||||
comparisonBaseline: z.enum(COMPARISON_BASELINES).optional(),
|
||||
// v1.4.18 — per-chart overlay prefs (3 toggles per chart card).
|
||||
// Optional so older clients that don't know the field can still PUT;
|
||||
// the resolver coerces malformed values away from the layout blob.
|
||||
chartOverlayPrefs: z
|
||||
.record(
|
||||
z.enum(CHART_OVERLAY_KEYS),
|
||||
z.object({
|
||||
showTrendIndicator: z.boolean(),
|
||||
showTrendArrow: z.boolean(),
|
||||
showTargetRange: z.boolean(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const GET = apiHandler(async () => {
|
||||
@@ -78,7 +93,26 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
return apiError(parsed.error.issues[0].message, 422);
|
||||
}
|
||||
|
||||
const normalized = serializeDashboardLayout(parsed.data as DashboardLayout);
|
||||
// v1.4.18 — preserve any per-chart overlay prefs that the client
|
||||
// didn't send. The dashboard-layout PUT typically saves widget
|
||||
// visibility / order; chart prefs are PUT through their own route
|
||||
// (`/api/dashboard/chart-overlay-prefs`) and would otherwise be
|
||||
// wiped here on a subsequent layout save.
|
||||
let mergedChartOverlayPrefs: ChartOverlayPrefsMap | undefined =
|
||||
parsed.data.chartOverlayPrefs as ChartOverlayPrefsMap | undefined;
|
||||
if (mergedChartOverlayPrefs === undefined) {
|
||||
const existing = await prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
select: { dashboardWidgetsJson: true },
|
||||
});
|
||||
mergedChartOverlayPrefs =
|
||||
resolveDashboardLayout(existing?.dashboardWidgetsJson)
|
||||
.chartOverlayPrefs ?? {};
|
||||
}
|
||||
const normalized = serializeDashboardLayout({
|
||||
...parsed.data,
|
||||
chartOverlayPrefs: mergedChartOverlayPrefs,
|
||||
} as DashboardLayout);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
|
||||
Reference in new issue
Block a user