Files
Sheep-Service/cron/tasks/stands.js

260 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
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 += `<rect x="${padding}" y="${y}" width="${totalWidth - padding * 2}" height="40" rx="8" fill="${isEven ? '#fbfbfb' : '#e7e7e1'}" />`;
// Текст часу
rowsSvg += `<text x="${padding + 7}" y="${y + 27}" font-family="Roboto Mono" font-size="15" fill="#313131">${hStart}-${hEnd}</text>`;
// Блоки з іменами
rowSheepIds.forEach((id, j) => {
const x = padding + timeWidth + (j * (blockWidth + gap));
const name = sheepMap.get(id) || '';
if(!sheepMap.get(id)) isNotFull = true; // Якщо є вільне місце
rowsSvg += `
<g>
<rect x="${x}" y="${y + 5}" width="${blockWidth}" height="30" rx="5" fill="${isEven ? '#e7e7e1' : '#fbfbfb'}" />
<text x="${x + 5}" y="${y + 25}" font-family="Roboto Mono" font-size="15" fill="#313131">${name}</text>
</g>`;
});
});
const finalSvg = `
<svg width="${totalWidth * 2}" height="${totalHeight * 2}" viewBox="0 0 ${totalWidth} ${totalHeight}" xmlns="http://www.w3.org/2000/svg">
<rect height="${totalHeight}" width="${totalWidth}" fill="#e7e7e1"/>
<text font-weight="bold" text-anchor="middle" font-family="'Roboto Mono'" font-size="24" y="52" x="${totalWidth / 2}" fill="#313131">
${date}${title}
</text>
${rowsSvg}
</svg>`;
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();