From 1c50e9395f8bdf2258bd4a64430752536ccac6a4 Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Tue, 10 Feb 2026 16:42:23 +0100 Subject: [PATCH] fix: past days UX improvements and clear missed logic (#152) - Render past days above 'Show past days' toggle when expanded - Auto-scroll to today when expanding past days - Remove blue hover color from past day dividers (use opacity instead) - Fix 'All taken' logic: green only for manually taken doses - Yellow styling stays for days with non-taken doses (even after dismissal) - Warning icon disappears after 'Clear missed' (dismissed doses not counted) --- frontend/src/components/SharedSchedule.tsx | 122 ++++++++------ frontend/src/pages/DashboardPage.tsx | 151 +++++++++++------- frontend/src/pages/SchedulePage.tsx | 117 +++++++++----- frontend/src/styles.css | 2 +- frontend/src/test/pages/SchedulePage.test.tsx | 1 + 5 files changed, 246 insertions(+), 147 deletions(-) diff --git a/frontend/src/components/SharedSchedule.tsx b/frontend/src/components/SharedSchedule.tsx index 74de4ab..1f769e1 100644 --- a/frontend/src/components/SharedSchedule.tsx +++ b/frontend/src/components/SharedSchedule.tsx @@ -729,54 +729,38 @@ export function SharedSchedule() {

{t("share.noSchedule")}

) : ( <> - {/* Past days toggle — identical to DashboardPage */} - {pastDays.length > 0 && - (() => { - const missedCount = missedPastDoseIds.length; - const totalPastDoses = pastDays.flatMap((d) => d.meds.flatMap((m) => m.doses.map((dose) => dose.id))); - return ( -
-
0 ? "has-missed" : ""}`} - onClick={() => setShowPastDays(!showPastDays)} - > - {showPastDays ? "▼" : "▶"} - - {showPastDays ? t("dashboard.schedules.hidePastDays") : t("dashboard.schedules.showPastDays")} - - - ({t("dashboard.schedules.pastDaysCount", { count: pastDays.length })}) - - {missedCount > 0 ? ( - - ⚠️ {missedCount} - - ) : totalPastDoses.length > 0 ? ( - - ✓ - - ) : null} -
-
- ); - })()} - {/* Past days (when expanded) — identical to DashboardPage */} + {/* Past days (when expanded) — rendered above toggle */} {showPastDays && pastDays.map((day) => { + // Get ALL dose IDs for this day (for total count and yellow styling) const allDoseIds = day.meds.flatMap((item) => item.doses.map((d) => d.id)); - const allDayTaken = - allDoseIds.length > 0 && allDoseIds.every((id) => takenDoses.has(id) || dismissedDoses.has(id)); - const takenCount = allDoseIds.filter((id) => takenDoses.has(id) || dismissedDoses.has(id)).length; + + // Really taken = all doses marked as taken by human (for green "All taken") + const allReallyTaken = allDoseIds.length > 0 && allDoseIds.every((id) => takenDoses.has(id)); + const takenCount = allDoseIds.filter((id) => takenDoses.has(id)).length; + + // Count missed doses that are NOT dismissed (for warning icon) + const missedNotDismissedCount = day.meds.reduce((count, item) => { + const med = data.medications.find((m) => m.name === item.medName); + const dismissedUntilDate = med?.dismissedUntil ?? undefined; + return ( + count + + item.doses.reduce((doseCount, d) => { + if (isDoseDismissed(d.id, dismissedUntilDate)) return doseCount; + if (takenDoses.has(d.id) || dismissedDoses.has(d.id)) return doseCount; + return doseCount + 1; + }, 0) + ); + }, 0); + const hasRealMissed = missedNotDismissedCount > 0; + const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr); const isCollapsed = !isManuallyExpanded; return (
0 ? "past-missed" : ""}`} + className={`day-block past ${isCollapsed ? "collapsed" : ""} ${allReallyTaken ? "all-taken" : allDoseIds.length > 0 ? "past-missed" : ""}`} >
{isCollapsed ? "▶" : "▼"} {day.dateStr} - {allDayTaken ? ( + {allReallyTaken ? ( ✓ {t("dashboard.schedules.allTaken")} ) : ( <> - - ⚠️ - + {hasRealMissed && ( + + ⚠️ + + )} {takenCount}/{allDoseIds.length} @@ -888,6 +874,50 @@ export function SharedSchedule() {
); })} + {/* Past days toggle */} + {pastDays.length > 0 && + (() => { + const missedCount = missedPastDoseIds.length; + const totalPastDoses = pastDays.flatMap((d) => d.meds.flatMap((m) => m.doses.map((dose) => dose.id))); + return ( +
+
0 ? "has-missed" : ""}`} + onClick={() => { + const wasCollapsed = !showPastDays; + setShowPastDays(!showPastDays); + if (wasCollapsed) { + setTimeout(() => { + document + .querySelector(".day-block.today") + ?.scrollIntoView({ behavior: "smooth", block: "center" }); + }, 50); + } + }} + > + {showPastDays ? "▼" : "▶"} + + {showPastDays ? t("dashboard.schedules.hidePastDays") : t("dashboard.schedules.showPastDays")} + + + ({t("dashboard.schedules.pastDaysCount", { count: pastDays.length })}) + + {missedCount > 0 ? ( + + ⚠️ {missedCount} + + ) : totalPastDoses.length > 0 ? ( + + ✓ + + ) : null} +
+
+ ); + })()} {/* Today (always visible) */} {todayDay && (() => { diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index d72e7a1..9c88861 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -5,7 +5,7 @@ import { useAuth } from "../components/Auth"; import { useAppContext } from "../context"; import type { Coverage } from "../types"; import { formatNumber, getExpiryClass, getSystemLocale } from "../utils/formatters"; -import { expandDoseIds, getStockStatus } from "../utils/schedule"; +import { expandDoseIds, getStockStatus, isDoseDismissed } from "../utils/schedule"; // Helper for user-specific localStorage keys function userStorageKey(userId: number | undefined, key: string): string { @@ -604,65 +604,37 @@ export function DashboardPage() {
- {/* Past days toggle */} - {pastDays.length > 0 && - (() => { - const missedCount = missedPastDoseIds.length; - const totalPastDoses = pastDays.flatMap((d) => d.meds.flatMap((m) => expandDoseIds(m.doses))); - return ( -
-
0 ? "has-missed" : ""}`} - onClick={() => setShowPastDays(!showPastDays)} - > - {showPastDays ? "▼" : "▶"} - - {showPastDays ? t("dashboard.schedules.hidePastDays") : t("dashboard.schedules.showPastDays")} - - - ({t("dashboard.schedules.pastDaysCount", { count: pastDays.length })}) - - {missedCount > 0 ? ( - - ⚠️ {missedCount} - - ) : totalPastDoses.length > 0 ? ( - - ✓ - - ) : null} -
- {missedCount > 0 && ( - - )} -
- ); - })()} - {/* Past days (when expanded) */} + {/* Past days (when expanded) — rendered above toggle */} {showPastDays && pastDays.map((day) => { + // Get ALL dose IDs for this day (for total count and yellow styling) const allDoseIds = day.meds.flatMap((item) => item.doses.flatMap((d) => { const takenByArray = Array.isArray(d.takenBy) ? d.takenBy : []; return takenByArray.length > 0 ? takenByArray.map((p) => `${d.id}-${p}`) : [d.id]; }) ); - const allDayTaken = - allDoseIds.length > 0 && allDoseIds.every((id) => takenDoses.has(id) || dismissedDoses.has(id)); - const takenCount = allDoseIds.filter((id) => takenDoses.has(id) || dismissedDoses.has(id)).length; + + // Really taken = all doses marked as taken by human (for green "All taken") + const allReallyTaken = allDoseIds.length > 0 && allDoseIds.every((id) => takenDoses.has(id)); + const takenCount = allDoseIds.filter((id) => takenDoses.has(id)).length; + + // Count missed doses that are NOT dismissed (for warning icon) + const missedNotDismissedCount = day.meds.reduce((count, item) => { + const med = meds.find((m) => m.name === item.medName); + const dismissedUntilDate = med?.dismissedUntil ?? undefined; + return ( + count + + item.doses.reduce((doseCount, d) => { + if (isDoseDismissed(d.id, dismissedUntilDate)) return doseCount; + const takenByArray = Array.isArray(d.takenBy) ? d.takenBy : []; + const ids = takenByArray.length > 0 ? takenByArray.map((p) => `${d.id}-${p}`) : [d.id]; + return doseCount + ids.filter((id) => !takenDoses.has(id) && !dismissedDoses.has(id)).length; + }, 0) + ); + }, 0); + const hasRealMissed = missedNotDismissedCount > 0; + const isAutoCollapsed = true; // Past days are always auto-collapsed const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr); const isCollapsed = !isManuallyExpanded; @@ -671,7 +643,7 @@ export function DashboardPage() { return (
0 ? "past-missed" : ""}`} + className={`day-block past ${isCollapsed ? "collapsed" : ""} ${allReallyTaken ? "all-taken" : allDoseIds.length > 0 ? "past-missed" : ""}`} >
{isCollapsed ? "▶" : "▼"} {day.dateStr} - {allDayTaken ? ( + {allReallyTaken ? ( ✓ {t("dashboard.schedules.allTaken")} ) : ( <> - - ⚠️ - + {hasRealMissed && ( + + ⚠️ + + )} {takenCount}/{allDoseIds.length} @@ -793,6 +767,63 @@ export function DashboardPage() {
); })} + {/* Past days toggle */} + {pastDays.length > 0 && + (() => { + const missedCount = missedPastDoseIds.length; + const totalPastDoses = pastDays.flatMap((d) => d.meds.flatMap((m) => expandDoseIds(m.doses))); + return ( +
+
0 ? "has-missed" : ""}`} + onClick={() => { + const wasCollapsed = !showPastDays; + setShowPastDays(!showPastDays); + if (wasCollapsed) { + setTimeout(() => { + document + .querySelector(".day-block.today") + ?.scrollIntoView({ behavior: "smooth", block: "center" }); + }, 50); + } + }} + > + {showPastDays ? "▼" : "▶"} + + {showPastDays ? t("dashboard.schedules.hidePastDays") : t("dashboard.schedules.showPastDays")} + + + ({t("dashboard.schedules.pastDaysCount", { count: pastDays.length })}) + + {missedCount > 0 ? ( + + ⚠️ {missedCount} + + ) : totalPastDoses.length > 0 ? ( + + ✓ + + ) : null} +
+ {missedCount > 0 && ( + + )} +
+ ); + })()} {/* Today - always visible */} {todayDay && (() => { diff --git a/frontend/src/pages/SchedulePage.tsx b/frontend/src/pages/SchedulePage.tsx index 37f194a..ae3b574 100644 --- a/frontend/src/pages/SchedulePage.tsx +++ b/frontend/src/pages/SchedulePage.tsx @@ -3,7 +3,7 @@ import { MedicationAvatar } from "../components"; import { useAuth } from "../components/Auth"; import { useAppContext } from "../context"; import type { Coverage } from "../types"; -import { expandDoseIds } from "../utils/schedule"; +import { expandDoseIds, isDoseDismissed } from "../utils/schedule"; // Helper for user-specific localStorage keys function userStorageKey(userId: number | undefined, key: string): string { @@ -65,6 +65,7 @@ export function SchedulePage() { pastDays, futureDays, takenDoses, + dismissedDoses, markDoseTaken, undoDoseTaken, coverageByMed, @@ -95,40 +96,37 @@ export function SchedulePage() {
- {/* Past days toggle */} - {pastDays.length > 0 && - (() => { - // Use context's missedPastDoseIds which handles dismissed doses and previous schedule detection - const missedCount = missedPastDoseIds.length; - return ( -
0 ? "has-missed" : ""}`} - onClick={() => setShowPastDays(!showPastDays)} - > - {showPastDays ? "▼" : "▶"} - - {showPastDays ? t("dashboard.schedules.hidePastDays") : t("dashboard.schedules.showPastDays")} - - - ({t("dashboard.schedules.pastDaysCount", { count: pastDays.length })}) - - {missedCount > 0 && ( - - ⚠️ {missedCount} - - )} -
- ); - })()} - {/* Past days (when expanded) */} + {/* Past days (when expanded) — rendered above toggle */} {showPastDays && pastDays.map((day) => { - const allDoseIds = day.meds.flatMap((item) => expandDoseIds(item.doses)); - const allDayTaken = allDoseIds.length > 0 && allDoseIds.every((id) => takenDoses.has(id)); + // Get ALL dose IDs for this day (for total count and yellow styling) + const allDoseIds = day.meds.flatMap((item) => + item.doses.flatMap((d) => { + const takenByArray = Array.isArray(d.takenBy) ? d.takenBy : []; + return takenByArray.length > 0 ? takenByArray.map((p) => `${d.id}-${p}`) : [d.id]; + }) + ); + + // Really taken = all doses marked as taken by human (for green "All taken") + const allReallyTaken = allDoseIds.length > 0 && allDoseIds.every((id) => takenDoses.has(id)); const takenCount = allDoseIds.filter((id) => takenDoses.has(id)).length; + + // Count missed doses that are NOT dismissed (for warning icon) + const missedNotDismissedCount = day.meds.reduce((count, item) => { + const med = meds.find((m) => m.name === item.medName); + const dismissedUntilDate = med?.dismissedUntil ?? undefined; + return ( + count + + item.doses.reduce((doseCount, d) => { + if (isDoseDismissed(d.id, dismissedUntilDate)) return doseCount; + const takenByArray = Array.isArray(d.takenBy) ? d.takenBy : []; + const ids = takenByArray.length > 0 ? takenByArray.map((p) => `${d.id}-${p}`) : [d.id]; + return doseCount + ids.filter((id) => !takenDoses.has(id) && !dismissedDoses.has(id)).length; + }, 0) + ); + }, 0); + const hasRealMissed = missedNotDismissedCount > 0; + const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr); const isCollapsed = !isManuallyExpanded; const worstStatus = getDayStockStatus(day.meds, coverageByMed, settings); @@ -136,7 +134,7 @@ export function SchedulePage() { return (
0 ? "past-missed" : ""}`} + className={`day-block past ${isCollapsed ? "collapsed" : ""} ${allReallyTaken ? "all-taken" : allDoseIds.length > 0 ? "past-missed" : ""}`} >
{isCollapsed ? "▶" : "▼"} {day.dateStr} - {allDayTaken ? ( + {allReallyTaken ? ( ✓ {t("dashboard.schedules.allTaken")} ) : ( <> - - ⚠️ - + {hasRealMissed && ( + + ⚠️ + + )} {takenCount}/{allDoseIds.length} @@ -246,6 +246,43 @@ export function SchedulePage() {
); })} + {/* Past days toggle */} + {pastDays.length > 0 && + (() => { + const missedCount = missedPastDoseIds.length; + return ( +
0 ? "has-missed" : ""}`} + onClick={() => { + const wasCollapsed = !showPastDays; + setShowPastDays(!showPastDays); + if (wasCollapsed) { + setTimeout(() => { + document + .querySelector(".day-block.today") + ?.scrollIntoView({ behavior: "smooth", block: "center" }); + }, 50); + } + }} + > + {showPastDays ? "▼" : "▶"} + + {showPastDays ? t("dashboard.schedules.hidePastDays") : t("dashboard.schedules.showPastDays")} + + + ({t("dashboard.schedules.pastDaysCount", { count: pastDays.length })}) + + {missedCount > 0 && ( + + ⚠️ {missedCount} + + )} +
+ ); + })()} {/* Current and future days */} {futureDays.map((day) => { const today = new Date(); diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 2b57e90..79f9d01 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -1500,7 +1500,7 @@ textarea.auto-resize { user-select: none; } .day-divider.clickable:hover { - color: var(--accent); + opacity: 0.8; } /* Keep warning/danger colors on hover */ .day-block.stock-warning .day-divider.clickable:hover { diff --git a/frontend/src/test/pages/SchedulePage.test.tsx b/frontend/src/test/pages/SchedulePage.test.tsx index dcdecd5..c480e82 100644 --- a/frontend/src/test/pages/SchedulePage.test.tsx +++ b/frontend/src/test/pages/SchedulePage.test.tsx @@ -94,6 +94,7 @@ const createMockContext = (overrides = {}) => ({ pastDays: [], futureDays: [], takenDoses: new Set(), + dismissedDoses: new Set(), markDoseTaken: vi.fn(), undoDoseTaken: vi.fn(), coverageByMed: {},