feat(profile): persist and expose the date-format preference

Accept dateFormat on the profile PATCH field-by-field, echo it from
the profile + /me reads, mirror it into the client auth user, and add
the field to the OpenAPI request/response schemas.
This commit is contained in:
Marc-André Bombeck committed 2026-06-26 12:15:09 +02:00
1 parent 37e539d0bf
commit 86a9ca2e1e
10 files changed
+57 -1

No files matched your search

+4
View File
@@ -96,6 +96,10 @@ export const GET = apiHandler(async () => {
// Hour-cycle display preference (AUTO follows the locale convention,
// H12 / H24 pin the cycle). Clients mirror this into their formatters.
timeFormat: user.timeFormat ?? "AUTO",
// Date-order display preference (AUTO follows the locale convention,
// DMY / MDY / YMD pin the field order). Clients mirror this into their
// formatters and the <DateField> primitive.
dateFormat: user.dateFormat ?? "AUTO",
lastReportPracticeName: user.lastReportPracticeName ?? null,
// v1.4.47 W3 — per-user Coach opt-out. Default `false` if the
// column is absent (partial-deploy rollback safety, see migration
+3
View File
@@ -42,6 +42,7 @@ export const GET = apiHandler(async () => {
locale: true,
timezone: true,
timeFormat: true,
dateFormat: true,
moodReminderEnabled: true,
fullName: true,
insurerName: true,
@@ -77,6 +78,7 @@ export const GET = apiHandler(async () => {
locale: dbUser?.locale ?? null,
timezone: dbUser?.timezone ?? "Europe/Berlin",
timeFormat: dbUser?.timeFormat ?? "AUTO",
dateFormat: dbUser?.dateFormat ?? "AUTO",
moodReminderEnabled: dbUser?.moodReminderEnabled ?? false,
fullName: dbUser?.fullName ?? null,
insurerName: dbUser?.insurerName ?? null,
@@ -114,6 +116,7 @@ export const PATCH = apiHandler(async (request: NextRequest) => {
locale: result.user.locale,
timezone: result.user.timezone,
timeFormat: result.user.timeFormat,
dateFormat: result.user.dateFormat,
moodReminderEnabled: result.user.moodReminderEnabled,
fullName: result.user.fullName,
insurerName: result.user.insurerName,
@@ -68,6 +68,7 @@ function buildUser(disableCoach: boolean): AuthUser {
glucoseUnit: null,
unitPreference: "metric",
timeFormat: "AUTO",
dateFormat: "AUTO",
disableCoach,
fullName: null,
insurerName: null,
@@ -70,6 +70,7 @@ function buildUser(modules: AuthUser["modules"]): AuthUser {
glucoseUnit: null,
unitPreference: "metric",
timeFormat: "AUTO",
dateFormat: "AUTO",
disableCoach: false,
fullName: null,
insurerName: null,
@@ -46,6 +46,7 @@ function buildUser(timeFormat: TimeFormatPreference): AuthUser {
glucoseUnit: null,
unitPreference: "metric",
timeFormat,
dateFormat: "AUTO",
disableCoach: false,
fullName: null,
insurerName: null,
@@ -49,6 +49,7 @@ function buildUser(unitPreference: "metric" | "imperial"): AuthUser {
glucoseUnit: null,
unitPreference,
timeFormat: "AUTO",
dateFormat: "AUTO",
disableCoach: false,
fullName: null,
insurerName: null,
+23 -1
View File
@@ -7,8 +7,12 @@ import { queryKeys } from "@/lib/query-keys";
import { apiGet, apiFetchRaw } from "@/lib/api/api-fetch";
import { retryOnceOnTransientError } from "@/lib/queries/retry-transient";
import { useTranslations } from "@/lib/i18n/context";
import type { TimeFormatPreference } from "@/lib/format-locale";
import type {
TimeFormatPreference,
DateFormatPreference,
} from "@/lib/format-locale";
import { isTimeFormatPreference, storeTimeFormat } from "@/lib/time-format";
import { isDateFormatPreference, storeDateFormat } from "@/lib/date-format";
import type { ModuleKey } from "@/lib/modules/registry";
import type { TourProgress } from "@/lib/onboarding/tour-progress";
import { clearOfflineCachesForSessionEnd } from "@/lib/pwa/query-persister";
@@ -75,6 +79,14 @@ export interface AuthUser {
* "AUTO" against a stale /me payload.
*/
timeFormat: TimeFormatPreference;
/**
* Date-order display preference. AUTO follows the locale convention
* (de → dd.MM.yyyy, en → MM/dd/yyyy); DMY / MDY / YMD pin the field order
* regardless of locale. Mirrored into localStorage by `fetchMe` so
* `useFormatters()` and the `<DateField>` primitive render the same order.
* Coerced to "AUTO" against a stale /me payload.
*/
dateFormat: DateFormatPreference;
/**
* v1.4.47 W3 — per-user Coach opt-out. When `true`, every Coach
* mount point (`<LayoutCoachFab>`, `<LayoutCoachMount>`, the
@@ -157,6 +169,15 @@ async function fetchMe(): Promise<AuthUser> {
? data.timeFormat
: "AUTO";
storeTimeFormat(timeFormat);
// Date-order preference: coerce against a stale /me payload, then mirror
// into localStorage so `useFormatters()` and the `<DateField>` primitive
// (which cannot reach the query cache) render the same field order.
const dateFormat: DateFormatPreference = isDateFormatPreference(
data.dateFormat,
)
? data.dateFormat
: "AUTO";
storeDateFormat(dateFormat);
return {
...(data as AuthUser),
disableCoach: data.disableCoach ?? false,
@@ -167,6 +188,7 @@ async function fetchMe(): Promise<AuthUser> {
// without the field) so the display defaults to metric.
unitPreference: data.unitPreference === "imperial" ? "imperial" : "metric",
timeFormat,
dateFormat,
// v1.15.0 — coerce against a stale /me payload so the cycle nav entry
// stays hidden by default when the field is absent.
cycleTrackingEnabled: data.cycleTrackingEnabled === true,
+6
View File
@@ -27,6 +27,9 @@ const extendedProfileSchema = profileSchema.extend({
// Hour-cycle display preference. AUTO follows the locale convention,
// H12 forces AM/PM, H24 forces 24-hour.
timeFormat: z.enum(["AUTO", "H12", "H24"]).optional(),
// Date-order display preference. AUTO follows the locale convention,
// DMY pins day-month-year, MDY month-day-year, YMD ISO yyyy-MM-dd.
dateFormat: z.enum(["AUTO", "DMY", "MDY", "YMD"]).optional(),
});
export interface ApplyProfileResult {
@@ -43,6 +46,7 @@ export interface ApplyProfileResult {
timezone: string;
locale: string | null;
timeFormat: "AUTO" | "H12" | "H24";
dateFormat: "AUTO" | "DMY" | "MDY" | "YMD";
moodReminderEnabled: boolean;
// v1.7.0 — patient-identity fields. `insuranceNumber` is returned in
// plaintext (decrypted on read by the route) so the client can render
@@ -110,6 +114,7 @@ export async function applyProfileUpdate(
if (data.locale !== undefined) updates.locale = data.locale;
if (data.timezone !== undefined) updates.timezone = data.timezone;
if (data.timeFormat !== undefined) updates.timeFormat = data.timeFormat;
if (data.dateFormat !== undefined) updates.dateFormat = data.dateFormat;
if (data.moodReminderEnabled !== undefined) {
updates.moodReminderEnabled = data.moodReminderEnabled;
}
@@ -206,6 +211,7 @@ export async function applyProfileUpdate(
timezone: updatedUser.timezone,
locale: updatedUser.locale,
timeFormat: updatedUser.timeFormat ?? "AUTO",
dateFormat: updatedUser.dateFormat ?? "AUTO",
moodReminderEnabled: updatedUser.moodReminderEnabled,
fullName: updatedUser.fullName,
insurerName: updatedUser.insurerName,
@@ -95,6 +95,7 @@ function buildUser(disableCoach: boolean): AuthUser {
glucoseUnit: null,
unitPreference: "metric",
timeFormat: "AUTO",
dateFormat: "AUTO",
disableCoach,
fullName: null,
insurerName: null,
+16
View File
@@ -185,6 +185,12 @@ const profileUpdateRequest = z
.describe(
"Hour-cycle display preference. AUTO follows the locale convention, H12 forces AM/PM, H24 forces 24-hour.",
),
dateFormat: z
.enum(["AUTO", "DMY", "MDY", "YMD"])
.optional()
.describe(
"Date-order display preference. AUTO follows the locale convention, DMY pins day-month-year (dd.MM.yyyy), MDY pins month-day-year (MM/dd/yyyy), YMD pins ISO yyyy-MM-dd.",
),
moodReminderEnabled: z.boolean().optional(),
fullName: z.string().max(120).nullable().optional(),
insurerName: z.string().max(120).nullable().optional(),
@@ -222,6 +228,11 @@ const profileResponse = z
.describe(
"Hour-cycle display preference. AUTO follows the locale convention, H12 forces AM/PM, H24 forces 24-hour.",
),
dateFormat: z
.enum(["AUTO", "DMY", "MDY", "YMD"])
.describe(
"Date-order display preference. AUTO follows the locale convention, DMY pins day-month-year (dd.MM.yyyy), MDY pins month-day-year (MM/dd/yyyy), YMD pins ISO yyyy-MM-dd.",
),
moodReminderEnabled: z.boolean(),
fullName: z.string().nullable(),
insurerName: z.string().nullable(),
@@ -261,6 +272,11 @@ const profileUpdateResponse = z
.describe(
"Hour-cycle display preference. AUTO follows the locale convention, H12 forces AM/PM, H24 forces 24-hour.",
),
dateFormat: z
.enum(["AUTO", "DMY", "MDY", "YMD"])
.describe(
"Date-order display preference. AUTO follows the locale convention, DMY pins day-month-year (dd.MM.yyyy), MDY pins month-day-year (MM/dd/yyyy), YMD pins ISO yyyy-MM-dd.",
),
moodReminderEnabled: z.boolean(),
fullName: z.string().nullable(),
insurerName: z.string().nullable(),