From 3ec1460c4e30eccb1bd9af687773bc4352f65c4e Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Mon, 9 Feb 2026 19:33:54 +0100 Subject: [PATCH] feat: frontend improvements - shared schedule, bottle type, settings UI, planner notifications (#146) - Rewrite SharedSchedule to match DashboardPage rendering with time-based consumption - Add bottle package type support across all views (MedDetail, Refill, Planner, Dashboard) - Redesign settings page with colored threshold chips, validation, and stock reminder display - Add shareStockStatus toggle and send manual reminder button - Pill/pills singular/plural consistency across all views - Planner send notification via push (Shoutrrr) in addition to email - Stock overflow warning and past-missed day styling - Update README: bottles in Smart Inventory, push in Trip Planner, new ENV section - 708 passing frontend tests including new coverage for all changes --- README.md | 11 +- frontend/package-lock.json | 4 +- frontend/src/components/MedDetailModal.tsx | 171 +++-- frontend/src/components/MobileEditModal.tsx | 71 +- frontend/src/components/SharedSchedule.tsx | 604 +++++++++--------- frontend/src/components/UserFilterModal.tsx | 3 +- frontend/src/context/AppContext.tsx | 4 +- frontend/src/hooks/useSettings.ts | 15 + frontend/src/i18n/de.json | 60 +- frontend/src/i18n/en.json | 60 +- frontend/src/pages/DashboardPage.tsx | 163 ++++- frontend/src/pages/MedicationsPage.tsx | 114 +++- frontend/src/pages/PlannerPage.tsx | 32 +- frontend/src/pages/SchedulePage.tsx | 10 +- frontend/src/pages/SettingsPage.tsx | 226 +++++-- frontend/src/styles.css | 109 +++- .../test/components/MedDetailModal.test.tsx | 195 ++++++ .../test/components/MobileEditModal.test.tsx | 49 ++ .../src/test/pages/DashboardPage.test.tsx | 143 ++++- .../src/test/pages/MedicationsPage.test.tsx | 175 +++++ frontend/src/test/pages/PlannerPage.test.tsx | 100 ++- frontend/src/test/pages/SettingsPage.test.tsx | 296 ++++++++- frontend/src/test/types.test.ts | 64 ++ frontend/src/types/index.ts | 8 + 24 files changed, 2115 insertions(+), 572 deletions(-) diff --git a/README.md b/README.md index 43eb160..89572f5 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Share your medication schedule with others via a public link. ### Smart Inventory -- Track exact stock: packs, blisters, and loose pills +- Track exact stock: packs, blisters, bottles, and loose pills - Display remaining days of supply - Automatic calculation based on intake schedule @@ -141,6 +141,7 @@ Share your medication schedule with others via a public link. ### Trip Planner - Calculate how many pills you need for a trip or date range - Plan ahead for vacations, business trips, or hospital stays +- Send demand reports via email or push notification ### Multi-Person Support - Manage medications for multiple people @@ -254,6 +255,14 @@ Configure push notifications in Settings → Push, or set defaults via environme | `DEFAULT_SHOUTRRR_STOCK_REMINDERS` | `true` | Send stock warnings via push | | `DEFAULT_SHOUTRRR_INTAKE_REMINDERS` | `true` | Send intake reminders via push | +### Default User Settings + +These defaults are applied when a new user is created. Once a user saves settings in the app, their values take precedence. + +| Variable | Default | Description | +|----------|---------|-------------| +| `DEFAULT_SHARE_STOCK_STATUS` | `true` | Show stock status (Normal/Low/Critical) on shared schedule links | + #### URL Examples **ntfy** (free, self-hostable): diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 476f43e..bb9fbca 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "medassist-ng-frontend", - "version": "1.8.8", + "version": "1.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "medassist-ng-frontend", - "version": "1.8.8", + "version": "1.9.0", "dependencies": { "i18next": "^24.2.2", "i18next-browser-languagedetector": "^8.0.4", diff --git a/frontend/src/components/MedDetailModal.tsx b/frontend/src/components/MedDetailModal.tsx index adf7527..ee0172e 100644 --- a/frontend/src/components/MedDetailModal.tsx +++ b/frontend/src/components/MedDetailModal.tsx @@ -195,6 +195,16 @@ export function MedDetailModal({ {currentStock} /{" "} {selectedMed.packageType === "bottle" ? (selectedMed.totalPills ?? packageSize) : packageSize} + {currentStock > + (selectedMed.packageType === "bottle" ? (selectedMed.totalPills ?? packageSize) : packageSize) && ( + + {" "} + ⚠️ + + )} @@ -266,7 +276,12 @@ export function MedDetailModal({
{selectedMed.blisters.map((blister, idx) => { - const personCount = Math.max(1, selectedMed.takenBy?.length || 1); + // When using new intakes format with per-intake takenBy, + // each intake already represents one person's dose — don't multiply. + // For legacy intakes (no per-intake takenBy), multiply by personCount. + const intake = selectedMed.intakes?.[idx]; + const hasPerIntakeTakenBy = !!intake?.takenBy; + const personCount = hasPerIntakeTakenBy ? 1 : Math.max(1, selectedMed.takenBy?.length || 1); const totalUsage = blister.usage * personCount; return (
@@ -350,10 +365,14 @@ export function MedDetailModal({ })} - + - {entry.packsAdded * selectedMed.blistersPerPack * selectedMed.pillsPerBlister + - entry.loosePillsAdded}{" "} - {t("common.pills")} + {(() => { + const total = + selectedMed.packageType === "bottle" + ? entry.loosePillsAdded + : entry.packsAdded * selectedMed.blistersPerPack * selectedMed.pillsPerBlister + + entry.loosePillsAdded; + return `+${total} ${total === 1 ? t("common.pill") : t("common.pills")}`; + })()}
))} @@ -408,24 +427,38 @@ export function MedDetailModal({

{selectedMed.name}

- - + {selectedMed.packageType === "blister" ? ( + <> + + + + ) : ( + + )}
@@ -440,12 +473,17 @@ export function MedDetailModal({ > {refillSaving ? t("common.saving") : t("refill.button")} - {(refillPacks > 0 || refillLoose > 0) && ( - - +{refillPacks * selectedMed.blistersPerPack * selectedMed.pillsPerBlister + refillLoose}{" "} - {t("common.pills")} - - )} + {(() => { + const totalRefill = + selectedMed.packageType === "blister" + ? refillPacks * selectedMed.blistersPerPack * selectedMed.pillsPerBlister + refillLoose + : refillLoose; + return totalRefill > 0 ? ( + + +{totalRefill} {totalRefill === 1 ? t("common.pill") : t("common.pills")} + + ) : null; + })()}
@@ -472,50 +510,67 @@ export function MedDetailModal({ {(() => { const dbTotal = getMedTotal(selectedMed); const currentTotal = medCoverage ? Math.round(medCoverage.medsLeft) : dbTotal; - const newTotal = editStockFullBlisters * selectedMed.pillsPerBlister + editStockPartialBlisterPills; + const isBottle = selectedMed.packageType === "bottle"; + const newTotal = isBottle + ? editStockPartialBlisterPills + : editStockFullBlisters * selectedMed.pillsPerBlister + editStockPartialBlisterPills; const difference = newTotal - currentTotal; return ( <>
- - + {isBottle ? ( + + ) : ( + <> + + + + )}
{t("editStock.currentTotal")}: - {currentTotal} {t("common.pills")} + {currentTotal} {currentTotal === 1 ? t("common.pill") : t("common.pills")}
{t("editStock.newTotal")}: - {newTotal} {t("common.pills")} + {newTotal} {newTotal === 1 ? t("common.pill") : t("common.pills")}
{t("editStock.difference")}: {difference > 0 ? "+" : ""} - {difference} {t("common.pills")} + {difference} {Math.abs(difference) === 1 ? t("common.pill") : t("common.pills")}
diff --git a/frontend/src/components/MobileEditModal.tsx b/frontend/src/components/MobileEditModal.tsx index 7c0cd33..1d5cfdb 100644 --- a/frontend/src/components/MobileEditModal.tsx +++ b/frontend/src/components/MobileEditModal.tsx @@ -266,7 +266,8 @@ export function MobileEditModal({ )}

- {t("form.total")}: {deriveTotalFromForm(form)} {t("common.pills")} + {t("form.total")}: {deriveTotalFromForm(form)}{" "} + {deriveTotalFromForm(form) === 1 ? t("common.pill") : t("common.pills")}