feat(doctor-report): server filter respects per-section toggles + PDF skips disabled sections

This commit is contained in:
Marc-André Bombeck committed 2026-05-14 13:29:53 +02:00
1 parent 330be965ac
commit 759fdaefcd
4 files changed
+281 -2

No files matched your search

+24 -1
View File
@@ -11,6 +11,11 @@ import {
} from "@/lib/doctor-report-data";
import { prisma } from "@/lib/db";
import { renderDoctorReportPdfBytes } from "@/lib/doctor-report-pdf-core";
import {
DEFAULT_DOCTOR_REPORT_PREFS,
doctorReportPrefsSchema,
type DoctorReportPrefs,
} from "@/lib/validations/doctor-report-prefs";
import { getServerTranslator } from "@/lib/i18n/server-translator";
import { parseLocaleFromAcceptLanguage } from "@/lib/format-locale";
import { locales, type Locale } from "@/lib/i18n/config";
@@ -48,6 +53,18 @@ export const POST = apiHandler(async (request: NextRequest) => {
const rawPracticeName = body?.practiceName;
const practiceName = sanitisePracticeName(rawPracticeName);
// v1.4.25 W6c — section toggles. Malformed shapes silently fall back
// to documented defaults so a forward-compat client never lands a
// 422 on a benign drift.
const sectionsParsed = doctorReportPrefsSchema.safeParse(
body?.sections ?? {},
);
const sectionsInput = sectionsParsed.success ? sectionsParsed.data : {};
const sections: DoctorReportPrefs = {
...DEFAULT_DOCTOR_REPORT_PREFS,
...sectionsInput,
};
if (typeof rawPracticeName === "string" && practiceName !== null) {
try {
await prisma.user.update({
@@ -60,7 +77,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
}
const [data, userTz] = await Promise.all([
collectDoctorReportData(user.id, range, { practiceName }),
collectDoctorReportData(user.id, range, { practiceName, sections }),
resolveUserTimezone(user.id),
]);
@@ -76,6 +93,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
endDate: range.end.toISOString(),
locale,
practiceNameProvided: practiceName !== null,
// v1.4.25 W6c — record exactly which sections this report
// rendered, so a future privacy review can confirm mood was
// never rendered when the user opted out.
sections,
},
});
@@ -111,6 +132,8 @@ interface PdfRequestBody {
endDate?: unknown;
locale?: unknown;
practiceName?: unknown;
/** v1.4.25 W6c — per-section visibility toggles. */
sections?: unknown;
}
/**
+16 -1
View File
@@ -15,6 +15,7 @@ import {
sanitisePracticeName,
} from "@/lib/doctor-report-data";
import { prisma } from "@/lib/db";
import { doctorReportPrefsSchema } from "@/lib/validations/doctor-report-prefs";
/**
* Collect data for doctor report PDF generation (client-side).
@@ -51,6 +52,13 @@ export const POST = apiHandler(async (request: NextRequest) => {
?.practiceName;
const practiceName = sanitisePracticeName(rawPracticeName);
// v1.4.25 W6c — section toggles. Malformed shapes silently fall
// through to the documented defaults so a forward-compat client
// never lands a 422 on a benign drift.
const rawSections = (body as Record<string, unknown> | null)?.sections;
const sectionsParsed = doctorReportPrefsSchema.safeParse(rawSections ?? {});
const sections = sectionsParsed.success ? sectionsParsed.data : {};
// Persist the most-recent practice name as a user preference so the
// dialog can pre-fill it next time. We only write when the caller
// actually supplied a non-empty string — passing `null`/empty does NOT
@@ -67,7 +75,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
}
}
const data = await collectDoctorReportData(user.id, range, { practiceName });
const data = await collectDoctorReportData(user.id, range, {
practiceName,
sections,
});
await auditLog("doctor-report.generate", {
userId: user.id,
@@ -77,6 +88,10 @@ export const POST = apiHandler(async (request: NextRequest) => {
startDate: range.start.toISOString(),
endDate: range.end.toISOString(),
practiceNameProvided: practiceName !== null,
// v1.4.25 W6c — record which sections were included. The audit
// trail proves mood data was never aggregated when the toggle
// was off (privacy contract).
sections,
},
});