diff --git a/backend/src/routes/medications.ts b/backend/src/routes/medications.ts index 4e9e13b..8d17137 100644 --- a/backend/src/routes/medications.ts +++ b/backend/src/routes/medications.ts @@ -817,11 +817,12 @@ export async function medicationRoutes(app: FastifyInstance) { } takenDoseIdsByMed.get(medId)!.add(dose.doseId); const rawTakenAt = Number(dose.takenAt); - const takenAtMs = Number.isFinite(rawTakenAt) - ? rawTakenAt < 1_000_000_000_000 - ? rawTakenAt * 1000 - : rawTakenAt - : new Date(dose.takenAt).getTime(); + let takenAtMs: number; + if (Number.isFinite(rawTakenAt)) { + takenAtMs = rawTakenAt < 1_000_000_000_000 ? rawTakenAt * 1000 : rawTakenAt; + } else { + takenAtMs = new Date(dose.takenAt).getTime(); + } takenDoseTimestamps.set(dose.doseId, takenAtMs); }); @@ -876,11 +877,14 @@ export async function medicationRoutes(app: FastifyInstance) { const intake = intakes[blisterIdx]; const intakePerson = intake?.takenBy; const fallbackPeople = parseTakenByJson(row.takenByJson); - const peopleForThisIntake = intakePerson - ? [intakePerson] - : fallbackPeople.length > 0 - ? fallbackPeople - : [null]; + let peopleForThisIntake: Array; + if (intakePerson) { + peopleForThisIntake = [intakePerson]; + } else if (fallbackPeople.length > 0) { + peopleForThisIntake = fallbackPeople; + } else { + peopleForThisIntake = [null]; + } let timeBasedConsumed = 0; let lastAutoConsumedDateMs = 0; diff --git a/backend/src/routes/refills.ts b/backend/src/routes/refills.ts index a6630f4..20b5164 100644 --- a/backend/src/routes/refills.ts +++ b/backend/src/routes/refills.ts @@ -77,7 +77,10 @@ export async function refillRoutes(app: FastifyInstance) { const newPackCount = med.packCount + effectivePacksAdded; const newLooseTablets = med.looseTablets + effectiveLoosePillsAdded; - const consumedRefills = usePrescription ? (isBottle ? 1 : effectivePacksAdded) : 0; + let consumedRefills = 0; + if (usePrescription) { + consumedRefills = isBottle ? 1 : effectivePacksAdded; + } const newRemainingRefills = usePrescription ? Math.max(0, remainingPrescriptionRefills - consumedRefills) : (med.prescriptionRemainingRefills ?? null); diff --git a/backend/src/services/reminder-scheduler.ts b/backend/src/services/reminder-scheduler.ts index 8611a15..c342ffc 100644 --- a/backend/src/services/reminder-scheduler.ts +++ b/backend/src/services/reminder-scheduler.ts @@ -167,11 +167,12 @@ async function getMedicationsNeedingReminder( } takenDoseIdsByMed.get(medId)!.add(dose.doseId); const rawTakenAt = Number(dose.takenAt); - const takenAtMs = Number.isFinite(rawTakenAt) - ? rawTakenAt < 1_000_000_000_000 - ? rawTakenAt * 1000 - : rawTakenAt - : new Date(dose.takenAt).getTime(); + let takenAtMs: number; + if (Number.isFinite(rawTakenAt)) { + takenAtMs = rawTakenAt < 1_000_000_000_000 ? rawTakenAt * 1000 : rawTakenAt; + } else { + takenAtMs = new Date(dose.takenAt).getTime(); + } takenDoseTimestamps.set(dose.doseId, takenAtMs); } @@ -216,7 +217,14 @@ async function getMedicationsNeedingReminder( const intake = intakes[blisterIdx]; const intakePerson = intake?.takenBy; const fallbackPeople = parseTakenByJson(row.takenByJson); - const peopleForThisIntake = intakePerson ? [intakePerson] : fallbackPeople.length > 0 ? fallbackPeople : [null]; + let peopleForThisIntake: Array; + if (intakePerson) { + peopleForThisIntake = [intakePerson]; + } else if (fallbackPeople.length > 0) { + peopleForThisIntake = fallbackPeople; + } else { + peopleForThisIntake = [null]; + } let timeBasedConsumed = 0; let lastAutoConsumedDateMs = 0; diff --git a/backend/src/test/export.test.ts b/backend/src/test/export.test.ts index ef8f052..410b806 100644 --- a/backend/src/test/export.test.ts +++ b/backend/src/test/export.test.ts @@ -152,8 +152,8 @@ async function registerExportRoutes(ctx: TestContext) { }); // POST /import - // biome-ignore lint/suspicious/noExplicitAny: test helper with dynamic import data shape app.post("/import", async (request, reply) => { + // biome-ignore lint/suspicious/noExplicitAny: test helper with dynamic import data shape const importData = request.body as any; // Basic validation diff --git a/backend/src/test/oidc.test.ts b/backend/src/test/oidc.test.ts index 64a2eed..117011a 100644 --- a/backend/src/test/oidc.test.ts +++ b/backend/src/test/oidc.test.ts @@ -1,5 +1,5 @@ import cookie from "@fastify/cookie"; -import Fastify, { type FastifyInstance } from "fastify"; +import Fastify from "fastify"; import { afterEach, describe, expect, it, vi } from "vitest"; type OidcMocks = { diff --git a/frontend/e2e/fixtures/index.ts b/frontend/e2e/fixtures/index.ts index e0ded2e..7fd71bd 100644 --- a/frontend/e2e/fixtures/index.ts +++ b/frontend/e2e/fixtures/index.ts @@ -70,7 +70,7 @@ async function setupAuthMeMock(page: Page): Promise { * auth.spec.ts should keep importing from `@playwright/test` directly * since it tests the unauthenticated flow. */ -export const test = base.extend<{}>({ +export const test = base.extend({ page: async ({ page }, use) => { await setupAuthMeMock(page); await use(page); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d23a6e9..d3d7d5a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "medassist-ng-frontend", - "version": "1.12.0", + "version": "1.14.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "medassist-ng-frontend", - "version": "1.12.0", + "version": "1.14.2", "dependencies": { "i18next": "^25.8.10", "i18next-browser-languagedetector": "^8.2.1", @@ -23,6 +23,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/node": "^25.3.0", "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", "@types/react-router-dom": "^5.3.3", @@ -1735,6 +1736,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -3302,6 +3313,13 @@ "node": ">=20.18.1" } }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 146ebff..7f7e437 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,6 +40,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/node": "^25.3.0", "@types/react": "^18.3.4", "@types/react-dom": "^18.3.0", "@types/react-router-dom": "^5.3.3", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index abc20f8..c51b9b8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { Navigate, Route, Routes, useNavigate } from "react-router-dom"; import { AboutModal, @@ -119,8 +119,6 @@ function AppContent() { // Medications meds, loadMeds, - // Settings - settings, // Refill showRefillModal, setShowRefillModal, @@ -190,6 +188,17 @@ function AppContent() { // Local-only state (not shared across components) const [showProfile, setShowProfile] = useState(false); const [showAbout, setShowAbout] = useState(false); + const closeProfile = useCallback(() => { + if (showProfile) { + window.history.back(); + } + }, [showProfile]); + + const closeAbout = useCallback(() => { + if (showAbout) { + window.history.back(); + } + }, [showAbout]); // Get centralized stockThresholds from context const { stockThresholds } = ctx; @@ -389,25 +398,15 @@ function AppContent() { openEditStockModal(selectedMed, coverage); }; - function openProfile() { + const openProfile = useCallback(() => { setShowProfile(true); window.history.pushState({ modal: "profile" }, ""); - } - function closeProfile() { - if (showProfile) { - window.history.back(); - } - } + }, []); - function openAbout() { + const openAbout = useCallback(() => { setShowAbout(true); window.history.pushState({ modal: "about" }, ""); - } - function closeAbout() { - if (showAbout) { - window.history.back(); - } - } + }, []); return (
diff --git a/frontend/src/components/Auth.tsx b/frontend/src/components/Auth.tsx index 2a69c2c..ae4a0f5 100644 --- a/frontend/src/components/Auth.tsx +++ b/frontend/src/components/Auth.tsx @@ -1,3 +1,4 @@ +/* biome-ignore-all lint/correctness/useExhaustiveDependencies: auth refresh callbacks intentionally coordinate via refs/guards */ import { createContext, type ReactNode, useCallback, useContext, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { log } from "../utils/logger"; @@ -70,7 +71,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { initialFetchDone.current = true; fetchAuthState(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [fetchAuthState]); // Proactively refresh token every 10 minutes to prevent expiration useEffect(() => { @@ -89,7 +90,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { return () => clearInterval(refreshInterval); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [user, authState?.authEnabled]); + }, [user, authState?.authEnabled, refreshUser, tryRefreshToken]); async function fetchAuthState(retryCount = 0) { const maxRetries = 3; diff --git a/frontend/src/components/Lightbox.tsx b/frontend/src/components/Lightbox.tsx index b2a54a4..874db79 100644 --- a/frontend/src/components/Lightbox.tsx +++ b/frontend/src/components/Lightbox.tsx @@ -31,7 +31,15 @@ export function Lightbox({ src, alt, onClose }: LightboxProps) { } return ( -
+
{ + if (e.key === "Escape") { + onClose(); + } + }} + >