mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
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:
1 parent
37e539d0bf
commit
86a9ca2e1e
10 files changed
+57
-1
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user