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:
Daniel Volz
2026-02-15 23:23:38 +01:00
committed by GitHub
parent c47a35d642
commit 4b697374f6
38 changed files with 2042 additions and 907 deletions
+83 -5
View File
@@ -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
*/