Files
iistwin/server/medschedule/worker.ts

202 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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