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
+3
View File
@@ -37,6 +37,7 @@ const inventorySchema = z.object({
pillsPerBlister: z.number().int().min(1).default(1),
looseTablets: z.number().int().min(0).default(0),
stockAdjustment: z.number().int().default(0), // Manual stock correction
packageType: z.enum(["blister", "bottle"]).default("blister"),
});
const medicationExportSchema = z.object({
@@ -276,6 +277,7 @@ export async function exportRoutes(app: FastifyInstance) {
pillsPerBlister: med.pillsPerBlister ?? 1,
looseTablets: med.looseTablets ?? 0,
stockAdjustment: med.stockAdjustment ?? 0,
packageType: med.packageType ?? "blister",
},
pillWeightMg: med.pillWeightMg,
doseUnit: med.doseUnit ?? "mg",
@@ -490,6 +492,7 @@ export async function exportRoutes(app: FastifyInstance) {
name: med.name,
genericName: med.genericName || null,
takenByJson,
packageType: med.inventory.packageType ?? "blister",
packCount: med.inventory.packCount,
blistersPerPack: med.inventory.blistersPerPack,
pillsPerBlister: med.inventory.pillsPerBlister,
+28 -12
View File
@@ -656,7 +656,13 @@ export async function medicationRoutes(app: FastifyInstance) {
const blistersPerPack = row.blistersPerPack ?? 1;
const looseTablets = row.looseTablets ?? 0;
const stockAdjustment = row.stockAdjustment ?? 0;
const originalTotalPills = packCount * blistersPerPack * pillsPerBlister + looseTablets + stockAdjustment;
const packageType = row.packageType ?? "blister";
// For bottle type, looseTablets IS the current stock (no blister math)
const originalTotalPills =
packageType === "bottle"
? looseTablets + stockAdjustment
: packCount * blistersPerPack * pillsPerBlister + looseTablets + stockAdjustment;
// Calculate consumption based on ACTUAL taken doses from dose_tracking
// This ensures Planner shows the same "current stock" as the Dashboard/Modal
@@ -735,18 +741,27 @@ export async function medicationRoutes(app: FastifyInstance) {
// Calculate AVAILABLE = stock AFTER the planned period (currentStock - usageTotal)
const availableAfterPeriod = Math.max(0, currentStock - usageTotal);
// Calculate stock breakdown for availableAfterPeriod
// Consumption order: loose pills first, then from blisters
const totalConsumedByEnd = originalTotalPills - availableAfterPeriod;
const looseConsumedByEnd = Math.min(totalConsumedByEnd, looseTablets);
const loosePillsRemaining = Math.max(0, looseTablets - looseConsumedByEnd);
const blisterPillsConsumed = totalConsumedByEnd - looseConsumedByEnd;
const originalBlisterPills = originalTotalPills - looseTablets;
const blisterPillsRemaining = Math.max(0, originalBlisterPills - blisterPillsConsumed);
let fullBlisters: number;
let loosePills: number;
const fullBlisters = pillsPerBlister > 0 ? Math.floor(blisterPillsRemaining / pillsPerBlister) : 0;
const openBlisterPills = pillsPerBlister > 0 ? blisterPillsRemaining % pillsPerBlister : 0;
const loosePills = loosePillsRemaining + openBlisterPills; // Combine open blister + remaining loose
if (packageType === "bottle") {
// Bottle type: no blisters, everything is loose pills
fullBlisters = 0;
loosePills = availableAfterPeriod;
} else {
// Blister type: calculate stock breakdown
// Consumption order: loose pills first, then from blisters
const totalConsumedByEnd = originalTotalPills - availableAfterPeriod;
const looseConsumedByEnd = Math.min(totalConsumedByEnd, looseTablets);
const loosePillsRemaining = Math.max(0, looseTablets - looseConsumedByEnd);
const blisterPillsConsumed = totalConsumedByEnd - looseConsumedByEnd;
const originalBlisterPills = originalTotalPills - looseTablets;
const blisterPillsRemaining = Math.max(0, originalBlisterPills - blisterPillsConsumed);
fullBlisters = pillsPerBlister > 0 ? Math.floor(blisterPillsRemaining / pillsPerBlister) : 0;
const openBlisterPills = pillsPerBlister > 0 ? blisterPillsRemaining % pillsPerBlister : 0;
loosePills = loosePillsRemaining + openBlisterPills; // Combine open blister + remaining loose
}
const enough = currentStock >= usageTotal;
return {
@@ -759,6 +774,7 @@ export async function medicationRoutes(app: FastifyInstance) {
fullBlisters,
loosePills,
enough,
packageType,
};
});
+11 -6
View File
@@ -78,9 +78,13 @@ export async function refillRoutes(app: FastifyInstance) {
})
.returning();
// Calculate pills added for response
const pillsPerPack = med.blistersPerPack * med.pillsPerBlister;
const totalPillsAdded = packsAdded * pillsPerPack + loosePillsAdded;
// Calculate pills added for response (packageType-aware)
const isBottle = (med.packageType ?? "blister") === "bottle";
const pillsPerPack = isBottle ? 0 : med.blistersPerPack * med.pillsPerBlister;
const totalPillsAdded = isBottle ? loosePillsAdded : packsAdded * pillsPerPack + loosePillsAdded;
const newTotalPills = isBottle
? newLooseTablets + (med.stockAdjustment ?? 0)
: newPackCount * pillsPerPack + newLooseTablets + (med.stockAdjustment ?? 0);
return {
success: true,
@@ -94,7 +98,7 @@ export async function refillRoutes(app: FastifyInstance) {
newStock: {
packCount: newPackCount,
looseTablets: newLooseTablets,
totalPills: newPackCount * pillsPerPack + newLooseTablets,
totalPills: newTotalPills,
},
};
});
@@ -120,13 +124,14 @@ export async function refillRoutes(app: FastifyInstance) {
.where(eq(refillHistory.medicationId, medId))
.orderBy(desc(refillHistory.refillDate));
const pillsPerPack = med.blistersPerPack * med.pillsPerBlister;
const isBottle = (med.packageType ?? "blister") === "bottle";
const pillsPerPack = isBottle ? 0 : med.blistersPerPack * med.pillsPerBlister;
return refills.map((r) => ({
id: r.id,
packsAdded: r.packsAdded,
loosePillsAdded: r.loosePillsAdded,
totalPillsAdded: r.packsAdded * pillsPerPack + r.loosePillsAdded,
totalPillsAdded: isBottle ? r.loosePillsAdded : r.packsAdded * pillsPerPack + r.loosePillsAdded,
refillDate: r.refillDate,
}));
});
+4 -1
View File
@@ -114,7 +114,9 @@ export async function shareRoutes(app: FastifyInstance) {
const takenByArray = parseTakenByJson(med.takenByJson);
const totalPills =
med.packCount * med.blistersPerPack * med.pillsPerBlister + med.looseTablets + (med.stockAdjustment ?? 0);
(med.packageType ?? "blister") === "bottle"
? med.looseTablets + (med.stockAdjustment ?? 0)
: med.packCount * med.blistersPerPack * med.pillsPerBlister + med.looseTablets + (med.stockAdjustment ?? 0);
return {
id: med.id,
name: med.name,
@@ -123,6 +125,7 @@ export async function shareRoutes(app: FastifyInstance) {
doseUnit: med.doseUnit ?? "mg",
imageUrl: med.imageUrl,
totalPills,
packageType: med.packageType ?? "blister",
packCount: med.packCount,
blistersPerPack: med.blistersPerPack,
looseTablets: med.looseTablets,