import { db } from "../db"; import { medFamilyGroups, medFamilyMembers, medMedications, medIntakeLogs, medCabinetItems, medCabinetStock, medMedicalRecords, medDailyCheckins, medBotBindings, organizations, users, type InsertMedFamilyGroup, type InsertMedFamilyMember, type InsertMedMedication, type InsertMedIntakeLog, type InsertMedCabinetItem, type InsertMedCabinetStock, type InsertMedMedicalRecord, type InsertMedDailyCheckin, type InsertMedBotBinding, } from "@shared/schema"; import { eq, and, gte, lte, asc, desc, inArray, sql, isNull } from "drizzle-orm"; import crypto from "crypto"; export class MedScheduleStorage { // ===================== // Organizations // ===================== async ensureMedScheduleOrganization(): Promise { const slug = "medschedule"; const [existing] = await db .select({ id: organizations.id }) .from(organizations) .where(eq(organizations.slug, slug)); if (existing) return existing.id; const [org] = await db .insert(organizations) .values({ name: "MedSchedule", slug, displayName: "MedSchedule", }) .returning(); return org.id; } // ===================== // Family groups // ===================== private generateInviteCode(): string { return crypto.randomBytes(4).toString("hex").toUpperCase(); } async createFamilyGroup(data: Omit) { const [group] = await db .insert(medFamilyGroups) .values({ ...data, inviteCode: this.generateInviteCode(), }) .returning(); return group; } async getFamilyGroupById(id: number, organizationId: number) { const [group] = await db .select() .from(medFamilyGroups) .where(and(eq(medFamilyGroups.id, id), eq(medFamilyGroups.organizationId, organizationId))); return group ?? null; } async getFamilyGroupByInviteCode(inviteCode: string, organizationId: number) { const [group] = await db .select() .from(medFamilyGroups) .where(and(eq(medFamilyGroups.inviteCode, inviteCode), eq(medFamilyGroups.organizationId, organizationId))); return group ?? null; } async getAllFamilyGroups(organizationId: number) { return db .select() .from(medFamilyGroups) .where(eq(medFamilyGroups.organizationId, organizationId)) .orderBy(desc(medFamilyGroups.createdAt)); } async getFamilyGroupsByUser(userId: number, organizationId: number) { const owned = await db .select() .from(medFamilyGroups) .where(and(eq(medFamilyGroups.createdBy, userId), eq(medFamilyGroups.organizationId, organizationId))); const memberRows = await db .select({ familyId: medFamilyMembers.familyId }) .from(medFamilyMembers) .where(eq(medFamilyMembers.userId, userId)); const memberFamilyIds = memberRows.map((m) => m.familyId).filter(Boolean); if (memberFamilyIds.length === 0) return owned; const joined = await db .select() .from(medFamilyGroups) .where(and(inArray(medFamilyGroups.id, memberFamilyIds), eq(medFamilyGroups.organizationId, organizationId))); const map = new Map(); for (const g of owned) map.set(g.id, g); for (const g of joined) map.set(g.id, g); return Array.from(map.values()); } async updateFamilyGroup(id: number, organizationId: number, updates: Partial) { const [group] = await db .update(medFamilyGroups) .set({ ...updates, updatedAt: new Date() }) .where(and(eq(medFamilyGroups.id, id), eq(medFamilyGroups.organizationId, organizationId))) .returning(); return group ?? null; } async deleteFamilyGroup(id: number, organizationId: number) { await db .delete(medFamilyGroups) .where(and(eq(medFamilyGroups.id, id), eq(medFamilyGroups.organizationId, organizationId))); } // ===================== // Family members // ===================== async createFamilyMember(data: InsertMedFamilyMember) { const [member] = await db.insert(medFamilyMembers).values(data).returning(); return member; } async getFamilyMembers(familyId: number, organizationId: number) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return []; return db .select() .from(medFamilyMembers) .where(eq(medFamilyMembers.familyId, familyId)) .orderBy(asc(medFamilyMembers.profileName)); } async getFamilyMemberById(id: number, familyId: number, organizationId: number) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return null; const [member] = await db .select() .from(medFamilyMembers) .where(and(eq(medFamilyMembers.id, id), eq(medFamilyMembers.familyId, familyId))); return member ?? null; } async updateFamilyMember(id: number, familyId: number, organizationId: number, updates: Partial) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return null; const [member] = await db .update(medFamilyMembers) .set({ ...updates, updatedAt: new Date() }) .where(and(eq(medFamilyMembers.id, id), eq(medFamilyMembers.familyId, familyId))) .returning(); return member ?? null; } async deleteFamilyMember(id: number, familyId: number, organizationId: number) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return; await db .delete(medFamilyMembers) .where(and(eq(medFamilyMembers.id, id), eq(medFamilyMembers.familyId, familyId))); } // ===================== // Medications // ===================== async createMedication(data: InsertMedMedication) { const [med] = await db.insert(medMedications).values(data).returning(); return med; } async getMedicationById(id: number, familyId: number, organizationId: number) { const [med] = await db .select() .from(medMedications) .where(and(eq(medMedications.id, id), eq(medMedications.familyId, familyId))) .innerJoin(medFamilyGroups, eq(medFamilyGroups.id, medMedications.familyId)); return med ?? null; } async getMedicationsByFamily(familyId: number, organizationId: number) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return []; return db .select() .from(medMedications) .where(eq(medMedications.familyId, familyId)) .orderBy(asc(medMedications.name)); } async getMedicationsByMember(memberId: number, familyId: number, organizationId: number) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return []; return db .select() .from(medMedications) .where(and(eq(medMedications.memberId, memberId), eq(medMedications.familyId, familyId))) .orderBy(asc(medMedications.name)); } async updateMedication(id: number, familyId: number, organizationId: number, updates: Partial) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return null; const [med] = await db .update(medMedications) .set({ ...updates, updatedAt: new Date() }) .where(and(eq(medMedications.id, id), eq(medMedications.familyId, familyId))) .returning(); return med ?? null; } async deleteMedication(id: number, familyId: number, organizationId: number) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return; await db .delete(medMedications) .where(and(eq(medMedications.id, id), eq(medMedications.familyId, familyId))); } // ===================== // Intake logs generation & queries // ===================== async generateIntakeLogs(medicationId: number, from: Date, to: Date) { const [med] = await db.select().from(medMedications).where(eq(medMedications.id, medicationId)); if (!med || !med.isActive) return []; const schedule = med.schedule as { type: "daily" | "weekly" | "interval" | "as_needed"; times: string[]; days?: number[]; intervalHours?: number; startDate?: string; endDate?: string; }; const start = new Date(schedule.startDate ?? med.createdAt); const end = schedule.endDate ? new Date(schedule.endDate) : to; const effectiveFrom = from > start ? from : start; const effectiveTo = to < end ? to : end; const slots: Date[] = []; const current = new Date(effectiveFrom); current.setHours(0, 0, 0, 0); while (current <= effectiveTo) { const dayOfWeek = current.getDay(); // 0..6 const matchesDay = schedule.type === "daily" || schedule.type === "as_needed" || (schedule.type === "weekly" && schedule.days?.includes(dayOfWeek)); if (matchesDay) { for (const time of schedule.times ?? []) { const [hours, minutes] = time.split(":").map(Number); const slot = new Date(current); slot.setHours(hours, minutes, 0, 0); if (slot >= effectiveFrom && slot <= effectiveTo) { slots.push(slot); } } } if (schedule.type === "interval" && schedule.intervalHours) { // interval handling: one base time per day + intervalHours const [hours, minutes] = (schedule.times[0] ?? "08:00").split(":").map(Number); let slot = new Date(current); slot.setHours(hours, minutes, 0, 0); while (slot <= effectiveTo && slot.getDate() === current.getDate()) { if (slot >= effectiveFrom) slots.push(new Date(slot)); slot = new Date(slot.getTime() + schedule.intervalHours * 60 * 60 * 1000); } } current.setDate(current.getDate() + 1); } if (slots.length === 0) return []; // Delete overlapping pending intakes in range to avoid duplicates await db .delete(medIntakeLogs) .where( and( eq(medIntakeLogs.medicationId, medicationId), eq(medIntakeLogs.status, "pending"), gte(medIntakeLogs.scheduledAt, effectiveFrom), lte(medIntakeLogs.scheduledAt, effectiveTo) ) ); const values: InsertMedIntakeLog[] = slots.map((slot) => ({ medicationId, memberId: med.memberId, scheduledAt: slot, status: "pending", })); return db.insert(medIntakeLogs).values(values).returning(); } async getPendingIntakes(organizationId: number, before: Date, after: Date = new Date(0)) { return db .select({ intake: medIntakeLogs, medication: medMedications, member: medFamilyMembers, family: medFamilyGroups, }) .from(medIntakeLogs) .innerJoin(medMedications, eq(medMedications.id, medIntakeLogs.medicationId)) .innerJoin(medFamilyMembers, eq(medFamilyMembers.id, medIntakeLogs.memberId)) .innerJoin(medFamilyGroups, eq(medFamilyGroups.id, medMedications.familyId)) .where( and( eq(medFamilyGroups.organizationId, organizationId), eq(medIntakeLogs.status, "pending"), gte(medIntakeLogs.scheduledAt, after), lte(medIntakeLogs.scheduledAt, before) ) ) .orderBy(asc(medIntakeLogs.scheduledAt)); } async getMissedIntakes(organizationId: number, minutesAgo: number) { const cutoff = new Date(Date.now() - minutesAgo * 60 * 1000); return db .select({ intake: medIntakeLogs, medication: medMedications, member: medFamilyMembers, family: medFamilyGroups, }) .from(medIntakeLogs) .innerJoin(medMedications, eq(medMedications.id, medIntakeLogs.medicationId)) .innerJoin(medFamilyMembers, eq(medFamilyMembers.id, medIntakeLogs.memberId)) .innerJoin(medFamilyGroups, eq(medFamilyGroups.id, medMedications.familyId)) .where( and( eq(medFamilyGroups.organizationId, organizationId), eq(medIntakeLogs.status, "pending"), lte(medIntakeLogs.scheduledAt, cutoff) ) ); } async getIntakesForDateRange(familyId: number, organizationId: number, from: Date, to: Date) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return []; return db .select({ intake: medIntakeLogs, medication: medMedications, member: medFamilyMembers, }) .from(medIntakeLogs) .innerJoin(medMedications, eq(medMedications.id, medIntakeLogs.medicationId)) .innerJoin(medFamilyMembers, eq(medFamilyMembers.id, medIntakeLogs.memberId)) .where( and( eq(medMedications.familyId, familyId), gte(medIntakeLogs.scheduledAt, from), lte(medIntakeLogs.scheduledAt, to) ) ) .orderBy(asc(medIntakeLogs.scheduledAt)); } async markIntakeTaken(intakeId: number, familyId: number, organizationId: number, note?: string) { const [intake] = await db .select({ intake: medIntakeLogs, medication: medMedications }) .from(medIntakeLogs) .innerJoin(medMedications, eq(medMedications.id, medIntakeLogs.medicationId)) .where(and(eq(medIntakeLogs.id, intakeId), eq(medMedications.familyId, familyId))) .innerJoin(medFamilyGroups, eq(medFamilyGroups.id, medMedications.familyId)); if (!intake || intake.medication.familyId !== familyId) return null; const [updated] = await db .update(medIntakeLogs) .set({ status: "taken", takenAt: new Date(), note: note ?? null }) .where(eq(medIntakeLogs.id, intakeId)) .returning(); return updated ?? null; } async markIntakeSkipped(intakeId: number, familyId: number, organizationId: number, note?: string) { const [intake] = await db .select({ intake: medIntakeLogs, medication: medMedications }) .from(medIntakeLogs) .innerJoin(medMedications, eq(medMedications.id, medIntakeLogs.medicationId)) .where(and(eq(medIntakeLogs.id, intakeId), eq(medMedications.familyId, familyId))) .innerJoin(medFamilyGroups, eq(medFamilyGroups.id, medMedications.familyId)); if (!intake) return null; const [updated] = await db .update(medIntakeLogs) .set({ status: "skipped", note: note ?? null }) .where(eq(medIntakeLogs.id, intakeId)) .returning(); return updated ?? null; } async snoozeIntake(intakeId: number, familyId: number, organizationId: number, minutes: number) { const [intake] = await db .select({ intake: medIntakeLogs, medication: medMedications }) .from(medIntakeLogs) .innerJoin(medMedications, eq(medMedications.id, medIntakeLogs.medicationId)) .where(and(eq(medIntakeLogs.id, intakeId), eq(medMedications.familyId, familyId))) .innerJoin(medFamilyGroups, eq(medFamilyGroups.id, medMedications.familyId)); if (!intake) return null; const newScheduled = new Date(Date.now() + minutes * 60 * 1000); const [updated] = await db .update(medIntakeLogs) .set({ scheduledAt: newScheduled, status: "snoozed" }) .where(eq(medIntakeLogs.id, intakeId)) .returning(); return updated ?? null; } // ===================== // Cabinet // ===================== async upsertCabinetItem( familyId: number, organizationId: number, data: Omit & { id?: number }, addedBy: number ) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return null; if (data.id) { const [item] = await db .update(medCabinetItems) .set({ ...data, familyId, addedBy, updatedAt: new Date() }) .where(and(eq(medCabinetItems.id, data.id), eq(medCabinetItems.familyId, familyId))) .returning(); return item ?? null; } const [item] = await db .insert(medCabinetItems) .values({ ...data, familyId, addedBy }) .returning(); return item; } async getCabinetItems(familyId: number, organizationId: number) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return []; return db .select() .from(medCabinetItems) .where(eq(medCabinetItems.familyId, familyId)) .orderBy(asc(medCabinetItems.name)); } async addCabinetStock( cabinetItemId: number, familyId: number, organizationId: number, change: number, reason: string, note: string | undefined, changedBy: number ) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return null; const [item] = await db .select() .from(medCabinetItems) .where(and(eq(medCabinetItems.id, cabinetItemId), eq(medCabinetItems.familyId, familyId))); if (!item) return null; await db.insert(medCabinetStock).values({ cabinetItemId, change, reason, note, changedBy, }); const [updated] = await db .update(medCabinetItems) .set({ quantity: (item.quantity ?? 0) + change, updatedAt: new Date() }) .where(eq(medCabinetItems.id, cabinetItemId)) .returning(); return updated ?? null; } async deleteCabinetItem(id: number, familyId: number, organizationId: number) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return; await db .delete(medCabinetItems) .where(and(eq(medCabinetItems.id, id), eq(medCabinetItems.familyId, familyId))); } // ===================== // Medical records // ===================== async createMedicalRecord(data: InsertMedMedicalRecord) { const [record] = await db.insert(medMedicalRecords).values(data).returning(); return record; } async getMedicalRecords(familyId: number, organizationId: number, memberId?: number) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return []; const conditions = [eq(medMedicalRecords.familyId, familyId)]; if (memberId) conditions.push(eq(medMedicalRecords.memberId, memberId)); return db .select() .from(medMedicalRecords) .where(and(...conditions)) .orderBy(desc(medMedicalRecords.recordDate)); } async updateMedicalRecord( id: number, familyId: number, organizationId: number, updates: Partial ) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return null; const [record] = await db .update(medMedicalRecords) .set({ ...updates, updatedAt: new Date() }) .where(and(eq(medMedicalRecords.id, id), eq(medMedicalRecords.familyId, familyId))) .returning(); return record ?? null; } async deleteMedicalRecord(id: number, familyId: number, organizationId: number) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return; await db .delete(medMedicalRecords) .where(and(eq(medMedicalRecords.id, id), eq(medMedicalRecords.familyId, familyId))); } // ===================== // Daily check-ins // ===================== async createDailyCheckin(data: InsertMedDailyCheckin) { const [checkin] = await db.insert(medDailyCheckins).values(data).returning(); return checkin; } async getPendingCheckins(familyId: number, organizationId: number) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return []; return db .select() .from(medDailyCheckins) .where(and(eq(medDailyCheckins.familyId, familyId), eq(medDailyCheckins.status, "pending"))) .orderBy(asc(medDailyCheckins.createdAt)); } async submitCheckinAnswer( id: number, familyId: number, organizationId: number, answerText: string, aiSummary?: string, aiAlert?: string ) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return null; const [checkin] = await db .update(medDailyCheckins) .set({ status: "answered", answerText, aiSummary: aiSummary ?? null, aiAlert: aiAlert ?? null, updatedAt: new Date(), }) .where(and(eq(medDailyCheckins.id, id), eq(medDailyCheckins.familyId, familyId))) .returning(); return checkin ?? null; } // ===================== // Bot bindings // ===================== async getOrCreateBotBinding(userId: number, organizationId: number) { const [existing] = await db .select() .from(medBotBindings) .where(and(eq(medBotBindings.userId, userId), eq(medBotBindings.organizationId, organizationId))); if (existing) return existing; const [binding] = await db .insert(medBotBindings) .values({ userId, organizationId }) .returning(); return binding; } async getBotBindingByTelegramChatId(organizationId: number, chatId: string) { const [binding] = await db .select() .from(medBotBindings) .where(and(eq(medBotBindings.organizationId, organizationId), eq(medBotBindings.telegramChatId, chatId))); return binding ?? null; } async getBotBindingByUserId(userId: number, organizationId: number) { const [binding] = await db .select() .from(medBotBindings) .where(and(eq(medBotBindings.userId, userId), eq(medBotBindings.organizationId, organizationId))); return binding ?? null; } async getFamilyCreator(familyId: number, organizationId: number) { const group = await this.getFamilyGroupById(familyId, organizationId); if (!group) return null; const [creator] = await db .select({ id: users.id, firstName: users.firstName, lastName: users.lastName, email: users.email }) .from(users) .where(eq(users.id, group.createdBy)); return creator ?? null; } async getBotBindingByMaxChatId(organizationId: number, chatId: string) { const [binding] = await db .select() .from(medBotBindings) .where(and(eq(medBotBindings.organizationId, organizationId), eq(medBotBindings.maxChatId, chatId))); return binding ?? null; } async bindTelegram(userId: number, organizationId: number, chatId: string, username?: string) { const binding = await this.getOrCreateBotBinding(userId, organizationId); const [updated] = await db .update(medBotBindings) .set({ telegramChatId: chatId, telegramUsername: username ?? null, updatedAt: new Date() }) .where(eq(medBotBindings.id, binding.id)) .returning(); return updated ?? binding; } async bindMax(userId: number, organizationId: number, chatId: string, username?: string) { const binding = await this.getOrCreateBotBinding(userId, organizationId); const [updated] = await db .update(medBotBindings) .set({ maxChatId: chatId, maxUsername: username ?? null, updatedAt: new Date() }) .where(eq(medBotBindings.id, binding.id)) .returning(); return updated ?? binding; } async getFamilyMembersWithBotBindings(familyId: number, organizationId: number) { const members = await this.getFamilyMembers(familyId, organizationId); if (members.length === 0) return []; const userIds = members.map((m) => m.userId).filter(Boolean) as number[]; if (userIds.length === 0) return members.map((m) => ({ ...m, bindings: null })); const bindings = await db .select() .from(medBotBindings) .where(and(eq(medBotBindings.organizationId, organizationId), inArray(medBotBindings.userId, userIds))); const bindingMap = new Map(bindings.map((b) => [b.userId, b])); return members.map((m) => ({ ...m, bindings: m.userId ? bindingMap.get(m.userId) ?? null : null })); } } export const medScheduleStorage = new MedScheduleStorage();