import type { doseTracking, medications } from "../db/schema.js"; import { isAmountBasedPackageType } from "../utils/package-profiles.js"; import { getTodayInTimezone, type Intake, normalizeIntakeUsageForStock, parseIntakesJson, parseLocalDateTime, } from "../utils/scheduler-utils.js"; const MS_PER_DAY = 86_400_000; const doseIdPattern = /^(\d+)-(\d+)-(\d+)(?:-(.+))?$/; type MedicationRow = typeof medications.$inferSelect; type DoseRow = typeof doseTracking.$inferSelect; export type SharedMedicationOverviewItem = { name: string; genericName: string | null; imageUrl: string | null; packageType: string; packCount: number; blistersPerPack: number; pillsPerBlister: number; totalPills: number | null; looseTablets: number; currentStock: number | null; capacity: number | null; daysLeft: number | null; nextIntakeDate: string | null; depletionDate: string | null; priority: "normal" | "high" | "out-of-stock" | null; expiryDate: string | null; medicationStartDate: string | null; prescriptionEnabled: boolean; prescriptionRemainingRefills: number | null; }; function toDateOnlyString(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; } function parseDateOnly(dateOnly: string): Date { const [year, month, day] = dateOnly.split("-").map((value) => Number.parseInt(value, 10)); return new Date(year, month - 1, day, 0, 0, 0, 0); } function computeCapacity(medication: MedicationRow): number { if (isAmountBasedPackageType(medication.packageType)) { return medication.totalPills ?? medication.looseTablets; } return medication.packCount * medication.blistersPerPack * medication.pillsPerBlister; } function computeDailyDoseRate(intakes: Intake[], medication: MedicationRow): number { return intakes.reduce((sum, intake) => { if (intake.every <= 0) return sum; const normalizedUsage = normalizeIntakeUsageForStock(intake, medication.medicationForm, medication.packageType); return sum + normalizedUsage / intake.every; }, 0); } function computeNextIntakeDate(intakes: Intake[], todayDateOnly: string): string | null { const today = parseDateOnly(todayDateOnly); let nextDate: Date | null = null; for (const intake of intakes) { if (intake.every <= 0) continue; const startDate = parseLocalDateTime(intake.start); const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate(), 0, 0, 0, 0); let candidate = startDateOnly; if (candidate.getTime() < today.getTime()) { const elapsedDays = Math.floor((today.getTime() - candidate.getTime()) / MS_PER_DAY); const intervals = Math.ceil(elapsedDays / intake.every); candidate = new Date(candidate.getTime() + intervals * intake.every * MS_PER_DAY); } if (!nextDate || candidate.getTime() < nextDate.getTime()) { nextDate = candidate; } } return nextDate ? toDateOnlyString(nextDate) : null; } function computeTakenAmount( medication: MedicationRow, intakes: Intake[], dosesByMedication: Map ): number { const doseRows = dosesByMedication.get(medication.id) ?? []; if (doseRows.length === 0) return 0; const correctionDateOnlyMs = medication.lastStockCorrectionAt ? new Date( medication.lastStockCorrectionAt.getFullYear(), medication.lastStockCorrectionAt.getMonth(), medication.lastStockCorrectionAt.getDate(), 0, 0, 0, 0 ).getTime() : 0; let takenAmount = 0; for (const dose of doseRows) { if (dose.dismissed) continue; const match = doseIdPattern.exec(dose.doseId); if (!match) continue; const intakeIndex = Number.parseInt(match[2], 10); const doseDateOnlyMs = Number.parseInt(match[3], 10); if (Number.isNaN(intakeIndex) || Number.isNaN(doseDateOnlyMs)) continue; if (doseDateOnlyMs < correctionDateOnlyMs) continue; const intake = intakes[intakeIndex]; if (!intake) continue; takenAmount += normalizeIntakeUsageForStock(intake, medication.medicationForm, medication.packageType); } return takenAmount; } function toNullableDate(value: string | null): string | null { if (!value) return null; return value.trim() ? value : null; } function computeOverviewPriority( currentStock: number, daysLeft: number | null, thresholdDays: number ): "normal" | "high" | "out-of-stock" { if (currentStock <= 0 || daysLeft === 0) return "out-of-stock"; if (daysLeft !== null && daysLeft <= thresholdDays) return "high"; return "normal"; } export function buildSharedMedicationOverview(options: { medications: MedicationRow[]; doses: DoseRow[]; thresholdDays: number; }): SharedMedicationOverviewItem[] { const { medications: medicationRows, doses, thresholdDays } = options; const dosesByMedication = new Map(); for (const dose of doses) { const match = doseIdPattern.exec(dose.doseId); if (!match) continue; const medicationId = Number.parseInt(match[1], 10); if (Number.isNaN(medicationId)) continue; const existing = dosesByMedication.get(medicationId) ?? []; existing.push(dose); dosesByMedication.set(medicationId, existing); } const todayDateOnly = getTodayInTimezone(); const todayDate = parseDateOnly(todayDateOnly); return medicationRows.map((medication) => { const intakes = parseIntakesJson( medication.intakesJson, { usageJson: medication.usageJson, everyJson: medication.everyJson, startJson: medication.startJson, }, medication.intakeRemindersEnabled ?? false ); const capacity = computeCapacity(medication); const dailyDoseRate = computeDailyDoseRate(intakes, medication); const takenAmount = computeTakenAmount(medication, intakes, dosesByMedication); const rawCurrentStock = capacity + (medication.stockAdjustment ?? 0) - takenAmount; const currentStock = Math.max(0, Math.floor(rawCurrentStock)); const daysLeft = dailyDoseRate > 0 ? Math.floor(currentStock / dailyDoseRate) : null; const depletionDate = daysLeft === null ? null : toDateOnlyString(new Date(todayDate.getTime() + daysLeft * MS_PER_DAY)); const priority = computeOverviewPriority(currentStock, daysLeft, thresholdDays); return { name: medication.name, genericName: medication.genericName, imageUrl: medication.imageUrl, packageType: medication.packageType, packCount: medication.packCount, blistersPerPack: medication.blistersPerPack, pillsPerBlister: medication.pillsPerBlister, totalPills: medication.totalPills, looseTablets: medication.looseTablets, currentStock, capacity, daysLeft, nextIntakeDate: computeNextIntakeDate(intakes, todayDateOnly), depletionDate, priority, expiryDate: toNullableDate(medication.expiryDate), medicationStartDate: toNullableDate(medication.medicationStartDate), prescriptionEnabled: medication.prescriptionEnabled ?? false, prescriptionRemainingRefills: medication.prescriptionRemainingRefills, }; }); }