From b588fb2f959ded2a1730ac50dc26596d29906669 Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Sat, 20 Dec 2025 19:48:23 +0100 Subject: [PATCH] feat: add reminder functionality with daily email notifications - Implemented reminder scheduler service to check for low stock medications and send email notifications. - Added repeat daily reminders option in settings to allow users to receive daily emails while stock is low. - Updated backend settings route to include new reminder state and settings. - Enhanced frontend to manage and display reminder settings, including last automatic email sent. - Improved UI for better user experience with new styles for settings and notifications. --- backend/Dockerfile | 2 +- backend/drizzle.config.ts | 12 - backend/src/db/migrations/.gitkeep | 0 backend/src/db/migrations/0000_init.sql | 50 --- backend/src/db/migrations/meta/_journal.json | 5 - backend/src/index.ts | 7 + backend/src/routes/planner.ts | 119 ++++--- backend/src/routes/settings.ts | 11 +- backend/src/services/reminder-scheduler.ts | 336 +++++++++++++++++++ frontend/src/App.tsx | 169 ++++++---- frontend/src/styles.css | 175 +++++++++- 11 files changed, 690 insertions(+), 196 deletions(-) delete mode 100644 backend/drizzle.config.ts delete mode 100644 backend/src/db/migrations/.gitkeep delete mode 100644 backend/src/db/migrations/0000_init.sql delete mode 100644 backend/src/db/migrations/meta/_journal.json create mode 100644 backend/src/services/reminder-scheduler.ts diff --git a/backend/Dockerfile b/backend/Dockerfile index ae14362..9d01efa 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,7 +1,7 @@ # Backend build FROM node:25-slim AS builder WORKDIR /app -COPY package.json tsconfig.json drizzle.config.ts ./ +COPY package.json tsconfig.json ./ COPY src ./src RUN npm install RUN npm run build diff --git a/backend/drizzle.config.ts b/backend/drizzle.config.ts deleted file mode 100644 index 67e8cf7..0000000 --- a/backend/drizzle.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "drizzle-kit"; - -const dbUrl = process.env.DATABASE_URL || "file:./data/medassist.db"; - -export default defineConfig({ - dialect: "libsql", - schema: "./src/db/schema.ts", - out: "./drizzle", - dbCredentials: { - url: dbUrl, - }, -}); diff --git a/backend/src/db/migrations/.gitkeep b/backend/src/db/migrations/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/backend/src/db/migrations/0000_init.sql b/backend/src/db/migrations/0000_init.sql deleted file mode 100644 index 3795932..0000000 --- a/backend/src/db/migrations/0000_init.sql +++ /dev/null @@ -1,50 +0,0 @@ -CREATE TABLE IF NOT EXISTS users ( - id integer PRIMARY KEY AUTOINCREMENT, - email text NOT NULL UNIQUE, - password_hash text NOT NULL, - role text NOT NULL DEFAULT 'user', - created_at integer NOT NULL DEFAULT (strftime('%s','now')), - updated_at integer NOT NULL DEFAULT (strftime('%s','now')) -); - -CREATE TABLE IF NOT EXISTS medications ( - id integer PRIMARY KEY AUTOINCREMENT, - name text NOT NULL UNIQUE, - count integer NOT NULL DEFAULT 0, - strips integer NOT NULL DEFAULT 0, - pack_count integer NOT NULL DEFAULT 1, - strips_per_pack integer NOT NULL DEFAULT 1, - tabs_per_strip integer NOT NULL DEFAULT 1, - loose_tablets integer NOT NULL DEFAULT 0, - usage_json text NOT NULL DEFAULT '[]', - every_json text NOT NULL DEFAULT '[]', - start_json text NOT NULL DEFAULT '[]', - strip_size integer NOT NULL DEFAULT 1, - updated_at integer NOT NULL DEFAULT (strftime('%s','now')) -); - -CREATE TABLE IF NOT EXISTS refresh_tokens ( - id integer PRIMARY KEY AUTOINCREMENT, - user_id integer NOT NULL, - token_id text NOT NULL UNIQUE, - expires_at integer NOT NULL, - rotated_at integer, - revoked integer NOT NULL DEFAULT 0, - created_at integer NOT NULL DEFAULT (strftime('%s','now')), - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); - -CREATE TABLE IF NOT EXISTS settings ( - id integer PRIMARY KEY AUTOINCREMENT, - smtp_host text, - smtp_port integer, - smtp_user text, - smtp_pass_encrypted text, - smtp_from text, - smtp_secure integer NOT NULL DEFAULT 0, - emails_per_day integer NOT NULL DEFAULT 3, - email_enabled integer NOT NULL DEFAULT 0, - notification_email text, - reminder_days_before integer NOT NULL DEFAULT 7, - updated_at integer NOT NULL DEFAULT (strftime('%s','now')) -); diff --git a/backend/src/db/migrations/meta/_journal.json b/backend/src/db/migrations/meta/_journal.json deleted file mode 100644 index c32c90d..0000000 --- a/backend/src/db/migrations/meta/_journal.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "entries": [ - { "idx": 0, "version": 1, "when": 1734633120, "tag": "0000_init", "breakpoint": false } - ] -} diff --git a/backend/src/index.ts b/backend/src/index.ts index aeeb97c..ecbdbed 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -11,6 +11,7 @@ import { authRoutes } from "./routes/auth.js"; import { medicationRoutes } from "./routes/medications.js"; import { settingsRoutes } from "./routes/settings.js"; import { plannerRoutes } from "./routes/planner.js"; +import { startReminderScheduler } from "./services/reminder-scheduler.js"; const app = Fastify({ logger: { @@ -65,6 +66,12 @@ const start = async () => { try { await app.listen({ port: env.PORT, host: "0.0.0.0" }); app.log.info(`Server running on ${env.PORT}`); + + // Start the automatic reminder scheduler + startReminderScheduler({ + info: (msg) => app.log.info(msg), + error: (msg) => app.log.error(msg), + }); } catch (err) { app.log.error(err); process.exit(1); diff --git a/backend/src/routes/planner.ts b/backend/src/routes/planner.ts index 9e75b93..d5e0e0a 100644 --- a/backend/src/routes/planner.ts +++ b/backend/src/routes/planner.ts @@ -1,5 +1,6 @@ import { FastifyInstance } from "fastify"; import nodemailer from "nodemailer"; +import { updateReminderSentTime } from "../services/reminder-scheduler.js"; type PlannerRow = { medicationId: number; @@ -61,22 +62,22 @@ export async function plannerRoutes(app: FastifyInstance) { day: "numeric", }); - // Build HTML table + // Build HTML table with horizontal scroll for mobile const tableRows = rows .map( (row) => ` - ${row.medicationName} - ${row.plannerUsage} pills - ${row.stripsNeeded} × ${row.stripSize} - ${row.stripsAvailable} blisters - - ${row.medicationName} + ${row.plannerUsage} pills + ${row.stripsNeeded} × ${row.stripSize} + ${row.stripsAvailable} + + - ${row.enough ? "✓ Enough" : "⚠ Out of Stock"} + ${row.enough ? "✓ OK" : "⚠ Low"} @@ -91,38 +92,40 @@ export async function plannerRoutes(app: FastifyInstance) { : "✓ All medications have sufficient supply for this period."; const html = ` -
-
-

MedAssist - Demand Calculator

-

Supply overview from ${fromDate} to ${untilDate}

+
+
+

MedAssist - Demand Calculator

+

Supply overview from ${fromDate} to ${untilDate}

-
0 ? "background: #fef2f2; border: 1px solid #fecaca;" : "background: #f0fdf4; border: 1px solid #bbf7d0;" }"> -

+

${summaryText}

- - - - - - - - - - - - ${tableRows} - -
MedicationUsageBlisters NeededAvailableStatus
+
+ + + + + + + + + + + + ${tableRows} + +
MedicationUsageNeededAvailableStatus
+
-
-

Sent from MedAssist Medication Planner

+
+

Sent from MedAssist Medication Planner

`; @@ -182,47 +185,50 @@ Sent from MedAssist Medication Planner`; return reply.status(400).send({ error: "SMTP not configured" }); } + // Build HTML table with horizontal scroll for mobile const tableRows = lowStock .map( (row) => ` - ${row.name} - ${row.medsLeft} pills - ${row.daysLeft ?? 0} days - ${row.depletionDate ?? "-"} + ${row.name} + ${row.medsLeft} + ${row.daysLeft ?? 0} + ${row.depletionDate ?? "-"} ` ) .join(""); const html = ` -
-
-

⚠️ MedAssist - Reorder Reminder

-

The following medications are running low and need to be reordered:

+
+
+

⚠️ MedAssist - Reorder Reminder

+

The following medications are running low and need to be reordered:

-
-

+

+

⚠️ ${lowStock.length} medication${lowStock.length > 1 ? "s" : ""} running low!

- - - - - - - - - - - ${tableRows} - -
MedicationCurrent PillsDays LeftRuns Out
+
+ + + + + + + + + + + ${tableRows} + +
MedicationPillsDaysRuns Out
+
-
-

Sent from MedAssist Medication Planner

+
+

Sent from MedAssist Medication Planner

`; @@ -255,6 +261,9 @@ Sent from MedAssist Medication Planner`; html, }); + // Update the reminder state to record this email was sent + updateReminderSentTime(); + return reply.send({ success: true, message: "Reminder email sent" }); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts index e9138eb..e4f1299 100644 --- a/backend/src/routes/settings.ts +++ b/backend/src/routes/settings.ts @@ -2,11 +2,13 @@ import { FastifyInstance } from "fastify"; import nodemailer from "nodemailer"; import { readFileSync, writeFileSync, existsSync } from "fs"; import { resolve } from "path"; +import { getReminderState } from "../services/reminder-scheduler.js"; type SettingsBody = { emailEnabled: boolean; notificationEmail: string; reminderDaysBefore: number; + repeatDailyReminders: boolean; lowStockDays: number; normalStockDays: number; highStockDays: number; @@ -24,6 +26,7 @@ type NotificationSettings = { emailEnabled: boolean; notificationEmail: string; reminderDaysBefore: number; + repeatDailyReminders: boolean; lowStockDays: number; normalStockDays: number; highStockDays: number; @@ -37,6 +40,7 @@ function loadNotificationSettings(): NotificationSettings { emailEnabled: saved.emailEnabled ?? false, notificationEmail: saved.notificationEmail ?? "", reminderDaysBefore: saved.reminderDaysBefore ?? 7, + repeatDailyReminders: saved.repeatDailyReminders ?? false, lowStockDays: saved.lowStockDays ?? 30, normalStockDays: saved.normalStockDays ?? 90, highStockDays: saved.highStockDays ?? 180, @@ -45,7 +49,7 @@ function loadNotificationSettings(): NotificationSettings { } catch { // ignore } - return { emailEnabled: false, notificationEmail: "", reminderDaysBefore: 7, lowStockDays: 30, normalStockDays: 90, highStockDays: 180 }; + return { emailEnabled: false, notificationEmail: "", reminderDaysBefore: 7, repeatDailyReminders: false, lowStockDays: 30, normalStockDays: 90, highStockDays: 180 }; } function saveNotificationSettings(settings: NotificationSettings): void { @@ -56,12 +60,14 @@ export async function settingsRoutes(app: FastifyInstance) { // Get settings - notification from JSON file, SMTP from process.env app.get("/settings", async (_request, reply) => { const notification = loadNotificationSettings(); + const reminderState = getReminderState(); return reply.send({ // Notification settings (user-configurable, stored in JSON) emailEnabled: notification.emailEnabled, notificationEmail: notification.notificationEmail, reminderDaysBefore: notification.reminderDaysBefore, + repeatDailyReminders: notification.repeatDailyReminders, lowStockDays: notification.lowStockDays, normalStockDays: notification.normalStockDays, highStockDays: notification.highStockDays, @@ -72,6 +78,8 @@ export async function settingsRoutes(app: FastifyInstance) { smtpFrom: process.env.SMTP_FROM ?? "", smtpSecure: process.env.SMTP_SECURE === "true", hasSmtpPassword: !!process.env.SMTP_PASS, + // Reminder state + lastAutoEmailSent: reminderState.lastAutoEmailSent, }); }); @@ -84,6 +92,7 @@ export async function settingsRoutes(app: FastifyInstance) { emailEnabled: body.emailEnabled, notificationEmail: body.notificationEmail, reminderDaysBefore: body.reminderDaysBefore, + repeatDailyReminders: body.repeatDailyReminders ?? false, lowStockDays: body.lowStockDays ?? 30, normalStockDays: body.normalStockDays ?? 90, highStockDays: body.highStockDays ?? 180, diff --git a/backend/src/services/reminder-scheduler.ts b/backend/src/services/reminder-scheduler.ts new file mode 100644 index 0000000..a6c5da5 --- /dev/null +++ b/backend/src/services/reminder-scheduler.ts @@ -0,0 +1,336 @@ +import nodemailer from "nodemailer"; +import { db } from "../db/client.js"; +import { medications } from "../db/schema.js"; +import { readFileSync, writeFileSync, existsSync } from "fs"; +import { resolve } from "path"; + +type Slice = { usage: number; every: number; start: string }; + +type NotificationSettings = { + emailEnabled: boolean; + notificationEmail: string; + reminderDaysBefore: number; + repeatDailyReminders: boolean; + lowStockDays: number; + normalStockDays: number; + highStockDays: number; +}; + +type ReminderState = { + lastAutoEmailSent: string | null; // ISO date string + lastAutoEmailDate: string | null; // YYYY-MM-DD - to track if we already sent today + notifiedMedications: string[]; // List of medication names that have been notified (cleared when restocked) +}; + +const notificationSettingsFile = resolve(process.cwd(), "data", "notification-settings.json"); +const reminderStateFile = resolve(process.cwd(), "data", "reminder-state.json"); + +function loadNotificationSettings(): NotificationSettings { + try { + if (existsSync(notificationSettingsFile)) { + const saved = JSON.parse(readFileSync(notificationSettingsFile, "utf-8")); + return { + emailEnabled: saved.emailEnabled ?? false, + notificationEmail: saved.notificationEmail ?? "", + reminderDaysBefore: saved.reminderDaysBefore ?? 7, + repeatDailyReminders: saved.repeatDailyReminders ?? false, + lowStockDays: saved.lowStockDays ?? 30, + normalStockDays: saved.normalStockDays ?? 90, + highStockDays: saved.highStockDays ?? 180, + }; + } + } catch { + // ignore + } + return { emailEnabled: false, notificationEmail: "", reminderDaysBefore: 7, repeatDailyReminders: false, lowStockDays: 30, normalStockDays: 90, highStockDays: 180 }; +} + +function loadReminderState(): ReminderState { + try { + if (existsSync(reminderStateFile)) { + const saved = JSON.parse(readFileSync(reminderStateFile, "utf-8")); + return { + lastAutoEmailSent: saved.lastAutoEmailSent ?? null, + lastAutoEmailDate: saved.lastAutoEmailDate ?? null, + notifiedMedications: saved.notifiedMedications ?? [], + }; + } + } catch { + // ignore + } + return { lastAutoEmailSent: null, lastAutoEmailDate: null, notifiedMedications: [] }; +} + +function saveReminderState(state: ReminderState): void { + writeFileSync(reminderStateFile, JSON.stringify(state, null, 2)); +} + +export function getReminderState(): ReminderState { + return loadReminderState(); +} + +export function updateReminderSentTime(): void { + const state = loadReminderState(); + const today = new Date().toISOString().split("T")[0]; + saveReminderState({ + ...state, + lastAutoEmailSent: new Date().toISOString(), + lastAutoEmailDate: today, + }); +} + +function parseSlices(row: { usageJson: string; everyJson: string; startJson: string }): Slice[] { + try { + const usage = JSON.parse(row.usageJson) as number[]; + const every = JSON.parse(row.everyJson) as number[]; + const start = JSON.parse(row.startJson) as string[]; + const len = Math.min(usage.length, every.length, start.length); + const slices: Slice[] = []; + for (let i = 0; i < len; i++) { + slices.push({ usage: usage[i], every: every[i], start: start[i] }); + } + return slices; + } catch { + return []; + } +} + +function calculateDailyUsage(slices: Slice[]): number { + return slices.reduce((sum, s) => sum + s.usage / s.every, 0); +} + +function calculateDepletionInfo(med: { count: number; slices: Slice[] }): { daysLeft: number | null; depletionDate: string | null } { + const dailyUsage = calculateDailyUsage(med.slices); + if (dailyUsage <= 0) return { daysLeft: null, depletionDate: null }; + + const daysLeft = Math.floor(med.count / dailyUsage); + const depletionMs = Date.now() + daysLeft * 86_400_000; + const depletionDate = new Date(depletionMs).toLocaleDateString("en-US", { + weekday: "short", + day: "2-digit", + month: "short", + }); + + return { daysLeft, depletionDate }; +} + +type LowStockItem = { + name: string; + medsLeft: number; + daysLeft: number | null; + depletionDate: string | null; +}; + +async function getMedicationsNeedingReminder(reminderDaysBefore: number): Promise { + const rows = await db.select().from(medications).orderBy(medications.id); + + const lowStock: LowStockItem[] = []; + + for (const row of rows) { + const slices = parseSlices(row); + const { daysLeft, depletionDate } = calculateDepletionInfo({ count: row.count, slices }); + + // Check if medication runs out within reminderDaysBefore days + if (daysLeft !== null && daysLeft <= reminderDaysBefore) { + lowStock.push({ + name: row.name, + medsLeft: row.count, + daysLeft, + depletionDate, + }); + } + } + + return lowStock; +} + +async function sendReminderEmail(email: string, lowStock: LowStockItem[]): Promise<{ success: boolean; error?: string }> { + const smtpHost = process.env.SMTP_HOST; + const smtpUser = process.env.SMTP_USER; + const smtpPass = process.env.SMTP_PASS; + const smtpPort = parseInt(process.env.SMTP_PORT ?? "587"); + const smtpSecure = process.env.SMTP_SECURE === "true"; + const smtpFrom = process.env.SMTP_FROM ?? smtpUser; + + if (!smtpHost || !smtpUser) { + return { success: false, error: "SMTP not configured" }; + } + + const tableRows = lowStock + .map( + (row) => ` + + ${row.name} + ${row.medsLeft} + ${row.daysLeft ?? 0} + ${row.depletionDate ?? "-"} + + ` + ) + .join(""); + + const html = ` +
+
+

⚠️ MedAssist - Automatic Reorder Reminder

+

The following medications are running low and need to be reordered:

+ +
+

+ ⚠️ ${lowStock.length} medication${lowStock.length > 1 ? "s" : ""} running low! +

+
+ +
+ + + + + + + + + + + ${tableRows} + +
MedicationPillsDaysRuns Out
+
+ +
+

+ 🤖 Automatic reminder from MedAssist +

+
+
+ `; + + const plainText = `MedAssist - Automatic Reorder Reminder + +The following medications are running low: + +${lowStock.map((r) => `${r.name}: ${r.medsLeft} pills left, ${r.daysLeft ?? 0} days remaining, runs out ${r.depletionDate ?? "soon"}`).join("\n")} + +--- +Automatic reminder from MedAssist`; + + try { + const transporter = nodemailer.createTransport({ + host: smtpHost, + port: smtpPort, + secure: smtpSecure, + auth: { + user: smtpUser, + pass: smtpPass ?? "", + }, + }); + + await transporter.sendMail({ + from: smtpFrom, + to: email, + subject: `⚠️ MedAssist Auto-Reminder: ${lowStock.length} Medication${lowStock.length > 1 ? "s" : ""} Running Low`, + text: plainText, + html, + }); + + return { success: true }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + return { success: false, error: errorMessage }; + } +} + +async function checkAndSendReminder(logger: { info: (msg: string) => void; error: (msg: string) => void }): Promise { + const settings = loadNotificationSettings(); + + // Check if email reminders are enabled + if (!settings.emailEnabled || !settings.notificationEmail) { + logger.info("[Reminder] Email reminders disabled or no email configured"); + return; + } + + const state = loadReminderState(); + const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD + + // Get all medications that need a reminder + const allLowStock = await getMedicationsNeedingReminder(settings.reminderDaysBefore); + + if (allLowStock.length === 0) { + // No low stock - clear the notified list (medications have been restocked) + if (state.notifiedMedications.length > 0) { + saveReminderState({ + ...state, + notifiedMedications: [], + }); + logger.info("[Reminder] Cleared notified medications list (all restocked)"); + } + logger.info("[Reminder] No medications need reminder"); + return; + } + + // Get names of currently low stock medications + const currentLowStockNames = allLowStock.map((m) => m.name); + + // Remove medications from notified list that are no longer low stock (restocked) + const stillLowStock = state.notifiedMedications.filter((name) => currentLowStockNames.includes(name)); + + // Find NEW medications that haven't been notified yet + const newLowStock = allLowStock.filter((m) => !state.notifiedMedications.includes(m.name)); + + // Determine what to send + let medsToNotify: LowStockItem[] = []; + + if (settings.repeatDailyReminders) { + // Daily reminders enabled - send for ALL low stock, but only once per day + if (state.lastAutoEmailDate === today) { + logger.info("[Reminder] Daily reminder already sent today, skipping"); + return; + } + medsToNotify = allLowStock; + } else { + // Only notify NEW medications (not previously notified) + if (newLowStock.length === 0) { + logger.info("[Reminder] No new medications to notify (already notified previously)"); + return; + } + medsToNotify = newLowStock; + } + + logger.info(`[Reminder] Sending reminder for ${medsToNotify.length} medications...`); + + const result = await sendReminderEmail(settings.notificationEmail, medsToNotify); + + if (result.success) { + // Update state + saveReminderState({ + lastAutoEmailSent: new Date().toISOString(), + lastAutoEmailDate: today, + notifiedMedications: [...new Set([...stillLowStock, ...medsToNotify.map((m) => m.name)])], + }); + logger.info(`[Reminder] Email sent successfully to ${settings.notificationEmail}`); + } else { + logger.error(`[Reminder] Failed to send email: ${result.error}`); + } +} + +let schedulerInterval: NodeJS.Timeout | null = null; + +export function startReminderScheduler(logger: { info: (msg: string) => void; error: (msg: string) => void }): void { + // Run check immediately on startup + logger.info("[Reminder] Starting reminder scheduler..."); + checkAndSendReminder(logger).catch((err) => logger.error(`[Reminder] Error: ${err}`)); + + // Then run every hour to check (will only send once per day) + schedulerInterval = setInterval(() => { + checkAndSendReminder(logger).catch((err) => logger.error(`[Reminder] Error: ${err}`)); + }, 60 * 60 * 1000); // Every hour + + logger.info("[Reminder] Scheduler started - checking hourly, sending max once per day"); +} + +export function stopReminderScheduler(): void { + if (schedulerInterval) { + clearInterval(schedulerInterval); + schedulerInterval = null; + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4d988a8..7f0a171 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -81,6 +81,7 @@ export default function App() { emailEnabled: false, notificationEmail: "", reminderDaysBefore: 7, + repeatDailyReminders: false, lowStockDays: 30, normalStockDays: 90, highStockDays: 180, @@ -91,6 +92,7 @@ export default function App() { smtpFrom: "", smtpSecure: false, hasSmtpPassword: false, + lastAutoEmailSent: null as string | null, }); const [savedSettings, setSavedSettings] = useState(settings); const [settingsLoading, setSettingsLoading] = useState(false); @@ -102,17 +104,12 @@ export default function App() { const [plannerEmailResult, setPlannerEmailResult] = useState<{ success: boolean; message: string } | null>(null); const [sendingReminderEmail, setSendingReminderEmail] = useState(false); const [reminderEmailResult, setReminderEmailResult] = useState<{ success: boolean; message: string } | null>(null); - const [lastReminderSent, setLastReminderSent] = useState(() => { - if (typeof window !== "undefined") { - return localStorage.getItem("lastReminderSent"); - } - return null; - }); // Check if settings have changed const settingsChanged = settings.emailEnabled !== savedSettings.emailEnabled || settings.notificationEmail !== savedSettings.notificationEmail || settings.reminderDaysBefore !== savedSettings.reminderDaysBefore || + settings.repeatDailyReminders !== savedSettings.repeatDailyReminders || settings.lowStockDays !== savedSettings.lowStockDays || settings.normalStockDays !== savedSettings.normalStockDays || settings.highStockDays !== savedSettings.highStockDays; @@ -172,6 +169,7 @@ export default function App() { emailEnabled: settings.emailEnabled, notificationEmail: settings.notificationEmail, reminderDaysBefore: settings.reminderDaysBefore, + repeatDailyReminders: settings.repeatDailyReminders, lowStockDays: settings.lowStockDays, normalStockDays: settings.normalStockDays, highStockDays: settings.highStockDays, @@ -261,10 +259,9 @@ export default function App() { }); const data = await res.json(); if (res.ok) { - const sentDate = new Date().toLocaleDateString([], { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" }); - setLastReminderSent(sentDate); - localStorage.setItem("lastReminderSent", sentDate); setReminderEmailResult({ success: true, message: data.message || "Email sent!" }); + // Reload settings to get updated lastAutoEmailSent + loadSettings(); } else { setReminderEmailResult({ success: false, message: data.error || "Failed to send" }); } @@ -401,7 +398,7 @@ export default function App() {
📧 - Email reminders active — Next check: {getNextReminderDate(settings.reminderDaysBefore, coverage.low)} + Automatic reminders active — {getReminderStatusText(settings.reminderDaysBefore, coverage.low, settings.lastAutoEmailSent)} → {settings.notificationEmail}
@@ -418,27 +415,25 @@ export default function App() {

All good, enough stock.

) : ( <> -
+
Name Current pills Days left Status Runs out - Next reminder - Email sent + Auto-remind
{coverage.low.map((row) => { const status = getStockStatus(row.daysLeft, row.medsLeft, settings); return (
- {row.name} - {formatNumber(row.medsLeft)} - {formatNumber(row.daysLeft)} - {status.label} - {row.depletionDate ?? "-"} - {getNextReminderForMed(row, settings.reminderDaysBefore)} - {lastReminderSent ?? "—"} + {row.name} + {formatNumber(row.medsLeft)} + {formatNumber(row.daysLeft)} + {status.label} + {row.depletionDate ?? "-"} + {getNextReminderForMed(row, settings.reminderDaysBefore)}
); })} @@ -446,7 +441,7 @@ export default function App() { {settings.emailEnabled && settings.notificationEmail && (
{reminderEmailResult && ( @@ -478,11 +473,11 @@ export default function App() { const status = getStockStatus(row.daysLeft, row.medsLeft, settings); return (
- {row.name} - {formatNumber(row.medsLeft)} - {formatNumber(row.daysLeft)} - {row.depletionDate ?? "-"} - {status.label} + {row.name} + {formatNumber(row.medsLeft)} + {formatNumber(row.daysLeft)} + {row.depletionDate ?? "-"} + {status.label}
); })} @@ -671,11 +666,11 @@ export default function App() {
{plannerRows.map((row) => (
- {row.medicationName} - {row.plannerUsage} pills - {row.stripsNeeded} × {row.stripSize} - {row.stripsAvailable} blisters - {row.enough ? "✓ Enough" : "⚠ Out of Stock"} + {row.medicationName} + {row.plannerUsage} pills + {row.stripsNeeded} × {row.stripSize} + {row.stripsAvailable} blisters + {row.enough ? "✓ Enough" : "⚠ Out of Stock"}
))}
@@ -701,8 +696,8 @@ export default function App() {
-

Email Notifications

- Reminder settings +

Automatic Email Reminders

+ Daily check
{settingsLoading ? (

Loading settings...

@@ -710,8 +705,8 @@ export default function App() {
- -

Get notified when medication is running low

+ +

Automatically send email when medications are running low