lawnmowing/server/telegram.js

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