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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user