feat: obsolete medication archiving, start date, and UI improvements (#215)
* feat: obsolete medication archiving, start date, and UI improvements - Add soft-archive (obsolete) for medications with dedicated section and toggle - Add medication start date field with date picker and validation - Add obsolete/reactivate API endpoints with proper auth - Filter obsolete meds from schedule, coverage, planner, and notifications - Improve UserFilterModal with intake schedules, stock badges, and click-to-open - Improve dashboard taken-by badges with per-intake bell icons - Add Escape key support to ConfirmModal and MobileEditModal - Fix Lightbox close button positioning near image - Add read-only mode support for MobileEditModal - DB migrations: 0008 (is_obsolete, obsolete_at), 0009 (medication_start_date) - All user-facing text uses i18n keys (en + de) * test: fix tests for obsolete medications and UI changes - Backend: add is_obsolete, obsolete_at, medication_start_date columns to test schemas - Backend: add test medication inserts in planner tests for active-med filtering - Frontend: update useMedications URL to include includeObsolete param - Frontend: fix MobileEditModal selectors and validation assertions - Frontend: add onClearUser prop to UserFilterModal test renders - Frontend: fix MedicationsPage and DashboardPage test assertions
This commit is contained in:
@@ -71,11 +71,56 @@ export function getRegionFromTimezone(): string | undefined {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locale for formatting based on app language and timezone region.
|
||||
* Combines app language (en/de) with region from timezone (DE/US/etc.)
|
||||
* Example: app=en + timezone=Europe/Berlin → en-DE (English text, German format)
|
||||
* Map region code to the region's primary language for date/number formatting.
|
||||
* This ensures dates use regional conventions (e.g., dots in Germany)
|
||||
* regardless of the app's UI language.
|
||||
*/
|
||||
const REGION_TO_LANG: Record<string, string> = {
|
||||
DE: "de",
|
||||
AT: "de",
|
||||
CH: "de",
|
||||
GB: "en",
|
||||
IE: "en",
|
||||
FR: "fr",
|
||||
ES: "es",
|
||||
IT: "it",
|
||||
NL: "nl",
|
||||
BE: "nl",
|
||||
PL: "pl",
|
||||
CZ: "cs",
|
||||
SE: "sv",
|
||||
NO: "nb",
|
||||
DK: "da",
|
||||
FI: "fi",
|
||||
GR: "el",
|
||||
PT: "pt",
|
||||
RU: "ru",
|
||||
UA: "uk",
|
||||
HU: "hu",
|
||||
RO: "ro",
|
||||
US: "en",
|
||||
CA: "en",
|
||||
MX: "es",
|
||||
BR: "pt",
|
||||
AR: "es",
|
||||
JP: "ja",
|
||||
CN: "zh",
|
||||
HK: "zh",
|
||||
SG: "en",
|
||||
KR: "ko",
|
||||
AE: "ar",
|
||||
IN: "en",
|
||||
AU: "en",
|
||||
NZ: "en",
|
||||
};
|
||||
|
||||
/**
|
||||
* Get locale for text-based date formatting (weekday names, month names).
|
||||
* Uses the app's UI language + timezone region so text appears in the app language
|
||||
* while regional conventions (day-first order) are respected.
|
||||
*
|
||||
* @param appLanguage - The app's UI language (e.g., 'en', 'de')
|
||||
* Example: app=en + timezone=Europe/Berlin → en-DE
|
||||
* → "Thu, 05. Feb." (English names, German order)
|
||||
*/
|
||||
export function getSystemLocale(appLanguage?: string): string {
|
||||
const region = getRegionFromTimezone();
|
||||
@@ -89,6 +134,25 @@ export function getSystemLocale(appLanguage?: string): string {
|
||||
return navigator.language || "en-US";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locale for purely numeric date/number formatting.
|
||||
* Uses the region's native language so separators match regional conventions
|
||||
* (e.g., dots in Germany: 14.02.2026, slashes in US: 02/14/2026).
|
||||
*
|
||||
* Only use this for numeric-only output (2-digit day/month/year, no text).
|
||||
* For output that includes weekday or month names, use getSystemLocale() instead.
|
||||
*/
|
||||
export function getNumericLocale(): string {
|
||||
const region = getRegionFromTimezone();
|
||||
|
||||
if (region) {
|
||||
const regionLang = REGION_TO_LANG[region] || "en";
|
||||
return `${regionLang}-${region}`;
|
||||
}
|
||||
|
||||
return navigator.language || "en-US";
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a number using the current locale with optional decimal places
|
||||
*/
|
||||
@@ -114,7 +178,7 @@ export function formatDateTime(iso: string | null | undefined, locale?: string):
|
||||
if (!match) return "-";
|
||||
|
||||
const [, year, month, day, hour, minute] = match;
|
||||
const effectiveLocale = locale ?? getSystemLocale();
|
||||
const effectiveLocale = locale ?? getNumericLocale();
|
||||
|
||||
// Create a date object for formatting, but use local timezone interpretation
|
||||
// by creating the date without the Z suffix
|
||||
@@ -136,6 +200,20 @@ export function formatDateTime(iso: string | null | undefined, locale?: string):
|
||||
return `${dateStr} ${timeStr}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date-only string (YYYY-MM-DD) or ISO datetime to a localized date (no time).
|
||||
*/
|
||||
export function formatDate(dateStr: string | null | undefined, locale?: string): string {
|
||||
if (!dateStr) return "-";
|
||||
const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (!match) return "-";
|
||||
const [, year, month, day] = match;
|
||||
const d = new Date(`${year}-${month}-${day}T00:00:00`);
|
||||
if (Number.isNaN(d.getTime())) return "-";
|
||||
const effectiveLocale = locale ?? getNumericLocale();
|
||||
return d.toLocaleDateString(effectiveLocale, { year: "numeric", month: "2-digit", day: "2-digit" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Pad a number to 2 digits with leading zero
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user