202 lines
7.9 KiB
TypeScript
202 lines
7.9 KiB
TypeScript
import { db } from "../db";
|
||
import { medScheduleStorage } from "./storage";
|
||
import { sendBotMessage } from "./bot-sender";
|
||
import { generateDailyCheckinQuestion } from "./ai.service";
|
||
import { withSuperAdmin } from "../db";
|
||
import { medIntakeLogs } from "@shared/schema";
|
||
import { eq } from "drizzle-orm";
|
||
import type { BotPlatform } from "./bot-sender";
|
||
|
||
let isRunning = false;
|
||
|
||
function toMsk(date: Date): Date {
|
||
return new Date(date.toLocaleString("en-US", { timeZone: "Europe/Moscow" }));
|
||
}
|
||
|
||
function mskDateString(date: Date): string {
|
||
return toMsk(date).toISOString().split("T")[0];
|
||
}
|
||
|
||
export function startMedScheduleWorker() {
|
||
if (isRunning) return;
|
||
isRunning = true;
|
||
|
||
// Каждую минуту — отправка напоминаний о приёме лекарств
|
||
setInterval(async () => {
|
||
try {
|
||
await withSuperAdmin(() => processReminders());
|
||
} catch (err) {
|
||
console.error("[MedSchedule Worker] Reminder error:", err);
|
||
}
|
||
}, 60_000);
|
||
|
||
// Каждые 15 минут — уведомления о пропущенных приёмах
|
||
setInterval(async () => {
|
||
try {
|
||
await withSuperAdmin(() => processMissedIntakes());
|
||
} catch (err) {
|
||
console.error("[MedSchedule Worker] Missed intakes error:", err);
|
||
}
|
||
}, 15 * 60_000);
|
||
|
||
// Каждый час — проверка сроков годности
|
||
setInterval(async () => {
|
||
try {
|
||
await withSuperAdmin(() => processExpiryAlerts());
|
||
} catch (err) {
|
||
console.error("[MedSchedule Worker] Expiry alerts error:", err);
|
||
}
|
||
}, 60 * 60_000);
|
||
|
||
// Ежедневный чек-ин в 9:00 по Москве
|
||
scheduleAtMskHour(9, async () => {
|
||
try {
|
||
await withSuperAdmin(() => processDailyCheckins());
|
||
} catch (err) {
|
||
console.error("[MedSchedule Worker] Daily check-in error:", err);
|
||
}
|
||
});
|
||
|
||
console.log("[MedSchedule Worker] started");
|
||
}
|
||
|
||
async function processReminders() {
|
||
const orgId = await medScheduleStorage.ensureMedScheduleOrganization();
|
||
const windowEnd = new Date(Date.now() + 5 * 60_000); // ближайшие 5 минут
|
||
const after = new Date(Date.now() - 60_000); // не раньше 1 минуты назад
|
||
const intakes = await medScheduleStorage.getPendingIntakes(orgId, windowEnd, after);
|
||
|
||
for (const { intake, medication, member, family } of intakes) {
|
||
const reminded = new Set(intake.remindedVia ?? []);
|
||
if (reminded.has("telegram") && reminded.has("max")) continue;
|
||
|
||
const target = await resolveNotificationTarget(member.userId, family.id, orgId);
|
||
if (!target) continue;
|
||
|
||
const text = `💊 Напоминание: ${member.profileName} нужно принять ${medication.name}${medication.dosage ? ` (${medication.dosage})` : ""} в ${formatTime(intake.scheduledAt)}`;
|
||
|
||
const result = await sendBotMessage(target.platform, target.chatId, text, {
|
||
parseMode: "HTML",
|
||
});
|
||
|
||
if (result.ok) {
|
||
reminded.add(target.platform);
|
||
await db
|
||
.update(medIntakeLogs)
|
||
.set({ remindedVia: Array.from(reminded) })
|
||
.where(eq(medIntakeLogs.id, intake.id));
|
||
}
|
||
}
|
||
}
|
||
|
||
async function processMissedIntakes() {
|
||
const orgId = await medScheduleStorage.ensureMedScheduleOrganization();
|
||
const missed = await medScheduleStorage.getMissedIntakes(orgId, 15);
|
||
for (const { intake, medication, member, family } of missed) {
|
||
const creator = await medScheduleStorage.getFamilyCreator(family.id, orgId);
|
||
if (!creator) continue;
|
||
const binding = await medScheduleStorage.getBotBindingByUserId(creator.id, orgId);
|
||
if (!binding?.telegramChatId && !binding?.maxChatId) continue;
|
||
|
||
const platform: BotPlatform = binding.telegramChatId ? "telegram" : "max";
|
||
const chatId = binding.telegramChatId || binding.maxChatId!;
|
||
const text = `⚠️ ${member.profileName} пропустил приём ${medication.name} (${formatTime(intake.scheduledAt)})`;
|
||
await sendBotMessage(platform, chatId, text);
|
||
}
|
||
}
|
||
|
||
async function processExpiryAlerts() {
|
||
const orgId = await medScheduleStorage.ensureMedScheduleOrganization();
|
||
const groups = await medScheduleStorage.getAllFamilyGroups(orgId);
|
||
const alertDays = 30;
|
||
const alertDate = new Date();
|
||
alertDate.setDate(alertDate.getDate() + alertDays);
|
||
|
||
for (const group of groups) {
|
||
const items = await medScheduleStorage.getCabinetItems(group.id, orgId);
|
||
const expiring = items.filter((i) => i.expiryDate && new Date(i.expiryDate) <= alertDate);
|
||
if (expiring.length === 0) continue;
|
||
|
||
const creator = await medScheduleStorage.getFamilyCreator(group.id, orgId);
|
||
if (!creator) continue;
|
||
const binding = await medScheduleStorage.getBotBindingByUserId(creator.id, orgId);
|
||
if (!binding?.telegramChatId && !binding?.maxChatId) continue;
|
||
|
||
const platform: BotPlatform = binding.telegramChatId ? "telegram" : "max";
|
||
const chatId = binding.telegramChatId || binding.maxChatId!;
|
||
const list = expiring.map((i) => `- ${i.name} (${i.expiryDate})`).join("\n");
|
||
const text = `📅 Истекающие лекарства:\n${list}`;
|
||
await sendBotMessage(platform, chatId, text);
|
||
}
|
||
}
|
||
|
||
async function processDailyCheckins() {
|
||
const orgId = await medScheduleStorage.ensureMedScheduleOrganization();
|
||
const groups = await medScheduleStorage.getAllFamilyGroups(orgId);
|
||
|
||
for (const group of groups) {
|
||
const members = await medScheduleStorage.getFamilyMembers(group.id, orgId);
|
||
const medications = await medScheduleStorage.getMedicationsByFamily(group.id, orgId);
|
||
const records = await medScheduleStorage.getMedicalRecords(group.id, orgId);
|
||
|
||
const generated = await generateDailyCheckinQuestion(orgId, members, medications, records);
|
||
if (!generated) continue;
|
||
|
||
const today = mskDateString(new Date());
|
||
await medScheduleStorage.createDailyCheckin({
|
||
familyId: group.id,
|
||
memberId: null,
|
||
checkInDate: today,
|
||
question: generated.question,
|
||
});
|
||
|
||
// Отправляем вопрос админу/создателю семьи
|
||
const creator = await medScheduleStorage.getFamilyCreator(group.id, orgId);
|
||
if (!creator) continue;
|
||
const binding = await medScheduleStorage.getBotBindingByUserId(creator.id, orgId);
|
||
if (!binding?.telegramChatId && !binding?.maxChatId) continue;
|
||
|
||
const platform: BotPlatform = binding.telegramChatId ? "telegram" : "max";
|
||
const chatId = binding.telegramChatId || binding.maxChatId!;
|
||
await sendBotMessage(platform, chatId, `🩺 Ежедневный чек-ин: ${generated.question}\n\nОтветьте текстом.`);
|
||
}
|
||
}
|
||
|
||
async function resolveNotificationTarget(
|
||
memberUserId: number | null,
|
||
familyId: number,
|
||
orgId: number
|
||
): Promise<{ platform: BotPlatform; chatId: string } | null> {
|
||
if (memberUserId) {
|
||
const binding = await medScheduleStorage.getBotBindingByUserId(memberUserId, orgId);
|
||
if (binding?.telegramChatId) return { platform: "telegram", chatId: binding.telegramChatId };
|
||
if (binding?.maxChatId) return { platform: "max", chatId: binding.maxChatId };
|
||
}
|
||
|
||
const creator = await medScheduleStorage.getFamilyCreator(familyId, orgId);
|
||
if (!creator) return null;
|
||
const binding = await medScheduleStorage.getBotBindingByUserId(creator.id, orgId);
|
||
if (binding?.telegramChatId) return { platform: "telegram", chatId: binding.telegramChatId };
|
||
if (binding?.maxChatId) return { platform: "max", chatId: binding.maxChatId };
|
||
return null;
|
||
}
|
||
|
||
function formatTime(date: Date): string {
|
||
return date.toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit", timeZone: "Europe/Moscow" });
|
||
}
|
||
|
||
function scheduleAtMskHour(hour: number, fn: () => Promise<void>) {
|
||
const now = new Date();
|
||
const mskNow = toMsk(now);
|
||
const target = new Date(mskNow);
|
||
target.setHours(hour, 0, 0, 0);
|
||
if (target <= mskNow) {
|
||
target.setDate(target.getDate() + 1);
|
||
}
|
||
const ms = target.getTime() - mskNow.getTime();
|
||
setTimeout(() => {
|
||
fn().catch(console.error);
|
||
setInterval(() => fn().catch(console.error), 24 * 60 * 60 * 1000);
|
||
}, ms);
|
||
}
|