From 1468c62d59854ab7054bdfce3ae98ee493b99df0 Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Thu, 25 Dec 2025 12:40:40 +0100 Subject: [PATCH] feat: add takenBy and pillWeightMg to intake reminders and update translations --- backend/src/i18n/translations.ts | 7 ++- .../src/services/intake-reminder-scheduler.ts | 58 +++++++++++++++++-- frontend/src/styles.css | 1 + 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/backend/src/i18n/translations.ts b/backend/src/i18n/translations.ts index 47e9358..e55b796 100644 --- a/backend/src/i18n/translations.ts +++ b/backend/src/i18n/translations.ts @@ -31,6 +31,7 @@ type TranslationKeys = { time: string; }; pills: string; + takenBy: string; footer: string; }; // Push notifications @@ -82,7 +83,8 @@ const translations: Record = { time: "Time", }, pills: "pills", - footer: "MedAssist-ng Medication Planner", + takenBy: "for {name}", + footer: "🤖 Automatic reminder from MedAssist-ng", }, push: { stockTitle: "MedAssist-ng: 1 Medication Running Low", @@ -129,7 +131,8 @@ const translations: Record = { time: "Uhrzeit", }, pills: "Tabletten", - footer: "MedAssist-ng Medikamentenplaner", + takenBy: "für {name}", + footer: "🤖 Automatische Erinnerung von MedAssist-ng", }, push: { stockTitle: "MedAssist-ng: 1 Medikament wird knapp", diff --git a/backend/src/services/intake-reminder-scheduler.ts b/backend/src/services/intake-reminder-scheduler.ts index 8e8d9f1..b10db69 100644 --- a/backend/src/services/intake-reminder-scheduler.ts +++ b/backend/src/services/intake-reminder-scheduler.ts @@ -62,9 +62,11 @@ type UpcomingIntake = { usage: number; intakeTime: Date; intakeTimeStr: string; + takenBy: string | null; + pillWeightMg: number | null; }; -function getUpcomingIntakes(medName: string, slices: Slice[], minutesBefore: number): UpcomingIntake[] { +function getUpcomingIntakes(medName: string, slices: Slice[], minutesBefore: number, takenBy: string | null, pillWeightMg: number | null): UpcomingIntake[] { const now = Date.now(); // Window to detect if "now" is the right time to send reminder // We check if the notify time (intake - 15min) falls within current minute ±1 @@ -116,6 +118,8 @@ function getUpcomingIntakes(medName: string, slices: Slice[], minutesBefore: num minute: "2-digit", timeZone: getTimezone() }), + takenBy, + pillWeightMg, }); } } @@ -136,12 +140,32 @@ async function sendIntakeReminderEmail(email: string, intakes: UpcomingIntake[], } const tr = getTranslations(language); + + // Helper to format dosage with weight + const formatDosage = (intake: UpcomingIntake): string => { + const pillText = `${intake.usage} ${intake.usage === 1 ? tr.common.pill : tr.intakeReminder.pills}`; + if (intake.pillWeightMg) { + const totalMg = intake.usage * intake.pillWeightMg; + const weightStr = totalMg >= 1000 ? `${(totalMg / 1000).toFixed(1)} g` : `${totalMg} mg`; + return `${pillText} (${weightStr})`; + } + return pillText; + }; + + // Helper to format medication name with takenBy + const formatMedName = (intake: UpcomingIntake): string => { + if (intake.takenBy) { + return `${intake.medName} ${t(tr.intakeReminder.takenBy, { name: intake.takenBy })}`; + } + return intake.medName; + }; + const tableRows = intakes .map( (intake) => ` - ${intake.medName} - ${intake.usage} ${tr.intakeReminder.pills} + ${formatMedName(intake)} + ${formatDosage(intake)} ${intake.intakeTimeStr} ` @@ -185,11 +209,25 @@ async function sendIntakeReminderEmail(email: string, intakes: UpcomingIntake[], `; + // Helper for plain text dosage + const formatDosagePlain = (intake: UpcomingIntake): string => { + const pillText = `${intake.usage} ${intake.usage === 1 ? tr.common.pill : tr.intakeReminder.pills}`; + if (intake.pillWeightMg) { + const totalMg = intake.usage * intake.pillWeightMg; + const weightStr = totalMg >= 1000 ? `${(totalMg / 1000).toFixed(1)} g` : `${totalMg} mg`; + return `${pillText} (${weightStr})`; + } + return pillText; + }; + const plainText = `${tr.intakeReminder.title} ${t(tr.intakeReminder.description, { minutes: REMINDER_MINUTES_BEFORE })} -${intakes.map((i) => `${i.medName}: ${i.usage} ${tr.intakeReminder.pills} ${tr.intakeReminder.tableHeaders.time.toLowerCase()}: ${i.intakeTimeStr}`).join("\n")} +${intakes.map((i) => { + const takenByStr = i.takenBy ? ` ${t(tr.intakeReminder.takenBy, { name: i.takenBy })}` : ""; + return `${i.medName}${takenByStr}: ${formatDosagePlain(i)} - ${i.intakeTimeStr}`; + }).join("\n")} --- ${tr.intakeReminder.footer}`; @@ -249,7 +287,7 @@ async function checkAndSendIntakeReminders(logger: { info: (msg: string) => void // Find all upcoming intakes across all medications for (const med of medsWithReminders) { const slices = parseSlices(med); - const upcoming = getUpcomingIntakes(med.name, slices, REMINDER_MINUTES_BEFORE); + const upcoming = getUpcomingIntakes(med.name, slices, REMINDER_MINUTES_BEFORE, med.takenBy, med.pillWeightMg); allUpcoming.push(...upcoming); } @@ -287,7 +325,15 @@ async function checkAndSendIntakeReminders(logger: { info: (msg: string) => void if (shoutrrrEnabled) { const title = t(tr.push.intakeTitle, { minutes: REMINDER_MINUTES_BEFORE }); const message = newReminders - .map((i) => `- ${i.medName}: ${t(tr.push.pillsAt, { count: i.usage, time: i.intakeTimeStr })}`) + .map((i) => { + const takenByStr = i.takenBy ? ` ${t(tr.intakeReminder.takenBy, { name: i.takenBy })}` : ""; + let dosage = `${i.usage} ${i.usage === 1 ? tr.common.pill : tr.common.pills}`; + if (i.pillWeightMg) { + const totalMg = i.usage * i.pillWeightMg; + dosage += totalMg >= 1000 ? ` (${(totalMg / 1000).toFixed(1)} g)` : ` (${totalMg} mg)`; + } + return `• ${i.medName}${takenByStr}: ${dosage} @ ${i.intakeTimeStr}`; + }) .join("\n"); const result = await sendShoutrrrNotification(settings.shoutrrrUrl, title, message); diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 3671b60..69460db 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -257,6 +257,7 @@ body { .tag.success { background: var(--success-bg); color: var(--success); border: 1px solid rgba(57, 217, 138, 0.25); } .tag.warning { background: var(--warning-bg); color: var(--warning); border: 1px solid rgba(252, 211, 77, 0.3); } .tag.danger { background: var(--danger-bg); color: var(--danger); border: 1px solid rgba(255, 94, 94, 0.3); } +.tag.high { background: var(--success-bg); color: var(--success); border: 1px solid rgba(57, 217, 138, 0.25); } .tag-row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; margin-top: 0.25rem; } .danger-text { color: var(--danger); font-weight: 700; } .warning-text { color: var(--warning); font-weight: 700; }