697 lines
23 KiB
TypeScript
697 lines
23 KiB
TypeScript
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<number> {
|
|
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<InsertMedFamilyGroup, "inviteCode">) {
|
|
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<number, typeof owned[number]>();
|
|
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<InsertMedFamilyGroup>) {
|
|
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<InsertMedFamilyMember>) {
|
|
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<InsertMedMedication>) {
|
|
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<InsertMedCabinetItem, "familyId" | "addedBy" | "id"> & { 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<InsertMedMedicalRecord>
|
|
) {
|
|
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();
|