feat: theme dropdown with system preference and comprehensive bottle-type fixes (#138)

- Replace dark/light toggle with Light/Dark/System dropdown menu
- System theme follows OS prefers-color-scheme setting
- Apply theme dropdown to shared schedule page
- Fix 7 packageType (bottle) bugs across stock calc, share, refills, export/import
- Fix planner bottle-type stock calculation and display
- Fix dailyRate double-counting with per-intake takenBy
- Fix About modal update check stale caching
- Fix intake reminder past-intake seeding and push title
- Fix phantom DB path in drizzle.config.ts
- Fix mobile dose field visibility
- Make medication name clickable in dashboard reminder bar
- Improve planner checkbox UX with inline tooltip
- Add 20+ new tests covering all fixes
This commit is contained in:
Daniel Volz
2026-02-08 20:32:40 +01:00
committed by GitHub
parent b19bcf02c2
commit 8c5deed4c2
29 changed files with 1053 additions and 166 deletions
+120
View File
@@ -289,6 +289,113 @@ describe("calculateCoverage", () => {
expect(result.all[0].daysLeft).toBeNull();
});
it("uses intakes format when available instead of blisters", () => {
// The new intakes format should be used for coverage calculation
// when med.intakes is present, falling through getBlistersForMed
const meds: Medication[] = [
{
id: 1,
name: "IntakesMed",
packCount: 1,
blistersPerPack: 1,
pillsPerBlister: 30,
looseTablets: 0,
takenBy: [],
blisters: [], // Empty blisters — intakes should be used instead
intakes: [
{
usage: 2,
every: 1,
start: "2024-03-10T09:00:00",
takenBy: null,
intakeRemindersEnabled: false,
},
],
updatedAt: null,
},
];
const result = calculateCoverage(meds, [], "en", 7, "automatic", new Set());
expect(result.all).toHaveLength(1);
// 30 pills, 2 per day consumed. March 10 09:00 to March 15 12:00 = 6 occurrences × 2 = 12 consumed
expect(result.all[0].medsLeft).toBe(18);
expect(result.all[0].daysLeft).toBe(9); // 18 pills / 2 per day = 9 days
});
it("per-intake takenBy counts person correctly in automatic mode", () => {
// When intakes have per-intake takenBy, each person-intake pair is counted
const meds: Medication[] = [
{
id: 1,
name: "PersonMed",
packCount: 1,
blistersPerPack: 1,
pillsPerBlister: 60,
looseTablets: 0,
takenBy: ["Alice", "Bob"],
blisters: [],
intakes: [
{
usage: 1,
every: 1,
start: "2024-03-10T09:00:00",
takenBy: "Alice",
intakeRemindersEnabled: false,
},
{
usage: 1,
every: 1,
start: "2024-03-10T09:00:00",
takenBy: "Bob",
intakeRemindersEnabled: false,
},
],
updatedAt: null,
},
];
const result = calculateCoverage(meds, [], "en", 7, "automatic", new Set());
expect(result.all).toHaveLength(1);
// 2 intakes × 1 pill/day × 6 occurrences = 12 consumed
// dailyRate = 2 (1/day × 2 people)
// medsLeft = 60 - 12 = 48, daysLeft = 48 / 2 = 24
expect(result.all[0].medsLeft).toBe(48);
expect(result.all[0].daysLeft).toBe(24);
});
it("automatic mode without stock correction counts from blister start", () => {
// Without stock correction, effectiveStart should be the blisterStart itself.
// This tests the `else` branch where effectiveStart = blisterStart.
const meds: Medication[] = [
{
id: 1,
name: "TestMed",
packCount: 1,
blistersPerPack: 1,
pillsPerBlister: 30,
looseTablets: 0,
takenBy: [],
blisters: [
{
usage: 1,
every: 1,
start: "2024-03-13T09:00:00", // 2 days ago + today = 3 occurrences
},
],
updatedAt: null,
// No lastStockCorrectionAt
},
];
const result = calculateCoverage(meds, [], "en", 7, "automatic", new Set());
expect(result.all).toHaveLength(1);
// March 13, 14, 15 at 09:00 — all past (it's 12:00 on March 15) = 3 consumed
expect(result.all[0].medsLeft).toBe(27);
});
it("filters low stock medications", () => {
const meds: Medication[] = [
{
@@ -1046,6 +1153,19 @@ describe("getStockStatus", () => {
expect(result.level).toBe("normal");
expect(result.label).toBe("status.noSchedule");
});
it("returns critical when daysLeft is at or below criticalStockDays", () => {
const thresholdsWithCritical: StockThresholds = {
lowStockDays: 30,
criticalStockDays: 7,
normalStockDays: 90,
highStockDays: 180,
};
const result = getStockStatus(5, 10, thresholdsWithCritical);
expect(result.level).toBe("critical");
expect(result.className).toBe("danger");
});
});
describe("getNextReminderForMed", () => {