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:
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useAuth } from "../components/Auth";
|
||||
import { useAppContext } from "../context";
|
||||
import { MedicationAvatar, ConfirmModal } from "../components";
|
||||
import { formatNumber, getExpiryClass } from "../utils/formatters";
|
||||
import { formatNumber, getExpiryClass, getSystemLocale } from "../utils/formatters";
|
||||
import type { Coverage } from "../types";
|
||||
|
||||
// Helper for user-specific localStorage keys
|
||||
@@ -32,9 +32,9 @@ function formatFullBlisters(count: number, t: (key: string) => string): string {
|
||||
}
|
||||
|
||||
// Helper to format open blister and loose pills
|
||||
function formatOpenBlisterAndLoose(openBlisterPills: number, loosePills: number, _pillsPerBlister: number, t: (key: string) => string): string {
|
||||
function formatOpenBlisterAndLoose(openBlisterPills: number, loosePills: number, pillsPerBlister: number, t: (key: string) => string): string {
|
||||
if (openBlisterPills === 0 && loosePills === 0) return "-";
|
||||
return `${openBlisterPills} ${t('common.pills')}`;
|
||||
return `${openBlisterPills} ${t('common.of')} ${pillsPerBlister} ${t('common.pills')}`;
|
||||
}
|
||||
|
||||
// Get total pills for a medication
|
||||
@@ -184,7 +184,7 @@ export function DashboardPage() {
|
||||
<span className="email-status-icon">{settings.emailEnabled && settings.shoutrrrEnabled ? "🔔" : settings.emailEnabled ? "📧" : "🔔"}</span>
|
||||
<span className="email-status-text">
|
||||
<span className="email-status-line">{t('dashboard.reminders.active')}</span>
|
||||
{getReminderStatusContent(settings.reminderDaysBefore, settings.lowStockDays, coverage.low, coverage.all, settings.lastAutoEmailSent, settings.lastNotificationType, settings.lastNotificationChannel, t, i18n.language)}
|
||||
{getReminderStatusContent(settings.reminderDaysBefore, settings.lowStockDays, coverage.low, coverage.all, settings.lastAutoEmailSent, settings.lastNotificationType, settings.lastNotificationChannel, t, getSystemLocale(i18n.language))}
|
||||
</span>
|
||||
{settings.emailEnabled && settings.notificationEmail && <span className="email-status-recipient">→ {settings.notificationEmail}</span>}
|
||||
</section>
|
||||
@@ -259,7 +259,7 @@ export function DashboardPage() {
|
||||
<span data-label={t('table.days')} className={textClass}>{formatNumber(row.daysLeft)}</span>
|
||||
<span data-label={t('table.status')} className={`status-chip ${status.className}`}>{t(status.label)}</span>
|
||||
<span data-label={t('table.runsOut')}>{row.depletionDate ?? "-"}</span>
|
||||
<span data-label={t('table.autoRemind')} className="next-reminder-date">{getNextReminderForMed(row, settings.reminderDaysBefore, i18n.language)}</span>
|
||||
<span data-label={t('table.autoRemind')} className="next-reminder-date">{getNextReminderForMed(row, settings.reminderDaysBefore, getSystemLocale(i18n.language))}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -322,18 +322,18 @@ export function DashboardPage() {
|
||||
<span className="med-icons">
|
||||
{med?.intakeRemindersEnabled && <span className="reminder-icon info-tooltip" data-tooltip={t('tooltips.intakeReminders')}>🔔</span>}
|
||||
{med?.notes && <span className="notes-icon info-tooltip" data-tooltip={t('tooltips.hasNotes')}>📝</span>}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span data-label={t('table.fullBlisters')} className={textClass}>{formatFullBlisters(stock.fullBlisters, t)}</span>
|
||||
<span data-label={t('table.openBlister')} className={textClass}>{formatOpenBlisterAndLoose(stock.openBlisterPills, stock.loosePills, med?.pillsPerBlister ?? 1, t)}</span>
|
||||
<span data-label={t('table.daysLeft')} className={textClass}>{formatNumber(row.daysLeft)}</span>
|
||||
<span data-label={t('table.runsOut')}>{row.depletionDate ?? "-"}</span>
|
||||
<span data-label={t('table.expiry')} className={expiryClass}>{med?.expiryDate ? new Date(med.expiryDate).toLocaleDateString(i18n.language, { day: "2-digit", month: "short", year: "2-digit" }) : "-"}</span>
|
||||
<span data-label={t('table.status')} className={`status-chip ${status.className}`}>{t(status.label)}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span data-label={t('table.fullBlisters')} className={textClass}>{formatFullBlisters(stock.fullBlisters, t)}</span>
|
||||
<span data-label={t('table.openBlister')} className={textClass}>{formatOpenBlisterAndLoose(stock.openBlisterPills, stock.loosePills, med?.pillsPerBlister ?? 1, t)}</span>
|
||||
<span data-label={t('table.daysLeft')} className={textClass}>{formatNumber(row.daysLeft)}</span>
|
||||
<span data-label={t('table.runsOut')}>{row.depletionDate ?? "-"}</span>
|
||||
<span data-label={t('table.expiry')} className={expiryClass}>{med?.expiryDate ? new Date(med.expiryDate).toLocaleDateString(getSystemLocale(i18n.language), { day: "2-digit", month: "short", year: "2-digit" }) : "-"}</span>
|
||||
<span data-label={t('table.status')} className={`status-chip ${status.className}`}>{t(status.label)}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useAppContext } from "../context";
|
||||
import { MedicationAvatar, MobileEditModal } from "../components";
|
||||
import { useMedicationForm } from "../hooks";
|
||||
import { formatNumber, formatDateTime } from "../utils/formatters";
|
||||
import { formatNumber, formatDateTime, combineDateAndTime } from "../utils/formatters";
|
||||
import { getPackageSize, FIELD_LIMITS } from "../types";
|
||||
import type { Medication } from "../types";
|
||||
|
||||
@@ -32,6 +32,7 @@ export function MedicationsPage() {
|
||||
const {
|
||||
form,
|
||||
setForm,
|
||||
setOriginalForm,
|
||||
editingId,
|
||||
setEditingId,
|
||||
formSaved,
|
||||
@@ -153,6 +154,9 @@ export function MedicationsPage() {
|
||||
// Reset form after successful save
|
||||
if (!editingId) {
|
||||
resetForm();
|
||||
} else {
|
||||
// Update originalForm so formChanged becomes false
|
||||
setOriginalForm(form);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Save error:", err);
|
||||
@@ -234,7 +238,7 @@ export function MedicationsPage() {
|
||||
<div className="blister-list">
|
||||
{med.blisters.map((s, idx) => (
|
||||
<div key={`${med.id}-${idx}`} className="blister-row-simple">
|
||||
{s.usage} {s.usage === 1 ? t('common.pill') : t('common.pills')} · {t('form.blisters.every')} {s.every} {s.every === 1 ? t('common.day') : t('common.days')} · {t('form.blisters.from')} {formatDateTime(s.start, i18n.language)}
|
||||
{s.usage} {s.usage === 1 ? t('common.pill') : t('common.pills')} · {t('form.blisters.every')} {s.every} {s.every === 1 ? t('common.day') : t('common.days')} · {t('form.blisters.from')} {formatDateTime(s.start)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -480,7 +484,7 @@ export function MedicationsPage() {
|
||||
</button>
|
||||
)}
|
||||
<button type="submit" disabled={saving || hasValidationErrors || (!formChanged && (formSaved || !!editingId))}>
|
||||
{saving ? t('common.saving') : formSaved && !formChanged ? t('common.saved') : t('common.save')}
|
||||
{formSaved && !formChanged ? t('common.saved') : t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -524,7 +528,4 @@ export function MedicationsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to combine date and time into ISO datetime with Z suffix
|
||||
function combineDateAndTime(date: string, time: string): string {
|
||||
return `${date}T${time}:00.000Z`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAppContext } from "../context";
|
||||
import { ConfirmModal, ExportModal } from "../components";
|
||||
import { getSystemLocale } from "../utils/formatters";
|
||||
|
||||
export function SettingsPage() {
|
||||
const { t, i18n } = useTranslation();
|
||||
@@ -321,13 +322,13 @@ export function SettingsPage() {
|
||||
{settings.nextScheduledCheck && (
|
||||
<div className="schedule-row">
|
||||
<span className="schedule-label">{t('settings.schedule.nextCheck')}</span>
|
||||
<span className="schedule-value">{new Date(settings.nextScheduledCheck).toLocaleString(i18n.language, { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" })}</span>
|
||||
<span className="schedule-value">{new Date(settings.nextScheduledCheck).toLocaleString(getSystemLocale(i18n.language), { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" })}</span>
|
||||
</div>
|
||||
)}
|
||||
{settings.lastAutoEmailSent && (
|
||||
<div className="schedule-row">
|
||||
<span className="schedule-label">{t('settings.schedule.lastSent')}</span>
|
||||
<span className="schedule-value">{new Date(settings.lastAutoEmailSent).toLocaleString(i18n.language, { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" })}</span>
|
||||
<span className="schedule-value">{new Date(settings.lastAutoEmailSent).toLocaleString(getSystemLocale(i18n.language), { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" })}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user