const db = require("../config/db"); const Notification = require("../utils/notification"); const sharp = require('sharp'); const TelegramBot = require("node-telegram-bot-api"); const util = require('util'); const dbAll = util.promisify(db.all).bind(db); const dbRun = util.promisify(db.run).bind(db); const TOKEN = process.env.TELEGRAM_TOKEN; const STAND_CHAT_ID = process.env.STAND_CHAT_ID; const bot = new TelegramBot(TOKEN, { polling: false }); class Stands { async check_add() { const sqlStands = ` SELECT id FROM stand_list WHERE status = '1' ORDER BY id `; db.all(sqlStands, (err, stands) => { if (err) { console.error('DB error:', err.message); return; } if (!stands.length) { console.log('There are no active stands'); return; } const dateNow = Date.now(); const sqlSchedule = ` SELECT 1 FROM stand_schedule WHERE stand_id = ? AND date >= ? LIMIT 1 `; let checked = 0; let emptyStands = 0; stands.forEach(stand => { db.get(sqlSchedule, [stand.id, dateNow], (err, row) => { if (err) { console.error('Schedule error:', err.message); return; } if (!row) emptyStands++; checked++; if (checked === stands.length) { console.log('Empty stands:', emptyStands); if (emptyStands > 0) { Notification.sendStandAdd({ title: 'Додайте нові дні служіння', body: `${emptyStands} з ${stands.length} стендів потребують додавання днів служіння.`, page: '/stand' }); } } }); }); }); } async check_entries() { try { const today = new Date(); today.setHours(0, 0, 0, 0); // 1. Отримуємо вісників і одразу робимо Map для швидкого пошуку const sheeps = await dbAll(`SELECT id, name FROM sheeps`); if (!sheeps.length) return console.log('There are no sheeps'); const sheepMap = new Map(sheeps.map(s => [s.id, s.name])); const startTomorrow = today.getTime() + 86400000; const startDayAfterTomorrow = today.getTime() + 86400000 * 2; const sqlStands = ` SELECT stand_schedule.*, (SELECT stand_list.title FROM stand_list WHERE stand_list.id = stand_schedule.stand_id) AS title FROM stand_schedule WHERE stand_schedule.date >= ? AND stand_schedule.date < ? ORDER BY stand_schedule.stand_id `; const schedule = await dbAll(sqlStands, [startTomorrow, startDayAfterTomorrow]); if (!schedule.length) return console.log('No active schedule'); // 2. Угруповання даних (Transform) const standsData = schedule.reduce((acc, item) => { if (!acc[item.stand_id]) { acc[item.stand_id] = { id: item.stand_id, title: item.title || `Стенд ${sId}`, hours: {}, maxSheepIdx: 0, date: item.date }; } if (!acc[item.stand_id].hours[item.hour]) { acc[item.stand_id].hours[item.hour] = {}; } acc[item.stand_id].hours[item.hour][item.number_sheep] = item.sheep_id; acc[item.stand_id].maxSheepIdx = Math.max(acc[item.stand_id].maxSheepIdx, item.number_sheep); return acc; }, {}); // 3. Генерація for (const standId in standsData) { const data = standsData[standId]; const title = data.title; const sortedHours = Object.keys(data.hours).map(Number).sort((a, b) => a - b); // Визначаємо крок (мінімальна різниця між годинником або 1) let step = 1; if (sortedHours.length > 1) { step = sortedHours[1] - sortedHours[0]; } const sheepIdsMatrix = sortedHours.map(h => { const row = []; for (let i = 0; i <= data.maxSheepIdx; i++) { row.push(data.hours[h][i] || null); } return row; }); const formattedDate = new Date(data.date).toLocaleDateString('ru-RU'); const result = await generateSchedule({ title: title, date: formattedDate, options: { start: sortedHours[0], quantity: data.maxSheepIdx + 1, step: step }, sheep_ids: sheepIdsMatrix, sheepMap // Передаємо Map для імен }); if (result && result.buffer) { try { let isNotFull = result.isNotFull ? '\n\n✏️ Ще є вільні місця, встигніть записатись' : ''; // 1. Надсилаємо нове фото const sentMessage = await bot.sendPhoto(STAND_CHAT_ID, result.buffer, { caption: `📍 *${title}*\n📅 ${formattedDate}${isNotFull}`, parse_mode: 'Markdown' }, { filename: result.fileName, contentType: 'image/png' }); // 2. Зберігаємо ID нового повідомлення у базі await dbRun( `INSERT INTO sent_messages (last_message_id, created_at) VALUES (?, ?)`, [sentMessage.message_id, Date.now()] ); console.log(`✅ Обновлено: ${title}`); } catch (err) { console.error('Помилка відправки в Telegram:', err.message); } } } } catch (err) { console.error('Logic error:', err); } } } // Генерування зображення графіка async function generateSchedule({ title, date, options, sheep_ids, sheepMap }) { const { start, step, quantity } = options; const rowsCount = sheep_ids.length; const rowHeight = 50; const timeWidth = 120; const gap = 10; const padding = 10; const totalWidth = 200 + (200 * quantity); const headerHeight = 85; const totalHeight = 136 + (rowHeight * (rowsCount - 1)); const mainRectWidth = totalWidth - (padding * 2); const availableForBlocks = mainRectWidth - timeWidth - 5; const blockWidth = (availableForBlocks - (gap * (quantity - 1))) / quantity; let isNotFull = false; const formatTime = (h) => { const hh = Math.floor(h).toString().padStart(2, '0'); const mm = (h % 1 * 60).toString().padStart(2, '0'); return `${hh}:${mm}`; }; let rowsSvg = ''; sheep_ids.forEach((rowSheepIds, i) => { const y = headerHeight + (i * rowHeight); const hStart = formatTime(start + (i * step)); const hEnd = formatTime(start + ((i + 1) * step)); const isEven = i % 2 === 0; // Фон рядка rowsSvg += ``; // Текст часу rowsSvg += `${hStart}-${hEnd}`; // Блоки з іменами rowSheepIds.forEach((id, j) => { const x = padding + timeWidth + (j * (blockWidth + gap)); const name = sheepMap.get(id) || ''; if(!sheepMap.get(id)) isNotFull = true; // Якщо є вільне місце rowsSvg += ` ${name} `; }); }); const finalSvg = ` ${date} • ${title} ${rowsSvg} `; try { const buffer = await sharp(Buffer.from(finalSvg)) .png() .toBuffer(); return { buffer, fileName: `${title}_${date}.png`, isNotFull }; } catch (e) { console.error('Sharp error:', e); return null; } } module.exports = new Stands();