From 33de0bc130b7ac80a5f1557ed57fe13fc8aadc2b Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Sun, 21 Dec 2025 12:36:45 +0100 Subject: [PATCH] feat: add user medications modal and schedule days selector with styles --- frontend/src/App.tsx | 82 ++++++++++++++++++++++--- frontend/src/styles.css | 132 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 9 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8f90b6f..2231949 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -148,6 +148,11 @@ export default function App() { const [uploadingImage, setUploadingImage] = useState(false); const [selectedMed, setSelectedMed] = useState(null); const [showImageLightbox, setShowImageLightbox] = useState(false); + const [selectedUser, setSelectedUser] = useState(null); + const [scheduleDays, setScheduleDays] = useState(() => { + const stored = localStorage.getItem("scheduleDays"); + return stored ? Number(stored) : 30; + }); // Track taken doses (stored in localStorage) const [takenDoses, setTakenDoses] = useState>(() => { @@ -191,6 +196,8 @@ export default function App() { if (e.key === "Escape") { if (showImageLightbox) { setShowImageLightbox(false); + } else if (selectedUser) { + setSelectedUser(null); } else if (selectedMed) { setSelectedMed(null); } @@ -198,7 +205,7 @@ export default function App() { }; document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); - }, [selectedMed, showImageLightbox]); + }, [selectedMed, showImageLightbox, selectedUser]); // Check if settings have changed const settingsChanged = settings.emailEnabled !== savedSettings.emailEnabled || @@ -218,7 +225,7 @@ export default function App() { const groupedSchedule = useMemo(() => { type DoseInfo = { id: string; timeStr: string; when: number; usage: number }; const days = new Map }>(); - schedule.events.slice(0, 200).forEach((event) => { + schedule.events.slice(0, 2000).forEach((event) => { const day = days.get(event.dateStr) ?? { dateStr: event.dateStr, meds: new Map() }; const medEntry = day.meds.get(event.medName) ?? { medName: event.medName, total: 0, doses: [], lastWhen: event.when }; medEntry.total += event.usage; @@ -227,8 +234,8 @@ export default function App() { day.meds.set(event.medName, medEntry); days.set(event.dateStr, day); }); - return Array.from(days.values()).map((d) => ({ dateStr: d.dateStr, meds: Array.from(d.meds.values()) })); - }, [schedule.events]); + return Array.from(days.values()).map((d) => ({ dateStr: d.dateStr, meds: Array.from(d.meds.values()) })).slice(0, scheduleDays); + }, [schedule.events, scheduleDays]); useEffect(() => { loadMeds(); @@ -614,7 +621,7 @@ export default function App() { const med = meds.find(m => m.name === row.name); return (
med && setSelectedMed(med)}> - {row.name}{med?.takenBy && {med.takenBy}}{med?.intakeRemindersEnabled && 🔔}{med?.notes && 📝} + {row.name}{med?.takenBy && { e.stopPropagation(); setSelectedUser(med.takenBy!); }}>{med.takenBy}}{med?.intakeRemindersEnabled && 🔔}{med?.notes && 📝} {formatNumber(row.medsLeft)} {formatNumber(row.daysLeft)} {status.label} @@ -662,7 +669,7 @@ export default function App() { const expiryClass = getExpiryClass(med?.expiryDate); return (
med && setSelectedMed(med)}> - {row.name}{med?.takenBy && {med.takenBy}}{med?.intakeRemindersEnabled && 🔔}{med?.notes && 📝} + {row.name}{med?.takenBy && { e.stopPropagation(); setSelectedUser(med.takenBy!); }}>{med.takenBy}}{med?.intakeRemindersEnabled && 🔔}{med?.notes && 📝} {formatNumber(row.medsLeft)} {formatNumber(row.daysLeft)} {row.depletionDate ?? "-"} @@ -679,7 +686,19 @@ export default function App() {

Upcoming Schedules

- Next 10 days +
{groupedSchedule.map((day) => ( @@ -708,7 +727,7 @@ export default function App() { return (
{dose.timeStr} - {dose.usage} pill{dose.usage !== 1 ? "s" : ""}{med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} mg)`}{med?.takenBy && taken by {med.takenBy}} + {dose.usage} pill{dose.usage !== 1 ? "s" : ""}{med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} mg)`}{med?.takenBy && taken by setSelectedUser(med.takenBy!)}>{med.takenBy}} {isTaken ? ( ) : ( @@ -1380,6 +1399,51 @@ export default function App() { )}
)} + + {/* User Medications Modal */} + {selectedUser && ( +
setSelectedUser(null)}> +
e.stopPropagation()}> + + +
+
{selectedUser.charAt(0).toUpperCase()}
+

{selectedUser}'s Medications

+
+ +
+ {meds.filter(m => m.takenBy === selectedUser).map((med) => { + const medCoverage = coverage.all.find(c => c.name === med.name); + const status = medCoverage ? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, settings) : null; + return ( +
{ setSelectedUser(null); setSelectedMed(med); }} + > + +
+ {med.name} + {med.genericName && {med.genericName}} +
+
+ {formatNumber(med.count)} pills + {status && {status.label}} +
+
+ ); + })} + {meds.filter(m => m.takenBy === selectedUser).length === 0 && ( +
No medications found for {selectedUser}
+ )} +
+ +
+ +
+
+
+ )} ); } @@ -1431,7 +1495,7 @@ function buildSchedulePreview(meds: Medication[]) { const events: Array<{ id: string; medName: string; timeStr: string; dateStr: string; usage: number; when: number }> = []; const now = new Date(); const end = new Date(); - end.setDate(end.getDate() + 10); + end.setDate(end.getDate() + 180); // 6 months horizon meds.forEach((med) => { med.slices.forEach((slice, idx) => { diff --git a/frontend/src/styles.css b/frontend/src/styles.css index d2ebb64..46297e1 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -186,6 +186,27 @@ body { .card-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; margin-bottom: 1rem; } .card h2 { margin: 0; font-size: 1.2rem; } +.schedule-days-select { + background: var(--accent-bg); + border: 1px solid var(--accent); + color: var(--text-muted); + padding: 0.25rem 0.5rem; + border-radius: 6px; + font-size: 0.75rem; + cursor: pointer; + outline: none; + transition: all 150ms ease; + width: auto; + max-width: 100px; + flex-shrink: 0; +} +.schedule-days-select:hover { + filter: brightness(1.15); +} +.schedule-days-select:focus { + border-color: var(--accent-light); +} + .pill { border: 1px solid var(--accent); color: var(--text-muted); background: var(--accent-bg); padding: 0.35rem 0.7rem; border-radius: 999px; font-size: 0.85rem; transition: all 150ms ease; } .pill.clickable { cursor: pointer; } .pill.clickable:hover { filter: brightness(1.15); transform: scale(1.02); } @@ -1718,6 +1739,117 @@ textarea { opacity: 0.85; } +.taken-by-badge.clickable, +.taken-by-name.clickable { + cursor: pointer; + transition: opacity 0.15s; +} + +.taken-by-badge.clickable:hover, +.taken-by-name.clickable:hover { + opacity: 1; + text-decoration: underline; +} + +/* User Medications Modal */ +.user-meds-modal { + max-width: 500px; + width: 95%; +} + +.user-meds-header { + display: flex; + align-items: center; + gap: 1rem; + padding: 1.5rem 2rem; + background: linear-gradient(135deg, var(--accent) 0%, #1e5bb8 100%); + border-radius: 16px 16px 0 0; +} + +.user-meds-header .user-avatar { + width: 50px; + height: 50px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + font-weight: 700; + color: white; +} + +.user-meds-header h2 { + margin: 0; + color: white; + font-size: 1.3rem; +} + +.user-meds-list { + padding: 1rem; + max-height: 400px; + overflow-y: auto; +} + +.user-med-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem; + border-radius: 8px; + transition: background 0.15s; +} + +.user-med-item.clickable { + cursor: pointer; +} + +.user-med-item.clickable:hover { + background: var(--accent-bg); +} + +.user-med-info { + flex: 1; + min-width: 0; +} + +.user-med-name { + display: block; + font-weight: 600; + color: var(--text-primary); +} + +.user-med-generic { + display: block; + font-size: 0.8rem; + color: var(--text-secondary); +} + +.user-med-stats { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.25rem; +} + +.user-med-pills { + font-size: 0.85rem; + color: var(--text-secondary); +} + +.user-meds-empty { + padding: 2rem; + text-align: center; + color: var(--text-secondary); +} + +.user-meds-footer { + padding: 1rem 1.5rem; + border-top: 1px solid var(--border-primary); + display: flex; + justify-content: flex-end; +} + .med-detail-header .med-avatar-lg { width: 100px; height: 100px;