Spaces:
Sleeping
Sleeping
{% extends "base.html" %} | |
{% block title %}Farmer Dashboard - Farm Management Portal{% endblock %} | |
{% block content %} | |
<div class="container mt-4"> | |
<!-- Welcome Header --> | |
<div class="row mb-4"> | |
<div class="col-12"> | |
<div class="card bg-success text-white"> | |
<div class="card-body"> | |
<h2><i class="fas fa-tachometer-alt me-2"></i>Welcome, {{ farmer.name }}!</h2> | |
<p class="mb-0">Manage your farms and get AI-powered daily recommendations</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- External AI Tools Quick Access --> | |
<!-- Quick Stats --> | |
<div class="row mb-4"> | |
<div class="col-md-3 mb-3"> | |
<div class="card text-center"> | |
<div class="card-body"> | |
<i class="fas fa-seedling fa-2x text-success mb-2"></i> | |
<h5>{{ farms|length }}</h5> | |
<small class="text-muted">Total Farms</small> | |
</div> | |
</div> | |
</div> | |
<div class="col-md-3 mb-3"> | |
<div class="card text-center"> | |
<div class="card-body"> | |
<i class="fas fa-calendar-check fa-2x text-primary mb-2"></i> | |
<h5>{{ recent_activities|length }}</h5> | |
<small class="text-muted">Recent Activities</small> | |
</div> | |
</div> | |
</div> | |
<div class="col-md-3 mb-3"> | |
<div class="card text-center"> | |
<div class="card-body"> | |
<i class="fas fa-sms fa-2x text-info mb-2"></i> | |
<h5>{% if today_advisory %}Active{% else %}Pending{% endif %}</h5> | |
<small class="text-muted">Today's Advisory</small> | |
</div> | |
</div> | |
</div> | |
<div class="col-md-3 mb-3"> | |
<div class="card text-center"> | |
<div class="card-body"> | |
<i class="fas fa-user fa-2x text-warning mb-2"></i> | |
<h5>{{ farmer.contact_number }}</h5> | |
<small class="text-muted">Contact</small> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="row"> | |
<!-- Today's Advisory --> | |
<div class="col-md-8 mb-4"> | |
<div class="card"> | |
<div class="card-header bg-primary text-white"> | |
<h5><i class="fas fa-brain me-2"></i>Today's AI Advisory</h5> | |
</div> | |
<div class="card-body"> | |
{% if today_advisory %} | |
<div class="alert alert-success"> | |
<h6><i class="fas fa-check-circle me-2"></i>Tasks to Do:</h6> | |
<p>{{ today_advisory.task_to_do }}</p> | |
</div> | |
<div class="alert alert-warning"> | |
<h6><i class="fas fa-exclamation-triangle me-2"></i>Tasks to Avoid:</h6> | |
<p>{{ today_advisory.task_to_avoid }}</p> | |
</div> | |
{% if today_advisory.reason_explanation %} | |
<div class="alert alert-info"> | |
<h6><i class="fas fa-info-circle me-2"></i>Explanation:</h6> | |
<p>{{ today_advisory.reason_explanation }}</p> | |
</div> | |
{% endif %} | |
{% if farms %} | |
<div class="mt-3"> | |
<button class="btn btn-success" onclick="sendSMSAdvisory({{ farms[0].id }})"> | |
<i class="fas fa-sms me-2"></i>Send SMS Alert | |
</button> | |
<button class="btn btn-info" onclick="sendTelegramAdvisory({{ farms[0].id }})"> | |
<i class="fab fa-telegram me-2"></i>Send Telegram Alert | |
</button> | |
<button class="btn btn-primary" onclick="generateNewAdvisory({{ farms[0].id }})"> | |
<i class="fas fa-sync me-2"></i>Refresh Advisory | |
</button> | |
</div> | |
{% endif %} | |
{% else %} | |
<div class="text-center text-muted py-4"> | |
<i class="fas fa-robot fa-3x mb-3"></i> | |
<h6>No advisory generated yet for today</h6> | |
<p>Click below to generate AI-powered recommendations</p> | |
{% if farms %} | |
<button class="btn btn-primary" onclick="generateNewAdvisory({{ farms[0].id }})"> | |
<i class="fas fa-magic me-2"></i>Generate Advisory | |
</button> | |
{% endif %} | |
</div> | |
{% endif %} | |
</div> | |
</div> | |
</div> | |
<!-- Quick Actions --> | |
<div class="col-md-4 mb-4"> | |
<div class="card"> | |
<div class="card-header bg-secondary text-white"> | |
<h5><i class="fas fa-bolt me-2"></i>Quick Actions</h5> | |
<small><i class="fas fa-robot me-1"></i>AI-powered tools for better farming</small> | |
</div> | |
<div class="card-body"> | |
<div class="d-grid gap-2"> | |
<a href="{{ url_for('add_farm') }}" class="btn btn-success"> | |
<i class="fas fa-plus me-2"></i>Add New Farm | |
</a> | |
{% if farms %} | |
<a href="{{ url_for('farm_details', farm_id=farms[0].id) }}" class="btn btn-primary"> | |
<i class="fas fa-eye me-2"></i>View Farm Details | |
</a> | |
<button class="btn btn-success" onclick="window.open('https://pranit144-weather-forecast-farmers.hf.space', '_blank')" title="AI-powered weather forecasting tool"> | |
<i class="fas fa-cloud me-2"></i>Smart Weather Forecast | |
</button> | |
<button class="btn btn-warning" onclick="viewWeatherAlerts({{ farms[0].id }})"> | |
<i class="fas fa-exclamation-triangle me-2"></i>Weather Alerts | |
</button> | |
<button class="btn btn-info" onclick="window.open('https://agri-ai-rosy.vercel.app/cropMarketTrendAnalyzer', '_blank')" title="AI market trend analyzer"> | |
<i class="fas fa-chart-line me-2"></i>Market Analyzer | |
</button> | |
<button class="btn btn-danger" onclick="window.open('https://agri-ai-rosy.vercel.app/plant-disease-detector', '_blank')" title="AI plant disease detector"> | |
<i class="fas fa-search me-2"></i>Disease Detector | |
</button> | |
<button class="btn btn-secondary" onclick="showSendImageModal()"> | |
<i class="fas fa-paper-plane me-2"></i>Send Image via Telegram | |
</button> | |
{% endif %} | |
<a href="{{ url_for('farmer_logout') }}" class="btn btn-outline-danger"> | |
<i class="fas fa-sign-out-alt me-2"></i>Logout | |
</a> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Daily Tasks Section --> | |
<div class="row mb-4"> | |
<div class="col-12"> | |
<div class="card"> | |
<div class="card-header bg-info text-white d-flex justify-content-between align-items-center"> | |
<h5><i class="fas fa-tasks me-2"></i>Today's Daily Tasks</h5> | |
<small id="task-date">{{ today_date or 'Today' }}</small> | |
</div> | |
<div class="card-body"> | |
<div id="daily-tasks-container"> | |
<div class="text-center text-muted py-4" id="no-tasks-message"> | |
<i class="fas fa-clipboard-list fa-3x mb-3"></i> | |
<h6>No daily tasks loaded</h6> | |
<p>Generate daily tasks to get AI-powered farming recommendations</p> | |
</div> | |
</div> | |
<div class="mt-3 text-center"> | |
<div class="btn-group" role="group"> | |
<button class="btn btn-primary" onclick="loadDailyTasks()"> | |
<i class="fas fa-sync me-2"></i>Load Today's Tasks | |
</button> | |
<button class="btn btn-success" onclick="generateDailyTasks()"> | |
<i class="fas fa-magic me-2"></i>Generate New Tasks | |
</button> | |
<button class="btn btn-info" onclick="sendTasksTelegram()" title="Send tasks to your Telegram"> | |
<i class="fab fa-telegram me-2"></i>Send to Telegram | |
</button> | |
<button class="btn btn-outline-danger" onclick="deleteAllTasks()" title="Delete all tasks for today"> | |
<i class="fas fa-trash me-2"></i>Clear All | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Farms List --> | |
{% if farms %} | |
<div class="row mb-4"> | |
<div class="col-12"> | |
<div class="card"> | |
<div class="card-header"> | |
<h5><i class="fas fa-list me-2"></i>Your Farms</h5> | |
</div> | |
<div class="card-body"> | |
<div class="table-responsive"> | |
<table class="table table-striped"> | |
<thead> | |
<tr> | |
<th>Farm Name</th> | |
<th>Size (Acres)</th> | |
<th>Crops</th> | |
<th>Irrigation</th> | |
<th>Actions</th> | |
</tr> | |
</thead> | |
<tbody> | |
{% for farm in farms %} | |
<tr> | |
<td>{{ farm.farm_name }}</td> | |
<td>{{ farm.farm_size }}</td> | |
<td> | |
{% for crop in farm.get_crop_types() %} | |
<span class="badge bg-success me-1">{{ crop }}</span> | |
{% endfor %} | |
</td> | |
<td>{{ farm.irrigation_type }}</td> | |
<td> | |
<div class="btn-group" role="group"> | |
<!-- Primary Actions --> | |
<a href="{{ url_for('farm_details', farm_id=farm.id) }}" class="btn btn-sm btn-outline-primary" title="View Details"> | |
<i class="fas fa-eye"></i> | |
</a> | |
<button class="btn btn-sm btn-outline-success" onclick="generateNewAdvisory({{ farm.id }})" title="Generate Daily Advisory"> | |
<i class="fas fa-brain"></i> | |
</button> | |
</div> | |
<!-- Yearly Plan Actions --> | |
<div class="btn-group ms-1" role="group"> | |
<button class="btn btn-sm btn-outline-info" onclick="generateYearlyPlan({{ farm.id }})" title="Generate Yearly Plan"> | |
<i class="fas fa-calendar-plus"></i> | |
</button> | |
<button class="btn btn-sm btn-outline-secondary" onclick="viewYearlyPlan({{ farm.id }})" title="View Yearly Plan"> | |
<i class="fas fa-calendar-alt"></i> | |
</button> | |
<button class="btn btn-sm btn-outline-warning" onclick="editYearlyPlan({{ farm.id }})" title="Edit Yearly Plan"> | |
<i class="fas fa-edit"></i> | |
</button> | |
<button class="btn btn-sm btn-outline-dark" onclick="downloadYearlyPlanPDF({{ farm.id }})" title="Download Yearly Plan PDF"> | |
<i class="fas fa-file-pdf"></i> | |
</button> | |
<button class="btn btn-sm btn-outline-primary" onclick="sendYearlyPlanTelegram({{ farm.id }})" title="Send Yearly Plan via Telegram"> | |
<i class="fas fa-paper-plane"></i> | |
</button> | |
</div> | |
<!-- Smart Features --> | |
<div class="btn-group ms-1" role="group"> | |
<button class="btn btn-sm btn-outline-info" onclick="window.open('https://pranit144-weather-forecast-farmers.hf.space', '_blank')" title="Weather Forecast"> | |
<i class="fas fa-cloud"></i> | |
</button> | |
<button class="btn btn-sm btn-outline-success" onclick="window.open('https://agri-ai-rosy.vercel.app/cropMarketTrendAnalyzer', '_blank')" title="Market Prices"> | |
<i class="fas fa-chart-line"></i> | |
</button> | |
<button class="btn btn-sm btn-outline-danger" onclick="window.open('https://agri-ai-rosy.vercel.app/plant-disease-detector', '_blank')" title="Disease Detection"> | |
<i class="fas fa-bug"></i> | |
</button> | |
</div> | |
<!-- Delete Actions --> | |
<div class="btn-group ms-1" role="group"> | |
<button class="btn btn-sm btn-outline-danger" onclick="deleteYearlyPlan({{ farm.id }})" title="Delete Yearly Plan"> | |
<i class="fas fa-calendar-times"></i> | |
</button> | |
<button class="btn btn-sm btn-danger" onclick="deleteFarm({{ farm.id }})" title="Delete Farm" | |
style="opacity: 0.7;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.7'"> | |
<i class="fas fa-trash-alt"></i> | |
</button> | |
</div> | |
</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endif %} | |
<!-- Recent Activities --> | |
{% if recent_activities %} | |
<div class="row"> | |
<div class="col-12"> | |
<div class="card"> | |
<div class="card-header"> | |
<h5><i class="fas fa-history me-2"></i>Recent Activities</h5> | |
</div> | |
<div class="card-body"> | |
<div class="timeline"> | |
{% for activity in recent_activities %} | |
<div class="timeline-item mb-3"> | |
<div class="d-flex"> | |
<div class="flex-shrink-0"> | |
<i class="fas fa-circle text-success"></i> | |
</div> | |
<div class="flex-grow-1 ms-3"> | |
<h6 class="mb-1">{{ activity.activity_type|title }}</h6> | |
<p class="mb-1">{{ activity.activity_description }}</p> | |
<small class="text-muted"> | |
Scheduled: {{ activity.scheduled_date.strftime('%d %b %Y') }} | |
| Status: <span class="badge bg-{{ 'success' if activity.status == 'completed' else 'warning' }}">{{ activity.status|title }}</span> | |
</small> | |
</div> | |
</div> | |
</div> | |
{% endfor %} | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endif %} | |
</div> | |
<!-- Loading Modal --> | |
<div class="modal fade" id="loadingModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1"> | |
<div class="modal-dialog modal-dialog-centered"> | |
<div class="modal-content"> | |
<div class="modal-body text-center"> | |
<div class="spinner-border text-primary" role="status"> | |
<span class="visually-hidden">Loading...</span> | |
</div> | |
<p class="mt-3 mb-0">Processing your request...</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Yearly Plan Modal --> | |
<div class="modal fade" id="yearlyPlanModal" tabindex="-1"> | |
<div class="modal-dialog modal-lg modal-dialog-scrollable"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title">Yearly Plan</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
</div> | |
<div class="modal-body"> | |
<div id="yearlyPlanContent">Loading...</div> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> | |
<button type="button" class="btn btn-primary" id="saveYearlyPlanBtn" style="display:none;">Save</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
{% block extra_js %} | |
<script> | |
// Helper to show/hide the Bootstrap 5 modal using vanilla JS | |
function _getLoadingModalInstance(){ | |
const modalEl = document.getElementById('loadingModal'); | |
return bootstrap.Modal.getOrCreateInstance(modalEl); | |
} | |
function _showYearlyPlanModal(html, isEdit = false) { | |
const el = document.getElementById('yearlyPlanContent'); | |
el.innerHTML = html; | |
const saveBtn = document.getElementById('saveYearlyPlanBtn'); | |
if (isEdit) { | |
saveBtn.style.display = 'inline-block'; | |
} else { | |
saveBtn.style.display = 'none'; | |
} | |
const m = new bootstrap.Modal(document.getElementById('yearlyPlanModal')); | |
m.show(); | |
} | |
function generateYearlyPlan(farmId) { | |
const modal = _getLoadingModalInstance(); | |
modal.show(); | |
fetch(`/farmer/farm/${farmId}/yearly_plan/generate`, {method: 'POST'}) | |
.then(response => response.json()) | |
.then(data => { | |
modal.hide(); | |
if (data.success) { | |
alert('Yearly plan generated successfully!'); | |
location.reload(); | |
} else { | |
alert('Failed to generate plan: ' + (data.error || 'Unknown error')); | |
} | |
}) | |
.catch(error => { | |
modal.hide(); | |
alert('Error: ' + error.message); | |
}); | |
} | |
function viewYearlyPlan(farmId) { | |
const modal = _getLoadingModalInstance(); | |
modal.show(); | |
fetch(`/farmer/farm/${farmId}/yearly_plan`) | |
.then(response => response.json()) | |
.then(data => { | |
modal.hide(); | |
if (data.success && data.plan) { | |
if (data.is_html && data.html_content) { | |
// Open comprehensive HTML plan in a new window for better viewing | |
const newWindow = window.open('', '_blank', 'width=1200,height=800,scrollbars=yes,resizable=yes'); | |
newWindow.document.write(data.html_content); | |
newWindow.document.close(); | |
newWindow.document.title = `Yearly Plan - ${data.plan.farm_name}`; | |
} else { | |
// Fallback to modal for simple plans | |
const html = ` | |
<div class="card"> | |
<div class="card-header bg-success text-white"> | |
<h6><i class="fas fa-seedling me-2"></i>${data.plan.farm_name || 'Farm'}</h6> | |
</div> | |
<div class="card-body"> | |
<div class="mb-2"> | |
<span class="badge bg-info">Year: ${data.plan.year || new Date().getFullYear()}</span> | |
${data.plan.ai_generated ? '<span class="badge bg-success ms-2">AI Generated</span>' : '<span class="badge bg-secondary ms-2">Basic Plan</span>'} | |
</div> | |
<h6>Summary:</h6> | |
<p class="text-muted">${data.plan.summary_text || 'No summary available'}</p> | |
<h6>Plan Details:</h6> | |
<pre class="bg-light p-3 rounded" style="max-height: 400px; overflow-y: auto;">${JSON.stringify(data.plan.plan_json || data.plan.plan, null, 2)}</pre> | |
<small class="text-muted">Generated: ${new Date(data.plan.generated_at || data.plan.created_at).toLocaleDateString()}</small> | |
</div> | |
<div class="card-footer text-center"> | |
<button class="btn btn-primary btn-sm" onclick="generateYearlyPlan(${farmId})"> | |
<i class="fas fa-sync-alt me-1"></i>Regenerate with AI | |
</button> | |
</div> | |
</div> | |
`; | |
_showYearlyPlanModal(html); | |
} | |
} else { | |
alert('No yearly plan found for this farm. Generate one first.'); | |
} | |
}) | |
.catch(error => { | |
modal.hide(); | |
alert('Error loading plan: ' + error.message); | |
}); | |
} | |
function editYearlyPlan(farmId) { | |
// First load the existing plan | |
fetch(`/farmer/farm/${farmId}/yearly_plan`) | |
.then(response => response.json()) | |
.then(data => { | |
if (!data.success || !data.plan) { | |
alert('No plan to edit. Generate one first.'); | |
return; | |
} | |
const planData = data.plan; | |
const html = ` | |
<div class="mb-3"> | |
<label class="form-label"><strong>Edit Plan for ${planData.farm_name}</strong></label> | |
</div> | |
<div class="mb-3"> | |
<label for="planSummary" class="form-label">Summary</label> | |
<textarea id="planSummary" class="form-control" rows="3">${planData.summary_text || ''}</textarea> | |
</div> | |
<div class="mb-3"> | |
<label for="planJson" class="form-label">Plan Details (JSON)</label> | |
<textarea id="planJson" class="form-control" rows="10">${JSON.stringify(planData.plan_json, null, 2)}</textarea> | |
</div> | |
`; | |
_showYearlyPlanModal(html, true); | |
// Set up save button handler | |
document.getElementById('saveYearlyPlanBtn').onclick = function() { | |
const summary = document.getElementById('planSummary').value; | |
const jsonText = document.getElementById('planJson').value; | |
let planJson; | |
try { | |
planJson = JSON.parse(jsonText); | |
} catch (err) { | |
alert('Invalid JSON format: ' + err.message); | |
return; | |
} | |
const modal = _getLoadingModalInstance(); | |
modal.show(); | |
fetch(`/farmer/farm/${farmId}/yearly_plan`, { | |
method: 'POST', | |
headers: {'Content-Type': 'application/json'}, | |
body: JSON.stringify({ | |
summary_text: summary, | |
plan_json: planJson | |
}) | |
}) | |
.then(response => response.json()) | |
.then(result => { | |
modal.hide(); | |
if (result.success) { | |
alert('Plan updated successfully!'); | |
bootstrap.Modal.getInstance(document.getElementById('yearlyPlanModal')).hide(); | |
location.reload(); | |
} else { | |
alert('Failed to save: ' + (result.error || 'Unknown error')); | |
} | |
}) | |
.catch(error => { | |
modal.hide(); | |
alert('Error saving plan: ' + error.message); | |
}); | |
}; | |
}) | |
.catch(error => { | |
alert('Error loading plan: ' + error.message); | |
}); | |
} | |
function deleteYearlyPlan(farmId) { | |
if (!confirm('Are you sure you want to delete the yearly plan for this farm?')) { | |
return; | |
} | |
const modal = _getLoadingModalInstance(); | |
modal.show(); | |
fetch(`/farmer/farm/${farmId}/yearly_plan/delete`, {method: 'POST'}) | |
.then(response => response.json()) | |
.then(data => { | |
modal.hide(); | |
if (data.success) { | |
alert('Yearly plan deleted successfully!'); | |
location.reload(); | |
} else { | |
alert('Failed to delete plan: ' + (data.error || 'Unknown error')); | |
} | |
}) | |
.catch(error => { | |
modal.hide(); | |
alert('Error deleting plan: ' + error.message); | |
}); | |
} | |
function deleteFarm(farmId) { | |
if (!confirm('Are you sure you want to delete this farm? This will remove all associated data including activities, advisories, and yearly plans. This action cannot be undone.')) { | |
return; | |
} | |
const modal = _getLoadingModalInstance(); | |
modal.show(); | |
fetch(`/farmer/farm/${farmId}/delete`, {method: 'POST'}) | |
.then(response => response.json()) | |
.then(data => { | |
modal.hide(); | |
if (data.success) { | |
alert('Farm deleted successfully!'); | |
location.reload(); | |
} else { | |
alert('Failed to delete farm: ' + (data.error || 'Unknown error')); | |
} | |
}) | |
.catch(error => { | |
modal.hide(); | |
alert('Error deleting farm: ' + error.message); | |
}); | |
} | |
function generateNewAdvisory(farmId) { | |
const modal = _getLoadingModalInstance(); | |
modal.show(); | |
fetch(`/generate_advisory/${farmId}`) | |
.then(response => { | |
if (!response.ok) { | |
return response.text().then(text => { throw new Error(text || 'Server error'); }); | |
} | |
return response.json(); | |
}) | |
.then(data => { | |
modal.hide(); | |
if (data && data.success) { | |
alert('Daily advisory generated successfully!'); | |
location.reload(); | |
} else { | |
alert('Failed to generate advisory: ' + (data && data.error ? data.error : 'Unknown error')); | |
} | |
}) | |
.catch(error => { | |
modal.hide(); | |
alert('Error generating advisory: ' + (error && error.message ? error.message : String(error))); | |
}); | |
} | |
function sendSMSAdvisory(farmId) { | |
const modal = _getLoadingModalInstance(); | |
modal.show(); | |
fetch(`/send_sms_advisory/${farmId}`) | |
.then(response => { | |
if (!response.ok) { | |
return response.text().then(text => { throw new Error(text || 'Server error'); }); | |
} | |
return response.json(); | |
}) | |
.then(data => { | |
modal.hide(); | |
if (data && data.success) { | |
alert('SMS sent successfully!'); | |
} else { | |
alert('Failed to send SMS: ' + (data && data.error ? data.error : 'Unknown error')); | |
} | |
}) | |
.catch(error => { | |
modal.hide(); | |
alert('Error sending SMS: ' + (error && error.message ? error.message : String(error))); | |
}); | |
} | |
function sendTelegramAdvisory(farmId) { | |
const modal = _getLoadingModalInstance(); | |
modal.show(); | |
fetch(`/send_telegram_advisory/${farmId}`) | |
.then(response => { | |
if (!response.ok) { | |
return response.text().then(text => { throw new Error(text || 'Server error'); }); | |
} | |
return response.json(); | |
}) | |
.then(data => { | |
modal.hide(); | |
if (data && data.success) { | |
alert('Telegram message sent successfully!'); | |
} else { | |
alert('Failed to send Telegram message: ' + (data && data.error ? data.error : 'Unknown error')); | |
} | |
}) | |
.catch(error => { | |
modal.hide(); | |
alert('Error sending Telegram message: ' + (error && error.message ? error.message : String(error))); | |
}); | |
} | |
function checkWeather(farmId) { | |
const modal = _getLoadingModalInstance(); | |
modal.show(); | |
fetch(`/api/weather/${farmId}`) | |
.then(response => { | |
if (!response.ok) { | |
return response.text().then(text => { throw new Error(text || 'Server error'); }); | |
} | |
return response.json(); | |
}) | |
.then(data => { | |
modal.hide(); | |
if (data.error) { | |
alert('Weather data unavailable: ' + data.error); | |
} else { | |
let weatherInfo = `Current Weather:\n`; | |
weatherInfo += `Temperature: ${data.main?.temp || 'N/A'}Β°C\n`; | |
weatherInfo += `Humidity: ${data.main?.humidity || 'N/A'}%\n`; | |
weatherInfo += `Condition: ${data.weather?.[0]?.description || 'N/A'}\n`; | |
weatherInfo += `Wind: ${data.wind?.speed ? (data.wind.speed * 3.6).toFixed(1) : 'N/A'} km/h\n`; | |
alert(weatherInfo); | |
} | |
}) | |
.catch(error => { | |
modal.hide(); | |
alert('Error fetching weather: ' + (error && error.message ? error.message : String(error))); | |
}); | |
} | |
// New feature functions | |
function downloadYearlyPlanPDF(farmId) { | |
const modal = _getLoadingModalInstance(); | |
modal.show(); | |
fetch(`/farmer/farm/${farmId}/yearly_plan/pdf`) | |
.then(response => { | |
modal.hide(); | |
if (response.ok) { | |
// Trigger download | |
const link = document.createElement('a'); | |
link.href = `/farmer/farm/${farmId}/yearly_plan/pdf`; | |
link.download = `yearly_plan_farm_${farmId}.pdf`; | |
document.body.appendChild(link); | |
link.click(); | |
document.body.removeChild(link); | |
} else { | |
alert('Failed to generate PDF. Please ensure you have a yearly plan first.'); | |
} | |
}) | |
.catch(error => { | |
modal.hide(); | |
alert('Error generating PDF: ' + error.message); | |
}); | |
} | |
function sendYearlyPlanTelegram(farmId) { | |
const modal = _getLoadingModalInstance(); | |
modal.show(); | |
fetch(`/farmer/farm/${farmId}/yearly_plan/send_telegram`, { | |
method: 'POST', | |
headers: {'Content-Type': 'application/json'} | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
modal.hide(); | |
if (data.success) { | |
alert('β ' + data.message); | |
} else { | |
alert('β ' + (data.error || 'Failed to send via Telegram')); | |
} | |
}) | |
.catch(error => { | |
modal.hide(); | |
alert('Error sending via Telegram: ' + error.message); | |
}); | |
} | |
function viewWeatherAlerts(farmId) { | |
const modal = _getLoadingModalInstance(); | |
modal.show(); | |
fetch(`/farmer/weather_alerts/${farmId}`) | |
.then(response => response.json()) | |
.then(data => { | |
modal.hide(); | |
if (data.success) { | |
let alertsHtml = '<h5><i class="fas fa-cloud-rain me-2"></i>Weather Alerts</h5>'; | |
if (data.alerts && data.alerts.length > 0) { | |
alertsHtml += '<div class="list-group">'; | |
data.alerts.forEach(alert => { | |
const severityClass = alert.severity === 'high' ? 'danger' : | |
alert.severity === 'medium' ? 'warning' : 'info'; | |
alertsHtml += ` | |
<div class="list-group-item list-group-item-${severityClass}"> | |
<div class="d-flex w-100 justify-content-between"> | |
<h6 class="mb-1">${alert.alert_type}</h6> | |
<small>${new Date(alert.created_at).toLocaleDateString()}</small> | |
</div> | |
<p class="mb-1">${alert.message}</p> | |
<small>Severity: <span class="badge bg-${severityClass}">${alert.severity}</span></small> | |
</div> | |
`; | |
}); | |
alertsHtml += '</div>'; | |
alertsHtml += ` | |
<div class="mt-3"> | |
<button class="btn btn-primary" onclick="enableWeatherAlerts(${farmId})"> | |
<i class="fas fa-bell me-2"></i>Enable Alerts | |
</button> | |
<button class="btn btn-danger" onclick="disableWeatherAlerts(${farmId})"> | |
<i class="fas fa-bell-slash me-2"></i>Disable Alerts | |
</button> | |
</div> | |
`; | |
} else { | |
alertsHtml += '<p class="text-muted">No weather alerts at this time.</p>'; | |
alertsHtml += ` | |
<button class="btn btn-primary" onclick="enableWeatherAlerts(${farmId})"> | |
<i class="fas fa-bell me-2"></i>Enable Weather Alerts | |
</button> | |
`; | |
} | |
_showYearlyPlanModal(alertsHtml); | |
} else { | |
alert('Failed to load weather alerts: ' + (data.error || 'Unknown error')); | |
} | |
}) | |
.catch(error => { | |
modal.hide(); | |
alert('Error loading weather alerts: ' + error.message); | |
}); | |
} | |
function enableWeatherAlerts(farmId) { | |
fetch(`/farmer/weather_alerts/${farmId}`, { | |
method: 'POST', | |
headers: {'Content-Type': 'application/json'}, | |
body: JSON.stringify({action: 'enable'}) | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
alert('Weather alerts enabled successfully!'); | |
viewWeatherAlerts(farmId); // Refresh the view | |
} else { | |
alert('Failed to enable alerts: ' + (data.error || 'Unknown error')); | |
} | |
}) | |
.catch(error => alert('Error: ' + error.message)); | |
} | |
function disableWeatherAlerts(farmId) { | |
fetch(`/farmer/weather_alerts/${farmId}`, { | |
method: 'POST', | |
headers: {'Content-Type': 'application/json'}, | |
body: JSON.stringify({action: 'disable'}) | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
alert('Weather alerts disabled successfully!'); | |
viewWeatherAlerts(farmId); // Refresh the view | |
} else { | |
alert('Failed to disable alerts: ' + (data.error || 'Unknown error')); | |
} | |
}) | |
.catch(error => alert('Error: ' + error.message)); | |
} | |
function viewMarketPrices(farmId) { | |
const modal = _getLoadingModalInstance(); | |
modal.show(); | |
fetch(`/farmer/market_prices`) | |
.then(response => response.json()) | |
.then(data => { | |
modal.hide(); | |
if (data.success) { | |
let pricesHtml = '<h5><i class="fas fa-chart-line me-2"></i>Market Prices</h5>'; | |
if (data.prices && data.prices.length > 0) { | |
pricesHtml += '<div class="table-responsive">'; | |
pricesHtml += '<table class="table table-striped">'; | |
pricesHtml += '<thead><tr><th>Crop</th><th>Market</th><th>Price</th><th>Trend</th><th>Date</th></tr></thead><tbody>'; | |
data.prices.forEach(price => { | |
const trendIcon = price.trend === 'up' ? 'π' : price.trend === 'down' ? 'π' : 'β‘οΈ'; | |
pricesHtml += ` | |
<tr> | |
<td><span class="badge bg-success">${price.crop_type}</span></td> | |
<td>${price.market_name}</td> | |
<td>βΉ${price.price_per_unit}/${price.unit}</td> | |
<td>${trendIcon} ${price.trend}</td> | |
<td>${new Date(price.date).toLocaleDateString()}</td> | |
</tr> | |
`; | |
}); | |
pricesHtml += '</tbody></table></div>'; | |
pricesHtml += ` | |
<div class="mt-3"> | |
<button class="btn btn-primary" onclick="refreshMarketPrices(${farmId})"> | |
<i class="fas fa-sync me-2"></i>Refresh Prices | |
</button> | |
<button class="btn btn-success" onclick="subscribeToMarketAlerts(${farmId})"> | |
<i class="fas fa-bell me-2"></i>Subscribe to Alerts | |
</button> | |
</div> | |
`; | |
} else { | |
pricesHtml += '<p class="text-muted">No market price data available.</p>'; | |
pricesHtml += ` | |
<button class="btn btn-primary" onclick="refreshMarketPrices(${farmId})"> | |
<i class="fas fa-sync me-2"></i>Fetch Market Prices | |
</button> | |
`; | |
} | |
_showYearlyPlanModal(pricesHtml); | |
} else { | |
alert('Failed to load market prices: ' + (data.error || 'Unknown error')); | |
} | |
}) | |
.catch(error => { | |
modal.hide(); | |
alert('Error loading market prices: ' + error.message); | |
}); | |
} | |
function refreshMarketPrices(farmId) { | |
const modal = _getLoadingModalInstance(); | |
modal.show(); | |
fetch(`/farmer/market_prices`, { | |
method: 'POST', | |
headers: {'Content-Type': 'application/json'}, | |
body: JSON.stringify({action: 'refresh'}) | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
modal.hide(); | |
if (data.success) { | |
alert('Market prices updated successfully!'); | |
viewMarketPrices(farmId); // Refresh the view | |
} else { | |
alert('Failed to refresh prices: ' + (data.error || 'Unknown error')); | |
} | |
}) | |
.catch(error => { | |
modal.hide(); | |
alert('Error refreshing prices: ' + error.message); | |
}); | |
} | |
function subscribeToMarketAlerts(farmId) { | |
fetch(`/farmer/market_prices`, { | |
method: 'POST', | |
headers: {'Content-Type': 'application/json'}, | |
body: JSON.stringify({action: 'subscribe'}) | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
alert('Subscribed to market price alerts successfully!'); | |
} else { | |
alert('Failed to subscribe: ' + (data.error || 'Unknown error')); | |
} | |
}) | |
.catch(error => alert('Error: ' + error.message)); | |
} | |
function detectDisease(farmId) { | |
const modal = _getLoadingModalInstance(); | |
modal.show(); | |
fetch(`/farmer/disease_detection/${farmId}`) | |
.then(response => response.json()) | |
.then(data => { | |
modal.hide(); | |
if (data.success) { | |
let diseaseHtml = '<h5><i class="fas fa-bug me-2"></i>Disease Detection</h5>'; | |
if (data.detections && data.detections.length > 0) { | |
diseaseHtml += '<div class="row">'; | |
data.detections.forEach(detection => { | |
const severityClass = detection.severity === 'high' ? 'danger' : | |
detection.severity === 'medium' ? 'warning' : 'success'; | |
diseaseHtml += ` | |
<div class="col-md-6 mb-3"> | |
<div class="card border-${severityClass}"> | |
<div class="card-header bg-${severityClass} text-white"> | |
<h6 class="mb-0">${detection.disease_name}</h6> | |
</div> | |
<div class="card-body"> | |
<p><strong>Confidence:</strong> ${Math.round(detection.confidence * 100)}%</p> | |
<p><strong>Severity:</strong> <span class="badge bg-${severityClass}">${detection.severity}</span></p> | |
<p><strong>Treatment:</strong> ${detection.treatment_recommendation}</p> | |
<small class="text-muted">Detected: ${new Date(detection.detection_date).toLocaleDateString()}</small> | |
</div> | |
</div> | |
</div> | |
`; | |
}); | |
diseaseHtml += '</div>'; | |
} else { | |
diseaseHtml += '<p class="text-muted">No disease detections recorded.</p>'; | |
} | |
diseaseHtml += ` | |
<div class="mt-3"> | |
<label for="diseaseImageUpload" class="btn btn-primary"> | |
<i class="fas fa-camera me-2"></i>Upload Crop Image | |
</label> | |
<input type="file" id="diseaseImageUpload" accept="image/*" style="display: none;" onchange="uploadDiseaseImage(${farmId}, this)"> | |
<button class="btn btn-success ms-2" onclick="viewDiseaseHistory(${farmId})"> | |
<i class="fas fa-history me-2"></i>View History | |
</button> | |
</div> | |
`; | |
_showYearlyPlanModal(diseaseHtml); | |
} else { | |
alert('Failed to load disease detection data: ' + (data.error || 'Unknown error')); | |
} | |
}) | |
.catch(error => { | |
modal.hide(); | |
alert('Error loading disease detection: ' + error.message); | |
}); | |
} | |
function uploadDiseaseImage(farmId, input) { | |
if (input.files && input.files[0]) { | |
const formData = new FormData(); | |
formData.append('image', input.files[0]); | |
const modal = _getLoadingModalInstance(); | |
modal.show(); | |
fetch(`/farmer/disease_detection/${farmId}`, { | |
method: 'POST', | |
body: formData | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
modal.hide(); | |
if (data.success) { | |
alert('Image uploaded and analyzed successfully!'); | |
detectDisease(farmId); // Refresh the view | |
} else { | |
alert('Failed to analyze image: ' + (data.error || 'Unknown error')); | |
} | |
}) | |
.catch(error => { | |
modal.hide(); | |
alert('Error uploading image: ' + error.message); | |
}); | |
} | |
} | |
function viewDiseaseHistory(farmId) { | |
window.open(`/farmer/disease_detection/${farmId}`, '_blank'); | |
} | |
function showSendImageModal() { | |
const modalHtml = ` | |
<div class="modal fade" id="sendImageModal" tabindex="-1"> | |
<div class="modal-dialog"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title"><i class="fas fa-paper-plane me-2"></i>Send Image via Telegram</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> | |
</div> | |
<div class="modal-body"> | |
<form id="sendImageForm" enctype="multipart/form-data"> | |
<div class="mb-3"> | |
<label for="imageFile" class="form-label">Select Image:</label> | |
<input type="file" class="form-control" id="imageFile" name="image" accept="image/*" required> | |
<div class="form-text">Supported formats: JPG, PNG, GIF, BMP</div> | |
</div> | |
<div class="mb-3"> | |
<label for="imageCaption" class="form-label">Caption (optional):</label> | |
<textarea class="form-control" id="imageCaption" name="caption" rows="3" placeholder="Add a description for your image..."></textarea> | |
</div> | |
</form> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> | |
<button type="button" class="btn btn-primary" onclick="sendImageTelegram()"> | |
<i class="fas fa-paper-plane me-2"></i>Send via Telegram | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
`; | |
// Remove existing modal if any | |
const existingModal = document.getElementById('sendImageModal'); | |
if (existingModal) { | |
existingModal.remove(); | |
} | |
// Add modal to body | |
document.body.insertAdjacentHTML('beforeend', modalHtml); | |
// Show modal | |
const modal = new bootstrap.Modal(document.getElementById('sendImageModal')); | |
modal.show(); | |
} | |
function sendImageTelegram() { | |
const form = document.getElementById('sendImageForm'); | |
const fileInput = document.getElementById('imageFile'); | |
const captionInput = document.getElementById('imageCaption'); | |
if (!fileInput.files[0]) { | |
alert('Please select an image file'); | |
return; | |
} | |
const formData = new FormData(); | |
formData.append('image', fileInput.files[0]); | |
formData.append('caption', captionInput.value || `π· Image from {{ current_user.name if current_user else 'Farmer' }}`); | |
const modal = bootstrap.Modal.getInstance(document.getElementById('sendImageModal')); | |
modal.hide(); | |
const loadingModal = _getLoadingModalInstance(); | |
loadingModal.show(); | |
fetch('/farmer/send_image_telegram', { | |
method: 'POST', | |
body: formData | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
loadingModal.hide(); | |
if (data.success) { | |
alert('β ' + data.message); | |
} else { | |
alert('β ' + (data.error || 'Failed to send image via Telegram')); | |
} | |
}) | |
.catch(error => { | |
loadingModal.hide(); | |
alert('Error sending image: ' + error.message); | |
}); | |
} | |
// ==================== DAILY TASKS FUNCTIONS ==================== | |
function loadDailyTasks(date = null) { | |
const targetDate = date || new Date().toISOString().split('T')[0]; | |
const loadingSpinner = '<div class="text-center py-4"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div><p class="mt-2">Loading daily tasks...</p></div>'; | |
document.getElementById('daily-tasks-container').innerHTML = loadingSpinner; | |
document.getElementById('task-date').textContent = targetDate; | |
fetch(`/farmer/daily_tasks?date=${targetDate}`) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
renderDailyTasks(data.tasks); | |
} else { | |
showNoTasksMessage('Failed to load tasks: ' + (data.error || 'Unknown error')); | |
} | |
}) | |
.catch(error => { | |
console.error('Error loading daily tasks:', error); | |
showNoTasksMessage('Error loading tasks. Please try again.'); | |
}); | |
} | |
function renderDailyTasks(tasks) { | |
const container = document.getElementById('daily-tasks-container'); | |
if (!tasks || tasks.length === 0) { | |
showNoTasksMessage(); | |
return; | |
} | |
let html = '<div class="row">'; | |
tasks.forEach((task, index) => { | |
const priorityClass = task.priority === 'high' ? 'danger' : task.priority === 'medium' ? 'warning' : 'success'; | |
const priorityIcon = task.priority === 'high' ? 'π΄' : task.priority === 'medium' ? 'π‘' : 'π’'; | |
const completedClass = task.is_completed ? 'border-success bg-light' : ''; | |
const completedIcon = task.is_completed ? 'β ' : 'β³'; | |
html += ` | |
<div class="col-md-6 col-lg-4 mb-3"> | |
<div class="card h-100 ${completedClass}"> | |
<div class="card-header d-flex justify-content-between align-items-center"> | |
<small class="text-${priorityClass}"> | |
${priorityIcon} ${task.priority.toUpperCase()} Priority | |
</small> | |
<small>${completedIcon} ${task.estimated_duration} min</small> | |
</div> | |
<div class="card-body"> | |
<h6 class="card-title">${task.task_title}</h6> | |
<p class="card-text small">${task.task_description}</p> | |
${task.crop_specific ? `<span class="badge bg-info mb-2">πΎ ${task.crop_specific}</span>` : ''} | |
${task.weather_dependent ? '<span class="badge bg-warning mb-2">π€οΈ Weather Dependent</span>' : ''} | |
</div> | |
<div class="card-footer bg-transparent"> | |
<div class="d-grid gap-1"> | |
${task.is_completed ? ` | |
<button class="btn btn-outline-secondary btn-sm" onclick="uncompleteTask(${task.id})"> | |
<i class="fas fa-undo me-1"></i>Mark as Incomplete | |
</button> | |
${task.rating ? `<small class="text-muted">Rating: ${'β'.repeat(task.rating)}</small>` : ''} | |
` : ` | |
<button class="btn btn-success btn-sm" onclick="completeTask(${task.id})"> | |
<i class="fas fa-check me-1"></i>Mark as Complete | |
</button> | |
`} | |
</div> | |
</div> | |
</div> | |
</div>`; | |
}); | |
html += '</div>'; | |
container.innerHTML = html; | |
} | |
function showNoTasksMessage(message = null) { | |
const defaultMessage = ` | |
<div class="text-center text-muted py-4" id="no-tasks-message"> | |
<i class="fas fa-clipboard-list fa-3x mb-3"></i> | |
<h6>No daily tasks available</h6> | |
<p>Generate daily tasks to get AI-powered farming recommendations</p> | |
</div>`; | |
const errorMessage = ` | |
<div class="text-center text-danger py-4"> | |
<i class="fas fa-exclamation-triangle fa-3x mb-3"></i> | |
<h6>Error Loading Tasks</h6> | |
<p>${message}</p> | |
</div>`; | |
document.getElementById('daily-tasks-container').innerHTML = message ? errorMessage : defaultMessage; | |
} | |
function generateDailyTasks() { | |
const today = new Date().toISOString().split('T')[0]; | |
const loadingSpinner = '<div class="text-center py-4"><div class="spinner-border text-success" role="status"><span class="visually-hidden">Generating...</span></div><p class="mt-2">Generating daily tasks with AI...</p></div>'; | |
document.getElementById('daily-tasks-container').innerHTML = loadingSpinner; | |
fetch('/farmer/daily_tasks/generate', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
date: today | |
}) | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
alert('β ' + data.message); | |
loadDailyTasks(); // Reload tasks to show the generated ones | |
} else { | |
alert('β ' + (data.error || 'Failed to generate daily tasks')); | |
showNoTasksMessage(); | |
} | |
}) | |
.catch(error => { | |
console.error('Error generating daily tasks:', error); | |
alert('Error generating daily tasks. Please try again.'); | |
showNoTasksMessage(); | |
}); | |
} | |
function completeTask(taskId) { | |
// Show rating modal | |
const rating = prompt('Rate this task completion (1-5 stars):'); | |
const feedback = prompt('Any feedback about this task? (optional):'); | |
let ratingNum = null; | |
if (rating && !isNaN(rating)) { | |
ratingNum = Math.max(1, Math.min(5, parseInt(rating))); | |
} | |
fetch(`/farmer/daily_tasks/${taskId}/complete`, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
rating: ratingNum, | |
feedback: feedback || '' | |
}) | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
alert('β Task completed successfully!'); | |
loadDailyTasks(); // Reload tasks to update status | |
} else { | |
alert('β ' + (data.error || 'Failed to complete task')); | |
} | |
}) | |
.catch(error => { | |
console.error('Error completing task:', error); | |
alert('Error completing task. Please try again.'); | |
}); | |
} | |
function uncompleteTask(taskId) { | |
if (!confirm('Are you sure you want to mark this task as incomplete?')) { | |
return; | |
} | |
fetch(`/farmer/daily_tasks/${taskId}/uncomplete`, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
} | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
alert('β Task marked as incomplete'); | |
loadDailyTasks(); // Reload tasks to update status | |
} else { | |
alert('β ' + (data.error || 'Failed to uncomplete task')); | |
} | |
}) | |
.catch(error => { | |
console.error('Error uncompleting task:', error); | |
alert('Error updating task. Please try again.'); | |
}); | |
} | |
function sendTasksTelegram() { | |
const today = new Date().toISOString().split('T')[0]; | |
fetch('/farmer/daily_tasks/send_telegram', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
date: today | |
}) | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
alert('β ' + data.message); | |
} else { | |
alert('β ' + (data.error || 'Failed to send tasks via Telegram')); | |
} | |
}) | |
.catch(error => { | |
console.error('Error sending tasks via Telegram:', error); | |
alert('Error sending tasks. Please try again.'); | |
}); | |
} | |
function deleteAllTasks() { | |
if (!confirm('Are you sure you want to delete all tasks for today? This action cannot be undone.')) { | |
return; | |
} | |
const today = new Date().toISOString().split('T')[0]; | |
fetch('/farmer/daily_tasks/delete_all', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
}, | |
body: JSON.stringify({ | |
date: today | |
}) | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.success) { | |
alert('β ' + data.message); | |
showNoTasksMessage(); // Show the no tasks message | |
} else { | |
alert('β ' + (data.error || 'Failed to delete tasks')); | |
} | |
}) | |
.catch(error => { | |
console.error('Error deleting tasks:', error); | |
alert('Error deleting tasks. Please try again.'); | |
}); | |
} | |
// Auto-load today's tasks when page loads | |
document.addEventListener('DOMContentLoaded', function() { | |
// Load today's tasks automatically | |
loadDailyTasks(); | |
}); | |
</script> | |
</script> | |
{% endblock %} | |