feat: MedSchedule модуль — семейный медицинский помощник с Telegram/MAX ботом, n8n OCR и /med UI
This commit is contained in:
696
server/medschedule/storage.ts
Normal file
696
server/medschedule/storage.ts
Normal file
@@ -0,0 +1,696 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user