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) { 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); }