feat: add user medications modal and schedule days selector with styles
This commit is contained in:
+73
-9
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user