chore: release v1.5.0 (#67)

* chore: release v1.4.0

* feat: timezone-aware locale formatting

- Add TIMEZONE_TO_REGION map for 50+ timezones worldwide
- Combine app language with timezone region (e.g., en + Europe/Berlin → en-DE)
- Fix times displaying in wrong timezone (treated as UTC instead of local)
- Add parseLocalDateTime() to handle ISO strings without UTC conversion
- Users now get regional formatting (24h time, local date format) regardless of app language
- Swedish user with en-SE locale now gets yyyy-mm-dd format and 24h time
- German user with en-DE locale gets dd.mm.yyyy format and 24h time
- Add missing i18n translation key 'lastSent'
- Update all getSystemLocale() calls to pass app language parameter

* chore: release v1.5.0

* fix: timezone-independent test for CI (use 14:00 instead of 22:00)

* fix: make timezone test independent of server timezone
This commit is contained in:
Daniel Volz
2026-01-23 21:42:57 +01:00
committed by GitHub
parent 0a4f8c5948
commit 8e2fd0a761
26 changed files with 1830 additions and 108 deletions
+47 -10
View File
@@ -15,6 +15,7 @@ import type {
ScheduleEvent,
} from "../types";
import { buildSchedulePreview, calculateCoverage } from "../utils/schedule";
import { getSystemLocale } from "../utils/formatters";
// =============================================================================
// Types
@@ -261,22 +262,23 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
}
}, [medications.meds, selectedMed]);
// Computed values
// Computed values - combine app language with timezone region for locale
const systemLocale = getSystemLocale(i18n.language);
const schedule = useMemo(
() => buildSchedulePreview(medications.meds, i18n.language, true),
[medications.meds, i18n.language]
() => buildSchedulePreview(medications.meds, systemLocale, true),
[medications.meds, systemLocale]
);
const coverage = useMemo(
() => calculateCoverage(
medications.meds,
schedule.events,
i18n.language,
systemLocale,
settingsHook.settings.reminderDaysBefore,
settingsHook.settings.stockCalculationMode,
doses.takenDoses
),
[medications.meds, schedule.events, i18n.language, settingsHook.settings.reminderDaysBefore, settingsHook.settings.stockCalculationMode, doses.takenDoses]
[medications.meds, schedule.events, systemLocale, settingsHook.settings.reminderDaysBefore, settingsHook.settings.stockCalculationMode, doses.takenDoses]
);
const depletionByMed = useMemo(
@@ -337,18 +339,53 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
const pastDays = useMemo(() => groupedSchedule.filter(d => d.isPast), [groupedSchedule]);
const futureDays = useMemo(() => groupedSchedule.filter(d => !d.isPast).slice(0, scheduleDays), [groupedSchedule, scheduleDays]);
// Build a map of medId -> end-of-day timestamp of last dismissed dose
// When user dismisses doses and then changes the schedule, old dismissed IDs no longer match
// Compare by DAY (end of day) so time changes within a day don't cause doses to reappear
const dismissedUntilByMed = useMemo(() => {
const map = new Map<string, number>();
for (const doseId of doses.dismissedDoses) {
// Format: medId-blisterIdx-timestamp or medId-blisterIdx-timestamp-person
const parts = doseId.split("-");
if (parts.length >= 3) {
const medId = parts[0];
const timestamp = parseInt(parts[2], 10);
if (!isNaN(timestamp)) {
// Convert to end of that day (23:59:59.999) for day-level comparison
const date = new Date(timestamp);
date.setHours(23, 59, 59, 999);
const endOfDay = date.getTime();
const current = map.get(medId) ?? 0;
if (endOfDay > current) map.set(medId, endOfDay);
}
}
}
return map;
}, [doses.dismissedDoses]);
const missedPastDoseIds = useMemo(() => {
const totalPastDoses = pastDays.flatMap(d =>
d.meds.flatMap(m =>
m.doses.flatMap(dose =>
(dose.takenBy || []).length > 0
m.doses.flatMap(dose => {
// Check if this dose is before the dismissed threshold for this medication
const parts = dose.id.split("-");
const medId = parts[0];
const timestamp = parts.length >= 3 ? parseInt(parts[2], 10) : 0;
const dismissedUntil = dismissedUntilByMed.get(medId) ?? 0;
// If this dose's day is at or before the dismissed day, treat as dismissed
if (timestamp > 0 && timestamp <= dismissedUntil) {
return [];
}
return (dose.takenBy || []).length > 0
? dose.takenBy.map((p: string) => `${dose.id}-${p}`)
: [dose.id]
)
: [dose.id];
})
)
);
return totalPastDoses.filter(id => !doses.takenDoses.has(id) && !doses.dismissedDoses.has(id));
}, [pastDays, doses.takenDoses, doses.dismissedDoses]);
}, [pastDays, doses.takenDoses, doses.dismissedDoses, dismissedUntilByMed]);
// Modal helpers with browser history support
const openMedDetail = useCallback((med: Medication) => {