diff --git a/.env.example b/.env.example index d85bd88..3ec2a9c 100644 --- a/.env.example +++ b/.env.example @@ -31,5 +31,10 @@ SMTP_SECURE=false # Rate limits EMAILS_PER_DAY=3 -# Default value only - frontend settings (stored in settings.json) take precedence -REMINDER_DAYS_BEFORE=7 \ No newline at end of file +# Admin settings default value only - frontend settings (stored in settings.json) take precedence +REMINDER_DAYS_BEFORE=7 + +# Admin settings (not editable in UI) +REMINDER_HOUR=6 # 24h format (0-23), e.g. 6 = 6:00 AM, 18 = 6:00 PM +REMINDER_MINUTES_BEFORE=15 # Minutes before intake to send reminder +EXPIRY_WARNING_DAYS=30 # Days before expiry to show yellow warning \ No newline at end of file diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts index ec0420e..5458a43 100644 --- a/backend/src/routes/settings.ts +++ b/backend/src/routes/settings.ts @@ -142,6 +142,8 @@ export async function settingsRoutes(app: FastifyInstance) { nextScheduledCheck: reminderState.nextScheduledCheck, lastNotificationType: reminderState.lastNotificationType, lastNotificationChannel: reminderState.lastNotificationChannel, + // Admin settings (from .env, read-only) + expiryWarningDays: parseInt(process.env.EXPIRY_WARNING_DAYS ?? "30", 10), }); }); diff --git a/backend/src/services/intake-reminder-scheduler.ts b/backend/src/services/intake-reminder-scheduler.ts index b10db69..9889f40 100644 --- a/backend/src/services/intake-reminder-scheduler.ts +++ b/backend/src/services/intake-reminder-scheduler.ts @@ -13,7 +13,7 @@ type IntakeReminderState = { sentReminders: string[]; // Array of "medName:timestamp" to track sent reminders }; -const REMINDER_MINUTES_BEFORE = 15; +const REMINDER_MINUTES_BEFORE = parseInt(process.env.REMINDER_MINUTES_BEFORE ?? "15", 10); const CHECK_INTERVAL_MS = 60 * 1000; // Check every 1 minute // Get current timezone from TZ env variable or default to UTC diff --git a/backend/src/services/reminder-scheduler.ts b/backend/src/services/reminder-scheduler.ts index 2299ca2..d00945a 100644 --- a/backend/src/services/reminder-scheduler.ts +++ b/backend/src/services/reminder-scheduler.ts @@ -36,7 +36,7 @@ type ReminderState = { lastNotificationChannel: "email" | "push" | "both" | null; // Channel used for last notification }; -const REMINDER_HOUR = 6; // 6:00 AM local time +const REMINDER_HOUR = parseInt(process.env.REMINDER_HOUR ?? "6", 10); // Default 6:00 AM local time // Get current timezone from TZ env variable or default to UTC function getTimezone(): string { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 67393bd..6b68aa0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -136,6 +136,8 @@ export default function App() { emailIntakeReminders: true, shoutrrrStockReminders: true, shoutrrrIntakeReminders: true, + // Admin settings (from .env, read-only) + expiryWarningDays: 30, }); const [savedSettings, setSavedSettings] = useState(settings); const [settingsLoading, setSettingsLoading] = useState(false); @@ -224,7 +226,7 @@ export default function App() { const schedule = useMemo(() => buildSchedulePreview(meds, i18n.language), [meds, i18n.language]); const totalTablets = useMemo(() => deriveTotal(form), [form]); - const coverage = useMemo(() => calculateCoverage(meds, schedule.events, i18n.language), [meds, schedule.events, i18n.language]); + const coverage = useMemo(() => calculateCoverage(meds, schedule.events, i18n.language, settings.reminderDaysBefore), [meds, schedule.events, i18n.language, settings.reminderDaysBefore]); const depletionByMed = useMemo(() => Object.fromEntries(coverage.all.map((c) => [c.name, c.depletionTime])), [coverage.all]); const coverageByMed = useMemo(() => Object.fromEntries(coverage.all.map((c) => [c.name, c])), [coverage.all]); const groupedSchedule = useMemo(() => { @@ -615,13 +617,32 @@ export default function App() {
{t('dashboard.reorder.noMeds')}
- ) : coverage.low.length === 0 ? ( -{t('dashboard.reorder.allGood')}
- ) : ( - <> -{t('dashboard.reorder.noMeds')}
; + } + + // Count medications with "Low" stock status (based on lowStockDays setting) + const lowStockCount = coverage.all.filter(c => { + if (c.medsLeft <= 0) return true; // out of stock + if (c.daysLeft === null) return false; // no schedule + return c.daysLeft < settings.lowStockDays; + }).length; + + if (coverage.low.length === 0) { + // No critical meds (≤3 days) + if (lowStockCount === 0) { + // All good - everything is Normal or High + return{t('dashboard.reorder.allGood')}
; + } else { + // Some meds are Low but not critical + return{t('dashboard.reorder.lowWarning', { count: lowStockCount })}
; + } + } + + return ( + <> +