feat: add takenBy and pillWeightMg to intake reminders and update translations

This commit is contained in:
Daniel Volz
2025-12-25 12:40:40 +01:00
parent 06da1d6eb8
commit 1468c62d59
3 changed files with 58 additions and 8 deletions
+5 -2
View File
@@ -31,6 +31,7 @@ type TranslationKeys = {
time: string;
};
pills: string;
takenBy: string;
footer: string;
};
// Push notifications
@@ -82,7 +83,8 @@ const translations: Record<Language, TranslationKeys> = {
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<Language, TranslationKeys> = {
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",
@@ -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 = `<strong>${intake.usage}</strong> ${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} <span style="color: #6b7280; font-size: 12px;">${t(tr.intakeReminder.takenBy, { name: intake.takenBy })}</span>`;
}
return intake.medName;
};
const tableRows = intakes
.map(
(intake) => `
<tr>
<td style="padding: 10px 12px; border-bottom: 1px solid #e5e7eb;">${intake.medName}</td>
<td style="padding: 10px 12px; border-bottom: 1px solid #e5e7eb; text-align: center;"><strong>${intake.usage}</strong> ${tr.intakeReminder.pills}</td>
<td style="padding: 10px 12px; border-bottom: 1px solid #e5e7eb;">${formatMedName(intake)}</td>
<td style="padding: 10px 12px; border-bottom: 1px solid #e5e7eb; text-align: center;">${formatDosage(intake)}</td>
<td style="padding: 10px 12px; border-bottom: 1px solid #e5e7eb; text-align: center;">${intake.intakeTimeStr}</td>
</tr>
`
@@ -185,11 +209,25 @@ async function sendIntakeReminderEmail(email: string, intakes: UpcomingIntake[],
</div>
`;
// 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);
+1
View File
@@ -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; }