diff --git a/backend/src/i18n/translations.ts b/backend/src/i18n/translations.ts index e9c092a..65fed2f 100644 --- a/backend/src/i18n/translations.ts +++ b/backend/src/i18n/translations.ts @@ -167,11 +167,13 @@ type TranslationKeys = { medication: string; usage: string; needed: string; + prescriptionRefills: string; available: string; status: string; }; statusEnough: string; statusEmpty: string; + prescriptionNotApplicable: string; }; // Common common: { @@ -286,11 +288,13 @@ const translations: Record = { medication: "Medication", usage: "Usage", needed: "Blisters needed", + prescriptionRefills: "Prescription refills", available: "Available", status: "Status", }, statusEnough: "✓ Enough", statusEmpty: "✗ Empty", + prescriptionNotApplicable: "–", }, common: { pill: "pill", @@ -405,11 +409,13 @@ const translations: Record = { medication: "Medikament", usage: "Verbrauch", needed: "Blister benötigt", + prescriptionRefills: "Rezept-Nachfüllungen", available: "Verfügbar", status: "Status", }, statusEnough: "✓ Ausreichend", statusEmpty: "✗ Leer", + prescriptionNotApplicable: "–", }, common: { pill: "Tablette", diff --git a/backend/src/routes/planner.ts b/backend/src/routes/planner.ts index 91eff99..67b6715 100644 --- a/backend/src/routes/planner.ts +++ b/backend/src/routes/planner.ts @@ -1,5 +1,8 @@ +import { eq } from "drizzle-orm"; import type { FastifyInstance, FastifyRequest } from "fastify"; import nodemailer from "nodemailer"; +import { db } from "../db/client.js"; +import { medications } from "../db/schema.js"; import { getDateLocale, getFooterHtml, @@ -132,6 +135,21 @@ export async function plannerRoutes(app: FastifyInstance) { const outOfStockCount = rows.filter((r) => !r.enough).length; const summaryText = outOfStockCount > 0 ? t(dc.summaryOutOfStock, { count: outOfStockCount }) : dc.summaryAllOk; + // Load prescription data for medications referenced in planner rows + const medIds = rows.map((r) => r.medicationId).filter(Boolean); + const allMeds = + medIds.length > 0 + ? await db + .select({ + id: medications.id, + prescriptionEnabled: medications.prescriptionEnabled, + prescriptionRemainingRefills: medications.prescriptionRemainingRefills, + }) + .from(medications) + .where(eq(medications.userId, userId)) + : []; + const prescriptionMap = new Map(allMeds.map((m) => [m.id, m])); + // Build plain text (shared between email and push) const plainText = `${dc.title} ${t(dc.description, { from: fromDate, until: untilDate })} @@ -143,12 +161,16 @@ ${rows const isBottle = r.packageType === "bottle"; const usage = `${r.plannerUsage} ${tr.common.pills}`; const needed = isBottle ? "–" : `${r.blistersNeeded} × ${r.blisterSize}`; + const medPrescription = prescriptionMap.get(r.medicationId); + const rxRefills = medPrescription?.prescriptionEnabled + ? String(medPrescription.prescriptionRemainingRefills ?? 0) + : dc.prescriptionNotApplicable; const loosePills = Math.round((Number(r.loosePills) || 0) * 10) / 10; const available = isBottle ? `${loosePills} ${tr.common.pills}` : `${r.fullBlisters} ${tr.common.blisters}${loosePills > 0 ? ` + ${loosePills} ${tr.common.pills}` : ""}`; const status = r.enough ? dc.statusEnough : dc.statusEmpty; - return `${r.medicationName}: ${usage}, ${needed}, ${available} - ${status}`; + return `${r.medicationName}: ${usage}, ${needed}, ${dc.tableHeaders.prescriptionRefills}: ${rxRefills}, ${available} - ${status}`; }) .join("\n")} @@ -182,6 +204,12 @@ ${getFooterPlain(language)}`; // "Blisters needed" column: dash for bottles const neededCell = isBottle ? "–" : `${safeBlistersNeeded} × ${safeBlisterSize}`; + // "Prescription refills" column + const medPrescription = prescriptionMap.get(row.medicationId); + const rxCell = medPrescription?.prescriptionEnabled + ? String(medPrescription.prescriptionRemainingRefills ?? 0) + : dc.prescriptionNotApplicable; + // "Available" column: match frontend format let availableCell: string; if (isBottle) { @@ -198,6 +226,7 @@ ${getFooterPlain(language)}`; ${safeName} ${safePlannerUsage} ${tr.common.pills} ${neededCell} + ${rxCell} ${availableCell} ${dc.tableHeaders.medication} ${dc.tableHeaders.usage} ${dc.tableHeaders.needed} + ${dc.tableHeaders.prescriptionRefills} ${dc.tableHeaders.available} ${dc.tableHeaders.status} diff --git a/backend/src/test/planner.test.ts b/backend/src/test/planner.test.ts index 8ac125b..89a999b 100644 --- a/backend/src/test/planner.test.ts +++ b/backend/src/test/planner.test.ts @@ -86,6 +86,39 @@ async function createSchema(client: Client) { is_active integer NOT NULL DEFAULT 1, 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, + user_id integer NOT NULL, + name text NOT NULL, + generic_name text, + taken_by_json text NOT NULL DEFAULT '[]', + package_type text NOT NULL DEFAULT 'blister', + pack_count integer NOT NULL DEFAULT 1, + blisters_per_pack integer NOT NULL DEFAULT 1, + pills_per_blister integer NOT NULL DEFAULT 1, + total_pills integer, + loose_tablets integer NOT NULL DEFAULT 0, + stock_adjustment integer NOT NULL DEFAULT 0, + last_stock_correction_at integer, + pill_weight_mg integer, + dose_unit text DEFAULT 'mg', + usage_json text NOT NULL DEFAULT '[]', + every_json text NOT NULL DEFAULT '[]', + start_json text NOT NULL DEFAULT '[]', + intakes_json text NOT NULL DEFAULT '[]', + image_url text, + expiry_date text, + notes text, + intake_reminders_enabled integer NOT NULL DEFAULT 0, + prescription_enabled integer NOT NULL DEFAULT 0, + prescription_authorized_refills integer, + prescription_remaining_refills integer, + prescription_low_refill_threshold integer NOT NULL DEFAULT 1, + prescription_expiry_date text, + dismissed_until text, + updated_at integer NOT NULL DEFAULT (strftime('%s','now')), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE )`, `CREATE TABLE IF NOT EXISTS user_settings ( id integer PRIMARY KEY AUTOINCREMENT, diff --git a/frontend/src/i18n/de.json b/frontend/src/i18n/de.json index f5b0a9c..3f7d856 100644 --- a/frontend/src/i18n/de.json +++ b/frontend/src/i18n/de.json @@ -207,6 +207,7 @@ "medication": "Medikament", "usage": "Verbrauch", "blistersNeeded": "Blister benötigt", + "prescriptionRefills": "Rezept-Nachfüllungen", "blisters": "Blister", "available": "Verfügbar" } diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index f2b677e..0ce6a46 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -207,6 +207,7 @@ "medication": "Medication", "usage": "Usage", "blistersNeeded": "Blisters needed", + "prescriptionRefills": "Prescription refills", "blisters": "Blisters", "available": "Available" } diff --git a/frontend/src/pages/PlannerPage.tsx b/frontend/src/pages/PlannerPage.tsx index 283f68b..491c6ed 100644 --- a/frontend/src/pages/PlannerPage.tsx +++ b/frontend/src/pages/PlannerPage.tsx @@ -196,16 +196,19 @@ export function PlannerPage() { {plannerRows.length > 0 && ( <> -
+
{t("planner.table.medication")} {t("planner.table.usage")} {t("planner.table.blistersNeeded")} + {t("planner.table.prescriptionRefills")} {t("planner.table.available")} {t("table.status")}
{plannerRows.map((row) => { - const med = meds.find((m) => m.name === row.medicationName); + const med = + meds.find((m) => m.id === row.medicationId) || meds.find((m) => m.name === row.medicationName); + const remainingRefills = med?.prescriptionEnabled ? (med.prescriptionRemainingRefills ?? 0) : null; return (
med && openMedDetail(med)}> @@ -219,6 +222,7 @@ export function PlannerPage() { {row.packageType === "bottle" ? "–" : `${row.blistersNeeded} × ${row.blisterSize}`} + {remainingRefills ?? "–"} {row.packageType === "bottle" ? ( `${Math.round(row.loosePills * 10) / 10} ${Math.round(row.loosePills * 10) / 10 === 1 ? t("common.pill") : t("common.pills")}`