272 lines
9.8 KiB
JavaScript
272 lines
9.8 KiB
JavaScript
import TelegramBot from 'node-telegram-bot-api';
|
|
import { createClient } from '@libsql/client';
|
|
|
|
// Initialize Telegram bot
|
|
const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
|
|
const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID;
|
|
|
|
let bot = null;
|
|
|
|
if (TELEGRAM_BOT_TOKEN) {
|
|
bot = new TelegramBot(TELEGRAM_BOT_TOKEN, { polling: false });
|
|
}
|
|
|
|
// Initialize database connection
|
|
const db = createClient({
|
|
url: 'file:lawn_scheduler.db'
|
|
});
|
|
|
|
// Helper function to calculate zone status
|
|
function calculateZoneStatus(zone) {
|
|
const today = new Date();
|
|
|
|
if (!zone.lastMowedDate) {
|
|
return {
|
|
...zone,
|
|
daysSinceLastMow: null,
|
|
daysUntilNext: null,
|
|
status: 'new',
|
|
isOverdue: false,
|
|
isDueToday: false,
|
|
isNew: true
|
|
};
|
|
}
|
|
|
|
const lastMowed = new Date(zone.lastMowedDate);
|
|
const daysSinceLastMow = Math.floor((today - lastMowed) / (1000 * 60 * 60 * 24));
|
|
|
|
let daysUntilNext;
|
|
let status = 'ok';
|
|
|
|
if (zone.scheduleType === 'specific' && zone.nextMowDate) {
|
|
const nextMowDate = new Date(zone.nextMowDate);
|
|
daysUntilNext = Math.floor((nextMowDate - today) / (1000 * 60 * 60 * 24));
|
|
} else {
|
|
daysUntilNext = zone.intervalDays - daysSinceLastMow;
|
|
}
|
|
|
|
if (daysUntilNext < 0) {
|
|
status = 'overdue';
|
|
} else if (daysUntilNext <= 0) {
|
|
status = 'due';
|
|
} else if (daysUntilNext <= 1) {
|
|
status = 'due';
|
|
}
|
|
|
|
return {
|
|
...zone,
|
|
daysSinceLastMow,
|
|
daysUntilNext,
|
|
status,
|
|
isOverdue: daysUntilNext < 0,
|
|
isDueToday: daysUntilNext <= 0 && daysUntilNext >= -1,
|
|
isNew: false
|
|
};
|
|
}
|
|
|
|
// Send Telegram message
|
|
export async function sendTelegramMessage(message) {
|
|
if (!bot || !TELEGRAM_CHAT_ID) {
|
|
console.log('Telegram not configured. Message would be:', message);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
await bot.sendMessage(TELEGRAM_CHAT_ID, message, { parse_mode: 'Markdown' });
|
|
console.log('Telegram message sent successfully');
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Failed to send Telegram message:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check for zones that need mowing and send notifications
|
|
export async function checkMowingReminders() {
|
|
try {
|
|
// First check if we're in mowing season
|
|
const seasonResult = await db.execute('SELECT * FROM season_settings WHERE id = 1');
|
|
if (seasonResult.rows.length > 0) {
|
|
const settings = seasonResult.rows[0];
|
|
if (settings.isActive) {
|
|
const now = new Date();
|
|
const currentMonth = now.getMonth() + 1;
|
|
const currentDay = now.getDate();
|
|
|
|
const startDate = new Date(now.getFullYear(), settings.startMonth - 1, settings.startDay);
|
|
const endDate = new Date(now.getFullYear(), settings.endMonth - 1, settings.endDay);
|
|
const currentDate = new Date(now.getFullYear(), currentMonth - 1, currentDay);
|
|
|
|
let inSeason;
|
|
if (startDate <= endDate) {
|
|
inSeason = currentDate >= startDate && currentDate <= endDate;
|
|
} else {
|
|
inSeason = currentDate >= startDate || currentDate <= endDate;
|
|
}
|
|
|
|
if (!inSeason) {
|
|
console.log('Not in mowing season, skipping reminders');
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
const result = await db.execute('SELECT * FROM zones ORDER BY name');
|
|
const zones = result.rows.map(row => ({
|
|
id: row.id,
|
|
name: row.name,
|
|
lastMowedDate: row.lastMowedDate,
|
|
intervalDays: row.intervalDays,
|
|
nextMowDate: row.nextMowDate,
|
|
scheduleType: row.scheduleType || 'interval',
|
|
area: row.area || 0,
|
|
}));
|
|
|
|
const zonesWithStatus = zones.map(calculateZoneStatus);
|
|
|
|
// Find zones that are due or overdue
|
|
const dueZones = zonesWithStatus.filter(zone => zone.isDueToday);
|
|
const overdueZones = zonesWithStatus.filter(zone => zone.isOverdue);
|
|
const newZones = zonesWithStatus.filter(zone => zone.isNew);
|
|
|
|
if (dueZones.length > 0 || overdueZones.length > 0 || newZones.length > 0) {
|
|
let message = '🌱 *Lawn Mowing Reminder*\n\n';
|
|
|
|
if (overdueZones.length > 0) {
|
|
message += '🚨 *OVERDUE ZONES:*\n';
|
|
overdueZones.forEach(zone => {
|
|
const daysOverdue = Math.abs(zone.daysUntilNext);
|
|
message += `• ${zone.name} - ${daysOverdue} day${daysOverdue > 1 ? 's' : ''} overdue\n`;
|
|
});
|
|
message += '\n';
|
|
}
|
|
|
|
if (dueZones.length > 0) {
|
|
message += '⏰ *DUE TODAY:*\n';
|
|
dueZones.forEach(zone => {
|
|
message += `• ${zone.name}\n`;
|
|
});
|
|
message += '\n';
|
|
}
|
|
|
|
if (newZones.length > 0) {
|
|
message += '🆕 *NEW ZONES (Not yet mowed):*\n';
|
|
newZones.forEach(zone => {
|
|
message += `• ${zone.name}\n`;
|
|
});
|
|
message += '\n';
|
|
}
|
|
|
|
message += `Total zones needing attention: ${overdueZones.length + dueZones.length + newZones.length}`;
|
|
|
|
await sendTelegramMessage(message);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking mowing reminders:', error);
|
|
}
|
|
}
|
|
|
|
// Generate and send weekly report
|
|
export async function sendWeeklyReport() {
|
|
try {
|
|
const result = await db.execute('SELECT * FROM zones ORDER BY name');
|
|
const zones = result.rows.map(row => ({
|
|
id: row.id,
|
|
name: row.name,
|
|
lastMowedDate: row.lastMowedDate,
|
|
intervalDays: row.intervalDays,
|
|
nextMowDate: row.nextMowDate,
|
|
scheduleType: row.scheduleType || 'interval',
|
|
area: row.area || 0,
|
|
}));
|
|
|
|
const zonesWithStatus = zones.map(calculateZoneStatus);
|
|
|
|
// Get weekly statistics
|
|
const weekAgo = new Date();
|
|
weekAgo.setDate(weekAgo.getDate() - 7);
|
|
|
|
const weeklyHistoryResult = await db.execute({
|
|
sql: `
|
|
SELECT
|
|
COUNT(*) as sessions,
|
|
SUM(duration) as totalMinutes,
|
|
SUM(z.area) as totalArea,
|
|
COUNT(DISTINCT mh.zoneId) as uniqueZones
|
|
FROM mowing_history mh
|
|
JOIN zones z ON mh.zoneId = z.id
|
|
WHERE mh.mowedDate >= ?
|
|
`,
|
|
args: [weekAgo.toISOString()]
|
|
});
|
|
|
|
const weeklyStats = weeklyHistoryResult.rows[0];
|
|
|
|
// Count zones by status
|
|
const overdueCount = zonesWithStatus.filter(zone => zone.isOverdue).length;
|
|
const dueCount = zonesWithStatus.filter(zone => zone.isDueToday).length;
|
|
const newCount = zonesWithStatus.filter(zone => zone.isNew).length;
|
|
const okCount = zonesWithStatus.filter(zone => zone.status === 'ok').length;
|
|
const totalArea = zones.reduce((sum, zone) => sum + zone.area, 0);
|
|
const mowedArea = zonesWithStatus
|
|
.filter(zone => zone.status === 'ok')
|
|
.reduce((sum, zone) => sum + zone.area, 0);
|
|
const mowedPercentage = totalArea > 0 ? (mowedArea / totalArea) * 100 : 0;
|
|
|
|
let message = '📊 *Weekly Lawn Care Report*\n';
|
|
message += `📅 ${new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}\n\n`;
|
|
|
|
// Overall status
|
|
message += '🏡 *ZONE STATUS:*\n';
|
|
message += `✅ Up to date: ${okCount} zones\n`;
|
|
message += `🆕 New zones: ${newCount} zones\n`;
|
|
message += `⏰ Due today: ${dueCount} zones\n`;
|
|
message += `🚨 Overdue: ${overdueCount} zones\n`;
|
|
message += `📐 Total area: ${totalArea.toLocaleString()} sq ft\n`;
|
|
message += `🌱 Mowed area: ${mowedPercentage.toFixed(1)}%\n\n`;
|
|
|
|
// Weekly activity
|
|
message += '📈 *THIS WEEK\'S ACTIVITY:*\n';
|
|
message += `🔄 Mowing sessions: ${weeklyStats.sessions || 0}\n`;
|
|
message += `⏱️ Total time: ${weeklyStats.totalMinutes ? Math.floor(weeklyStats.totalMinutes / 60) : 0}h ${weeklyStats.totalMinutes ? weeklyStats.totalMinutes % 60 : 0}m\n`;
|
|
message += `📐 Area mowed: ${(weeklyStats.totalArea || 0).toLocaleString()} sq ft\n`;
|
|
message += `🎯 Zones maintained: ${weeklyStats.uniqueZones || 0}\n\n`;
|
|
|
|
// Zones needing attention
|
|
const needsAttention = [...zonesWithStatus.filter(zone => zone.isOverdue || zone.isDueToday || zone.isNew)];
|
|
if (needsAttention.length > 0) {
|
|
message += '⚠️ *NEEDS ATTENTION:*\n';
|
|
needsAttention.forEach(zone => {
|
|
let status = '';
|
|
if (zone.isOverdue) {
|
|
status = `🚨 ${Math.abs(zone.daysUntilNext)} days overdue`;
|
|
} else if (zone.isDueToday) {
|
|
status = '⏰ Due today';
|
|
} else if (zone.isNew) {
|
|
status = '🆕 Not yet mowed';
|
|
}
|
|
message += `• ${zone.name} - ${status}\n`;
|
|
});
|
|
} else {
|
|
message += '🎉 *All zones are up to date!*\n';
|
|
}
|
|
|
|
await sendTelegramMessage(message);
|
|
} catch (error) {
|
|
console.error('Error generating weekly report:', error);
|
|
}
|
|
}
|
|
|
|
// Test Telegram connection
|
|
export async function testTelegramConnection() {
|
|
if (!bot || !TELEGRAM_CHAT_ID) {
|
|
return { success: false, message: 'Telegram not configured' };
|
|
}
|
|
|
|
try {
|
|
await sendTelegramMessage('🤖 *Lawn Care Manager*\n\nTelegram notifications are working correctly!');
|
|
return { success: true, message: 'Test message sent successfully' };
|
|
} catch (error) {
|
|
return { success: false, message: error.message };
|
|
}
|
|
} |