chore: fix lint errors and reduce warnings across codebase (#234)

* chore: fix lint errors and reduce warnings across codebase

- Fix noExplicitAny catches in backend routes and plugins
- Fix noNestedTernary issues in backend services
- Add keyboard event handlers for useKeyWithClickEvents in frontend
- Disable noImportantStyles rule in biome.json
- Fix formatting errors across all changed files
- Fix test file lint issues

Closes #233

* fix: restore any types in test files for TS compatibility

* fix: revert Auth.tsx dependency array changes that caused infinite re-render

* fix: null-safe user.username access in AppContext dependency array
This commit is contained in:
Daniel Volz
2026-02-17 05:21:47 +01:00
committed by GitHub
parent 08a18fc14a
commit 89d565bc9d
50 changed files with 621 additions and 259 deletions
+12 -2
View File
@@ -51,8 +51,18 @@ export default function AboutModal({ isOpen, onClose }: AboutModalProps) {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content about-modal" onClick={(e) => e.stopPropagation()}>
<div
className="modal-overlay"
onClick={onClose}
onKeyDown={(e) => {
if (e.key === "Escape") onClose();
}}
>
<div
className="modal-content about-modal"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<button className="modal-close" onClick={onClose}>
×
</button>
-1
View File
@@ -5,7 +5,6 @@ import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom";
import { useUnsavedChanges } from "../context";
import type { ThemePreference } from "../hooks";
import { useTheme } from "../hooks";
import { useAuth } from "./Auth";
+13 -2
View File
@@ -39,8 +39,19 @@ export function ConfirmModal({
}, [onCancel]);
return (
<div className={`modal-overlay${overlayClassName ? ` ${overlayClassName}` : ""}`} onClick={onCancel}>
<div className="modal-content" onClick={(e) => e.stopPropagation()} style={{ maxWidth: "450px" }}>
<div
className={`modal-overlay${overlayClassName ? ` ${overlayClassName}` : ""}`}
onClick={onCancel}
onKeyDown={(e) => {
if (e.key === "Escape") onCancel();
}}
>
<div
className="modal-content"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
style={{ maxWidth: "450px" }}
>
<button className="modal-close" onClick={onCancel}>
×
</button>
+7 -1
View File
@@ -28,7 +28,13 @@ export function DateInput({ value, placeholder, className, ...rest }: DateInputP
}, []);
return (
<div className={`date-input-wrapper ${className ?? ""}`} onClick={handleClick}>
<div
className={`date-input-wrapper ${className ?? ""}`}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") handleClick();
}}
>
<span className="date-input-display" aria-hidden="true">
{displayValue || placeholder || ""}
</span>
+7 -1
View File
@@ -29,7 +29,13 @@ export function DateTimeInput({ value, placeholder, className, ...rest }: DateTi
}, []);
return (
<div className={`date-input-wrapper ${className ?? ""}`} onClick={handleClick}>
<div
className={`date-input-wrapper ${className ?? ""}`}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") handleClick();
}}
>
<span className="date-input-display" aria-hidden="true">
{displayValue || placeholder || ""}
</span>
+13 -2
View File
@@ -13,8 +13,19 @@ export default function ExportModal({ isOpen, onClose, onExport, exporting }: Ex
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()} style={{ maxWidth: "450px" }}>
<div
className="modal-overlay"
onClick={onClose}
onKeyDown={(e) => {
if (e.key === "Escape") onClose();
}}
>
<div
className="modal-content"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
style={{ maxWidth: "450px" }}
>
<button className="modal-close" onClick={onClose}>
×
</button>
+14 -2
View File
@@ -19,12 +19,24 @@ export function Lightbox({ src, alt, onClose }: LightboxProps) {
}
return (
<div className="lightbox-overlay" onClick={handleOverlayClick}>
<div
className="lightbox-overlay"
onClick={handleOverlayClick}
onKeyDown={(e) => {
if (e.key === "Escape") onClose();
}}
>
<div className="lightbox-container">
<button className="lightbox-close" onClick={onClose}>
×
</button>
<img src={src} alt={alt} className="lightbox-image" onClick={(e) => e.stopPropagation()} />
<img
src={src}
alt={alt}
className="lightbox-image"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
/>
</div>
</div>
);
+43 -9
View File
@@ -154,14 +154,24 @@ export function MedDetailModal({
const packageSize = getPackageSize(selectedMed);
const currentStock = medCoverage ? Math.round(medCoverage.medsLeft) : getMedTotal(selectedMed);
const status = medCoverage ? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, settings) : null;
const textClass =
status?.className === "danger" ? "danger-text" : status?.className === "warning" ? "warning-text" : "success-text";
const fallbackTextClass = status?.className === "warning" ? "warning-text" : "success-text";
const textClass = status?.className === "danger" ? "danger-text" : fallbackTextClass;
const stock = getBlisterStock(currentStock, selectedMed.pillsPerBlister, selectedMed.looseTablets, packageSize);
const fullForBounds = Math.max(0, parseStockInput(editStockFullInput));
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content med-detail-modal" onClick={(e) => e.stopPropagation()}>
<div
className="modal-overlay"
onClick={onClose}
onKeyDown={(e) => {
if (e.key === "Escape") onClose();
}}
>
<div
className="modal-content med-detail-modal"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<button className="modal-close" onClick={onClose}>
×
</button>
@@ -172,6 +182,11 @@ export function MedDetailModal({
<div
className={`med-detail-avatar-wrapper ${selectedMed.imageUrl ? "clickable" : ""}`}
onClick={() => selectedMed.imageUrl && onOpenImageLightbox()}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (selectedMed.imageUrl) onOpenImageLightbox();
}
}}
>
<MedicationAvatar name={selectedMed.name} imageUrl={selectedMed.imageUrl} size="lg" />
{selectedMed.imageUrl && <span className="expand-icon">🔍</span>}
@@ -408,6 +423,9 @@ export function MedDetailModal({
<h3
className="section-header-clickable"
onClick={() => onRefillHistoryExpandedChange(!refillHistoryExpanded)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") onRefillHistoryExpandedChange(!refillHistoryExpanded);
}}
>
{t("refill.history")} ({refillHistory.length})
<span className="expand-arrow">{refillHistoryExpanded ? "▼" : "▶"}</span>
@@ -488,8 +506,16 @@ export function MedDetailModal({
e.stopPropagation();
onCloseRefillModal();
}}
onKeyDown={(e) => {
e.stopPropagation();
if (e.key === "Escape") onCloseRefillModal();
}}
>
<div className="modal-content refill-modal" onClick={(e) => e.stopPropagation()}>
<div
className="modal-content refill-modal"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<button className="modal-close" onClick={onCloseRefillModal}>
×
</button>
@@ -585,8 +611,16 @@ export function MedDetailModal({
e.stopPropagation();
onCloseEditStockModal();
}}
onKeyDown={(e) => {
e.stopPropagation();
if (e.key === "Escape") onCloseEditStockModal();
}}
>
<div className="modal-content edit-stock-modal" onClick={(e) => e.stopPropagation()}>
<div
className="modal-content edit-stock-modal"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<button className="modal-close" onClick={onCloseEditStockModal}>
×
</button>
@@ -602,6 +636,8 @@ export function MedDetailModal({
? editStockPartialBlisterPills
: editStockFullBlisters * selectedMed.pillsPerBlister + editStockPartialBlisterPills;
const difference = newTotal - currentTotal;
const negativeFallback = difference < 0 ? "negative" : "";
const differenceClass = difference > 0 ? "positive" : negativeFallback;
return (
<>
@@ -691,9 +727,7 @@ export function MedDetailModal({
{newTotal} {newTotal === 1 ? t("common.pill") : t("common.pills")}
</span>
</div>
<div
className={`summary-row difference ${difference > 0 ? "positive" : difference < 0 ? "negative" : ""}`}
>
<div className={`summary-row difference ${differenceClass}`}>
<span>{t("editStock.difference")}:</span>
<span>
{difference > 0 ? "+" : ""}
+18 -3
View File
@@ -137,13 +137,28 @@ export function MobileEditModal({
const currentMed = editingId ? meds.find((m) => m.id === editingId) : null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content edit-modal" onClick={(e) => e.stopPropagation()}>
<div
className="modal-overlay"
onClick={onClose}
onKeyDown={(e) => {
if (e.key === "Escape") onClose();
}}
>
<div
className="modal-content edit-modal"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<div className="edit-modal-header">
<button type="button" className="ghost small btn-nav" onClick={onClose}>
{t("common.back")}
</button>
<h2>{editingId ? (readOnlyMode ? t("form.viewEntry") : t("form.editEntry")) : t("form.newEntry")}</h2>
<h2>
{(() => {
const editLabel = readOnlyMode ? t("form.viewEntry") : t("form.editEntry");
return editingId ? editLabel : t("form.newEntry");
})()}
</h2>
</div>
<form
className="form-grid mobile-edit-form"
+12 -2
View File
@@ -9,8 +9,18 @@ export default function ProfileModal({ isOpen, onClose }: ProfileModalProps) {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content profile-modal" onClick={(e) => e.stopPropagation()}>
<div
className="modal-overlay"
onClick={onClose}
onKeyDown={(e) => {
if (e.key === "Escape") onClose();
}}
>
<div
className="modal-content profile-modal"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<button className="modal-close" onClick={onClose}>
×
</button>
+82 -64
View File
@@ -42,8 +42,18 @@ export function ShareDialog({
if (!show) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content share-dialog-modal" onClick={(e) => e.stopPropagation()}>
<div
className="modal-overlay"
onClick={onClose}
onKeyDown={(e) => {
if (e.key === "Escape") onClose();
}}
>
<div
className="modal-content share-dialog-modal"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<button className="modal-close" onClick={onClose}>
×
</button>
@@ -53,71 +63,79 @@ export function ShareDialog({
<p className="share-dialog-description">{t("share.description")}</p>
</div>
{sharePeople.length === 0 ? (
<div className="share-dialog-empty">
<p>{t("share.noPeople")}</p>
</div>
) : shareLink ? (
<div className="share-dialog-result">
<p className="share-success">{t("share.linkGenerated")}</p>
<div className="share-link-box">
<input
type="text"
value={shareLink}
readOnly
className="share-link-input"
onClick={(e) => (e.target as HTMLInputElement).select()}
/>
<button className="btn-copy" onClick={onCopyShareLink}>
{shareCopied ? "✓" : "📋"}
</button>
</div>
{shareCopied && <span className="share-copied-hint">{t("share.copied")}</span>}
<div className="share-dialog-footer">
<button
className="ghost"
onClick={() => {
onShareLinkChange(null);
onShareCopiedChange(false);
}}
>
{t("share.generateAnother")}
</button>
<button onClick={onClose}>{t("common.close")}</button>
</div>
</div>
) : (
<div className="share-dialog-form">
<div className="form-group">
<label>{t("share.selectPerson")}</label>
<select value={shareSelectedPerson} onChange={(e) => onShareSelectedPersonChange(e.target.value)}>
{sharePeople.map((person) => (
<option key={person} value={person}>
{person}
</option>
))}
</select>
</div>
{(() => {
if (sharePeople.length === 0) {
return (
<div className="share-dialog-empty">
<p>{t("share.noPeople")}</p>
</div>
);
}
if (shareLink) {
return (
<div className="share-dialog-result">
<p className="share-success">{t("share.linkGenerated")}</p>
<div className="share-link-box">
<input
type="text"
value={shareLink}
readOnly
className="share-link-input"
onClick={(e) => (e.target as HTMLInputElement).select()}
/>
<button className="btn-copy" onClick={onCopyShareLink}>
{shareCopied ? "✓" : "📋"}
</button>
</div>
{shareCopied && <span className="share-copied-hint">{t("share.copied")}</span>}
<div className="share-dialog-footer">
<button
className="ghost"
onClick={() => {
onShareLinkChange(null);
onShareCopiedChange(false);
}}
>
{t("share.generateAnother")}
</button>
<button onClick={onClose}>{t("common.close")}</button>
</div>
</div>
);
}
return (
<div className="share-dialog-form">
<div className="form-group">
<label>{t("share.selectPerson")}</label>
<select value={shareSelectedPerson} onChange={(e) => onShareSelectedPersonChange(e.target.value)}>
{sharePeople.map((person) => (
<option key={person} value={person}>
{person}
</option>
))}
</select>
</div>
<div className="form-group">
<label>{t("share.selectPeriod")}</label>
<select value={shareSelectedDays} onChange={(e) => onShareSelectedDaysChange(Number(e.target.value))}>
<option value={30}>{t("dashboard.schedules.1month")}</option>
<option value={90}>{t("dashboard.schedules.3months")}</option>
<option value={180}>{t("dashboard.schedules.6months")}</option>
</select>
</div>
<div className="form-group">
<label>{t("share.selectPeriod")}</label>
<select value={shareSelectedDays} onChange={(e) => onShareSelectedDaysChange(Number(e.target.value))}>
<option value={30}>{t("dashboard.schedules.1month")}</option>
<option value={90}>{t("dashboard.schedules.3months")}</option>
<option value={180}>{t("dashboard.schedules.6months")}</option>
</select>
</div>
<div className="share-dialog-footer">
<button className="ghost" onClick={onClose}>
{t("common.cancel")}
</button>
<button onClick={onGenerateShareLink} disabled={shareGenerating || !shareSelectedPerson}>
{shareGenerating ? t("share.generating") : t("share.generateLink")}
</button>
<div className="share-dialog-footer">
<button className="ghost" onClick={onClose}>
{t("common.cancel")}
</button>
<button onClick={onGenerateShareLink} disabled={shareGenerating || !shareSelectedPerson}>
{shareGenerating ? t("share.generating") : t("share.generateLink")}
</button>
</div>
</div>
</div>
)}
);
})()}
</div>
</div>
);
+59 -14
View File
@@ -209,7 +209,7 @@ export function SharedSchedule() {
// Get dose ID - for per-intake takenBy, the ID already has the person suffix
// This helper is kept for compatibility but since dose.id already includes the suffix, it just returns the id
function getDoseId(doseId: string, _person: string | null): string {
function _getDoseId(doseId: string, _person: string | null): string {
// The dose.id already includes the person suffix if there's a per-intake takenBy
return doseId;
}
@@ -479,7 +479,8 @@ export function SharedSchedule() {
const intake = intakes[blisterIdx];
const intakePerson = intake?.takenBy;
const peopleForThisIntake = intakePerson ? [intakePerson] : med.takenBy?.length > 0 ? med.takenBy : [null];
const fallbackPeople = med.takenBy?.length > 0 ? med.takenBy : [null];
const peopleForThisIntake = intakePerson ? [intakePerson] : fallbackPeople;
let timeBasedConsumed = 0;
let lastAutoConsumedDateMs = 0;
@@ -579,7 +580,8 @@ export function SharedSchedule() {
const status = getStockStatus(coverage.daysLeft, coverage.medsLeft, stockThresholds);
return status.className;
});
return statuses.includes("danger") ? "danger" : statuses.includes("warning") ? "warning" : "success";
const fallbackStatus = statuses.includes("warning") ? "warning" : "success";
return statuses.includes("danger") ? "danger" : fallbackStatus;
}
// Whether to show stock status indicators on the shared schedule
@@ -606,7 +608,7 @@ export function SharedSchedule() {
const missedPastDoseIds = useMemo(() => {
const allPastDoseIds = pastDays.flatMap((d) => d.meds.flatMap((m) => m.doses.map((dose) => dose.id)));
return allPastDoseIds.filter((id) => !isDoseIdDone(id));
}, [pastDays, takenDoses, dismissedDoses, data]);
}, [pastDays, isDoseIdDone]);
if (loading) {
return (
@@ -714,14 +716,19 @@ export function SharedSchedule() {
</div>
</div>
</div>
<p className="shared-schedule-period">
{t("share.period")}:{" "}
{data.scheduleDays === 30
? t("dashboard.schedules.1month")
: data.scheduleDays === 90
? t("dashboard.schedules.3months")
: t("dashboard.schedules.6months")}
</p>
{(() => {
const periodLabel =
data.scheduleDays === 30
? t("dashboard.schedules.1month")
: data.scheduleDays === 90
? t("dashboard.schedules.3months")
: t("dashboard.schedules.6months");
return (
<p className="shared-schedule-period">
{t("share.period")}: {periodLabel}
</p>
);
})()}
</header>
<div className="timeline">
@@ -757,14 +764,18 @@ export function SharedSchedule() {
const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr);
const isCollapsed = !isManuallyExpanded;
const pastMissedClass = allDoseIds.length > 0 ? "past-missed" : "";
return (
<div
key={day.dateStr}
className={`day-block past ${isCollapsed ? "collapsed" : ""} ${allReallyTaken ? "all-taken" : allDoseIds.length > 0 ? "past-missed" : ""}`}
className={`day-block past ${isCollapsed ? "collapsed" : ""} ${allReallyTaken ? "all-taken" : pastMissedClass}`}
>
<div
className="day-divider clickable"
onClick={() => toggleDayCollapse(day.dateStr, true)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") toggleDayCollapse(day.dateStr, true);
}}
title={isCollapsed ? t("common.expand") : t("common.collapse")}
>
<span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span>
@@ -817,6 +828,11 @@ export function SharedSchedule() {
<div
className={med?.imageUrl ? "med-avatar clickable" : ""}
onClick={() => med?.imageUrl && openLightbox(med.imageUrl, med.name)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (med?.imageUrl) openLightbox(med.imageUrl, med.name);
}
}}
>
<MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
</div>
@@ -894,6 +910,9 @@ export function SharedSchedule() {
}, 50);
}
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") setShowPastDays(!showPastDays);
}}
>
<span className="past-days-icon">{showPastDays ? "▼" : "▶"}</span>
<span className="past-days-label">
@@ -941,6 +960,9 @@ export function SharedSchedule() {
<div
className="day-divider clickable"
onClick={() => toggleDayCollapse(day.dateStr, isAutoCollapsed)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") toggleDayCollapse(day.dateStr, isAutoCollapsed);
}}
title={isCollapsed ? t("common.expand") : t("common.collapse")}
>
<span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span>
@@ -982,6 +1004,11 @@ export function SharedSchedule() {
<div
className={med?.imageUrl ? "med-avatar clickable" : ""}
onClick={() => med?.imageUrl && openLightbox(med.imageUrl, med.name)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (med?.imageUrl) openLightbox(med.imageUrl, med.name);
}
}}
>
<MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
</div>
@@ -1058,6 +1085,9 @@ export function SharedSchedule() {
<div
className={`future-days-toggle ${showFutureDays ? "expanded" : ""}`}
onClick={() => setShowFutureDays(!showFutureDays)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") setShowFutureDays(!showFutureDays);
}}
>
<span className="future-days-icon">{showFutureDays ? "▼" : "▶"}</span>
<span className="future-days-label">
@@ -1099,6 +1129,9 @@ export function SharedSchedule() {
<div
className="day-divider clickable"
onClick={() => toggleDayCollapse(day.dateStr, isAutoCollapsed)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") toggleDayCollapse(day.dateStr, isAutoCollapsed);
}}
title={isCollapsed ? t("common.expand") : t("common.collapse")}
>
<span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span>
@@ -1139,6 +1172,11 @@ export function SharedSchedule() {
<div
className={med?.imageUrl ? "med-avatar clickable" : ""}
onClick={() => med?.imageUrl && openLightbox(med.imageUrl, med.name)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (med?.imageUrl) openLightbox(med.imageUrl, med.name);
}
}}
>
<MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
</div>
@@ -1215,7 +1253,13 @@ export function SharedSchedule() {
{/* Image Lightbox */}
{lightboxImage && (
<div className="lightbox-overlay" onClick={closeLightbox}>
<div
className="lightbox-overlay"
onClick={closeLightbox}
onKeyDown={(e) => {
if (e.key === "Escape") closeLightbox();
}}
>
<button className="lightbox-close" onClick={closeLightbox}>
×
</button>
@@ -1224,6 +1268,7 @@ export function SharedSchedule() {
alt={lightboxImage.name}
className="lightbox-image"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
/>
</div>
)}
+18 -2
View File
@@ -36,8 +36,18 @@ export function UserFilterModal({
const userMeds = meds.filter((m) => !m.isObsolete && (m.takenBy || []).includes(selectedUser));
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content user-meds-modal" onClick={(e) => e.stopPropagation()}>
<div
className="modal-overlay"
onClick={onClose}
onKeyDown={(e) => {
if (e.key === "Escape") onClose();
}}
>
<div
className="modal-content user-meds-modal"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<button className="modal-close" onClick={onClose}>
×
</button>
@@ -75,6 +85,12 @@ export function UserFilterModal({
onClearUser();
onOpenMedDetail(med);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
onClearUser();
onOpenMedDetail(med);
}
}}
>
<MedicationAvatar name={med.name} imageUrl={med.imageUrl} size="sm" />
<div className="user-med-info">
+4 -3
View File
@@ -6,7 +6,7 @@ import { useCollapsedDays, useDoses, useMedications, useRefill, useSettings, use
import type { Coverage, Medication, ScheduleEvent, StockThresholds } from "../types";
import { getSystemLocale } from "../utils/formatters";
import { log } from "../utils/logger";
import { buildSchedulePreview, calculateCoverage, computeMissedPastDoseIds, isDoseDismissed } from "../utils/schedule";
import { buildSchedulePreview, calculateCoverage, computeMissedPastDoseIds } from "../utils/schedule";
// =============================================================================
// Types
@@ -366,7 +366,8 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
// Normal/High stock
return "success";
});
return statuses.includes("danger") ? "danger" : statuses.includes("warning") ? "warning" : "success";
const fallbackStatus = statuses.includes("warning") ? "warning" : "success";
return statuses.includes("danger") ? "danger" : fallbackStatus;
},
[coverageByMed, depletionByMed, settingsHook.settings.lowStockDays]
);
@@ -536,7 +537,7 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
}
setExporting(false);
},
[t]
[t, user?.username]
);
// Handle file selection for import
+2 -5
View File
@@ -215,6 +215,7 @@ export function useMedicationForm(): UseMedicationFormReturn {
const remainingRefills = Math.min(Math.max(0, med.prescriptionRemainingRefills ?? 0), authorizedRefills);
const lowRefillThreshold = Math.min(Math.max(0, med.prescriptionLowRefillThreshold ?? 1), authorizedRefills);
const bottleTotalPills = med.packageType === "bottle" && med.looseTablets ? String(med.looseTablets) : "";
const editForm: FormState = {
name: med.name,
genericName: med.genericName ?? "",
@@ -223,11 +224,7 @@ export function useMedicationForm(): UseMedicationFormReturn {
packCount: String(med.packCount),
blistersPerPack: String(med.blistersPerPack),
pillsPerBlister: String(med.pillsPerBlister),
totalPills: med.totalPills
? String(med.totalPills)
: med.packageType === "bottle" && med.looseTablets
? String(med.looseTablets)
: "",
totalPills: med.totalPills ? String(med.totalPills) : bottleTotalPills,
looseTablets: String(med.looseTablets),
pillWeightMg: med.pillWeightMg ? String(med.pillWeightMg) : "",
doseUnit: med.doseUnit ?? "mg",
+100 -6
View File
@@ -82,7 +82,7 @@ export function getReminderStatusData(
_allLowCoverage: Coverage[],
allCoverage: Coverage[],
lastAutoEmailSent: string | null,
lastNotificationType: string | null,
_lastNotificationType: string | null,
_lastNotificationChannel: string | null,
lastReminderMedName: string | null,
lastReminderTakenBy: string | null,
@@ -401,6 +401,11 @@ export function DashboardPage() {
<span
className={`med-link clickable ${textClass}`}
onClick={() => medication && openMedDetail(medication)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (medication) openMedDetail(medication);
}
}}
>
{med.name}
</span>
@@ -430,6 +435,11 @@ export function DashboardPage() {
<span
className={`med-link clickable ${textClass}`}
onClick={() => medication && openMedDetail(medication)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (medication) openMedDetail(medication);
}
}}
>
{med.name}
</span>
@@ -453,7 +463,13 @@ export function DashboardPage() {
<span key={name}>
{idx > 0 && ", "}
{medication ? (
<span className="med-link clickable" onClick={() => openMedDetail(medication)}>
<span
className="med-link clickable"
onClick={() => openMedDetail(medication)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") openMedDetail(medication);
}}
>
{name}
</span>
) : (
@@ -475,7 +491,13 @@ export function DashboardPage() {
(() => {
const medication = meds.find((m) => m.name === reminderData.lastIntakeSent!.medName);
return medication ? (
<span className="med-link clickable" onClick={() => openMedDetail(medication)}>
<span
className="med-link clickable"
onClick={() => openMedDetail(medication)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") openMedDetail(medication);
}}
>
{reminderData.lastIntakeSent!.medName}
</span>
) : (
@@ -553,7 +575,15 @@ export function DashboardPage() {
return (
<span key={c.name}>
{idx > 0 && ", "}
<span className={`med-link clickable ${textClass}`} onClick={() => med && openMedDetail(med)}>
<span
className={`med-link clickable ${textClass}`}
onClick={() => med && openMedDetail(med)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (med) openMedDetail(med);
}
}}
>
{c.name}
</span>
<span className={`reminder-days-left ${textClass}`}>
@@ -603,7 +633,16 @@ export function DashboardPage() {
med ? getMedTotal(med) : Math.round(row.medsLeft)
);
return (
<div key={row.name} className="table-row clickable" onClick={() => med && openMedDetail(med)}>
<div
key={row.name}
className="table-row clickable"
onClick={() => med && openMedDetail(med)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (med) openMedDetail(med);
}
}}
>
<span data-label={t("table.name")} className="cell-with-avatar">
<span className="med-name-line">
<MedicationAvatar name={row.name} imageUrl={med?.imageUrl} />
@@ -629,6 +668,12 @@ export function DashboardPage() {
e.stopPropagation();
openUserFilter(person);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.stopPropagation();
openUserFilter(person);
}
}}
>
{person}
{med.intakes?.some((i) => i.takenBy === person && i.intakeRemindersEnabled) && " 🔔"}
@@ -740,7 +785,7 @@ export function DashboardPage() {
const isAutoCollapsed = true; // Past days are always auto-collapsed
const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr);
const isCollapsed = !isManuallyExpanded;
const worstStatus = getDayStockStatus(day.meds);
const _worstStatus = getDayStockStatus(day.meds);
return (
<div
@@ -750,6 +795,9 @@ export function DashboardPage() {
<div
className="day-divider clickable"
onClick={() => toggleDayCollapse(day.dateStr, isAutoCollapsed)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") toggleDayCollapse(day.dateStr, isAutoCollapsed);
}}
title={isCollapsed ? t("common.expand") : t("common.collapse")}
>
<span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span>
@@ -791,6 +839,11 @@ export function DashboardPage() {
<div
className={med?.imageUrl ? "med-avatar clickable" : ""}
onClick={() => med?.imageUrl && openScheduleLightbox(`/api/images/${med.imageUrl}`)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (med?.imageUrl) openScheduleLightbox(`/api/images/${med.imageUrl}`);
}
}}
>
<MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
</div>
@@ -833,6 +886,9 @@ export function DashboardPage() {
<span
className="person-name clickable"
onClick={() => openUserFilter(person)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") openUserFilter(person);
}}
>
{person}
</span>
@@ -889,6 +945,19 @@ export function DashboardPage() {
}, 50);
}
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
const wasCollapsed = !showPastDays;
setShowPastDays(!showPastDays);
if (wasCollapsed) {
setTimeout(() => {
document
.querySelector(".day-block.today")
?.scrollIntoView({ behavior: "smooth", block: "center" });
}, 50);
}
}
}}
>
<span className="past-days-icon">{showPastDays ? "▼" : "▶"}</span>
<span className="past-days-label">
@@ -963,6 +1032,9 @@ export function DashboardPage() {
<div
className="day-divider clickable"
onClick={() => toggleDayCollapse(day.dateStr, isAutoCollapsed)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") toggleDayCollapse(day.dateStr, isAutoCollapsed);
}}
title={isCollapsed ? t("common.expand") : t("common.collapse")}
>
<span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span>
@@ -998,6 +1070,11 @@ export function DashboardPage() {
<div
className={med?.imageUrl ? "med-avatar clickable" : ""}
onClick={() => med?.imageUrl && openScheduleLightbox(`/api/images/${med.imageUrl}`)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (med?.imageUrl) openScheduleLightbox(`/api/images/${med.imageUrl}`);
}
}}
>
<MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
</div>
@@ -1044,6 +1121,9 @@ export function DashboardPage() {
<span
className="person-name clickable"
onClick={() => openUserFilter(person)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") openUserFilter(person);
}}
>
{person}
</span>
@@ -1096,6 +1176,9 @@ export function DashboardPage() {
<div
className={`future-days-toggle ${showFutureDays ? "expanded" : ""}`}
onClick={() => setShowFutureDays(!showFutureDays)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") setShowFutureDays(!showFutureDays);
}}
>
<span className="future-days-icon">{showFutureDays ? "▼" : "▶"}</span>
<span className="future-days-label">
@@ -1150,6 +1233,9 @@ export function DashboardPage() {
<div
className="day-divider clickable"
onClick={() => toggleDayCollapse(day.dateStr, isAutoCollapsed)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") toggleDayCollapse(day.dateStr, isAutoCollapsed);
}}
title={isCollapsed ? t("common.expand") : t("common.collapse")}
>
<span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span>
@@ -1185,6 +1271,11 @@ export function DashboardPage() {
<div
className={med?.imageUrl ? "med-avatar clickable" : ""}
onClick={() => med?.imageUrl && openScheduleLightbox(`/api/images/${med.imageUrl}`)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (med?.imageUrl) openScheduleLightbox(`/api/images/${med.imageUrl}`);
}
}}
>
<MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
</div>
@@ -1227,6 +1318,9 @@ export function DashboardPage() {
<span
className="person-name clickable"
onClick={() => openUserFilter(person)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") openUserFilter(person);
}}
>
{person}
</span>
+11
View File
@@ -621,6 +621,11 @@ export function MedicationsPage() {
onClick={() =>
med.imageUrl && setLightboxImage({ src: `/api/images/${med.imageUrl}`, alt: med.name })
}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (med.imageUrl) setLightboxImage({ src: `/api/images/${med.imageUrl}`, alt: med.name });
}
}}
>
<MedicationAvatar name={med.name} imageUrl={med.imageUrl} size="lg" />
</span>
@@ -738,6 +743,12 @@ export function MedicationsPage() {
onClick={() =>
med.imageUrl && setLightboxImage({ src: `/api/images/${med.imageUrl}`, alt: med.name })
}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (med.imageUrl)
setLightboxImage({ src: `/api/images/${med.imageUrl}`, alt: med.name });
}
}}
>
<MedicationAvatar name={med.name} imageUrl={med.imageUrl} size="lg" />
</span>
+10 -1
View File
@@ -206,7 +206,16 @@ export function PlannerPage() {
meds.find((m) => m.id === row.medicationId) || meds.find((m) => m.name === row.medicationName);
const remainingRefills = med?.prescriptionEnabled ? (med.prescriptionRemainingRefills ?? 0) : null;
return (
<div key={row.medicationId} className="table-row clickable" onClick={() => med && openMedDetail(med)}>
<div
key={row.medicationId}
className="table-row clickable"
onClick={() => med && openMedDetail(med)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (med) openMedDetail(med);
}
}}
>
<span data-label={t("planner.table.medication")} className="cell-with-avatar">
<MedicationAvatar name={row.medicationName} imageUrl={med?.imageUrl} />
{row.medicationName}
+27 -2
View File
@@ -129,7 +129,7 @@ export function SchedulePage() {
const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr);
const isCollapsed = !isManuallyExpanded;
const worstStatus = getDayStockStatus(day.meds, coverageByMed, settings);
const _worstStatus = getDayStockStatus(day.meds, coverageByMed, settings);
return (
<div
@@ -139,6 +139,9 @@ export function SchedulePage() {
<div
className="day-divider clickable"
onClick={() => toggleDayCollapse(day.dateStr, true)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") toggleDayCollapse(day.dateStr, true);
}}
title={isCollapsed ? t("common.expand") : t("common.collapse")}
>
<span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span>
@@ -210,6 +213,9 @@ export function SchedulePage() {
<span
className="person-name clickable"
onClick={() => openUserFilter(person)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") openUserFilter(person);
}}
>
{person}
</span>
@@ -264,6 +270,19 @@ export function SchedulePage() {
}, 50);
}
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
const wasCollapsed = !showPastDays;
setShowPastDays(!showPastDays);
if (wasCollapsed) {
setTimeout(() => {
document
.querySelector(".day-block.today")
?.scrollIntoView({ behavior: "smooth", block: "center" });
}, 50);
}
}
}}
>
<span className="past-days-icon">{showPastDays ? "▼" : "▶"}</span>
<span className="past-days-label">
@@ -351,7 +370,13 @@ export function SchedulePage() {
className={`dose-person ${isTaken ? "taken" : ""} ${isOverdue ? "overdue" : ""}`}
>
{person && (
<span className="person-name clickable" onClick={() => openUserFilter(person)}>
<span
className="person-name clickable"
onClick={() => openUserFilter(person)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") openUserFilter(person);
}}
>
{person}
</span>
)}
+7
View File
@@ -664,6 +664,13 @@ body.modal-open {
border-radius: 8px;
padding: 0.1rem 0.25rem;
margin: -0.1rem -0.25rem 0.8rem;
background: none;
box-shadow: none;
color: inherit;
}
.med-group-head-toggle:hover {
background: var(--bg-tertiary);
}
.med-group-head-toggle:hover .med-group-title {
@@ -706,7 +706,7 @@ describe("MedDetailModal bottle package type", () => {
expect(screen.getByText(/refill\.pillsToAdd/i)).toBeInTheDocument();
// Should NOT show packs label in refill
const refillModal = document.querySelector(".refill-modal");
const _refillModal = document.querySelector(".refill-modal");
// Packs label should not be present for bottle type
expect(screen.queryByText("refill.packs")).not.toBeInTheDocument();
});
+2 -5
View File
@@ -1892,11 +1892,8 @@ function groupEventsIntoPastDays(
const medMap = dayMap.get(dateKey)!;
if (!medMap.has(event.medName)) medMap.set(event.medName, []);
// Mirror AppContext normalization: string|null → string[]
const takenBy = Array.isArray(event.takenBy)
? event.takenBy
: typeof event.takenBy === "string"
? [event.takenBy]
: [];
const singleOrEmpty = typeof event.takenBy === "string" ? [event.takenBy] : [];
const takenBy = Array.isArray(event.takenBy) ? event.takenBy : singleOrEmpty;
medMap.get(event.medName)!.push({ id: event.id, takenBy });
}
+2 -1
View File
@@ -171,7 +171,8 @@ export function calculateCoverage(
// For per-intake takenBy, only count for that person
// For legacy (no takenBy), count for all people in medication takenBy
const peopleForThisIntake = intakePerson ? [intakePerson] : m.takenBy?.length > 0 ? m.takenBy : [null];
const fallbackPeople = m.takenBy?.length > 0 ? m.takenBy : [null];
const peopleForThisIntake = intakePerson ? [intakePerson] : fallbackPeople;
// Time-based: count doses where the scheduled time has already passed
let timeBasedConsumed = 0;