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