feat: add user medications modal and schedule days selector with styles

This commit is contained in:
Daniel Volz
2025-12-21 12:36:45 +01:00
parent eb3bfea940
commit 33de0bc130
2 changed files with 205 additions and 9 deletions
+73 -9
View File
@@ -148,6 +148,11 @@ export default function App() {
const [uploadingImage, setUploadingImage] = useState(false);
const [selectedMed, setSelectedMed] = useState<Medication | null>(null);
const [showImageLightbox, setShowImageLightbox] = useState(false);
const [selectedUser, setSelectedUser] = useState<string | null>(null);
const [scheduleDays, setScheduleDays] = useState<number>(() => {
const stored = localStorage.getItem("scheduleDays");
return stored ? Number(stored) : 30;
});
// Track taken doses (stored in localStorage)
const [takenDoses, setTakenDoses] = useState<Set<string>>(() => {
@@ -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<string, { dateStr: string; meds: Map<string, { medName: string; total: number; doses: DoseInfo[]; lastWhen: number }> }>();
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 (
<div key={row.name} className="table-row clickable" onClick={() => med && setSelectedMed(med)}>
<span data-label="Name" className="cell-with-avatar"><MedicationAvatar name={row.name} imageUrl={med?.imageUrl} />{row.name}{med?.takenBy && <span className="taken-by-badge">{med.takenBy}</span>}{med?.intakeRemindersEnabled && <span className="reminder-icon info-tooltip" data-tooltip="Intake reminders enabled">🔔</span>}{med?.notes && <span className="notes-icon info-tooltip" data-tooltip="Has notes">📝</span>}</span>
<span data-label="Name" className="cell-with-avatar"><MedicationAvatar name={row.name} imageUrl={med?.imageUrl} />{row.name}{med?.takenBy && <span className="taken-by-badge clickable" onClick={(e) => { e.stopPropagation(); setSelectedUser(med.takenBy!); }}>{med.takenBy}</span>}{med?.intakeRemindersEnabled && <span className="reminder-icon info-tooltip" data-tooltip="Intake reminders enabled">🔔</span>}{med?.notes && <span className="notes-icon info-tooltip" data-tooltip="Has notes">📝</span>}</span>
<span data-label="Pills" className={row.medsLeft <= 0 ? "danger-text" : ""}>{formatNumber(row.medsLeft)}</span>
<span data-label="Days" className={status.className === "danger" ? "danger-text" : status.className === "warning" ? "warning-text" : ""}>{formatNumber(row.daysLeft)}</span>
<span data-label="Status" className={`status-chip ${status.className}`}>{status.label}</span>
@@ -662,7 +669,7 @@ export default function App() {
const expiryClass = getExpiryClass(med?.expiryDate);
return (
<div key={row.name} className="table-row clickable" onClick={() => med && setSelectedMed(med)}>
<span data-label="Name" className="cell-with-avatar"><MedicationAvatar name={row.name} imageUrl={med?.imageUrl} />{row.name}{med?.takenBy && <span className="taken-by-badge">{med.takenBy}</span>}{med?.intakeRemindersEnabled && <span className="reminder-icon info-tooltip" data-tooltip="Intake reminders enabled">🔔</span>}{med?.notes && <span className="notes-icon info-tooltip" data-tooltip="Has notes">📝</span>}</span>
<span data-label="Name" className="cell-with-avatar"><MedicationAvatar name={row.name} imageUrl={med?.imageUrl} />{row.name}{med?.takenBy && <span className="taken-by-badge clickable" onClick={(e) => { e.stopPropagation(); setSelectedUser(med.takenBy!); }}>{med.takenBy}</span>}{med?.intakeRemindersEnabled && <span className="reminder-icon info-tooltip" data-tooltip="Intake reminders enabled">🔔</span>}{med?.notes && <span className="notes-icon info-tooltip" data-tooltip="Has notes">📝</span>}</span>
<span data-label="Pills" className={row.medsLeft <= 0 ? "danger-text" : ""}>{formatNumber(row.medsLeft)}</span>
<span data-label="Days left" className={status.className === "danger" ? "danger-text" : status.className === "warning" ? "warning-text" : ""}>{formatNumber(row.daysLeft)}</span>
<span data-label="Runs out">{row.depletionDate ?? "-"}</span>
@@ -679,7 +686,19 @@ export default function App() {
<article className="card">
<div className="card-head">
<h2>Upcoming Schedules</h2>
<span className="pill neutral">Next 10 days</span>
<select
className="schedule-days-select"
value={scheduleDays}
onChange={(e) => {
const val = Number(e.target.value);
setScheduleDays(val);
localStorage.setItem("scheduleDays", String(val));
}}
>
<option value={30}>1 month</option>
<option value={90}>3 months</option>
<option value={180}>6 months</option>
</select>
</div>
<div className="timeline">
{groupedSchedule.map((day) => (
@@ -708,7 +727,7 @@ export default function App() {
return (
<div key={dose.id} className={`dose-item ${isTaken ? "taken" : ""}`}>
<span className="dose-time">{dose.timeStr}</span>
<span className="dose-usage">{dose.usage} pill{dose.usage !== 1 ? "s" : ""}{med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} mg)`}{med?.takenBy && <span className="taken-by-inline"> taken by <span className="taken-by-name">{med.takenBy}</span></span>}</span>
<span className="dose-usage">{dose.usage} pill{dose.usage !== 1 ? "s" : ""}{med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} mg)`}{med?.takenBy && <span className="taken-by-inline"> taken by <span className="taken-by-name clickable" onClick={() => setSelectedUser(med.takenBy!)}>{med.takenBy}</span></span>}</span>
{isTaken ? (
<button className="dose-btn undo" onClick={() => undoDoseTaken(dose.id)} title="Undo"></button>
) : (
@@ -1380,6 +1399,51 @@ export default function App() {
)}
</div>
)}
{/* User Medications Modal */}
{selectedUser && (
<div className="modal-overlay" onClick={() => setSelectedUser(null)}>
<div className="modal-content user-meds-modal" onClick={(e) => e.stopPropagation()}>
<button className="modal-close" onClick={() => setSelectedUser(null)}>×</button>
<div className="user-meds-header">
<div className="user-avatar">{selectedUser.charAt(0).toUpperCase()}</div>
<h2>{selectedUser}'s Medications</h2>
</div>
<div className="user-meds-list">
{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 (
<div
key={med.id}
className="user-med-item clickable"
onClick={() => { setSelectedUser(null); setSelectedMed(med); }}
>
<MedicationAvatar name={med.name} imageUrl={med.imageUrl} size="sm" />
<div className="user-med-info">
<span className="user-med-name">{med.name}</span>
{med.genericName && <span className="user-med-generic">{med.genericName}</span>}
</div>
<div className="user-med-stats">
<span className="user-med-pills">{formatNumber(med.count)} pills</span>
{status && <span className={`status-chip ${status.className}`}>{status.label}</span>}
</div>
</div>
);
})}
{meds.filter(m => m.takenBy === selectedUser).length === 0 && (
<div className="user-meds-empty">No medications found for {selectedUser}</div>
)}
</div>
<div className="user-meds-footer">
<button className="ghost" onClick={() => setSelectedUser(null)}>Close</button>
</div>
</div>
</div>
)}
</main>
);
}
@@ -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) => {
+132
View File
@@ -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;