feat(export): promote Arztbericht hero card and shell on settings export

The doctor-report PDF is HealthLog's flagship "data out" path. The
v1.4.16 phase B7 layout treated it as one of five equal cards in the
grid; this change lifts it into a hero card at the top of
`/settings/export`. The remaining four export cards (Measurements CSV,
Medications CSV, Mood CSV, Full JSON backup) move under a dedicated
"Weitere Export-Optionen" / "Other export options" sub-heading.

The hero reuses the `hero-gradient + glow-purple` visual treatment from
the Insights hero strip, so the surface feels consistent across the
app. The CTA routes through the existing `<DoctorReportDialog>` +
`/api/doctor-report` flow — only the framing changed. Mobile-first: at
< sm the CTA falls onto its own row and clears the 44 px WCAG touch
floor (`h-11 sm:h-10`); the button carries an `aria-describedby` link
to the value statement so screen readers announce the framing
alongside the action label.
This commit is contained in:
Marc-André Bombeck committed 2026-05-17 14:26:24 +02:00
1 parent 5a6f921928
commit 0af77230c8
4 files changed
+312 -120

No files matched your search

@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
createMeasurementSchema,
getUnitForType,
listMeasurementsSchema,
validateMeasurementRange,
} from "../measurement";
@@ -173,4 +174,65 @@ describe("measurement validation", () => {
}
});
});
// v1.4.37 W7c — list-view "one row per day" mode for cumulative
// types. The schema pins the optional `groupBy` enum + `dayKey`
// shape; the route gates each branch on whether the resolved type
// is in `CUMULATIVE_HK_TYPES`. Tests below pin the parser surface.
describe("listMeasurementsSchema — v1.4.37 W7c groupBy + dayKey", () => {
it("accepts an omitted groupBy + dayKey (legacy per-sample list)", () => {
const parsed = listMeasurementsSchema.safeParse({});
expect(parsed.success).toBe(true);
if (parsed.success) {
expect(parsed.data.groupBy).toBeUndefined();
expect(parsed.data.dayKey).toBeUndefined();
}
});
it("accepts groupBy=day", () => {
const parsed = listMeasurementsSchema.safeParse({
type: "ACTIVITY_STEPS",
groupBy: "day",
});
expect(parsed.success).toBe(true);
if (parsed.success) {
expect(parsed.data.groupBy).toBe("day");
}
});
it("rejects an unknown groupBy value", () => {
const parsed = listMeasurementsSchema.safeParse({
type: "ACTIVITY_STEPS",
groupBy: "week",
});
expect(parsed.success).toBe(false);
});
it("accepts a well-formed YYYY-MM-DD dayKey", () => {
const parsed = listMeasurementsSchema.safeParse({
type: "ACTIVITY_STEPS",
dayKey: "2026-05-15",
});
expect(parsed.success).toBe(true);
if (parsed.success) {
expect(parsed.data.dayKey).toBe("2026-05-15");
}
});
it("rejects a malformed dayKey (DD.MM.YYYY)", () => {
const parsed = listMeasurementsSchema.safeParse({
type: "ACTIVITY_STEPS",
dayKey: "15.05.2026",
});
expect(parsed.success).toBe(false);
});
it("rejects a partial dayKey (missing day component)", () => {
const parsed = listMeasurementsSchema.safeParse({
type: "ACTIVITY_STEPS",
dayKey: "2026-05",
});
expect(parsed.success).toBe(false);
});
});
});
+16
View File
@@ -272,6 +272,22 @@ export const listMeasurementsSchema = z.object({
// when the rollup bucket set is empty for the requested window so
// brand-new accounts still see correct data on their first chart.
source: z.enum(["rollup"]).optional(),
// v1.4.37 W7c — list-view "one row per day" mode for cumulative
// types (steps, active energy, distance, flights, daylight). When
// `groupBy=day` is set and `type` is a cumulative HK type, the route
// returns one synthesised row per user-TZ day with `value` = SUM
// and `sampleCount` = number of per-sample rows behind the bucket.
// Omitted = legacy per-sample list behaviour (iOS contract stable).
groupBy: z.enum(["day"]).optional(),
// v1.4.37 W7c — drill-down to per-sample rows for a single day in
// the user's IANA timezone. Format `YYYY-MM-DD`; the route resolves
// the day boundary against the user's `User.timezone`. Used by the
// expandable list row to reveal the chunks that contributed to the
// collapsed daily total.
dayKey: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, "dayKey must be YYYY-MM-DD")
.optional(),
});
export const createBatchMeasurementSchema = z.object({