/// import { type Cell, cell, Default, handler, lift, recipe, str, } from "commontools"; interface MedicationDoseSeed { id?: string; name?: string; dosage?: string; scheduledTime?: string; instructions?: string; } interface MedicationDose { id: string; medication: string; dosage: string; scheduledTime: string; instructions: string; } interface DoseRecord { id: string; medication: string; scheduledTime: string; takenAt: string; } interface AdherenceSnapshot { total: number; taken: number; pending: number; percentage: number; } interface MedicationAdherenceArgs { doses: Default; } interface MarkDoseEvent { doseId?: string; takenAt?: string; } const timePattern = /^\d{2}:\d{2}$/; const toHoursMinutes = (value: Date): string => { const hours = value.getUTCHours().toString().padStart(2, "0"); const minutes = value.getUTCMinutes().toString().padStart(2, "0"); return `${hours}:${minutes}`; }; const sanitizeTime = (value: unknown, fallback: string): string => { if (typeof value === "string") { const trimmed = value.trim(); if (timePattern.test(trimmed)) { return trimmed; } const attempt = new Date(trimmed); if (!Number.isNaN(attempt.getTime())) { return toHoursMinutes(attempt); } } return fallback; }; const sanitizeDoseId = (value: unknown, fallback: string): string => { if (typeof value === "string") { const trimmed = value.trim(); if (trimmed.length > 0) { return trimmed; } } return fallback; }; const sanitizeMedicationName = ( value: unknown, fallbackIndex: number, ): string => { if (typeof value === "string") { const trimmed = value.trim(); if (trimmed.length > 0) { return trimmed; } } return `Medication ${fallbackIndex + 1}`; }; const sanitizeDosage = (value: unknown): string => { if (typeof value === "string") { const trimmed = value.trim(); if (trimmed.length > 0) { return trimmed; } } return "Standard dosage"; }; const sanitizeInstructions = (value: unknown): string => { if (typeof value === "string") { const trimmed = value.trim(); if (trimmed.length > 0) { return trimmed; } } return "Take with water"; }; const compareTimes = (left: string, right: string): number => { if (left === right) return 0; const [leftHours, leftMinutes] = left.split(":", 2); const [rightHours, rightMinutes] = right.split(":", 2); const leftValue = Number.parseInt(leftHours, 10) * 60 + Number.parseInt(leftMinutes, 10); const rightValue = Number.parseInt(rightHours, 10) * 60 + Number.parseInt(rightMinutes, 10); return leftValue - rightValue; }; const toMedicationDose = ( seed: MedicationDoseSeed | undefined, index: number, usedIds: Set, ): MedicationDose => { const baseId = sanitizeDoseId(seed?.id, `dose-${index + 1}`); let id = baseId; let disambiguator = 1; while (usedIds.has(id)) { disambiguator += 1; id = `${baseId}-${disambiguator}`; } usedIds.add(id); const medication = sanitizeMedicationName(seed?.name, index); const dosage = sanitizeDosage(seed?.dosage); const scheduledTime = sanitizeTime(seed?.scheduledTime, "08:00"); const instructions = sanitizeInstructions(seed?.instructions); return { id, medication, dosage, scheduledTime, instructions }; }; const sanitizeSchedule = ( entries: readonly MedicationDoseSeed[] | undefined, ): MedicationDose[] => { if (!Array.isArray(entries)) { return []; } const usedIds = new Set(); const sanitized = entries.map((entry, index) => toMedicationDose(entry, index, usedIds) ); sanitized.sort((left, right) => compareTimes(left.scheduledTime, right.scheduledTime) ); return sanitized; }; const computeAdherenceSnapshot = ( input: { schedule: MedicationDose[]; records: DoseRecord[] }, ): AdherenceSnapshot => { const total = input.schedule.length; const taken = Math.min(input.records.length, total); const pending = Math.max(total - taken, 0); const percentage = total === 0 ? 100 : Math.round((taken / total) * 10000) / 100; return { total, taken, pending, percentage }; }; const computeUpcoming = ( input: { schedule: MedicationDose[]; records: DoseRecord[] }, ): MedicationDose[] => { if (input.schedule.length === 0) { return []; } const takenIds = new Set(); for (const record of input.records) { takenIds.add(record.id); } return input.schedule.filter((dose) => !takenIds.has(dose.id)); }; const markDoseTaken = handler( ( event: MarkDoseEvent | undefined, context: { taken: Cell; history: Cell; schedule: Cell; }, ) => { const schedule = context.schedule.get() ?? []; const id = sanitizeDoseId(event?.doseId, ""); if (id.length === 0) return; const dose = schedule.find((entry) => entry.id === id); if (!dose) return; const existing = context.taken.get(); const log = context.history.get(); const takenRecords = Array.isArray(existing) ? existing : []; if (takenRecords.some((record) => record.id === id)) { return; } const takenAt = sanitizeTime(event?.takenAt, dose.scheduledTime); const nextRecords = [...takenRecords, { id: dose.id, medication: dose.medication, scheduledTime: dose.scheduledTime, takenAt, }]; nextRecords.sort((left, right) => compareTimes(left.scheduledTime, right.scheduledTime) ); context.taken.set(nextRecords); const message = `Took ${dose.medication} scheduled for ${dose.scheduledTime} at ${takenAt}`; const historyEntries = Array.isArray(log) ? log : []; context.history.set([...historyEntries, message]); }, ); const resetAdherence = handler( ( _event: unknown, context: { taken: Cell; history: Cell }, ) => { context.taken.set([]); context.history.set([]); }, ); export const medicationAdherencePattern = recipe( "Medication Adherence Pattern", ({ doses }) => { const schedule = lift(sanitizeSchedule)(doses); const takenRecords = cell([]); const history = cell([]); const adherence = lift((input: { schedule: MedicationDose[]; records: DoseRecord[]; }) => computeAdherenceSnapshot(input))({ schedule, records: takenRecords, }); const adherencePercentage = lift((snapshot: AdherenceSnapshot) => snapshot.percentage )(adherence); const takenCount = lift((snapshot: AdherenceSnapshot) => snapshot.taken)( adherence, ); const totalCount = lift((snapshot: AdherenceSnapshot) => snapshot.total)( adherence, ); const remainingCount = lift((snapshot: AdherenceSnapshot) => snapshot.pending )(adherence); const percentageLabel = str`${adherencePercentage}% adherence`; const adherenceLabel = str`${takenCount} of ${totalCount} doses taken`; const remainingLabel = lift((count: number) => `${count} dose${count === 1 ? "" : "s"} remaining` )(remainingCount); const upcomingDoses = lift((input: { schedule: MedicationDose[]; records: DoseRecord[]; }) => computeUpcoming(input))({ schedule, records: takenRecords, }); return { schedule, takenRecords, history, stats: adherence, adherencePercentage, percentageLabel, adherenceLabel, remainingLabel, upcomingDoses, markDose: markDoseTaken({ taken: takenRecords, history, schedule, }), reset: resetAdherence({ taken: takenRecords, history }), }; }, ); export type { AdherenceSnapshot, DoseRecord, MedicationDose };