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
+2 -4
View File
@@ -1,5 +1,4 @@
import { existsSync, statSync } from "node:fs"; import { existsSync, statSync } from "node:fs";
import { resolve } from "node:path";
import { type Client, createClient } from "@libsql/client"; import { type Client, createClient } from "@libsql/client";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { drizzle } from "drizzle-orm/libsql"; import { drizzle } from "drizzle-orm/libsql";
@@ -8,7 +7,6 @@ import { log } from "../utils/logger.js";
import { import {
ensureDataDirectory, ensureDataDirectory,
ensureDefaultUser, ensureDefaultUser,
getDataDir,
getDbPaths, getDbPaths,
repairOrphanedDoseIds, repairOrphanedDoseIds,
repairTrailingHyphenDoseIds, repairTrailingHyphenDoseIds,
@@ -65,8 +63,8 @@ let client: Client;
try { try {
client = createClient({ url }); client = createClient({ url });
log.debug(`[DB] Database client created successfully`); log.debug(`[DB] Database client created successfully`);
} catch (err: any) { } catch (err: unknown) {
log.error(`[DB] ERROR: Failed to create database client: ${err.message}`); log.error(`[DB] ERROR: Failed to create database client: ${(err as Error).message}`);
log.error(`[DB] Database path: ${dbPath}`); log.error(`[DB] Database path: ${dbPath}`);
process.exit(1); process.exit(1);
} }
+23 -23
View File
@@ -71,8 +71,8 @@ export function ensureDataDirectory(dataDir: string): { success: boolean; error?
writeFileSync(testFile, "test"); writeFileSync(testFile, "test");
return { success: true }; return { success: true };
} catch (err: any) { } catch (err: unknown) {
return { success: false, error: err.message }; return { success: false, error: (err as Error).message };
} }
} }
@@ -87,14 +87,14 @@ export async function runDrizzleMigrations(
try { try {
await migrate(database, { migrationsFolder }); await migrate(database, { migrationsFolder });
return { success: true }; return { success: true };
} catch (err: any) { } catch (err: unknown) {
// If the error is about existing schema objects, the DB is already up-to-date // If the error is about existing schema objects, the DB is already up-to-date
// This happens when ALTER migrations in client.ts have already added the columns, // This happens when ALTER migrations in client.ts have already added the columns,
// or when tables were created before drizzle migrations were introduced // or when tables were created before drizzle migrations were introduced
if (err.message?.includes("duplicate column") || err.message?.includes("already exists")) { if ((err as Error).message?.includes("duplicate column") || (err as Error).message?.includes("already exists")) {
return { success: true, warning: `Schema already up-to-date: ${err.message}` }; return { success: true, warning: `Schema already up-to-date: ${(err as Error).message}` };
} }
return { success: false, error: err.message }; return { success: false, error: (err as Error).message };
} }
} }
@@ -158,10 +158,10 @@ export async function runAlterMigrations(client: Client): Promise<{ success: boo
for (const sql of alterMigrations) { for (const sql of alterMigrations) {
try { try {
await client.execute(sql); await client.execute(sql);
} catch (e: any) { } catch (e: unknown) {
// Silently ignore "duplicate column" errors - column already exists // Silently ignore "duplicate column" errors - column already exists
if (!e.message?.includes("duplicate column")) { if (!(e as Error).message?.includes("duplicate column")) {
errors.push(e.message); errors.push((e as Error).message);
} }
} }
} }
@@ -182,10 +182,10 @@ export async function runAlterMigrations(client: Client): Promise<{ success: boo
for (const sql of createTableMigrations) { for (const sql of createTableMigrations) {
try { try {
await client.execute(sql); await client.execute(sql);
} catch (e: any) { } catch (e: unknown) {
// Silently ignore "table already exists" errors // Silently ignore "table already exists" errors
if (!e.message?.includes("already exists")) { if (!(e as Error).message?.includes("already exists")) {
errors.push(e.message); errors.push((e as Error).message);
} }
} }
} }
@@ -199,10 +199,10 @@ export async function runAlterMigrations(client: Client): Promise<{ success: boo
for (const sql of createIndexMigrations) { for (const sql of createIndexMigrations) {
try { try {
await client.execute(sql); await client.execute(sql);
} catch (e: any) { } catch (e: unknown) {
// Silently ignore "already exists" errors // Silently ignore "already exists" errors
if (!e.message?.includes("already exists")) { if (!(e as Error).message?.includes("already exists")) {
errors.push(e.message); errors.push((e as Error).message);
} }
} }
} }
@@ -227,8 +227,8 @@ export async function ensureDefaultUser(client: Client, authEnabled: boolean): P
return true; // Created return true; // Created
} }
return false; // Already exists return false; // Already exists
} catch (e: any) { } catch (e: unknown) {
console.error(`[DB] Error creating default user:`, e.message); console.error(`[DB] Error creating default user:`, (e as Error).message);
return false; return false;
} }
} }
@@ -255,8 +255,8 @@ export async function repairTrailingHyphenDoseIds(client: Client): Promise<{ rep
"UPDATE dose_tracking SET dose_id = RTRIM(dose_id, '-') WHERE dose_id LIKE '%-'" "UPDATE dose_tracking SET dose_id = RTRIM(dose_id, '-') WHERE dose_id LIKE '%-'"
); );
repaired = result.rowsAffected; repaired = result.rowsAffected;
} catch (e: any) { } catch (e: unknown) {
errors.push(`Trailing-hyphen repair failed: ${e.message}`); errors.push(`Trailing-hyphen repair failed: ${(e as Error).message}`);
} }
return { repaired, errors }; return { repaired, errors };
@@ -379,14 +379,14 @@ export async function repairOrphanedDoseIds(client: Client): Promise<{ repaired:
args: [newDoseId, dose.id], args: [newDoseId, dose.id],
}); });
repaired++; repaired++;
} catch (e: any) { } catch (e: unknown) {
errors.push(`Failed to repair dose ${dose.id}: ${e.message}`); errors.push(`Failed to repair dose ${dose.id}: ${(e as Error).message}`);
} }
} }
} }
} }
} catch (e: any) { } catch (e: unknown) {
errors.push(`Repair failed: ${e.message}`); errors.push(`Repair failed: ${(e as Error).message}`);
} }
return { repaired, errors }; return { repaired, errors };
+2 -2
View File
@@ -41,8 +41,8 @@ export async function executeMigration(
const executed = Number(tables.rows[0].count) || 0; const executed = Number(tables.rows[0].count) || 0;
return { success: true, executed, errors }; return { success: true, executed, errors };
} catch (err: any) { } catch (err: unknown) {
errors.push(err.message); errors.push((err as Error).message);
return { success: false, executed: 0, errors }; return { success: false, executed: 0, errors };
} }
} }
+5 -2
View File
@@ -142,9 +142,12 @@ export async function requireAuth(request: FastifyRequest, reply: FastifyReply)
id: user.id, id: user.id,
username: user.username, username: user.username,
}; };
} catch (err: any) { } catch (err: unknown) {
// Re-throw our own errors // Re-throw our own errors
if (err?.message === "AUTH_REQUIRED" || err?.message === "USER_NOT_FOUND" || err?.message === "ACCOUNT_DISABLED") { if (
err instanceof Error &&
(err.message === "AUTH_REQUIRED" || err.message === "USER_NOT_FOUND" || err.message === "ACCOUNT_DISABLED")
) {
throw err; throw err;
} }
// JWT verification failed // JWT verification failed
+2 -5
View File
@@ -864,11 +864,8 @@ export async function medicationRoutes(app: FastifyInstance) {
const takenByJson = row.takenByJson ? JSON.parse(row.takenByJson) : []; const takenByJson = row.takenByJson ? JSON.parse(row.takenByJson) : [];
const intake = intakes[blisterIdx]; const intake = intakes[blisterIdx];
const intakePerson = intake?.takenBy; const intakePerson = intake?.takenBy;
const peopleForThisIntake: (string | null)[] = intakePerson const takenByFallback: (string | null)[] = takenByJson.length > 0 ? takenByJson : [null];
? [intakePerson] const peopleForThisIntake: (string | null)[] = intakePerson ? [intakePerson] : takenByFallback;
: takenByJson.length > 0
? takenByJson
: [null];
// Generate expected dose IDs and check if they're taken // Generate expected dose IDs and check if they're taken
for (let i = 0; i < occurrences; i++) { for (let i = 0; i < occurrences; i++) {
+6 -3
View File
@@ -104,7 +104,7 @@ export async function oidcRoutes(app: FastifyInstance) {
}); });
return reply.redirect(authUrl.href); return reply.redirect(authUrl.href);
} catch (err: any) { } catch (err: unknown) {
console.error("[OIDC] Login error:", err); console.error("[OIDC] Login error:", err);
return reply.redirect(`${getFrontendUrl()}/?error=oidc_init_failed`); return reply.redirect(`${getFrontendUrl()}/?error=oidc_init_failed`);
} }
@@ -167,7 +167,10 @@ export async function oidcRoutes(app: FastifyInstance) {
// Extract username from configured claim // Extract username from configured claim
const usernameClaim = env.OIDC_USERNAME_CLAIM; const usernameClaim = env.OIDC_USERNAME_CLAIM;
const username = const username =
(userInfo as any)[usernameClaim] || userInfo.preferred_username || userInfo.email || userInfo.sub; (userInfo as Record<string, string>)[usernameClaim] ||
userInfo.preferred_username ||
userInfo.email ||
userInfo.sub;
const oidcSubject = userInfo.sub; const oidcSubject = userInfo.sub;
if (!username || !oidcSubject) { if (!username || !oidcSubject) {
@@ -210,7 +213,7 @@ export async function oidcRoutes(app: FastifyInstance) {
// In dev: CORS_ORIGINS contains the frontend URL // In dev: CORS_ORIGINS contains the frontend URL
const frontendUrl = env.CORS_ORIGINS.split(",")[0] || "http://localhost:5173"; const frontendUrl = env.CORS_ORIGINS.split(",")[0] || "http://localhost:5173";
return reply.redirect(`${frontendUrl}/dashboard`); return reply.redirect(`${frontendUrl}/dashboard`);
} catch (err: any) { } catch (err: unknown) {
console.error("[OIDC] Callback error:", err); console.error("[OIDC] Callback error:", err);
return reply.redirect(`${getFrontendUrl()}/?error=oidc_callback_failed`); return reply.redirect(`${getFrontendUrl()}/?error=oidc_callback_failed`);
} }
+16 -11
View File
@@ -509,8 +509,10 @@ ${getFooterPlain(language)}`;
const buildTableRow = (row: LowStockItem) => { const buildTableRow = (row: LowStockItem) => {
const isEmpty = row.medsLeft <= 0; const isEmpty = row.medsLeft <= 0;
const isCritical = row.isCritical !== false; const isCritical = row.isCritical !== false;
const statusIcon = isEmpty ? "🚨" : isCritical ? "🚨" : "⚠️"; const nonEmptyIcon = isCritical ? "🚨" : "⚠️";
const rowBg = isEmpty ? "#fef2f2" : isCritical ? "#fff7ed" : "white"; const statusIcon = isEmpty ? "🚨" : nonEmptyIcon;
const nonEmptyBg = isCritical ? "#fff7ed" : "white";
const rowBg = isEmpty ? "#fef2f2" : nonEmptyBg;
const safeName = escapeHtml(row.name); const safeName = escapeHtml(row.name);
const safeMedsLeft = Number(row.medsLeft) || 0; const safeMedsLeft = Number(row.medsLeft) || 0;
const safeDaysLeft = Number(row.daysLeft) || 0; const safeDaysLeft = Number(row.daysLeft) || 0;
@@ -586,7 +588,7 @@ ${getFooterPlain(language)}`;
// Send push notification if enabled // Send push notification if enabled
if (notificationSettings.shoutrrrEnabled && notificationSettings.shoutrrrUrl) { if (notificationSettings.shoutrrrEnabled && notificationSettings.shoutrrrUrl) {
const message = messageParts.join("\n") + `\n\n---\n${getFooterPlain(language)}`; const message = `${messageParts.join("\n")}\n\n---\n${getFooterPlain(language)}`;
try { try {
const pushResult = await sendShoutrrrNotification(notificationSettings.shoutrrrUrl, notificationTitle, message); const pushResult = await sendShoutrrrNotification(notificationSettings.shoutrrrUrl, notificationTitle, message);
@@ -603,7 +605,8 @@ ${getFooterPlain(language)}`;
// Update the reminder state to record this notification was sent // Update the reminder state to record this notification was sent
if (results.email || results.push) { if (results.email || results.push) {
const channel = results.email && results.push ? "both" : results.email ? "email" : "push"; const singleChannel = results.email ? "email" : "push";
const channel = results.email && results.push ? "both" : singleChannel;
updateReminderSentTime("stock", channel); updateReminderSentTime("stock", channel);
// Also update user settings in database so frontend can display the info // Also update user settings in database so frontend can display the info
@@ -700,14 +703,15 @@ ${getFooterPlain(language)}`;
const bodyText = const bodyText =
emptyRx.length > 0 ? tr.prescriptionReminder.descriptionEmpty : tr.prescriptionReminder.descriptionLow; emptyRx.length > 0 ? tr.prescriptionReminder.descriptionEmpty : tr.prescriptionReminder.descriptionLow;
const alertText = const emptyAlert =
emptyRx.length > 0 emptyRx.length === 1
? emptyRx.length === 1
? tr.prescriptionReminder.alertEmptySingle ? tr.prescriptionReminder.alertEmptySingle
: t(tr.prescriptionReminder.alertEmptyMultiple, { count: emptyRx.length }) : t(tr.prescriptionReminder.alertEmptyMultiple, { count: emptyRx.length });
: lowRx.length === 1 const lowAlert =
lowRx.length === 1
? tr.prescriptionReminder.alertLowSingle ? tr.prescriptionReminder.alertLowSingle
: t(tr.prescriptionReminder.alertLowMultiple, { count: lowRx.length }); : t(tr.prescriptionReminder.alertLowMultiple, { count: lowRx.length });
const alertText = emptyRx.length > 0 ? emptyAlert : lowAlert;
const tableRows = filteredPrescriptionLow const tableRows = filteredPrescriptionLow
.map((item) => { .map((item) => {
@@ -807,7 +811,7 @@ ${getFooterPlain(language)}`;
); );
} }
} }
const message = messageParts.join("\n") + `\n\n---\n${getFooterPlain(language)}`; const message = `${messageParts.join("\n")}\n\n---\n${getFooterPlain(language)}`;
try { try {
const pushResult = await sendShoutrrrNotification(userSettings.shoutrrrUrl, title, message); const pushResult = await sendShoutrrrNotification(userSettings.shoutrrrUrl, title, message);
@@ -823,7 +827,8 @@ ${getFooterPlain(language)}`;
} }
if (results.email || results.push) { if (results.email || results.push) {
const channel = results.email && results.push ? "both" : results.email ? "email" : "push"; const singleChannel = results.email ? "email" : "push";
const channel = results.email && results.push ? "both" : singleChannel;
updateReminderSentTime("prescription", channel); updateReminderSentTime("prescription", channel);
await updateUserReminderSentTime(userId, "prescription", channel, medNames); await updateUserReminderSentTime(userId, "prescription", channel, medNames);
} }
+3 -3
View File
@@ -1,5 +1,5 @@
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { FastifyInstance } from "fastify"; import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import nodemailer from "nodemailer"; import nodemailer from "nodemailer";
import { db } from "../db/client.js"; import { db } from "../db/client.js";
import { userSettings } from "../db/schema.js"; import { userSettings } from "../db/schema.js";
@@ -239,7 +239,7 @@ export async function settingsRoutes(app: FastifyInstance) {
// Helper to get user ID from request // Helper to get user ID from request
// Returns anonymous user ID when auth is disabled // Returns anonymous user ID when auth is disabled
async function getUserId(request: any, reply: any): Promise<number> { async function getUserId(request: FastifyRequest, reply: FastifyReply): Promise<number> {
// If auth is disabled, use the anonymous user // If auth is disabled, use the anonymous user
if (!env.AUTH_ENABLED) { if (!env.AUTH_ENABLED) {
return getAnonymousUserId(); return getAnonymousUserId();
@@ -544,7 +544,7 @@ export async function sendShoutrrrNotification(
} }
// Use ONLY the reconstructed URL from validation - never the original urlStr // Use ONLY the reconstructed URL from validation - never the original urlStr
const { url: sanitizedUrl, isNtfy, auth } = validation; const { url: sanitizedUrl, isNtfy: _isNtfy, auth } = validation;
let targetUrl: string; let targetUrl: string;
const method = "POST"; const method = "POST";
@@ -22,7 +22,6 @@ import {
getTimezone, getTimezone,
getTodaysIntakes, getTodaysIntakes,
getUpcomingIntakes, getUpcomingIntakes,
type Intake,
type IntakeReminderState, type IntakeReminderState,
parseIntakeReminderState, parseIntakeReminderState,
parseIntakesJson, parseIntakesJson,
@@ -321,7 +320,7 @@ async function checkAndSendIntakeRemindersForUser(
}); });
// Process each intake separately to track blisterIndex // Process each intake separately to track blisterIndex
intakesWithReminders.forEach((intake, blisterIndex) => { intakesWithReminders.forEach((intake, _blisterIndex) => {
const actualIndex = intakes.indexOf(intake); // Get the actual index in original array const actualIndex = intakes.indexOf(intake); // Get the actual index in original array
logger.debug( logger.debug(
`[IntakeReminder] User ${settings.userId}: Intake ${actualIndex} - start: ${intake.start}, every: ${intake.every} days, usage: ${intake.usage}, takenBy: ${intake.takenBy || "(none)"}` `[IntakeReminder] User ${settings.userId}: Intake ${actualIndex} - start: ${intake.start}, every: ${intake.every} days, usage: ${intake.usage}, takenBy: ${intake.takenBy || "(none)"}`
@@ -684,7 +683,8 @@ async function checkAndSendIntakeRemindersForUser(
saveIntakeReminderState(state); saveIntakeReminderState(state);
// Update global reminder state for UI display // Update global reminder state for UI display
const channel = emailSuccess && shoutrrrSuccess ? "both" : emailSuccess ? "email" : "push"; const singleChannel = emailSuccess ? "email" : "push";
const channel = emailSuccess && shoutrrrSuccess ? "both" : singleChannel;
updateReminderSentTime("intake", channel); updateReminderSentTime("intake", channel);
// Also update user settings in database so frontend can display the info // Also update user settings in database so frontend can display the info
+18 -14
View File
@@ -275,8 +275,10 @@ async function sendReminderEmail(
.map((row) => { .map((row) => {
const isEmpty = row.medsLeft <= 0; const isEmpty = row.medsLeft <= 0;
const isCritical = row.isCritical; const isCritical = row.isCritical;
const statusIcon = isEmpty ? "🚨" : isCritical ? "🚨" : "⚠️"; const nonEmptyIcon = isCritical ? "🚨" : "⚠️";
const rowBg = isEmpty ? "#fef2f2" : isCritical ? "#fff7ed" : "white"; const statusIcon = isEmpty ? "🚨" : nonEmptyIcon;
const nonEmptyBg = isCritical ? "#fff7ed" : "white";
const rowBg = isEmpty ? "#fef2f2" : nonEmptyBg;
return ` return `
<tr style="background: ${rowBg};"> <tr style="background: ${rowBg};">
<td style="padding: 10px 12px; border-bottom: 1px solid #e5e7eb; white-space: nowrap;">${statusIcon} ${row.name}</td> <td style="padding: 10px 12px; border-bottom: 1px solid #e5e7eb; white-space: nowrap;">${statusIcon} ${row.name}</td>
@@ -329,7 +331,8 @@ ${lowStock.map((r) => `${r.name}: ${r.medsLeft} ${tr.common.pills}, ${r.daysLeft
--- ---
${getFooterPlain(language)}${isRepeatDaily ? `\n\n${tr.stockReminder.repeatDailyNote}` : ""}`; ${getFooterPlain(language)}${isRepeatDaily ? `\n\n${tr.stockReminder.repeatDailyNote}` : ""}`;
const subjectPlural = lowStock.length === 1 ? "" : language === "de" ? "e" : "s"; const pluralSuffix = language === "de" ? "e" : "s";
const subjectPlural = lowStock.length === 1 ? "" : pluralSuffix;
const subject = t(tr.stockReminder.subject, { count: lowStock.length, s: subjectPlural, e: subjectPlural }); const subject = t(tr.stockReminder.subject, { count: lowStock.length, s: subjectPlural, e: subjectPlural });
try { try {
@@ -460,7 +463,7 @@ async function checkAndSendReminderForUser(
) )
); );
} }
const message = messageParts.join("\n") + `\n\n---\n${getFooterPlain(language)}`; const message = `${messageParts.join("\n")}\n\n---\n${getFooterPlain(language)}`;
const result = await sendShoutrrrNotification(settings.shoutrrrUrl!, title, message); const result = await sendShoutrrrNotification(settings.shoutrrrUrl!, title, message);
shoutrrrSuccess = result.success; shoutrrrSuccess = result.success;
if (!result.success) { if (!result.success) {
@@ -470,7 +473,8 @@ async function checkAndSendReminderForUser(
if (emailSuccess || shoutrrrSuccess) { if (emailSuccess || shoutrrrSuccess) {
const currentState = loadReminderState(); const currentState = loadReminderState();
const channel = emailSuccess && shoutrrrSuccess ? "both" : emailSuccess ? "email" : "push"; const singleChannel = emailSuccess ? "email" : "push";
const channel = emailSuccess && shoutrrrSuccess ? "both" : singleChannel;
saveReminderState({ saveReminderState({
lastAutoEmailSent: new Date().toISOString(), lastAutoEmailSent: new Date().toISOString(),
lastAutoEmailDate: today, lastAutoEmailDate: today,
@@ -480,7 +484,6 @@ async function checkAndSendReminderForUser(
lastNotificationChannel: channel, lastNotificationChannel: channel,
}); });
const firstMed = allLowStock[0];
const medNames = allLowStock.map((m) => m.name).join(", "); const medNames = allLowStock.map((m) => m.name).join(", ");
await updateUserReminderSentTime(settings.userId, "stock", channel, medNames); await updateUserReminderSentTime(settings.userId, "stock", channel, medNames);
} }
@@ -537,14 +540,15 @@ async function checkAndSendReminderForUser(
const bodyText = const bodyText =
emptyRx.length > 0 ? tr.prescriptionReminder.descriptionEmpty : tr.prescriptionReminder.descriptionLow; emptyRx.length > 0 ? tr.prescriptionReminder.descriptionEmpty : tr.prescriptionReminder.descriptionLow;
const alertText = const emptyAlert =
emptyRx.length > 0 emptyRx.length === 1
? emptyRx.length === 1
? tr.prescriptionReminder.alertEmptySingle ? tr.prescriptionReminder.alertEmptySingle
: t(tr.prescriptionReminder.alertEmptyMultiple, { count: emptyRx.length }) : t(tr.prescriptionReminder.alertEmptyMultiple, { count: emptyRx.length });
: lowRx.length === 1 const lowAlert =
lowRx.length === 1
? tr.prescriptionReminder.alertLowSingle ? tr.prescriptionReminder.alertLowSingle
: t(tr.prescriptionReminder.alertLowMultiple, { count: lowRx.length }); : t(tr.prescriptionReminder.alertLowMultiple, { count: lowRx.length });
const alertText = emptyRx.length > 0 ? emptyAlert : lowAlert;
const tableRows = allPrescriptionLow const tableRows = allPrescriptionLow
.map((item) => { .map((item) => {
@@ -649,7 +653,7 @@ async function checkAndSendReminderForUser(
); );
} }
} }
const message = messageParts.join("\n") + `\n\n---\n${getFooterPlain(language)}`; const message = `${messageParts.join("\n")}\n\n---\n${getFooterPlain(language)}`;
const result = await sendShoutrrrNotification(settings.shoutrrrUrl!, title, message); const result = await sendShoutrrrNotification(settings.shoutrrrUrl!, title, message);
shoutrrrSuccess = result.success; shoutrrrSuccess = result.success;
if (!result.success) { if (!result.success) {
@@ -659,7 +663,8 @@ async function checkAndSendReminderForUser(
if (emailSuccess || shoutrrrSuccess) { if (emailSuccess || shoutrrrSuccess) {
const currentState = loadReminderState(); const currentState = loadReminderState();
const channel = emailSuccess && shoutrrrSuccess ? "both" : emailSuccess ? "email" : "push"; const singleChannel = emailSuccess ? "email" : "push";
const channel = emailSuccess && shoutrrrSuccess ? "both" : singleChannel;
saveReminderState({ saveReminderState({
lastAutoEmailSent: new Date().toISOString(), lastAutoEmailSent: new Date().toISOString(),
lastAutoEmailDate: today, lastAutoEmailDate: today,
@@ -669,7 +674,6 @@ async function checkAndSendReminderForUser(
lastNotificationChannel: channel, lastNotificationChannel: channel,
}); });
const firstMed = allPrescriptionLow[0];
const medNames = allPrescriptionLow.map((m) => m.name).join(", "); const medNames = allPrescriptionLow.map((m) => m.name).join(", ");
await updateUserReminderSentTime(settings.userId, "prescription", channel, medNames); await updateUserReminderSentTime(settings.userId, "prescription", channel, medNames);
} }
+9 -9
View File
@@ -294,8 +294,8 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
// Should set cookies // Should set cookies
const cookies = response.cookies; const cookies = response.cookies;
expect(cookies.find((c: any) => c.name === "access_token")).toBeDefined(); expect(cookies.find((c: { name: string }) => c.name === "access_token")).toBeDefined();
expect(cookies.find((c: any) => c.name === "refresh_token")).toBeDefined(); expect(cookies.find((c: { name: string }) => c.name === "refresh_token")).toBeDefined();
}); });
it("should login case-insensitively with different username casing", async () => { it("should login case-insensitively with different username casing", async () => {
@@ -393,7 +393,7 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
}, },
}); });
const refreshToken = login.cookies.find((c: any) => c.name === "refresh_token"); const refreshToken = login.cookies.find((c: { name: string }) => c.name === "refresh_token");
const response = await app.inject({ const response = await app.inject({
method: "POST", method: "POST",
@@ -456,7 +456,7 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
}, },
}); });
const refreshToken = login.cookies.find((c: any) => c.name === "refresh_token"); const refreshToken = login.cookies.find((c: { name: string }) => c.name === "refresh_token");
const response = await app.inject({ const response = await app.inject({
method: "POST", method: "POST",
@@ -506,7 +506,7 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
}, },
}); });
const accessToken = login.cookies.find((c: any) => c.name === "access_token"); const accessToken = login.cookies.find((c: { name: string }) => c.name === "access_token");
const response = await app.inject({ const response = await app.inject({
method: "GET", method: "GET",
@@ -604,7 +604,7 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
}, },
}); });
const accessToken = login.cookies.find((c: any) => c.name === "access_token"); const accessToken = login.cookies.find((c: { name: string }) => c.name === "access_token");
const response = await app.inject({ const response = await app.inject({
method: "PUT", method: "PUT",
@@ -653,7 +653,7 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
}, },
}); });
const accessToken = login.cookies.find((c: any) => c.name === "access_token"); const accessToken = login.cookies.find((c: { name: string }) => c.name === "access_token");
const response = await app.inject({ const response = await app.inject({
method: "PUT", method: "PUT",
@@ -689,7 +689,7 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
}, },
}); });
const accessToken = login.cookies.find((c: any) => c.name === "access_token"); const accessToken = login.cookies.find((c: { name: string }) => c.name === "access_token");
const response = await app.inject({ const response = await app.inject({
method: "PUT", method: "PUT",
@@ -742,7 +742,7 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
}, },
}); });
const accessToken = login.cookies.find((c: any) => c.name === "access_token"); const accessToken = login.cookies.find((c: { name: string }) => c.name === "access_token");
// Delete account // Delete account
const response = await app.inject({ const response = await app.inject({
+1 -1
View File
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
import { createClient } from "@libsql/client"; import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql"; import { drizzle } from "drizzle-orm/libsql";
import { migrate } from "drizzle-orm/libsql/migrator"; import { migrate } from "drizzle-orm/libsql/migrator";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it } from "vitest";
// Import utility functions from db-utils (no side effects, unlike client.ts which initializes the DB) // Import utility functions from db-utils (no side effects, unlike client.ts which initializes the DB)
import { import {
+1 -1
View File
@@ -271,7 +271,7 @@ describe("Dose Tracking API", () => {
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
const data = response.json(); const data = response.json();
expect(data.doses).toHaveLength(2); expect(data.doses).toHaveLength(2);
expect(data.doses.map((d: any) => d.doseId).sort()).toEqual([doseId1, doseId2].sort()); expect(data.doses.map((d: { doseId: string }) => d.doseId).sort()).toEqual([doseId1, doseId2].sort());
// Each dose should have a takenAt timestamp // Each dose should have a takenAt timestamp
for (const dose of data.doses) { for (const dose of data.doses) {
expect(dose.takenAt).toBeTypeOf("number"); expect(dose.takenAt).toBeTypeOf("number");
+11 -9
View File
@@ -1671,7 +1671,7 @@ describe("E2E Tests with Real Routes", () => {
url: "/medications", url: "/medications",
}); });
expect(medsResponse.statusCode).toBe(200); expect(medsResponse.statusCode).toBe(200);
const med = medsResponse.json().find((m: any) => m.id === medId); const med = medsResponse.json().find((m: Record<string, unknown>) => m.id === medId);
expect(med.prescriptionRemainingRefills).toBe(1); expect(med.prescriptionRemainingRefills).toBe(1);
const historyResponse = await app.inject({ const historyResponse = await app.inject({
@@ -1809,8 +1809,10 @@ describe("E2E Tests with Real Routes", () => {
const refills = response.json(); const refills = response.json();
expect(refills).toHaveLength(2); expect(refills).toHaveLength(2);
// Check both refills exist (order may vary) // Check both refills exist (order may vary)
const hasPackRefill = refills.some((r: any) => r.packsAdded === 1 && r.loosePillsAdded === 0); const hasPackRefill = refills.some((r: Record<string, unknown>) => r.packsAdded === 1 && r.loosePillsAdded === 0);
const hasLooseRefill = refills.some((r: any) => r.packsAdded === 0 && r.loosePillsAdded === 5); const hasLooseRefill = refills.some(
(r: Record<string, unknown>) => r.packsAdded === 0 && r.loosePillsAdded === 5
);
expect(hasPackRefill).toBe(true); expect(hasPackRefill).toBe(true);
expect(hasLooseRefill).toBe(true); expect(hasLooseRefill).toBe(true);
}); });
@@ -1888,7 +1890,7 @@ describe("E2E Tests with Real Routes", () => {
expect(getResponse.statusCode).toBe(200); expect(getResponse.statusCode).toBe(200);
const meds = getResponse.json(); const meds = getResponse.json();
const med = meds.find((m: any) => m.id === medId); const med = meds.find((m: Record<string, unknown>) => m.id === medId);
expect(med).toBeDefined(); expect(med).toBeDefined();
expect(med.stockAdjustment).toBe(-7); expect(med.stockAdjustment).toBe(-7);
expect(med.lastStockCorrectionAt).toBeTruthy(); expect(med.lastStockCorrectionAt).toBeTruthy();
@@ -1934,7 +1936,7 @@ describe("E2E Tests with Real Routes", () => {
method: "GET", method: "GET",
url: "/medications", url: "/medications",
}); });
const med = getResponse.json().find((m: any) => m.id === medId); const med = getResponse.json().find((m: Record<string, unknown>) => m.id === medId);
expect(med.name).toBe("Renamed Med"); expect(med.name).toBe("Renamed Med");
expect(med.stockAdjustment).toBe(-5); expect(med.stockAdjustment).toBe(-5);
}); });
@@ -2003,7 +2005,7 @@ describe("E2E Tests with Real Routes", () => {
// Verify adjustment is set // Verify adjustment is set
let getMeds = await app.inject({ method: "GET", url: "/medications" }); let getMeds = await app.inject({ method: "GET", url: "/medications" });
let med = getMeds.json().find((m: any) => m.id === medId); let med = getMeds.json().find((m: Record<string, unknown>) => m.id === medId);
expect(med.stockAdjustment).toBe(-10); expect(med.stockAdjustment).toBe(-10);
// Edit medication with CHANGED stock fields (packCount 1 → 2) // Edit medication with CHANGED stock fields (packCount 1 → 2)
@@ -2022,7 +2024,7 @@ describe("E2E Tests with Real Routes", () => {
// stockAdjustment should be reset to 0 // stockAdjustment should be reset to 0
getMeds = await app.inject({ method: "GET", url: "/medications" }); getMeds = await app.inject({ method: "GET", url: "/medications" });
med = getMeds.json().find((m: any) => m.id === medId); med = getMeds.json().find((m: Record<string, unknown>) => m.id === medId);
expect(med.stockAdjustment).toBe(0); expect(med.stockAdjustment).toBe(0);
expect(med.lastStockCorrectionAt).toBeTruthy(); expect(med.lastStockCorrectionAt).toBeTruthy();
}); });
@@ -2066,7 +2068,7 @@ describe("E2E Tests with Real Routes", () => {
// stockAdjustment should be preserved // stockAdjustment should be preserved
const getMeds = await app.inject({ method: "GET", url: "/medications" }); const getMeds = await app.inject({ method: "GET", url: "/medications" });
const med = getMeds.json().find((m: any) => m.id === medId); const med = getMeds.json().find((m: Record<string, unknown>) => m.id === medId);
expect(med.name).toBe("Renamed Preserve Med"); expect(med.name).toBe("Renamed Preserve Med");
expect(med.stockAdjustment).toBe(-5); expect(med.stockAdjustment).toBe(-5);
}); });
@@ -2114,7 +2116,7 @@ describe("E2E Tests with Real Routes", () => {
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
const data = response.json(); const data = response.json();
const med = data.find((m: any) => m.medicationId === medId); const med = data.find((m: Record<string, unknown>) => m.medicationId === medId);
expect(med).toBeDefined(); expect(med).toBeDefined();
// Total should be very close to 113 (not 112 or lower from phantom consumption) // Total should be very close to 113 (not 112 or lower from phantom consumption)
// Allow up to 1 pill of natural consumption (test runs fast, but at most 1 day could pass) // Allow up to 1 pill of natural consumption (test runs fast, but at most 1 day could pass)
+1 -1
View File
@@ -3,7 +3,7 @@ import { z } from "zod";
// Mock process.exit to prevent tests from exiting // Mock process.exit to prevent tests from exiting
const mockExit = vi.fn(); const mockExit = vi.fn();
vi.spyOn(process, "exit").mockImplementation(mockExit as any); vi.spyOn(process, "exit").mockImplementation(mockExit as unknown as (...args: unknown[]) => never);
// Re-create the schema from env.ts for testing // Re-create the schema from env.ts for testing
const EnvSchema = z.object({ const EnvSchema = z.object({
+18 -9
View File
@@ -23,10 +23,12 @@ async function registerExportRoutes(ctx: TestContext) {
const userId = 1; // Test user ID const userId = 1; // Test user ID
// Helper to parse blisters from DB // Helper to parse blisters from DB
function parseBlisters(row: any): Array<{ usage: number; every: number; start: string; remind: boolean }> { function parseBlisters(
const usage = JSON.parse(row.usage_json || "[]") as number[]; row: Record<string, unknown>
const every = JSON.parse(row.every_json || "[]") as number[]; ): Array<{ usage: number; every: number; start: string; remind: boolean }> {
const start = JSON.parse(row.start_json || "[]") as string[]; const usage = JSON.parse((row.usage_json as string) || "[]") as number[];
const every = JSON.parse((row.every_json as string) || "[]") as number[];
const start = JSON.parse((row.start_json as string) || "[]") as string[];
const len = Math.min(usage.length, every.length, start.length); const len = Math.min(usage.length, every.length, start.length);
return Array.from({ length: len }, (_, i) => ({ return Array.from({ length: len }, (_, i) => ({
usage: usage[i], usage: usage[i],
@@ -99,7 +101,7 @@ async function registerExportRoutes(ctx: TestContext) {
args: [userId], args: [userId],
}); });
let settings; let settings: Record<string, unknown> | undefined;
if (settingsResult.rows.length > 0) { if (settingsResult.rows.length > 0) {
const s = settingsResult.rows[0]; const s = settingsResult.rows[0];
settings = { settings = {
@@ -150,7 +152,8 @@ async function registerExportRoutes(ctx: TestContext) {
}); });
// POST /import // POST /import
app.post<{ Body: any }>("/import", async (request, reply) => { // biome-ignore lint/suspicious/noExplicitAny: test helper with dynamic import data shape
app.post("/import", async (request, reply) => {
const importData = request.body as any; const importData = request.body as any;
// Basic validation // Basic validation
@@ -167,9 +170,15 @@ async function registerExportRoutes(ctx: TestContext) {
// Import medications // Import medications
const exportIdToNewId = new Map<string, number>(); const exportIdToNewId = new Map<string, number>();
for (const med of importData.medications || []) { for (const med of importData.medications || []) {
const usageJson = JSON.stringify((med.schedules || []).map((s: any) => s.usage)); const usageJson = JSON.stringify(
const everyJson = JSON.stringify((med.schedules || []).map((s: any) => s.every)); ((med.schedules as Array<Record<string, unknown>>) || []).map((s: Record<string, unknown>) => s.usage)
const startJson = JSON.stringify((med.schedules || []).map((s: any) => s.start)); );
const everyJson = JSON.stringify(
((med.schedules as Array<Record<string, unknown>>) || []).map((s: Record<string, unknown>) => s.every)
);
const startJson = JSON.stringify(
((med.schedules as Array<Record<string, unknown>>) || []).map((s: Record<string, unknown>) => s.start)
);
const takenByJson = JSON.stringify(med.takenBy || []); const takenByJson = JSON.stringify(med.takenBy || []);
const result = await client.execute({ const result = await client.execute({
+4 -4
View File
@@ -1333,8 +1333,8 @@ describe("Integration Tests", () => {
url: "/medications", url: "/medications",
}); });
const meds = medsRes.json(); const meds = medsRes.json();
const med1 = meds.find((m: any) => m.id === med1Id); const med1 = meds.find((m: Record<string, unknown>) => m.id === med1Id);
const med2 = meds.find((m: any) => m.id === med2Id); const med2 = meds.find((m: Record<string, unknown>) => m.id === med2Id);
expect(med1.dismissedUntil).toBe("2025-01-15"); expect(med1.dismissedUntil).toBe("2025-01-15");
expect(med2.dismissedUntil).toBe("2025-01-15"); expect(med2.dismissedUntil).toBe("2025-01-15");
@@ -1376,7 +1376,7 @@ describe("Integration Tests", () => {
method: "GET", method: "GET",
url: "/medications", url: "/medications",
}); });
const med = medsRes.json().find((m: any) => m.id === medId); const med = medsRes.json().find((m: Record<string, unknown>) => m.id === medId);
expect(med.dismissedUntil).toBeNull(); expect(med.dismissedUntil).toBeNull();
}); });
@@ -1446,7 +1446,7 @@ describe("Integration Tests", () => {
method: "GET", method: "GET",
url: "/medications", url: "/medications",
}); });
const med = medsRes.json().find((m: any) => m.id === medId); const med = medsRes.json().find((m: Record<string, unknown>) => m.id === medId);
expect(med.dismissedUntil).toBeNull(); expect(med.dismissedUntil).toBeNull();
}); });
}); });
+1 -1
View File
@@ -63,7 +63,7 @@ vi.mock("../services/reminder-scheduler.js", () => ({
// Mock sendShoutrrrNotification from settings // Mock sendShoutrrrNotification from settings
vi.mock("../routes/settings.js", async (importOriginal) => { vi.mock("../routes/settings.js", async (importOriginal) => {
const original = (await importOriginal()) as any; const original = (await importOriginal()) as Record<string, unknown>;
return { return {
...original, ...original,
sendShoutrrrNotification: mockSendShoutrrr, sendShoutrrrNotification: mockSendShoutrrr,
+16 -8
View File
@@ -4,7 +4,7 @@ import { resolve } from "node:path";
import cookie from "@fastify/cookie"; import cookie from "@fastify/cookie";
import cors from "@fastify/cors"; import cors from "@fastify/cors";
import sensible from "@fastify/sensible"; import sensible from "@fastify/sensible";
import Fastify from "fastify"; import Fastify, { type FastifyInstance } from "fastify";
import { afterEach, describe, expect, it } from "vitest"; import { afterEach, describe, expect, it } from "vitest";
// Import from utils to avoid index.ts import side effects (server start) // Import from utils to avoid index.ts import side effects (server start)
@@ -294,10 +294,18 @@ describe("Server Bootstrap", () => {
refreshCookieOptions, refreshCookieOptions,
}); });
expect((app as any).config.accessTtl).toBe(15); const appWithConfig = app as unknown as {
expect((app as any).config.refreshTtl).toBe(7); config: {
expect((app as any).config.cookieOptions.httpOnly).toBe(true); accessTtl: number;
expect((app as any).config.refreshCookieOptions.maxAge).toBe(7 * 24 * 60 * 60); refreshTtl: number;
cookieOptions: { httpOnly: boolean };
refreshCookieOptions: { maxAge: number };
};
};
expect(appWithConfig.config.accessTtl).toBe(15);
expect(appWithConfig.config.refreshTtl).toBe(7);
expect(appWithConfig.config.cookieOptions.httpOnly).toBe(true);
expect(appWithConfig.config.refreshCookieOptions.maxAge).toBe(7 * 24 * 60 * 60);
await app.close(); await app.close();
}); });
@@ -364,15 +372,15 @@ describe("Server Bootstrap", () => {
const app = Fastify({ logger: false }); const app = Fastify({ logger: false });
// Mock route plugins // Mock route plugins
const healthRoutes = async (app: any) => { const healthRoutes = async (app: FastifyInstance) => {
app.get("/health", async () => ({ status: "ok" })); app.get("/health", async () => ({ status: "ok" }));
}; };
const authRoutes = async (app: any) => { const authRoutes = async (app: FastifyInstance) => {
app.post("/auth/login", async () => ({ token: "mock" })); app.post("/auth/login", async () => ({ token: "mock" }));
}; };
const medicationRoutes = async (app: any) => { const medicationRoutes = async (app: FastifyInstance) => {
app.get("/medications", async () => []); app.get("/medications", async () => []);
}; };
+2 -2
View File
@@ -612,8 +612,8 @@ describe("Stock Calculation API", () => {
const data = response.json(); const data = response.json();
expect(data).toHaveLength(2); expect(data).toHaveLength(2);
const medA = data.find((d: any) => d.medicationName === "Med A"); const medA = data.find((d: Record<string, unknown>) => d.medicationName === "Med A");
const medB = data.find((d: any) => d.medicationName === "Med B"); const medB = data.find((d: Record<string, unknown>) => d.medicationName === "Med B");
expect(medA.plannerUsage).toBe(10); // 10 days × 1 pill expect(medA.plannerUsage).toBe(10); // 10 days × 1 pill
expect(medB.plannerUsage).toBe(10); // 5 doses × 2 pills expect(medB.plannerUsage).toBe(10); // 5 doses × 2 pills
+3 -3
View File
@@ -191,7 +191,7 @@ export function parseIntakesJson(
try { try {
const parsed = JSON.parse(intakesJson); const parsed = JSON.parse(intakesJson);
if (Array.isArray(parsed) && parsed.length > 0) { if (Array.isArray(parsed) && parsed.length > 0) {
return parsed.map((intake: any) => ({ return parsed.map((intake: Record<string, unknown>) => ({
usage: typeof intake.usage === "number" ? intake.usage : 0, usage: typeof intake.usage === "number" ? intake.usage : 0,
every: typeof intake.every === "number" ? intake.every : 1, every: typeof intake.every === "number" ? intake.every : 1,
start: typeof intake.start === "string" ? intake.start : new Date().toISOString(), start: typeof intake.start === "string" ? intake.start : new Date().toISOString(),
@@ -312,7 +312,7 @@ export type UpcomingIntake = {
export function getTodaysIntakes( export function getTodaysIntakes(
medName: string, medName: string,
intakes: Intake[], intakes: Intake[],
medicationTakenBy: string[], // Medication-level takenBy as fallback _medicationTakenBy: string[], // Medication-level takenBy as fallback
pillWeightMg: number | null, pillWeightMg: number | null,
locale: string, locale: string,
tz?: string, tz?: string,
@@ -388,7 +388,7 @@ export function getUpcomingIntakes(
medName: string, medName: string,
intakes: Intake[], intakes: Intake[],
minutesBefore: number, minutesBefore: number,
medicationTakenBy: string[], // Medication-level takenBy as fallback _medicationTakenBy: string[], // Medication-level takenBy as fallback
pillWeightMg: number | null, pillWeightMg: number | null,
locale: string, locale: string,
tz?: string, tz?: string,
+2 -1
View File
@@ -16,7 +16,8 @@
"rules": { "rules": {
"recommended": true, "recommended": true,
"complexity": { "complexity": {
"noForEach": "off" "noForEach": "off",
"noImportantStyles": "off"
}, },
"suspicious": { "suspicious": {
"noExplicitAny": "warn", "noExplicitAny": "warn",
-1
View File
@@ -2,7 +2,6 @@ import {
authFile, authFile,
createMedicationViaAPI, createMedicationViaAPI,
deleteAllMedicationsViaAPI, deleteAllMedicationsViaAPI,
deleteMedicationViaAPI,
expect, expect,
navigateTo, navigateTo,
type TestMedication, type TestMedication,
+1 -1
View File
@@ -71,7 +71,7 @@ test.describe("Medications Page", () => {
// Either blister or bottle fields depending on package type // Either blister or bottle fields depending on package type
const blistersField = page.getByLabel(/Blisters per pack/i); const blistersField = page.getByLabel(/Blisters per pack/i);
const pillsField = page.getByLabel(/Pills per blister/i); const _pillsField = page.getByLabel(/Pills per blister/i);
const capacityField = page.getByLabel(/Total Capacity/i); const capacityField = page.getByLabel(/Total Capacity/i);
const hasBlister = await blistersField.isVisible().catch(() => false); const hasBlister = await blistersField.isVisible().catch(() => false);
-1
View File
@@ -3,7 +3,6 @@ import {
authFile, authFile,
createMedicationViaAPI, createMedicationViaAPI,
deleteAllMedicationsViaAPI, deleteAllMedicationsViaAPI,
deleteMedicationViaAPI,
expect, expect,
navigateTo, navigateTo,
type TestMedication, type TestMedication,
-1
View File
@@ -2,7 +2,6 @@ import {
authFile, authFile,
createMedicationViaAPI, createMedicationViaAPI,
deleteAllMedicationsViaAPI, deleteAllMedicationsViaAPI,
deleteMedicationViaAPI,
expect, expect,
navigateTo, navigateTo,
type TestMedication, type TestMedication,
+1 -1
View File
@@ -160,7 +160,7 @@ test.describe("Share Schedule", () => {
// Should show the shared schedule page (not the login page) // Should show the shared schedule page (not the login page)
// Wait for either the schedule content or an error // Wait for either the schedule content or an error
const sharedContent = page.locator(".shared-schedule, .share-page"); const _sharedContent = page.locator(".shared-schedule, .share-page");
const dayBlock = page.locator(".day-block"); const dayBlock = page.locator(".day-block");
const medName = page.getByText(MED_ALICE); const medName = page.getByText(MED_ALICE);
+12 -2
View File
@@ -51,8 +51,18 @@ export default function AboutModal({ isOpen, onClose }: AboutModalProps) {
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
<div className="modal-overlay" onClick={onClose}> <div
<div className="modal-content about-modal" onClick={(e) => e.stopPropagation()}> 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 className="modal-close" onClick={onClose}>
× ×
</button> </button>
-1
View File
@@ -5,7 +5,6 @@ import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { useUnsavedChanges } from "../context"; import { useUnsavedChanges } from "../context";
import type { ThemePreference } from "../hooks";
import { useTheme } from "../hooks"; import { useTheme } from "../hooks";
import { useAuth } from "./Auth"; import { useAuth } from "./Auth";
+13 -2
View File
@@ -39,8 +39,19 @@ export function ConfirmModal({
}, [onCancel]); }, [onCancel]);
return ( return (
<div className={`modal-overlay${overlayClassName ? ` ${overlayClassName}` : ""}`} onClick={onCancel}> <div
<div className="modal-content" onClick={(e) => e.stopPropagation()} style={{ maxWidth: "450px" }}> 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 className="modal-close" onClick={onCancel}>
× ×
</button> </button>
+7 -1
View File
@@ -28,7 +28,13 @@ export function DateInput({ value, placeholder, className, ...rest }: DateInputP
}, []); }, []);
return ( 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"> <span className="date-input-display" aria-hidden="true">
{displayValue || placeholder || ""} {displayValue || placeholder || ""}
</span> </span>
+7 -1
View File
@@ -29,7 +29,13 @@ export function DateTimeInput({ value, placeholder, className, ...rest }: DateTi
}, []); }, []);
return ( 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"> <span className="date-input-display" aria-hidden="true">
{displayValue || placeholder || ""} {displayValue || placeholder || ""}
</span> </span>
+13 -2
View File
@@ -13,8 +13,19 @@ export default function ExportModal({ isOpen, onClose, onExport, exporting }: Ex
if (!isOpen) return null; if (!isOpen) return null;
return ( return (
<div className="modal-overlay" onClick={onClose}> <div
<div className="modal-content" onClick={(e) => e.stopPropagation()} style={{ maxWidth: "450px" }}> 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 className="modal-close" onClick={onClose}>
× ×
</button> </button>
+14 -2
View File
@@ -19,12 +19,24 @@ export function Lightbox({ src, alt, onClose }: LightboxProps) {
} }
return ( return (
<div className="lightbox-overlay" onClick={handleOverlayClick}> <div
className="lightbox-overlay"
onClick={handleOverlayClick}
onKeyDown={(e) => {
if (e.key === "Escape") onClose();
}}
>
<div className="lightbox-container"> <div className="lightbox-container">
<button className="lightbox-close" onClick={onClose}> <button className="lightbox-close" onClick={onClose}>
× ×
</button> </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>
</div> </div>
); );
+43 -9
View File
@@ -154,14 +154,24 @@ export function MedDetailModal({
const packageSize = getPackageSize(selectedMed); const packageSize = getPackageSize(selectedMed);
const currentStock = medCoverage ? Math.round(medCoverage.medsLeft) : getMedTotal(selectedMed); const currentStock = medCoverage ? Math.round(medCoverage.medsLeft) : getMedTotal(selectedMed);
const status = medCoverage ? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, settings) : null; const status = medCoverage ? getStockStatus(medCoverage.daysLeft, medCoverage.medsLeft, settings) : null;
const textClass = const fallbackTextClass = status?.className === "warning" ? "warning-text" : "success-text";
status?.className === "danger" ? "danger-text" : 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 stock = getBlisterStock(currentStock, selectedMed.pillsPerBlister, selectedMed.looseTablets, packageSize);
const fullForBounds = Math.max(0, parseStockInput(editStockFullInput)); const fullForBounds = Math.max(0, parseStockInput(editStockFullInput));
return ( return (
<div className="modal-overlay" onClick={onClose}> <div
<div className="modal-content med-detail-modal" onClick={(e) => e.stopPropagation()}> 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 className="modal-close" onClick={onClose}>
× ×
</button> </button>
@@ -172,6 +182,11 @@ export function MedDetailModal({
<div <div
className={`med-detail-avatar-wrapper ${selectedMed.imageUrl ? "clickable" : ""}`} className={`med-detail-avatar-wrapper ${selectedMed.imageUrl ? "clickable" : ""}`}
onClick={() => selectedMed.imageUrl && onOpenImageLightbox()} 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" /> <MedicationAvatar name={selectedMed.name} imageUrl={selectedMed.imageUrl} size="lg" />
{selectedMed.imageUrl && <span className="expand-icon">🔍</span>} {selectedMed.imageUrl && <span className="expand-icon">🔍</span>}
@@ -408,6 +423,9 @@ export function MedDetailModal({
<h3 <h3
className="section-header-clickable" className="section-header-clickable"
onClick={() => onRefillHistoryExpandedChange(!refillHistoryExpanded)} onClick={() => onRefillHistoryExpandedChange(!refillHistoryExpanded)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") onRefillHistoryExpandedChange(!refillHistoryExpanded);
}}
> >
{t("refill.history")} ({refillHistory.length}) {t("refill.history")} ({refillHistory.length})
<span className="expand-arrow">{refillHistoryExpanded ? "▼" : "▶"}</span> <span className="expand-arrow">{refillHistoryExpanded ? "▼" : "▶"}</span>
@@ -488,8 +506,16 @@ export function MedDetailModal({
e.stopPropagation(); e.stopPropagation();
onCloseRefillModal(); onCloseRefillModal();
}} }}
onKeyDown={(e) => {
e.stopPropagation();
if (e.key === "Escape") onCloseRefillModal();
}}
>
<div
className="modal-content refill-modal"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
> >
<div className="modal-content refill-modal" onClick={(e) => e.stopPropagation()}>
<button className="modal-close" onClick={onCloseRefillModal}> <button className="modal-close" onClick={onCloseRefillModal}>
× ×
</button> </button>
@@ -585,8 +611,16 @@ export function MedDetailModal({
e.stopPropagation(); e.stopPropagation();
onCloseEditStockModal(); onCloseEditStockModal();
}} }}
onKeyDown={(e) => {
e.stopPropagation();
if (e.key === "Escape") onCloseEditStockModal();
}}
>
<div
className="modal-content edit-stock-modal"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
> >
<div className="modal-content edit-stock-modal" onClick={(e) => e.stopPropagation()}>
<button className="modal-close" onClick={onCloseEditStockModal}> <button className="modal-close" onClick={onCloseEditStockModal}>
× ×
</button> </button>
@@ -602,6 +636,8 @@ export function MedDetailModal({
? editStockPartialBlisterPills ? editStockPartialBlisterPills
: editStockFullBlisters * selectedMed.pillsPerBlister + editStockPartialBlisterPills; : editStockFullBlisters * selectedMed.pillsPerBlister + editStockPartialBlisterPills;
const difference = newTotal - currentTotal; const difference = newTotal - currentTotal;
const negativeFallback = difference < 0 ? "negative" : "";
const differenceClass = difference > 0 ? "positive" : negativeFallback;
return ( return (
<> <>
@@ -691,9 +727,7 @@ export function MedDetailModal({
{newTotal} {newTotal === 1 ? t("common.pill") : t("common.pills")} {newTotal} {newTotal === 1 ? t("common.pill") : t("common.pills")}
</span> </span>
</div> </div>
<div <div className={`summary-row difference ${differenceClass}`}>
className={`summary-row difference ${difference > 0 ? "positive" : difference < 0 ? "negative" : ""}`}
>
<span>{t("editStock.difference")}:</span> <span>{t("editStock.difference")}:</span>
<span> <span>
{difference > 0 ? "+" : ""} {difference > 0 ? "+" : ""}
+18 -3
View File
@@ -137,13 +137,28 @@ export function MobileEditModal({
const currentMed = editingId ? meds.find((m) => m.id === editingId) : null; const currentMed = editingId ? meds.find((m) => m.id === editingId) : null;
return ( return (
<div className="modal-overlay" onClick={onClose}> <div
<div className="modal-content edit-modal" onClick={(e) => e.stopPropagation()}> 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"> <div className="edit-modal-header">
<button type="button" className="ghost small btn-nav" onClick={onClose}> <button type="button" className="ghost small btn-nav" onClick={onClose}>
{t("common.back")} {t("common.back")}
</button> </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> </div>
<form <form
className="form-grid mobile-edit-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; if (!isOpen) return null;
return ( return (
<div className="modal-overlay" onClick={onClose}> <div
<div className="modal-content profile-modal" onClick={(e) => e.stopPropagation()}> 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 className="modal-close" onClick={onClose}>
× ×
</button> </button>
+24 -6
View File
@@ -42,8 +42,18 @@ export function ShareDialog({
if (!show) return null; if (!show) return null;
return ( return (
<div className="modal-overlay" onClick={onClose}> <div
<div className="modal-content share-dialog-modal" onClick={(e) => e.stopPropagation()}> 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 className="modal-close" onClick={onClose}>
× ×
</button> </button>
@@ -53,11 +63,16 @@ export function ShareDialog({
<p className="share-dialog-description">{t("share.description")}</p> <p className="share-dialog-description">{t("share.description")}</p>
</div> </div>
{sharePeople.length === 0 ? ( {(() => {
if (sharePeople.length === 0) {
return (
<div className="share-dialog-empty"> <div className="share-dialog-empty">
<p>{t("share.noPeople")}</p> <p>{t("share.noPeople")}</p>
</div> </div>
) : shareLink ? ( );
}
if (shareLink) {
return (
<div className="share-dialog-result"> <div className="share-dialog-result">
<p className="share-success">{t("share.linkGenerated")}</p> <p className="share-success">{t("share.linkGenerated")}</p>
<div className="share-link-box"> <div className="share-link-box">
@@ -86,7 +101,9 @@ export function ShareDialog({
<button onClick={onClose}>{t("common.close")}</button> <button onClick={onClose}>{t("common.close")}</button>
</div> </div>
</div> </div>
) : ( );
}
return (
<div className="share-dialog-form"> <div className="share-dialog-form">
<div className="form-group"> <div className="form-group">
<label>{t("share.selectPerson")}</label> <label>{t("share.selectPerson")}</label>
@@ -117,7 +134,8 @@ export function ShareDialog({
</button> </button>
</div> </div>
</div> </div>
)} );
})()}
</div> </div>
</div> </div>
); );
+55 -10
View File
@@ -209,7 +209,7 @@ export function SharedSchedule() {
// Get dose ID - for per-intake takenBy, the ID already has the person suffix // 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 // 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 // The dose.id already includes the person suffix if there's a per-intake takenBy
return doseId; return doseId;
} }
@@ -479,7 +479,8 @@ export function SharedSchedule() {
const intake = intakes[blisterIdx]; const intake = intakes[blisterIdx];
const intakePerson = intake?.takenBy; 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 timeBasedConsumed = 0;
let lastAutoConsumedDateMs = 0; let lastAutoConsumedDateMs = 0;
@@ -579,7 +580,8 @@ export function SharedSchedule() {
const status = getStockStatus(coverage.daysLeft, coverage.medsLeft, stockThresholds); const status = getStockStatus(coverage.daysLeft, coverage.medsLeft, stockThresholds);
return status.className; 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 // Whether to show stock status indicators on the shared schedule
@@ -606,7 +608,7 @@ export function SharedSchedule() {
const missedPastDoseIds = useMemo(() => { const missedPastDoseIds = useMemo(() => {
const allPastDoseIds = pastDays.flatMap((d) => d.meds.flatMap((m) => m.doses.map((dose) => dose.id))); const allPastDoseIds = pastDays.flatMap((d) => d.meds.flatMap((m) => m.doses.map((dose) => dose.id)));
return allPastDoseIds.filter((id) => !isDoseIdDone(id)); return allPastDoseIds.filter((id) => !isDoseIdDone(id));
}, [pastDays, takenDoses, dismissedDoses, data]); }, [pastDays, isDoseIdDone]);
if (loading) { if (loading) {
return ( return (
@@ -714,14 +716,19 @@ export function SharedSchedule() {
</div> </div>
</div> </div>
</div> </div>
<p className="shared-schedule-period"> {(() => {
{t("share.period")}:{" "} const periodLabel =
{data.scheduleDays === 30 data.scheduleDays === 30
? t("dashboard.schedules.1month") ? t("dashboard.schedules.1month")
: data.scheduleDays === 90 : data.scheduleDays === 90
? t("dashboard.schedules.3months") ? t("dashboard.schedules.3months")
: t("dashboard.schedules.6months")} : t("dashboard.schedules.6months");
return (
<p className="shared-schedule-period">
{t("share.period")}: {periodLabel}
</p> </p>
);
})()}
</header> </header>
<div className="timeline"> <div className="timeline">
@@ -757,14 +764,18 @@ export function SharedSchedule() {
const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr); const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr);
const isCollapsed = !isManuallyExpanded; const isCollapsed = !isManuallyExpanded;
const pastMissedClass = allDoseIds.length > 0 ? "past-missed" : "";
return ( return (
<div <div
key={day.dateStr} 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 <div
className="day-divider clickable" className="day-divider clickable"
onClick={() => toggleDayCollapse(day.dateStr, true)} 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")} title={isCollapsed ? t("common.expand") : t("common.collapse")}
> >
<span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span> <span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span>
@@ -817,6 +828,11 @@ export function SharedSchedule() {
<div <div
className={med?.imageUrl ? "med-avatar clickable" : ""} className={med?.imageUrl ? "med-avatar clickable" : ""}
onClick={() => med?.imageUrl && openLightbox(med.imageUrl, med.name)} 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" /> <MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
</div> </div>
@@ -894,6 +910,9 @@ export function SharedSchedule() {
}, 50); }, 50);
} }
}} }}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") setShowPastDays(!showPastDays);
}}
> >
<span className="past-days-icon">{showPastDays ? "▼" : "▶"}</span> <span className="past-days-icon">{showPastDays ? "▼" : "▶"}</span>
<span className="past-days-label"> <span className="past-days-label">
@@ -941,6 +960,9 @@ export function SharedSchedule() {
<div <div
className="day-divider clickable" className="day-divider clickable"
onClick={() => toggleDayCollapse(day.dateStr, isAutoCollapsed)} 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")} title={isCollapsed ? t("common.expand") : t("common.collapse")}
> >
<span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span> <span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span>
@@ -982,6 +1004,11 @@ export function SharedSchedule() {
<div <div
className={med?.imageUrl ? "med-avatar clickable" : ""} className={med?.imageUrl ? "med-avatar clickable" : ""}
onClick={() => med?.imageUrl && openLightbox(med.imageUrl, med.name)} 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" /> <MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
</div> </div>
@@ -1058,6 +1085,9 @@ export function SharedSchedule() {
<div <div
className={`future-days-toggle ${showFutureDays ? "expanded" : ""}`} className={`future-days-toggle ${showFutureDays ? "expanded" : ""}`}
onClick={() => setShowFutureDays(!showFutureDays)} onClick={() => setShowFutureDays(!showFutureDays)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") setShowFutureDays(!showFutureDays);
}}
> >
<span className="future-days-icon">{showFutureDays ? "▼" : "▶"}</span> <span className="future-days-icon">{showFutureDays ? "▼" : "▶"}</span>
<span className="future-days-label"> <span className="future-days-label">
@@ -1099,6 +1129,9 @@ export function SharedSchedule() {
<div <div
className="day-divider clickable" className="day-divider clickable"
onClick={() => toggleDayCollapse(day.dateStr, isAutoCollapsed)} 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")} title={isCollapsed ? t("common.expand") : t("common.collapse")}
> >
<span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span> <span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span>
@@ -1139,6 +1172,11 @@ export function SharedSchedule() {
<div <div
className={med?.imageUrl ? "med-avatar clickable" : ""} className={med?.imageUrl ? "med-avatar clickable" : ""}
onClick={() => med?.imageUrl && openLightbox(med.imageUrl, med.name)} 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" /> <MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
</div> </div>
@@ -1215,7 +1253,13 @@ export function SharedSchedule() {
{/* Image Lightbox */} {/* Image Lightbox */}
{lightboxImage && ( {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 className="lightbox-close" onClick={closeLightbox}>
× ×
</button> </button>
@@ -1224,6 +1268,7 @@ export function SharedSchedule() {
alt={lightboxImage.name} alt={lightboxImage.name}
className="lightbox-image" className="lightbox-image"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
/> />
</div> </div>
)} )}
+18 -2
View File
@@ -36,8 +36,18 @@ export function UserFilterModal({
const userMeds = meds.filter((m) => !m.isObsolete && (m.takenBy || []).includes(selectedUser)); const userMeds = meds.filter((m) => !m.isObsolete && (m.takenBy || []).includes(selectedUser));
return ( return (
<div className="modal-overlay" onClick={onClose}> <div
<div className="modal-content user-meds-modal" onClick={(e) => e.stopPropagation()}> 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 className="modal-close" onClick={onClose}>
× ×
</button> </button>
@@ -75,6 +85,12 @@ export function UserFilterModal({
onClearUser(); onClearUser();
onOpenMedDetail(med); onOpenMedDetail(med);
}} }}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
onClearUser();
onOpenMedDetail(med);
}
}}
> >
<MedicationAvatar name={med.name} imageUrl={med.imageUrl} size="sm" /> <MedicationAvatar name={med.name} imageUrl={med.imageUrl} size="sm" />
<div className="user-med-info"> <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 type { Coverage, Medication, ScheduleEvent, StockThresholds } from "../types";
import { getSystemLocale } from "../utils/formatters"; import { getSystemLocale } from "../utils/formatters";
import { log } from "../utils/logger"; import { log } from "../utils/logger";
import { buildSchedulePreview, calculateCoverage, computeMissedPastDoseIds, isDoseDismissed } from "../utils/schedule"; import { buildSchedulePreview, calculateCoverage, computeMissedPastDoseIds } from "../utils/schedule";
// ============================================================================= // =============================================================================
// Types // Types
@@ -366,7 +366,8 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
// Normal/High stock // Normal/High stock
return "success"; 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] [coverageByMed, depletionByMed, settingsHook.settings.lowStockDays]
); );
@@ -536,7 +537,7 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
} }
setExporting(false); setExporting(false);
}, },
[t] [t, user?.username]
); );
// Handle file selection for import // 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 remainingRefills = Math.min(Math.max(0, med.prescriptionRemainingRefills ?? 0), authorizedRefills);
const lowRefillThreshold = Math.min(Math.max(0, med.prescriptionLowRefillThreshold ?? 1), 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 = { const editForm: FormState = {
name: med.name, name: med.name,
genericName: med.genericName ?? "", genericName: med.genericName ?? "",
@@ -223,11 +224,7 @@ export function useMedicationForm(): UseMedicationFormReturn {
packCount: String(med.packCount), packCount: String(med.packCount),
blistersPerPack: String(med.blistersPerPack), blistersPerPack: String(med.blistersPerPack),
pillsPerBlister: String(med.pillsPerBlister), pillsPerBlister: String(med.pillsPerBlister),
totalPills: med.totalPills totalPills: med.totalPills ? String(med.totalPills) : bottleTotalPills,
? String(med.totalPills)
: med.packageType === "bottle" && med.looseTablets
? String(med.looseTablets)
: "",
looseTablets: String(med.looseTablets), looseTablets: String(med.looseTablets),
pillWeightMg: med.pillWeightMg ? String(med.pillWeightMg) : "", pillWeightMg: med.pillWeightMg ? String(med.pillWeightMg) : "",
doseUnit: med.doseUnit ?? "mg", doseUnit: med.doseUnit ?? "mg",
+100 -6
View File
@@ -82,7 +82,7 @@ export function getReminderStatusData(
_allLowCoverage: Coverage[], _allLowCoverage: Coverage[],
allCoverage: Coverage[], allCoverage: Coverage[],
lastAutoEmailSent: string | null, lastAutoEmailSent: string | null,
lastNotificationType: string | null, _lastNotificationType: string | null,
_lastNotificationChannel: string | null, _lastNotificationChannel: string | null,
lastReminderMedName: string | null, lastReminderMedName: string | null,
lastReminderTakenBy: string | null, lastReminderTakenBy: string | null,
@@ -401,6 +401,11 @@ export function DashboardPage() {
<span <span
className={`med-link clickable ${textClass}`} className={`med-link clickable ${textClass}`}
onClick={() => medication && openMedDetail(medication)} onClick={() => medication && openMedDetail(medication)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (medication) openMedDetail(medication);
}
}}
> >
{med.name} {med.name}
</span> </span>
@@ -430,6 +435,11 @@ export function DashboardPage() {
<span <span
className={`med-link clickable ${textClass}`} className={`med-link clickable ${textClass}`}
onClick={() => medication && openMedDetail(medication)} onClick={() => medication && openMedDetail(medication)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
if (medication) openMedDetail(medication);
}
}}
> >
{med.name} {med.name}
</span> </span>
@@ -453,7 +463,13 @@ export function DashboardPage() {
<span key={name}> <span key={name}>
{idx > 0 && ", "} {idx > 0 && ", "}
{medication ? ( {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} {name}
</span> </span>
) : ( ) : (
@@ -475,7 +491,13 @@ export function DashboardPage() {
(() => { (() => {
const medication = meds.find((m) => m.name === reminderData.lastIntakeSent!.medName); const medication = meds.find((m) => m.name === reminderData.lastIntakeSent!.medName);
return medication ? ( 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} {reminderData.lastIntakeSent!.medName}
</span> </span>
) : ( ) : (
@@ -553,7 +575,15 @@ export function DashboardPage() {
return ( return (
<span key={c.name}> <span key={c.name}>
{idx > 0 && ", "} {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} {c.name}
</span> </span>
<span className={`reminder-days-left ${textClass}`}> <span className={`reminder-days-left ${textClass}`}>
@@ -603,7 +633,16 @@ export function DashboardPage() {
med ? getMedTotal(med) : Math.round(row.medsLeft) med ? getMedTotal(med) : Math.round(row.medsLeft)
); );
return ( 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 data-label={t("table.name")} className="cell-with-avatar">
<span className="med-name-line"> <span className="med-name-line">
<MedicationAvatar name={row.name} imageUrl={med?.imageUrl} /> <MedicationAvatar name={row.name} imageUrl={med?.imageUrl} />
@@ -629,6 +668,12 @@ export function DashboardPage() {
e.stopPropagation(); e.stopPropagation();
openUserFilter(person); openUserFilter(person);
}} }}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.stopPropagation();
openUserFilter(person);
}
}}
> >
{person} {person}
{med.intakes?.some((i) => i.takenBy === person && i.intakeRemindersEnabled) && " 🔔"} {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 isAutoCollapsed = true; // Past days are always auto-collapsed
const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr); const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr);
const isCollapsed = !isManuallyExpanded; const isCollapsed = !isManuallyExpanded;
const worstStatus = getDayStockStatus(day.meds); const _worstStatus = getDayStockStatus(day.meds);
return ( return (
<div <div
@@ -750,6 +795,9 @@ export function DashboardPage() {
<div <div
className="day-divider clickable" className="day-divider clickable"
onClick={() => toggleDayCollapse(day.dateStr, isAutoCollapsed)} 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")} title={isCollapsed ? t("common.expand") : t("common.collapse")}
> >
<span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span> <span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span>
@@ -791,6 +839,11 @@ export function DashboardPage() {
<div <div
className={med?.imageUrl ? "med-avatar clickable" : ""} className={med?.imageUrl ? "med-avatar clickable" : ""}
onClick={() => med?.imageUrl && openScheduleLightbox(`/api/images/${med.imageUrl}`)} 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" /> <MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
</div> </div>
@@ -833,6 +886,9 @@ export function DashboardPage() {
<span <span
className="person-name clickable" className="person-name clickable"
onClick={() => openUserFilter(person)} onClick={() => openUserFilter(person)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") openUserFilter(person);
}}
> >
{person} {person}
</span> </span>
@@ -889,6 +945,19 @@ export function DashboardPage() {
}, 50); }, 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-icon">{showPastDays ? "▼" : "▶"}</span>
<span className="past-days-label"> <span className="past-days-label">
@@ -963,6 +1032,9 @@ export function DashboardPage() {
<div <div
className="day-divider clickable" className="day-divider clickable"
onClick={() => toggleDayCollapse(day.dateStr, isAutoCollapsed)} 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")} title={isCollapsed ? t("common.expand") : t("common.collapse")}
> >
<span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span> <span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span>
@@ -998,6 +1070,11 @@ export function DashboardPage() {
<div <div
className={med?.imageUrl ? "med-avatar clickable" : ""} className={med?.imageUrl ? "med-avatar clickable" : ""}
onClick={() => med?.imageUrl && openScheduleLightbox(`/api/images/${med.imageUrl}`)} 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" /> <MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
</div> </div>
@@ -1044,6 +1121,9 @@ export function DashboardPage() {
<span <span
className="person-name clickable" className="person-name clickable"
onClick={() => openUserFilter(person)} onClick={() => openUserFilter(person)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") openUserFilter(person);
}}
> >
{person} {person}
</span> </span>
@@ -1096,6 +1176,9 @@ export function DashboardPage() {
<div <div
className={`future-days-toggle ${showFutureDays ? "expanded" : ""}`} className={`future-days-toggle ${showFutureDays ? "expanded" : ""}`}
onClick={() => setShowFutureDays(!showFutureDays)} onClick={() => setShowFutureDays(!showFutureDays)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") setShowFutureDays(!showFutureDays);
}}
> >
<span className="future-days-icon">{showFutureDays ? "▼" : "▶"}</span> <span className="future-days-icon">{showFutureDays ? "▼" : "▶"}</span>
<span className="future-days-label"> <span className="future-days-label">
@@ -1150,6 +1233,9 @@ export function DashboardPage() {
<div <div
className="day-divider clickable" className="day-divider clickable"
onClick={() => toggleDayCollapse(day.dateStr, isAutoCollapsed)} 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")} title={isCollapsed ? t("common.expand") : t("common.collapse")}
> >
<span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span> <span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span>
@@ -1185,6 +1271,11 @@ export function DashboardPage() {
<div <div
className={med?.imageUrl ? "med-avatar clickable" : ""} className={med?.imageUrl ? "med-avatar clickable" : ""}
onClick={() => med?.imageUrl && openScheduleLightbox(`/api/images/${med.imageUrl}`)} 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" /> <MedicationAvatar name={item.medName} imageUrl={med?.imageUrl} size="sm" />
</div> </div>
@@ -1227,6 +1318,9 @@ export function DashboardPage() {
<span <span
className="person-name clickable" className="person-name clickable"
onClick={() => openUserFilter(person)} onClick={() => openUserFilter(person)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") openUserFilter(person);
}}
> >
{person} {person}
</span> </span>
+11
View File
@@ -621,6 +621,11 @@ export function MedicationsPage() {
onClick={() => onClick={() =>
med.imageUrl && setLightboxImage({ src: `/api/images/${med.imageUrl}`, alt: med.name }) 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" /> <MedicationAvatar name={med.name} imageUrl={med.imageUrl} size="lg" />
</span> </span>
@@ -738,6 +743,12 @@ export function MedicationsPage() {
onClick={() => onClick={() =>
med.imageUrl && setLightboxImage({ src: `/api/images/${med.imageUrl}`, alt: med.name }) 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" /> <MedicationAvatar name={med.name} imageUrl={med.imageUrl} size="lg" />
</span> </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); meds.find((m) => m.id === row.medicationId) || meds.find((m) => m.name === row.medicationName);
const remainingRefills = med?.prescriptionEnabled ? (med.prescriptionRemainingRefills ?? 0) : null; const remainingRefills = med?.prescriptionEnabled ? (med.prescriptionRemainingRefills ?? 0) : null;
return ( 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"> <span data-label={t("planner.table.medication")} className="cell-with-avatar">
<MedicationAvatar name={row.medicationName} imageUrl={med?.imageUrl} /> <MedicationAvatar name={row.medicationName} imageUrl={med?.imageUrl} />
{row.medicationName} {row.medicationName}
+27 -2
View File
@@ -129,7 +129,7 @@ export function SchedulePage() {
const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr); const isManuallyExpanded = manuallyExpandedDays.has(day.dateStr);
const isCollapsed = !isManuallyExpanded; const isCollapsed = !isManuallyExpanded;
const worstStatus = getDayStockStatus(day.meds, coverageByMed, settings); const _worstStatus = getDayStockStatus(day.meds, coverageByMed, settings);
return ( return (
<div <div
@@ -139,6 +139,9 @@ export function SchedulePage() {
<div <div
className="day-divider clickable" className="day-divider clickable"
onClick={() => toggleDayCollapse(day.dateStr, true)} 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")} title={isCollapsed ? t("common.expand") : t("common.collapse")}
> >
<span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span> <span className="day-collapse-icon">{isCollapsed ? "▶" : "▼"}</span>
@@ -210,6 +213,9 @@ export function SchedulePage() {
<span <span
className="person-name clickable" className="person-name clickable"
onClick={() => openUserFilter(person)} onClick={() => openUserFilter(person)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") openUserFilter(person);
}}
> >
{person} {person}
</span> </span>
@@ -264,6 +270,19 @@ export function SchedulePage() {
}, 50); }, 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-icon">{showPastDays ? "▼" : "▶"}</span>
<span className="past-days-label"> <span className="past-days-label">
@@ -351,7 +370,13 @@ export function SchedulePage() {
className={`dose-person ${isTaken ? "taken" : ""} ${isOverdue ? "overdue" : ""}`} className={`dose-person ${isTaken ? "taken" : ""} ${isOverdue ? "overdue" : ""}`}
> >
{person && ( {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} {person}
</span> </span>
)} )}
+7
View File
@@ -664,6 +664,13 @@ body.modal-open {
border-radius: 8px; border-radius: 8px;
padding: 0.1rem 0.25rem; padding: 0.1rem 0.25rem;
margin: -0.1rem -0.25rem 0.8rem; 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 { .med-group-head-toggle:hover .med-group-title {
@@ -706,7 +706,7 @@ describe("MedDetailModal bottle package type", () => {
expect(screen.getByText(/refill\.pillsToAdd/i)).toBeInTheDocument(); expect(screen.getByText(/refill\.pillsToAdd/i)).toBeInTheDocument();
// Should NOT show packs label in refill // 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 // Packs label should not be present for bottle type
expect(screen.queryByText("refill.packs")).not.toBeInTheDocument(); expect(screen.queryByText("refill.packs")).not.toBeInTheDocument();
}); });
+2 -5
View File
@@ -1892,11 +1892,8 @@ function groupEventsIntoPastDays(
const medMap = dayMap.get(dateKey)!; const medMap = dayMap.get(dateKey)!;
if (!medMap.has(event.medName)) medMap.set(event.medName, []); if (!medMap.has(event.medName)) medMap.set(event.medName, []);
// Mirror AppContext normalization: string|null → string[] // Mirror AppContext normalization: string|null → string[]
const takenBy = Array.isArray(event.takenBy) const singleOrEmpty = typeof event.takenBy === "string" ? [event.takenBy] : [];
? event.takenBy const takenBy = Array.isArray(event.takenBy) ? event.takenBy : singleOrEmpty;
: typeof event.takenBy === "string"
? [event.takenBy]
: [];
medMap.get(event.medName)!.push({ id: event.id, takenBy }); 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 per-intake takenBy, only count for that person
// For legacy (no takenBy), count for all people in medication takenBy // 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 // Time-based: count doses where the scheduled time has already passed
let timeBasedConsumed = 0; let timeBasedConsumed = 0;