import type { doseTracking, medications } from "../db/schema.js"; import { isAmountBasedPackageType } from "../utils/package-profiles.js"; import { countScheduledOccurrencesInRange, getDateOnlyTimestamp, getNextScheduledOccurrenceTime, normalizeIntakeUsageForStock, parseIntakesJson, parseLocalDateTime, parseTakenByJson, } from "../utils/scheduler-utils.js"; type MedicationRow = typeof medications.$inferSelect; type DoseRow = typeof doseTracking.$inferSelect; const doseIdPattern = /^(\d+)-(\d+)-(\d+)(?:-(.+))?$/; function getDoseTakenAtMs(dose: DoseRow): number { const rawTakenAt = Number(dose.takenAt); if (Number.isFinite(rawTakenAt)) { return rawTakenAt < 1_000_000_000_000 ? rawTakenAt * 1000 : rawTakenAt; } return new Date(dose.takenAt).getTime(); } export function computeMedicationCurrentStock(options: { medication: MedicationRow; doses: DoseRow[]; stockCalculationMode: "automatic" | "manual"; nowMs?: number; }): number { const { medication, doses, stockCalculationMode, nowMs = Date.now() } = options; const intakes = parseIntakesJson( medication.intakesJson, { usageJson: medication.usageJson, everyJson: medication.everyJson, startJson: medication.startJson, }, medication.intakeRemindersEnabled ?? false ); const baseStock = isAmountBasedPackageType(medication.packageType) ? medication.looseTablets + (medication.stockAdjustment ?? 0) : medication.packCount * medication.blistersPerPack * medication.pillsPerBlister + medication.looseTablets + (medication.stockAdjustment ?? 0); const relevantDoses = doses.filter((dose) => !dose.dismissed); const stockCorrectionCutoff = medication.lastStockCorrectionAt ? new Date(medication.lastStockCorrectionAt).getTime() : 0; let consumed = 0; if (stockCalculationMode === "automatic") { const medicationTakenBy = parseTakenByJson(medication.takenByJson); intakes.forEach((intake, intakeIndex) => { const usage = normalizeIntakeUsageForStock(intake, medication.medicationForm, medication.packageType); const intakeStart = parseLocalDateTime(intake.start).getTime(); if (Number.isNaN(intakeStart)) return; const effectiveStart = stockCorrectionCutoff > 0 && stockCorrectionCutoff >= intakeStart ? getNextScheduledOccurrenceTime(intake, stockCorrectionCutoff, false) : intakeStart; if (effectiveStart === null) return; let peopleForThisIntake: Array; if (intake.takenBy) { peopleForThisIntake = [intake.takenBy]; } else if (medicationTakenBy.length > 0) { peopleForThisIntake = medicationTakenBy; } else { peopleForThisIntake = [null]; } let lastAutoConsumedDateMs = 0; if (effectiveStart <= nowMs) { const { count: occurrences, lastOccurrenceMs } = countScheduledOccurrencesInRange( intake, effectiveStart, nowMs ); consumed += occurrences * usage * peopleForThisIntake.length; if (lastOccurrenceMs !== null) { lastAutoConsumedDateMs = getDateOnlyTimestamp(new Date(lastOccurrenceMs)); } } const stockCorrectionDateOnly = stockCorrectionCutoff > 0 ? getDateOnlyTimestamp(new Date(stockCorrectionCutoff)) : 0; const earlyCutoff = Math.max(lastAutoConsumedDateMs, stockCorrectionDateOnly); for (const dose of relevantDoses) { const match = doseIdPattern.exec(dose.doseId); if (!match) continue; const parsedMedicationId = Number.parseInt(match[1], 10); const parsedIntakeIndex = Number.parseInt(match[2], 10); const doseDateOnlyMs = Number.parseInt(match[3], 10); if ( Number.isNaN(parsedMedicationId) || Number.isNaN(parsedIntakeIndex) || Number.isNaN(doseDateOnlyMs) || parsedMedicationId !== medication.id || parsedIntakeIndex !== intakeIndex ) { continue; } if (doseDateOnlyMs > earlyCutoff) { consumed += usage; } } }); } else { intakes.forEach((intake, intakeIndex) => { const usage = normalizeIntakeUsageForStock(intake, medication.medicationForm, medication.packageType); const intakeStart = parseLocalDateTime(intake.start); const intakeStartDateOnly = new Date( intakeStart.getFullYear(), intakeStart.getMonth(), intakeStart.getDate() ).getTime(); if (Number.isNaN(intakeStartDateOnly)) return; for (const dose of relevantDoses) { const match = doseIdPattern.exec(dose.doseId); if (!match) continue; const parsedMedicationId = Number.parseInt(match[1], 10); const parsedIntakeIndex = Number.parseInt(match[2], 10); const doseDateOnlyMs = Number.parseInt(match[3], 10); if ( Number.isNaN(parsedMedicationId) || Number.isNaN(parsedIntakeIndex) || Number.isNaN(doseDateOnlyMs) || parsedMedicationId !== medication.id || parsedIntakeIndex !== intakeIndex ) { continue; } const takenAtMs = getDoseTakenAtMs(dose); const afterCorrectionOrNoCorrection = stockCorrectionCutoff === 0 || takenAtMs > stockCorrectionCutoff; if (doseDateOnlyMs >= intakeStartDateOnly && afterCorrectionOrNoCorrection) { consumed += usage; } } }); } return Math.max(0, Math.floor(baseStock - consumed)); }