feat(schedule): enhance dose button styles for better user experience and add visual cues for future doses

This commit is contained in:
Daniel Volz
2025-12-26 23:54:15 +01:00
parent c0959f681a
commit f34c2c9578
2 changed files with 78 additions and 51 deletions
+27 -12
View File
@@ -362,9 +362,9 @@ function AppContent() {
const coverageByMed = useMemo(() => Object.fromEntries(coverage.all.map((c) => [c.name, c])), [coverage.all]); const coverageByMed = useMemo(() => Object.fromEntries(coverage.all.map((c) => [c.name, c])), [coverage.all]);
const groupedSchedule = useMemo(() => { const groupedSchedule = useMemo(() => {
type DoseInfo = { id: string; timeStr: string; when: number; usage: number }; 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 }> }>(); const days = new Map<string, { dateStr: string; date: Date; meds: Map<string, { medName: string; total: number; doses: DoseInfo[]; lastWhen: number }> }>();
schedule.events.slice(0, 2000).forEach((event) => { schedule.events.slice(0, 2000).forEach((event) => {
const day = days.get(event.dateStr) ?? { dateStr: event.dateStr, meds: new Map() }; const day = days.get(event.dateStr) ?? { dateStr: event.dateStr, date: new Date(event.when), meds: new Map() };
const medEntry = day.meds.get(event.medName) ?? { medName: event.medName, total: 0, doses: [], lastWhen: event.when }; const medEntry = day.meds.get(event.medName) ?? { medName: event.medName, total: 0, doses: [], lastWhen: event.when };
medEntry.total += event.usage; medEntry.total += event.usage;
medEntry.doses.push({ id: event.id, timeStr: event.timeStr, when: event.when, usage: event.usage }); medEntry.doses.push({ id: event.id, timeStr: event.timeStr, when: event.when, usage: event.usage });
@@ -372,7 +372,7 @@ function AppContent() {
day.meds.set(event.medName, medEntry); day.meds.set(event.medName, medEntry);
days.set(event.dateStr, day); days.set(event.dateStr, day);
}); });
return Array.from(days.values()).map((d) => ({ dateStr: d.dateStr, meds: Array.from(d.meds.values()) })).slice(0, scheduleDays); return Array.from(days.values()).map((d) => ({ dateStr: d.dateStr, date: d.date, meds: Array.from(d.meds.values()) })).slice(0, scheduleDays);
}, [schedule.events, scheduleDays]); }, [schedule.events, scheduleDays]);
useEffect(() => { useEffect(() => {
@@ -1008,8 +1008,15 @@ function AppContent() {
const allDayTaken = allDoseIds.length > 0 && allDoseIds.every((id) => takenDoses.has(id)); const allDayTaken = allDoseIds.length > 0 && allDoseIds.every((id) => takenDoses.has(id));
const takenCount = allDoseIds.filter((id) => takenDoses.has(id)).length; const takenCount = allDoseIds.filter((id) => takenDoses.has(id)).length;
// Determine if day should be collapsed // Check if this is today, past, or future
const isAutoCollapsed = allDayTaken; const today = new Date();
today.setHours(0, 0, 0, 0);
const dayDate = new Date(day.date);
dayDate.setHours(0, 0, 0, 0);
const isToday = dayDate.getTime() === today.getTime();
// Determine if day should be collapsed: only today is expanded by default
const isAutoCollapsed = allDayTaken || !isToday;
const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr); const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr);
const isManuallyCollapsed = manuallyCollapsedDays.has(day.dateStr); const isManuallyCollapsed = manuallyCollapsedDays.has(day.dateStr);
const isCollapsed = isAutoCollapsed ? !isManuallyExpanded : isManuallyCollapsed; const isCollapsed = isAutoCollapsed ? !isManuallyExpanded : isManuallyCollapsed;
@@ -1041,7 +1048,6 @@ function AppContent() {
? { className: "danger", label: "status.outOfStock" } ? { className: "danger", label: "status.outOfStock" }
: medCoverage ? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, settings) : null; : medCoverage ? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, settings) : null;
const allTaken = item.doses.every((d) => takenDoses.has(d.id)); const allTaken = item.doses.every((d) => takenDoses.has(d.id));
const takenCount = item.doses.filter((d) => takenDoses.has(d.id)).length;
return ( return (
<div key={`${day.dateStr}-${item.medName}`} className={`time-row ${allTaken ? "taken" : ""}`}> <div key={`${day.dateStr}-${item.medName}`} className={`time-row ${allTaken ? "taken" : ""}`}>
<div className="time-main"> <div className="time-main">
@@ -1057,14 +1063,15 @@ function AppContent() {
{item.doses.map((dose) => { {item.doses.map((dose) => {
const isTaken = takenDoses.has(dose.id); const isTaken = takenDoses.has(dose.id);
const isOverdue = dose.when < Date.now(); const isOverdue = dose.when < Date.now();
const isFutureDose = dose.when > Date.now();
return ( return (
<div key={dose.id} className={`dose-item ${isTaken ? "taken" : ""} ${isOverdue ? "overdue" : ""}`}> <div key={dose.id} className={`dose-item ${isTaken ? "taken" : ""} ${isOverdue ? "overdue" : ""} ${isFutureDose ? "future" : ""}`}>
<span className="dose-time">{dose.timeStr}</span> <span className="dose-time">{dose.timeStr}</span>
<span className="dose-usage">{dose.usage} {dose.usage !== 1 ? t('common.pills') : t('common.pill')}{med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} mg)`}{med?.takenBy && <span className="taken-by-inline"> {t('dose.takenBy')} <span className="taken-by-name clickable" onClick={() => setSelectedUser(med.takenBy!)}>{med.takenBy}</span></span>}</span> <span className="dose-usage">{dose.usage} {dose.usage !== 1 ? t('common.pills') : t('common.pill')}{med?.pillWeightMg && ` (${dose.usage * med.pillWeightMg} mg)`}{med?.takenBy && <span className="taken-by-inline"> {t('dose.takenBy')} <span className="taken-by-name clickable" onClick={() => setSelectedUser(med.takenBy!)}>{med.takenBy}</span></span>}</span>
{isTaken ? ( {isTaken ? (
<button className="dose-btn undo" onClick={() => undoDoseTaken(dose.id)} title={t('common.undo')}></button> <button className="dose-btn undo" onClick={() => undoDoseTaken(dose.id)} title={t('common.undo')}></button>
) : ( ) : (
<button className="dose-btn take" onClick={() => markDoseTaken(dose.id)} title={t('dose.markAsTaken')}></button> <button className="dose-btn take" onClick={() => markDoseTaken(dose.id)} title={t('dose.markAsTaken')} disabled={isFutureDose}></button>
)} )}
</div> </div>
); );
@@ -2681,8 +2688,15 @@ function SharedSchedule() {
const allDayTaken = allDoseIds.length > 0 && allDoseIds.every((id) => takenDoses.has(id)); const allDayTaken = allDoseIds.length > 0 && allDoseIds.every((id) => takenDoses.has(id));
const takenCount = allDoseIds.filter((id) => takenDoses.has(id)).length; const takenCount = allDoseIds.filter((id) => takenDoses.has(id)).length;
// Determine if day should be collapsed // Check if this is today, past, or future
const isAutoCollapsed = allDayTaken; const today = new Date();
today.setHours(0, 0, 0, 0);
const dayDate = new Date(day.date);
dayDate.setHours(0, 0, 0, 0);
const isToday = dayDate.getTime() === today.getTime();
// Determine if day should be collapsed: only today is expanded by default
const isAutoCollapsed = allDayTaken || !isToday;
const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr); const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr);
const isManuallyCollapsed = manuallyCollapsedDays.has(day.dateStr); const isManuallyCollapsed = manuallyCollapsedDays.has(day.dateStr);
const isCollapsed = isAutoCollapsed ? !isManuallyExpanded : isManuallyCollapsed; const isCollapsed = isAutoCollapsed ? !isManuallyExpanded : isManuallyCollapsed;
@@ -2728,8 +2742,9 @@ function SharedSchedule() {
{item.doses.map((dose) => { {item.doses.map((dose) => {
const isTaken = takenDoses.has(dose.id); const isTaken = takenDoses.has(dose.id);
const isOverdue = dose.when < Date.now() && !isTaken; const isOverdue = dose.when < Date.now() && !isTaken;
const isFutureDose = dose.when > Date.now();
return ( return (
<div key={dose.id} className={`dose-item ${isTaken ? "taken" : ""} ${isOverdue ? "overdue" : ""}`}> <div key={dose.id} className={`dose-item ${isTaken ? "taken" : ""} ${isOverdue ? "overdue" : ""} ${isFutureDose ? "future" : ""}`}>
<span className="dose-time">{dose.timeStr}</span> <span className="dose-time">{dose.timeStr}</span>
<span className="dose-usage"> <span className="dose-usage">
{dose.usage} {dose.usage !== 1 ? t('common.pills') : t('common.pill')} {dose.usage} {dose.usage !== 1 ? t('common.pills') : t('common.pill')}
@@ -2738,7 +2753,7 @@ function SharedSchedule() {
{isTaken ? ( {isTaken ? (
<button className="dose-btn undo" onClick={() => undoDoseTaken(dose.id)} title={t('common.undo')}></button> <button className="dose-btn undo" onClick={() => undoDoseTaken(dose.id)} title={t('common.undo')}></button>
) : ( ) : (
<button className="dose-btn take" onClick={() => markDoseTaken(dose.id)} title={t('dose.markAsTaken')}></button> <button className="dose-btn take" onClick={() => markDoseTaken(dose.id)} title={t('dose.markAsTaken')} disabled={isFutureDose}></button>
)} )}
</div> </div>
); );
+13 -1
View File
@@ -543,12 +543,24 @@ textarea {
color: var(--success); color: var(--success);
} }
.dose-btn.take:hover { .dose-btn.take:hover:not(:disabled) {
background: var(--success); background: var(--success);
color: white; color: white;
transform: scale(1.1); transform: scale(1.1);
} }
.dose-btn.take:disabled {
opacity: 0.3;
cursor: not-allowed;
background: var(--bg-tertiary);
border-color: var(--border-primary);
color: var(--text-secondary);
}
.dose-item.future {
opacity: 0.5;
}
.dose-btn.undo { .dose-btn.undo {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-secondary); border: 1px solid var(--border-secondary);