Spaces:
Sleeping
Sleeping
Upload 56 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env +43 -0
- .gitattributes +1 -0
- __pycache__/app.cpython-39.pyc +0 -0
- __pycache__/daily_task_service.cpython-39.pyc +0 -0
- __pycache__/disease_detection_service.cpython-39.pyc +0 -0
- __pycache__/gemini_service.cpython-39.pyc +0 -0
- __pycache__/market_price_service.cpython-39.pyc +0 -0
- __pycache__/models.cpython-39.pyc +0 -0
- __pycache__/pdf_generator_service.cpython-39.pyc +0 -0
- __pycache__/telegram_service.cpython-39.pyc +0 -0
- __pycache__/twilio_service.cpython-39.pyc +0 -0
- __pycache__/weather_alert_service.cpython-39.pyc +0 -0
- __pycache__/weather_service.cpython-39.pyc +0 -0
- app.py +0 -0
- daily_task_service.py +865 -0
- disease_detection_service.py +356 -0
- farm_management.db +0 -0
- farms.db +0 -0
- gemini_service.py +382 -0
- generated_pdfs/yearly_plan_PRANIT Ravindra CHILBULE_20250906_140900.html +120 -0
- generated_pdfs/yearly_plan_PRANIT Ravindra CHILBULE_20250906_142024.html +120 -0
- generated_pdfs/yearly_plan_PRANIT Ravindra CHILBULE_20250906_142505.html +120 -0
- generated_pdfs/yearly_plan_PRANIT Ravindra CHILBULE_20250906_172353.html +120 -0
- generated_pdfs/yearly_plan_PRANIT Ravindra CHILBULE_20250906_172354.html +120 -0
- generated_pdfs/yearly_plan_PRANIT Ravindra CHILBULE_20250906_172448.html +120 -0
- generated_pdfs/yearly_plan_PRANIT Ravindra CHILBULE_20250906_172530.html +120 -0
- instance/farm_management.db +3 -0
- instance/farms.db +0 -0
- market_price_service.py +329 -0
- migrate_db.py +314 -0
- models.py +775 -0
- pdf_generator_service.py +428 -0
- requirements.txt +13 -0
- scripts/__pycache__/register_telegram_chat.cpython-39.pyc +0 -0
- scripts/get_telegram_chat_id.py +44 -0
- scripts/register_telegram_chat.py +71 -0
- telegram_service.py +645 -0
- templates/add_farm.html +630 -0
- templates/admin_dashboard.html +360 -0
- templates/admin_farmers.html +293 -0
- templates/admin_login.html +52 -0
- templates/admin_sms_logs.html +361 -0
- templates/base.html +157 -0
- templates/edit_farm.html +422 -0
- templates/error.html +44 -0
- templates/farm_details.html +252 -0
- templates/farmer_dashboard.html +1348 -0
- templates/farmer_dashboard_fixed.html +311 -0
- templates/farmer_login.html +60 -0
- templates/farmer_register.html +131 -0
.env
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Farm Management Portal Environment Variables
|
| 2 |
+
|
| 3 |
+
# Flask Configuration
|
| 4 |
+
SECRET_KEY=your-secret-key-change-this-in-production
|
| 5 |
+
FLASK_ENV=development
|
| 6 |
+
|
| 7 |
+
# Database Configuration
|
| 8 |
+
DATABASE_URL=sqlite:///farm_management.db
|
| 9 |
+
|
| 10 |
+
# Gemini AI Configuration
|
| 11 |
+
GEMINI_API_KEY=AIzaSyDlESeCJKKyvH1y7xTfjRNaqtQiorgFxjw
|
| 12 |
+
|
| 13 |
+
# Twilio Configuration
|
| 14 |
+
TWILIO_ACCOUNT_SID=ACe45f7038c5338a153d1126ca6d547c84
|
| 15 |
+
TWILIO_AUTH_TOKEN=48b9eea898885ef395d48edc74924340
|
| 16 |
+
TWILIO_PHONE_NUMBER=+17627287857
|
| 17 |
+
TWILIO_MESSAGING_SERVICE_SID=MG9be309a19ba005c801f36d56db5fe3ae
|
| 18 |
+
|
| 19 |
+
# Telegram Bot Configuration
|
| 20 |
+
TELEGRAM_BOT_TOKEN=8448417664:AAEcBQ8QBas8gdMyDNlTdO0s4YDhTLSrtO8
|
| 21 |
+
|
| 22 |
+
# Optional: quick mapping of farmer chat IDs for admin convenience.
|
| 23 |
+
# Format: FARMER_TELEGRAM_CHAT_IDS=<farmer_db_id>:<chat_id>,<farmer_db_id2>:<chat_id2>
|
| 24 |
+
# Example: FARMER_TELEGRAM_CHAT_IDS=3:5397241102,5:987654321
|
| 25 |
+
FARMER_TELEGRAM_CHAT_IDS=3:5397241102
|
| 26 |
+
|
| 27 |
+
# Optional global fallback chat id: if a farmer has no chat id configured the app will send messages to this id.
|
| 28 |
+
# Use carefully - messages will go to one global recipient.
|
| 29 |
+
GLOBAL_TELEGRAM_CHAT_ID=5397241102
|
| 30 |
+
|
| 31 |
+
# Weather API Configuration
|
| 32 |
+
OPENWEATHER_API_KEY=8ed5800e5a71e3fe1751e757a1d4e986
|
| 33 |
+
WEATHER_API_KEY=8ed5800e5a71e3fe1751e757a1d4e986
|
| 34 |
+
|
| 35 |
+
# Email Configuration (Optional)
|
| 36 |
+
MAIL_SERVER=smtp.gmail.com
|
| 37 |
+
MAIL_PORT=587
|
| 38 |
+
MAIL_USE_TLS=True
|
| 39 |
+
MAIL_USERNAME=your-email@gmail.com
|
| 40 |
+
MAIL_PASSWORD=your-email-password
|
| 41 |
+
|
| 42 |
+
# Redis Configuration (Optional)
|
| 43 |
+
REDIS_URL=redis://localhost:6379/0
|
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
instance/farm_management.db filter=lfs diff=lfs merge=lfs -text
|
__pycache__/app.cpython-39.pyc
ADDED
|
Binary file (37.9 kB). View file
|
|
|
__pycache__/daily_task_service.cpython-39.pyc
ADDED
|
Binary file (19 kB). View file
|
|
|
__pycache__/disease_detection_service.cpython-39.pyc
ADDED
|
Binary file (10.4 kB). View file
|
|
|
__pycache__/gemini_service.cpython-39.pyc
ADDED
|
Binary file (16.3 kB). View file
|
|
|
__pycache__/market_price_service.cpython-39.pyc
ADDED
|
Binary file (10.3 kB). View file
|
|
|
__pycache__/models.cpython-39.pyc
ADDED
|
Binary file (24.2 kB). View file
|
|
|
__pycache__/pdf_generator_service.cpython-39.pyc
ADDED
|
Binary file (12.7 kB). View file
|
|
|
__pycache__/telegram_service.cpython-39.pyc
ADDED
|
Binary file (20.5 kB). View file
|
|
|
__pycache__/twilio_service.cpython-39.pyc
ADDED
|
Binary file (5.41 kB). View file
|
|
|
__pycache__/weather_alert_service.cpython-39.pyc
ADDED
|
Binary file (8.87 kB). View file
|
|
|
__pycache__/weather_service.cpython-39.pyc
ADDED
|
Binary file (11.1 kB). View file
|
|
|
app.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
daily_task_service.py
ADDED
|
@@ -0,0 +1,865 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Daily Task Management Service
|
| 3 |
+
|
| 4 |
+
This service generates daily farming tasks based on:
|
| 5 |
+
- Seasonal farming calendar
|
| 6 |
+
- Weather conditions
|
| 7 |
+
- Crop growth stages
|
| 8 |
+
- Soil conditions
|
| 9 |
+
- Previous task completion history
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import logging
|
| 13 |
+
from datetime import datetime, date, timedelta
|
| 14 |
+
from typing import List, Dict, Optional
|
| 15 |
+
import json
|
| 16 |
+
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
class DailyTaskService:
|
| 20 |
+
"""Service for generating and managing daily farming tasks"""
|
| 21 |
+
|
| 22 |
+
def __init__(self, gemini_service=None, weather_service=None):
|
| 23 |
+
self.gemini_service = gemini_service
|
| 24 |
+
self.weather_service = weather_service
|
| 25 |
+
|
| 26 |
+
# Predefined task templates based on seasons and farming activities
|
| 27 |
+
self.task_templates = self._initialize_task_templates()
|
| 28 |
+
|
| 29 |
+
def _initialize_task_templates(self) -> Dict:
|
| 30 |
+
"""Initialize predefined task templates for different farming activities"""
|
| 31 |
+
|
| 32 |
+
return {
|
| 33 |
+
# ==================== CROP FARMING TASKS ====================
|
| 34 |
+
'irrigation': {
|
| 35 |
+
'morning': {
|
| 36 |
+
'title': 'Morning Irrigation Check',
|
| 37 |
+
'description': 'Check soil moisture levels and irrigate crops if needed. Focus on areas that dried out overnight.',
|
| 38 |
+
'duration': 30,
|
| 39 |
+
'priority': 'high',
|
| 40 |
+
'weather_dependent': True,
|
| 41 |
+
'farm_types': ['crop', 'mixed']
|
| 42 |
+
},
|
| 43 |
+
'evening': {
|
| 44 |
+
'title': 'Evening Irrigation',
|
| 45 |
+
'description': 'Water crops in the evening to minimize water loss due to evaporation.',
|
| 46 |
+
'duration': 45,
|
| 47 |
+
'priority': 'medium',
|
| 48 |
+
'weather_dependent': True,
|
| 49 |
+
'farm_types': ['crop', 'mixed']
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
'fertilizing': {
|
| 53 |
+
'organic': {
|
| 54 |
+
'title': 'Apply Organic Fertilizer',
|
| 55 |
+
'description': 'Apply compost or organic manure to crops. Focus on base application around plant roots.',
|
| 56 |
+
'duration': 60,
|
| 57 |
+
'priority': 'high',
|
| 58 |
+
'weather_dependent': False,
|
| 59 |
+
'farm_types': ['crop', 'mixed']
|
| 60 |
+
},
|
| 61 |
+
'chemical': {
|
| 62 |
+
'title': 'Chemical Fertilizer Application',
|
| 63 |
+
'description': 'Apply chemical fertilizers as per soil test recommendations. Ensure proper timing and quantities.',
|
| 64 |
+
'duration': 45,
|
| 65 |
+
'priority': 'high',
|
| 66 |
+
'weather_dependent': True,
|
| 67 |
+
'farm_types': ['crop', 'mixed']
|
| 68 |
+
}
|
| 69 |
+
},
|
| 70 |
+
'pest_control': {
|
| 71 |
+
'inspection': {
|
| 72 |
+
'title': 'Pest and Disease Inspection',
|
| 73 |
+
'description': 'Inspect crops for signs of pests, diseases, or nutrient deficiencies. Take photos if issues found.',
|
| 74 |
+
'duration': 30,
|
| 75 |
+
'priority': 'high',
|
| 76 |
+
'weather_dependent': False,
|
| 77 |
+
'farm_types': ['crop', 'mixed']
|
| 78 |
+
},
|
| 79 |
+
'spraying': {
|
| 80 |
+
'title': 'Pest Control Spraying',
|
| 81 |
+
'description': 'Apply pesticides or bio-pesticides as needed. Avoid spraying during windy or rainy conditions.',
|
| 82 |
+
'duration': 60,
|
| 83 |
+
'priority': 'medium',
|
| 84 |
+
'weather_dependent': True,
|
| 85 |
+
'farm_types': ['crop', 'mixed']
|
| 86 |
+
}
|
| 87 |
+
},
|
| 88 |
+
'weeding': {
|
| 89 |
+
'manual': {
|
| 90 |
+
'title': 'Manual Weeding',
|
| 91 |
+
'description': 'Remove weeds manually from crop rows. Focus on areas around young plants.',
|
| 92 |
+
'duration': 90,
|
| 93 |
+
'priority': 'medium',
|
| 94 |
+
'weather_dependent': False,
|
| 95 |
+
'farm_types': ['crop', 'mixed']
|
| 96 |
+
},
|
| 97 |
+
'mechanical': {
|
| 98 |
+
'title': 'Mechanical Weeding',
|
| 99 |
+
'description': 'Use hoe or cultivator to remove weeds between crop rows.',
|
| 100 |
+
'duration': 60,
|
| 101 |
+
'priority': 'medium',
|
| 102 |
+
'weather_dependent': False,
|
| 103 |
+
'farm_types': ['crop', 'mixed']
|
| 104 |
+
}
|
| 105 |
+
},
|
| 106 |
+
|
| 107 |
+
# ==================== DAIRY FARMING TASKS ====================
|
| 108 |
+
'milking': {
|
| 109 |
+
'morning': {
|
| 110 |
+
'title': 'Morning Milking',
|
| 111 |
+
'description': 'Milk cows/buffaloes in the morning. Ensure clean milking equipment and proper hygiene.',
|
| 112 |
+
'duration': 90,
|
| 113 |
+
'priority': 'high',
|
| 114 |
+
'weather_dependent': False,
|
| 115 |
+
'farm_types': ['dairy', 'mixed']
|
| 116 |
+
},
|
| 117 |
+
'evening': {
|
| 118 |
+
'title': 'Evening Milking',
|
| 119 |
+
'description': 'Evening milking session. Clean udders and equipment before milking.',
|
| 120 |
+
'duration': 90,
|
| 121 |
+
'priority': 'high',
|
| 122 |
+
'weather_dependent': False,
|
| 123 |
+
'farm_types': ['dairy', 'mixed']
|
| 124 |
+
}
|
| 125 |
+
},
|
| 126 |
+
'dairy_feeding': {
|
| 127 |
+
'concentrate': {
|
| 128 |
+
'title': 'Provide Concentrate Feed',
|
| 129 |
+
'description': 'Give concentrate feed to dairy animals. Adjust quantity based on milk production.',
|
| 130 |
+
'duration': 45,
|
| 131 |
+
'priority': 'high',
|
| 132 |
+
'weather_dependent': False,
|
| 133 |
+
'farm_types': ['dairy', 'livestock', 'mixed']
|
| 134 |
+
},
|
| 135 |
+
'fodder': {
|
| 136 |
+
'title': 'Fodder Distribution',
|
| 137 |
+
'description': 'Distribute fresh green fodder or dry fodder to dairy animals.',
|
| 138 |
+
'duration': 60,
|
| 139 |
+
'priority': 'high',
|
| 140 |
+
'weather_dependent': False,
|
| 141 |
+
'farm_types': ['dairy', 'livestock', 'mixed']
|
| 142 |
+
}
|
| 143 |
+
},
|
| 144 |
+
'dairy_health': {
|
| 145 |
+
'health_check': {
|
| 146 |
+
'title': 'Daily Health Monitoring',
|
| 147 |
+
'description': 'Check animals for signs of illness, lameness, or distress. Monitor body temperature if needed.',
|
| 148 |
+
'duration': 30,
|
| 149 |
+
'priority': 'high',
|
| 150 |
+
'weather_dependent': False,
|
| 151 |
+
'farm_types': ['dairy', 'livestock', 'poultry', 'mixed']
|
| 152 |
+
},
|
| 153 |
+
'udder_care': {
|
| 154 |
+
'title': 'Udder Health Check',
|
| 155 |
+
'description': 'Inspect udders for mastitis signs. Apply udder care products if necessary.',
|
| 156 |
+
'duration': 20,
|
| 157 |
+
'priority': 'medium',
|
| 158 |
+
'weather_dependent': False,
|
| 159 |
+
'farm_types': ['dairy', 'mixed']
|
| 160 |
+
}
|
| 161 |
+
},
|
| 162 |
+
|
| 163 |
+
# ==================== POULTRY FARMING TASKS ====================
|
| 164 |
+
'poultry_feeding': {
|
| 165 |
+
'morning_feed': {
|
| 166 |
+
'title': 'Morning Poultry Feeding',
|
| 167 |
+
'description': 'Provide morning feed to chickens/birds. Check feed quality and quantity.',
|
| 168 |
+
'duration': 30,
|
| 169 |
+
'priority': 'high',
|
| 170 |
+
'weather_dependent': False,
|
| 171 |
+
'farm_types': ['poultry', 'mixed']
|
| 172 |
+
},
|
| 173 |
+
'evening_feed': {
|
| 174 |
+
'title': 'Evening Poultry Feeding',
|
| 175 |
+
'description': 'Give evening feed to poultry. Ensure clean water supply.',
|
| 176 |
+
'duration': 30,
|
| 177 |
+
'priority': 'high',
|
| 178 |
+
'weather_dependent': False,
|
| 179 |
+
'farm_types': ['poultry', 'mixed']
|
| 180 |
+
}
|
| 181 |
+
},
|
| 182 |
+
'egg_collection': {
|
| 183 |
+
'daily': {
|
| 184 |
+
'title': 'Egg Collection',
|
| 185 |
+
'description': 'Collect eggs from nesting boxes. Handle carefully and store properly.',
|
| 186 |
+
'duration': 20,
|
| 187 |
+
'priority': 'high',
|
| 188 |
+
'weather_dependent': False,
|
| 189 |
+
'farm_types': ['poultry', 'mixed']
|
| 190 |
+
}
|
| 191 |
+
},
|
| 192 |
+
'poultry_health': {
|
| 193 |
+
'flock_monitoring': {
|
| 194 |
+
'title': 'Flock Health Monitoring',
|
| 195 |
+
'description': 'Observe bird behavior, appetite, and signs of disease. Remove sick birds if necessary.',
|
| 196 |
+
'duration': 25,
|
| 197 |
+
'priority': 'high',
|
| 198 |
+
'weather_dependent': False,
|
| 199 |
+
'farm_types': ['poultry', 'mixed']
|
| 200 |
+
},
|
| 201 |
+
'coop_cleaning': {
|
| 202 |
+
'title': 'Coop Cleaning',
|
| 203 |
+
'description': 'Clean poultry house, remove droppings, and ensure proper ventilation.',
|
| 204 |
+
'duration': 45,
|
| 205 |
+
'priority': 'medium',
|
| 206 |
+
'weather_dependent': False,
|
| 207 |
+
'farm_types': ['poultry', 'mixed']
|
| 208 |
+
}
|
| 209 |
+
},
|
| 210 |
+
|
| 211 |
+
# ==================== LIVESTOCK FARMING TASKS ====================
|
| 212 |
+
'livestock_feeding': {
|
| 213 |
+
'grazing': {
|
| 214 |
+
'title': 'Livestock Grazing Management',
|
| 215 |
+
'description': 'Move livestock to fresh grazing areas. Monitor grass quality and quantity.',
|
| 216 |
+
'duration': 60,
|
| 217 |
+
'priority': 'medium',
|
| 218 |
+
'weather_dependent': True,
|
| 219 |
+
'farm_types': ['livestock', 'mixed']
|
| 220 |
+
},
|
| 221 |
+
'supplemental': {
|
| 222 |
+
'title': 'Supplemental Feeding',
|
| 223 |
+
'description': 'Provide additional feed supplements to livestock. Check mineral salt availability.',
|
| 224 |
+
'duration': 45,
|
| 225 |
+
'priority': 'medium',
|
| 226 |
+
'weather_dependent': False,
|
| 227 |
+
'farm_types': ['livestock', 'mixed']
|
| 228 |
+
}
|
| 229 |
+
},
|
| 230 |
+
'livestock_care': {
|
| 231 |
+
'water_supply': {
|
| 232 |
+
'title': 'Water Supply Check',
|
| 233 |
+
'description': 'Ensure clean drinking water is available for all animals. Clean water troughs.',
|
| 234 |
+
'duration': 20,
|
| 235 |
+
'priority': 'high',
|
| 236 |
+
'weather_dependent': False,
|
| 237 |
+
'farm_types': ['dairy', 'livestock', 'poultry', 'mixed']
|
| 238 |
+
},
|
| 239 |
+
'shelter_maintenance': {
|
| 240 |
+
'title': 'Shelter Maintenance',
|
| 241 |
+
'description': 'Check and maintain animal shelters. Repair any damage and ensure proper ventilation.',
|
| 242 |
+
'duration': 60,
|
| 243 |
+
'priority': 'low',
|
| 244 |
+
'weather_dependent': False,
|
| 245 |
+
'farm_types': ['dairy', 'livestock', 'poultry', 'mixed']
|
| 246 |
+
}
|
| 247 |
+
},
|
| 248 |
+
|
| 249 |
+
# ==================== FISHERY TASKS ====================
|
| 250 |
+
'fishery': {
|
| 251 |
+
'water_quality': {
|
| 252 |
+
'title': 'Water Quality Monitoring',
|
| 253 |
+
'description': 'Check pond water quality, pH levels, and oxygen content. Monitor fish behavior.',
|
| 254 |
+
'duration': 30,
|
| 255 |
+
'priority': 'high',
|
| 256 |
+
'weather_dependent': False,
|
| 257 |
+
'farm_types': ['fishery', 'mixed']
|
| 258 |
+
},
|
| 259 |
+
'fish_feeding': {
|
| 260 |
+
'title': 'Fish Feeding',
|
| 261 |
+
'description': 'Provide appropriate feed to fish. Adjust quantity based on fish size and water temperature.',
|
| 262 |
+
'duration': 20,
|
| 263 |
+
'priority': 'high',
|
| 264 |
+
'weather_dependent': False,
|
| 265 |
+
'farm_types': ['fishery', 'mixed']
|
| 266 |
+
},
|
| 267 |
+
'pond_maintenance': {
|
| 268 |
+
'title': 'Pond Maintenance',
|
| 269 |
+
'description': 'Clean pond surroundings, check water level, and maintain aeration systems.',
|
| 270 |
+
'duration': 45,
|
| 271 |
+
'priority': 'medium',
|
| 272 |
+
'weather_dependent': False,
|
| 273 |
+
'farm_types': ['fishery', 'mixed']
|
| 274 |
+
}
|
| 275 |
+
},
|
| 276 |
+
|
| 277 |
+
# ==================== COMMON MAINTENANCE TASKS ====================
|
| 278 |
+
'maintenance': {
|
| 279 |
+
'equipment': {
|
| 280 |
+
'title': 'Equipment Maintenance',
|
| 281 |
+
'description': 'Check and maintain farming equipment. Clean, oil, and repair as needed.',
|
| 282 |
+
'duration': 60,
|
| 283 |
+
'priority': 'low',
|
| 284 |
+
'weather_dependent': False,
|
| 285 |
+
'farm_types': ['crop', 'dairy', 'livestock', 'poultry', 'fishery', 'mixed']
|
| 286 |
+
},
|
| 287 |
+
'infrastructure': {
|
| 288 |
+
'title': 'Farm Infrastructure Check',
|
| 289 |
+
'description': 'Inspect farm infrastructure including fencing, storage, and water systems.',
|
| 290 |
+
'duration': 45,
|
| 291 |
+
'priority': 'medium',
|
| 292 |
+
'weather_dependent': False,
|
| 293 |
+
'farm_types': ['crop', 'dairy', 'livestock', 'poultry', 'fishery', 'mixed']
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
def generate_daily_tasks(self, farmer, target_date: date = None) -> List[Dict]:
|
| 299 |
+
"""
|
| 300 |
+
Generate daily tasks for a farmer based on various factors
|
| 301 |
+
|
| 302 |
+
Args:
|
| 303 |
+
farmer: Farmer object
|
| 304 |
+
target_date: Date to generate tasks for (default: today)
|
| 305 |
+
|
| 306 |
+
Returns:
|
| 307 |
+
List of task dictionaries
|
| 308 |
+
"""
|
| 309 |
+
|
| 310 |
+
if not target_date:
|
| 311 |
+
target_date = date.today()
|
| 312 |
+
|
| 313 |
+
tasks = []
|
| 314 |
+
|
| 315 |
+
try:
|
| 316 |
+
# Get farmer's farms
|
| 317 |
+
farms = getattr(farmer, 'farms', [])
|
| 318 |
+
|
| 319 |
+
for farm in farms:
|
| 320 |
+
# Generate tasks for each farm
|
| 321 |
+
farm_tasks = self._generate_farm_tasks(farmer, farm, target_date)
|
| 322 |
+
tasks.extend(farm_tasks)
|
| 323 |
+
|
| 324 |
+
# Generate general farmer tasks (not farm-specific)
|
| 325 |
+
general_tasks = self._generate_general_tasks(farmer, target_date)
|
| 326 |
+
tasks.extend(general_tasks)
|
| 327 |
+
|
| 328 |
+
# Sort tasks by priority and estimated duration
|
| 329 |
+
tasks = self._prioritize_tasks(tasks)
|
| 330 |
+
|
| 331 |
+
logger.info(f"Generated {len(tasks)} daily tasks for farmer {farmer.name} for {target_date}")
|
| 332 |
+
return tasks
|
| 333 |
+
|
| 334 |
+
except Exception as e:
|
| 335 |
+
logger.error(f"Error generating daily tasks for farmer {farmer.id}: {str(e)}")
|
| 336 |
+
return self._get_fallback_tasks(farmer, target_date)
|
| 337 |
+
|
| 338 |
+
def _generate_farm_tasks(self, farmer, farm, target_date: date) -> List[Dict]:
|
| 339 |
+
"""Generate tasks specific to a farm based on its type"""
|
| 340 |
+
|
| 341 |
+
tasks = []
|
| 342 |
+
|
| 343 |
+
try:
|
| 344 |
+
# Get farm type (default to crop if not specified)
|
| 345 |
+
farm_type = getattr(farm, 'farm_type', 'crop')
|
| 346 |
+
|
| 347 |
+
# Get current season and month
|
| 348 |
+
month = target_date.month
|
| 349 |
+
season = self._get_season(month)
|
| 350 |
+
|
| 351 |
+
# Get weather conditions if available
|
| 352 |
+
weather_info = self._get_weather_info(farm, target_date)
|
| 353 |
+
|
| 354 |
+
# Generate tasks based on farm type
|
| 355 |
+
if farm_type == 'crop':
|
| 356 |
+
tasks.extend(self._get_crop_farming_tasks(farm, target_date, season, weather_info))
|
| 357 |
+
elif farm_type == 'dairy':
|
| 358 |
+
tasks.extend(self._get_dairy_farming_tasks(farm, target_date, weather_info))
|
| 359 |
+
elif farm_type == 'poultry':
|
| 360 |
+
tasks.extend(self._get_poultry_farming_tasks(farm, target_date, weather_info))
|
| 361 |
+
elif farm_type == 'livestock':
|
| 362 |
+
tasks.extend(self._get_livestock_farming_tasks(farm, target_date, weather_info))
|
| 363 |
+
elif farm_type == 'fishery':
|
| 364 |
+
tasks.extend(self._get_fishery_tasks(farm, target_date, weather_info))
|
| 365 |
+
elif farm_type == 'mixed':
|
| 366 |
+
# For mixed farming, generate tasks from all applicable types
|
| 367 |
+
tasks.extend(self._get_mixed_farming_tasks(farm, target_date, season, weather_info))
|
| 368 |
+
|
| 369 |
+
# Add routine maintenance tasks for all farm types
|
| 370 |
+
tasks.extend(self._get_routine_maintenance_tasks(farm, target_date))
|
| 371 |
+
|
| 372 |
+
except Exception as e:
|
| 373 |
+
logger.error(f"Error generating farm tasks for farm {farm.id}: {str(e)}")
|
| 374 |
+
|
| 375 |
+
return tasks
|
| 376 |
+
|
| 377 |
+
def _get_crop_farming_tasks(self, farm, target_date: date, season: str, weather_info: Dict) -> List[Dict]:
|
| 378 |
+
"""Generate tasks specific to crop farming"""
|
| 379 |
+
|
| 380 |
+
tasks = []
|
| 381 |
+
month = target_date.month
|
| 382 |
+
|
| 383 |
+
# Get crops grown on this farm
|
| 384 |
+
crops = getattr(farm, 'get_crop_types', lambda: ['rice', 'wheat'])()
|
| 385 |
+
|
| 386 |
+
# Season-specific crop tasks
|
| 387 |
+
if season == 'kharif': # June-October
|
| 388 |
+
tasks.extend(self._get_kharif_tasks(farm, target_date, weather_info))
|
| 389 |
+
elif season == 'rabi': # November-April
|
| 390 |
+
tasks.extend(self._get_rabi_tasks(farm, target_date, weather_info))
|
| 391 |
+
else: # Summer - May
|
| 392 |
+
tasks.extend(self._get_summer_tasks(farm, target_date, weather_info))
|
| 393 |
+
|
| 394 |
+
# Daily crop management tasks
|
| 395 |
+
if not weather_info.get('rain_expected', False):
|
| 396 |
+
tasks.append(self._create_task_from_template('irrigation', 'morning', farm))
|
| 397 |
+
|
| 398 |
+
# Weekly pest inspection
|
| 399 |
+
if target_date.weekday() == 0: # Monday
|
| 400 |
+
tasks.append(self._create_task_from_template('pest_control', 'inspection', farm))
|
| 401 |
+
|
| 402 |
+
return tasks
|
| 403 |
+
|
| 404 |
+
def _get_dairy_farming_tasks(self, farm, target_date: date, weather_info: Dict) -> List[Dict]:
|
| 405 |
+
"""Generate tasks specific to dairy farming"""
|
| 406 |
+
|
| 407 |
+
tasks = []
|
| 408 |
+
|
| 409 |
+
# Essential daily tasks for dairy farms
|
| 410 |
+
tasks.extend([
|
| 411 |
+
self._create_task_from_template('milking', 'morning', farm),
|
| 412 |
+
self._create_task_from_template('milking', 'evening', farm),
|
| 413 |
+
self._create_task_from_template('dairy_feeding', 'concentrate', farm),
|
| 414 |
+
self._create_task_from_template('dairy_feeding', 'fodder', farm),
|
| 415 |
+
self._create_task_from_template('dairy_health', 'health_check', farm),
|
| 416 |
+
self._create_task_from_template('livestock_care', 'water_supply', farm)
|
| 417 |
+
])
|
| 418 |
+
|
| 419 |
+
# Weekly udder care
|
| 420 |
+
if target_date.weekday() in [1, 4]: # Tuesday and Friday
|
| 421 |
+
tasks.append(self._create_task_from_template('dairy_health', 'udder_care', farm))
|
| 422 |
+
|
| 423 |
+
return tasks
|
| 424 |
+
|
| 425 |
+
def _get_poultry_farming_tasks(self, farm, target_date: date, weather_info: Dict) -> List[Dict]:
|
| 426 |
+
"""Generate tasks specific to poultry farming"""
|
| 427 |
+
|
| 428 |
+
tasks = []
|
| 429 |
+
|
| 430 |
+
# Daily poultry tasks
|
| 431 |
+
tasks.extend([
|
| 432 |
+
self._create_task_from_template('poultry_feeding', 'morning_feed', farm),
|
| 433 |
+
self._create_task_from_template('poultry_feeding', 'evening_feed', farm),
|
| 434 |
+
self._create_task_from_template('egg_collection', 'daily', farm),
|
| 435 |
+
self._create_task_from_template('poultry_health', 'flock_monitoring', farm),
|
| 436 |
+
self._create_task_from_template('livestock_care', 'water_supply', farm)
|
| 437 |
+
])
|
| 438 |
+
|
| 439 |
+
# Weekly coop cleaning
|
| 440 |
+
if target_date.weekday() == 6: # Sunday
|
| 441 |
+
tasks.append(self._create_task_from_template('poultry_health', 'coop_cleaning', farm))
|
| 442 |
+
|
| 443 |
+
return tasks
|
| 444 |
+
|
| 445 |
+
def _get_livestock_farming_tasks(self, farm, target_date: date, weather_info: Dict) -> List[Dict]:
|
| 446 |
+
"""Generate tasks specific to general livestock farming"""
|
| 447 |
+
|
| 448 |
+
tasks = []
|
| 449 |
+
|
| 450 |
+
# Daily livestock care
|
| 451 |
+
tasks.extend([
|
| 452 |
+
self._create_task_from_template('livestock_feeding', 'supplemental', farm),
|
| 453 |
+
self._create_task_from_template('dairy_health', 'health_check', farm),
|
| 454 |
+
self._create_task_from_template('livestock_care', 'water_supply', farm)
|
| 455 |
+
])
|
| 456 |
+
|
| 457 |
+
# Weather-dependent grazing
|
| 458 |
+
if not weather_info.get('rain_expected', False) and weather_info.get('temperature', 25) < 35:
|
| 459 |
+
tasks.append(self._create_task_from_template('livestock_feeding', 'grazing', farm))
|
| 460 |
+
|
| 461 |
+
return tasks
|
| 462 |
+
|
| 463 |
+
def _get_fishery_tasks(self, farm, target_date: date, weather_info: Dict) -> List[Dict]:
|
| 464 |
+
"""Generate tasks specific to fish farming"""
|
| 465 |
+
|
| 466 |
+
tasks = []
|
| 467 |
+
|
| 468 |
+
# Daily fish farming tasks
|
| 469 |
+
tasks.extend([
|
| 470 |
+
self._create_task_from_template('fishery', 'water_quality', farm),
|
| 471 |
+
self._create_task_from_template('fishery', 'fish_feeding', farm)
|
| 472 |
+
])
|
| 473 |
+
|
| 474 |
+
# Regular pond maintenance
|
| 475 |
+
if target_date.weekday() in [2, 5]: # Wednesday and Saturday
|
| 476 |
+
tasks.append(self._create_task_from_template('fishery', 'pond_maintenance', farm))
|
| 477 |
+
|
| 478 |
+
return tasks
|
| 479 |
+
|
| 480 |
+
def _get_mixed_farming_tasks(self, farm, target_date: date, season: str, weather_info: Dict) -> List[Dict]:
|
| 481 |
+
"""Generate tasks for mixed farming operations"""
|
| 482 |
+
|
| 483 |
+
tasks = []
|
| 484 |
+
|
| 485 |
+
# Get farm's livestock and crop information
|
| 486 |
+
livestock_types = getattr(farm, 'get_livestock_types', lambda: [])()
|
| 487 |
+
crop_types = getattr(farm, 'get_crop_types', lambda: [])()
|
| 488 |
+
|
| 489 |
+
# Add crop tasks if crops are grown
|
| 490 |
+
if crop_types:
|
| 491 |
+
tasks.extend(self._get_crop_farming_tasks(farm, target_date, season, weather_info))
|
| 492 |
+
|
| 493 |
+
# Add livestock tasks based on what animals are present
|
| 494 |
+
has_dairy = any('cow' in str(animal).lower() or 'buffalo' in str(animal).lower()
|
| 495 |
+
for animal in livestock_types)
|
| 496 |
+
has_poultry = any('chicken' in str(animal).lower() or 'hen' in str(animal).lower()
|
| 497 |
+
for animal in livestock_types)
|
| 498 |
+
|
| 499 |
+
if has_dairy:
|
| 500 |
+
# Add essential dairy tasks
|
| 501 |
+
tasks.extend([
|
| 502 |
+
self._create_task_from_template('milking', 'morning', farm),
|
| 503 |
+
self._create_task_from_template('milking', 'evening', farm),
|
| 504 |
+
self._create_task_from_template('dairy_feeding', 'concentrate', farm)
|
| 505 |
+
])
|
| 506 |
+
|
| 507 |
+
if has_poultry:
|
| 508 |
+
# Add essential poultry tasks
|
| 509 |
+
tasks.extend([
|
| 510 |
+
self._create_task_from_template('poultry_feeding', 'morning_feed', farm),
|
| 511 |
+
self._create_task_from_template('egg_collection', 'daily', farm)
|
| 512 |
+
])
|
| 513 |
+
|
| 514 |
+
if livestock_types:
|
| 515 |
+
# Add general livestock care
|
| 516 |
+
tasks.extend([
|
| 517 |
+
self._create_task_from_template('dairy_health', 'health_check', farm),
|
| 518 |
+
self._create_task_from_template('livestock_care', 'water_supply', farm)
|
| 519 |
+
])
|
| 520 |
+
|
| 521 |
+
return tasks
|
| 522 |
+
|
| 523 |
+
def _get_routine_maintenance_tasks(self, farm, target_date: date) -> List[Dict]:
|
| 524 |
+
"""Generate routine maintenance tasks applicable to all farm types"""
|
| 525 |
+
|
| 526 |
+
tasks = []
|
| 527 |
+
day_of_week = target_date.weekday()
|
| 528 |
+
|
| 529 |
+
# Weekly equipment maintenance
|
| 530 |
+
if day_of_week == 0: # Monday
|
| 531 |
+
tasks.append(self._create_task_from_template('maintenance', 'equipment', farm))
|
| 532 |
+
|
| 533 |
+
# Monthly infrastructure check
|
| 534 |
+
if target_date.day <= 7 and day_of_week == 0: # First Monday of month
|
| 535 |
+
tasks.append(self._create_task_from_template('maintenance', 'infrastructure', farm))
|
| 536 |
+
|
| 537 |
+
return tasks
|
| 538 |
+
|
| 539 |
+
def _create_task_from_template(self, category: str, task_type: str, farm) -> Dict:
|
| 540 |
+
"""Create a task from predefined templates"""
|
| 541 |
+
|
| 542 |
+
template = self.task_templates.get(category, {}).get(task_type, {})
|
| 543 |
+
|
| 544 |
+
return {
|
| 545 |
+
'task_type': category,
|
| 546 |
+
'task_title': template.get('title', 'Farm Task'),
|
| 547 |
+
'task_description': template.get('description', 'Complete this farming task.'),
|
| 548 |
+
'priority': template.get('priority', 'medium'),
|
| 549 |
+
'estimated_duration': template.get('duration', 30),
|
| 550 |
+
'weather_dependent': template.get('weather_dependent', False),
|
| 551 |
+
'farm_id': farm.id,
|
| 552 |
+
'crop_specific': None
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
def _generate_general_tasks(self, farmer, target_date: date) -> List[Dict]:
|
| 556 |
+
"""Generate general tasks not specific to any farm"""
|
| 557 |
+
|
| 558 |
+
tasks = []
|
| 559 |
+
|
| 560 |
+
# Market price checking
|
| 561 |
+
if target_date.weekday() in [0, 3]: # Monday and Thursday
|
| 562 |
+
tasks.append({
|
| 563 |
+
'task_type': 'market_research',
|
| 564 |
+
'task_title': 'Check Market Prices',
|
| 565 |
+
'task_description': 'Check current market prices for your crops and plan selling strategy accordingly.',
|
| 566 |
+
'priority': 'medium',
|
| 567 |
+
'estimated_duration': 15,
|
| 568 |
+
'weather_dependent': False,
|
| 569 |
+
'farm_id': None,
|
| 570 |
+
'crop_specific': None
|
| 571 |
+
})
|
| 572 |
+
|
| 573 |
+
# Weather planning
|
| 574 |
+
tasks.append({
|
| 575 |
+
'task_type': 'planning',
|
| 576 |
+
'task_title': 'Review Weather Forecast',
|
| 577 |
+
'task_description': 'Check 7-day weather forecast and plan farming activities accordingly.',
|
| 578 |
+
'priority': 'high',
|
| 579 |
+
'estimated_duration': 10,
|
| 580 |
+
'weather_dependent': False,
|
| 581 |
+
'farm_id': None,
|
| 582 |
+
'crop_specific': None
|
| 583 |
+
})
|
| 584 |
+
|
| 585 |
+
return tasks
|
| 586 |
+
|
| 587 |
+
def _get_season(self, month: int) -> str:
|
| 588 |
+
"""Determine agricultural season based on month"""
|
| 589 |
+
|
| 590 |
+
if month in [6, 7, 8, 9, 10]: # June to October
|
| 591 |
+
return 'kharif'
|
| 592 |
+
elif month in [11, 12, 1, 2, 3, 4]: # November to April
|
| 593 |
+
return 'rabi'
|
| 594 |
+
else: # May
|
| 595 |
+
return 'summer'
|
| 596 |
+
|
| 597 |
+
def _get_kharif_tasks(self, farm, target_date: date, weather_info: Dict) -> List[Dict]:
|
| 598 |
+
"""Generate tasks for Kharif season"""
|
| 599 |
+
|
| 600 |
+
tasks = []
|
| 601 |
+
month = target_date.month
|
| 602 |
+
|
| 603 |
+
# Get crops grown on this farm
|
| 604 |
+
crops = farm.get_crop_types() if hasattr(farm, 'get_crop_types') else ['rice', 'cotton']
|
| 605 |
+
|
| 606 |
+
if month == 6: # June - Sowing season
|
| 607 |
+
tasks.append({
|
| 608 |
+
'task_type': 'sowing',
|
| 609 |
+
'task_title': 'Kharif Crop Sowing',
|
| 610 |
+
'task_description': f'Sow {", ".join(crops)} seeds according to recommended spacing and depth.',
|
| 611 |
+
'priority': 'high',
|
| 612 |
+
'estimated_duration': 120,
|
| 613 |
+
'weather_dependent': True,
|
| 614 |
+
'farm_id': farm.id,
|
| 615 |
+
'crop_specific': crops[0] if crops else None
|
| 616 |
+
})
|
| 617 |
+
|
| 618 |
+
elif month in [7, 8]: # July-August - Growth period
|
| 619 |
+
tasks.extend([
|
| 620 |
+
{
|
| 621 |
+
'task_type': 'weeding',
|
| 622 |
+
'task_title': 'Weed Control',
|
| 623 |
+
'task_description': 'Remove weeds from crop fields to prevent competition for nutrients.',
|
| 624 |
+
'priority': 'high',
|
| 625 |
+
'estimated_duration': 90,
|
| 626 |
+
'weather_dependent': False,
|
| 627 |
+
'farm_id': farm.id,
|
| 628 |
+
'crop_specific': None
|
| 629 |
+
},
|
| 630 |
+
{
|
| 631 |
+
'task_type': 'fertilizing',
|
| 632 |
+
'task_title': 'Top Dressing Fertilizer',
|
| 633 |
+
'task_description': 'Apply nitrogen fertilizer for healthy crop growth.',
|
| 634 |
+
'priority': 'high',
|
| 635 |
+
'estimated_duration': 60,
|
| 636 |
+
'weather_dependent': True,
|
| 637 |
+
'farm_id': farm.id,
|
| 638 |
+
'crop_specific': None
|
| 639 |
+
}
|
| 640 |
+
])
|
| 641 |
+
|
| 642 |
+
elif month in [9, 10]: # September-October - Maturity and harvesting
|
| 643 |
+
tasks.append({
|
| 644 |
+
'task_type': 'harvesting',
|
| 645 |
+
'task_title': 'Kharif Crop Harvesting',
|
| 646 |
+
'task_description': f'Harvest mature {", ".join(crops)} crops at optimal moisture content.',
|
| 647 |
+
'priority': 'high',
|
| 648 |
+
'estimated_duration': 180,
|
| 649 |
+
'weather_dependent': True,
|
| 650 |
+
'farm_id': farm.id,
|
| 651 |
+
'crop_specific': crops[0] if crops else None
|
| 652 |
+
})
|
| 653 |
+
|
| 654 |
+
return tasks
|
| 655 |
+
|
| 656 |
+
def _get_rabi_tasks(self, farm, target_date: date, weather_info: Dict) -> List[Dict]:
|
| 657 |
+
"""Generate tasks for Rabi season"""
|
| 658 |
+
|
| 659 |
+
tasks = []
|
| 660 |
+
month = target_date.month
|
| 661 |
+
|
| 662 |
+
crops = farm.get_crop_types() if hasattr(farm, 'get_crop_types') else ['wheat', 'mustard']
|
| 663 |
+
|
| 664 |
+
if month in [11, 12]: # November-December - Sowing
|
| 665 |
+
tasks.append({
|
| 666 |
+
'task_type': 'sowing',
|
| 667 |
+
'task_title': 'Rabi Crop Sowing',
|
| 668 |
+
'task_description': f'Sow {", ".join(crops)} with proper seed treatment and spacing.',
|
| 669 |
+
'priority': 'high',
|
| 670 |
+
'estimated_duration': 120,
|
| 671 |
+
'weather_dependent': False,
|
| 672 |
+
'farm_id': farm.id,
|
| 673 |
+
'crop_specific': crops[0] if crops else None
|
| 674 |
+
})
|
| 675 |
+
|
| 676 |
+
elif month in [1, 2]: # January-February - Growth
|
| 677 |
+
tasks.extend([
|
| 678 |
+
{
|
| 679 |
+
'task_type': 'irrigation',
|
| 680 |
+
'task_title': 'Winter Irrigation',
|
| 681 |
+
'task_description': 'Provide adequate irrigation to rabi crops during dry winter period.',
|
| 682 |
+
'priority': 'high',
|
| 683 |
+
'estimated_duration': 45,
|
| 684 |
+
'weather_dependent': False,
|
| 685 |
+
'farm_id': farm.id,
|
| 686 |
+
'crop_specific': None
|
| 687 |
+
},
|
| 688 |
+
{
|
| 689 |
+
'task_type': 'pest_control',
|
| 690 |
+
'task_title': 'Pest Monitoring',
|
| 691 |
+
'task_description': 'Monitor for aphids and other winter pests. Apply control measures if needed.',
|
| 692 |
+
'priority': 'medium',
|
| 693 |
+
'estimated_duration': 30,
|
| 694 |
+
'weather_dependent': False,
|
| 695 |
+
'farm_id': farm.id,
|
| 696 |
+
'crop_specific': None
|
| 697 |
+
}
|
| 698 |
+
])
|
| 699 |
+
|
| 700 |
+
elif month in [3, 4]: # March-April - Maturity and harvesting
|
| 701 |
+
tasks.append({
|
| 702 |
+
'task_type': 'harvesting',
|
| 703 |
+
'task_title': 'Rabi Crop Harvesting',
|
| 704 |
+
'task_description': f'Harvest {", ".join(crops)} when crops reach physiological maturity.',
|
| 705 |
+
'priority': 'high',
|
| 706 |
+
'estimated_duration': 180,
|
| 707 |
+
'weather_dependent': True,
|
| 708 |
+
'farm_id': farm.id,
|
| 709 |
+
'crop_specific': crops[0] if crops else None
|
| 710 |
+
})
|
| 711 |
+
|
| 712 |
+
return tasks
|
| 713 |
+
|
| 714 |
+
def _get_summer_tasks(self, farm, target_date: date, weather_info: Dict) -> List[Dict]:
|
| 715 |
+
"""Generate tasks for Summer season"""
|
| 716 |
+
|
| 717 |
+
tasks = []
|
| 718 |
+
|
| 719 |
+
# Summer preparation tasks
|
| 720 |
+
tasks.extend([
|
| 721 |
+
{
|
| 722 |
+
'task_type': 'soil_care',
|
| 723 |
+
'task_title': 'Summer Plowing',
|
| 724 |
+
'task_description': 'Deep plowing to break soil crust and improve water infiltration.',
|
| 725 |
+
'priority': 'medium',
|
| 726 |
+
'estimated_duration': 180,
|
| 727 |
+
'weather_dependent': False,
|
| 728 |
+
'farm_id': farm.id,
|
| 729 |
+
'crop_specific': None
|
| 730 |
+
},
|
| 731 |
+
{
|
| 732 |
+
'task_type': 'maintenance',
|
| 733 |
+
'task_title': 'Equipment Preparation',
|
| 734 |
+
'task_description': 'Service and repair farming equipment for upcoming Kharif season.',
|
| 735 |
+
'priority': 'medium',
|
| 736 |
+
'estimated_duration': 120,
|
| 737 |
+
'weather_dependent': False,
|
| 738 |
+
'farm_id': farm.id,
|
| 739 |
+
'crop_specific': None
|
| 740 |
+
}
|
| 741 |
+
])
|
| 742 |
+
|
| 743 |
+
return tasks
|
| 744 |
+
|
| 745 |
+
def _get_routine_tasks(self, farm, target_date: date, weather_info: Dict) -> List[Dict]:
|
| 746 |
+
"""Generate routine daily tasks"""
|
| 747 |
+
|
| 748 |
+
tasks = []
|
| 749 |
+
day_of_week = target_date.weekday()
|
| 750 |
+
|
| 751 |
+
# Daily irrigation check
|
| 752 |
+
if not weather_info.get('rain_expected', False):
|
| 753 |
+
tasks.append({
|
| 754 |
+
'task_type': 'irrigation',
|
| 755 |
+
'task_title': 'Daily Irrigation Check',
|
| 756 |
+
'task_description': 'Check soil moisture and irrigate crops if needed.',
|
| 757 |
+
'priority': 'high',
|
| 758 |
+
'estimated_duration': 30,
|
| 759 |
+
'weather_dependent': True,
|
| 760 |
+
'farm_id': farm.id,
|
| 761 |
+
'crop_specific': None
|
| 762 |
+
})
|
| 763 |
+
|
| 764 |
+
# Weekly pest inspection
|
| 765 |
+
if day_of_week == 0: # Monday
|
| 766 |
+
tasks.append({
|
| 767 |
+
'task_type': 'pest_control',
|
| 768 |
+
'task_title': 'Weekly Pest Inspection',
|
| 769 |
+
'task_description': 'Thoroughly inspect all crops for pests, diseases, and nutrient deficiencies.',
|
| 770 |
+
'priority': 'high',
|
| 771 |
+
'estimated_duration': 45,
|
| 772 |
+
'weather_dependent': False,
|
| 773 |
+
'farm_id': farm.id,
|
| 774 |
+
'crop_specific': None
|
| 775 |
+
})
|
| 776 |
+
|
| 777 |
+
return tasks
|
| 778 |
+
|
| 779 |
+
def _get_weather_info(self, farm, target_date: date) -> Dict:
|
| 780 |
+
"""Get weather information for task planning"""
|
| 781 |
+
|
| 782 |
+
try:
|
| 783 |
+
if self.weather_service and hasattr(farm, 'latitude') and hasattr(farm, 'longitude'):
|
| 784 |
+
if farm.latitude and farm.longitude:
|
| 785 |
+
weather = self.weather_service.get_weather_data(farm.latitude, farm.longitude)
|
| 786 |
+
return {
|
| 787 |
+
'temperature': weather.get('temperature', 25),
|
| 788 |
+
'humidity': weather.get('humidity', 60),
|
| 789 |
+
'rain_expected': weather.get('rainfall', 0) > 0,
|
| 790 |
+
'wind_speed': weather.get('wind_speed', 5)
|
| 791 |
+
}
|
| 792 |
+
except Exception as e:
|
| 793 |
+
logger.error(f"Error getting weather info: {str(e)}")
|
| 794 |
+
|
| 795 |
+
return {'temperature': 25, 'humidity': 60, 'rain_expected': False, 'wind_speed': 5}
|
| 796 |
+
|
| 797 |
+
def _prioritize_tasks(self, tasks: List[Dict]) -> List[Dict]:
|
| 798 |
+
"""Sort tasks by priority and other factors"""
|
| 799 |
+
|
| 800 |
+
priority_order = {'high': 3, 'medium': 2, 'low': 1}
|
| 801 |
+
|
| 802 |
+
return sorted(tasks, key=lambda x: (
|
| 803 |
+
priority_order.get(x.get('priority', 'medium'), 2),
|
| 804 |
+
-x.get('estimated_duration', 60) # Shorter tasks first within same priority
|
| 805 |
+
), reverse=True)
|
| 806 |
+
|
| 807 |
+
def _get_fallback_tasks(self, farmer, target_date: date) -> List[Dict]:
|
| 808 |
+
"""Provide basic fallback tasks when AI generation fails"""
|
| 809 |
+
|
| 810 |
+
return [
|
| 811 |
+
{
|
| 812 |
+
'task_type': 'inspection',
|
| 813 |
+
'task_title': 'Daily Farm Inspection',
|
| 814 |
+
'task_description': 'Walk through your farm and inspect crops for any issues.',
|
| 815 |
+
'priority': 'high',
|
| 816 |
+
'estimated_duration': 30,
|
| 817 |
+
'weather_dependent': False,
|
| 818 |
+
'farm_id': None,
|
| 819 |
+
'crop_specific': None
|
| 820 |
+
},
|
| 821 |
+
{
|
| 822 |
+
'task_type': 'planning',
|
| 823 |
+
'task_title': 'Review Daily Plan',
|
| 824 |
+
'task_description': 'Review your farming activities and plan for tomorrow.',
|
| 825 |
+
'priority': 'medium',
|
| 826 |
+
'estimated_duration': 15,
|
| 827 |
+
'weather_dependent': False,
|
| 828 |
+
'farm_id': None,
|
| 829 |
+
'crop_specific': None
|
| 830 |
+
}
|
| 831 |
+
]
|
| 832 |
+
|
| 833 |
+
def format_task_for_telegram(self, task: Dict, farmer_name: str) -> str:
|
| 834 |
+
"""Format a task for Telegram notification"""
|
| 835 |
+
|
| 836 |
+
priority_emoji = {'high': '🔴', 'medium': '🟡', 'low': '🟢'}
|
| 837 |
+
type_emoji = {
|
| 838 |
+
'irrigation': '💧',
|
| 839 |
+
'fertilizing': '🌱',
|
| 840 |
+
'pest_control': '🛡️',
|
| 841 |
+
'weeding': '🌾',
|
| 842 |
+
'sowing': '🌱',
|
| 843 |
+
'harvesting': '🌽',
|
| 844 |
+
'soil_care': '🌍',
|
| 845 |
+
'maintenance': '🔧',
|
| 846 |
+
'inspection': '👀',
|
| 847 |
+
'planning': '📋',
|
| 848 |
+
'market_research': '💰'
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
emoji = type_emoji.get(task.get('task_type', ''), '📋')
|
| 852 |
+
priority = priority_emoji.get(task.get('priority', 'medium'), '🟡')
|
| 853 |
+
|
| 854 |
+
message = f"{emoji} <b>{task.get('task_title', 'Farm Task')}</b>\n"
|
| 855 |
+
message += f"{priority} Priority: {task.get('priority', 'medium').title()}\n"
|
| 856 |
+
message += f"⏱️ Duration: {task.get('estimated_duration', 30)} minutes\n\n"
|
| 857 |
+
message += f"📝 <b>Description:</b>\n{task.get('task_description', 'Complete this farming task.')}\n\n"
|
| 858 |
+
|
| 859 |
+
if task.get('crop_specific'):
|
| 860 |
+
message += f"🌾 <b>Crop:</b> {task.get('crop_specific')}\n"
|
| 861 |
+
|
| 862 |
+
if task.get('weather_dependent'):
|
| 863 |
+
message += f"🌤️ <i>Weather dependent task</i>\n"
|
| 864 |
+
|
| 865 |
+
return message
|
disease_detection_service.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
from typing import Dict, List, Optional
|
| 5 |
+
import logging
|
| 6 |
+
try:
|
| 7 |
+
from PIL import Image
|
| 8 |
+
except ImportError:
|
| 9 |
+
Image = None
|
| 10 |
+
try:
|
| 11 |
+
import google.generativeai as genai
|
| 12 |
+
except ImportError:
|
| 13 |
+
genai = None
|
| 14 |
+
from models import DiseaseDetection, Farm, db
|
| 15 |
+
|
| 16 |
+
# Configure logging
|
| 17 |
+
logging.basicConfig(level=logging.INFO)
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
class DiseaseDetectionService:
|
| 21 |
+
"""Service for AI-powered crop disease detection and treatment recommendations"""
|
| 22 |
+
|
| 23 |
+
def __init__(self, gemini_api_key: str):
|
| 24 |
+
self.gemini_api_key = gemini_api_key
|
| 25 |
+
genai.configure(api_key=gemini_api_key)
|
| 26 |
+
self.model = genai.GenerativeModel('gemini-2.0-flash')
|
| 27 |
+
|
| 28 |
+
# Common diseases database for quick reference
|
| 29 |
+
self.disease_database = {
|
| 30 |
+
'rice': {
|
| 31 |
+
'blast': {
|
| 32 |
+
'symptoms': 'Diamond-shaped lesions on leaves, brown borders with gray centers',
|
| 33 |
+
'treatment': 'Apply fungicides like Tricyclazole, improve drainage',
|
| 34 |
+
'prevention': 'Use resistant varieties, avoid excessive nitrogen'
|
| 35 |
+
},
|
| 36 |
+
'bacterial_blight': {
|
| 37 |
+
'symptoms': 'Water-soaked lesions that turn yellow then brown',
|
| 38 |
+
'treatment': 'Copper-based bactericides, remove infected plants',
|
| 39 |
+
'prevention': 'Use certified seeds, avoid overhead irrigation'
|
| 40 |
+
}
|
| 41 |
+
},
|
| 42 |
+
'wheat': {
|
| 43 |
+
'rust': {
|
| 44 |
+
'symptoms': 'Orange-red pustules on leaves and stems',
|
| 45 |
+
'treatment': 'Fungicides like Propiconazole, early application',
|
| 46 |
+
'prevention': 'Resistant varieties, proper spacing'
|
| 47 |
+
},
|
| 48 |
+
'powdery_mildew': {
|
| 49 |
+
'symptoms': 'White powdery growth on leaves',
|
| 50 |
+
'treatment': 'Sulfur-based fungicides, improve air circulation',
|
| 51 |
+
'prevention': 'Avoid dense planting, reduce humidity'
|
| 52 |
+
}
|
| 53 |
+
},
|
| 54 |
+
'tomato': {
|
| 55 |
+
'late_blight': {
|
| 56 |
+
'symptoms': 'Dark green water-soaked spots, white mold on leaf undersides',
|
| 57 |
+
'treatment': 'Copper fungicides, remove infected parts',
|
| 58 |
+
'prevention': 'Improve ventilation, avoid overhead watering'
|
| 59 |
+
},
|
| 60 |
+
'early_blight': {
|
| 61 |
+
'symptoms': 'Concentric rings on leaves, starts from bottom',
|
| 62 |
+
'treatment': 'Fungicides, remove lower leaves',
|
| 63 |
+
'prevention': 'Crop rotation, proper spacing'
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
def analyze_disease_from_text(self, farm_id: int, crop_name: str, symptoms: str) -> Dict:
|
| 69 |
+
"""Analyze disease based on text symptoms description"""
|
| 70 |
+
try:
|
| 71 |
+
farm = Farm.query.get(farm_id)
|
| 72 |
+
if not farm:
|
| 73 |
+
return {'error': 'Farm not found'}
|
| 74 |
+
|
| 75 |
+
# Create prompt for Gemini AI
|
| 76 |
+
prompt = self._create_disease_analysis_prompt(crop_name, symptoms)
|
| 77 |
+
|
| 78 |
+
# Get AI analysis
|
| 79 |
+
response = self.model.generate_content(prompt)
|
| 80 |
+
ai_analysis = self._parse_disease_response(response.text)
|
| 81 |
+
|
| 82 |
+
# Save to database
|
| 83 |
+
detection = DiseaseDetection(
|
| 84 |
+
farm_id=farm_id,
|
| 85 |
+
crop_name=crop_name,
|
| 86 |
+
disease_name=ai_analysis.get('disease_name', 'Unknown'),
|
| 87 |
+
confidence_score=ai_analysis.get('confidence', 0.0),
|
| 88 |
+
symptoms=symptoms,
|
| 89 |
+
treatment=ai_analysis.get('treatment', ''),
|
| 90 |
+
prevention=ai_analysis.get('prevention', ''),
|
| 91 |
+
ai_analysis=json.dumps(ai_analysis),
|
| 92 |
+
severity=ai_analysis.get('severity', 'unknown')
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
db.session.add(detection)
|
| 96 |
+
db.session.commit()
|
| 97 |
+
|
| 98 |
+
return {
|
| 99 |
+
'success': True,
|
| 100 |
+
'detection_id': detection.id,
|
| 101 |
+
'analysis': ai_analysis
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
except Exception as e:
|
| 105 |
+
logger.error(f"Error analyzing disease: {str(e)}")
|
| 106 |
+
return {'error': f'Analysis failed: {str(e)}'}
|
| 107 |
+
|
| 108 |
+
def analyze_disease_from_image(self, farm_id: int, crop_name: str, image_path: str, symptoms: str = "") -> Dict:
|
| 109 |
+
"""Analyze disease from uploaded image"""
|
| 110 |
+
try:
|
| 111 |
+
farm = Farm.query.get(farm_id)
|
| 112 |
+
if not farm:
|
| 113 |
+
return {'error': 'Farm not found'}
|
| 114 |
+
|
| 115 |
+
# Verify image exists
|
| 116 |
+
if not os.path.exists(image_path):
|
| 117 |
+
return {'error': 'Image file not found'}
|
| 118 |
+
|
| 119 |
+
# Create prompt for image analysis
|
| 120 |
+
prompt = self._create_image_analysis_prompt(crop_name, symptoms)
|
| 121 |
+
|
| 122 |
+
# Load and process image
|
| 123 |
+
image = Image.open(image_path)
|
| 124 |
+
|
| 125 |
+
# Get AI analysis with image
|
| 126 |
+
response = self.model.generate_content([prompt, image])
|
| 127 |
+
ai_analysis = self._parse_disease_response(response.text)
|
| 128 |
+
|
| 129 |
+
# Save to database
|
| 130 |
+
detection = DiseaseDetection(
|
| 131 |
+
farm_id=farm_id,
|
| 132 |
+
crop_name=crop_name,
|
| 133 |
+
disease_name=ai_analysis.get('disease_name', 'Unknown'),
|
| 134 |
+
confidence_score=ai_analysis.get('confidence', 0.0),
|
| 135 |
+
symptoms=symptoms,
|
| 136 |
+
treatment=ai_analysis.get('treatment', ''),
|
| 137 |
+
prevention=ai_analysis.get('prevention', ''),
|
| 138 |
+
image_path=image_path,
|
| 139 |
+
ai_analysis=json.dumps(ai_analysis),
|
| 140 |
+
severity=ai_analysis.get('severity', 'unknown')
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
db.session.add(detection)
|
| 144 |
+
db.session.commit()
|
| 145 |
+
|
| 146 |
+
return {
|
| 147 |
+
'success': True,
|
| 148 |
+
'detection_id': detection.id,
|
| 149 |
+
'analysis': ai_analysis
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
except Exception as e:
|
| 153 |
+
logger.error(f"Error analyzing disease from image: {str(e)}")
|
| 154 |
+
return {'error': f'Image analysis failed: {str(e)}'}
|
| 155 |
+
|
| 156 |
+
def _create_disease_analysis_prompt(self, crop_name: str, symptoms: str) -> str:
|
| 157 |
+
"""Create prompt for disease analysis"""
|
| 158 |
+
return f"""
|
| 159 |
+
You are an expert plant pathologist. Analyze the following crop disease symptoms and provide a detailed diagnosis.
|
| 160 |
+
|
| 161 |
+
CROP: {crop_name}
|
| 162 |
+
SYMPTOMS: {symptoms}
|
| 163 |
+
|
| 164 |
+
Please provide your analysis in the following JSON format:
|
| 165 |
+
{{
|
| 166 |
+
"disease_name": "Most likely disease name",
|
| 167 |
+
"confidence": 0.85,
|
| 168 |
+
"severity": "mild/moderate/severe",
|
| 169 |
+
"symptoms_analysis": "Detailed analysis of symptoms",
|
| 170 |
+
"treatment": "Immediate treatment recommendations",
|
| 171 |
+
"prevention": "Future prevention measures",
|
| 172 |
+
"additional_info": "Any additional relevant information",
|
| 173 |
+
"urgency": "low/medium/high"
|
| 174 |
+
}}
|
| 175 |
+
|
| 176 |
+
Consider:
|
| 177 |
+
1. Common diseases for this crop
|
| 178 |
+
2. Seasonal factors
|
| 179 |
+
3. Environmental conditions
|
| 180 |
+
4. Treatment urgency
|
| 181 |
+
5. Cost-effective solutions for farmers
|
| 182 |
+
|
| 183 |
+
Provide practical, actionable advice that farmers can easily implement.
|
| 184 |
+
"""
|
| 185 |
+
|
| 186 |
+
def _create_image_analysis_prompt(self, crop_name: str, symptoms: str = "") -> str:
|
| 187 |
+
"""Create prompt for image-based disease analysis"""
|
| 188 |
+
base_prompt = f"""
|
| 189 |
+
You are an expert plant pathologist. Analyze this image of {crop_name} crop for signs of disease or pest damage.
|
| 190 |
+
|
| 191 |
+
CROP: {crop_name}
|
| 192 |
+
"""
|
| 193 |
+
|
| 194 |
+
if symptoms:
|
| 195 |
+
base_prompt += f"REPORTED SYMPTOMS: {symptoms}\n"
|
| 196 |
+
|
| 197 |
+
base_prompt += """
|
| 198 |
+
Please examine the image carefully and provide your analysis in the following JSON format:
|
| 199 |
+
{
|
| 200 |
+
"disease_name": "Identified disease or pest issue",
|
| 201 |
+
"confidence": 0.85,
|
| 202 |
+
"severity": "mild/moderate/severe",
|
| 203 |
+
"visual_symptoms": "What you observe in the image",
|
| 204 |
+
"treatment": "Immediate treatment recommendations",
|
| 205 |
+
"prevention": "Future prevention measures",
|
| 206 |
+
"affected_area": "Percentage of plant affected",
|
| 207 |
+
"urgency": "low/medium/high",
|
| 208 |
+
"additional_observations": "Any other relevant findings"
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
Look for:
|
| 212 |
+
1. Leaf discoloration or spots
|
| 213 |
+
2. Wilting or deformation
|
| 214 |
+
3. Pest damage
|
| 215 |
+
4. Fungal growth
|
| 216 |
+
5. Nutrient deficiencies
|
| 217 |
+
6. Environmental stress signs
|
| 218 |
+
|
| 219 |
+
Provide practical, cost-effective treatment recommendations suitable for farmers.
|
| 220 |
+
"""
|
| 221 |
+
return base_prompt
|
| 222 |
+
|
| 223 |
+
def _parse_disease_response(self, response_text: str) -> Dict:
|
| 224 |
+
"""Parse AI response and extract disease information"""
|
| 225 |
+
try:
|
| 226 |
+
# Try to extract JSON from response
|
| 227 |
+
start_idx = response_text.find('{')
|
| 228 |
+
end_idx = response_text.rfind('}') + 1
|
| 229 |
+
|
| 230 |
+
if start_idx != -1 and end_idx != -1:
|
| 231 |
+
json_text = response_text[start_idx:end_idx]
|
| 232 |
+
analysis = json.loads(json_text)
|
| 233 |
+
|
| 234 |
+
# Ensure required fields
|
| 235 |
+
analysis.setdefault('disease_name', 'Unknown Disease')
|
| 236 |
+
analysis.setdefault('confidence', 0.5)
|
| 237 |
+
analysis.setdefault('severity', 'moderate')
|
| 238 |
+
analysis.setdefault('treatment', 'Consult agricultural expert')
|
| 239 |
+
analysis.setdefault('prevention', 'Maintain good crop hygiene')
|
| 240 |
+
analysis.setdefault('urgency', 'medium')
|
| 241 |
+
|
| 242 |
+
return analysis
|
| 243 |
+
else:
|
| 244 |
+
# Manual parsing if JSON not found
|
| 245 |
+
return self._manual_parse_response(response_text)
|
| 246 |
+
|
| 247 |
+
except json.JSONDecodeError:
|
| 248 |
+
return self._manual_parse_response(response_text)
|
| 249 |
+
except Exception as e:
|
| 250 |
+
logger.error(f"Error parsing disease response: {str(e)}")
|
| 251 |
+
return self._get_fallback_analysis()
|
| 252 |
+
|
| 253 |
+
def _manual_parse_response(self, response_text: str) -> Dict:
|
| 254 |
+
"""Manually parse response if JSON parsing fails"""
|
| 255 |
+
analysis = {
|
| 256 |
+
'disease_name': 'Unknown Disease',
|
| 257 |
+
'confidence': 0.5,
|
| 258 |
+
'severity': 'moderate',
|
| 259 |
+
'treatment': 'Consult agricultural expert',
|
| 260 |
+
'prevention': 'Maintain good crop hygiene',
|
| 261 |
+
'urgency': 'medium'
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
lines = response_text.lower().split('\n')
|
| 265 |
+
|
| 266 |
+
for line in lines:
|
| 267 |
+
if 'disease' in line and 'name' in line:
|
| 268 |
+
analysis['disease_name'] = line.split(':')[-1].strip()
|
| 269 |
+
elif 'treatment' in line:
|
| 270 |
+
analysis['treatment'] = line.split(':')[-1].strip()
|
| 271 |
+
elif 'prevention' in line:
|
| 272 |
+
analysis['prevention'] = line.split(':')[-1].strip()
|
| 273 |
+
elif 'severe' in line:
|
| 274 |
+
analysis['severity'] = 'severe'
|
| 275 |
+
elif 'mild' in line:
|
| 276 |
+
analysis['severity'] = 'mild'
|
| 277 |
+
|
| 278 |
+
return analysis
|
| 279 |
+
|
| 280 |
+
def _get_fallback_analysis(self) -> Dict:
|
| 281 |
+
"""Provide fallback analysis when AI fails"""
|
| 282 |
+
return {
|
| 283 |
+
'disease_name': 'Analysis Failed',
|
| 284 |
+
'confidence': 0.0,
|
| 285 |
+
'severity': 'unknown',
|
| 286 |
+
'treatment': 'Please consult with a local agricultural expert or extension officer',
|
| 287 |
+
'prevention': 'Maintain good crop hygiene and monitoring practices',
|
| 288 |
+
'urgency': 'medium',
|
| 289 |
+
'error': 'AI analysis unavailable'
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
def get_disease_history(self, farm_id: int, days: int = 30) -> List[DiseaseDetection]:
|
| 293 |
+
"""Get disease detection history for a farm"""
|
| 294 |
+
from_date = datetime.now() - timedelta(days=days)
|
| 295 |
+
|
| 296 |
+
return DiseaseDetection.query.filter(
|
| 297 |
+
DiseaseDetection.farm_id == farm_id,
|
| 298 |
+
DiseaseDetection.detected_at >= from_date
|
| 299 |
+
).order_by(DiseaseDetection.detected_at.desc()).all()
|
| 300 |
+
|
| 301 |
+
def update_treatment_status(self, detection_id: int, status: str, notes: str = "") -> bool:
|
| 302 |
+
"""Update treatment status for a disease detection"""
|
| 303 |
+
try:
|
| 304 |
+
detection = DiseaseDetection.query.get(detection_id)
|
| 305 |
+
if not detection:
|
| 306 |
+
return False
|
| 307 |
+
|
| 308 |
+
detection.status = status
|
| 309 |
+
if status == 'resolved':
|
| 310 |
+
detection.resolved_at = datetime.now()
|
| 311 |
+
|
| 312 |
+
db.session.commit()
|
| 313 |
+
return True
|
| 314 |
+
|
| 315 |
+
except Exception as e:
|
| 316 |
+
logger.error(f"Error updating treatment status: {str(e)}")
|
| 317 |
+
return False
|
| 318 |
+
|
| 319 |
+
def get_preventive_recommendations(self, crop_name: str, season: str = "") -> Dict:
|
| 320 |
+
"""Get preventive recommendations for common diseases"""
|
| 321 |
+
crop_lower = crop_name.lower()
|
| 322 |
+
|
| 323 |
+
if crop_lower in self.disease_database:
|
| 324 |
+
diseases = self.disease_database[crop_lower]
|
| 325 |
+
recommendations = []
|
| 326 |
+
|
| 327 |
+
for disease, info in diseases.items():
|
| 328 |
+
recommendations.append({
|
| 329 |
+
'disease': disease.replace('_', ' ').title(),
|
| 330 |
+
'prevention': info['prevention'],
|
| 331 |
+
'early_symptoms': info['symptoms']
|
| 332 |
+
})
|
| 333 |
+
|
| 334 |
+
return {
|
| 335 |
+
'crop': crop_name,
|
| 336 |
+
'recommendations': recommendations,
|
| 337 |
+
'general_tips': [
|
| 338 |
+
'Regular field monitoring',
|
| 339 |
+
'Proper crop rotation',
|
| 340 |
+
'Maintain field hygiene',
|
| 341 |
+
'Use disease-resistant varieties',
|
| 342 |
+
'Proper water management'
|
| 343 |
+
]
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
return {
|
| 347 |
+
'crop': crop_name,
|
| 348 |
+
'recommendations': [],
|
| 349 |
+
'general_tips': [
|
| 350 |
+
'Regular field monitoring is essential',
|
| 351 |
+
'Maintain proper spacing between plants',
|
| 352 |
+
'Ensure good drainage',
|
| 353 |
+
'Use certified seeds',
|
| 354 |
+
'Follow integrated pest management'
|
| 355 |
+
]
|
| 356 |
+
}
|
farm_management.db
ADDED
|
File without changes
|
farms.db
ADDED
|
Binary file (8.19 kB). View file
|
|
|
gemini_service.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import google.generativeai as genai
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
from datetime import datetime, date
|
| 5 |
+
from typing import Dict, Any, Optional
|
| 6 |
+
import logging
|
| 7 |
+
|
| 8 |
+
# Configure logging
|
| 9 |
+
logging.basicConfig(level=logging.INFO)
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
class GeminiAIService:
|
| 13 |
+
"""Service class for Gemini AI integration"""
|
| 14 |
+
|
| 15 |
+
def __init__(self, api_key: str):
|
| 16 |
+
"""Initialize Gemini AI service"""
|
| 17 |
+
self.api_key = api_key
|
| 18 |
+
genai.configure(api_key=api_key)
|
| 19 |
+
self.model = genai.GenerativeModel('gemini-2.0-flash')
|
| 20 |
+
|
| 21 |
+
def generate_daily_advisory(self, farmer_data: Dict[str, Any], farm_data: Dict[str, Any],
|
| 22 |
+
soil_data: Dict[str, Any], weather_data: Dict[str, Any],
|
| 23 |
+
current_date: str = None) -> Dict[str, str]:
|
| 24 |
+
"""
|
| 25 |
+
Generate daily farming advisory using Gemini AI
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
farmer_data: Farmer information
|
| 29 |
+
farm_data: Farm details including crops
|
| 30 |
+
soil_data: Current soil parameters
|
| 31 |
+
weather_data: Current weather data
|
| 32 |
+
current_date: Date for the advisory (default: today)
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
Dictionary with task_to_do, task_to_avoid, and reason_explanation
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
if current_date is None:
|
| 39 |
+
current_date = date.today().strftime('%Y-%m-%d')
|
| 40 |
+
|
| 41 |
+
# Prepare the prompt for Gemini
|
| 42 |
+
prompt = self._create_advisory_prompt(farmer_data, farm_data, soil_data, weather_data, current_date)
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
# Generate response from Gemini
|
| 46 |
+
response = self.model.generate_content(prompt)
|
| 47 |
+
|
| 48 |
+
# Parse the response
|
| 49 |
+
advisory = self._parse_gemini_response(response.text)
|
| 50 |
+
|
| 51 |
+
logger.info(f"Generated advisory for farmer {farmer_data.get('name')} on {current_date}")
|
| 52 |
+
return advisory
|
| 53 |
+
|
| 54 |
+
except Exception as e:
|
| 55 |
+
logger.error(f"Error generating advisory: {str(e)}")
|
| 56 |
+
return self._get_fallback_advisory()
|
| 57 |
+
|
| 58 |
+
def _create_advisory_prompt(self, farmer_data: Dict[str, Any], farm_data: Dict[str, Any],
|
| 59 |
+
soil_data: Dict[str, Any], weather_data: Dict[str, Any],
|
| 60 |
+
current_date: str) -> str:
|
| 61 |
+
"""Create a detailed prompt for Gemini AI"""
|
| 62 |
+
|
| 63 |
+
crop_types = ', '.join(farm_data.get('crop_types', []))
|
| 64 |
+
|
| 65 |
+
prompt = f"""
|
| 66 |
+
You are an expert agricultural advisor. Generate a daily farming advisory for the following farmer:
|
| 67 |
+
|
| 68 |
+
FARMER INFORMATION:
|
| 69 |
+
- Name: {farmer_data.get('name', 'Unknown')}
|
| 70 |
+
- Farm Name: {farm_data.get('farm_name', 'Unknown')}
|
| 71 |
+
- Farm Size: {farm_data.get('farm_size', 0)} acres
|
| 72 |
+
- Crops: {crop_types}
|
| 73 |
+
- Irrigation Type: {farm_data.get('irrigation_type', 'Unknown')}
|
| 74 |
+
|
| 75 |
+
SOIL CONDITIONS:
|
| 76 |
+
- Soil Type: {soil_data.get('soil_type', 'Unknown')}
|
| 77 |
+
- pH Level: {soil_data.get('ph_level', 'Unknown')}
|
| 78 |
+
- Nitrogen: {soil_data.get('nitrogen_level', 'Unknown')} ppm
|
| 79 |
+
- Phosphorus: {soil_data.get('phosphorus_level', 'Unknown')} ppm
|
| 80 |
+
- Potassium: {soil_data.get('potassium_level', 'Unknown')} ppm
|
| 81 |
+
- Moisture: {soil_data.get('moisture_percentage', 'Unknown')}%
|
| 82 |
+
|
| 83 |
+
WEATHER CONDITIONS:
|
| 84 |
+
- Date: {current_date}
|
| 85 |
+
- Temperature: {weather_data.get('main', {}).get('temp_min', 'Unknown')}°C - {weather_data.get('main', {}).get('temp_max', 'Unknown')}°C
|
| 86 |
+
- Humidity: {weather_data.get('main', {}).get('humidity', 'Unknown')}%
|
| 87 |
+
- Wind Speed: {weather_data.get('wind', {}).get('speed', 0) * 3.6:.1f} km/h
|
| 88 |
+
- Weather Condition: {weather_data.get('weather', [{}])[0].get('description', 'Unknown').title()}
|
| 89 |
+
|
| 90 |
+
INSTRUCTIONS:
|
| 91 |
+
Please provide a daily farming advisory in the following JSON format:
|
| 92 |
+
{{
|
| 93 |
+
"task_to_do": "Specific task the farmer should do today",
|
| 94 |
+
"task_to_avoid": "Specific task the farmer should avoid today",
|
| 95 |
+
"reason_explanation": "Simple, farmer-friendly explanation for the recommendations",
|
| 96 |
+
"crop_stage": "Current estimated crop growth stage"
|
| 97 |
+
}}
|
| 98 |
+
|
| 99 |
+
Consider the weather conditions, soil parameters, crop requirements, and seasonal farming practices.
|
| 100 |
+
Provide practical, actionable advice that a farmer can easily understand and implement.
|
| 101 |
+
Make the language simple and direct.
|
| 102 |
+
"""
|
| 103 |
+
|
| 104 |
+
return prompt
|
| 105 |
+
|
| 106 |
+
def _parse_gemini_response(self, response_text: str) -> Dict[str, str]:
|
| 107 |
+
"""Parse Gemini's response and extract advisory information"""
|
| 108 |
+
|
| 109 |
+
try:
|
| 110 |
+
# Try to extract JSON from the response
|
| 111 |
+
# Look for JSON-like content in the response
|
| 112 |
+
start_idx = response_text.find('{')
|
| 113 |
+
end_idx = response_text.rfind('}') + 1
|
| 114 |
+
|
| 115 |
+
if start_idx != -1 and end_idx != -1:
|
| 116 |
+
json_text = response_text[start_idx:end_idx]
|
| 117 |
+
advisory = json.loads(json_text)
|
| 118 |
+
|
| 119 |
+
# Validate required fields
|
| 120 |
+
required_fields = ['task_to_do', 'task_to_avoid', 'reason_explanation']
|
| 121 |
+
for field in required_fields:
|
| 122 |
+
if field not in advisory:
|
| 123 |
+
advisory[field] = "No recommendation available"
|
| 124 |
+
|
| 125 |
+
return advisory
|
| 126 |
+
else:
|
| 127 |
+
# If no JSON found, try to parse manually
|
| 128 |
+
return self._manual_parse_response(response_text)
|
| 129 |
+
|
| 130 |
+
except json.JSONDecodeError:
|
| 131 |
+
# If JSON parsing fails, try manual parsing
|
| 132 |
+
return self._manual_parse_response(response_text)
|
| 133 |
+
except Exception as e:
|
| 134 |
+
logger.error(f"Error parsing Gemini response: {str(e)}")
|
| 135 |
+
return self._get_fallback_advisory()
|
| 136 |
+
|
| 137 |
+
def _manual_parse_response(self, response_text: str) -> Dict[str, str]:
|
| 138 |
+
"""Manually parse response if JSON parsing fails"""
|
| 139 |
+
|
| 140 |
+
lines = response_text.split('\n')
|
| 141 |
+
advisory = {
|
| 142 |
+
'task_to_do': 'No specific task recommended',
|
| 143 |
+
'task_to_avoid': 'No specific task to avoid',
|
| 144 |
+
'reason_explanation': 'Advisory not available',
|
| 145 |
+
'crop_stage': 'Unknown'
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
for line in lines:
|
| 149 |
+
line = line.strip()
|
| 150 |
+
if line.startswith('✅') or 'task_to_do' in line.lower():
|
| 151 |
+
advisory['task_to_do'] = line.replace('✅', '').strip()
|
| 152 |
+
elif line.startswith('❌') or 'task_to_avoid' in line.lower():
|
| 153 |
+
advisory['task_to_avoid'] = line.replace('❌', '').strip()
|
| 154 |
+
elif line.startswith('ℹ️') or 'reason' in line.lower():
|
| 155 |
+
advisory['reason_explanation'] = line.replace('ℹ️', '').strip()
|
| 156 |
+
|
| 157 |
+
return advisory
|
| 158 |
+
|
| 159 |
+
def _get_fallback_advisory(self) -> Dict[str, str]:
|
| 160 |
+
"""Provide a fallback advisory when Gemini fails"""
|
| 161 |
+
|
| 162 |
+
return {
|
| 163 |
+
'task_to_do': 'Check crop condition and water levels',
|
| 164 |
+
'task_to_avoid': 'Avoid heavy farm work during extreme weather',
|
| 165 |
+
'reason_explanation': 'General farming best practices for safety and crop health',
|
| 166 |
+
'crop_stage': 'Unknown'
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
def generate_year_plan(self, farmer_data: Dict[str, Any], farm_data: Dict[str, Any],
|
| 170 |
+
soil_data: Dict[str, Any]) -> Dict[str, Any]:
|
| 171 |
+
"""Generate a comprehensive year-long farming plan with detailed HTML formatting"""
|
| 172 |
+
|
| 173 |
+
crop_types = ', '.join(farm_data.get('crop_types', []))
|
| 174 |
+
farm_size = farm_data.get('farm_size', 0)
|
| 175 |
+
irrigation_type = farm_data.get('irrigation_type', 'Unknown')
|
| 176 |
+
soil_type = soil_data.get('soil_type', 'Unknown')
|
| 177 |
+
|
| 178 |
+
# Construct crop details with areas (assume equal distribution if not specified)
|
| 179 |
+
crops = farm_data.get('crop_types', [])
|
| 180 |
+
if crops and farm_size:
|
| 181 |
+
area_per_crop = farm_size / len(crops)
|
| 182 |
+
crop_details = ", ".join([f"{crop} ({area_per_crop:.1f} acres)" for crop in crops])
|
| 183 |
+
else:
|
| 184 |
+
crop_details = crop_types or "Mixed crops"
|
| 185 |
+
|
| 186 |
+
prompt = f"""
|
| 187 |
+
You are a skilled agriculture expert and data analyst. I am providing you with the following farm details:
|
| 188 |
+
|
| 189 |
+
FARM INFORMATION:
|
| 190 |
+
- Farmer Name: {farmer_data.get('name', 'Unknown')}
|
| 191 |
+
- Farm Name: {farm_data.get('farm_name', 'Unknown')}
|
| 192 |
+
- Total Farm Area: {farm_size} acres
|
| 193 |
+
- Location/Address: {farmer_data.get('address', 'Unknown')}
|
| 194 |
+
- Irrigation Type: {irrigation_type}
|
| 195 |
+
- Soil Type: {soil_type}
|
| 196 |
+
- Soil pH: {soil_data.get('ph_level', 'Unknown')}
|
| 197 |
+
- Nitrogen Level: {soil_data.get('nitrogen_level', 'Unknown')} ppm
|
| 198 |
+
- Phosphorus Level: {soil_data.get('phosphorus_level', 'Unknown')} ppm
|
| 199 |
+
- Potassium Level: {soil_data.get('potassium_level', 'Unknown')} ppm
|
| 200 |
+
- Current Crops: {crop_details}
|
| 201 |
+
|
| 202 |
+
Using the above input, please generate a fully formatted HTML page with enhanced CSS styling that includes the following sections:
|
| 203 |
+
|
| 204 |
+
1. **Main Heading:**
|
| 205 |
+
- A centrally aligned, bold heading in green titled "Comprehensive Yearly Farming Plan 2025" with proper spacing and clear font sizes.
|
| 206 |
+
|
| 207 |
+
2. **Farm Overview Statistics:**
|
| 208 |
+
- A left-aligned, blue bold subheading "Farm Overview & Current Status".
|
| 209 |
+
- Below it, include a green table with columns for:
|
| 210 |
+
- Parameter
|
| 211 |
+
- Current Value
|
| 212 |
+
- Recommended Range
|
| 213 |
+
- Status (Good/Needs Improvement)
|
| 214 |
+
- Include farm size, soil parameters, irrigation type, etc.
|
| 215 |
+
|
| 216 |
+
3. **Monthly Farming Calendar (12-Month Plan):**
|
| 217 |
+
- A left-aligned, blue bold subheading titled "Monthly Farming Calendar".
|
| 218 |
+
- Create a comprehensive green table displaying month-wise activities for the entire year:
|
| 219 |
+
- Month
|
| 220 |
+
- Primary Activities
|
| 221 |
+
- Crop Operations
|
| 222 |
+
- Fertilizer Schedule
|
| 223 |
+
- Irrigation Requirements
|
| 224 |
+
- Expected Weather Considerations
|
| 225 |
+
- Include specific activities for each month based on crop cycles, seasons (Rabi/Kharif), and local agricultural practices.
|
| 226 |
+
|
| 227 |
+
4. **Crop-wise Annual Strategy:**
|
| 228 |
+
- A left-aligned, blue bold subheading "Crop-wise Annual Strategy".
|
| 229 |
+
- Add a detailed green table showing for each crop:
|
| 230 |
+
- Crop Name
|
| 231 |
+
- Sowing Period
|
| 232 |
+
- Growing Duration
|
| 233 |
+
- Harvest Period
|
| 234 |
+
- Expected Yield (per acre)
|
| 235 |
+
- Estimated Revenue
|
| 236 |
+
- Key Care Instructions
|
| 237 |
+
|
| 238 |
+
5. **Financial Projections:**
|
| 239 |
+
- Include a section with a blue left-aligned subheading "Annual Financial Forecast".
|
| 240 |
+
- Present tables showing:
|
| 241 |
+
- Expected Production Costs (seeds, fertilizers, labor, etc.)
|
| 242 |
+
- Projected Revenue by crop
|
| 243 |
+
- Estimated Profit Margins
|
| 244 |
+
- Monthly cash flow predictions
|
| 245 |
+
|
| 246 |
+
6. **Soil Management Plan:**
|
| 247 |
+
- A blue left-aligned subheading "Soil Health & Fertilizer Schedule".
|
| 248 |
+
- Create tables for:
|
| 249 |
+
- Soil testing schedule
|
| 250 |
+
- Fertilizer application timeline
|
| 251 |
+
- Organic matter enhancement plan
|
| 252 |
+
- pH management strategies
|
| 253 |
+
|
| 254 |
+
7. **Risk Management & Weather Planning:**
|
| 255 |
+
- A blue left-aligned subheading "Risk Management Strategies".
|
| 256 |
+
- Include tables for:
|
| 257 |
+
- Seasonal weather challenges
|
| 258 |
+
- Pest and disease prevention calendar
|
| 259 |
+
- Backup crop strategies
|
| 260 |
+
- Insurance and financial protection
|
| 261 |
+
|
| 262 |
+
8. **Key Recommendations & Action Items:**
|
| 263 |
+
- At the bottom, include a bullet-point list with specific, actionable recommendations.
|
| 264 |
+
- Style each bullet point in bold with yellow highlights on key dates, quantities, and financial figures.
|
| 265 |
+
- Include immediate actions, seasonal priorities, and long-term improvements.
|
| 266 |
+
|
| 267 |
+
IMPORTANT REQUIREMENTS:
|
| 268 |
+
- Generate the entire response in Hindi language. All text, headings, table headers, and content should be in Hindi.
|
| 269 |
+
- Use realistic data based on Indian agricultural practices and the provided farm details.
|
| 270 |
+
- Make tables interactive, beautiful, and colorful with proper spacing and margins.
|
| 271 |
+
- Increase font size for headings and optimize spacing for readability.
|
| 272 |
+
- Do not include any extra spacing at the beginning or end of the response.
|
| 273 |
+
- Ensure all content is properly formatted as HTML with appropriate CSS styling.
|
| 274 |
+
- Include specific dates, quantities, and actionable advice suitable for Indian farmers.
|
| 275 |
+
- Consider Indian seasons (Rabi, Kharif, Zayad) and local agricultural practices.
|
| 276 |
+
- Use metric measurements and Indian rupee currency where applicable.
|
| 277 |
+
|
| 278 |
+
Generate a comprehensive, professional, and farmer-friendly yearly plan that can guide the farmer throughout the entire agricultural year.
|
| 279 |
+
"""
|
| 280 |
+
|
| 281 |
+
try:
|
| 282 |
+
response = self.model.generate_content(prompt)
|
| 283 |
+
|
| 284 |
+
# Return the comprehensive HTML plan
|
| 285 |
+
html_plan = response.text.strip()
|
| 286 |
+
|
| 287 |
+
return {
|
| 288 |
+
'plan': html_plan,
|
| 289 |
+
'type': 'comprehensive_html',
|
| 290 |
+
'generated_at': datetime.now().isoformat(),
|
| 291 |
+
'ai_generated': True,
|
| 292 |
+
'farmer_name': farmer_data.get('name'),
|
| 293 |
+
'farm_name': farm_data.get('farm_name')
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
except Exception as e:
|
| 297 |
+
logger.error(f"Error generating comprehensive year plan: {str(e)}")
|
| 298 |
+
return {
|
| 299 |
+
'plan': self._get_fallback_yearly_plan(farmer_data, farm_data, soil_data),
|
| 300 |
+
'type': 'fallback_html',
|
| 301 |
+
'generated_at': datetime.now().isoformat(),
|
| 302 |
+
'ai_generated': False
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
def _get_fallback_yearly_plan(self, farmer_data: Dict[str, Any], farm_data: Dict[str, Any], soil_data: Dict[str, Any]) -> str:
|
| 306 |
+
"""Generate a fallback yearly plan in HTML format when AI fails"""
|
| 307 |
+
|
| 308 |
+
crop_types = ', '.join(farm_data.get('crop_types', ['Mixed crops']))
|
| 309 |
+
|
| 310 |
+
return f"""
|
| 311 |
+
<!DOCTYPE html>
|
| 312 |
+
<html>
|
| 313 |
+
<head>
|
| 314 |
+
<style>
|
| 315 |
+
body {{ font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }}
|
| 316 |
+
.header {{ text-align: center; color: #2e7d32; font-size: 28px; font-weight: bold; margin-bottom: 30px; }}
|
| 317 |
+
.section {{ margin-bottom: 25px; }}
|
| 318 |
+
.section-title {{ color: #1976d2; font-size: 20px; font-weight: bold; margin-bottom: 15px; }}
|
| 319 |
+
table {{ width: 100%; border-collapse: collapse; background-color: #e8f5e8; margin-bottom: 20px; }}
|
| 320 |
+
th {{ background-color: #4caf50; color: white; padding: 12px; text-align: left; }}
|
| 321 |
+
td {{ padding: 10px; border: 1px solid #ddd; }}
|
| 322 |
+
.highlight {{ background-color: #ffeb3b; font-weight: bold; }}
|
| 323 |
+
.recommendation {{ margin: 10px 0; padding: 10px; background-color: #fff3e0; border-left: 4px solid #ff9800; }}
|
| 324 |
+
</style>
|
| 325 |
+
</head>
|
| 326 |
+
<body>
|
| 327 |
+
<div class="header">वार्षिक खेती योजना 2025</div>
|
| 328 |
+
|
| 329 |
+
<div class="section">
|
| 330 |
+
<div class="section-title">खेत की जानकारी</div>
|
| 331 |
+
<table>
|
| 332 |
+
<tr><th>विवरण</th><th>मान</th></tr>
|
| 333 |
+
<tr><td>किसान का ���ाम</td><td>{farmer_data.get('name', 'अज्ञात')}</td></tr>
|
| 334 |
+
<tr><td>खेत का नाम</td><td>{farm_data.get('farm_name', 'अज्ञात')}</td></tr>
|
| 335 |
+
<tr><td>कुल क्षेत्रफल</td><td>{farm_data.get('farm_size', 0)} एकड़</td></tr>
|
| 336 |
+
<tr><td>फसलें</td><td>{crop_types}</td></tr>
|
| 337 |
+
<tr><td>सिंचाई प्रकार</td><td>{farm_data.get('irrigation_type', 'अज्ञात')}</td></tr>
|
| 338 |
+
</table>
|
| 339 |
+
</div>
|
| 340 |
+
|
| 341 |
+
<div class="section">
|
| 342 |
+
<div class="section-title">मासिक गतिविधि कैलेंडर</div>
|
| 343 |
+
<table>
|
| 344 |
+
<tr><th>महीना</th><th>मुख्य गतिविधियां</th><th>सिंचाई</th></tr>
|
| 345 |
+
<tr><td>जनवरी</td><td>रबी फसल की देखभाल, खाद डालना</td><td>आवश्यकता अनुसार</td></tr>
|
| 346 |
+
<tr><td>फरवरी</td><td>फसल की निगरानी, कीट नियंत्रण</td><td>नियमित</td></tr>
|
| 347 |
+
<tr><td>मार्च</td><td>रबी फसल की कटाई तैयारी</td><td>कम</td></tr>
|
| 348 |
+
<tr><td>अप्रैल</td><td>रबी फसल कटाई, खरीफ की तैयारी</td><td>गर्मी के कारण अधिक</td></tr>
|
| 349 |
+
<tr><td>मई</td><td>खेत की तैयारी, बीज खरीदारी</td><td>अधिक</td></tr>
|
| 350 |
+
<tr><td>जून</td><td>खरीफ फसल बुआई</td><td>मानसून शुरुआत</td></tr>
|
| 351 |
+
<tr><td>जुलाई</td><td>खरीफ फसल देखभाल</td><td>मानसून</td></tr>
|
| 352 |
+
<tr><td>अगस्त</td><td>निराई-गुड़ाई, खाद</td><td>मानसून</td></tr>
|
| 353 |
+
<tr><td>सितंबर</td><td>फसल निगरानी</td><td>मध्यम</td></tr>
|
| 354 |
+
<tr><td>अक्टूबर</td><td>खरीफ कटाई, रबी तैयारी</td><td>आवश्यकता अनुसार</td></tr>
|
| 355 |
+
<tr><td>नवंबर</td><td>रबी फसल बुआई</td><td>नियमित</td></tr>
|
| 356 |
+
<tr><td>दिसंबर</td><td>रबी फसल देखभाल</td><td>ठंड में कम</td></tr>
|
| 357 |
+
</table>
|
| 358 |
+
</div>
|
| 359 |
+
|
| 360 |
+
<div class="recommendation">
|
| 361 |
+
<strong>मुख्य सुझाव:</strong><br>
|
| 362 |
+
• मिट्टी की जांच <span class="highlight">वर्ष में दो बार</span> कराएं<br>
|
| 363 |
+
• उन्नत बीजों का प्रयोग करें<br>
|
| 364 |
+
• <span class="highlight">समय पर</span> सिंचाई और खाद डालें<br>
|
| 365 |
+
• कीट-रोग की नियमित निगरानी करें<br>
|
| 366 |
+
• मौसम की जानकारी रखें
|
| 367 |
+
</div>
|
| 368 |
+
</body>
|
| 369 |
+
</html>
|
| 370 |
+
"""
|
| 371 |
+
|
| 372 |
+
def format_sms_message(farmer_name: str, advisory: Dict[str, str]) -> str:
|
| 373 |
+
"""Format the advisory as an SMS message"""
|
| 374 |
+
|
| 375 |
+
message = f"Good Morning, {farmer_name} 🌱\n"
|
| 376 |
+
message += f"✅ Task: {advisory.get('task_to_do', 'No task')}\n"
|
| 377 |
+
message += f"❌ Avoid: {advisory.get('task_to_avoid', 'No restrictions')}\n"
|
| 378 |
+
|
| 379 |
+
if advisory.get('reason_explanation'):
|
| 380 |
+
message += f"ℹ️ {advisory.get('reason_explanation')}"
|
| 381 |
+
|
| 382 |
+
return message
|
generated_pdfs/yearly_plan_PRANIT Ravindra CHILBULE_20250906_140900.html
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
<!DOCTYPE html>
|
| 3 |
+
<html lang="en">
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Yearly Farming Plan - PRANIT Ravindra CHILBULE</title>
|
| 8 |
+
<style>
|
| 9 |
+
body {
|
| 10 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 11 |
+
line-height: 1.6;
|
| 12 |
+
margin: 20px;
|
| 13 |
+
background-color: #f9f9f9;
|
| 14 |
+
}
|
| 15 |
+
.container {
|
| 16 |
+
max-width: 800px;
|
| 17 |
+
margin: 0 auto;
|
| 18 |
+
background: white;
|
| 19 |
+
padding: 30px;
|
| 20 |
+
border-radius: 10px;
|
| 21 |
+
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
| 22 |
+
}
|
| 23 |
+
.header {
|
| 24 |
+
text-align: center;
|
| 25 |
+
color: #2c5530;
|
| 26 |
+
border-bottom: 3px solid #4CAF50;
|
| 27 |
+
padding-bottom: 20px;
|
| 28 |
+
margin-bottom: 30px;
|
| 29 |
+
}
|
| 30 |
+
.section {
|
| 31 |
+
margin-bottom: 30px;
|
| 32 |
+
}
|
| 33 |
+
.section h2 {
|
| 34 |
+
color: #1976d2;
|
| 35 |
+
border-left: 4px solid #4CAF50;
|
| 36 |
+
padding-left: 15px;
|
| 37 |
+
}
|
| 38 |
+
table {
|
| 39 |
+
width: 100%;
|
| 40 |
+
border-collapse: collapse;
|
| 41 |
+
margin: 15px 0;
|
| 42 |
+
}
|
| 43 |
+
th, td {
|
| 44 |
+
padding: 12px;
|
| 45 |
+
text-align: left;
|
| 46 |
+
border: 1px solid #ddd;
|
| 47 |
+
}
|
| 48 |
+
th {
|
| 49 |
+
background-color: #4CAF50;
|
| 50 |
+
color: white;
|
| 51 |
+
}
|
| 52 |
+
tr:nth-child(even) {
|
| 53 |
+
background-color: #f2f2f2;
|
| 54 |
+
}
|
| 55 |
+
.info-grid {
|
| 56 |
+
display: grid;
|
| 57 |
+
grid-template-columns: 1fr 2fr;
|
| 58 |
+
gap: 10px;
|
| 59 |
+
margin: 15px 0;
|
| 60 |
+
}
|
| 61 |
+
.info-label {
|
| 62 |
+
font-weight: bold;
|
| 63 |
+
color: #333;
|
| 64 |
+
}
|
| 65 |
+
.footer {
|
| 66 |
+
text-align: center;
|
| 67 |
+
margin-top: 40px;
|
| 68 |
+
padding-top: 20px;
|
| 69 |
+
border-top: 2px solid #eee;
|
| 70 |
+
color: #666;
|
| 71 |
+
}
|
| 72 |
+
.highlight {
|
| 73 |
+
background-color: #fff3cd;
|
| 74 |
+
padding: 15px;
|
| 75 |
+
border-left: 4px solid #ffc107;
|
| 76 |
+
margin: 15px 0;
|
| 77 |
+
}
|
| 78 |
+
</style>
|
| 79 |
+
</head>
|
| 80 |
+
<body>
|
| 81 |
+
<div class="container">
|
| 82 |
+
<div class="header">
|
| 83 |
+
<h1>🌾 Comprehensive Yearly Farming Plan</h1>
|
| 84 |
+
<p>AI-Generated Agricultural Strategy</p>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<div class="section">
|
| 88 |
+
<h2>Farmer Information</h2>
|
| 89 |
+
<div class="info-grid">
|
| 90 |
+
<div class="info-label">Name:</div>
|
| 91 |
+
<div>PRANIT Ravindra CHILBULE</div>
|
| 92 |
+
<div class="info-label">Contact:</div>
|
| 93 |
+
<div>9763059811</div>
|
| 94 |
+
<div class="info-label">Address:</div>
|
| 95 |
+
<div>N/A</div>
|
| 96 |
+
<div class="info-label">Plan Year:</div>
|
| 97 |
+
<div>2025</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<div class="section">
|
| 102 |
+
<h2>Farm 1: jnjnn</h2>
|
| 103 |
+
<p><em>Plan details could not be processed: Expecting value: line 1 column 1 (char 0)</em></p> </div>
|
| 104 |
+
|
| 105 |
+
<div class="section">
|
| 106 |
+
<h2>Summary & Recommendations</h2>
|
| 107 |
+
<p>Updated plan for 1 farms</p>
|
| 108 |
+
<div class="highlight">
|
| 109 |
+
<strong>📱 Next Steps:</strong> Access your daily advisories through the farmer dashboard. Monitor weather alerts and market prices for optimal farming decisions.
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<div class="footer">
|
| 114 |
+
<p>Generated on: September 06, 2025 at 02:09 PM</p>
|
| 115 |
+
<p>🌱 <strong>Powered by AI Agriculture Assistant</strong></p>
|
| 116 |
+
<p><em>This plan is generated based on AI analysis and should be used in conjunction with local agricultural expertise.</em></p>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
</body>
|
| 120 |
+
</html>
|
generated_pdfs/yearly_plan_PRANIT Ravindra CHILBULE_20250906_142024.html
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
<!DOCTYPE html>
|
| 3 |
+
<html lang="en">
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Yearly Farming Plan - PRANIT Ravindra CHILBULE</title>
|
| 8 |
+
<style>
|
| 9 |
+
body {
|
| 10 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 11 |
+
line-height: 1.6;
|
| 12 |
+
margin: 20px;
|
| 13 |
+
background-color: #f9f9f9;
|
| 14 |
+
}
|
| 15 |
+
.container {
|
| 16 |
+
max-width: 800px;
|
| 17 |
+
margin: 0 auto;
|
| 18 |
+
background: white;
|
| 19 |
+
padding: 30px;
|
| 20 |
+
border-radius: 10px;
|
| 21 |
+
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
| 22 |
+
}
|
| 23 |
+
.header {
|
| 24 |
+
text-align: center;
|
| 25 |
+
color: #2c5530;
|
| 26 |
+
border-bottom: 3px solid #4CAF50;
|
| 27 |
+
padding-bottom: 20px;
|
| 28 |
+
margin-bottom: 30px;
|
| 29 |
+
}
|
| 30 |
+
.section {
|
| 31 |
+
margin-bottom: 30px;
|
| 32 |
+
}
|
| 33 |
+
.section h2 {
|
| 34 |
+
color: #1976d2;
|
| 35 |
+
border-left: 4px solid #4CAF50;
|
| 36 |
+
padding-left: 15px;
|
| 37 |
+
}
|
| 38 |
+
table {
|
| 39 |
+
width: 100%;
|
| 40 |
+
border-collapse: collapse;
|
| 41 |
+
margin: 15px 0;
|
| 42 |
+
}
|
| 43 |
+
th, td {
|
| 44 |
+
padding: 12px;
|
| 45 |
+
text-align: left;
|
| 46 |
+
border: 1px solid #ddd;
|
| 47 |
+
}
|
| 48 |
+
th {
|
| 49 |
+
background-color: #4CAF50;
|
| 50 |
+
color: white;
|
| 51 |
+
}
|
| 52 |
+
tr:nth-child(even) {
|
| 53 |
+
background-color: #f2f2f2;
|
| 54 |
+
}
|
| 55 |
+
.info-grid {
|
| 56 |
+
display: grid;
|
| 57 |
+
grid-template-columns: 1fr 2fr;
|
| 58 |
+
gap: 10px;
|
| 59 |
+
margin: 15px 0;
|
| 60 |
+
}
|
| 61 |
+
.info-label {
|
| 62 |
+
font-weight: bold;
|
| 63 |
+
color: #333;
|
| 64 |
+
}
|
| 65 |
+
.footer {
|
| 66 |
+
text-align: center;
|
| 67 |
+
margin-top: 40px;
|
| 68 |
+
padding-top: 20px;
|
| 69 |
+
border-top: 2px solid #eee;
|
| 70 |
+
color: #666;
|
| 71 |
+
}
|
| 72 |
+
.highlight {
|
| 73 |
+
background-color: #fff3cd;
|
| 74 |
+
padding: 15px;
|
| 75 |
+
border-left: 4px solid #ffc107;
|
| 76 |
+
margin: 15px 0;
|
| 77 |
+
}
|
| 78 |
+
</style>
|
| 79 |
+
</head>
|
| 80 |
+
<body>
|
| 81 |
+
<div class="container">
|
| 82 |
+
<div class="header">
|
| 83 |
+
<h1>🌾 Comprehensive Yearly Farming Plan</h1>
|
| 84 |
+
<p>AI-Generated Agricultural Strategy</p>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<div class="section">
|
| 88 |
+
<h2>Farmer Information</h2>
|
| 89 |
+
<div class="info-grid">
|
| 90 |
+
<div class="info-label">Name:</div>
|
| 91 |
+
<div>PRANIT Ravindra CHILBULE</div>
|
| 92 |
+
<div class="info-label">Contact:</div>
|
| 93 |
+
<div>9763059811</div>
|
| 94 |
+
<div class="info-label">Address:</div>
|
| 95 |
+
<div>N/A</div>
|
| 96 |
+
<div class="info-label">Plan Year:</div>
|
| 97 |
+
<div>2025</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<div class="section">
|
| 102 |
+
<h2>Farm 1: jnjnn</h2>
|
| 103 |
+
<p><em>Plan details could not be processed: Expecting value: line 1 column 1 (char 0)</em></p> </div>
|
| 104 |
+
|
| 105 |
+
<div class="section">
|
| 106 |
+
<h2>Summary & Recommendations</h2>
|
| 107 |
+
<p>Updated plan for 1 farms</p>
|
| 108 |
+
<div class="highlight">
|
| 109 |
+
<strong>📱 Next Steps:</strong> Access your daily advisories through the farmer dashboard. Monitor weather alerts and market prices for optimal farming decisions.
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<div class="footer">
|
| 114 |
+
<p>Generated on: September 06, 2025 at 02:20 PM</p>
|
| 115 |
+
<p>🌱 <strong>Powered by AI Agriculture Assistant</strong></p>
|
| 116 |
+
<p><em>This plan is generated based on AI analysis and should be used in conjunction with local agricultural expertise.</em></p>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
</body>
|
| 120 |
+
</html>
|
generated_pdfs/yearly_plan_PRANIT Ravindra CHILBULE_20250906_142505.html
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
<!DOCTYPE html>
|
| 3 |
+
<html lang="en">
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Yearly Farming Plan - PRANIT Ravindra CHILBULE</title>
|
| 8 |
+
<style>
|
| 9 |
+
body {
|
| 10 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 11 |
+
line-height: 1.6;
|
| 12 |
+
margin: 20px;
|
| 13 |
+
background-color: #f9f9f9;
|
| 14 |
+
}
|
| 15 |
+
.container {
|
| 16 |
+
max-width: 800px;
|
| 17 |
+
margin: 0 auto;
|
| 18 |
+
background: white;
|
| 19 |
+
padding: 30px;
|
| 20 |
+
border-radius: 10px;
|
| 21 |
+
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
| 22 |
+
}
|
| 23 |
+
.header {
|
| 24 |
+
text-align: center;
|
| 25 |
+
color: #2c5530;
|
| 26 |
+
border-bottom: 3px solid #4CAF50;
|
| 27 |
+
padding-bottom: 20px;
|
| 28 |
+
margin-bottom: 30px;
|
| 29 |
+
}
|
| 30 |
+
.section {
|
| 31 |
+
margin-bottom: 30px;
|
| 32 |
+
}
|
| 33 |
+
.section h2 {
|
| 34 |
+
color: #1976d2;
|
| 35 |
+
border-left: 4px solid #4CAF50;
|
| 36 |
+
padding-left: 15px;
|
| 37 |
+
}
|
| 38 |
+
table {
|
| 39 |
+
width: 100%;
|
| 40 |
+
border-collapse: collapse;
|
| 41 |
+
margin: 15px 0;
|
| 42 |
+
}
|
| 43 |
+
th, td {
|
| 44 |
+
padding: 12px;
|
| 45 |
+
text-align: left;
|
| 46 |
+
border: 1px solid #ddd;
|
| 47 |
+
}
|
| 48 |
+
th {
|
| 49 |
+
background-color: #4CAF50;
|
| 50 |
+
color: white;
|
| 51 |
+
}
|
| 52 |
+
tr:nth-child(even) {
|
| 53 |
+
background-color: #f2f2f2;
|
| 54 |
+
}
|
| 55 |
+
.info-grid {
|
| 56 |
+
display: grid;
|
| 57 |
+
grid-template-columns: 1fr 2fr;
|
| 58 |
+
gap: 10px;
|
| 59 |
+
margin: 15px 0;
|
| 60 |
+
}
|
| 61 |
+
.info-label {
|
| 62 |
+
font-weight: bold;
|
| 63 |
+
color: #333;
|
| 64 |
+
}
|
| 65 |
+
.footer {
|
| 66 |
+
text-align: center;
|
| 67 |
+
margin-top: 40px;
|
| 68 |
+
padding-top: 20px;
|
| 69 |
+
border-top: 2px solid #eee;
|
| 70 |
+
color: #666;
|
| 71 |
+
}
|
| 72 |
+
.highlight {
|
| 73 |
+
background-color: #fff3cd;
|
| 74 |
+
padding: 15px;
|
| 75 |
+
border-left: 4px solid #ffc107;
|
| 76 |
+
margin: 15px 0;
|
| 77 |
+
}
|
| 78 |
+
</style>
|
| 79 |
+
</head>
|
| 80 |
+
<body>
|
| 81 |
+
<div class="container">
|
| 82 |
+
<div class="header">
|
| 83 |
+
<h1>🌾 Comprehensive Yearly Farming Plan</h1>
|
| 84 |
+
<p>AI-Generated Agricultural Strategy</p>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<div class="section">
|
| 88 |
+
<h2>Farmer Information</h2>
|
| 89 |
+
<div class="info-grid">
|
| 90 |
+
<div class="info-label">Name:</div>
|
| 91 |
+
<div>PRANIT Ravindra CHILBULE</div>
|
| 92 |
+
<div class="info-label">Contact:</div>
|
| 93 |
+
<div>9763059811</div>
|
| 94 |
+
<div class="info-label">Address:</div>
|
| 95 |
+
<div>N/A</div>
|
| 96 |
+
<div class="info-label">Plan Year:</div>
|
| 97 |
+
<div>2025</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<div class="section">
|
| 102 |
+
<h2>Farm 1: jnjnn</h2>
|
| 103 |
+
<p><em>Plan details could not be processed: Expecting value: line 1 column 1 (char 0)</em></p> </div>
|
| 104 |
+
|
| 105 |
+
<div class="section">
|
| 106 |
+
<h2>Summary & Recommendations</h2>
|
| 107 |
+
<p>Updated plan for 1 farms</p>
|
| 108 |
+
<div class="highlight">
|
| 109 |
+
<strong>📱 Next Steps:</strong> Access your daily advisories through the farmer dashboard. Monitor weather alerts and market prices for optimal farming decisions.
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<div class="footer">
|
| 114 |
+
<p>Generated on: September 06, 2025 at 02:25 PM</p>
|
| 115 |
+
<p>🌱 <strong>Powered by AI Agriculture Assistant</strong></p>
|
| 116 |
+
<p><em>This plan is generated based on AI analysis and should be used in conjunction with local agricultural expertise.</em></p>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
</body>
|
| 120 |
+
</html>
|
generated_pdfs/yearly_plan_PRANIT Ravindra CHILBULE_20250906_172353.html
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
<!DOCTYPE html>
|
| 3 |
+
<html lang="en">
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Yearly Farming Plan - PRANIT Ravindra CHILBULE</title>
|
| 8 |
+
<style>
|
| 9 |
+
body {
|
| 10 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 11 |
+
line-height: 1.6;
|
| 12 |
+
margin: 20px;
|
| 13 |
+
background-color: #f9f9f9;
|
| 14 |
+
}
|
| 15 |
+
.container {
|
| 16 |
+
max-width: 800px;
|
| 17 |
+
margin: 0 auto;
|
| 18 |
+
background: white;
|
| 19 |
+
padding: 30px;
|
| 20 |
+
border-radius: 10px;
|
| 21 |
+
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
| 22 |
+
}
|
| 23 |
+
.header {
|
| 24 |
+
text-align: center;
|
| 25 |
+
color: #2c5530;
|
| 26 |
+
border-bottom: 3px solid #4CAF50;
|
| 27 |
+
padding-bottom: 20px;
|
| 28 |
+
margin-bottom: 30px;
|
| 29 |
+
}
|
| 30 |
+
.section {
|
| 31 |
+
margin-bottom: 30px;
|
| 32 |
+
}
|
| 33 |
+
.section h2 {
|
| 34 |
+
color: #1976d2;
|
| 35 |
+
border-left: 4px solid #4CAF50;
|
| 36 |
+
padding-left: 15px;
|
| 37 |
+
}
|
| 38 |
+
table {
|
| 39 |
+
width: 100%;
|
| 40 |
+
border-collapse: collapse;
|
| 41 |
+
margin: 15px 0;
|
| 42 |
+
}
|
| 43 |
+
th, td {
|
| 44 |
+
padding: 12px;
|
| 45 |
+
text-align: left;
|
| 46 |
+
border: 1px solid #ddd;
|
| 47 |
+
}
|
| 48 |
+
th {
|
| 49 |
+
background-color: #4CAF50;
|
| 50 |
+
color: white;
|
| 51 |
+
}
|
| 52 |
+
tr:nth-child(even) {
|
| 53 |
+
background-color: #f2f2f2;
|
| 54 |
+
}
|
| 55 |
+
.info-grid {
|
| 56 |
+
display: grid;
|
| 57 |
+
grid-template-columns: 1fr 2fr;
|
| 58 |
+
gap: 10px;
|
| 59 |
+
margin: 15px 0;
|
| 60 |
+
}
|
| 61 |
+
.info-label {
|
| 62 |
+
font-weight: bold;
|
| 63 |
+
color: #333;
|
| 64 |
+
}
|
| 65 |
+
.footer {
|
| 66 |
+
text-align: center;
|
| 67 |
+
margin-top: 40px;
|
| 68 |
+
padding-top: 20px;
|
| 69 |
+
border-top: 2px solid #eee;
|
| 70 |
+
color: #666;
|
| 71 |
+
}
|
| 72 |
+
.highlight {
|
| 73 |
+
background-color: #fff3cd;
|
| 74 |
+
padding: 15px;
|
| 75 |
+
border-left: 4px solid #ffc107;
|
| 76 |
+
margin: 15px 0;
|
| 77 |
+
}
|
| 78 |
+
</style>
|
| 79 |
+
</head>
|
| 80 |
+
<body>
|
| 81 |
+
<div class="container">
|
| 82 |
+
<div class="header">
|
| 83 |
+
<h1>🌾 Comprehensive Yearly Farming Plan</h1>
|
| 84 |
+
<p>AI-Generated Agricultural Strategy</p>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<div class="section">
|
| 88 |
+
<h2>Farmer Information</h2>
|
| 89 |
+
<div class="info-grid">
|
| 90 |
+
<div class="info-label">Name:</div>
|
| 91 |
+
<div>PRANIT Ravindra CHILBULE</div>
|
| 92 |
+
<div class="info-label">Contact:</div>
|
| 93 |
+
<div>9763059811</div>
|
| 94 |
+
<div class="info-label">Address:</div>
|
| 95 |
+
<div>N/A</div>
|
| 96 |
+
<div class="info-label">Plan Year:</div>
|
| 97 |
+
<div>2025</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<div class="section">
|
| 102 |
+
<h2>Farm 1: jnjnn</h2>
|
| 103 |
+
<p><em>Plan details could not be processed: Expecting value: line 1 column 1 (char 0)</em></p> </div>
|
| 104 |
+
|
| 105 |
+
<div class="section">
|
| 106 |
+
<h2>Summary & Recommendations</h2>
|
| 107 |
+
<p>Updated plan for 1 farms</p>
|
| 108 |
+
<div class="highlight">
|
| 109 |
+
<strong>📱 Next Steps:</strong> Access your daily advisories through the farmer dashboard. Monitor weather alerts and market prices for optimal farming decisions.
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<div class="footer">
|
| 114 |
+
<p>Generated on: September 06, 2025 at 05:23 PM</p>
|
| 115 |
+
<p>🌱 <strong>Powered by AI Agriculture Assistant</strong></p>
|
| 116 |
+
<p><em>This plan is generated based on AI analysis and should be used in conjunction with local agricultural expertise.</em></p>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
</body>
|
| 120 |
+
</html>
|
generated_pdfs/yearly_plan_PRANIT Ravindra CHILBULE_20250906_172354.html
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
<!DOCTYPE html>
|
| 3 |
+
<html lang="en">
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Yearly Farming Plan - PRANIT Ravindra CHILBULE</title>
|
| 8 |
+
<style>
|
| 9 |
+
body {
|
| 10 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 11 |
+
line-height: 1.6;
|
| 12 |
+
margin: 20px;
|
| 13 |
+
background-color: #f9f9f9;
|
| 14 |
+
}
|
| 15 |
+
.container {
|
| 16 |
+
max-width: 800px;
|
| 17 |
+
margin: 0 auto;
|
| 18 |
+
background: white;
|
| 19 |
+
padding: 30px;
|
| 20 |
+
border-radius: 10px;
|
| 21 |
+
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
| 22 |
+
}
|
| 23 |
+
.header {
|
| 24 |
+
text-align: center;
|
| 25 |
+
color: #2c5530;
|
| 26 |
+
border-bottom: 3px solid #4CAF50;
|
| 27 |
+
padding-bottom: 20px;
|
| 28 |
+
margin-bottom: 30px;
|
| 29 |
+
}
|
| 30 |
+
.section {
|
| 31 |
+
margin-bottom: 30px;
|
| 32 |
+
}
|
| 33 |
+
.section h2 {
|
| 34 |
+
color: #1976d2;
|
| 35 |
+
border-left: 4px solid #4CAF50;
|
| 36 |
+
padding-left: 15px;
|
| 37 |
+
}
|
| 38 |
+
table {
|
| 39 |
+
width: 100%;
|
| 40 |
+
border-collapse: collapse;
|
| 41 |
+
margin: 15px 0;
|
| 42 |
+
}
|
| 43 |
+
th, td {
|
| 44 |
+
padding: 12px;
|
| 45 |
+
text-align: left;
|
| 46 |
+
border: 1px solid #ddd;
|
| 47 |
+
}
|
| 48 |
+
th {
|
| 49 |
+
background-color: #4CAF50;
|
| 50 |
+
color: white;
|
| 51 |
+
}
|
| 52 |
+
tr:nth-child(even) {
|
| 53 |
+
background-color: #f2f2f2;
|
| 54 |
+
}
|
| 55 |
+
.info-grid {
|
| 56 |
+
display: grid;
|
| 57 |
+
grid-template-columns: 1fr 2fr;
|
| 58 |
+
gap: 10px;
|
| 59 |
+
margin: 15px 0;
|
| 60 |
+
}
|
| 61 |
+
.info-label {
|
| 62 |
+
font-weight: bold;
|
| 63 |
+
color: #333;
|
| 64 |
+
}
|
| 65 |
+
.footer {
|
| 66 |
+
text-align: center;
|
| 67 |
+
margin-top: 40px;
|
| 68 |
+
padding-top: 20px;
|
| 69 |
+
border-top: 2px solid #eee;
|
| 70 |
+
color: #666;
|
| 71 |
+
}
|
| 72 |
+
.highlight {
|
| 73 |
+
background-color: #fff3cd;
|
| 74 |
+
padding: 15px;
|
| 75 |
+
border-left: 4px solid #ffc107;
|
| 76 |
+
margin: 15px 0;
|
| 77 |
+
}
|
| 78 |
+
</style>
|
| 79 |
+
</head>
|
| 80 |
+
<body>
|
| 81 |
+
<div class="container">
|
| 82 |
+
<div class="header">
|
| 83 |
+
<h1>🌾 Comprehensive Yearly Farming Plan</h1>
|
| 84 |
+
<p>AI-Generated Agricultural Strategy</p>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<div class="section">
|
| 88 |
+
<h2>Farmer Information</h2>
|
| 89 |
+
<div class="info-grid">
|
| 90 |
+
<div class="info-label">Name:</div>
|
| 91 |
+
<div>PRANIT Ravindra CHILBULE</div>
|
| 92 |
+
<div class="info-label">Contact:</div>
|
| 93 |
+
<div>9763059811</div>
|
| 94 |
+
<div class="info-label">Address:</div>
|
| 95 |
+
<div>N/A</div>
|
| 96 |
+
<div class="info-label">Plan Year:</div>
|
| 97 |
+
<div>2025</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<div class="section">
|
| 102 |
+
<h2>Farm 1: jnjnn</h2>
|
| 103 |
+
<p><em>Plan details could not be processed: Expecting value: line 1 column 1 (char 0)</em></p> </div>
|
| 104 |
+
|
| 105 |
+
<div class="section">
|
| 106 |
+
<h2>Summary & Recommendations</h2>
|
| 107 |
+
<p>Updated plan for 1 farms</p>
|
| 108 |
+
<div class="highlight">
|
| 109 |
+
<strong>📱 Next Steps:</strong> Access your daily advisories through the farmer dashboard. Monitor weather alerts and market prices for optimal farming decisions.
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<div class="footer">
|
| 114 |
+
<p>Generated on: September 06, 2025 at 05:23 PM</p>
|
| 115 |
+
<p>🌱 <strong>Powered by AI Agriculture Assistant</strong></p>
|
| 116 |
+
<p><em>This plan is generated based on AI analysis and should be used in conjunction with local agricultural expertise.</em></p>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
</body>
|
| 120 |
+
</html>
|
generated_pdfs/yearly_plan_PRANIT Ravindra CHILBULE_20250906_172448.html
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
<!DOCTYPE html>
|
| 3 |
+
<html lang="en">
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Yearly Farming Plan - PRANIT Ravindra CHILBULE</title>
|
| 8 |
+
<style>
|
| 9 |
+
body {
|
| 10 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 11 |
+
line-height: 1.6;
|
| 12 |
+
margin: 20px;
|
| 13 |
+
background-color: #f9f9f9;
|
| 14 |
+
}
|
| 15 |
+
.container {
|
| 16 |
+
max-width: 800px;
|
| 17 |
+
margin: 0 auto;
|
| 18 |
+
background: white;
|
| 19 |
+
padding: 30px;
|
| 20 |
+
border-radius: 10px;
|
| 21 |
+
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
| 22 |
+
}
|
| 23 |
+
.header {
|
| 24 |
+
text-align: center;
|
| 25 |
+
color: #2c5530;
|
| 26 |
+
border-bottom: 3px solid #4CAF50;
|
| 27 |
+
padding-bottom: 20px;
|
| 28 |
+
margin-bottom: 30px;
|
| 29 |
+
}
|
| 30 |
+
.section {
|
| 31 |
+
margin-bottom: 30px;
|
| 32 |
+
}
|
| 33 |
+
.section h2 {
|
| 34 |
+
color: #1976d2;
|
| 35 |
+
border-left: 4px solid #4CAF50;
|
| 36 |
+
padding-left: 15px;
|
| 37 |
+
}
|
| 38 |
+
table {
|
| 39 |
+
width: 100%;
|
| 40 |
+
border-collapse: collapse;
|
| 41 |
+
margin: 15px 0;
|
| 42 |
+
}
|
| 43 |
+
th, td {
|
| 44 |
+
padding: 12px;
|
| 45 |
+
text-align: left;
|
| 46 |
+
border: 1px solid #ddd;
|
| 47 |
+
}
|
| 48 |
+
th {
|
| 49 |
+
background-color: #4CAF50;
|
| 50 |
+
color: white;
|
| 51 |
+
}
|
| 52 |
+
tr:nth-child(even) {
|
| 53 |
+
background-color: #f2f2f2;
|
| 54 |
+
}
|
| 55 |
+
.info-grid {
|
| 56 |
+
display: grid;
|
| 57 |
+
grid-template-columns: 1fr 2fr;
|
| 58 |
+
gap: 10px;
|
| 59 |
+
margin: 15px 0;
|
| 60 |
+
}
|
| 61 |
+
.info-label {
|
| 62 |
+
font-weight: bold;
|
| 63 |
+
color: #333;
|
| 64 |
+
}
|
| 65 |
+
.footer {
|
| 66 |
+
text-align: center;
|
| 67 |
+
margin-top: 40px;
|
| 68 |
+
padding-top: 20px;
|
| 69 |
+
border-top: 2px solid #eee;
|
| 70 |
+
color: #666;
|
| 71 |
+
}
|
| 72 |
+
.highlight {
|
| 73 |
+
background-color: #fff3cd;
|
| 74 |
+
padding: 15px;
|
| 75 |
+
border-left: 4px solid #ffc107;
|
| 76 |
+
margin: 15px 0;
|
| 77 |
+
}
|
| 78 |
+
</style>
|
| 79 |
+
</head>
|
| 80 |
+
<body>
|
| 81 |
+
<div class="container">
|
| 82 |
+
<div class="header">
|
| 83 |
+
<h1>🌾 Comprehensive Yearly Farming Plan</h1>
|
| 84 |
+
<p>AI-Generated Agricultural Strategy</p>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<div class="section">
|
| 88 |
+
<h2>Farmer Information</h2>
|
| 89 |
+
<div class="info-grid">
|
| 90 |
+
<div class="info-label">Name:</div>
|
| 91 |
+
<div>PRANIT Ravindra CHILBULE</div>
|
| 92 |
+
<div class="info-label">Contact:</div>
|
| 93 |
+
<div>9763059811</div>
|
| 94 |
+
<div class="info-label">Address:</div>
|
| 95 |
+
<div>N/A</div>
|
| 96 |
+
<div class="info-label">Plan Year:</div>
|
| 97 |
+
<div>2025</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<div class="section">
|
| 102 |
+
<h2>Farm 1: jnjnn</h2>
|
| 103 |
+
<p><em>Plan details could not be processed: Expecting value: line 1 column 1 (char 0)</em></p> </div>
|
| 104 |
+
|
| 105 |
+
<div class="section">
|
| 106 |
+
<h2>Summary & Recommendations</h2>
|
| 107 |
+
<p>AI-Generated Comprehensive Yearly Plan for jnjnn (12.0 acres)</p>
|
| 108 |
+
<div class="highlight">
|
| 109 |
+
<strong>📱 Next Steps:</strong> Access your daily advisories through the farmer dashboard. Monitor weather alerts and market prices for optimal farming decisions.
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<div class="footer">
|
| 114 |
+
<p>Generated on: September 06, 2025 at 05:24 PM</p>
|
| 115 |
+
<p>🌱 <strong>Powered by AI Agriculture Assistant</strong></p>
|
| 116 |
+
<p><em>This plan is generated based on AI analysis and should be used in conjunction with local agricultural expertise.</em></p>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
</body>
|
| 120 |
+
</html>
|
generated_pdfs/yearly_plan_PRANIT Ravindra CHILBULE_20250906_172530.html
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
<!DOCTYPE html>
|
| 3 |
+
<html lang="en">
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Yearly Farming Plan - PRANIT Ravindra CHILBULE</title>
|
| 8 |
+
<style>
|
| 9 |
+
body {
|
| 10 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 11 |
+
line-height: 1.6;
|
| 12 |
+
margin: 20px;
|
| 13 |
+
background-color: #f9f9f9;
|
| 14 |
+
}
|
| 15 |
+
.container {
|
| 16 |
+
max-width: 800px;
|
| 17 |
+
margin: 0 auto;
|
| 18 |
+
background: white;
|
| 19 |
+
padding: 30px;
|
| 20 |
+
border-radius: 10px;
|
| 21 |
+
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
| 22 |
+
}
|
| 23 |
+
.header {
|
| 24 |
+
text-align: center;
|
| 25 |
+
color: #2c5530;
|
| 26 |
+
border-bottom: 3px solid #4CAF50;
|
| 27 |
+
padding-bottom: 20px;
|
| 28 |
+
margin-bottom: 30px;
|
| 29 |
+
}
|
| 30 |
+
.section {
|
| 31 |
+
margin-bottom: 30px;
|
| 32 |
+
}
|
| 33 |
+
.section h2 {
|
| 34 |
+
color: #1976d2;
|
| 35 |
+
border-left: 4px solid #4CAF50;
|
| 36 |
+
padding-left: 15px;
|
| 37 |
+
}
|
| 38 |
+
table {
|
| 39 |
+
width: 100%;
|
| 40 |
+
border-collapse: collapse;
|
| 41 |
+
margin: 15px 0;
|
| 42 |
+
}
|
| 43 |
+
th, td {
|
| 44 |
+
padding: 12px;
|
| 45 |
+
text-align: left;
|
| 46 |
+
border: 1px solid #ddd;
|
| 47 |
+
}
|
| 48 |
+
th {
|
| 49 |
+
background-color: #4CAF50;
|
| 50 |
+
color: white;
|
| 51 |
+
}
|
| 52 |
+
tr:nth-child(even) {
|
| 53 |
+
background-color: #f2f2f2;
|
| 54 |
+
}
|
| 55 |
+
.info-grid {
|
| 56 |
+
display: grid;
|
| 57 |
+
grid-template-columns: 1fr 2fr;
|
| 58 |
+
gap: 10px;
|
| 59 |
+
margin: 15px 0;
|
| 60 |
+
}
|
| 61 |
+
.info-label {
|
| 62 |
+
font-weight: bold;
|
| 63 |
+
color: #333;
|
| 64 |
+
}
|
| 65 |
+
.footer {
|
| 66 |
+
text-align: center;
|
| 67 |
+
margin-top: 40px;
|
| 68 |
+
padding-top: 20px;
|
| 69 |
+
border-top: 2px solid #eee;
|
| 70 |
+
color: #666;
|
| 71 |
+
}
|
| 72 |
+
.highlight {
|
| 73 |
+
background-color: #fff3cd;
|
| 74 |
+
padding: 15px;
|
| 75 |
+
border-left: 4px solid #ffc107;
|
| 76 |
+
margin: 15px 0;
|
| 77 |
+
}
|
| 78 |
+
</style>
|
| 79 |
+
</head>
|
| 80 |
+
<body>
|
| 81 |
+
<div class="container">
|
| 82 |
+
<div class="header">
|
| 83 |
+
<h1>🌾 Comprehensive Yearly Farming Plan</h1>
|
| 84 |
+
<p>AI-Generated Agricultural Strategy</p>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<div class="section">
|
| 88 |
+
<h2>Farmer Information</h2>
|
| 89 |
+
<div class="info-grid">
|
| 90 |
+
<div class="info-label">Name:</div>
|
| 91 |
+
<div>PRANIT Ravindra CHILBULE</div>
|
| 92 |
+
<div class="info-label">Contact:</div>
|
| 93 |
+
<div>9763059811</div>
|
| 94 |
+
<div class="info-label">Address:</div>
|
| 95 |
+
<div>N/A</div>
|
| 96 |
+
<div class="info-label">Plan Year:</div>
|
| 97 |
+
<div>2025</div>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
|
| 101 |
+
<div class="section">
|
| 102 |
+
<h2>Farm 1: jnjnn</h2>
|
| 103 |
+
<p><em>Plan details could not be processed: Expecting value: line 1 column 1 (char 0)</em></p> </div>
|
| 104 |
+
|
| 105 |
+
<div class="section">
|
| 106 |
+
<h2>Summary & Recommendations</h2>
|
| 107 |
+
<p>AI-Generated Comprehensive Yearly Plan for jnjnn (12.0 acres)</p>
|
| 108 |
+
<div class="highlight">
|
| 109 |
+
<strong>📱 Next Steps:</strong> Access your daily advisories through the farmer dashboard. Monitor weather alerts and market prices for optimal farming decisions.
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<div class="footer">
|
| 114 |
+
<p>Generated on: September 06, 2025 at 05:25 PM</p>
|
| 115 |
+
<p>🌱 <strong>Powered by AI Agriculture Assistant</strong></p>
|
| 116 |
+
<p><em>This plan is generated based on AI analysis and should be used in conjunction with local agricultural expertise.</em></p>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
</body>
|
| 120 |
+
</html>
|
instance/farm_management.db
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9951e724cfbaef1c93506d2a86a6b4594db68853b8f499d7580ffe714994c1c3
|
| 3 |
+
size 180224
|
instance/farms.db
ADDED
|
Binary file (24.6 kB). View file
|
|
|
market_price_service.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import json
|
| 3 |
+
from datetime import datetime, date, timedelta
|
| 4 |
+
from typing import Dict, List, Optional
|
| 5 |
+
import logging
|
| 6 |
+
from models import MarketPrice, db
|
| 7 |
+
|
| 8 |
+
# Configure logging
|
| 9 |
+
logging.basicConfig(level=logging.INFO)
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
class MarketPriceService:
|
| 13 |
+
"""Service for fetching and managing crop market prices"""
|
| 14 |
+
|
| 15 |
+
def __init__(self):
|
| 16 |
+
# Government API endpoints (these are examples - replace with actual APIs)
|
| 17 |
+
self.government_api = "https://api.data.gov.in/resource/9ef84268-d588-465a-a308-a864a43d0070"
|
| 18 |
+
self.backup_sources = [
|
| 19 |
+
"https://agmarknet.gov.in/", # Agricultural Marketing Division
|
| 20 |
+
"https://enam.gov.in/" # National Agriculture Market
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
def fetch_and_update_prices(self, crops: List[str] = None, states: List[str] = None) -> bool:
|
| 24 |
+
"""Fetch latest market prices and update database"""
|
| 25 |
+
try:
|
| 26 |
+
# Default crops if none specified
|
| 27 |
+
if not crops:
|
| 28 |
+
crops = [
|
| 29 |
+
'Rice', 'Wheat', 'Maize', 'Sugarcane', 'Cotton', 'Soybean',
|
| 30 |
+
'Groundnut', 'Sunflower', 'Mustard', 'Gram', 'Arhar', 'Moong',
|
| 31 |
+
'Masoor', 'Onion', 'Potato', 'Tomato', 'Chilli', 'Turmeric'
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
# Default states if none specified
|
| 35 |
+
if not states:
|
| 36 |
+
states = [
|
| 37 |
+
'Maharashtra', 'Uttar Pradesh', 'Karnataka', 'Gujarat',
|
| 38 |
+
'Rajasthan', 'Madhya Pradesh', 'Tamil Nadu', 'Andhra Pradesh'
|
| 39 |
+
]
|
| 40 |
+
|
| 41 |
+
updated_count = 0
|
| 42 |
+
|
| 43 |
+
# Try government API first
|
| 44 |
+
updated_count += self._fetch_from_government_api(crops, states)
|
| 45 |
+
|
| 46 |
+
# If government API fails, use fallback data
|
| 47 |
+
if updated_count == 0:
|
| 48 |
+
updated_count += self._generate_fallback_prices(crops, states)
|
| 49 |
+
|
| 50 |
+
logger.info(f"Updated {updated_count} market price records")
|
| 51 |
+
return updated_count > 0
|
| 52 |
+
|
| 53 |
+
except Exception as e:
|
| 54 |
+
logger.error(f"Error updating market prices: {str(e)}")
|
| 55 |
+
return False
|
| 56 |
+
|
| 57 |
+
def _fetch_from_government_api(self, crops: List[str], states: List[str]) -> int:
|
| 58 |
+
"""Fetch prices from government API"""
|
| 59 |
+
try:
|
| 60 |
+
# This is a placeholder - replace with actual government API implementation
|
| 61 |
+
# The actual API would require proper authentication and parameters
|
| 62 |
+
|
| 63 |
+
# For demonstration, we'll generate realistic data
|
| 64 |
+
return self._generate_realistic_prices(crops, states)
|
| 65 |
+
|
| 66 |
+
except Exception as e:
|
| 67 |
+
logger.error(f"Government API fetch failed: {str(e)}")
|
| 68 |
+
return 0
|
| 69 |
+
|
| 70 |
+
def _generate_realistic_prices(self, crops: List[str], states: List[str]) -> int:
|
| 71 |
+
"""Generate realistic market prices based on typical market rates"""
|
| 72 |
+
|
| 73 |
+
# Base prices per quintal (in INR) - these are approximate market rates
|
| 74 |
+
base_prices = {
|
| 75 |
+
'Rice': {'min': 1800, 'max': 2500, 'avg': 2100},
|
| 76 |
+
'Wheat': {'min': 1900, 'max': 2300, 'avg': 2100},
|
| 77 |
+
'Maize': {'min': 1400, 'max': 1800, 'avg': 1600},
|
| 78 |
+
'Sugarcane': {'min': 250, 'max': 350, 'avg': 300}, # per ton
|
| 79 |
+
'Cotton': {'min': 5500, 'max': 7500, 'avg': 6500},
|
| 80 |
+
'Soybean': {'min': 3500, 'max': 4500, 'avg': 4000},
|
| 81 |
+
'Groundnut': {'min': 4500, 'max': 6000, 'avg': 5250},
|
| 82 |
+
'Sunflower': {'min': 4000, 'max': 5500, 'avg': 4750},
|
| 83 |
+
'Mustard': {'min': 4200, 'max': 5200, 'avg': 4700},
|
| 84 |
+
'Gram': {'min': 4500, 'max': 6000, 'avg': 5250},
|
| 85 |
+
'Arhar': {'min': 5500, 'max': 7000, 'avg': 6250},
|
| 86 |
+
'Moong': {'min': 6000, 'max': 8000, 'avg': 7000},
|
| 87 |
+
'Masoor': {'min': 4500, 'max': 6500, 'avg': 5500},
|
| 88 |
+
'Onion': {'min': 800, 'max': 2500, 'avg': 1650},
|
| 89 |
+
'Potato': {'min': 600, 'max': 1800, 'avg': 1200},
|
| 90 |
+
'Tomato': {'min': 800, 'max': 3000, 'avg': 1900},
|
| 91 |
+
'Chilli': {'min': 8000, 'max': 15000, 'avg': 11500},
|
| 92 |
+
'Turmeric': {'min': 7000, 'max': 12000, 'avg': 9500}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
markets_by_state = {
|
| 96 |
+
'Maharashtra': ['Mumbai', 'Pune', 'Nashik', 'Nagpur', 'Aurangabad'],
|
| 97 |
+
'Uttar Pradesh': ['Lucknow', 'Kanpur', 'Agra', 'Varanasi', 'Meerut'],
|
| 98 |
+
'Karnataka': ['Bangalore', 'Mysore', 'Hubli', 'Belgaum', 'Mangalore'],
|
| 99 |
+
'Gujarat': ['Ahmedabad', 'Surat', 'Vadodara', 'Rajkot', 'Bhavnagar'],
|
| 100 |
+
'Rajasthan': ['Jaipur', 'Jodhpur', 'Kota', 'Bikaner', 'Udaipur'],
|
| 101 |
+
'Madhya Pradesh': ['Bhopal', 'Indore', 'Gwalior', 'Jabalpur', 'Ujjain'],
|
| 102 |
+
'Tamil Nadu': ['Chennai', 'Coimbatore', 'Madurai', 'Salem', 'Trichy'],
|
| 103 |
+
'Andhra Pradesh': ['Hyderabad', 'Vijayawada', 'Visakhapatnam', 'Guntur', 'Tirupati']
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
updated_count = 0
|
| 107 |
+
today = date.today()
|
| 108 |
+
|
| 109 |
+
for crop in crops:
|
| 110 |
+
if crop not in base_prices:
|
| 111 |
+
continue
|
| 112 |
+
|
| 113 |
+
for state in states:
|
| 114 |
+
if state not in markets_by_state:
|
| 115 |
+
continue
|
| 116 |
+
|
| 117 |
+
for market in markets_by_state[state][:3]: # Top 3 markets per state
|
| 118 |
+
try:
|
| 119 |
+
# Add some market variation (±20%)
|
| 120 |
+
base = base_prices[crop]
|
| 121 |
+
variation = 0.8 + (hash(f"{crop}{state}{market}") % 40) / 100 # 0.8 to 1.2
|
| 122 |
+
|
| 123 |
+
min_price = int(base['min'] * variation)
|
| 124 |
+
max_price = int(base['max'] * variation)
|
| 125 |
+
avg_price = int(base['avg'] * variation)
|
| 126 |
+
|
| 127 |
+
# Check if price already exists for today
|
| 128 |
+
existing = MarketPrice.query.filter_by(
|
| 129 |
+
crop_name=crop,
|
| 130 |
+
market_name=market,
|
| 131 |
+
state=state,
|
| 132 |
+
price_date=today
|
| 133 |
+
).first()
|
| 134 |
+
|
| 135 |
+
if not existing:
|
| 136 |
+
price_record = MarketPrice(
|
| 137 |
+
crop_name=crop,
|
| 138 |
+
market_name=market,
|
| 139 |
+
state=state,
|
| 140 |
+
district=market, # Simplified
|
| 141 |
+
min_price=min_price,
|
| 142 |
+
max_price=max_price,
|
| 143 |
+
avg_price=avg_price,
|
| 144 |
+
price_unit='per quintal' if crop != 'Sugarcane' else 'per ton',
|
| 145 |
+
price_date=today,
|
| 146 |
+
source='market_api'
|
| 147 |
+
)
|
| 148 |
+
db.session.add(price_record)
|
| 149 |
+
updated_count += 1
|
| 150 |
+
|
| 151 |
+
except Exception as e:
|
| 152 |
+
logger.error(f"Error creating price record for {crop} in {market}: {str(e)}")
|
| 153 |
+
continue
|
| 154 |
+
|
| 155 |
+
try:
|
| 156 |
+
db.session.commit()
|
| 157 |
+
return updated_count
|
| 158 |
+
except Exception as e:
|
| 159 |
+
logger.error(f"Error committing price records: {str(e)}")
|
| 160 |
+
db.session.rollback()
|
| 161 |
+
return 0
|
| 162 |
+
|
| 163 |
+
def _generate_fallback_prices(self, crops: List[str], states: List[str]) -> int:
|
| 164 |
+
"""Generate fallback prices when APIs are unavailable"""
|
| 165 |
+
return self._generate_realistic_prices(crops, states)
|
| 166 |
+
|
| 167 |
+
def get_latest_prices(self, crop_name: str, state: str = None, limit: int = 10) -> List[MarketPrice]:
|
| 168 |
+
"""Get latest prices for a crop"""
|
| 169 |
+
query = MarketPrice.query.filter_by(crop_name=crop_name)
|
| 170 |
+
|
| 171 |
+
if state:
|
| 172 |
+
query = query.filter_by(state=state)
|
| 173 |
+
|
| 174 |
+
return query.order_by(MarketPrice.price_date.desc()).limit(limit).all()
|
| 175 |
+
|
| 176 |
+
def get_price_trends(self, crop_name: str, days: int = 30) -> Dict:
|
| 177 |
+
"""Get price trends for a crop over specified days"""
|
| 178 |
+
from_date = date.today() - timedelta(days=days)
|
| 179 |
+
|
| 180 |
+
prices = MarketPrice.query.filter(
|
| 181 |
+
MarketPrice.crop_name == crop_name,
|
| 182 |
+
MarketPrice.price_date >= from_date
|
| 183 |
+
).order_by(MarketPrice.price_date.desc()).all()
|
| 184 |
+
|
| 185 |
+
if not prices:
|
| 186 |
+
return {'trend': 'no_data', 'prices': []}
|
| 187 |
+
|
| 188 |
+
# Calculate trend
|
| 189 |
+
recent_avg = sum(p.avg_price for p in prices[:7]) / min(7, len(prices))
|
| 190 |
+
older_avg = sum(p.avg_price for p in prices[7:14]) / max(1, min(7, len(prices) - 7))
|
| 191 |
+
|
| 192 |
+
if recent_avg > older_avg * 1.05:
|
| 193 |
+
trend = 'rising'
|
| 194 |
+
elif recent_avg < older_avg * 0.95:
|
| 195 |
+
trend = 'falling'
|
| 196 |
+
else:
|
| 197 |
+
trend = 'stable'
|
| 198 |
+
|
| 199 |
+
return {
|
| 200 |
+
'trend': trend,
|
| 201 |
+
'recent_avg': recent_avg,
|
| 202 |
+
'older_avg': older_avg,
|
| 203 |
+
'prices': [p.as_dict() for p in prices]
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
def get_best_selling_recommendation(self, crop_name: str, farmer_state: str) -> Dict:
|
| 207 |
+
"""Get recommendation for best time and place to sell"""
|
| 208 |
+
|
| 209 |
+
# Get prices from nearby states
|
| 210 |
+
nearby_states = self._get_nearby_states(farmer_state)
|
| 211 |
+
all_states = [farmer_state] + nearby_states
|
| 212 |
+
|
| 213 |
+
best_prices = []
|
| 214 |
+
for state in all_states:
|
| 215 |
+
latest_prices = self.get_latest_prices(crop_name, state, 5)
|
| 216 |
+
if latest_prices:
|
| 217 |
+
avg_price = sum(p.avg_price for p in latest_prices) / len(latest_prices)
|
| 218 |
+
best_prices.append({
|
| 219 |
+
'state': state,
|
| 220 |
+
'avg_price': avg_price,
|
| 221 |
+
'markets': [p.market_name for p in latest_prices[:3]]
|
| 222 |
+
})
|
| 223 |
+
|
| 224 |
+
if not best_prices:
|
| 225 |
+
return {'recommendation': 'no_data'}
|
| 226 |
+
|
| 227 |
+
# Sort by price
|
| 228 |
+
best_prices.sort(key=lambda x: x['avg_price'], reverse=True)
|
| 229 |
+
|
| 230 |
+
# Get trends
|
| 231 |
+
trends = self.get_price_trends(crop_name)
|
| 232 |
+
|
| 233 |
+
recommendation = {
|
| 234 |
+
'best_market': best_prices[0],
|
| 235 |
+
'trend': trends['trend'],
|
| 236 |
+
'recommendation': self._generate_selling_advice(trends['trend'], best_prices)
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
return recommendation
|
| 240 |
+
|
| 241 |
+
def _get_nearby_states(self, state: str) -> List[str]:
|
| 242 |
+
"""Get nearby states for price comparison"""
|
| 243 |
+
state_neighbors = {
|
| 244 |
+
'Maharashtra': ['Gujarat', 'Karnataka', 'Madhya Pradesh'],
|
| 245 |
+
'Gujarat': ['Maharashtra', 'Rajasthan', 'Madhya Pradesh'],
|
| 246 |
+
'Karnataka': ['Maharashtra', 'Tamil Nadu', 'Andhra Pradesh'],
|
| 247 |
+
'Tamil Nadu': ['Karnataka', 'Andhra Pradesh'],
|
| 248 |
+
'Uttar Pradesh': ['Madhya Pradesh', 'Rajasthan'],
|
| 249 |
+
'Rajasthan': ['Gujarat', 'Madhya Pradesh', 'Uttar Pradesh'],
|
| 250 |
+
'Madhya Pradesh': ['Maharashtra', 'Gujarat', 'Uttar Pradesh', 'Rajasthan'],
|
| 251 |
+
'Andhra Pradesh': ['Karnataka', 'Tamil Nadu']
|
| 252 |
+
}
|
| 253 |
+
return state_neighbors.get(state, [])
|
| 254 |
+
|
| 255 |
+
def _generate_selling_advice(self, trend: str, best_prices: List[Dict]) -> str:
|
| 256 |
+
"""Generate selling advice based on trends and prices"""
|
| 257 |
+
if trend == 'rising':
|
| 258 |
+
return "Prices are rising. Consider waiting a few more days for better rates."
|
| 259 |
+
elif trend == 'falling':
|
| 260 |
+
return "Prices are falling. Sell immediately to avoid further losses."
|
| 261 |
+
else:
|
| 262 |
+
if len(best_prices) > 1 and best_prices[0]['avg_price'] > best_prices[1]['avg_price'] * 1.1:
|
| 263 |
+
return f"Current prices are good. Consider selling in {best_prices[0]['state']} for best rates."
|
| 264 |
+
else:
|
| 265 |
+
return "Prices are stable. Sell when convenient."
|
| 266 |
+
|
| 267 |
+
def get_crop_price(self, crop_name: str, state: str = None) -> Dict:
|
| 268 |
+
"""Get current price for a crop"""
|
| 269 |
+
try:
|
| 270 |
+
# Get latest prices
|
| 271 |
+
latest_prices = self.get_latest_prices(crop_name, state, limit=5)
|
| 272 |
+
|
| 273 |
+
if not latest_prices:
|
| 274 |
+
# No data found, return fallback
|
| 275 |
+
return {
|
| 276 |
+
'crop_type': crop_name,
|
| 277 |
+
'market_name': 'Market data unavailable',
|
| 278 |
+
'price_per_unit': 'N/A',
|
| 279 |
+
'unit': 'per quintal',
|
| 280 |
+
'trend': 'stable',
|
| 281 |
+
'date': date.today().isoformat(),
|
| 282 |
+
'error': 'No price data available'
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
# Calculate average from latest prices
|
| 286 |
+
avg_price = sum(p.avg_price for p in latest_prices) / len(latest_prices)
|
| 287 |
+
latest = latest_prices[0]
|
| 288 |
+
|
| 289 |
+
# Determine trend based on recent prices
|
| 290 |
+
trend = 'stable'
|
| 291 |
+
if len(latest_prices) >= 2:
|
| 292 |
+
recent_avg = sum(p.avg_price for p in latest_prices[:2]) / 2
|
| 293 |
+
older_avg = sum(p.avg_price for p in latest_prices[2:]) / len(latest_prices[2:]) if len(latest_prices) > 2 else recent_avg
|
| 294 |
+
|
| 295 |
+
if recent_avg > older_avg * 1.05:
|
| 296 |
+
trend = 'up'
|
| 297 |
+
elif recent_avg < older_avg * 0.95:
|
| 298 |
+
trend = 'down'
|
| 299 |
+
|
| 300 |
+
return {
|
| 301 |
+
'crop_type': crop_name,
|
| 302 |
+
'market_name': latest.market_name,
|
| 303 |
+
'price_per_unit': f"₹{int(avg_price)}",
|
| 304 |
+
'unit': latest.price_unit,
|
| 305 |
+
'trend': trend,
|
| 306 |
+
'date': latest.price_date.isoformat()
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
except Exception as e:
|
| 310 |
+
logger.error(f"Error getting crop price for {crop_name}: {str(e)}")
|
| 311 |
+
return {
|
| 312 |
+
'crop_type': crop_name,
|
| 313 |
+
'market_name': 'Error',
|
| 314 |
+
'price_per_unit': 'N/A',
|
| 315 |
+
'unit': 'per quintal',
|
| 316 |
+
'trend': 'stable',
|
| 317 |
+
'date': date.today().isoformat(),
|
| 318 |
+
'error': f'Failed to get price: {str(e)}'
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
def refresh_crop_price(self, crop_name: str) -> bool:
|
| 322 |
+
"""Force refresh price data for a specific crop"""
|
| 323 |
+
try:
|
| 324 |
+
# Trigger fresh fetch for this crop
|
| 325 |
+
self.fetch_and_update_prices([crop_name])
|
| 326 |
+
return True
|
| 327 |
+
except Exception as e:
|
| 328 |
+
logger.error(f"Error refreshing price for {crop_name}: {str(e)}")
|
| 329 |
+
return False
|
migrate_db.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Database migration script to add missing columns
|
| 4 |
+
"""
|
| 5 |
+
import sqlite3
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
def migrate_database():
|
| 9 |
+
"""Add missing columns to the database"""
|
| 10 |
+
|
| 11 |
+
# Find the database file
|
| 12 |
+
db_paths = [
|
| 13 |
+
'instance/farm_management.db',
|
| 14 |
+
'farm_management.db',
|
| 15 |
+
'farms.db'
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
db_path = None
|
| 19 |
+
for path in db_paths:
|
| 20 |
+
if os.path.exists(path):
|
| 21 |
+
db_path = path
|
| 22 |
+
break
|
| 23 |
+
|
| 24 |
+
if not db_path:
|
| 25 |
+
print("Database file not found!")
|
| 26 |
+
return False
|
| 27 |
+
|
| 28 |
+
print(f"Found database at: {db_path}")
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
conn = sqlite3.connect(db_path)
|
| 32 |
+
cursor = conn.cursor()
|
| 33 |
+
|
| 34 |
+
# Check if weather_alerts_enabled column exists
|
| 35 |
+
cursor.execute("PRAGMA table_info(farms)")
|
| 36 |
+
columns = [row[1] for row in cursor.fetchall()]
|
| 37 |
+
|
| 38 |
+
if 'weather_alerts_enabled' not in columns:
|
| 39 |
+
print("Adding weather_alerts_enabled column to farms table...")
|
| 40 |
+
cursor.execute("ALTER TABLE farms ADD COLUMN weather_alerts_enabled BOOLEAN DEFAULT 1")
|
| 41 |
+
print("✓ Added weather_alerts_enabled column")
|
| 42 |
+
else:
|
| 43 |
+
print("✓ weather_alerts_enabled column already exists")
|
| 44 |
+
|
| 45 |
+
# Create weather_alerts table if it doesn't exist
|
| 46 |
+
cursor.execute("""
|
| 47 |
+
CREATE TABLE IF NOT EXISTS weather_alerts (
|
| 48 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 49 |
+
farm_id INTEGER,
|
| 50 |
+
alert_type VARCHAR(100),
|
| 51 |
+
severity VARCHAR(50),
|
| 52 |
+
message TEXT,
|
| 53 |
+
recommendations TEXT,
|
| 54 |
+
is_active BOOLEAN DEFAULT 1,
|
| 55 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 56 |
+
FOREIGN KEY (farm_id) REFERENCES farms (id)
|
| 57 |
+
)
|
| 58 |
+
""")
|
| 59 |
+
print("✓ weather_alerts table ready")
|
| 60 |
+
|
| 61 |
+
# Create market_prices table if it doesn't exist
|
| 62 |
+
cursor.execute("""
|
| 63 |
+
CREATE TABLE IF NOT EXISTS market_prices (
|
| 64 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 65 |
+
crop_type VARCHAR(100),
|
| 66 |
+
market_name VARCHAR(200),
|
| 67 |
+
price_per_unit FLOAT,
|
| 68 |
+
unit VARCHAR(50),
|
| 69 |
+
trend VARCHAR(20),
|
| 70 |
+
date DATE,
|
| 71 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 72 |
+
)
|
| 73 |
+
""")
|
| 74 |
+
print("✓ market_prices table ready")
|
| 75 |
+
|
| 76 |
+
# Create disease_detections table if it doesn't exist
|
| 77 |
+
cursor.execute("""
|
| 78 |
+
CREATE TABLE IF NOT EXISTS disease_detections (
|
| 79 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 80 |
+
farm_id INTEGER,
|
| 81 |
+
disease_name VARCHAR(200),
|
| 82 |
+
confidence_score FLOAT,
|
| 83 |
+
treatment_recommendation TEXT,
|
| 84 |
+
image_path VARCHAR(500),
|
| 85 |
+
detected_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 86 |
+
FOREIGN KEY (farm_id) REFERENCES farms (id)
|
| 87 |
+
)
|
| 88 |
+
""")
|
| 89 |
+
print("✓ disease_detections table ready")
|
| 90 |
+
|
| 91 |
+
# Add new livestock fields to farms table
|
| 92 |
+
try:
|
| 93 |
+
# Check if farm_type column exists
|
| 94 |
+
cursor.execute("PRAGMA table_info(farms)")
|
| 95 |
+
columns = [column[1] for column in cursor.fetchall()]
|
| 96 |
+
|
| 97 |
+
if 'farm_type' not in columns:
|
| 98 |
+
cursor.execute("ALTER TABLE farms ADD COLUMN farm_type TEXT DEFAULT 'crop'")
|
| 99 |
+
print("✓ Added farm_type column to farms table")
|
| 100 |
+
|
| 101 |
+
if 'livestock_types' not in columns:
|
| 102 |
+
cursor.execute("ALTER TABLE farms ADD COLUMN livestock_types TEXT")
|
| 103 |
+
print("✓ Added livestock_types column to farms table")
|
| 104 |
+
|
| 105 |
+
if 'livestock_count' not in columns:
|
| 106 |
+
cursor.execute("ALTER TABLE farms ADD COLUMN livestock_count INTEGER")
|
| 107 |
+
print("✓ Added livestock_count column to farms table")
|
| 108 |
+
|
| 109 |
+
if 'housing_type' not in columns:
|
| 110 |
+
cursor.execute("ALTER TABLE farms ADD COLUMN housing_type TEXT")
|
| 111 |
+
print("✓ Added housing_type column to farms table")
|
| 112 |
+
|
| 113 |
+
if 'feeding_system' not in columns:
|
| 114 |
+
cursor.execute("ALTER TABLE farms ADD COLUMN feeding_system TEXT")
|
| 115 |
+
print("✓ Added feeding_system column to farms table")
|
| 116 |
+
|
| 117 |
+
if 'breed_info' not in columns:
|
| 118 |
+
cursor.execute("ALTER TABLE farms ADD COLUMN breed_info TEXT")
|
| 119 |
+
print("✓ Added breed_info column to farms table")
|
| 120 |
+
|
| 121 |
+
except Exception as e:
|
| 122 |
+
print(f"Error adding livestock columns: {e}")
|
| 123 |
+
|
| 124 |
+
print("✓ Multi-sector farm fields ready")
|
| 125 |
+
|
| 126 |
+
except Exception as e:
|
| 127 |
+
print(f"Error updating farms table: {str(e)}")
|
| 128 |
+
|
| 129 |
+
# Create livestock_records table
|
| 130 |
+
try:
|
| 131 |
+
cursor.execute('''
|
| 132 |
+
CREATE TABLE IF NOT EXISTS livestock_records (
|
| 133 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 134 |
+
farm_id INTEGER NOT NULL,
|
| 135 |
+
farmer_id INTEGER NOT NULL,
|
| 136 |
+
animal_id TEXT NOT NULL,
|
| 137 |
+
animal_type TEXT NOT NULL,
|
| 138 |
+
breed TEXT,
|
| 139 |
+
gender TEXT,
|
| 140 |
+
date_of_birth DATE,
|
| 141 |
+
date_acquired DATE NOT NULL DEFAULT CURRENT_DATE,
|
| 142 |
+
acquisition_method TEXT,
|
| 143 |
+
current_weight REAL,
|
| 144 |
+
vaccination_status TEXT,
|
| 145 |
+
health_status TEXT DEFAULT 'healthy',
|
| 146 |
+
breeding_status TEXT,
|
| 147 |
+
milk_production_avg REAL,
|
| 148 |
+
egg_production_avg INTEGER,
|
| 149 |
+
is_active BOOLEAN DEFAULT 1,
|
| 150 |
+
status TEXT DEFAULT 'active',
|
| 151 |
+
notes TEXT,
|
| 152 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 153 |
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 154 |
+
FOREIGN KEY (farm_id) REFERENCES farms (id),
|
| 155 |
+
FOREIGN KEY (farmer_id) REFERENCES farmers (id)
|
| 156 |
+
)
|
| 157 |
+
''')
|
| 158 |
+
print("✓ livestock_records table ready")
|
| 159 |
+
except Exception as e:
|
| 160 |
+
print(f"Error creating livestock_records table: {str(e)}")
|
| 161 |
+
|
| 162 |
+
# Create production_records table
|
| 163 |
+
try:
|
| 164 |
+
cursor.execute('''
|
| 165 |
+
CREATE TABLE IF NOT EXISTS production_records (
|
| 166 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 167 |
+
farm_id INTEGER NOT NULL,
|
| 168 |
+
farmer_id INTEGER NOT NULL,
|
| 169 |
+
livestock_id INTEGER,
|
| 170 |
+
production_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
| 171 |
+
production_type TEXT NOT NULL,
|
| 172 |
+
quantity REAL NOT NULL,
|
| 173 |
+
unit TEXT NOT NULL,
|
| 174 |
+
quality_grade TEXT,
|
| 175 |
+
unit_price REAL,
|
| 176 |
+
total_value REAL,
|
| 177 |
+
buyer_info TEXT,
|
| 178 |
+
notes TEXT,
|
| 179 |
+
weather_conditions TEXT,
|
| 180 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 181 |
+
FOREIGN KEY (farm_id) REFERENCES farms (id),
|
| 182 |
+
FOREIGN KEY (farmer_id) REFERENCES farmers (id),
|
| 183 |
+
FOREIGN KEY (livestock_id) REFERENCES livestock_records (id)
|
| 184 |
+
)
|
| 185 |
+
''')
|
| 186 |
+
print("✓ production_records table ready")
|
| 187 |
+
except Exception as e:
|
| 188 |
+
print(f"Error creating production_records table: {str(e)}")
|
| 189 |
+
|
| 190 |
+
# Create feed_records table
|
| 191 |
+
try:
|
| 192 |
+
cursor.execute('''
|
| 193 |
+
CREATE TABLE IF NOT EXISTS feed_records (
|
| 194 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 195 |
+
farm_id INTEGER NOT NULL,
|
| 196 |
+
farmer_id INTEGER NOT NULL,
|
| 197 |
+
feed_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
| 198 |
+
feed_type TEXT NOT NULL,
|
| 199 |
+
feed_name TEXT NOT NULL,
|
| 200 |
+
quantity REAL NOT NULL,
|
| 201 |
+
unit TEXT NOT NULL DEFAULT 'kg',
|
| 202 |
+
cost_per_unit REAL,
|
| 203 |
+
total_cost REAL,
|
| 204 |
+
supplier TEXT,
|
| 205 |
+
target_animals TEXT,
|
| 206 |
+
animals_count INTEGER,
|
| 207 |
+
protein_content REAL,
|
| 208 |
+
energy_content REAL,
|
| 209 |
+
notes TEXT,
|
| 210 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 211 |
+
FOREIGN KEY (farm_id) REFERENCES farms (id),
|
| 212 |
+
FOREIGN KEY (farmer_id) REFERENCES farmers (id)
|
| 213 |
+
)
|
| 214 |
+
''')
|
| 215 |
+
print("✓ feed_records table ready")
|
| 216 |
+
except Exception as e:
|
| 217 |
+
print(f"Error creating feed_records table: {str(e)}")
|
| 218 |
+
|
| 219 |
+
# Create health_records table
|
| 220 |
+
try:
|
| 221 |
+
cursor.execute('''
|
| 222 |
+
CREATE TABLE IF NOT EXISTS health_records (
|
| 223 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 224 |
+
farm_id INTEGER NOT NULL,
|
| 225 |
+
farmer_id INTEGER NOT NULL,
|
| 226 |
+
livestock_id INTEGER NOT NULL,
|
| 227 |
+
event_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
| 228 |
+
event_type TEXT NOT NULL,
|
| 229 |
+
vaccine_name TEXT,
|
| 230 |
+
vaccine_batch TEXT,
|
| 231 |
+
next_due_date DATE,
|
| 232 |
+
symptoms TEXT,
|
| 233 |
+
diagnosis TEXT,
|
| 234 |
+
treatment_given TEXT,
|
| 235 |
+
medication TEXT,
|
| 236 |
+
dosage TEXT,
|
| 237 |
+
vet_name TEXT,
|
| 238 |
+
vet_contact TEXT,
|
| 239 |
+
cost REAL,
|
| 240 |
+
follow_up_required BOOLEAN DEFAULT 0,
|
| 241 |
+
follow_up_date DATE,
|
| 242 |
+
recovery_status TEXT,
|
| 243 |
+
notes TEXT,
|
| 244 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 245 |
+
FOREIGN KEY (farm_id) REFERENCES farms (id),
|
| 246 |
+
FOREIGN KEY (farmer_id) REFERENCES farmers (id),
|
| 247 |
+
FOREIGN KEY (livestock_id) REFERENCES livestock_records (id)
|
| 248 |
+
)
|
| 249 |
+
''')
|
| 250 |
+
print("✓ health_records table ready")
|
| 251 |
+
except Exception as e:
|
| 252 |
+
print(f"Error creating health_records table: {str(e)}")
|
| 253 |
+
|
| 254 |
+
# Create daily_tasks table
|
| 255 |
+
cursor.execute("""
|
| 256 |
+
CREATE TABLE IF NOT EXISTS daily_tasks (
|
| 257 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 258 |
+
farmer_id INTEGER,
|
| 259 |
+
farm_id INTEGER,
|
| 260 |
+
task_date DATE DEFAULT (date('now')),
|
| 261 |
+
task_type VARCHAR(100) NOT NULL,
|
| 262 |
+
task_title VARCHAR(200) NOT NULL,
|
| 263 |
+
task_description TEXT NOT NULL,
|
| 264 |
+
priority VARCHAR(20) DEFAULT 'medium',
|
| 265 |
+
estimated_duration INTEGER,
|
| 266 |
+
weather_dependent BOOLEAN DEFAULT 0,
|
| 267 |
+
crop_specific VARCHAR(100),
|
| 268 |
+
is_completed BOOLEAN DEFAULT 0,
|
| 269 |
+
completed_at DATETIME,
|
| 270 |
+
completion_notes TEXT,
|
| 271 |
+
completion_rating INTEGER,
|
| 272 |
+
sent_via_telegram BOOLEAN DEFAULT 0,
|
| 273 |
+
telegram_sent_at DATETIME,
|
| 274 |
+
reminder_sent BOOLEAN DEFAULT 0,
|
| 275 |
+
reminder_sent_at DATETIME,
|
| 276 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 277 |
+
created_by VARCHAR(50) DEFAULT 'system',
|
| 278 |
+
FOREIGN KEY (farmer_id) REFERENCES farmers (id),
|
| 279 |
+
FOREIGN KEY (farm_id) REFERENCES farms (id)
|
| 280 |
+
)
|
| 281 |
+
""")
|
| 282 |
+
print("✓ daily_tasks table ready")
|
| 283 |
+
|
| 284 |
+
# Create task_completions table if it doesn't exist
|
| 285 |
+
cursor.execute("""
|
| 286 |
+
CREATE TABLE IF NOT EXISTS task_completions (
|
| 287 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 288 |
+
task_id INTEGER,
|
| 289 |
+
farmer_id INTEGER,
|
| 290 |
+
completed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 291 |
+
completion_method VARCHAR(50) DEFAULT 'dashboard',
|
| 292 |
+
completion_status VARCHAR(50) DEFAULT 'completed',
|
| 293 |
+
notes TEXT,
|
| 294 |
+
rating INTEGER,
|
| 295 |
+
issues_faced TEXT,
|
| 296 |
+
time_taken INTEGER,
|
| 297 |
+
FOREIGN KEY (task_id) REFERENCES daily_tasks (id),
|
| 298 |
+
FOREIGN KEY (farmer_id) REFERENCES farmers (id)
|
| 299 |
+
)
|
| 300 |
+
""")
|
| 301 |
+
print("✓ task_completions table ready")
|
| 302 |
+
|
| 303 |
+
conn.commit()
|
| 304 |
+
conn.close()
|
| 305 |
+
|
| 306 |
+
print("Database migration completed successfully!")
|
| 307 |
+
return True
|
| 308 |
+
|
| 309 |
+
except Exception as e:
|
| 310 |
+
print(f"Error during migration: {e}")
|
| 311 |
+
return False
|
| 312 |
+
|
| 313 |
+
if __name__ == "__main__":
|
| 314 |
+
migrate_database()
|
models.py
ADDED
|
@@ -0,0 +1,775 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask_sqlalchemy import SQLAlchemy
|
| 2 |
+
from flask_login import UserMixin
|
| 3 |
+
from datetime import datetime, date
|
| 4 |
+
from werkzeug.security import generate_password_hash, check_password_hash
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
# Initialize the SQLAlchemy db instance for the app to import
|
| 8 |
+
db = SQLAlchemy()
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class Farmer(UserMixin, db.Model):
|
| 12 |
+
__tablename__ = 'farmers'
|
| 13 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 14 |
+
name = db.Column(db.String(200), nullable=False)
|
| 15 |
+
age = db.Column(db.Integer, nullable=True)
|
| 16 |
+
gender = db.Column(db.String(20), nullable=True)
|
| 17 |
+
aadhaar_id = db.Column(db.String(50), unique=True, nullable=False)
|
| 18 |
+
contact_number = db.Column(db.String(50), nullable=True)
|
| 19 |
+
address = db.Column(db.Text, nullable=True)
|
| 20 |
+
password_hash = db.Column(db.String(255), nullable=False)
|
| 21 |
+
telegram_chat_id = db.Column(db.String(100), nullable=True)
|
| 22 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 23 |
+
|
| 24 |
+
farms = db.relationship('Farm', backref='owner', lazy=True)
|
| 25 |
+
|
| 26 |
+
def set_password(self, password: str):
|
| 27 |
+
self.password_hash = generate_password_hash(password)
|
| 28 |
+
|
| 29 |
+
def check_password(self, password: str) -> bool:
|
| 30 |
+
if not self.password_hash:
|
| 31 |
+
return False
|
| 32 |
+
return check_password_hash(self.password_hash, password)
|
| 33 |
+
|
| 34 |
+
def __repr__(self):
|
| 35 |
+
return f'<Farmer {self.id} {self.name}>'
|
| 36 |
+
|
| 37 |
+
def as_dict(self):
|
| 38 |
+
return {
|
| 39 |
+
'id': self.id,
|
| 40 |
+
'name': self.name,
|
| 41 |
+
'age': self.age,
|
| 42 |
+
'gender': self.gender,
|
| 43 |
+
'aadhaar_id': self.aadhaar_id,
|
| 44 |
+
'contact_number': self.contact_number,
|
| 45 |
+
'address': self.address,
|
| 46 |
+
'telegram_chat_id': self.telegram_chat_id,
|
| 47 |
+
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class Farm(db.Model):
|
| 52 |
+
__tablename__ = 'farms'
|
| 53 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 54 |
+
farmer_id = db.Column(db.Integer, db.ForeignKey('farmers.id'), nullable=False)
|
| 55 |
+
farm_name = db.Column(db.String(200), nullable=False)
|
| 56 |
+
farm_size = db.Column(db.Float, nullable=True) # acres
|
| 57 |
+
|
| 58 |
+
# Multi-sector support
|
| 59 |
+
farm_type = db.Column(db.String(50), default='crop') # crop, dairy, poultry, livestock, fishery, mixed
|
| 60 |
+
|
| 61 |
+
# Crop farming fields
|
| 62 |
+
irrigation_type = db.Column(db.String(100), nullable=True)
|
| 63 |
+
crop_types = db.Column(db.Text, nullable=True) # comma-separated names for quick display
|
| 64 |
+
crop_details = db.Column(db.Text, nullable=True) # JSON string for detailed crop info
|
| 65 |
+
|
| 66 |
+
# Livestock/Dairy/Poultry fields
|
| 67 |
+
livestock_types = db.Column(db.Text, nullable=True) # JSON string: cows, buffaloes, goats, etc.
|
| 68 |
+
livestock_count = db.Column(db.Integer, nullable=True) # Total number of animals
|
| 69 |
+
housing_type = db.Column(db.String(100), nullable=True) # shed, barn, free-range, cages
|
| 70 |
+
feeding_system = db.Column(db.String(100), nullable=True) # automatic, manual, grazing, mixed
|
| 71 |
+
breed_info = db.Column(db.Text, nullable=True) # JSON string for breed details
|
| 72 |
+
|
| 73 |
+
# Common fields
|
| 74 |
+
latitude = db.Column(db.Float, nullable=True)
|
| 75 |
+
longitude = db.Column(db.Float, nullable=True)
|
| 76 |
+
field_coordinates = db.Column(db.Text, nullable=True) # JSON string of coordinates
|
| 77 |
+
weather_alerts_enabled = db.Column(db.Boolean, default=True, nullable=False) # Enable/disable weather alerts
|
| 78 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 79 |
+
|
| 80 |
+
soil_data = db.relationship('SoilData', backref='farm', lazy=True)
|
| 81 |
+
|
| 82 |
+
def set_crop_types(self, types_list):
|
| 83 |
+
try:
|
| 84 |
+
if isinstance(types_list, (list, tuple)):
|
| 85 |
+
self.crop_types = ','.join([str(t).strip() for t in types_list if t])
|
| 86 |
+
else:
|
| 87 |
+
self.crop_types = str(types_list)
|
| 88 |
+
except Exception:
|
| 89 |
+
self.crop_types = None
|
| 90 |
+
|
| 91 |
+
def get_crop_types(self):
|
| 92 |
+
if not self.crop_types:
|
| 93 |
+
return []
|
| 94 |
+
return [t.strip() for t in self.crop_types.split(',') if t.strip()]
|
| 95 |
+
|
| 96 |
+
def set_crop_details(self, details):
|
| 97 |
+
try:
|
| 98 |
+
self.crop_details = json.dumps(details or [])
|
| 99 |
+
except Exception:
|
| 100 |
+
self.crop_details = json.dumps([])
|
| 101 |
+
|
| 102 |
+
def get_crop_details(self):
|
| 103 |
+
try:
|
| 104 |
+
return json.loads(self.crop_details) if self.crop_details else []
|
| 105 |
+
except Exception:
|
| 106 |
+
return []
|
| 107 |
+
|
| 108 |
+
def set_livestock_types(self, livestock_data):
|
| 109 |
+
"""Set livestock types and details as JSON"""
|
| 110 |
+
try:
|
| 111 |
+
self.livestock_types = json.dumps(livestock_data or [])
|
| 112 |
+
except Exception:
|
| 113 |
+
self.livestock_types = json.dumps([])
|
| 114 |
+
|
| 115 |
+
def get_livestock_types(self):
|
| 116 |
+
"""Get livestock types and details as list"""
|
| 117 |
+
try:
|
| 118 |
+
return json.loads(self.livestock_types) if self.livestock_types else []
|
| 119 |
+
except Exception:
|
| 120 |
+
return []
|
| 121 |
+
|
| 122 |
+
def set_breed_info(self, breed_data):
|
| 123 |
+
"""Set breed information as JSON"""
|
| 124 |
+
try:
|
| 125 |
+
self.breed_info = json.dumps(breed_data or {})
|
| 126 |
+
except Exception:
|
| 127 |
+
self.breed_info = json.dumps({})
|
| 128 |
+
|
| 129 |
+
def get_breed_info(self):
|
| 130 |
+
"""Get breed information as dictionary"""
|
| 131 |
+
try:
|
| 132 |
+
return json.loads(self.breed_info) if self.breed_info else {}
|
| 133 |
+
except Exception:
|
| 134 |
+
return {}
|
| 135 |
+
|
| 136 |
+
def get_farm_type_display(self):
|
| 137 |
+
"""Get user-friendly farm type display name"""
|
| 138 |
+
type_mapping = {
|
| 139 |
+
'crop': 'Crop Farming',
|
| 140 |
+
'dairy': 'Dairy Farming',
|
| 141 |
+
'poultry': 'Poultry Farming',
|
| 142 |
+
'livestock': 'Livestock Farming',
|
| 143 |
+
'fishery': 'Fish Farming',
|
| 144 |
+
'mixed': 'Mixed Farming'
|
| 145 |
+
}
|
| 146 |
+
return type_mapping.get(self.farm_type, 'Crop Farming')
|
| 147 |
+
|
| 148 |
+
def __repr__(self):
|
| 149 |
+
return f'<Farm {self.id} {self.farm_name}>'
|
| 150 |
+
|
| 151 |
+
def as_dict(self):
|
| 152 |
+
return {
|
| 153 |
+
'id': self.id,
|
| 154 |
+
'farmer_id': self.farmer_id,
|
| 155 |
+
'farm_name': self.farm_name,
|
| 156 |
+
'farm_size': self.farm_size,
|
| 157 |
+
'farm_type': self.farm_type,
|
| 158 |
+
'farm_type_display': self.get_farm_type_display(),
|
| 159 |
+
'irrigation_type': self.irrigation_type,
|
| 160 |
+
'latitude': self.latitude,
|
| 161 |
+
'longitude': self.longitude,
|
| 162 |
+
'field_coordinates': json.loads(self.field_coordinates) if self.field_coordinates else None,
|
| 163 |
+
'crop_types': self.get_crop_types(),
|
| 164 |
+
'crop_details': self.get_crop_details(),
|
| 165 |
+
'livestock_types': self.get_livestock_types(),
|
| 166 |
+
'livestock_count': self.livestock_count,
|
| 167 |
+
'housing_type': self.housing_type,
|
| 168 |
+
'feeding_system': self.feeding_system,
|
| 169 |
+
'breed_info': self.get_breed_info(),
|
| 170 |
+
'weather_alerts_enabled': self.weather_alerts_enabled,
|
| 171 |
+
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
class SoilData(db.Model):
|
| 176 |
+
__tablename__ = 'soil_data'
|
| 177 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 178 |
+
farm_id = db.Column(db.Integer, db.ForeignKey('farms.id'), nullable=False)
|
| 179 |
+
soil_type = db.Column(db.String(200), nullable=True)
|
| 180 |
+
ph_level = db.Column(db.Float, nullable=True)
|
| 181 |
+
nitrogen_level = db.Column(db.Float, nullable=True)
|
| 182 |
+
phosphorus_level = db.Column(db.Float, nullable=True)
|
| 183 |
+
potassium_level = db.Column(db.Float, nullable=True)
|
| 184 |
+
moisture_percentage = db.Column(db.Float, nullable=True)
|
| 185 |
+
# NOTE: some deployments may have older schema without recorded_at column.
|
| 186 |
+
# Keep as optional attribute access in as_dict to avoid OperationalError.
|
| 187 |
+
|
| 188 |
+
def __repr__(self):
|
| 189 |
+
return f'<SoilData {self.id} farm={self.farm_id}>'
|
| 190 |
+
|
| 191 |
+
def as_dict(self):
|
| 192 |
+
rec_at = getattr(self, 'recorded_at', None)
|
| 193 |
+
return {
|
| 194 |
+
'id': self.id,
|
| 195 |
+
'farm_id': self.farm_id,
|
| 196 |
+
'soil_type': self.soil_type,
|
| 197 |
+
'ph_level': self.ph_level,
|
| 198 |
+
'nitrogen_level': self.nitrogen_level,
|
| 199 |
+
'phosphorus_level': self.phosphorus_level,
|
| 200 |
+
'potassium_level': self.potassium_level,
|
| 201 |
+
'moisture_percentage': self.moisture_percentage,
|
| 202 |
+
'recorded_at': rec_at.isoformat() if rec_at else None
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
class FarmingActivity(db.Model):
|
| 207 |
+
__tablename__ = 'farming_activities'
|
| 208 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 209 |
+
farmer_id = db.Column(db.Integer, db.ForeignKey('farmers.id'), nullable=False)
|
| 210 |
+
farm_id = db.Column(db.Integer, db.ForeignKey('farms.id'), nullable=True)
|
| 211 |
+
activity_type = db.Column(db.String(200), nullable=True)
|
| 212 |
+
scheduled_date = db.Column(db.DateTime, nullable=True)
|
| 213 |
+
notes = db.Column(db.Text, nullable=True)
|
| 214 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 215 |
+
|
| 216 |
+
def __repr__(self):
|
| 217 |
+
return f'<FarmingActivity {self.id} {self.activity_type}>'
|
| 218 |
+
|
| 219 |
+
def as_dict(self):
|
| 220 |
+
return {
|
| 221 |
+
'id': self.id,
|
| 222 |
+
'farmer_id': self.farmer_id,
|
| 223 |
+
'farm_id': self.farm_id,
|
| 224 |
+
'activity_type': self.activity_type,
|
| 225 |
+
'scheduled_date': self.scheduled_date.isoformat() if self.scheduled_date else None,
|
| 226 |
+
'notes': self.notes,
|
| 227 |
+
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
class WeatherData(db.Model):
|
| 232 |
+
__tablename__ = 'weather_data'
|
| 233 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 234 |
+
farm_id = db.Column(db.Integer, db.ForeignKey('farms.id'), nullable=True)
|
| 235 |
+
date = db.Column(db.Date, default=date.today)
|
| 236 |
+
data = db.Column(db.Text, nullable=True) # JSON blob of weather info
|
| 237 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 238 |
+
|
| 239 |
+
def get_data(self):
|
| 240 |
+
try:
|
| 241 |
+
return json.loads(self.data) if self.data else {}
|
| 242 |
+
except Exception:
|
| 243 |
+
return {}
|
| 244 |
+
|
| 245 |
+
def __repr__(self):
|
| 246 |
+
return f'<WeatherData {self.id} farm={self.farm_id} date={self.date}>'
|
| 247 |
+
|
| 248 |
+
def as_dict(self):
|
| 249 |
+
return {
|
| 250 |
+
'id': self.id,
|
| 251 |
+
'farm_id': self.farm_id,
|
| 252 |
+
'date': self.date.isoformat() if self.date else None,
|
| 253 |
+
'data': self.get_data(),
|
| 254 |
+
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
class DailyAdvisory(db.Model):
|
| 259 |
+
__tablename__ = 'daily_advisories'
|
| 260 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 261 |
+
farm_id = db.Column(db.Integer, db.ForeignKey('farms.id'), nullable=False)
|
| 262 |
+
date = db.Column(db.Date, nullable=False)
|
| 263 |
+
# Updated columns to match the existing database schema
|
| 264 |
+
task_to_do = db.Column(db.Text, nullable=True)
|
| 265 |
+
task_to_avoid = db.Column(db.Text, nullable=True)
|
| 266 |
+
reason_explanation = db.Column(db.Text, nullable=True)
|
| 267 |
+
crop_stage = db.Column(db.String(50), nullable=True)
|
| 268 |
+
weather_context = db.Column(db.Text, nullable=True) # JSON blob
|
| 269 |
+
gemini_response = db.Column(db.Text, nullable=True) # raw AI response JSON
|
| 270 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 271 |
+
|
| 272 |
+
def __repr__(self):
|
| 273 |
+
return f'<DailyAdvisory {self.id} farm={self.farm_id} date={self.date}>'
|
| 274 |
+
|
| 275 |
+
def as_dict(self):
|
| 276 |
+
return {
|
| 277 |
+
'id': self.id,
|
| 278 |
+
'farm_id': self.farm_id,
|
| 279 |
+
'date': self.date.isoformat() if self.date else None,
|
| 280 |
+
'task_to_do': self.task_to_do,
|
| 281 |
+
'task_to_avoid': self.task_to_avoid,
|
| 282 |
+
'reason_explanation': self.reason_explanation,
|
| 283 |
+
'crop_stage': self.crop_stage,
|
| 284 |
+
'weather_context': json.loads(self.weather_context) if self.weather_context else None,
|
| 285 |
+
'gemini_response': json.loads(self.gemini_response) if self.gemini_response else None,
|
| 286 |
+
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
class SMSLog(db.Model):
|
| 291 |
+
__tablename__ = 'sms_logs'
|
| 292 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 293 |
+
# Adjusted to match the current DB schema in instance/farm_management.db
|
| 294 |
+
farmer_id = db.Column(db.Integer, db.ForeignKey('farmers.id'), nullable=True)
|
| 295 |
+
phone_number = db.Column(db.String(15), nullable=True)
|
| 296 |
+
message_content = db.Column(db.Text, nullable=True)
|
| 297 |
+
twilio_sid = db.Column(db.String(100), nullable=True)
|
| 298 |
+
status = db.Column(db.String(20), nullable=True)
|
| 299 |
+
sent_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 300 |
+
delivered_at = db.Column(db.DateTime, nullable=True)
|
| 301 |
+
error_message = db.Column(db.Text, nullable=True)
|
| 302 |
+
|
| 303 |
+
def as_dict(self):
|
| 304 |
+
return {
|
| 305 |
+
'id': self.id,
|
| 306 |
+
'farmer_id': self.farmer_id,
|
| 307 |
+
'phone_number': self.phone_number,
|
| 308 |
+
'message_content': self.message_content,
|
| 309 |
+
'twilio_sid': self.twilio_sid,
|
| 310 |
+
'status': self.status,
|
| 311 |
+
'sent_at': self.sent_at.isoformat() if self.sent_at else None,
|
| 312 |
+
'delivered_at': self.delivered_at.isoformat() if self.delivered_at else None,
|
| 313 |
+
'error_message': self.error_message
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
def __repr__(self):
|
| 317 |
+
return f'<SMSLog {self.id} to={self.recipient} status={self.status}>'
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
class AdminUser(db.Model):
|
| 321 |
+
__tablename__ = 'admin_users'
|
| 322 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 323 |
+
username = db.Column(db.String(150), unique=True, nullable=False)
|
| 324 |
+
password_hash = db.Column(db.String(255), nullable=False)
|
| 325 |
+
last_login = db.Column(db.DateTime, nullable=True)
|
| 326 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 327 |
+
|
| 328 |
+
def set_password(self, password: str):
|
| 329 |
+
self.password_hash = generate_password_hash(password)
|
| 330 |
+
|
| 331 |
+
def check_password(self, password: str) -> bool:
|
| 332 |
+
if not self.password_hash:
|
| 333 |
+
return False
|
| 334 |
+
return check_password_hash(self.password_hash, password)
|
| 335 |
+
|
| 336 |
+
def __repr__(self):
|
| 337 |
+
return f'<AdminUser {self.username}>'
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
class YearlyPlan(db.Model):
|
| 341 |
+
__tablename__ = 'yearly_plans'
|
| 342 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 343 |
+
farmer_id = db.Column(db.Integer, db.ForeignKey('farmers.id'), nullable=False)
|
| 344 |
+
year = db.Column(db.Integer, nullable=False)
|
| 345 |
+
plan_json = db.Column(db.Text, nullable=True) # JSON blob of the full plan
|
| 346 |
+
summary_text = db.Column(db.Text, nullable=True) # short text summary for messages
|
| 347 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 348 |
+
|
| 349 |
+
def as_dict(self):
|
| 350 |
+
try:
|
| 351 |
+
return {
|
| 352 |
+
'id': self.id,
|
| 353 |
+
'farmer_id': self.farmer_id,
|
| 354 |
+
'year': self.year,
|
| 355 |
+
'plan': json.loads(self.plan_json) if self.plan_json else {},
|
| 356 |
+
'summary': self.summary_text,
|
| 357 |
+
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 358 |
+
}
|
| 359 |
+
except Exception:
|
| 360 |
+
return {
|
| 361 |
+
'id': self.id,
|
| 362 |
+
'farmer_id': self.farmer_id,
|
| 363 |
+
'year': self.year,
|
| 364 |
+
'plan': {},
|
| 365 |
+
'summary': self.summary_text,
|
| 366 |
+
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
class WeatherAlert(db.Model):
|
| 371 |
+
__tablename__ = 'weather_alerts'
|
| 372 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 373 |
+
farm_id = db.Column(db.Integer, db.ForeignKey('farms.id'), nullable=False)
|
| 374 |
+
alert_type = db.Column(db.String(100), nullable=False) # 'rain', 'storm', 'drought', 'frost', 'heat_wave'
|
| 375 |
+
severity = db.Column(db.String(50), nullable=False) # 'low', 'medium', 'high', 'critical'
|
| 376 |
+
title = db.Column(db.String(200), nullable=False)
|
| 377 |
+
message = db.Column(db.Text, nullable=False)
|
| 378 |
+
recommended_action = db.Column(db.Text, nullable=True)
|
| 379 |
+
weather_data = db.Column(db.Text, nullable=True) # JSON string
|
| 380 |
+
is_active = db.Column(db.Boolean, default=True)
|
| 381 |
+
is_sent = db.Column(db.Boolean, default=False)
|
| 382 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 383 |
+
expires_at = db.Column(db.DateTime, nullable=True)
|
| 384 |
+
|
| 385 |
+
def as_dict(self):
|
| 386 |
+
return {
|
| 387 |
+
'id': self.id,
|
| 388 |
+
'farm_id': self.farm_id,
|
| 389 |
+
'alert_type': self.alert_type,
|
| 390 |
+
'severity': self.severity,
|
| 391 |
+
'title': self.title,
|
| 392 |
+
'message': self.message,
|
| 393 |
+
'recommended_action': self.recommended_action,
|
| 394 |
+
'is_active': self.is_active,
|
| 395 |
+
'is_sent': self.is_sent,
|
| 396 |
+
'created_at': self.created_at.isoformat() if self.created_at else None,
|
| 397 |
+
'expires_at': self.expires_at.isoformat() if self.expires_at else None
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
class MarketPrice(db.Model):
|
| 402 |
+
__tablename__ = 'market_prices'
|
| 403 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 404 |
+
crop_name = db.Column(db.String(100), nullable=False)
|
| 405 |
+
market_name = db.Column(db.String(200), nullable=False)
|
| 406 |
+
state = db.Column(db.String(100), nullable=False)
|
| 407 |
+
district = db.Column(db.String(100), nullable=False)
|
| 408 |
+
min_price = db.Column(db.Float, nullable=True)
|
| 409 |
+
max_price = db.Column(db.Float, nullable=True)
|
| 410 |
+
avg_price = db.Column(db.Float, nullable=False)
|
| 411 |
+
price_unit = db.Column(db.String(50), default='per quintal')
|
| 412 |
+
price_date = db.Column(db.Date, nullable=False)
|
| 413 |
+
source = db.Column(db.String(100), nullable=True) # 'government', 'market_api', 'manual'
|
| 414 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 415 |
+
|
| 416 |
+
def as_dict(self):
|
| 417 |
+
return {
|
| 418 |
+
'id': self.id,
|
| 419 |
+
'crop_name': self.crop_name,
|
| 420 |
+
'market_name': self.market_name,
|
| 421 |
+
'state': self.state,
|
| 422 |
+
'district': self.district,
|
| 423 |
+
'min_price': self.min_price,
|
| 424 |
+
'max_price': self.max_price,
|
| 425 |
+
'avg_price': self.avg_price,
|
| 426 |
+
'price_unit': self.price_unit,
|
| 427 |
+
'price_date': self.price_date.isoformat() if self.price_date else None,
|
| 428 |
+
'source': self.source,
|
| 429 |
+
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
class DiseaseDetection(db.Model):
|
| 434 |
+
__tablename__ = 'disease_detections'
|
| 435 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 436 |
+
farm_id = db.Column(db.Integer, db.ForeignKey('farms.id'), nullable=False)
|
| 437 |
+
crop_name = db.Column(db.String(100), nullable=False)
|
| 438 |
+
disease_name = db.Column(db.String(200), nullable=True)
|
| 439 |
+
confidence_score = db.Column(db.Float, nullable=True)
|
| 440 |
+
symptoms = db.Column(db.Text, nullable=True)
|
| 441 |
+
treatment = db.Column(db.Text, nullable=True)
|
| 442 |
+
prevention = db.Column(db.Text, nullable=True)
|
| 443 |
+
image_path = db.Column(db.String(500), nullable=True)
|
| 444 |
+
ai_analysis = db.Column(db.Text, nullable=True) # JSON string
|
| 445 |
+
status = db.Column(db.String(50), default='detected') # 'detected', 'treating', 'resolved'
|
| 446 |
+
severity = db.Column(db.String(50), nullable=True) # 'mild', 'moderate', 'severe'
|
| 447 |
+
detected_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 448 |
+
resolved_at = db.Column(db.DateTime, nullable=True)
|
| 449 |
+
|
| 450 |
+
def as_dict(self):
|
| 451 |
+
return {
|
| 452 |
+
'id': self.id,
|
| 453 |
+
'farm_id': self.farm_id,
|
| 454 |
+
'crop_name': self.crop_name,
|
| 455 |
+
'disease_name': self.disease_name,
|
| 456 |
+
'confidence_score': self.confidence_score,
|
| 457 |
+
'symptoms': self.symptoms,
|
| 458 |
+
'treatment': self.treatment,
|
| 459 |
+
'prevention': self.prevention,
|
| 460 |
+
'status': self.status,
|
| 461 |
+
'severity': self.severity,
|
| 462 |
+
'detected_at': self.detected_at.isoformat() if self.detected_at else None,
|
| 463 |
+
'resolved_at': self.resolved_at.isoformat() if self.resolved_at else None
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
class DailyTask(db.Model):
|
| 468 |
+
__tablename__ = 'daily_tasks'
|
| 469 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 470 |
+
farmer_id = db.Column(db.Integer, db.ForeignKey('farmers.id'), nullable=False)
|
| 471 |
+
farm_id = db.Column(db.Integer, db.ForeignKey('farms.id'), nullable=True)
|
| 472 |
+
task_date = db.Column(db.Date, nullable=False, default=date.today)
|
| 473 |
+
task_type = db.Column(db.String(100), nullable=False) # 'irrigation', 'fertilizing', 'pest_control', etc.
|
| 474 |
+
task_title = db.Column(db.String(200), nullable=False)
|
| 475 |
+
task_description = db.Column(db.Text, nullable=False)
|
| 476 |
+
priority = db.Column(db.String(20), default='medium') # 'high', 'medium', 'low'
|
| 477 |
+
estimated_duration = db.Column(db.Integer, nullable=True) # in minutes
|
| 478 |
+
weather_dependent = db.Column(db.Boolean, default=False)
|
| 479 |
+
crop_specific = db.Column(db.String(100), nullable=True)
|
| 480 |
+
|
| 481 |
+
# Completion tracking
|
| 482 |
+
is_completed = db.Column(db.Boolean, default=False)
|
| 483 |
+
completed_at = db.Column(db.DateTime, nullable=True)
|
| 484 |
+
completion_notes = db.Column(db.Text, nullable=True)
|
| 485 |
+
completion_rating = db.Column(db.Integer, nullable=True) # 1-5 rating from farmer
|
| 486 |
+
|
| 487 |
+
# Notification tracking
|
| 488 |
+
sent_via_telegram = db.Column(db.Boolean, default=False)
|
| 489 |
+
telegram_sent_at = db.Column(db.DateTime, nullable=True)
|
| 490 |
+
reminder_sent = db.Column(db.Boolean, default=False)
|
| 491 |
+
reminder_sent_at = db.Column(db.DateTime, nullable=True)
|
| 492 |
+
|
| 493 |
+
# Meta
|
| 494 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 495 |
+
created_by = db.Column(db.String(50), default='system') # 'system', 'ai', 'manual'
|
| 496 |
+
|
| 497 |
+
def as_dict(self):
|
| 498 |
+
return {
|
| 499 |
+
'id': self.id,
|
| 500 |
+
'farmer_id': self.farmer_id,
|
| 501 |
+
'farm_id': self.farm_id,
|
| 502 |
+
'task_date': self.task_date.isoformat() if self.task_date else None,
|
| 503 |
+
'task_type': self.task_type,
|
| 504 |
+
'task_title': self.task_title,
|
| 505 |
+
'task_description': self.task_description,
|
| 506 |
+
'priority': self.priority,
|
| 507 |
+
'estimated_duration': self.estimated_duration,
|
| 508 |
+
'weather_dependent': self.weather_dependent,
|
| 509 |
+
'crop_specific': self.crop_specific,
|
| 510 |
+
'is_completed': self.is_completed,
|
| 511 |
+
'completed_at': self.completed_at.isoformat() if self.completed_at else None,
|
| 512 |
+
'completion_notes': self.completion_notes,
|
| 513 |
+
'completion_rating': self.completion_rating,
|
| 514 |
+
'sent_via_telegram': self.sent_via_telegram,
|
| 515 |
+
'telegram_sent_at': self.telegram_sent_at.isoformat() if self.telegram_sent_at else None,
|
| 516 |
+
'reminder_sent': self.reminder_sent,
|
| 517 |
+
'reminder_sent_at': self.reminder_sent_at.isoformat() if self.reminder_sent_at else None,
|
| 518 |
+
'created_at': self.created_at.isoformat() if self.created_at else None,
|
| 519 |
+
'created_by': self.created_by
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
def __repr__(self):
|
| 523 |
+
return f'<DailyTask {self.id} {self.task_title} for {self.task_date}>'
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
class TaskCompletion(db.Model):
|
| 527 |
+
__tablename__ = 'task_completions'
|
| 528 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 529 |
+
task_id = db.Column(db.Integer, db.ForeignKey('daily_tasks.id'), nullable=False)
|
| 530 |
+
farmer_id = db.Column(db.Integer, db.ForeignKey('farmers.id'), nullable=False)
|
| 531 |
+
completed_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 532 |
+
completion_method = db.Column(db.String(50), default='dashboard') # 'dashboard', 'telegram', 'mobile'
|
| 533 |
+
completion_status = db.Column(db.String(50), default='completed') # 'completed', 'partially_completed', 'skipped'
|
| 534 |
+
notes = db.Column(db.Text, nullable=True)
|
| 535 |
+
rating = db.Column(db.Integer, nullable=True) # 1-5 rating
|
| 536 |
+
issues_faced = db.Column(db.Text, nullable=True)
|
| 537 |
+
time_taken = db.Column(db.Integer, nullable=True) # actual time taken in minutes
|
| 538 |
+
|
| 539 |
+
def as_dict(self):
|
| 540 |
+
return {
|
| 541 |
+
'id': self.id,
|
| 542 |
+
'task_id': self.task_id,
|
| 543 |
+
'farmer_id': self.farmer_id,
|
| 544 |
+
'completed_at': self.completed_at.isoformat() if self.completed_at else None,
|
| 545 |
+
'completion_method': self.completion_method,
|
| 546 |
+
'completion_status': self.completion_status,
|
| 547 |
+
'notes': self.notes,
|
| 548 |
+
'rating': self.rating,
|
| 549 |
+
'issues_faced': self.issues_faced,
|
| 550 |
+
'time_taken': self.time_taken
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
def __repr__(self):
|
| 554 |
+
return f'<TaskCompletion {self.id} for task {self.task_id}>'
|
| 555 |
+
|
| 556 |
+
|
| 557 |
+
# ==================== LIVESTOCK MANAGEMENT MODELS ====================
|
| 558 |
+
|
| 559 |
+
class LivestockRecord(db.Model):
|
| 560 |
+
"""Model for individual animal records in livestock farming"""
|
| 561 |
+
__tablename__ = 'livestock_records'
|
| 562 |
+
|
| 563 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 564 |
+
farm_id = db.Column(db.Integer, db.ForeignKey('farms.id'), nullable=False)
|
| 565 |
+
farmer_id = db.Column(db.Integer, db.ForeignKey('farmers.id'), nullable=False)
|
| 566 |
+
|
| 567 |
+
# Animal identification
|
| 568 |
+
animal_id = db.Column(db.String(50), nullable=False) # Tag number or custom ID
|
| 569 |
+
animal_type = db.Column(db.String(50), nullable=False) # cow, buffalo, goat, chicken, fish
|
| 570 |
+
breed = db.Column(db.String(100), nullable=True)
|
| 571 |
+
gender = db.Column(db.String(10), nullable=True) # male, female
|
| 572 |
+
|
| 573 |
+
# Basic info
|
| 574 |
+
date_of_birth = db.Column(db.Date, nullable=True)
|
| 575 |
+
date_acquired = db.Column(db.Date, nullable=False, default=datetime.utcnow)
|
| 576 |
+
acquisition_method = db.Column(db.String(50), nullable=True) # purchased, born, gifted
|
| 577 |
+
current_weight = db.Column(db.Float, nullable=True) # in kg
|
| 578 |
+
|
| 579 |
+
# Health and breeding
|
| 580 |
+
vaccination_status = db.Column(db.Text, nullable=True) # JSON of vaccination records
|
| 581 |
+
health_status = db.Column(db.String(50), default='healthy') # healthy, sick, treatment
|
| 582 |
+
breeding_status = db.Column(db.String(50), nullable=True) # pregnant, lactating, dry
|
| 583 |
+
|
| 584 |
+
# Production tracking (for dairy, poultry, etc.)
|
| 585 |
+
milk_production_avg = db.Column(db.Float, nullable=True) # liters per day
|
| 586 |
+
egg_production_avg = db.Column(db.Integer, nullable=True) # eggs per day
|
| 587 |
+
|
| 588 |
+
# Status
|
| 589 |
+
is_active = db.Column(db.Boolean, default=True)
|
| 590 |
+
status = db.Column(db.String(50), default='active') # active, sold, deceased, transferred
|
| 591 |
+
notes = db.Column(db.Text, nullable=True)
|
| 592 |
+
|
| 593 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 594 |
+
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
| 595 |
+
|
| 596 |
+
def as_dict(self):
|
| 597 |
+
return {
|
| 598 |
+
'id': self.id,
|
| 599 |
+
'farm_id': self.farm_id,
|
| 600 |
+
'animal_id': self.animal_id,
|
| 601 |
+
'animal_type': self.animal_type,
|
| 602 |
+
'breed': self.breed,
|
| 603 |
+
'gender': self.gender,
|
| 604 |
+
'date_of_birth': self.date_of_birth.isoformat() if self.date_of_birth else None,
|
| 605 |
+
'date_acquired': self.date_acquired.isoformat() if self.date_acquired else None,
|
| 606 |
+
'current_weight': self.current_weight,
|
| 607 |
+
'health_status': self.health_status,
|
| 608 |
+
'breeding_status': self.breeding_status,
|
| 609 |
+
'milk_production_avg': self.milk_production_avg,
|
| 610 |
+
'egg_production_avg': self.egg_production_avg,
|
| 611 |
+
'is_active': self.is_active,
|
| 612 |
+
'status': self.status,
|
| 613 |
+
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
def __repr__(self):
|
| 617 |
+
return f'<LivestockRecord {self.animal_id} - {self.animal_type}>'
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
class ProductionRecord(db.Model):
|
| 621 |
+
"""Model for tracking daily production (milk, eggs, etc.)"""
|
| 622 |
+
__tablename__ = 'production_records'
|
| 623 |
+
|
| 624 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 625 |
+
farm_id = db.Column(db.Integer, db.ForeignKey('farms.id'), nullable=False)
|
| 626 |
+
farmer_id = db.Column(db.Integer, db.ForeignKey('farmers.id'), nullable=False)
|
| 627 |
+
livestock_id = db.Column(db.Integer, db.ForeignKey('livestock_records.id'), nullable=True)
|
| 628 |
+
|
| 629 |
+
# Production details
|
| 630 |
+
production_date = db.Column(db.Date, nullable=False, default=datetime.utcnow)
|
| 631 |
+
production_type = db.Column(db.String(50), nullable=False) # milk, eggs, meat, fish
|
| 632 |
+
quantity = db.Column(db.Float, nullable=False) # liters, pieces, kg
|
| 633 |
+
unit = db.Column(db.String(20), nullable=False) # liters, pieces, kg
|
| 634 |
+
quality_grade = db.Column(db.String(20), nullable=True) # A, B, C or premium, standard
|
| 635 |
+
|
| 636 |
+
# Economic data
|
| 637 |
+
unit_price = db.Column(db.Float, nullable=True) # price per unit
|
| 638 |
+
total_value = db.Column(db.Float, nullable=True) # total value
|
| 639 |
+
buyer_info = db.Column(db.String(200), nullable=True) # where sold
|
| 640 |
+
|
| 641 |
+
# Additional info
|
| 642 |
+
notes = db.Column(db.Text, nullable=True)
|
| 643 |
+
weather_conditions = db.Column(db.String(100), nullable=True)
|
| 644 |
+
|
| 645 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 646 |
+
|
| 647 |
+
def as_dict(self):
|
| 648 |
+
return {
|
| 649 |
+
'id': self.id,
|
| 650 |
+
'farm_id': self.farm_id,
|
| 651 |
+
'livestock_id': self.livestock_id,
|
| 652 |
+
'production_date': self.production_date.isoformat() if self.production_date else None,
|
| 653 |
+
'production_type': self.production_type,
|
| 654 |
+
'quantity': self.quantity,
|
| 655 |
+
'unit': self.unit,
|
| 656 |
+
'quality_grade': self.quality_grade,
|
| 657 |
+
'unit_price': self.unit_price,
|
| 658 |
+
'total_value': self.total_value,
|
| 659 |
+
'buyer_info': self.buyer_info,
|
| 660 |
+
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
def __repr__(self):
|
| 664 |
+
return f'<ProductionRecord {self.production_type} - {self.quantity} {self.unit}>'
|
| 665 |
+
|
| 666 |
+
|
| 667 |
+
class FeedRecord(db.Model):
|
| 668 |
+
"""Model for tracking feed consumption and costs"""
|
| 669 |
+
__tablename__ = 'feed_records'
|
| 670 |
+
|
| 671 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 672 |
+
farm_id = db.Column(db.Integer, db.ForeignKey('farms.id'), nullable=False)
|
| 673 |
+
farmer_id = db.Column(db.Integer, db.ForeignKey('farmers.id'), nullable=False)
|
| 674 |
+
|
| 675 |
+
# Feed details
|
| 676 |
+
feed_date = db.Column(db.Date, nullable=False, default=datetime.utcnow)
|
| 677 |
+
feed_type = db.Column(db.String(100), nullable=False) # grass, concentrate, grain, etc.
|
| 678 |
+
feed_name = db.Column(db.String(200), nullable=False) # specific feed name/brand
|
| 679 |
+
quantity = db.Column(db.Float, nullable=False) # in kg or specified unit
|
| 680 |
+
unit = db.Column(db.String(20), nullable=False, default='kg')
|
| 681 |
+
|
| 682 |
+
# Cost tracking
|
| 683 |
+
cost_per_unit = db.Column(db.Float, nullable=True)
|
| 684 |
+
total_cost = db.Column(db.Float, nullable=True)
|
| 685 |
+
supplier = db.Column(db.String(200), nullable=True)
|
| 686 |
+
|
| 687 |
+
# Target animals
|
| 688 |
+
target_animals = db.Column(db.Text, nullable=True) # JSON list of animal IDs or types
|
| 689 |
+
animals_count = db.Column(db.Integer, nullable=True) # number of animals fed
|
| 690 |
+
|
| 691 |
+
# Nutritional info
|
| 692 |
+
protein_content = db.Column(db.Float, nullable=True) # percentage
|
| 693 |
+
energy_content = db.Column(db.Float, nullable=True) # kcal/kg
|
| 694 |
+
|
| 695 |
+
notes = db.Column(db.Text, nullable=True)
|
| 696 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 697 |
+
|
| 698 |
+
def as_dict(self):
|
| 699 |
+
return {
|
| 700 |
+
'id': self.id,
|
| 701 |
+
'farm_id': self.farm_id,
|
| 702 |
+
'feed_date': self.feed_date.isoformat() if self.feed_date else None,
|
| 703 |
+
'feed_type': self.feed_type,
|
| 704 |
+
'feed_name': self.feed_name,
|
| 705 |
+
'quantity': self.quantity,
|
| 706 |
+
'unit': self.unit,
|
| 707 |
+
'cost_per_unit': self.cost_per_unit,
|
| 708 |
+
'total_cost': self.total_cost,
|
| 709 |
+
'supplier': self.supplier,
|
| 710 |
+
'animals_count': self.animals_count,
|
| 711 |
+
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
def __repr__(self):
|
| 715 |
+
return f'<FeedRecord {self.feed_name} - {self.quantity} {self.unit}>'
|
| 716 |
+
|
| 717 |
+
|
| 718 |
+
class HealthRecord(db.Model):
|
| 719 |
+
"""Model for tracking animal health, vaccinations, and treatments"""
|
| 720 |
+
__tablename__ = 'health_records'
|
| 721 |
+
|
| 722 |
+
id = db.Column(db.Integer, primary_key=True)
|
| 723 |
+
farm_id = db.Column(db.Integer, db.ForeignKey('farms.id'), nullable=False)
|
| 724 |
+
farmer_id = db.Column(db.Integer, db.ForeignKey('farmers.id'), nullable=False)
|
| 725 |
+
livestock_id = db.Column(db.Integer, db.ForeignKey('livestock_records.id'), nullable=False)
|
| 726 |
+
|
| 727 |
+
# Health event details
|
| 728 |
+
event_date = db.Column(db.Date, nullable=False, default=datetime.utcnow)
|
| 729 |
+
event_type = db.Column(db.String(50), nullable=False) # vaccination, treatment, checkup, illness
|
| 730 |
+
|
| 731 |
+
# Vaccination details
|
| 732 |
+
vaccine_name = db.Column(db.String(200), nullable=True)
|
| 733 |
+
vaccine_batch = db.Column(db.String(100), nullable=True)
|
| 734 |
+
next_due_date = db.Column(db.Date, nullable=True)
|
| 735 |
+
|
| 736 |
+
# Treatment details
|
| 737 |
+
symptoms = db.Column(db.Text, nullable=True)
|
| 738 |
+
diagnosis = db.Column(db.Text, nullable=True)
|
| 739 |
+
treatment_given = db.Column(db.Text, nullable=True)
|
| 740 |
+
medication = db.Column(db.Text, nullable=True)
|
| 741 |
+
dosage = db.Column(db.String(100), nullable=True)
|
| 742 |
+
|
| 743 |
+
# Veterinary info
|
| 744 |
+
vet_name = db.Column(db.String(200), nullable=True)
|
| 745 |
+
vet_contact = db.Column(db.String(100), nullable=True)
|
| 746 |
+
cost = db.Column(db.Float, nullable=True)
|
| 747 |
+
|
| 748 |
+
# Follow-up
|
| 749 |
+
follow_up_required = db.Column(db.Boolean, default=False)
|
| 750 |
+
follow_up_date = db.Column(db.Date, nullable=True)
|
| 751 |
+
recovery_status = db.Column(db.String(50), nullable=True) # recovered, recovering, chronic
|
| 752 |
+
|
| 753 |
+
notes = db.Column(db.Text, nullable=True)
|
| 754 |
+
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
| 755 |
+
|
| 756 |
+
def as_dict(self):
|
| 757 |
+
return {
|
| 758 |
+
'id': self.id,
|
| 759 |
+
'farm_id': self.farm_id,
|
| 760 |
+
'livestock_id': self.livestock_id,
|
| 761 |
+
'event_date': self.event_date.isoformat() if self.event_date else None,
|
| 762 |
+
'event_type': self.event_type,
|
| 763 |
+
'vaccine_name': self.vaccine_name,
|
| 764 |
+
'symptoms': self.symptoms,
|
| 765 |
+
'diagnosis': self.diagnosis,
|
| 766 |
+
'treatment_given': self.treatment_given,
|
| 767 |
+
'vet_name': self.vet_name,
|
| 768 |
+
'cost': self.cost,
|
| 769 |
+
'recovery_status': self.recovery_status,
|
| 770 |
+
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
def __repr__(self):
|
| 774 |
+
return f'<HealthRecord {self.event_type} for livestock {self.livestock_id}>'
|
| 775 |
+
|
pdf_generator_service.py
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from typing import Dict, Optional
|
| 5 |
+
import logging
|
| 6 |
+
try:
|
| 7 |
+
from reportlab.lib.pagesizes import letter, A4
|
| 8 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
| 9 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 10 |
+
from reportlab.lib.units import inch
|
| 11 |
+
from reportlab.lib import colors
|
| 12 |
+
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
| 13 |
+
REPORTLAB_AVAILABLE = True
|
| 14 |
+
except ImportError:
|
| 15 |
+
REPORTLAB_AVAILABLE = False
|
| 16 |
+
|
| 17 |
+
# Configure logging
|
| 18 |
+
logging.basicConfig(level=logging.INFO)
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
class PDFGeneratorService:
|
| 22 |
+
"""Service for generating PDF reports for yearly plans and other documents"""
|
| 23 |
+
|
| 24 |
+
def __init__(self, output_dir: str = "generated_pdfs"):
|
| 25 |
+
self.output_dir = output_dir
|
| 26 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 27 |
+
|
| 28 |
+
if not REPORTLAB_AVAILABLE:
|
| 29 |
+
logger.warning("ReportLab not available. PDF generation will use fallback HTML to PDF")
|
| 30 |
+
|
| 31 |
+
def generate_yearly_plan_pdf(self, farmer_data: Dict, plan_data: Dict) -> Optional[str]:
|
| 32 |
+
"""Generate PDF for yearly plan"""
|
| 33 |
+
try:
|
| 34 |
+
if REPORTLAB_AVAILABLE:
|
| 35 |
+
return self._generate_with_reportlab(farmer_data, plan_data)
|
| 36 |
+
else:
|
| 37 |
+
return self._generate_with_html_fallback(farmer_data, plan_data)
|
| 38 |
+
|
| 39 |
+
except Exception as e:
|
| 40 |
+
logger.error(f"Error generating yearly plan PDF: {str(e)}")
|
| 41 |
+
return None
|
| 42 |
+
|
| 43 |
+
def _generate_with_reportlab(self, farmer_data: Dict, plan_data: Dict) -> str:
|
| 44 |
+
"""Generate PDF using ReportLab"""
|
| 45 |
+
try:
|
| 46 |
+
# Create filename
|
| 47 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 48 |
+
filename = f"yearly_plan_{farmer_data.get('name', 'farmer')}_{timestamp}.pdf"
|
| 49 |
+
filepath = os.path.join(self.output_dir, filename)
|
| 50 |
+
|
| 51 |
+
# Create document
|
| 52 |
+
doc = SimpleDocTemplate(filepath, pagesize=A4, topMargin=0.5*inch)
|
| 53 |
+
styles = getSampleStyleSheet()
|
| 54 |
+
|
| 55 |
+
# Custom styles
|
| 56 |
+
title_style = ParagraphStyle(
|
| 57 |
+
'CustomTitle',
|
| 58 |
+
parent=styles['Heading1'],
|
| 59 |
+
fontSize=24,
|
| 60 |
+
textColor=colors.darkgreen,
|
| 61 |
+
spaceAfter=20,
|
| 62 |
+
alignment=TA_CENTER
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
heading_style = ParagraphStyle(
|
| 66 |
+
'CustomHeading',
|
| 67 |
+
parent=styles['Heading2'],
|
| 68 |
+
fontSize=16,
|
| 69 |
+
textColor=colors.blue,
|
| 70 |
+
spaceAfter=12,
|
| 71 |
+
spaceBefore=20
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
story = []
|
| 75 |
+
|
| 76 |
+
# Title
|
| 77 |
+
story.append(Paragraph("🌾 Comprehensive Yearly Farming Plan", title_style))
|
| 78 |
+
story.append(Spacer(1, 20))
|
| 79 |
+
|
| 80 |
+
# Farmer Information
|
| 81 |
+
story.append(Paragraph("Farmer Information", heading_style))
|
| 82 |
+
farmer_info = [
|
| 83 |
+
['Name:', farmer_data.get('name', 'N/A')],
|
| 84 |
+
['Contact:', farmer_data.get('contact_number', 'N/A')],
|
| 85 |
+
['Address:', farmer_data.get('address', 'N/A')],
|
| 86 |
+
['Plan Year:', plan_data.get('year', datetime.now().year)]
|
| 87 |
+
]
|
| 88 |
+
|
| 89 |
+
farmer_table = Table(farmer_info, colWidths=[2*inch, 4*inch])
|
| 90 |
+
farmer_table.setStyle(TableStyle([
|
| 91 |
+
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
|
| 92 |
+
('FONTSIZE', (0, 0), (-1, -1), 12),
|
| 93 |
+
('TEXTCOLOR', (0, 0), (0, -1), colors.darkblue),
|
| 94 |
+
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
|
| 95 |
+
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 96 |
+
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
|
| 97 |
+
]))
|
| 98 |
+
story.append(farmer_table)
|
| 99 |
+
story.append(Spacer(1, 20))
|
| 100 |
+
|
| 101 |
+
# Farm Plans
|
| 102 |
+
farms = plan_data.get('farms', [])
|
| 103 |
+
for i, farm in enumerate(farms):
|
| 104 |
+
story.append(Paragraph(f"Farm {i+1}: {farm.get('farm_name', 'Unknown Farm')}", heading_style))
|
| 105 |
+
|
| 106 |
+
# Parse farm plan
|
| 107 |
+
try:
|
| 108 |
+
farm_plan = json.loads(farm.get('plan', '{}')) if isinstance(farm.get('plan'), str) else farm.get('plan', {})
|
| 109 |
+
|
| 110 |
+
# Monthly Plan Table
|
| 111 |
+
if 'monthly_plan' in farm_plan:
|
| 112 |
+
story.append(Paragraph("Monthly Farming Schedule", styles['Heading3']))
|
| 113 |
+
|
| 114 |
+
monthly_data = [['Month', 'Planned Activities']]
|
| 115 |
+
for month_data in farm_plan['monthly_plan']:
|
| 116 |
+
month = month_data.get('month', '')
|
| 117 |
+
tasks = month_data.get('tasks', [])
|
| 118 |
+
tasks_text = '\n'.join(tasks[:3]) # Show max 3 tasks
|
| 119 |
+
monthly_data.append([month, tasks_text])
|
| 120 |
+
|
| 121 |
+
monthly_table = Table(monthly_data, colWidths=[1.5*inch, 4.5*inch])
|
| 122 |
+
monthly_table.setStyle(TableStyle([
|
| 123 |
+
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 124 |
+
('FONTSIZE', (0, 0), (-1, -1), 10),
|
| 125 |
+
('BACKGROUND', (0, 0), (-1, 0), colors.lightgreen),
|
| 126 |
+
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
| 127 |
+
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
| 128 |
+
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 129 |
+
('GRID', (0, 0), (-1, -1), 1, colors.black),
|
| 130 |
+
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey]),
|
| 131 |
+
]))
|
| 132 |
+
story.append(monthly_table)
|
| 133 |
+
story.append(Spacer(1, 15))
|
| 134 |
+
|
| 135 |
+
# Crop Information
|
| 136 |
+
crops = farm_plan.get('crops', [])
|
| 137 |
+
if crops:
|
| 138 |
+
story.append(Paragraph("Crop Details", styles['Heading3']))
|
| 139 |
+
crop_data = [['Crop Name', 'Sowing Month', 'Area']]
|
| 140 |
+
for crop in crops:
|
| 141 |
+
crop_data.append([
|
| 142 |
+
crop.get('name', 'Unknown'),
|
| 143 |
+
crop.get('sowing_month', 'N/A'),
|
| 144 |
+
crop.get('area', 'N/A')
|
| 145 |
+
])
|
| 146 |
+
|
| 147 |
+
crop_table = Table(crop_data, colWidths=[2*inch, 2*inch, 2*inch])
|
| 148 |
+
crop_table.setStyle(TableStyle([
|
| 149 |
+
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 150 |
+
('BACKGROUND', (0, 0), (-1, 0), colors.lightblue),
|
| 151 |
+
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
| 152 |
+
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
| 153 |
+
('GRID', (0, 0), (-1, -1), 1, colors.black),
|
| 154 |
+
]))
|
| 155 |
+
story.append(crop_table)
|
| 156 |
+
story.append(Spacer(1, 15))
|
| 157 |
+
|
| 158 |
+
except Exception as e:
|
| 159 |
+
logger.error(f"Error processing farm plan: {str(e)}")
|
| 160 |
+
story.append(Paragraph("Plan details could not be processed", styles['Normal']))
|
| 161 |
+
story.append(Spacer(1, 10))
|
| 162 |
+
|
| 163 |
+
# Summary and Recommendations
|
| 164 |
+
story.append(Paragraph("Summary & Recommendations", heading_style))
|
| 165 |
+
summary_text = plan_data.get('summary', 'Comprehensive yearly plan generated with AI analysis.')
|
| 166 |
+
story.append(Paragraph(summary_text, styles['Normal']))
|
| 167 |
+
story.append(Spacer(1, 10))
|
| 168 |
+
|
| 169 |
+
# Footer
|
| 170 |
+
story.append(Spacer(1, 30))
|
| 171 |
+
footer_text = f"Generated on: {datetime.now().strftime('%B %d, %Y at %I:%M %p')}"
|
| 172 |
+
story.append(Paragraph(footer_text, styles['Normal']))
|
| 173 |
+
story.append(Paragraph("🌱 Powered by AI Agriculture Assistant", styles['Normal']))
|
| 174 |
+
|
| 175 |
+
# Build PDF
|
| 176 |
+
doc.build(story)
|
| 177 |
+
|
| 178 |
+
logger.info(f"Successfully generated PDF: {filepath}")
|
| 179 |
+
return filepath
|
| 180 |
+
|
| 181 |
+
except Exception as e:
|
| 182 |
+
logger.error(f"Error generating ReportLab PDF: {str(e)}")
|
| 183 |
+
return None
|
| 184 |
+
|
| 185 |
+
def _generate_with_html_fallback(self, farmer_data: Dict, plan_data: Dict) -> str:
|
| 186 |
+
"""Generate HTML file as fallback when ReportLab is not available"""
|
| 187 |
+
try:
|
| 188 |
+
# Create filename
|
| 189 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 190 |
+
filename = f"yearly_plan_{farmer_data.get('name', 'farmer')}_{timestamp}.html"
|
| 191 |
+
filepath = os.path.join(self.output_dir, filename)
|
| 192 |
+
|
| 193 |
+
# Generate HTML content
|
| 194 |
+
html_content = self._create_html_report(farmer_data, plan_data)
|
| 195 |
+
|
| 196 |
+
# Save HTML file
|
| 197 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 198 |
+
f.write(html_content)
|
| 199 |
+
|
| 200 |
+
logger.info(f"Successfully generated HTML report: {filepath}")
|
| 201 |
+
return filepath
|
| 202 |
+
|
| 203 |
+
except Exception as e:
|
| 204 |
+
logger.error(f"Error generating HTML fallback: {str(e)}")
|
| 205 |
+
return None
|
| 206 |
+
|
| 207 |
+
def _create_html_report(self, farmer_data: Dict, plan_data: Dict) -> str:
|
| 208 |
+
"""Create HTML report content"""
|
| 209 |
+
|
| 210 |
+
html = f"""
|
| 211 |
+
<!DOCTYPE html>
|
| 212 |
+
<html lang="en">
|
| 213 |
+
<head>
|
| 214 |
+
<meta charset="UTF-8">
|
| 215 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 216 |
+
<title>Yearly Farming Plan - {farmer_data.get('name', 'Farmer')}</title>
|
| 217 |
+
<style>
|
| 218 |
+
body {{
|
| 219 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 220 |
+
line-height: 1.6;
|
| 221 |
+
margin: 20px;
|
| 222 |
+
background-color: #f9f9f9;
|
| 223 |
+
}}
|
| 224 |
+
.container {{
|
| 225 |
+
max-width: 800px;
|
| 226 |
+
margin: 0 auto;
|
| 227 |
+
background: white;
|
| 228 |
+
padding: 30px;
|
| 229 |
+
border-radius: 10px;
|
| 230 |
+
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
| 231 |
+
}}
|
| 232 |
+
.header {{
|
| 233 |
+
text-align: center;
|
| 234 |
+
color: #2c5530;
|
| 235 |
+
border-bottom: 3px solid #4CAF50;
|
| 236 |
+
padding-bottom: 20px;
|
| 237 |
+
margin-bottom: 30px;
|
| 238 |
+
}}
|
| 239 |
+
.section {{
|
| 240 |
+
margin-bottom: 30px;
|
| 241 |
+
}}
|
| 242 |
+
.section h2 {{
|
| 243 |
+
color: #1976d2;
|
| 244 |
+
border-left: 4px solid #4CAF50;
|
| 245 |
+
padding-left: 15px;
|
| 246 |
+
}}
|
| 247 |
+
table {{
|
| 248 |
+
width: 100%;
|
| 249 |
+
border-collapse: collapse;
|
| 250 |
+
margin: 15px 0;
|
| 251 |
+
}}
|
| 252 |
+
th, td {{
|
| 253 |
+
padding: 12px;
|
| 254 |
+
text-align: left;
|
| 255 |
+
border: 1px solid #ddd;
|
| 256 |
+
}}
|
| 257 |
+
th {{
|
| 258 |
+
background-color: #4CAF50;
|
| 259 |
+
color: white;
|
| 260 |
+
}}
|
| 261 |
+
tr:nth-child(even) {{
|
| 262 |
+
background-color: #f2f2f2;
|
| 263 |
+
}}
|
| 264 |
+
.info-grid {{
|
| 265 |
+
display: grid;
|
| 266 |
+
grid-template-columns: 1fr 2fr;
|
| 267 |
+
gap: 10px;
|
| 268 |
+
margin: 15px 0;
|
| 269 |
+
}}
|
| 270 |
+
.info-label {{
|
| 271 |
+
font-weight: bold;
|
| 272 |
+
color: #333;
|
| 273 |
+
}}
|
| 274 |
+
.footer {{
|
| 275 |
+
text-align: center;
|
| 276 |
+
margin-top: 40px;
|
| 277 |
+
padding-top: 20px;
|
| 278 |
+
border-top: 2px solid #eee;
|
| 279 |
+
color: #666;
|
| 280 |
+
}}
|
| 281 |
+
.highlight {{
|
| 282 |
+
background-color: #fff3cd;
|
| 283 |
+
padding: 15px;
|
| 284 |
+
border-left: 4px solid #ffc107;
|
| 285 |
+
margin: 15px 0;
|
| 286 |
+
}}
|
| 287 |
+
</style>
|
| 288 |
+
</head>
|
| 289 |
+
<body>
|
| 290 |
+
<div class="container">
|
| 291 |
+
<div class="header">
|
| 292 |
+
<h1>🌾 Comprehensive Yearly Farming Plan</h1>
|
| 293 |
+
<p>AI-Generated Agricultural Strategy</p>
|
| 294 |
+
</div>
|
| 295 |
+
|
| 296 |
+
<div class="section">
|
| 297 |
+
<h2>Farmer Information</h2>
|
| 298 |
+
<div class="info-grid">
|
| 299 |
+
<div class="info-label">Name:</div>
|
| 300 |
+
<div>{farmer_data.get('name', 'N/A')}</div>
|
| 301 |
+
<div class="info-label">Contact:</div>
|
| 302 |
+
<div>{farmer_data.get('contact_number', 'N/A')}</div>
|
| 303 |
+
<div class="info-label">Address:</div>
|
| 304 |
+
<div>{farmer_data.get('address', 'N/A')}</div>
|
| 305 |
+
<div class="info-label">Plan Year:</div>
|
| 306 |
+
<div>{plan_data.get('year', datetime.now().year)}</div>
|
| 307 |
+
</div>
|
| 308 |
+
</div>
|
| 309 |
+
"""
|
| 310 |
+
|
| 311 |
+
# Add farm details
|
| 312 |
+
farms = plan_data.get('farms', [])
|
| 313 |
+
for i, farm in enumerate(farms):
|
| 314 |
+
html += f"""
|
| 315 |
+
<div class="section">
|
| 316 |
+
<h2>Farm {i+1}: {farm.get('farm_name', 'Unknown Farm')}</h2>
|
| 317 |
+
"""
|
| 318 |
+
|
| 319 |
+
# Parse farm plan
|
| 320 |
+
try:
|
| 321 |
+
farm_plan = json.loads(farm.get('plan', '{}')) if isinstance(farm.get('plan'), str) else farm.get('plan', {})
|
| 322 |
+
|
| 323 |
+
# Monthly Plan
|
| 324 |
+
if 'monthly_plan' in farm_plan:
|
| 325 |
+
html += """
|
| 326 |
+
<h3>Monthly Farming Schedule</h3>
|
| 327 |
+
<table>
|
| 328 |
+
<thead>
|
| 329 |
+
<tr>
|
| 330 |
+
<th>Month</th>
|
| 331 |
+
<th>Planned Activities</th>
|
| 332 |
+
</tr>
|
| 333 |
+
</thead>
|
| 334 |
+
<tbody>
|
| 335 |
+
"""
|
| 336 |
+
for month_data in farm_plan['monthly_plan']:
|
| 337 |
+
month = month_data.get('month', '')
|
| 338 |
+
tasks = month_data.get('tasks', [])
|
| 339 |
+
tasks_html = '<br>'.join(tasks[:4]) # Show max 4 tasks
|
| 340 |
+
html += f"""
|
| 341 |
+
<tr>
|
| 342 |
+
<td><strong>{month}</strong></td>
|
| 343 |
+
<td>{tasks_html}</td>
|
| 344 |
+
</tr>
|
| 345 |
+
"""
|
| 346 |
+
html += """
|
| 347 |
+
</tbody>
|
| 348 |
+
</table>
|
| 349 |
+
"""
|
| 350 |
+
|
| 351 |
+
# Crop Information
|
| 352 |
+
crops = farm_plan.get('crops', [])
|
| 353 |
+
if crops:
|
| 354 |
+
html += """
|
| 355 |
+
<h3>Crop Details</h3>
|
| 356 |
+
<table>
|
| 357 |
+
<thead>
|
| 358 |
+
<tr>
|
| 359 |
+
<th>Crop Name</th>
|
| 360 |
+
<th>Sowing Month</th>
|
| 361 |
+
<th>Area</th>
|
| 362 |
+
</tr>
|
| 363 |
+
</thead>
|
| 364 |
+
<tbody>
|
| 365 |
+
"""
|
| 366 |
+
for crop in crops:
|
| 367 |
+
html += f"""
|
| 368 |
+
<tr>
|
| 369 |
+
<td>{crop.get('name', 'Unknown')}</td>
|
| 370 |
+
<td>{crop.get('sowing_month', 'N/A')}</td>
|
| 371 |
+
<td>{crop.get('area', 'N/A')}</td>
|
| 372 |
+
</tr>
|
| 373 |
+
"""
|
| 374 |
+
html += """
|
| 375 |
+
</tbody>
|
| 376 |
+
</table>
|
| 377 |
+
"""
|
| 378 |
+
|
| 379 |
+
# AI Generation Note
|
| 380 |
+
if farm_plan.get('ai_generated'):
|
| 381 |
+
html += """
|
| 382 |
+
<div class="highlight">
|
| 383 |
+
<strong>🤖 AI-Generated Plan:</strong> This plan was created using advanced AI analysis of soil conditions, weather patterns, and agricultural best practices.
|
| 384 |
+
</div>
|
| 385 |
+
"""
|
| 386 |
+
|
| 387 |
+
except Exception as e:
|
| 388 |
+
html += f"<p><em>Plan details could not be processed: {str(e)}</em></p>"
|
| 389 |
+
|
| 390 |
+
html += " </div>\n"
|
| 391 |
+
|
| 392 |
+
# Summary
|
| 393 |
+
summary_text = plan_data.get('summary', 'Comprehensive yearly plan generated with AI analysis.')
|
| 394 |
+
html += f"""
|
| 395 |
+
<div class="section">
|
| 396 |
+
<h2>Summary & Recommendations</h2>
|
| 397 |
+
<p>{summary_text}</p>
|
| 398 |
+
<div class="highlight">
|
| 399 |
+
<strong>📱 Next Steps:</strong> Access your daily advisories through the farmer dashboard. Monitor weather alerts and market prices for optimal farming decisions.
|
| 400 |
+
</div>
|
| 401 |
+
</div>
|
| 402 |
+
|
| 403 |
+
<div class="footer">
|
| 404 |
+
<p>Generated on: {datetime.now().strftime('%B %d, %Y at %I:%M %p')}</p>
|
| 405 |
+
<p>🌱 <strong>Powered by AI Agriculture Assistant</strong></p>
|
| 406 |
+
<p><em>This plan is generated based on AI analysis and should be used in conjunction with local agricultural expertise.</em></p>
|
| 407 |
+
</div>
|
| 408 |
+
</div>
|
| 409 |
+
</body>
|
| 410 |
+
</html>
|
| 411 |
+
"""
|
| 412 |
+
|
| 413 |
+
return html
|
| 414 |
+
|
| 415 |
+
def cleanup_old_files(self, days_old: int = 30):
|
| 416 |
+
"""Clean up old generated files"""
|
| 417 |
+
try:
|
| 418 |
+
import time
|
| 419 |
+
cutoff_time = time.time() - (days_old * 24 * 60 * 60)
|
| 420 |
+
|
| 421 |
+
for filename in os.listdir(self.output_dir):
|
| 422 |
+
filepath = os.path.join(self.output_dir, filename)
|
| 423 |
+
if os.path.isfile(filepath) and os.path.getctime(filepath) < cutoff_time:
|
| 424 |
+
os.remove(filepath)
|
| 425 |
+
logger.info(f"Removed old file: {filename}")
|
| 426 |
+
|
| 427 |
+
except Exception as e:
|
| 428 |
+
logger.error(f"Error cleaning up old files: {str(e)}")
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask==2.2.3
|
| 2 |
+
flask-sqlalchemy==3.0.3
|
| 3 |
+
gunicorn==20.1.0
|
| 4 |
+
requests==2.28.2
|
| 5 |
+
python-dotenv==1.0.0
|
| 6 |
+
google-generativeai==0.3.2
|
| 7 |
+
flask-login==0.6.3
|
| 8 |
+
flask-wtf==1.1.1
|
| 9 |
+
wtforms==3.0.1
|
| 10 |
+
celery==5.3.4
|
| 11 |
+
redis==5.0.1
|
| 12 |
+
apscheduler==3.10.4
|
| 13 |
+
werkzeug==2.2.3
|
scripts/__pycache__/register_telegram_chat.cpython-39.pyc
ADDED
|
Binary file (3 kB). View file
|
|
|
scripts/get_telegram_chat_id.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Helper script to fetch chat_id for users who message the bot.
|
| 2 |
+
|
| 3 |
+
Usage:
|
| 4 |
+
1. Ensure TELEGRAM_BOT_TOKEN is set in your environment or in a .env file.
|
| 5 |
+
2. Run the script: python scripts/get_telegram_chat_id.py
|
| 6 |
+
3. Send a message to @Krushi_Mitra_Bot from the farmer's Telegram account.
|
| 7 |
+
4. The script will print recent updates with chat IDs.
|
| 8 |
+
|
| 9 |
+
This script polls Telegram's getUpdates endpoint once and prints chat ids.
|
| 10 |
+
"""
|
| 11 |
+
import os
|
| 12 |
+
import requests
|
| 13 |
+
from dotenv import load_dotenv
|
| 14 |
+
|
| 15 |
+
load_dotenv()
|
| 16 |
+
TOKEN = os.getenv('TELEGRAM_BOT_TOKEN')
|
| 17 |
+
if not TOKEN:
|
| 18 |
+
print('Please set TELEGRAM_BOT_TOKEN in environment or .env')
|
| 19 |
+
raise SystemExit(1)
|
| 20 |
+
|
| 21 |
+
URL = f'https://api.telegram.org/bot{TOKEN}/getUpdates'
|
| 22 |
+
|
| 23 |
+
resp = requests.get(URL, params={'timeout': 5})
|
| 24 |
+
if resp.status_code != 200:
|
| 25 |
+
print('Failed to fetch updates:', resp.status_code, resp.text)
|
| 26 |
+
raise SystemExit(1)
|
| 27 |
+
|
| 28 |
+
data = resp.json()
|
| 29 |
+
if not data.get('ok'):
|
| 30 |
+
print('Telegram API error:', data)
|
| 31 |
+
raise SystemExit(1)
|
| 32 |
+
|
| 33 |
+
results = data.get('result', [])
|
| 34 |
+
if not results:
|
| 35 |
+
print('No recent updates. Ask the farmer to send a message to the bot and retry.')
|
| 36 |
+
else:
|
| 37 |
+
print('Recent updates:')
|
| 38 |
+
for u in results:
|
| 39 |
+
update_id = u.get('update_id')
|
| 40 |
+
msg = u.get('message') or u.get('edited_message') or {}
|
| 41 |
+
chat = msg.get('chat', {})
|
| 42 |
+
print(f"update_id={update_id} chat.id={chat.get('id')} chat.username={chat.get('username')} name={chat.get('first_name')} {chat.get('last_name')}")
|
| 43 |
+
|
| 44 |
+
print('\nTip: copy the chat.id value and use the admin endpoint POST /admin/register_telegram/<farmer_id> with JSON {"chat_id": <id>}')
|
scripts/register_telegram_chat.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Register a Telegram chat_id for a farmer by updating the instance DB.
|
| 2 |
+
|
| 3 |
+
This is a small admin helper which writes the chat id directly into
|
| 4 |
+
`instance/farm_management.db` to avoid needing to authenticate via the
|
| 5 |
+
web admin UI. Usage:
|
| 6 |
+
|
| 7 |
+
python scripts/register_telegram_chat.py --farmer-id 1 --chat-id 123456789
|
| 8 |
+
|
| 9 |
+
It will print what it changed and exit with a non-zero code on error.
|
| 10 |
+
"""
|
| 11 |
+
import argparse
|
| 12 |
+
import sqlite3
|
| 13 |
+
import os
|
| 14 |
+
import sys
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def register_chat(farmer_id: int, chat_id: str, db_path: str):
|
| 18 |
+
if not os.path.exists(db_path):
|
| 19 |
+
print(f"Database not found: {db_path}")
|
| 20 |
+
return 2
|
| 21 |
+
|
| 22 |
+
con = sqlite3.connect(db_path)
|
| 23 |
+
cur = con.cursor()
|
| 24 |
+
|
| 25 |
+
# Check farmer exists
|
| 26 |
+
cur.execute("SELECT id, name, telegram_chat_id FROM farmers WHERE id = ?", (farmer_id,))
|
| 27 |
+
row = cur.fetchone()
|
| 28 |
+
if not row:
|
| 29 |
+
print(f"Farmer id {farmer_id} not found in {db_path}")
|
| 30 |
+
con.close()
|
| 31 |
+
return 3
|
| 32 |
+
|
| 33 |
+
old = row[2]
|
| 34 |
+
cur.execute("UPDATE farmers SET telegram_chat_id = ? WHERE id = ?", (str(chat_id), farmer_id))
|
| 35 |
+
con.commit()
|
| 36 |
+
con.close()
|
| 37 |
+
|
| 38 |
+
print(f"Updated farmer {farmer_id} telegram_chat_id: {old} -> {chat_id}")
|
| 39 |
+
return 0
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def main():
|
| 43 |
+
p = argparse.ArgumentParser()
|
| 44 |
+
p.add_argument('--farmer-id', '-f', type=int, required=False, help='Farmer id to update')
|
| 45 |
+
p.add_argument('--chat-id', '-c', required=True, help='Telegram chat id to set')
|
| 46 |
+
p.add_argument('--all', '-a', action='store_true', help='Apply chat id to all farmers')
|
| 47 |
+
p.add_argument('--db', default=os.path.join('instance', 'farm_management.db'), help='Path to instance DB')
|
| 48 |
+
args = p.parse_args()
|
| 49 |
+
|
| 50 |
+
if args.all:
|
| 51 |
+
# Update all farmers
|
| 52 |
+
if not os.path.exists(args.db):
|
| 53 |
+
print(f"Database not found: {args.db}")
|
| 54 |
+
sys.exit(2)
|
| 55 |
+
con = sqlite3.connect(args.db)
|
| 56 |
+
cur = con.cursor()
|
| 57 |
+
cur.execute("UPDATE farmers SET telegram_chat_id = ?", (str(args.chat_id),))
|
| 58 |
+
con.commit()
|
| 59 |
+
con.close()
|
| 60 |
+
print(f"Updated telegram_chat_id for ALL farmers -> {args.chat_id}")
|
| 61 |
+
rc = 0
|
| 62 |
+
else:
|
| 63 |
+
if not args.farmer_id:
|
| 64 |
+
print('Either --farmer-id or --all must be provided')
|
| 65 |
+
sys.exit(1)
|
| 66 |
+
rc = register_chat(args.farmer_id, args.chat_id, args.db)
|
| 67 |
+
sys.exit(rc)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
if __name__ == '__main__':
|
| 71 |
+
main()
|
telegram_service.py
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import logging
|
| 3 |
+
import json
|
| 4 |
+
from typing import Optional
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
# Configure logging
|
| 8 |
+
logging.basicConfig(level=logging.INFO)
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
class TelegramBotService:
|
| 12 |
+
"""Service class for Telegram Bot integration"""
|
| 13 |
+
|
| 14 |
+
def __init__(self, bot_token: str):
|
| 15 |
+
"""Initialize Telegram Bot service"""
|
| 16 |
+
self.bot_token = bot_token
|
| 17 |
+
self.base_url = f"https://api.telegram.org/bot{bot_token}"
|
| 18 |
+
|
| 19 |
+
def send_message(self, chat_id: str, message: str, parse_mode: str = 'HTML') -> dict:
|
| 20 |
+
"""
|
| 21 |
+
Send message via Telegram Bot
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
chat_id: Telegram chat ID or username
|
| 25 |
+
message: Message content to send
|
| 26 |
+
parse_mode: Message formatting (HTML, Markdown, or None)
|
| 27 |
+
|
| 28 |
+
Returns:
|
| 29 |
+
Dictionary with status and message details
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
url = f"{self.base_url}/sendMessage"
|
| 34 |
+
|
| 35 |
+
payload = {
|
| 36 |
+
'chat_id': chat_id,
|
| 37 |
+
'text': message,
|
| 38 |
+
'parse_mode': parse_mode
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
response = requests.post(url, json=payload, timeout=10)
|
| 42 |
+
|
| 43 |
+
if response.status_code == 200:
|
| 44 |
+
result = response.json()
|
| 45 |
+
if result.get('ok'):
|
| 46 |
+
logger.info(f"Telegram message sent successfully to {chat_id}")
|
| 47 |
+
return {
|
| 48 |
+
'status': 'sent',
|
| 49 |
+
'message_id': result['result']['message_id'],
|
| 50 |
+
'chat_id': chat_id,
|
| 51 |
+
'message': message,
|
| 52 |
+
'sent_at': datetime.utcnow(),
|
| 53 |
+
'error': None
|
| 54 |
+
}
|
| 55 |
+
else:
|
| 56 |
+
error_msg = result.get('description', 'Unknown Telegram API error')
|
| 57 |
+
logger.error(f"Telegram API error: {error_msg}")
|
| 58 |
+
return {
|
| 59 |
+
'status': 'failed',
|
| 60 |
+
'message_id': None,
|
| 61 |
+
'chat_id': chat_id,
|
| 62 |
+
'message': message,
|
| 63 |
+
'sent_at': datetime.utcnow(),
|
| 64 |
+
'error': error_msg
|
| 65 |
+
}
|
| 66 |
+
else:
|
| 67 |
+
error_msg = f"HTTP {response.status_code}: {response.text}"
|
| 68 |
+
logger.error(f"Failed to send Telegram message: {error_msg}")
|
| 69 |
+
return {
|
| 70 |
+
'status': 'failed',
|
| 71 |
+
'message_id': None,
|
| 72 |
+
'chat_id': chat_id,
|
| 73 |
+
'message': message,
|
| 74 |
+
'sent_at': datetime.utcnow(),
|
| 75 |
+
'error': error_msg
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
except Exception as e:
|
| 79 |
+
logger.error(f"Failed to send Telegram message to {chat_id}: {str(e)}")
|
| 80 |
+
|
| 81 |
+
return {
|
| 82 |
+
'status': 'failed',
|
| 83 |
+
'message_id': None,
|
| 84 |
+
'chat_id': chat_id,
|
| 85 |
+
'message': message,
|
| 86 |
+
'sent_at': datetime.utcnow(),
|
| 87 |
+
'error': str(e)
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
def get_me(self) -> Optional[dict]:
|
| 91 |
+
"""
|
| 92 |
+
Get bot information
|
| 93 |
+
|
| 94 |
+
Returns:
|
| 95 |
+
Bot info dictionary or None if failed
|
| 96 |
+
"""
|
| 97 |
+
|
| 98 |
+
try:
|
| 99 |
+
url = f"{self.base_url}/getMe"
|
| 100 |
+
response = requests.get(url, timeout=10)
|
| 101 |
+
|
| 102 |
+
if response.status_code == 200:
|
| 103 |
+
result = response.json()
|
| 104 |
+
if result.get('ok'):
|
| 105 |
+
return result['result']
|
| 106 |
+
|
| 107 |
+
return None
|
| 108 |
+
|
| 109 |
+
except Exception as e:
|
| 110 |
+
logger.error(f"Failed to get bot info: {str(e)}")
|
| 111 |
+
return None
|
| 112 |
+
|
| 113 |
+
def send_document(self, chat_id: str, document_path: str, caption: str = None, parse_mode: str = 'HTML') -> dict:
|
| 114 |
+
"""
|
| 115 |
+
Send document (PDF, Word, etc.) via Telegram Bot
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
chat_id: Telegram chat ID or username
|
| 119 |
+
document_path: Path to the document file
|
| 120 |
+
caption: Optional caption for the document
|
| 121 |
+
parse_mode: Caption formatting (HTML, Markdown, or None)
|
| 122 |
+
|
| 123 |
+
Returns:
|
| 124 |
+
Dictionary with status and message details
|
| 125 |
+
"""
|
| 126 |
+
|
| 127 |
+
try:
|
| 128 |
+
import os
|
| 129 |
+
if not os.path.exists(document_path):
|
| 130 |
+
return {
|
| 131 |
+
'status': 'failed',
|
| 132 |
+
'error': 'Document file not found'
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
url = f"{self.base_url}/sendDocument"
|
| 136 |
+
|
| 137 |
+
with open(document_path, 'rb') as document:
|
| 138 |
+
files = {'document': document}
|
| 139 |
+
data = {'chat_id': chat_id}
|
| 140 |
+
|
| 141 |
+
if caption:
|
| 142 |
+
data['caption'] = caption
|
| 143 |
+
data['parse_mode'] = parse_mode
|
| 144 |
+
|
| 145 |
+
response = requests.post(url, files=files, data=data, timeout=30)
|
| 146 |
+
|
| 147 |
+
if response.status_code == 200:
|
| 148 |
+
result = response.json()
|
| 149 |
+
if result.get('ok'):
|
| 150 |
+
logger.info(f"Telegram document sent successfully to {chat_id}")
|
| 151 |
+
return {
|
| 152 |
+
'status': 'sent',
|
| 153 |
+
'message_id': result['result']['message_id'],
|
| 154 |
+
'chat_id': chat_id,
|
| 155 |
+
'document_path': document_path,
|
| 156 |
+
'sent_at': datetime.utcnow(),
|
| 157 |
+
'error': None
|
| 158 |
+
}
|
| 159 |
+
else:
|
| 160 |
+
error_msg = result.get('description', 'Unknown Telegram API error')
|
| 161 |
+
logger.error(f"Telegram API error: {error_msg}")
|
| 162 |
+
return {
|
| 163 |
+
'status': 'failed',
|
| 164 |
+
'error': error_msg
|
| 165 |
+
}
|
| 166 |
+
else:
|
| 167 |
+
error_msg = f"HTTP {response.status_code}: {response.text}"
|
| 168 |
+
logger.error(f"Failed to send Telegram document: {error_msg}")
|
| 169 |
+
return {
|
| 170 |
+
'status': 'failed',
|
| 171 |
+
'error': error_msg
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
except Exception as e:
|
| 175 |
+
logger.error(f"Failed to send Telegram document to {chat_id}: {str(e)}")
|
| 176 |
+
return {
|
| 177 |
+
'status': 'failed',
|
| 178 |
+
'error': str(e)
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
def send_photo(self, chat_id: str, photo_path: str, caption: str = None, parse_mode: str = 'HTML') -> dict:
|
| 182 |
+
"""
|
| 183 |
+
Send photo/image via Telegram Bot
|
| 184 |
+
|
| 185 |
+
Args:
|
| 186 |
+
chat_id: Telegram chat ID or username
|
| 187 |
+
photo_path: Path to the photo file
|
| 188 |
+
caption: Optional caption for the photo
|
| 189 |
+
parse_mode: Caption formatting (HTML, Markdown, or None)
|
| 190 |
+
|
| 191 |
+
Returns:
|
| 192 |
+
Dictionary with status and message details
|
| 193 |
+
"""
|
| 194 |
+
|
| 195 |
+
try:
|
| 196 |
+
import os
|
| 197 |
+
if not os.path.exists(photo_path):
|
| 198 |
+
return {
|
| 199 |
+
'status': 'failed',
|
| 200 |
+
'error': 'Photo file not found'
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
url = f"{self.base_url}/sendPhoto"
|
| 204 |
+
|
| 205 |
+
with open(photo_path, 'rb') as photo:
|
| 206 |
+
files = {'photo': photo}
|
| 207 |
+
data = {'chat_id': chat_id}
|
| 208 |
+
|
| 209 |
+
if caption:
|
| 210 |
+
data['caption'] = caption
|
| 211 |
+
data['parse_mode'] = parse_mode
|
| 212 |
+
|
| 213 |
+
response = requests.post(url, files=files, data=data, timeout=30)
|
| 214 |
+
|
| 215 |
+
if response.status_code == 200:
|
| 216 |
+
result = response.json()
|
| 217 |
+
if result.get('ok'):
|
| 218 |
+
logger.info(f"Telegram photo sent successfully to {chat_id}")
|
| 219 |
+
return {
|
| 220 |
+
'status': 'sent',
|
| 221 |
+
'message_id': result['result']['message_id'],
|
| 222 |
+
'chat_id': chat_id,
|
| 223 |
+
'photo_path': photo_path,
|
| 224 |
+
'sent_at': datetime.utcnow(),
|
| 225 |
+
'error': None
|
| 226 |
+
}
|
| 227 |
+
else:
|
| 228 |
+
error_msg = result.get('description', 'Unknown Telegram API error')
|
| 229 |
+
logger.error(f"Telegram API error: {error_msg}")
|
| 230 |
+
return {
|
| 231 |
+
'status': 'failed',
|
| 232 |
+
'error': error_msg
|
| 233 |
+
}
|
| 234 |
+
else:
|
| 235 |
+
error_msg = f"HTTP {response.status_code}: {response.text}"
|
| 236 |
+
logger.error(f"Failed to send Telegram photo: {error_msg}")
|
| 237 |
+
return {
|
| 238 |
+
'status': 'failed',
|
| 239 |
+
'error': error_msg
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
except Exception as e:
|
| 243 |
+
logger.error(f"Failed to send Telegram photo to {chat_id}: {str(e)}")
|
| 244 |
+
return {
|
| 245 |
+
'status': 'failed',
|
| 246 |
+
'error': str(e)
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
def send_bulk_messages(self, recipients: list, message: str) -> list:
|
| 250 |
+
"""
|
| 251 |
+
Send message to multiple recipients
|
| 252 |
+
|
| 253 |
+
Args:
|
| 254 |
+
recipients: List of chat IDs
|
| 255 |
+
message: Message content
|
| 256 |
+
|
| 257 |
+
Returns:
|
| 258 |
+
List of send results
|
| 259 |
+
"""
|
| 260 |
+
|
| 261 |
+
results = []
|
| 262 |
+
|
| 263 |
+
for recipient in recipients:
|
| 264 |
+
result = self.send_message(recipient, message)
|
| 265 |
+
results.append(result)
|
| 266 |
+
|
| 267 |
+
return results
|
| 268 |
+
|
| 269 |
+
def format_daily_advisory_telegram(farmer_name: str, advisory_data: dict) -> str:
|
| 270 |
+
"""
|
| 271 |
+
Format daily advisory as Telegram message with HTML formatting
|
| 272 |
+
|
| 273 |
+
Args:
|
| 274 |
+
farmer_name: Name of the farmer
|
| 275 |
+
advisory_data: Advisory data from AI
|
| 276 |
+
|
| 277 |
+
Returns:
|
| 278 |
+
Formatted Telegram message with HTML
|
| 279 |
+
"""
|
| 280 |
+
|
| 281 |
+
message = f"🌱 <b>नमस्ते {farmer_name} जी!</b>\n\n"
|
| 282 |
+
message += f"📅 <b>आज की सलाह ({datetime.now().strftime('%d/%m/%Y')})</b>\n\n"
|
| 283 |
+
|
| 284 |
+
# Task to do
|
| 285 |
+
if advisory_data.get('task_to_do'):
|
| 286 |
+
message += f"✅ <b>आज करें:</b>\n{advisory_data['task_to_do']}\n\n"
|
| 287 |
+
|
| 288 |
+
# Task to avoid
|
| 289 |
+
if advisory_data.get('task_to_avoid'):
|
| 290 |
+
message += f"❌ <b>आज न करें:</b>\n{advisory_data['task_to_avoid']}\n\n"
|
| 291 |
+
|
| 292 |
+
# Reason/explanation
|
| 293 |
+
if advisory_data.get('reason_explanation'):
|
| 294 |
+
message += f"💡 <b>कारण:</b>\n{advisory_data['reason_explanation']}\n\n"
|
| 295 |
+
|
| 296 |
+
message += "🙏 <b>शुभ दिन!</b>\n"
|
| 297 |
+
message += "<i>- कृषि मित्र बॉट</i>"
|
| 298 |
+
|
| 299 |
+
return message
|
| 300 |
+
|
| 301 |
+
def format_weather_alert_telegram(farmer_name: str, weather_alert: str) -> str:
|
| 302 |
+
"""
|
| 303 |
+
Format weather alert as Telegram message
|
| 304 |
+
|
| 305 |
+
Args:
|
| 306 |
+
farmer_name: Name of the farmer
|
| 307 |
+
weather_alert: Weather alert message
|
| 308 |
+
|
| 309 |
+
Returns:
|
| 310 |
+
Formatted Telegram message
|
| 311 |
+
"""
|
| 312 |
+
|
| 313 |
+
message = f"🌦️ <b>मौसम अलर्ट!</b>\n\n"
|
| 314 |
+
message += f"<b>{farmer_name} जी,</b>\n\n"
|
| 315 |
+
message += f"{weather_alert}\n\n"
|
| 316 |
+
message += "⚠️ <b>कृपया अपनी फसल की सुरक्षा करें।</b>\n"
|
| 317 |
+
message += "<i>- कृषि मित्र बॉट</i>"
|
| 318 |
+
|
| 319 |
+
return message
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
def format_yearly_plan_summary(farmer_name: str, plan_summary) -> str:
|
| 323 |
+
"""
|
| 324 |
+
Format a yearly plan summary for Telegram - handles both dict and string inputs.
|
| 325 |
+
"""
|
| 326 |
+
message = f"🌾 <b>नमस्ते {farmer_name} जी!</b>\n\n"
|
| 327 |
+
|
| 328 |
+
# Handle string input (simple summary)
|
| 329 |
+
if isinstance(plan_summary, str):
|
| 330 |
+
message += f"📅 <b>आपकी वार्षिक खेती योजना तैयार है!</b>\n\n"
|
| 331 |
+
message += plan_summary + "\n\n"
|
| 332 |
+
message += "👉 पूरी विस्तृत योजना देखने के लिए अपने डैशबोर्ड पर जाएं।\n"
|
| 333 |
+
message += "📊 यह AI तकनीक से बनाई गई विस्तृत योजना है।\n"
|
| 334 |
+
message += "<i>- कृषि मित्र बॉट</i>"
|
| 335 |
+
return message
|
| 336 |
+
|
| 337 |
+
# Handle dict input (comprehensive plan)
|
| 338 |
+
year = plan_summary.get('year', 'Current Year')
|
| 339 |
+
message += f"📅 <b>आपकी वार्षिक खेती योजना ({year}) तैयार है!</b>\n\n"
|
| 340 |
+
message += "🤖 <b>AI तकनीक से बनाई गई विस्तृत योजना</b>\n\n"
|
| 341 |
+
|
| 342 |
+
farms = plan_summary.get('farms', [])
|
| 343 |
+
for f in farms:
|
| 344 |
+
farm_name = f.get('farm_name', 'Unknown Farm')
|
| 345 |
+
message += f"🏡 <b>{farm_name}</b>\n"
|
| 346 |
+
|
| 347 |
+
# Check if AI generated
|
| 348 |
+
if f.get('ai_generated', False):
|
| 349 |
+
message += "✅ AI द्वारा विश्लेषण किया गया\n"
|
| 350 |
+
|
| 351 |
+
message += "📋 मिली है विस्तृत जानकारी:\n"
|
| 352 |
+
message += "• मासिक गतिविधि कैलेंडर\n"
|
| 353 |
+
message += "• फसल-वार रणनीति\n"
|
| 354 |
+
message += "• वित्तीय अनुमान\n"
|
| 355 |
+
message += "• मिट्टी प्रबंधन योजना\n"
|
| 356 |
+
message += "• जोखिम प्रबंधन\n\n"
|
| 357 |
+
|
| 358 |
+
message += "� <b>मुख्य विशेषताएं:</b>\n"
|
| 359 |
+
message += "📊 विस्तृत तालिकाओं के साथ\n"
|
| 360 |
+
message += "📈 वित्तीय अनुमान के साथ\n"
|
| 361 |
+
message += "🌡️ मौसम आधारित सुझाव\n"
|
| 362 |
+
message += "🧪 मिट्टी परीक्षण आधारित सलाह\n\n"
|
| 363 |
+
|
| 364 |
+
message += "👆 <b>पूरी योजना देखने के लिए:</b>\n"
|
| 365 |
+
message += "1️⃣ अपने डैशबोर्ड पर जाएं\n"
|
| 366 |
+
message += "2️⃣ अपने खेत के पास 'View Plan' पर क्लिक करें\n"
|
| 367 |
+
message += "3️⃣ विस्तृत HTML रिपोर्ट देखें\n\n"
|
| 368 |
+
|
| 369 |
+
message += "🌟 <b>यह AI द्वारा बनाई गई पेशेवर खेती योजना है</b>\n"
|
| 370 |
+
message += "<i>- कृषि मित्र बॉट</i>"
|
| 371 |
+
return message
|
| 372 |
+
|
| 373 |
+
def format_complete_yearly_plan_telegram(farmer_name: str, farm_name: str, plan_data: dict) -> list:
|
| 374 |
+
"""
|
| 375 |
+
Format complete yearly plan for Telegram (split into multiple messages due to length)
|
| 376 |
+
|
| 377 |
+
Args:
|
| 378 |
+
farmer_name: Name of the farmer
|
| 379 |
+
farm_name: Name of the farm
|
| 380 |
+
plan_data: Complete plan data from gemini service
|
| 381 |
+
|
| 382 |
+
Returns:
|
| 383 |
+
List of formatted messages for Telegram
|
| 384 |
+
"""
|
| 385 |
+
|
| 386 |
+
messages = []
|
| 387 |
+
|
| 388 |
+
# Message 1: Header and Farm Overview
|
| 389 |
+
msg1 = f"🌾 <b>Comprehensive Yearly Farming Plan 2025</b>\n"
|
| 390 |
+
msg1 += f"👨🌾 <b>Farmer:</b> {farmer_name}\n"
|
| 391 |
+
msg1 += f"🏡 <b>Farm:</b> {farm_name}\n\n"
|
| 392 |
+
|
| 393 |
+
# Extract farm overview from plan data
|
| 394 |
+
farms = plan_data.get('farms', [])
|
| 395 |
+
if farms:
|
| 396 |
+
farm = farms[0] # Get first farm
|
| 397 |
+
if 'plan' in farm:
|
| 398 |
+
# Try to extract key information from HTML plan
|
| 399 |
+
plan_content = farm['plan']
|
| 400 |
+
|
| 401 |
+
# Extract farm size, crops, etc. from plan content
|
| 402 |
+
msg1 += "📊 <b>Farm Overview:</b>\n"
|
| 403 |
+
if 'farm_size' in str(plan_content).lower():
|
| 404 |
+
msg1 += "• Farm details included in comprehensive plan\n"
|
| 405 |
+
if 'crop' in str(plan_content).lower():
|
| 406 |
+
msg1 += "• Crop-wise strategies provided\n"
|
| 407 |
+
if 'month' in str(plan_content).lower():
|
| 408 |
+
msg1 += "• Monthly calendar included\n"
|
| 409 |
+
msg1 += "\n"
|
| 410 |
+
|
| 411 |
+
msg1 += "📋 <b>Plan Components:</b>\n"
|
| 412 |
+
msg1 += "✅ Monthly Farming Calendar\n"
|
| 413 |
+
msg1 += "✅ Crop-wise Annual Strategy\n"
|
| 414 |
+
msg1 += "✅ Financial Projections\n"
|
| 415 |
+
msg1 += "✅ Soil Management Plan\n"
|
| 416 |
+
msg1 += "✅ Risk Management\n"
|
| 417 |
+
msg1 += "✅ Seasonal Recommendations\n\n"
|
| 418 |
+
|
| 419 |
+
messages.append(msg1)
|
| 420 |
+
|
| 421 |
+
# Message 2: Key Monthly Activities (simplified)
|
| 422 |
+
msg2 = "📅 <b>Key Monthly Activities Summary:</b>\n\n"
|
| 423 |
+
|
| 424 |
+
monthly_activities = {
|
| 425 |
+
"जनवरी (January)": "• रबी फसल की देखभाल\n• गेहूं/सरसों की निराई\n• सब्जियों की बुआई",
|
| 426 |
+
"फरवरी (February)": "• फसल में सिंचाई\n• उर्वरक का छिड़काव\n• बीज तैयारी",
|
| 427 |
+
"मार्च (March)": "• रबी फसल की कटाई\n• खरीफ की तैयारी\n• मिट्टी की जांच",
|
| 428 |
+
"अप्रैल (April)": "• खेत की जुताई\n• कम्पोस्ट तैयारी\n• सिंचाई व्यवस्था",
|
| 429 |
+
"मई (May)": "• खरीफ बीज तैयारी\n• मिट्टी उपचार\n• पानी की व्यवस्था",
|
| 430 |
+
"जून (June)": "• खरीफ बुआई\n• धान/मक्का रोपाई\n• कीट नियंत्रण"
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
for month, activities in list(monthly_activities.items())[:6]:
|
| 434 |
+
msg2 += f"<b>{month}:</b>\n{activities}\n\n"
|
| 435 |
+
|
| 436 |
+
messages.append(msg2)
|
| 437 |
+
|
| 438 |
+
# Message 3: Remaining months and recommendations
|
| 439 |
+
msg3 = "📅 <b>Remaining Months & Key Tips:</b>\n\n"
|
| 440 |
+
|
| 441 |
+
remaining_months = {
|
| 442 |
+
"जुलाई (July)": "• खरीफ फसल देखभाल\n• कीट-रोग नियंत्रण\n• निराई-गुड़ाई",
|
| 443 |
+
"अगस्त (August)": "• उर्वरक प्रबंधन\n• सिंचाई नियमित\n• फसल निगरानी",
|
| 444 |
+
"सितंबर (September)": "• फसल सुरक्षा\n• कटाई तैयारी\n• बाजार जानकारी",
|
| 445 |
+
"अक्टूबर (October)": "• खरीफ कटाई\n• रबी तैयारी\n• भंडारण व्यवस्था",
|
| 446 |
+
"नवंबर (November)": "• रबी बुआई\n• गेहूं-सरसों रोपाई\n• खाद प्रबंधन",
|
| 447 |
+
"दिसंबर (December)": "• फसल देखभाल\n• सिंचाई व्यवस्था\n• अगले वर्ष योजना"
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
for month, activities in remaining_months.items():
|
| 451 |
+
msg3 += f"<b>{month}:</b>\n{activities}\n\n"
|
| 452 |
+
|
| 453 |
+
msg3 += "💡 <b>Important Tips:</b>\n"
|
| 454 |
+
msg3 += "• मौसम अपडेट नियमित देखें\n"
|
| 455 |
+
msg3 += "• मिट्टी परीक्षण साल में दो बार\n"
|
| 456 |
+
msg3 += "• कीट-रोग की निगरानी रखें\n"
|
| 457 |
+
msg3 += "• बाजार भाव की जानकारी रखें\n\n"
|
| 458 |
+
|
| 459 |
+
messages.append(msg3)
|
| 460 |
+
|
| 461 |
+
# Message 4: Financial and final recommendations
|
| 462 |
+
msg4 = "💰 <b>Financial Planning & Final Notes:</b>\n\n"
|
| 463 |
+
msg4 += "<b>वित्तीय योजना:</b>\n"
|
| 464 |
+
msg4 += "• बीज, खाद की लागत का बजट\n"
|
| 465 |
+
msg4 += "• अपेक्षित उत्पादन की गणना\n"
|
| 466 |
+
msg4 += "• बाजार मूल्य का अनुमान\n"
|
| 467 |
+
msg4 += "• मुनाफे का अनुमान\n\n"
|
| 468 |
+
|
| 469 |
+
msg4 += "<b>जोखिम प्रबंधन:</b>\n"
|
| 470 |
+
msg4 += "• फसल बीमा कराएं\n"
|
| 471 |
+
msg4 += "• मौसम आधारित बचाव\n"
|
| 472 |
+
msg4 += "• वैकल्पिक फसल योजना\n"
|
| 473 |
+
msg4 += "• पानी संरक्षण करें\n\n"
|
| 474 |
+
|
| 475 |
+
msg4 += "📄 <b>Complete detailed plan with tables and charts is available in the PDF file sent above.</b>\n\n"
|
| 476 |
+
msg4 += "🌟 <b>यह AI-powered comprehensive farming plan है जो आपकी मिट्टी, मौसम, और फसल के डेटा के आधार पर बनाई गई है।</b>\n\n"
|
| 477 |
+
msg4 += "<i>🤖 Generated by Agricultural AI Assistant</i>"
|
| 478 |
+
|
| 479 |
+
messages.append(msg4)
|
| 480 |
+
|
| 481 |
+
return messages
|
| 482 |
+
|
| 483 |
+
def extract_plan_content_from_html(html_content: str) -> dict:
|
| 484 |
+
"""
|
| 485 |
+
Extract key information from HTML plan content for Telegram formatting
|
| 486 |
+
|
| 487 |
+
Args:
|
| 488 |
+
html_content: HTML content of the yearly plan
|
| 489 |
+
|
| 490 |
+
Returns:
|
| 491 |
+
Dictionary with extracted content
|
| 492 |
+
"""
|
| 493 |
+
|
| 494 |
+
try:
|
| 495 |
+
# Remove HTML tags and extract text content
|
| 496 |
+
import re
|
| 497 |
+
|
| 498 |
+
# Clean HTML content
|
| 499 |
+
text_content = re.sub(r'<[^>]+>', '', html_content)
|
| 500 |
+
text_content = re.sub(r'\s+', ' ', text_content).strip()
|
| 501 |
+
|
| 502 |
+
extracted = {
|
| 503 |
+
'farm_overview': '',
|
| 504 |
+
'monthly_activities': '',
|
| 505 |
+
'crop_strategy': '',
|
| 506 |
+
'financial_info': '',
|
| 507 |
+
'soil_management': ''
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
# Look for specific sections
|
| 511 |
+
if 'Farm Overview' in text_content:
|
| 512 |
+
try:
|
| 513 |
+
start = text_content.find('Farm Overview')
|
| 514 |
+
end = text_content.find('Monthly Farming Calendar', start) if 'Monthly Farming Calendar' in text_content else start + 500
|
| 515 |
+
extracted['farm_overview'] = text_content[start:end][:500] + "..."
|
| 516 |
+
except:
|
| 517 |
+
pass
|
| 518 |
+
|
| 519 |
+
if 'Monthly Farming Calendar' in text_content:
|
| 520 |
+
try:
|
| 521 |
+
start = text_content.find('Monthly Farming Calendar')
|
| 522 |
+
end = text_content.find('Crop-wise Annual Strategy', start) if 'Crop-wise Annual Strategy' in text_content else start + 800
|
| 523 |
+
extracted['monthly_activities'] = text_content[start:end][:800] + "..."
|
| 524 |
+
except:
|
| 525 |
+
pass
|
| 526 |
+
|
| 527 |
+
if 'Crop-wise Annual Strategy' in text_content:
|
| 528 |
+
try:
|
| 529 |
+
start = text_content.find('Crop-wise Annual Strategy')
|
| 530 |
+
end = text_content.find('Financial', start) if 'Financial' in text_content else start + 600
|
| 531 |
+
extracted['crop_strategy'] = text_content[start:end][:600] + "..."
|
| 532 |
+
except:
|
| 533 |
+
pass
|
| 534 |
+
|
| 535 |
+
return extracted
|
| 536 |
+
|
| 537 |
+
except Exception as e:
|
| 538 |
+
logger.error(f"Error extracting plan content from HTML: {str(e)}")
|
| 539 |
+
return {}
|
| 540 |
+
|
| 541 |
+
def format_enhanced_yearly_plan_telegram(farmer_name: str, farm_name: str, plan_data: dict) -> list:
|
| 542 |
+
"""
|
| 543 |
+
Enhanced yearly plan formatting that tries to extract actual plan content
|
| 544 |
+
|
| 545 |
+
Args:
|
| 546 |
+
farmer_name: Name of the farmer
|
| 547 |
+
farm_name: Name of the farm
|
| 548 |
+
plan_data: Complete plan data from gemini service
|
| 549 |
+
|
| 550 |
+
Returns:
|
| 551 |
+
List of formatted messages for Telegram
|
| 552 |
+
"""
|
| 553 |
+
|
| 554 |
+
messages = []
|
| 555 |
+
|
| 556 |
+
# Try to extract actual content from the plan
|
| 557 |
+
actual_content = {}
|
| 558 |
+
farms = plan_data.get('farms', [])
|
| 559 |
+
if farms and 'plan' in farms[0]:
|
| 560 |
+
actual_content = extract_plan_content_from_html(farms[0]['plan'])
|
| 561 |
+
|
| 562 |
+
# Message 1: Header and Farm Overview
|
| 563 |
+
msg1 = f"🌾 <b>Complete Yearly Farming Plan 2025</b>\n"
|
| 564 |
+
msg1 += f"👨🌾 <b>Farmer:</b> {farmer_name}\n"
|
| 565 |
+
msg1 += f"🏡 <b>Farm:</b> {farm_name}\n\n"
|
| 566 |
+
|
| 567 |
+
if actual_content.get('farm_overview'):
|
| 568 |
+
msg1 += "📊 <b>Farm Overview (From Your AI Plan):</b>\n"
|
| 569 |
+
msg1 += f"{actual_content['farm_overview'][:500]}...\n\n"
|
| 570 |
+
else:
|
| 571 |
+
msg1 += "📊 <b>Plan Components Generated:</b>\n"
|
| 572 |
+
msg1 += "✅ Detailed Farm Analysis\n"
|
| 573 |
+
msg1 += "✅ Soil & Weather Assessment\n"
|
| 574 |
+
msg1 += "✅ Crop Performance Metrics\n"
|
| 575 |
+
msg1 += "✅ Monthly Activity Schedule\n\n"
|
| 576 |
+
|
| 577 |
+
messages.append(msg1)
|
| 578 |
+
|
| 579 |
+
# Message 2: Monthly Activities
|
| 580 |
+
msg2 = "📅 <b>Monthly Farming Calendar:</b>\n\n"
|
| 581 |
+
|
| 582 |
+
if actual_content.get('monthly_activities'):
|
| 583 |
+
msg2 += f"{actual_content['monthly_activities'][:800]}...\n\n"
|
| 584 |
+
else:
|
| 585 |
+
# Provide comprehensive monthly guide
|
| 586 |
+
msg2 += "<b>🌱 Kharif Season (June-October):</b>\n"
|
| 587 |
+
msg2 += "• June: Land preparation, seed sowing\n"
|
| 588 |
+
msg2 += "• July: Transplanting, weed management\n"
|
| 589 |
+
msg2 += "• August: Fertilizer application, pest control\n"
|
| 590 |
+
msg2 += "• September: Crop monitoring, irrigation\n"
|
| 591 |
+
msg2 += "• October: Harvesting preparation\n\n"
|
| 592 |
+
|
| 593 |
+
msg2 += "<b>❄️ Rabi Season (November-April):</b>\n"
|
| 594 |
+
msg2 += "• November: Rabi crop sowing\n"
|
| 595 |
+
msg2 += "• December: Crop establishment, irrigation\n"
|
| 596 |
+
msg2 += "• January: Mid-season care, fertilizing\n"
|
| 597 |
+
msg2 += "• February: Disease monitoring, spraying\n"
|
| 598 |
+
msg2 += "• March: Pre-harvest activities\n"
|
| 599 |
+
msg2 += "• April: Harvesting, storage\n\n"
|
| 600 |
+
|
| 601 |
+
messages.append(msg2)
|
| 602 |
+
|
| 603 |
+
# Message 3: Crop Strategy and Financial Info
|
| 604 |
+
msg3 = "🌾 <b>Crop Strategy & Financial Planning:</b>\n\n"
|
| 605 |
+
|
| 606 |
+
if actual_content.get('crop_strategy'):
|
| 607 |
+
msg3 += f"<b>Crop-wise Strategy:</b>\n{actual_content['crop_strategy'][:600]}...\n\n"
|
| 608 |
+
else:
|
| 609 |
+
msg3 += "<b>Strategic Recommendations:</b>\n"
|
| 610 |
+
msg3 += "• Diversified cropping for risk reduction\n"
|
| 611 |
+
msg3 += "• Optimal seed varieties for your soil\n"
|
| 612 |
+
msg3 += "• Integrated pest management\n"
|
| 613 |
+
msg3 += "• Water-efficient irrigation scheduling\n"
|
| 614 |
+
msg3 += "• Market-oriented crop selection\n\n"
|
| 615 |
+
|
| 616 |
+
msg3 += "💰 <b>Financial Planning:</b>\n"
|
| 617 |
+
msg3 += "• Input cost optimization\n"
|
| 618 |
+
msg3 += "• Expected yield calculations\n"
|
| 619 |
+
msg3 += "• Revenue projections per crop\n"
|
| 620 |
+
msg3 += "• Profit margin analysis\n"
|
| 621 |
+
msg3 += "• Cash flow management\n\n"
|
| 622 |
+
|
| 623 |
+
messages.append(msg3)
|
| 624 |
+
|
| 625 |
+
# Message 4: Final recommendations and notes
|
| 626 |
+
msg4 = "🎯 <b>Key Implementation Tips:</b>\n\n"
|
| 627 |
+
msg4 += "<b>🔬 Soil Management:</b>\n"
|
| 628 |
+
msg4 += "• Regular soil testing (twice yearly)\n"
|
| 629 |
+
msg4 += "• Organic matter enhancement\n"
|
| 630 |
+
msg4 += "• Balanced fertilizer application\n"
|
| 631 |
+
msg4 += "• pH level monitoring\n\n"
|
| 632 |
+
|
| 633 |
+
msg4 += "<b>⚠️ Risk Management:</b>\n"
|
| 634 |
+
msg4 += "• Weather-based crop insurance\n"
|
| 635 |
+
msg4 += "• Diversified income sources\n"
|
| 636 |
+
msg4 += "• Emergency fund planning\n"
|
| 637 |
+
msg4 += "• Alternative crop options\n\n"
|
| 638 |
+
|
| 639 |
+
msg4 += "📄 <b>The complete detailed plan with tables, charts, and specific calculations is available in the PDF sent above.</b>\n\n"
|
| 640 |
+
msg4 += "🤖 <b>This AI-generated plan is based on your specific soil data, weather patterns, and crop requirements.</b>\n\n"
|
| 641 |
+
msg4 += "<i>📱 For any questions, contact our agricultural support team!</i>"
|
| 642 |
+
|
| 643 |
+
messages.append(msg4)
|
| 644 |
+
|
| 645 |
+
return messages
|
templates/add_farm.html
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Add Farm - Farm Management Portal{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block extra_css %}
|
| 6 |
+
<style>
|
| 7 |
+
#map {
|
| 8 |
+
height: 400px;
|
| 9 |
+
width: 100%;
|
| 10 |
+
border-radius: 8px;
|
| 11 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 12 |
+
margin-bottom: 20px;
|
| 13 |
+
}
|
| 14 |
+
.form-section {
|
| 15 |
+
background-color: #f8f9fa;
|
| 16 |
+
padding: 20px;
|
| 17 |
+
border-radius: 8px;
|
| 18 |
+
margin-bottom: 20px;
|
| 19 |
+
}
|
| 20 |
+
</style>
|
| 21 |
+
{% endblock %}
|
| 22 |
+
|
| 23 |
+
{% block content %}
|
| 24 |
+
<div class="container mt-4">
|
| 25 |
+
<div class="row">
|
| 26 |
+
<div class="col-12">
|
| 27 |
+
<div class="card">
|
| 28 |
+
<div class="card-header bg-success text-white">
|
| 29 |
+
<h4><i class="fas fa-plus me-2"></i>Add New Farm</h4>
|
| 30 |
+
</div>
|
| 31 |
+
<div class="card-body">
|
| 32 |
+
<form method="POST" id="farmForm">
|
| 33 |
+
<div class="row">
|
| 34 |
+
<!-- Farm Details -->
|
| 35 |
+
<div class="col-md-6">
|
| 36 |
+
<div class="form-section">
|
| 37 |
+
<h5><i class="fas fa-seedling me-2"></i>Farm Details</h5>
|
| 38 |
+
|
| 39 |
+
<div class="mb-3">
|
| 40 |
+
<label for="farm_name" class="form-label">Farm Name *</label>
|
| 41 |
+
<input type="text" class="form-control" id="farm_name" name="farm_name" required>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<div class="row">
|
| 45 |
+
<div class="col-md-6 mb-3">
|
| 46 |
+
<label for="farm_size" class="form-label">Farm Size (Acres) *</label>
|
| 47 |
+
<input type="number" class="form-control" id="farm_size" name="farm_size" step="0.01" min="0.1" required>
|
| 48 |
+
</div>
|
| 49 |
+
<div class="col-md-6 mb-3">
|
| 50 |
+
<label for="irrigation_type" class="form-label">Irrigation Type *</label>
|
| 51 |
+
<select class="form-select" id="irrigation_type" name="irrigation_type" required>
|
| 52 |
+
<option value="">Select Irrigation</option>
|
| 53 |
+
<option value="Drip">Drip Irrigation</option>
|
| 54 |
+
<option value="Sprinkler">Sprinkler</option>
|
| 55 |
+
<option value="Flood">Flood Irrigation</option>
|
| 56 |
+
<option value="Rain-fed">Rain-fed</option>
|
| 57 |
+
<option value="Bore-well">Bore-well</option>
|
| 58 |
+
</select>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<div class="mb-3">
|
| 63 |
+
<label for="farm_type" class="form-label">Farm Type *</label>
|
| 64 |
+
<select class="form-select" id="farm_type" name="farm_type" required onchange="toggleFarmSections()">
|
| 65 |
+
<option value="">Select Farm Type</option>
|
| 66 |
+
<option value="crop">Crop Farming</option>
|
| 67 |
+
<option value="dairy">Dairy Farm</option>
|
| 68 |
+
<option value="poultry">Poultry Farm</option>
|
| 69 |
+
<option value="goat">Goat Farming</option>
|
| 70 |
+
<option value="pig">Pig Farming</option>
|
| 71 |
+
<option value="fishery">Fish Farming</option>
|
| 72 |
+
<option value="mixed">Mixed Farming</option>
|
| 73 |
+
</select>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<!-- Crop Farming Section -->
|
| 77 |
+
<div class="mb-3" id="crop-section" style="display: none;">
|
| 78 |
+
<label class="form-label">Crop Types *</label>
|
| 79 |
+
<div class="row">
|
| 80 |
+
<div class="col-md-6">
|
| 81 |
+
<div class="form-check">
|
| 82 |
+
<input class="form-check-input" type="checkbox" value="Rice" id="crop_rice" name="crop_types">
|
| 83 |
+
<label class="form-check-label" for="crop_rice">Rice</label>
|
| 84 |
+
</div>
|
| 85 |
+
<div class="form-check">
|
| 86 |
+
<input class="form-check-input" type="checkbox" value="Wheat" id="crop_wheat" name="crop_types">
|
| 87 |
+
<label class="form-check-label" for="crop_wheat">Wheat</label>
|
| 88 |
+
</div>
|
| 89 |
+
<div class="form-check">
|
| 90 |
+
<input class="form-check-input" type="checkbox" value="Cotton" id="crop_cotton" name="crop_types">
|
| 91 |
+
<label class="form-check-label" for="crop_cotton">Cotton</label>
|
| 92 |
+
</div>
|
| 93 |
+
<div class="form-check">
|
| 94 |
+
<input class="form-check-input" type="checkbox" value="Sugarcane" id="crop_sugarcane" name="crop_types">
|
| 95 |
+
<label class="form-check-label" for="crop_sugarcane">Sugarcane</label>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
<div class="col-md-6">
|
| 99 |
+
<div class="form-check">
|
| 100 |
+
<input class="form-check-input" type="checkbox" value="Maize" id="crop_maize" name="crop_types">
|
| 101 |
+
<label class="form-check-label" for="crop_maize">Maize</label>
|
| 102 |
+
</div>
|
| 103 |
+
<div class="form-check">
|
| 104 |
+
<input class="form-check-input" type="checkbox" value="Vegetables" id="crop_vegetables" name="crop_types">
|
| 105 |
+
<label class="form-check-label" for="crop_vegetables">Vegetables</label>
|
| 106 |
+
</div>
|
| 107 |
+
<div class="form-check">
|
| 108 |
+
<input class="form-check-input" type="checkbox" value="Fruits" id="crop_fruits" name="crop_types">
|
| 109 |
+
<label class="form-check-label" for="crop_fruits">Fruits</label>
|
| 110 |
+
</div>
|
| 111 |
+
<div class="form-check">
|
| 112 |
+
<input class="form-check-input" type="checkbox" value="Pulses" id="crop_pulses" name="crop_types">
|
| 113 |
+
<label class="form-check-label" for="crop_pulses">Pulses</label>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
|
| 119 |
+
<!-- Livestock Section -->
|
| 120 |
+
<div class="mb-3" id="livestock-section" style="display: none;">
|
| 121 |
+
<label class="form-label">Livestock Details</label>
|
| 122 |
+
|
| 123 |
+
<div class="row">
|
| 124 |
+
<div class="col-md-6 mb-3">
|
| 125 |
+
<label for="livestock_types" class="form-label">Livestock Types</label>
|
| 126 |
+
<select class="form-select" id="livestock_types" name="livestock_types" multiple>
|
| 127 |
+
<option value="Cows">Cows</option>
|
| 128 |
+
<option value="Buffalo">Buffalo</option>
|
| 129 |
+
<option value="Chickens">Chickens</option>
|
| 130 |
+
<option value="Ducks">Ducks</option>
|
| 131 |
+
<option value="Goats">Goats</option>
|
| 132 |
+
<option value="Sheep">Sheep</option>
|
| 133 |
+
<option value="Pigs">Pigs</option>
|
| 134 |
+
<option value="Fish">Fish</option>
|
| 135 |
+
</select>
|
| 136 |
+
<div class="form-text">Hold Ctrl to select multiple types</div>
|
| 137 |
+
</div>
|
| 138 |
+
<div class="col-md-6 mb-3">
|
| 139 |
+
<label for="livestock_count" class="form-label">Total Livestock Count</label>
|
| 140 |
+
<input type="number" class="form-control" id="livestock_count" name="livestock_count" min="1">
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
|
| 144 |
+
<div class="row">
|
| 145 |
+
<div class="col-md-6 mb-3">
|
| 146 |
+
<label for="housing_type" class="form-label">Housing Type</label>
|
| 147 |
+
<select class="form-select" id="housing_type" name="housing_type">
|
| 148 |
+
<option value="">Select Housing Type</option>
|
| 149 |
+
<option value="Open">Open Housing</option>
|
| 150 |
+
<option value="Semi-Open">Semi-Open Housing</option>
|
| 151 |
+
<option value="Closed">Closed Housing</option>
|
| 152 |
+
<option value="Cage">Cage System</option>
|
| 153 |
+
<option value="Free-Range">Free Range</option>
|
| 154 |
+
<option value="Pond">Pond System</option>
|
| 155 |
+
</select>
|
| 156 |
+
</div>
|
| 157 |
+
<div class="col-md-6 mb-3">
|
| 158 |
+
<label for="feeding_system" class="form-label">Feeding System</label>
|
| 159 |
+
<select class="form-select" id="feeding_system" name="feeding_system">
|
| 160 |
+
<option value="">Select Feeding System</option>
|
| 161 |
+
<option value="Grazing">Grazing</option>
|
| 162 |
+
<option value="Stall-fed">Stall Feeding</option>
|
| 163 |
+
<option value="Mixed">Mixed System</option>
|
| 164 |
+
<option value="Automatic">Automatic Feeding</option>
|
| 165 |
+
<option value="Manual">Manual Feeding</option>
|
| 166 |
+
</select>
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
|
| 170 |
+
<div class="mb-3">
|
| 171 |
+
<label for="breed_info" class="form-label">Breed Information</label>
|
| 172 |
+
<textarea class="form-control" id="breed_info" name="breed_info" rows="2" placeholder="Describe the breeds of livestock you have..."></textarea>
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
|
| 176 |
+
<!-- Detailed crops table: name, area, sowing month -->
|
| 177 |
+
<div class="mb-3" id="crop-details-section" style="display: none;">
|
| 178 |
+
<label class="form-label">Crops (name, area in acres, sowing month) *</label>
|
| 179 |
+
<div class="table-responsive">
|
| 180 |
+
<table class="table table-bordered" id="crops-table-add">
|
| 181 |
+
<thead>
|
| 182 |
+
<tr>
|
| 183 |
+
<th>Crop Name</th>
|
| 184 |
+
<th>Area (acres)</th>
|
| 185 |
+
<th>Sowing Month</th>
|
| 186 |
+
<th>Actions</th>
|
| 187 |
+
</tr>
|
| 188 |
+
</thead>
|
| 189 |
+
<tbody id="crops-tbody-add">
|
| 190 |
+
<tr>
|
| 191 |
+
<td><input type="text" class="form-control" name="crop_name[]" required></td>
|
| 192 |
+
<td><input type="number" class="form-control" name="crop_area[]" step="0.01" min="0" required></td>
|
| 193 |
+
<td>
|
| 194 |
+
<select class="form-select" name="crop_month[]" required>
|
| 195 |
+
<option value="">Select Month</option>
|
| 196 |
+
<option>January</option>
|
| 197 |
+
<option>February</option>
|
| 198 |
+
<option>March</option>
|
| 199 |
+
<option>April</option>
|
| 200 |
+
<option>May</option>
|
| 201 |
+
<option>June</option>
|
| 202 |
+
<option>July</option>
|
| 203 |
+
<option>August</option>
|
| 204 |
+
<option>September</option>
|
| 205 |
+
<option>October</option>
|
| 206 |
+
<option>November</option>
|
| 207 |
+
<option>December</option>
|
| 208 |
+
</select>
|
| 209 |
+
</td>
|
| 210 |
+
<td><button type="button" class="btn btn-danger btn-sm remove-crop">Remove</button></td>
|
| 211 |
+
</tr>
|
| 212 |
+
</tbody>
|
| 213 |
+
</table>
|
| 214 |
+
<button type="button" id="add-crop-row" class="btn btn-success">Add Crop</button>
|
| 215 |
+
</div>
|
| 216 |
+
</div>
|
| 217 |
+
|
| 218 |
+
<!-- Soil Data Section -->
|
| 219 |
+
<h5><i class="fas fa-mountain me-2"></i>Soil Information</h5>
|
| 220 |
+
|
| 221 |
+
<div class="mb-3">
|
| 222 |
+
<label for="soil_type" class="form-label">Soil Type</label>
|
| 223 |
+
<select class="form-select" id="soil_type" name="soil_type">
|
| 224 |
+
<option value="">Select Soil Type</option>
|
| 225 |
+
<option value="Black">Black Soil</option>
|
| 226 |
+
<option value="Red">Red Soil</option>
|
| 227 |
+
<option value="Clay">Clay Soil</option>
|
| 228 |
+
<option value="Sandy">Sandy Soil</option>
|
| 229 |
+
<option value="Loamy">Loamy Soil</option>
|
| 230 |
+
</select>
|
| 231 |
+
</div>
|
| 232 |
+
|
| 233 |
+
<div class="row">
|
| 234 |
+
<div class="col-md-6 mb-3">
|
| 235 |
+
<label for="ph_level" class="form-label">pH Level</label>
|
| 236 |
+
<input type="number" class="form-control" id="ph_level" name="ph_level" step="0.1" min="0" max="14">
|
| 237 |
+
</div>
|
| 238 |
+
<div class="col-md-6 mb-3">
|
| 239 |
+
<label for="moisture_percentage" class="form-label">Moisture %</label>
|
| 240 |
+
<input type="number" class="form-control" id="moisture_percentage" name="moisture_percentage" step="0.1" min="0" max="100">
|
| 241 |
+
</div>
|
| 242 |
+
</div>
|
| 243 |
+
|
| 244 |
+
<div class="row">
|
| 245 |
+
<div class="col-md-4 mb-3">
|
| 246 |
+
<label for="nitrogen_level" class="form-label">Nitrogen (ppm)</label>
|
| 247 |
+
<input type="number" class="form-control" id="nitrogen_level" name="nitrogen_level" step="0.1" min="0">
|
| 248 |
+
</div>
|
| 249 |
+
<div class="col-md-4 mb-3">
|
| 250 |
+
<label for="phosphorus_level" class="form-label">Phosphorus (ppm)</label>
|
| 251 |
+
<input type="number" class="form-control" id="phosphorus_level" name="phosphorus_level" step="0.1" min="0">
|
| 252 |
+
</div>
|
| 253 |
+
<div class="col-md-4 mb-3">
|
| 254 |
+
<label for="potassium_level" class="form-label">Potassium (ppm)</label>
|
| 255 |
+
<input type="number" class="form-control" id="potassium_level" name="potassium_level" step="0.1" min="0">
|
| 256 |
+
</div>
|
| 257 |
+
</div>
|
| 258 |
+
</div>
|
| 259 |
+
</div>
|
| 260 |
+
|
| 261 |
+
<!-- Map Section -->
|
| 262 |
+
<div class="col-md-6">
|
| 263 |
+
<div class="form-section">
|
| 264 |
+
<h5><i class="fas fa-map-marker-alt me-2"></i>Farm Location</h5>
|
| 265 |
+
|
| 266 |
+
<div class="mb-3">
|
| 267 |
+
<div id="map"></div>
|
| 268 |
+
</div>
|
| 269 |
+
|
| 270 |
+
<div class="row mb-3">
|
| 271 |
+
<div class="col-6">
|
| 272 |
+
<button type="button" id="startDrawingBtn" class="btn btn-success w-100">
|
| 273 |
+
<i class="fas fa-draw-polygon me-2"></i>Draw Boundary
|
| 274 |
+
</button>
|
| 275 |
+
</div>
|
| 276 |
+
<div class="col-6">
|
| 277 |
+
<button type="button" id="clearDrawingBtn" class="btn btn-danger w-100">
|
| 278 |
+
<i class="fas fa-trash me-2"></i>Clear
|
| 279 |
+
</button>
|
| 280 |
+
</div>
|
| 281 |
+
</div>
|
| 282 |
+
|
| 283 |
+
<div class="alert alert-info" id="drawingStatus">
|
| 284 |
+
<i class="fas fa-info-circle me-2"></i>
|
| 285 |
+
Click "Draw Boundary" and mark your farm area on the map.
|
| 286 |
+
</div>
|
| 287 |
+
|
| 288 |
+
<div class="row">
|
| 289 |
+
<div class="col-6">
|
| 290 |
+
<label for="latitude" class="form-label">Latitude *</label>
|
| 291 |
+
<input type="number" class="form-control" id="latitude" name="latitude" step="0.000001" readonly required>
|
| 292 |
+
</div>
|
| 293 |
+
<div class="col-6">
|
| 294 |
+
<label for="longitude" class="form-label">Longitude *</label>
|
| 295 |
+
<input type="number" class="form-control" id="longitude" name="longitude" step="0.000001" readonly required>
|
| 296 |
+
</div>
|
| 297 |
+
</div>
|
| 298 |
+
|
| 299 |
+
<!-- Hidden fields for coordinates data -->
|
| 300 |
+
<input type="hidden" id="field_coordinates" name="field_coordinates">
|
| 301 |
+
</div>
|
| 302 |
+
</div>
|
| 303 |
+
</div>
|
| 304 |
+
|
| 305 |
+
<div class="row mt-4">
|
| 306 |
+
<div class="col-12 text-center">
|
| 307 |
+
<button type="submit" class="btn btn-success btn-lg me-3" id="submitBtn" disabled>
|
| 308 |
+
<i class="fas fa-save me-2"></i>Save Farm
|
| 309 |
+
</button>
|
| 310 |
+
<a href="{{ url_for('farmer_dashboard') }}" class="btn btn-secondary btn-lg">
|
| 311 |
+
<i class="fas fa-arrow-left me-2"></i>Back to Dashboard
|
| 312 |
+
</a>
|
| 313 |
+
</div>
|
| 314 |
+
</div>
|
| 315 |
+
</form>
|
| 316 |
+
</div>
|
| 317 |
+
</div>
|
| 318 |
+
</div>
|
| 319 |
+
</div>
|
| 320 |
+
</div>
|
| 321 |
+
{% endblock %}
|
| 322 |
+
|
| 323 |
+
{% block extra_js %}
|
| 324 |
+
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBvVLjWmCja331H8SuIZ4UlJdZytuYkC6Y&libraries=drawing,places&callback=initMap" async defer></script>
|
| 325 |
+
|
| 326 |
+
<script>
|
| 327 |
+
// Toggle farm sections based on farm type
|
| 328 |
+
function toggleFarmSections() {
|
| 329 |
+
const farmType = document.getElementById('farm_type').value;
|
| 330 |
+
const cropSection = document.getElementById('crop-section');
|
| 331 |
+
const livestockSection = document.getElementById('livestock-section');
|
| 332 |
+
const cropDetailsSection = document.getElementById('crop-details-section');
|
| 333 |
+
|
| 334 |
+
// Hide all sections first
|
| 335 |
+
cropSection.style.display = 'none';
|
| 336 |
+
livestockSection.style.display = 'none';
|
| 337 |
+
cropDetailsSection.style.display = 'none';
|
| 338 |
+
|
| 339 |
+
// Show relevant sections based on farm type
|
| 340 |
+
if (farmType === 'crop' || farmType === 'mixed') {
|
| 341 |
+
cropSection.style.display = 'block';
|
| 342 |
+
cropDetailsSection.style.display = 'block';
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
if (farmType === 'dairy' || farmType === 'poultry' || farmType === 'goat' ||
|
| 346 |
+
farmType === 'pig' || farmType === 'fishery' || farmType === 'mixed') {
|
| 347 |
+
livestockSection.style.display = 'block';
|
| 348 |
+
|
| 349 |
+
// Auto-select appropriate livestock types based on farm type
|
| 350 |
+
const livestockSelect = document.getElementById('livestock_types');
|
| 351 |
+
for (let option of livestockSelect.options) {
|
| 352 |
+
option.selected = false; // Clear previous selections
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
switch(farmType) {
|
| 356 |
+
case 'dairy':
|
| 357 |
+
for (let option of livestockSelect.options) {
|
| 358 |
+
if (option.value === 'Cows' || option.value === 'Buffalo') {
|
| 359 |
+
option.selected = true;
|
| 360 |
+
}
|
| 361 |
+
}
|
| 362 |
+
break;
|
| 363 |
+
case 'poultry':
|
| 364 |
+
for (let option of livestockSelect.options) {
|
| 365 |
+
if (option.value === 'Chickens' || option.value === 'Ducks') {
|
| 366 |
+
option.selected = true;
|
| 367 |
+
}
|
| 368 |
+
}
|
| 369 |
+
break;
|
| 370 |
+
case 'goat':
|
| 371 |
+
for (let option of livestockSelect.options) {
|
| 372 |
+
if (option.value === 'Goats') {
|
| 373 |
+
option.selected = true;
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
break;
|
| 377 |
+
case 'pig':
|
| 378 |
+
for (let option of livestockSelect.options) {
|
| 379 |
+
if (option.value === 'Pigs') {
|
| 380 |
+
option.selected = true;
|
| 381 |
+
}
|
| 382 |
+
}
|
| 383 |
+
break;
|
| 384 |
+
case 'fishery':
|
| 385 |
+
for (let option of livestockSelect.options) {
|
| 386 |
+
if (option.value === 'Fish') {
|
| 387 |
+
option.selected = true;
|
| 388 |
+
}
|
| 389 |
+
}
|
| 390 |
+
break;
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
let map, drawingManager, polygon;
|
| 396 |
+
let isDrawing = false;
|
| 397 |
+
|
| 398 |
+
function initMap() {
|
| 399 |
+
// Initialize map (centered on India)
|
| 400 |
+
map = new google.maps.Map(document.getElementById('map'), {
|
| 401 |
+
center: { lat: 20.5937, lng: 78.9629 },
|
| 402 |
+
zoom: 5,
|
| 403 |
+
mapTypeControl: true,
|
| 404 |
+
fullscreenControl: true,
|
| 405 |
+
streetViewControl: true
|
| 406 |
+
});
|
| 407 |
+
|
| 408 |
+
// Initialize drawing manager
|
| 409 |
+
drawingManager = new google.maps.drawing.DrawingManager({
|
| 410 |
+
drawingMode: null,
|
| 411 |
+
drawingControl: false,
|
| 412 |
+
polygonOptions: {
|
| 413 |
+
fillColor: '#4CAF50',
|
| 414 |
+
fillOpacity: 0.3,
|
| 415 |
+
strokeWeight: 2,
|
| 416 |
+
strokeColor: '#4CAF50',
|
| 417 |
+
clickable: true,
|
| 418 |
+
editable: true
|
| 419 |
+
}
|
| 420 |
+
});
|
| 421 |
+
|
| 422 |
+
drawingManager.setMap(map);
|
| 423 |
+
|
| 424 |
+
// Helper: compute approximate area in acres from polygon vertices
|
| 425 |
+
function computeAreaAcresFromLatLngArray(latlngArray) {
|
| 426 |
+
// latlngArray: [{lat, lng}, ...]
|
| 427 |
+
if (!latlngArray || latlngArray.length < 3) return 0;
|
| 428 |
+
|
| 429 |
+
// Compute average latitude
|
| 430 |
+
let latSum = 0;
|
| 431 |
+
latlngArray.forEach(p => latSum += p.lat);
|
| 432 |
+
const latAvg = latSum / latlngArray.length;
|
| 433 |
+
const latAvgRad = latAvg * Math.PI / 180.0;
|
| 434 |
+
|
| 435 |
+
const metersPerDegLat = 111132.92;
|
| 436 |
+
const metersPerDegLon = 111320.0 * Math.cos(latAvgRad);
|
| 437 |
+
|
| 438 |
+
// Convert to planar meters
|
| 439 |
+
const pts = latlngArray.map(p => ({
|
| 440 |
+
x: p.lng * metersPerDegLon,
|
| 441 |
+
y: p.lat * metersPerDegLat
|
| 442 |
+
}));
|
| 443 |
+
|
| 444 |
+
// Shoelace
|
| 445 |
+
let area = 0;
|
| 446 |
+
for (let i = 0; i < pts.length; i++) {
|
| 447 |
+
const j = (i + 1) % pts.length;
|
| 448 |
+
area += pts[i].x * pts[j].y - pts[j].x * pts[i].y;
|
| 449 |
+
}
|
| 450 |
+
area = Math.abs(area) / 2.0; // in m^2
|
| 451 |
+
|
| 452 |
+
// convert to acres
|
| 453 |
+
const acres = area / 4046.8564224;
|
| 454 |
+
return Math.round(acres * 10000) / 10000; // 4 decimal places
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
// Event listener for polygon completion
|
| 458 |
+
drawingManager.addListener('polygoncomplete', function(poly) {
|
| 459 |
+
if (polygon) {
|
| 460 |
+
polygon.setMap(null);
|
| 461 |
+
}
|
| 462 |
+
polygon = poly;
|
| 463 |
+
|
| 464 |
+
// Get polygon coordinates
|
| 465 |
+
const coordinates = [];
|
| 466 |
+
const vertices = polygon.getPath();
|
| 467 |
+
|
| 468 |
+
for (let i = 0; i < vertices.getLength(); i++) {
|
| 469 |
+
const xy = vertices.getAt(i);
|
| 470 |
+
coordinates.push([xy.lng(), xy.lat()]);
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
// Convert coordinates to objects with lat,lng for easier processing
|
| 474 |
+
const latlngObjects = coordinates.map(c => ({ lat: c[1], lng: c[0] }));
|
| 475 |
+
|
| 476 |
+
// Store coordinates
|
| 477 |
+
document.getElementById('field_coordinates').value = JSON.stringify(latlngObjects);
|
| 478 |
+
|
| 479 |
+
// Calculate center point
|
| 480 |
+
const bounds = new google.maps.LatLngBounds();
|
| 481 |
+
vertices.forEach(vertex => bounds.extend(vertex));
|
| 482 |
+
const center = bounds.getCenter();
|
| 483 |
+
|
| 484 |
+
// Update latitude and longitude fields
|
| 485 |
+
document.getElementById('latitude').value = center.lat();
|
| 486 |
+
document.getElementById('longitude').value = center.lng();
|
| 487 |
+
|
| 488 |
+
// Compute area and update farm_size input automatically
|
| 489 |
+
const acres = computeAreaAcresFromLatLngArray(latlngObjects);
|
| 490 |
+
if (acres > 0) {
|
| 491 |
+
document.getElementById('farm_size').value = acres;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
// Update status
|
| 495 |
+
document.getElementById('drawingStatus').innerHTML =
|
| 496 |
+
'<i class="fas fa-check-circle me-2"></i>Farm boundary marked successfully!';
|
| 497 |
+
document.getElementById('drawingStatus').className = 'alert alert-success';
|
| 498 |
+
|
| 499 |
+
// Enable submit button
|
| 500 |
+
document.getElementById('submitBtn').disabled = false;
|
| 501 |
+
|
| 502 |
+
// Stop drawing mode
|
| 503 |
+
drawingManager.setDrawingMode(null);
|
| 504 |
+
isDrawing = false;
|
| 505 |
+
|
| 506 |
+
// Update button text
|
| 507 |
+
document.getElementById('startDrawingBtn').innerHTML =
|
| 508 |
+
'<i class="fas fa-edit me-2"></i>Edit Boundary';
|
| 509 |
+
});
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
// Start drawing
|
| 513 |
+
document.getElementById('startDrawingBtn').addEventListener('click', function() {
|
| 514 |
+
if (!isDrawing) {
|
| 515 |
+
drawingManager.setDrawingMode(google.maps.drawing.OverlayType.POLYGON);
|
| 516 |
+
isDrawing = true;
|
| 517 |
+
this.innerHTML = '<i class="fas fa-stop me-2"></i>Stop Drawing';
|
| 518 |
+
this.className = 'btn btn-warning w-100';
|
| 519 |
+
} else {
|
| 520 |
+
drawingManager.setDrawingMode(null);
|
| 521 |
+
isDrawing = false;
|
| 522 |
+
this.innerHTML = '<i class="fas fa-draw-polygon me-2"></i>Draw Boundary';
|
| 523 |
+
this.className = 'btn btn-success w-100';
|
| 524 |
+
}
|
| 525 |
+
});
|
| 526 |
+
|
| 527 |
+
// Clear drawing
|
| 528 |
+
document.getElementById('clearDrawingBtn').addEventListener('click', function() {
|
| 529 |
+
if (polygon) {
|
| 530 |
+
polygon.setMap(null);
|
| 531 |
+
polygon = null;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
// Clear form fields
|
| 535 |
+
document.getElementById('field_coordinates').value = '';
|
| 536 |
+
document.getElementById('latitude').value = '';
|
| 537 |
+
document.getElementById('longitude').value = '';
|
| 538 |
+
|
| 539 |
+
// Reset status
|
| 540 |
+
document.getElementById('drawingStatus').innerHTML =
|
| 541 |
+
'<i class="fas fa-info-circle me-2"></i>Click "Draw Boundary" and mark your farm area on the map.';
|
| 542 |
+
document.getElementById('drawingStatus').className = 'alert alert-info';
|
| 543 |
+
|
| 544 |
+
// Disable submit button
|
| 545 |
+
document.getElementById('submitBtn').disabled = true;
|
| 546 |
+
|
| 547 |
+
// Reset drawing mode
|
| 548 |
+
drawingManager.setDrawingMode(null);
|
| 549 |
+
isDrawing = false;
|
| 550 |
+
|
| 551 |
+
// Reset button
|
| 552 |
+
const drawBtn = document.getElementById('startDrawingBtn');
|
| 553 |
+
drawBtn.innerHTML = '<i class="fas fa-draw-polygon me-2"></i>Draw Boundary';
|
| 554 |
+
drawBtn.className = 'btn btn-success w-100';
|
| 555 |
+
});
|
| 556 |
+
|
| 557 |
+
// Form validation
|
| 558 |
+
document.getElementById('farmForm').addEventListener('submit', function(e) {
|
| 559 |
+
const cropTypes = document.querySelectorAll('input[name="crop_types"]:checked');
|
| 560 |
+
|
| 561 |
+
if (cropTypes.length === 0) {
|
| 562 |
+
e.preventDefault();
|
| 563 |
+
alert('Please select at least one crop type.');
|
| 564 |
+
return false;
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
if (!document.getElementById('latitude').value || !document.getElementById('longitude').value) {
|
| 568 |
+
e.preventDefault();
|
| 569 |
+
alert('Please mark your farm location on the map.');
|
| 570 |
+
return false;
|
| 571 |
+
}
|
| 572 |
+
});
|
| 573 |
+
|
| 574 |
+
// Crop rows management (add/remove)
|
| 575 |
+
document.getElementById('add-crop-row').addEventListener('click', function() {
|
| 576 |
+
const tbody = document.getElementById('crops-tbody-add');
|
| 577 |
+
const row = document.createElement('tr');
|
| 578 |
+
row.innerHTML = `
|
| 579 |
+
<td><input type="text" class="form-control" name="crop_name[]" required></td>
|
| 580 |
+
<td><input type="number" class="form-control" name="crop_area[]" step="0.01" min="0" required></td>
|
| 581 |
+
<td>
|
| 582 |
+
<select class="form-select" name="crop_month[]" required>
|
| 583 |
+
<option value="">Select Month</option>
|
| 584 |
+
<option>January</option>
|
| 585 |
+
<option>February</option>
|
| 586 |
+
<option>March</option>
|
| 587 |
+
<option>April</option>
|
| 588 |
+
<option>May</option>
|
| 589 |
+
<option>June</option>
|
| 590 |
+
<option>July</option>
|
| 591 |
+
<option>August</option>
|
| 592 |
+
<option>September</option>
|
| 593 |
+
<option>October</option>
|
| 594 |
+
<option>November</option>
|
| 595 |
+
<option>December</option>
|
| 596 |
+
</select>
|
| 597 |
+
</td>
|
| 598 |
+
<td><button type="button" class="btn btn-danger btn-sm remove-crop">Remove</button></td>
|
| 599 |
+
`;
|
| 600 |
+
tbody.appendChild(row);
|
| 601 |
+
|
| 602 |
+
// Attach remove handler
|
| 603 |
+
row.querySelector('.remove-crop').addEventListener('click', function() {
|
| 604 |
+
if (tbody.children.length > 1) row.remove();
|
| 605 |
+
else alert('At least one crop is required');
|
| 606 |
+
});
|
| 607 |
+
});
|
| 608 |
+
|
| 609 |
+
// Attach remove handler to initial row(s)
|
| 610 |
+
document.querySelectorAll('#crops-tbody-add .remove-crop').forEach(btn => {
|
| 611 |
+
btn.addEventListener('click', function() {
|
| 612 |
+
const tbody = document.getElementById('crops-tbody-add');
|
| 613 |
+
if (tbody.children.length > 1) this.closest('tr').remove();
|
| 614 |
+
else alert('At least one crop is required');
|
| 615 |
+
});
|
| 616 |
+
});
|
| 617 |
+
|
| 618 |
+
// Get user location
|
| 619 |
+
if (navigator.geolocation) {
|
| 620 |
+
navigator.geolocation.getCurrentPosition(function(position) {
|
| 621 |
+
const userLocation = {
|
| 622 |
+
lat: position.coords.latitude,
|
| 623 |
+
lng: position.coords.longitude
|
| 624 |
+
};
|
| 625 |
+
map.setCenter(userLocation);
|
| 626 |
+
map.setZoom(15);
|
| 627 |
+
});
|
| 628 |
+
}
|
| 629 |
+
</script>
|
| 630 |
+
{% endblock %}
|
templates/admin_dashboard.html
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Admin Dashboard - Farm Management Portal{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="container mt-4">
|
| 7 |
+
<!-- Admin Header -->
|
| 8 |
+
<div class="row mb-4">
|
| 9 |
+
<div class="col-12">
|
| 10 |
+
<div class="card bg-dark text-white">
|
| 11 |
+
<div class="card-body">
|
| 12 |
+
<h2><i class="fas fa-shield-alt me-2"></i>Admin Dashboard</h2>
|
| 13 |
+
<p class="mb-0">System Management and Monitoring</p>
|
| 14 |
+
</div>
|
| 15 |
+
</div>
|
| 16 |
+
</div>
|
| 17 |
+
</div>
|
| 18 |
+
|
| 19 |
+
<!-- Statistics Cards -->
|
| 20 |
+
<div class="row mb-4">
|
| 21 |
+
<div class="col-md-3 mb-3">
|
| 22 |
+
<div class="card bg-primary text-white">
|
| 23 |
+
<div class="card-body text-center">
|
| 24 |
+
<i class="fas fa-users fa-3x mb-2"></i>
|
| 25 |
+
<h3>{{ total_farmers }}</h3>
|
| 26 |
+
<p class="mb-0">Total Farmers</p>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="col-md-3 mb-3">
|
| 31 |
+
<div class="card bg-success text-white">
|
| 32 |
+
<div class="card-body text-center">
|
| 33 |
+
<i class="fas fa-seedling fa-3x mb-2"></i>
|
| 34 |
+
<h3>{{ total_farms }}</h3>
|
| 35 |
+
<p class="mb-0">Total Farms</p>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
<div class="col-md-3 mb-3">
|
| 40 |
+
<div class="card bg-info text-white">
|
| 41 |
+
<div class="card-body text-center">
|
| 42 |
+
<i class="fas fa-brain fa-3x mb-2"></i>
|
| 43 |
+
<h3>{{ total_advisories }}</h3>
|
| 44 |
+
<p class="mb-0">AI Advisories</p>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
<div class="col-md-3 mb-3">
|
| 49 |
+
<div class="card bg-warning text-dark">
|
| 50 |
+
<div class="card-body text-center">
|
| 51 |
+
<i class="fas fa-sms fa-3x mb-2"></i>
|
| 52 |
+
<h3>{{ total_sms }}</h3>
|
| 53 |
+
<p class="mb-0">SMS Sent</p>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
<div class="row">
|
| 60 |
+
<!-- Quick Actions -->
|
| 61 |
+
<div class="col-md-4 mb-4">
|
| 62 |
+
<div class="card">
|
| 63 |
+
<div class="card-header bg-secondary text-white">
|
| 64 |
+
<h5><i class="fas fa-bolt me-2"></i>Quick Actions</h5>
|
| 65 |
+
</div>
|
| 66 |
+
<div class="card-body">
|
| 67 |
+
<div class="d-grid gap-2">
|
| 68 |
+
<a href="{{ url_for('admin_farmers') }}" class="btn btn-primary">
|
| 69 |
+
<i class="fas fa-users me-2"></i>Manage Farmers
|
| 70 |
+
</a>
|
| 71 |
+
<a href="{{ url_for('admin_sms_logs') }}" class="btn btn-info">
|
| 72 |
+
<i class="fas fa-sms me-2"></i>SMS Logs
|
| 73 |
+
</a>
|
| 74 |
+
<button class="btn btn-success" onclick="generateAllAdvisories()">
|
| 75 |
+
<i class="fas fa-magic me-2"></i>Generate All Advisories
|
| 76 |
+
</button>
|
| 77 |
+
<button class="btn btn-warning" onclick="sendAllSMS()">
|
| 78 |
+
<i class="fas fa-broadcast-tower me-2"></i>Send All SMS
|
| 79 |
+
</button>
|
| 80 |
+
<a href="{{ url_for('admin_logout') }}" class="btn btn-outline-danger">
|
| 81 |
+
<i class="fas fa-sign-out-alt me-2"></i>Logout
|
| 82 |
+
</a>
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
<!-- Recent Farmers -->
|
| 89 |
+
<div class="col-md-8 mb-4">
|
| 90 |
+
<div class="card">
|
| 91 |
+
<div class="card-header">
|
| 92 |
+
<h5><i class="fas fa-user-plus me-2"></i>Recent Farmers</h5>
|
| 93 |
+
</div>
|
| 94 |
+
<div class="card-body">
|
| 95 |
+
{% if recent_farmers %}
|
| 96 |
+
<div class="table-responsive">
|
| 97 |
+
<table class="table table-striped table-sm">
|
| 98 |
+
<thead>
|
| 99 |
+
<tr>
|
| 100 |
+
<th>Name</th>
|
| 101 |
+
<th>Contact</th>
|
| 102 |
+
<th>Registered</th>
|
| 103 |
+
<th>Status</th>
|
| 104 |
+
<th>Actions</th>
|
| 105 |
+
</tr>
|
| 106 |
+
</thead>
|
| 107 |
+
<tbody>
|
| 108 |
+
{% for farmer in recent_farmers %}
|
| 109 |
+
<tr>
|
| 110 |
+
<td>{{ farmer.name }}</td>
|
| 111 |
+
<td>{{ farmer.contact_number }}</td>
|
| 112 |
+
<td>{{ farmer.created_at.strftime('%d %b %Y') }}</td>
|
| 113 |
+
<td>
|
| 114 |
+
<span class="badge bg-{{ 'success' if farmer.is_active else 'danger' }}">
|
| 115 |
+
{{ 'Active' if farmer.is_active else 'Inactive' }}
|
| 116 |
+
</span>
|
| 117 |
+
</td>
|
| 118 |
+
<td>
|
| 119 |
+
<button class="btn btn-sm btn-outline-primary" onclick="viewFarmerDetails({{ farmer.id }})">
|
| 120 |
+
<i class="fas fa-eye"></i>
|
| 121 |
+
</button>
|
| 122 |
+
</td>
|
| 123 |
+
</tr>
|
| 124 |
+
{% endfor %}
|
| 125 |
+
</tbody>
|
| 126 |
+
</table>
|
| 127 |
+
</div>
|
| 128 |
+
{% else %}
|
| 129 |
+
<div class="text-center text-muted py-3">
|
| 130 |
+
<i class="fas fa-users fa-3x mb-3"></i>
|
| 131 |
+
<p>No farmers registered yet</p>
|
| 132 |
+
</div>
|
| 133 |
+
{% endif %}
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
|
| 139 |
+
<!-- SMS Logs -->
|
| 140 |
+
<div class="row">
|
| 141 |
+
<div class="col-12">
|
| 142 |
+
<div class="card">
|
| 143 |
+
<div class="card-header">
|
| 144 |
+
<h5><i class="fas fa-history me-2"></i>Recent SMS Activity</h5>
|
| 145 |
+
</div>
|
| 146 |
+
<div class="card-body">
|
| 147 |
+
{% if recent_sms %}
|
| 148 |
+
<div class="table-responsive">
|
| 149 |
+
<table class="table table-striped table-sm">
|
| 150 |
+
<thead>
|
| 151 |
+
<tr>
|
| 152 |
+
<th>Farmer</th>
|
| 153 |
+
<th>Phone</th>
|
| 154 |
+
<th>Message</th>
|
| 155 |
+
<th>Status</th>
|
| 156 |
+
<th>Sent At</th>
|
| 157 |
+
</tr>
|
| 158 |
+
</thead>
|
| 159 |
+
<tbody>
|
| 160 |
+
{% for sms in recent_sms %}
|
| 161 |
+
<tr>
|
| 162 |
+
<td>{{ sms.farmer.name if sms.farmer else 'Unknown' }}</td>
|
| 163 |
+
<td>{{ sms.phone_number }}</td>
|
| 164 |
+
<td>
|
| 165 |
+
<span class="text-truncate d-inline-block" style="max-width: 200px;" title="{{ sms.message_content }}">
|
| 166 |
+
{{ sms.message_content[:50] + '...' if sms.message_content|length > 50 else sms.message_content }}
|
| 167 |
+
</span>
|
| 168 |
+
</td>
|
| 169 |
+
<td>
|
| 170 |
+
<span class="badge bg-{{ 'success' if sms.status == 'sent' else 'danger' if sms.status == 'failed' else 'warning' }}">
|
| 171 |
+
{{ sms.status|title }}
|
| 172 |
+
</span>
|
| 173 |
+
</td>
|
| 174 |
+
<td>{{ sms.sent_at.strftime('%d %b %Y %H:%M') }}</td>
|
| 175 |
+
</tr>
|
| 176 |
+
{% endfor %}
|
| 177 |
+
</tbody>
|
| 178 |
+
</table>
|
| 179 |
+
</div>
|
| 180 |
+
{% else %}
|
| 181 |
+
<div class="text-center text-muted py-3">
|
| 182 |
+
<i class="fas fa-sms fa-3x mb-3"></i>
|
| 183 |
+
<p>No SMS activity yet</p>
|
| 184 |
+
</div>
|
| 185 |
+
{% endif %}
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
</div>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
|
| 192 |
+
<!-- Loading Modal -->
|
| 193 |
+
<div class="modal fade" id="loadingModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1">
|
| 194 |
+
<div class="modal-dialog modal-dialog-centered">
|
| 195 |
+
<div class="modal-content">
|
| 196 |
+
<div class="modal-body text-center">
|
| 197 |
+
<div class="spinner-border text-primary" role="status">
|
| 198 |
+
<span class="visually-hidden">Loading...</span>
|
| 199 |
+
</div>
|
| 200 |
+
<p class="mt-3 mb-0">Processing request...</p>
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
</div>
|
| 205 |
+
|
| 206 |
+
<!-- Farmer Details Modal -->
|
| 207 |
+
<div class="modal fade" id="farmerModal" tabindex="-1">
|
| 208 |
+
<div class="modal-dialog modal-lg">
|
| 209 |
+
<div class="modal-content">
|
| 210 |
+
<div class="modal-header">
|
| 211 |
+
<h5 class="modal-title">Farmer Details</h5>
|
| 212 |
+
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
| 213 |
+
</div>
|
| 214 |
+
<div class="modal-body" id="farmerModalBody">
|
| 215 |
+
<!-- Farmer details will be loaded here -->
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
{% endblock %}
|
| 221 |
+
|
| 222 |
+
{% block extra_js %}
|
| 223 |
+
<script>
|
| 224 |
+
// Helper to show/hide the Bootstrap 5 modal using vanilla JS
|
| 225 |
+
function _getLoadingModalInstance(){
|
| 226 |
+
const modalEl = document.getElementById('loadingModal');
|
| 227 |
+
return bootstrap.Modal.getOrCreateInstance(modalEl);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
function _getFarmerModalInstance(){
|
| 231 |
+
const modalEl = document.getElementById('farmerModal');
|
| 232 |
+
return bootstrap.Modal.getOrCreateInstance(modalEl);
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
function generateAllAdvisories() {
|
| 236 |
+
if (!confirm('Generate advisories for all farms? This may take a few minutes.')) {
|
| 237 |
+
return;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
const modal = _getLoadingModalInstance();
|
| 241 |
+
modal.show();
|
| 242 |
+
|
| 243 |
+
fetch('/admin/generate_all_advisories', {method: 'POST'})
|
| 244 |
+
.then(response => response.json())
|
| 245 |
+
.then(data => {
|
| 246 |
+
modal.hide();
|
| 247 |
+
if (data.success) {
|
| 248 |
+
alert(`Successfully generated ${data.count} advisories!`);
|
| 249 |
+
location.reload();
|
| 250 |
+
} else {
|
| 251 |
+
alert('Failed to generate advisories: ' + (data.error || 'Unknown error'));
|
| 252 |
+
}
|
| 253 |
+
})
|
| 254 |
+
.catch(error => {
|
| 255 |
+
modal.hide();
|
| 256 |
+
alert('Error: ' + error.message);
|
| 257 |
+
});
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
function sendAllSMS() {
|
| 261 |
+
if (!confirm('Send SMS advisories to all farmers? This will send messages immediately.')) {
|
| 262 |
+
return;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
const modal = _getLoadingModalInstance();
|
| 266 |
+
modal.show();
|
| 267 |
+
|
| 268 |
+
fetch('/admin/send_all_sms', {method: 'POST'})
|
| 269 |
+
.then(response => response.json())
|
| 270 |
+
.then(data => {
|
| 271 |
+
modal.hide();
|
| 272 |
+
if (data.success) {
|
| 273 |
+
alert(`Successfully sent ${data.sent} SMS messages!`);
|
| 274 |
+
location.reload();
|
| 275 |
+
} else {
|
| 276 |
+
alert('Failed to send SMS: ' + (data.error || 'Unknown error'));
|
| 277 |
+
}
|
| 278 |
+
})
|
| 279 |
+
.catch(error => {
|
| 280 |
+
modal.hide();
|
| 281 |
+
alert('Error: ' + error.message);
|
| 282 |
+
});
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
function viewFarmerDetails(farmerId) {
|
| 286 |
+
const modal = _getLoadingModalInstance();
|
| 287 |
+
modal.show();
|
| 288 |
+
|
| 289 |
+
fetch(`/admin/farmer/${farmerId}`)
|
| 290 |
+
.then(response => response.json())
|
| 291 |
+
.then(data => {
|
| 292 |
+
modal.hide();
|
| 293 |
+
if (data.success) {
|
| 294 |
+
const farmer = data.farmer;
|
| 295 |
+
const farms = data.farms;
|
| 296 |
+
|
| 297 |
+
let farmsHtml = '';
|
| 298 |
+
if (farms.length > 0) {
|
| 299 |
+
farmsHtml = `
|
| 300 |
+
<h6>Farms:</h6>
|
| 301 |
+
<div class="table-responsive">
|
| 302 |
+
<table class="table table-sm">
|
| 303 |
+
<thead>
|
| 304 |
+
<tr>
|
| 305 |
+
<th>Name</th>
|
| 306 |
+
<th>Size</th>
|
| 307 |
+
<th>Crops</th>
|
| 308 |
+
</tr>
|
| 309 |
+
</thead>
|
| 310 |
+
<tbody>
|
| 311 |
+
`;
|
| 312 |
+
farms.forEach(farm => {
|
| 313 |
+
farmsHtml += `
|
| 314 |
+
<tr>
|
| 315 |
+
<td>${farm.farm_name}</td>
|
| 316 |
+
<td>${farm.farm_size} acres</td>
|
| 317 |
+
<td>${farm.crop_types.join(', ')}</td>
|
| 318 |
+
</tr>
|
| 319 |
+
`;
|
| 320 |
+
});
|
| 321 |
+
farmsHtml += '</tbody></table></div>';
|
| 322 |
+
} else {
|
| 323 |
+
farmsHtml = '<p class="text-muted">No farms registered</p>';
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
document.getElementById('farmerModalBody').innerHTML = `
|
| 327 |
+
<div class="row">
|
| 328 |
+
<div class="col-md-6">
|
| 329 |
+
<strong>Name:</strong> ${farmer.name}<br>
|
| 330 |
+
<strong>Age:</strong> ${farmer.age || 'N/A'}<br>
|
| 331 |
+
<strong>Gender:</strong> ${farmer.gender || 'N/A'}<br>
|
| 332 |
+
<strong>Aadhaar ID:</strong> ${farmer.aadhaar_id}
|
| 333 |
+
</div>
|
| 334 |
+
<div class="col-md-6">
|
| 335 |
+
<strong>Contact:</strong> ${farmer.contact_number}<br>
|
| 336 |
+
<strong>Address:</strong> ${farmer.address}<br>
|
| 337 |
+
<strong>Status:</strong>
|
| 338 |
+
<span class="badge bg-${farmer.is_active ? 'success' : 'danger'}">
|
| 339 |
+
${farmer.is_active ? 'Active' : 'Inactive'}
|
| 340 |
+
</span><br>
|
| 341 |
+
<strong>Registered:</strong> ${new Date(farmer.created_at).toLocaleDateString()}
|
| 342 |
+
</div>
|
| 343 |
+
</div>
|
| 344 |
+
<hr>
|
| 345 |
+
${farmsHtml}
|
| 346 |
+
`;
|
| 347 |
+
|
| 348 |
+
const farmerModal = _getFarmerModalInstance();
|
| 349 |
+
farmerModal.show();
|
| 350 |
+
} else {
|
| 351 |
+
alert('Failed to load farmer details');
|
| 352 |
+
}
|
| 353 |
+
})
|
| 354 |
+
.catch(error => {
|
| 355 |
+
modal.hide();
|
| 356 |
+
alert('Error: ' + error.message);
|
| 357 |
+
});
|
| 358 |
+
}
|
| 359 |
+
</script>
|
| 360 |
+
{% endblock %}
|
templates/admin_farmers.html
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Manage Farmers - Admin Portal{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="container-fluid py-4">
|
| 7 |
+
<div class="row">
|
| 8 |
+
<div class="col-12">
|
| 9 |
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
| 10 |
+
<h2><i class="fas fa-users text-primary me-3"></i>Manage Farmers</h2>
|
| 11 |
+
<div>
|
| 12 |
+
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-outline-secondary me-2">
|
| 13 |
+
<i class="fas fa-arrow-left me-2"></i>Back to Dashboard
|
| 14 |
+
</a>
|
| 15 |
+
<button class="btn btn-success" data-bs-toggle="modal" data-bs-target="#addFarmerModal">
|
| 16 |
+
<i class="fas fa-user-plus me-2"></i>Add Farmer
|
| 17 |
+
</button>
|
| 18 |
+
</div>
|
| 19 |
+
</div>
|
| 20 |
+
</div>
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<!-- Farmers Table -->
|
| 24 |
+
<div class="row">
|
| 25 |
+
<div class="col-12">
|
| 26 |
+
<div class="card shadow">
|
| 27 |
+
<div class="card-header bg-primary text-white">
|
| 28 |
+
<h5 class="mb-0"><i class="fas fa-list me-2"></i>All Farmers</h5>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="card-body">
|
| 31 |
+
<div class="table-responsive">
|
| 32 |
+
<table class="table table-striped" id="farmersTable">
|
| 33 |
+
<thead>
|
| 34 |
+
<tr>
|
| 35 |
+
<th>ID</th>
|
| 36 |
+
<th>Name</th>
|
| 37 |
+
<th>Aadhaar</th>
|
| 38 |
+
<th>Phone</th>
|
| 39 |
+
<th>District</th>
|
| 40 |
+
<th>State</th>
|
| 41 |
+
<th>Farms</th>
|
| 42 |
+
<th>Registered</th>
|
| 43 |
+
<th>Actions</th>
|
| 44 |
+
</tr>
|
| 45 |
+
</thead>
|
| 46 |
+
<tbody>
|
| 47 |
+
{% for farmer in farmers %}
|
| 48 |
+
<tr>
|
| 49 |
+
<td>{{ farmer.id }}</td>
|
| 50 |
+
<td>{{ farmer.name }}</td>
|
| 51 |
+
<td>{{ farmer.aadhaar_number[:4] }}****{{ farmer.aadhaar_number[-4:] }}</td>
|
| 52 |
+
<td>{{ farmer.phone_number }}</td>
|
| 53 |
+
<td>{{ farmer.district }}</td>
|
| 54 |
+
<td>{{ farmer.state }}</td>
|
| 55 |
+
<td>
|
| 56 |
+
<span class="badge bg-info">{{ farmer.farms|length }}</span>
|
| 57 |
+
</td>
|
| 58 |
+
<td>{{ farmer.created_at.strftime('%Y-%m-%d') }}</td>
|
| 59 |
+
<td>
|
| 60 |
+
<div class="btn-group" role="group">
|
| 61 |
+
<button class="btn btn-sm btn-primary" onclick="viewFarmer({{ farmer.id }})">
|
| 62 |
+
<i class="fas fa-eye"></i>
|
| 63 |
+
</button>
|
| 64 |
+
<button class="btn btn-sm btn-warning" onclick="editFarmer({{ farmer.id }})">
|
| 65 |
+
<i class="fas fa-edit"></i>
|
| 66 |
+
</button>
|
| 67 |
+
<button class="btn btn-sm btn-danger" onclick="deleteFarmer({{ farmer.id }})">
|
| 68 |
+
<i class="fas fa-trash"></i>
|
| 69 |
+
</button>
|
| 70 |
+
</div>
|
| 71 |
+
</td>
|
| 72 |
+
</tr>
|
| 73 |
+
{% endfor %}
|
| 74 |
+
</tbody>
|
| 75 |
+
</table>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<!-- Add Farmer Modal -->
|
| 84 |
+
<div class="modal fade" id="addFarmerModal" tabindex="-1">
|
| 85 |
+
<div class="modal-dialog modal-lg">
|
| 86 |
+
<div class="modal-content">
|
| 87 |
+
<div class="modal-header">
|
| 88 |
+
<h5 class="modal-title"><i class="fas fa-user-plus me-2"></i>Add New Farmer</h5>
|
| 89 |
+
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
| 90 |
+
</div>
|
| 91 |
+
<form id="addFarmerForm">
|
| 92 |
+
<div class="modal-body">
|
| 93 |
+
<div class="row">
|
| 94 |
+
<div class="col-md-6 mb-3">
|
| 95 |
+
<label for="farmerName" class="form-label">Full Name *</label>
|
| 96 |
+
<input type="text" class="form-control" id="farmerName" required>
|
| 97 |
+
</div>
|
| 98 |
+
<div class="col-md-6 mb-3">
|
| 99 |
+
<label for="farmerAadhaar" class="form-label">Aadhaar Number *</label>
|
| 100 |
+
<input type="text" class="form-control" id="farmerAadhaar" maxlength="12" required>
|
| 101 |
+
</div>
|
| 102 |
+
<div class="col-md-6 mb-3">
|
| 103 |
+
<label for="farmerPhone" class="form-label">Phone Number *</label>
|
| 104 |
+
<input type="tel" class="form-control" id="farmerPhone" required>
|
| 105 |
+
</div>
|
| 106 |
+
<div class="col-md-6 mb-3">
|
| 107 |
+
<label for="farmerEmail" class="form-label">Email</label>
|
| 108 |
+
<input type="email" class="form-control" id="farmerEmail">
|
| 109 |
+
</div>
|
| 110 |
+
<div class="col-md-6 mb-3">
|
| 111 |
+
<label for="farmerDistrict" class="form-label">District *</label>
|
| 112 |
+
<input type="text" class="form-control" id="farmerDistrict" required>
|
| 113 |
+
</div>
|
| 114 |
+
<div class="col-md-6 mb-3">
|
| 115 |
+
<label for="farmerState" class="form-label">State *</label>
|
| 116 |
+
<input type="text" class="form-control" id="farmerState" required>
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
<div class="modal-footer">
|
| 121 |
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
| 122 |
+
<button type="submit" class="btn btn-success">
|
| 123 |
+
<i class="fas fa-save me-2"></i>Add Farmer
|
| 124 |
+
</button>
|
| 125 |
+
</div>
|
| 126 |
+
</form>
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
|
| 131 |
+
<!-- View Farmer Modal -->
|
| 132 |
+
<div class="modal fade" id="viewFarmerModal" tabindex="-1">
|
| 133 |
+
<div class="modal-dialog modal-lg">
|
| 134 |
+
<div class="modal-content">
|
| 135 |
+
<div class="modal-header">
|
| 136 |
+
<h5 class="modal-title"><i class="fas fa-user me-2"></i>Farmer Details</h5>
|
| 137 |
+
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
| 138 |
+
</div>
|
| 139 |
+
<div class="modal-body" id="farmerDetailsContent">
|
| 140 |
+
<!-- Farmer details will be loaded here -->
|
| 141 |
+
</div>
|
| 142 |
+
<div class="modal-footer">
|
| 143 |
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
</div>
|
| 148 |
+
|
| 149 |
+
<script>
|
| 150 |
+
// Initialize DataTable
|
| 151 |
+
$(document).ready(function() {
|
| 152 |
+
$('#farmersTable').DataTable({
|
| 153 |
+
order: [[0, 'desc']],
|
| 154 |
+
pageLength: 10,
|
| 155 |
+
responsive: true
|
| 156 |
+
});
|
| 157 |
+
});
|
| 158 |
+
|
| 159 |
+
// Add Farmer Form
|
| 160 |
+
$('#addFarmerForm').on('submit', function(e) {
|
| 161 |
+
e.preventDefault();
|
| 162 |
+
|
| 163 |
+
const formData = {
|
| 164 |
+
name: $('#farmerName').val(),
|
| 165 |
+
aadhaar_number: $('#farmerAadhaar').val(),
|
| 166 |
+
phone_number: $('#farmerPhone').val(),
|
| 167 |
+
email: $('#farmerEmail').val(),
|
| 168 |
+
district: $('#farmerDistrict').val(),
|
| 169 |
+
state: $('#farmerState').val()
|
| 170 |
+
};
|
| 171 |
+
|
| 172 |
+
$.ajax({
|
| 173 |
+
url: '/admin/add_farmer',
|
| 174 |
+
method: 'POST',
|
| 175 |
+
contentType: 'application/json',
|
| 176 |
+
data: JSON.stringify(formData),
|
| 177 |
+
success: function(response) {
|
| 178 |
+
if (response.success) {
|
| 179 |
+
showToast('Farmer added successfully!', 'success');
|
| 180 |
+
$('#addFarmerModal').modal('hide');
|
| 181 |
+
location.reload();
|
| 182 |
+
} else {
|
| 183 |
+
showToast(response.message || 'Error adding farmer', 'error');
|
| 184 |
+
}
|
| 185 |
+
},
|
| 186 |
+
error: function() {
|
| 187 |
+
showToast('Error adding farmer', 'error');
|
| 188 |
+
}
|
| 189 |
+
});
|
| 190 |
+
});
|
| 191 |
+
|
| 192 |
+
// View Farmer Details
|
| 193 |
+
function viewFarmer(farmerId) {
|
| 194 |
+
$.ajax({
|
| 195 |
+
url: `/admin/farmer/${farmerId}`,
|
| 196 |
+
method: 'GET',
|
| 197 |
+
success: function(farmer) {
|
| 198 |
+
const content = `
|
| 199 |
+
<div class="row">
|
| 200 |
+
<div class="col-md-6">
|
| 201 |
+
<h6>Personal Information</h6>
|
| 202 |
+
<p><strong>Name:</strong> ${farmer.name}</p>
|
| 203 |
+
<p><strong>Aadhaar:</strong> ${farmer.aadhaar_number}</p>
|
| 204 |
+
<p><strong>Phone:</strong> ${farmer.phone_number}</p>
|
| 205 |
+
<p><strong>Email:</strong> ${farmer.email || 'Not provided'}</p>
|
| 206 |
+
</div>
|
| 207 |
+
<div class="col-md-6">
|
| 208 |
+
<h6>Location</h6>
|
| 209 |
+
<p><strong>District:</strong> ${farmer.district}</p>
|
| 210 |
+
<p><strong>State:</strong> ${farmer.state}</p>
|
| 211 |
+
<p><strong>Registered:</strong> ${new Date(farmer.created_at).toLocaleDateString()}</p>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
<hr>
|
| 215 |
+
<h6>Farms (${farmer.farms.length})</h6>
|
| 216 |
+
<div class="table-responsive">
|
| 217 |
+
<table class="table table-sm">
|
| 218 |
+
<thead>
|
| 219 |
+
<tr>
|
| 220 |
+
<th>Name</th>
|
| 221 |
+
<th>Area</th>
|
| 222 |
+
<th>Crops</th>
|
| 223 |
+
<th>Location</th>
|
| 224 |
+
</tr>
|
| 225 |
+
</thead>
|
| 226 |
+
<tbody>
|
| 227 |
+
${farmer.farms.map(farm => `
|
| 228 |
+
<tr>
|
| 229 |
+
<td>${farm.name}</td>
|
| 230 |
+
<td>${farm.area} acres</td>
|
| 231 |
+
<td>${farm.crop_types}</td>
|
| 232 |
+
<td>${farm.latitude.toFixed(4)}, ${farm.longitude.toFixed(4)}</td>
|
| 233 |
+
</tr>
|
| 234 |
+
`).join('')}
|
| 235 |
+
</tbody>
|
| 236 |
+
</table>
|
| 237 |
+
</div>
|
| 238 |
+
`;
|
| 239 |
+
$('#farmerDetailsContent').html(content);
|
| 240 |
+
$('#viewFarmerModal').modal('show');
|
| 241 |
+
},
|
| 242 |
+
error: function() {
|
| 243 |
+
showToast('Error loading farmer details', 'error');
|
| 244 |
+
}
|
| 245 |
+
});
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
// Edit Farmer
|
| 249 |
+
function editFarmer(farmerId) {
|
| 250 |
+
// Implementation for edit farmer
|
| 251 |
+
showToast('Edit farmer functionality coming soon!', 'info');
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
// Delete Farmer
|
| 255 |
+
function deleteFarmer(farmerId) {
|
| 256 |
+
if (confirm('Are you sure you want to delete this farmer? This action cannot be undone.')) {
|
| 257 |
+
$.ajax({
|
| 258 |
+
url: `/admin/farmer/${farmerId}`,
|
| 259 |
+
method: 'DELETE',
|
| 260 |
+
success: function(response) {
|
| 261 |
+
if (response.success) {
|
| 262 |
+
showToast('Farmer deleted successfully!', 'success');
|
| 263 |
+
location.reload();
|
| 264 |
+
} else {
|
| 265 |
+
showToast(response.message || 'Error deleting farmer', 'error');
|
| 266 |
+
}
|
| 267 |
+
},
|
| 268 |
+
error: function() {
|
| 269 |
+
showToast('Error deleting farmer', 'error');
|
| 270 |
+
}
|
| 271 |
+
});
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
// Toast notification function
|
| 276 |
+
function showToast(message, type) {
|
| 277 |
+
const toast = document.createElement('div');
|
| 278 |
+
toast.className = `alert alert-${type === 'success' ? 'success' : type === 'error' ? 'danger' : 'info'} alert-dismissible fade show position-fixed`;
|
| 279 |
+
toast.style.top = '20px';
|
| 280 |
+
toast.style.right = '20px';
|
| 281 |
+
toast.style.zIndex = '9999';
|
| 282 |
+
toast.innerHTML = `
|
| 283 |
+
${message}
|
| 284 |
+
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
| 285 |
+
`;
|
| 286 |
+
document.body.appendChild(toast);
|
| 287 |
+
|
| 288 |
+
setTimeout(() => {
|
| 289 |
+
toast.remove();
|
| 290 |
+
}, 5000);
|
| 291 |
+
}
|
| 292 |
+
</script>
|
| 293 |
+
{% endblock %}
|
templates/admin_login.html
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Admin Login - Farm Management Portal{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="container">
|
| 7 |
+
<div class="row justify-content-center mt-5">
|
| 8 |
+
<div class="col-md-6 col-lg-4">
|
| 9 |
+
<div class="card">
|
| 10 |
+
<div class="card-header bg-dark text-white text-center">
|
| 11 |
+
<h4><i class="fas fa-shield-alt me-2"></i>Admin Login</h4>
|
| 12 |
+
</div>
|
| 13 |
+
<div class="card-body">
|
| 14 |
+
<form method="POST">
|
| 15 |
+
<div class="mb-3">
|
| 16 |
+
<label for="username" class="form-label">Username</label>
|
| 17 |
+
<input type="text" class="form-control" id="username" name="username"
|
| 18 |
+
placeholder="Enter admin username" required>
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<div class="mb-3">
|
| 22 |
+
<label for="password" class="form-label">Password</label>
|
| 23 |
+
<input type="password" class="form-control" id="password" name="password"
|
| 24 |
+
placeholder="Enter admin password" required>
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
<div class="d-grid">
|
| 28 |
+
<button type="submit" class="btn btn-dark">
|
| 29 |
+
<i class="fas fa-sign-in-alt me-2"></i>Login as Admin
|
| 30 |
+
</button>
|
| 31 |
+
</div>
|
| 32 |
+
</form>
|
| 33 |
+
|
| 34 |
+
<hr>
|
| 35 |
+
|
| 36 |
+
<div class="text-center">
|
| 37 |
+
<small class="text-muted">Default Credentials:</small><br>
|
| 38 |
+
<small><strong>Username:</strong> admin</small><br>
|
| 39 |
+
<small><strong>Password:</strong> admin123</small>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<div class="text-center mt-3">
|
| 43 |
+
<a href="{{ url_for('index') }}" class="btn btn-outline-secondary btn-sm">
|
| 44 |
+
<i class="fas fa-home me-1"></i>Back to Home
|
| 45 |
+
</a>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
{% endblock %}
|
templates/admin_sms_logs.html
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}SMS Logs - Admin Portal{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="container-fluid py-4">
|
| 7 |
+
<div class="row">
|
| 8 |
+
<div class="col-12">
|
| 9 |
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
| 10 |
+
<h2><i class="fas fa-sms text-success me-3"></i>SMS Logs</h2>
|
| 11 |
+
<div>
|
| 12 |
+
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-outline-secondary me-2">
|
| 13 |
+
<i class="fas fa-arrow-left me-2"></i>Back to Dashboard
|
| 14 |
+
</a>
|
| 15 |
+
<button class="btn btn-success" onclick="sendTestSMS()">
|
| 16 |
+
<i class="fas fa-paper-plane me-2"></i>Send Test SMS
|
| 17 |
+
</button>
|
| 18 |
+
</div>
|
| 19 |
+
</div>
|
| 20 |
+
</div>
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<!-- SMS Statistics -->
|
| 24 |
+
<div class="row mb-4">
|
| 25 |
+
<div class="col-md-3">
|
| 26 |
+
<div class="card bg-primary text-white">
|
| 27 |
+
<div class="card-body text-center">
|
| 28 |
+
<h3>{{ total_sms }}</h3>
|
| 29 |
+
<p class="mb-0">Total SMS Sent</p>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
<div class="col-md-3">
|
| 34 |
+
<div class="card bg-success text-white">
|
| 35 |
+
<div class="card-body text-center">
|
| 36 |
+
<h3>{{ delivered_sms }}</h3>
|
| 37 |
+
<p class="mb-0">Delivered</p>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
<div class="col-md-3">
|
| 42 |
+
<div class="card bg-warning text-white">
|
| 43 |
+
<div class="card-body text-center">
|
| 44 |
+
<h3>{{ pending_sms }}</h3>
|
| 45 |
+
<p class="mb-0">Pending</p>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
</div>
|
| 49 |
+
<div class="col-md-3">
|
| 50 |
+
<div class="card bg-danger text-white">
|
| 51 |
+
<div class="card-body text-center">
|
| 52 |
+
<h3>{{ failed_sms }}</h3>
|
| 53 |
+
<p class="mb-0">Failed</p>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
<!-- SMS Logs Table -->
|
| 60 |
+
<div class="row">
|
| 61 |
+
<div class="col-12">
|
| 62 |
+
<div class="card shadow">
|
| 63 |
+
<div class="card-header bg-success text-white">
|
| 64 |
+
<h5 class="mb-0"><i class="fas fa-list me-2"></i>SMS History</h5>
|
| 65 |
+
</div>
|
| 66 |
+
<div class="card-body">
|
| 67 |
+
<!-- Filter Controls -->
|
| 68 |
+
<div class="row mb-3">
|
| 69 |
+
<div class="col-md-3">
|
| 70 |
+
<select class="form-select" id="statusFilter">
|
| 71 |
+
<option value="">All Status</option>
|
| 72 |
+
<option value="sent">Sent</option>
|
| 73 |
+
<option value="delivered">Delivered</option>
|
| 74 |
+
<option value="failed">Failed</option>
|
| 75 |
+
<option value="pending">Pending</option>
|
| 76 |
+
</select>
|
| 77 |
+
</div>
|
| 78 |
+
<div class="col-md-3">
|
| 79 |
+
<input type="date" class="form-control" id="dateFilter" placeholder="Filter by date">
|
| 80 |
+
</div>
|
| 81 |
+
<div class="col-md-4">
|
| 82 |
+
<input type="text" class="form-control" id="phoneFilter" placeholder="Search by phone number">
|
| 83 |
+
</div>
|
| 84 |
+
<div class="col-md-2">
|
| 85 |
+
<button class="btn btn-primary" onclick="applyFilters()">
|
| 86 |
+
<i class="fas fa-filter me-2"></i>Filter
|
| 87 |
+
</button>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
|
| 91 |
+
<div class="table-responsive">
|
| 92 |
+
<table class="table table-striped" id="smsLogsTable">
|
| 93 |
+
<thead>
|
| 94 |
+
<tr>
|
| 95 |
+
<th>ID</th>
|
| 96 |
+
<th>Farmer</th>
|
| 97 |
+
<th>Phone</th>
|
| 98 |
+
<th>Message Type</th>
|
| 99 |
+
<th>Content</th>
|
| 100 |
+
<th>Status</th>
|
| 101 |
+
<th>Sent At</th>
|
| 102 |
+
<th>Delivered At</th>
|
| 103 |
+
<th>Actions</th>
|
| 104 |
+
</tr>
|
| 105 |
+
</thead>
|
| 106 |
+
<tbody>
|
| 107 |
+
{% for sms in sms_logs %}
|
| 108 |
+
<tr>
|
| 109 |
+
<td>{{ sms.id }}</td>
|
| 110 |
+
<td>{{ sms.farmer.name if sms.farmer else 'N/A' }}</td>
|
| 111 |
+
<td>{{ sms.phone_number }}</td>
|
| 112 |
+
<td>
|
| 113 |
+
<span class="badge bg-info">{{ sms.message_type }}</span>
|
| 114 |
+
</td>
|
| 115 |
+
<td>
|
| 116 |
+
<div class="text-truncate" style="max-width: 200px;" title="{{ sms.message_content }}">
|
| 117 |
+
{{ sms.message_content }}
|
| 118 |
+
</div>
|
| 119 |
+
</td>
|
| 120 |
+
<td>
|
| 121 |
+
{% if sms.delivery_status == 'delivered' %}
|
| 122 |
+
<span class="badge bg-success">Delivered</span>
|
| 123 |
+
{% elif sms.delivery_status == 'failed' %}
|
| 124 |
+
<span class="badge bg-danger">Failed</span>
|
| 125 |
+
{% elif sms.delivery_status == 'pending' %}
|
| 126 |
+
<span class="badge bg-warning">Pending</span>
|
| 127 |
+
{% else %}
|
| 128 |
+
<span class="badge bg-primary">Sent</span>
|
| 129 |
+
{% endif %}
|
| 130 |
+
</td>
|
| 131 |
+
<td>{{ sms.sent_at.strftime('%Y-%m-%d %H:%M') if sms.sent_at else 'N/A' }}</td>
|
| 132 |
+
<td>{{ sms.delivered_at.strftime('%Y-%m-%d %H:%M') if sms.delivered_at else 'N/A' }}</td>
|
| 133 |
+
<td>
|
| 134 |
+
<div class="btn-group" role="group">
|
| 135 |
+
<button class="btn btn-sm btn-primary" onclick="viewSMS({{ sms.id }})">
|
| 136 |
+
<i class="fas fa-eye"></i>
|
| 137 |
+
</button>
|
| 138 |
+
{% if sms.delivery_status in ['failed', 'pending'] %}
|
| 139 |
+
<button class="btn btn-sm btn-warning" onclick="retrySMS({{ sms.id }})">
|
| 140 |
+
<i class="fas fa-redo"></i>
|
| 141 |
+
</button>
|
| 142 |
+
{% endif %}
|
| 143 |
+
</div>
|
| 144 |
+
</td>
|
| 145 |
+
</tr>
|
| 146 |
+
{% endfor %}
|
| 147 |
+
</tbody>
|
| 148 |
+
</table>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
+
|
| 156 |
+
<!-- View SMS Modal -->
|
| 157 |
+
<div class="modal fade" id="viewSMSModal" tabindex="-1">
|
| 158 |
+
<div class="modal-dialog modal-lg">
|
| 159 |
+
<div class="modal-content">
|
| 160 |
+
<div class="modal-header">
|
| 161 |
+
<h5 class="modal-title"><i class="fas fa-sms me-2"></i>SMS Details</h5>
|
| 162 |
+
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
| 163 |
+
</div>
|
| 164 |
+
<div class="modal-body" id="smsDetailsContent">
|
| 165 |
+
<!-- SMS details will be loaded here -->
|
| 166 |
+
</div>
|
| 167 |
+
<div class="modal-footer">
|
| 168 |
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
|
| 174 |
+
<!-- Send Test SMS Modal -->
|
| 175 |
+
<div class="modal fade" id="testSMSModal" tabindex="-1">
|
| 176 |
+
<div class="modal-dialog">
|
| 177 |
+
<div class="modal-content">
|
| 178 |
+
<div class="modal-header">
|
| 179 |
+
<h5 class="modal-title"><i class="fas fa-paper-plane me-2"></i>Send Test SMS</h5>
|
| 180 |
+
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
| 181 |
+
</div>
|
| 182 |
+
<form id="testSMSForm">
|
| 183 |
+
<div class="modal-body">
|
| 184 |
+
<div class="mb-3">
|
| 185 |
+
<label for="testPhone" class="form-label">Phone Number *</label>
|
| 186 |
+
<input type="tel" class="form-control" id="testPhone" required>
|
| 187 |
+
</div>
|
| 188 |
+
<div class="mb-3">
|
| 189 |
+
<label for="testMessage" class="form-label">Message *</label>
|
| 190 |
+
<textarea class="form-control" id="testMessage" rows="4" maxlength="160" required placeholder="Enter your test message here..."></textarea>
|
| 191 |
+
<div class="form-text">
|
| 192 |
+
<span id="charCount">0</span>/160 characters
|
| 193 |
+
</div>
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
<div class="modal-footer">
|
| 197 |
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
| 198 |
+
<button type="submit" class="btn btn-success">
|
| 199 |
+
<i class="fas fa-paper-plane me-2"></i>Send SMS
|
| 200 |
+
</button>
|
| 201 |
+
</div>
|
| 202 |
+
</form>
|
| 203 |
+
</div>
|
| 204 |
+
</div>
|
| 205 |
+
</div>
|
| 206 |
+
|
| 207 |
+
<script>
|
| 208 |
+
// Initialize DataTable
|
| 209 |
+
$(document).ready(function() {
|
| 210 |
+
$('#smsLogsTable').DataTable({
|
| 211 |
+
order: [[0, 'desc']],
|
| 212 |
+
pageLength: 25,
|
| 213 |
+
responsive: true
|
| 214 |
+
});
|
| 215 |
+
|
| 216 |
+
// Character counter for test message
|
| 217 |
+
$('#testMessage').on('input', function() {
|
| 218 |
+
const length = $(this).val().length;
|
| 219 |
+
$('#charCount').text(length);
|
| 220 |
+
if (length > 160) {
|
| 221 |
+
$('#charCount').addClass('text-danger');
|
| 222 |
+
} else {
|
| 223 |
+
$('#charCount').removeClass('text-danger');
|
| 224 |
+
}
|
| 225 |
+
});
|
| 226 |
+
});
|
| 227 |
+
|
| 228 |
+
// Apply filters
|
| 229 |
+
function applyFilters() {
|
| 230 |
+
const status = $('#statusFilter').val();
|
| 231 |
+
const date = $('#dateFilter').val();
|
| 232 |
+
const phone = $('#phoneFilter').val();
|
| 233 |
+
|
| 234 |
+
const params = new URLSearchParams();
|
| 235 |
+
if (status) params.append('status', status);
|
| 236 |
+
if (date) params.append('date', date);
|
| 237 |
+
if (phone) params.append('phone', phone);
|
| 238 |
+
|
| 239 |
+
window.location.href = `{{ url_for('admin_sms_logs') }}?${params.toString()}`;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
// View SMS Details
|
| 243 |
+
function viewSMS(smsId) {
|
| 244 |
+
$.ajax({
|
| 245 |
+
url: `/admin/sms/${smsId}`,
|
| 246 |
+
method: 'GET',
|
| 247 |
+
success: function(sms) {
|
| 248 |
+
const content = `
|
| 249 |
+
<div class="row">
|
| 250 |
+
<div class="col-md-6">
|
| 251 |
+
<h6>Message Information</h6>
|
| 252 |
+
<p><strong>ID:</strong> ${sms.id}</p>
|
| 253 |
+
<p><strong>Type:</strong> ${sms.message_type}</p>
|
| 254 |
+
<p><strong>Phone:</strong> ${sms.phone_number}</p>
|
| 255 |
+
<p><strong>Farmer:</strong> ${sms.farmer ? sms.farmer.name : 'N/A'}</p>
|
| 256 |
+
</div>
|
| 257 |
+
<div class="col-md-6">
|
| 258 |
+
<h6>Delivery Status</h6>
|
| 259 |
+
<p><strong>Status:</strong>
|
| 260 |
+
<span class="badge bg-${sms.delivery_status === 'delivered' ? 'success' :
|
| 261 |
+
sms.delivery_status === 'failed' ? 'danger' :
|
| 262 |
+
sms.delivery_status === 'pending' ? 'warning' : 'primary'}">
|
| 263 |
+
${sms.delivery_status}
|
| 264 |
+
</span>
|
| 265 |
+
</p>
|
| 266 |
+
<p><strong>Sent At:</strong> ${sms.sent_at ? new Date(sms.sent_at).toLocaleString() : 'N/A'}</p>
|
| 267 |
+
<p><strong>Delivered At:</strong> ${sms.delivered_at ? new Date(sms.delivered_at).toLocaleString() : 'N/A'}</p>
|
| 268 |
+
<p><strong>Error Message:</strong> ${sms.error_message || 'None'}</p>
|
| 269 |
+
</div>
|
| 270 |
+
</div>
|
| 271 |
+
<hr>
|
| 272 |
+
<h6>Message Content</h6>
|
| 273 |
+
<div class="bg-light p-3 rounded">
|
| 274 |
+
${sms.message_content}
|
| 275 |
+
</div>
|
| 276 |
+
`;
|
| 277 |
+
$('#smsDetailsContent').html(content);
|
| 278 |
+
$('#viewSMSModal').modal('show');
|
| 279 |
+
},
|
| 280 |
+
error: function() {
|
| 281 |
+
showToast('Error loading SMS details', 'error');
|
| 282 |
+
}
|
| 283 |
+
});
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
// Retry SMS
|
| 287 |
+
function retrySMS(smsId) {
|
| 288 |
+
if (confirm('Are you sure you want to retry sending this SMS?')) {
|
| 289 |
+
$.ajax({
|
| 290 |
+
url: `/admin/sms/${smsId}/retry`,
|
| 291 |
+
method: 'POST',
|
| 292 |
+
success: function(response) {
|
| 293 |
+
if (response.success) {
|
| 294 |
+
showToast('SMS retry initiated successfully!', 'success');
|
| 295 |
+
location.reload();
|
| 296 |
+
} else {
|
| 297 |
+
showToast(response.message || 'Error retrying SMS', 'error');
|
| 298 |
+
}
|
| 299 |
+
},
|
| 300 |
+
error: function() {
|
| 301 |
+
showToast('Error retrying SMS', 'error');
|
| 302 |
+
}
|
| 303 |
+
});
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
// Send Test SMS
|
| 308 |
+
function sendTestSMS() {
|
| 309 |
+
$('#testSMSModal').modal('show');
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
// Test SMS Form
|
| 313 |
+
$('#testSMSForm').on('submit', function(e) {
|
| 314 |
+
e.preventDefault();
|
| 315 |
+
|
| 316 |
+
const formData = {
|
| 317 |
+
phone_number: $('#testPhone').val(),
|
| 318 |
+
message: $('#testMessage').val()
|
| 319 |
+
};
|
| 320 |
+
|
| 321 |
+
$.ajax({
|
| 322 |
+
url: '/admin/send_test_sms',
|
| 323 |
+
method: 'POST',
|
| 324 |
+
contentType: 'application/json',
|
| 325 |
+
data: JSON.stringify(formData),
|
| 326 |
+
success: function(response) {
|
| 327 |
+
if (response.success) {
|
| 328 |
+
showToast('Test SMS sent successfully!', 'success');
|
| 329 |
+
$('#testSMSModal').modal('hide');
|
| 330 |
+
$('#testSMSForm')[0].reset();
|
| 331 |
+
$('#charCount').text('0');
|
| 332 |
+
setTimeout(() => location.reload(), 2000);
|
| 333 |
+
} else {
|
| 334 |
+
showToast(response.message || 'Error sending test SMS', 'error');
|
| 335 |
+
}
|
| 336 |
+
},
|
| 337 |
+
error: function() {
|
| 338 |
+
showToast('Error sending test SMS', 'error');
|
| 339 |
+
}
|
| 340 |
+
});
|
| 341 |
+
});
|
| 342 |
+
|
| 343 |
+
// Toast notification function
|
| 344 |
+
function showToast(message, type) {
|
| 345 |
+
const toast = document.createElement('div');
|
| 346 |
+
toast.className = `alert alert-${type === 'success' ? 'success' : type === 'error' ? 'danger' : 'info'} alert-dismissible fade show position-fixed`;
|
| 347 |
+
toast.style.top = '20px';
|
| 348 |
+
toast.style.right = '20px';
|
| 349 |
+
toast.style.zIndex = '9999';
|
| 350 |
+
toast.innerHTML = `
|
| 351 |
+
${message}
|
| 352 |
+
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
| 353 |
+
`;
|
| 354 |
+
document.body.appendChild(toast);
|
| 355 |
+
|
| 356 |
+
setTimeout(() => {
|
| 357 |
+
toast.remove();
|
| 358 |
+
}, 5000);
|
| 359 |
+
}
|
| 360 |
+
</script>
|
| 361 |
+
{% endblock %}
|
templates/base.html
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>{% block title %}Farm Management Portal{% endblock %}</title>
|
| 7 |
+
|
| 8 |
+
<!-- Bootstrap CSS -->
|
| 9 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 10 |
+
<!-- Font Awesome -->
|
| 11 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
| 12 |
+
|
| 13 |
+
<style>
|
| 14 |
+
.navbar-brand {
|
| 15 |
+
font-weight: bold;
|
| 16 |
+
}
|
| 17 |
+
.card {
|
| 18 |
+
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
| 19 |
+
border: 1px solid rgba(0, 0, 0, 0.125);
|
| 20 |
+
}
|
| 21 |
+
.btn-primary {
|
| 22 |
+
background-color: #2d5f3f;
|
| 23 |
+
border-color: #2d5f3f;
|
| 24 |
+
}
|
| 25 |
+
.btn-primary:hover {
|
| 26 |
+
background-color: #1e3f2a;
|
| 27 |
+
border-color: #1e3f2a;
|
| 28 |
+
}
|
| 29 |
+
.bg-success {
|
| 30 |
+
background-color: #2d5f3f !important;
|
| 31 |
+
}
|
| 32 |
+
.text-success {
|
| 33 |
+
color: #2d5f3f !important;
|
| 34 |
+
}
|
| 35 |
+
.alert-dismissible .btn-close {
|
| 36 |
+
padding: 0.75rem 1.25rem;
|
| 37 |
+
}
|
| 38 |
+
.footer {
|
| 39 |
+
background-color: #2d5f3f;
|
| 40 |
+
color: white;
|
| 41 |
+
padding: 20px 0;
|
| 42 |
+
margin-top: 50px;
|
| 43 |
+
}
|
| 44 |
+
</style>
|
| 45 |
+
|
| 46 |
+
{% block extra_css %}{% endblock %}
|
| 47 |
+
</head>
|
| 48 |
+
<body class="bg-light">
|
| 49 |
+
<!-- Navigation -->
|
| 50 |
+
<nav class="navbar navbar-expand-lg navbar-dark bg-success">
|
| 51 |
+
<div class="container">
|
| 52 |
+
<a class="navbar-brand" href="{{ url_for('index') }}">
|
| 53 |
+
<i class="fas fa-seedling me-2"></i>Farm Management Portal
|
| 54 |
+
</a>
|
| 55 |
+
|
| 56 |
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
| 57 |
+
<span class="navbar-toggler-icon"></span>
|
| 58 |
+
</button>
|
| 59 |
+
|
| 60 |
+
<div class="collapse navbar-collapse" id="navbarNav">
|
| 61 |
+
<ul class="navbar-nav me-auto">
|
| 62 |
+
<li class="nav-item">
|
| 63 |
+
<a class="nav-link" href="{{ url_for('index') }}">
|
| 64 |
+
<i class="fas fa-home me-1"></i>Home
|
| 65 |
+
</a>
|
| 66 |
+
</li>
|
| 67 |
+
|
| 68 |
+
{% if current_user.is_authenticated %}
|
| 69 |
+
<li class="nav-item">
|
| 70 |
+
<a class="nav-link" href="{{ url_for('farmer_dashboard') }}">
|
| 71 |
+
<i class="fas fa-dashboard me-1"></i>Dashboard
|
| 72 |
+
</a>
|
| 73 |
+
</li>
|
| 74 |
+
{% endif %}
|
| 75 |
+
</ul>
|
| 76 |
+
|
| 77 |
+
<ul class="navbar-nav">
|
| 78 |
+
{% if current_user.is_authenticated %}
|
| 79 |
+
<li class="nav-item dropdown">
|
| 80 |
+
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
|
| 81 |
+
<i class="fas fa-user me-1"></i>{{ current_user.farmer.name }}
|
| 82 |
+
</a>
|
| 83 |
+
<ul class="dropdown-menu">
|
| 84 |
+
<li><a class="dropdown-item" href="{{ url_for('farmer_dashboard') }}">
|
| 85 |
+
<i class="fas fa-dashboard me-2"></i>Dashboard
|
| 86 |
+
</a></li>
|
| 87 |
+
<li><hr class="dropdown-divider"></li>
|
| 88 |
+
<li><a class="dropdown-item" href="{{ url_for('farmer_logout') }}">
|
| 89 |
+
<i class="fas fa-sign-out-alt me-2"></i>Logout
|
| 90 |
+
</a></li>
|
| 91 |
+
</ul>
|
| 92 |
+
</li>
|
| 93 |
+
{% else %}
|
| 94 |
+
<li class="nav-item">
|
| 95 |
+
<a class="nav-link" href="{{ url_for('farmer_login') }}">
|
| 96 |
+
<i class="fas fa-sign-in-alt me-1"></i>Login
|
| 97 |
+
</a>
|
| 98 |
+
</li>
|
| 99 |
+
<li class="nav-item">
|
| 100 |
+
<a class="nav-link" href="{{ url_for('farmer_register') }}">
|
| 101 |
+
<i class="fas fa-user-plus me-1"></i>Register
|
| 102 |
+
</a>
|
| 103 |
+
</li>
|
| 104 |
+
{% endif %}
|
| 105 |
+
|
| 106 |
+
<li class="nav-item">
|
| 107 |
+
<a class="nav-link" href="{{ url_for('admin_login') }}">
|
| 108 |
+
<i class="fas fa-cog me-1"></i>Admin
|
| 109 |
+
</a>
|
| 110 |
+
</li>
|
| 111 |
+
</ul>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
</nav>
|
| 115 |
+
|
| 116 |
+
<!-- Flash Messages -->
|
| 117 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 118 |
+
{% if messages %}
|
| 119 |
+
<div class="container mt-3">
|
| 120 |
+
{% for category, message in messages %}
|
| 121 |
+
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' if category == 'success' else 'info' }} alert-dismissible fade show" role="alert">
|
| 122 |
+
<i class="fas fa-{{ 'exclamation-triangle' if category == 'error' else 'check-circle' if category == 'success' else 'info-circle' }} me-2"></i>
|
| 123 |
+
{{ message }}
|
| 124 |
+
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
| 125 |
+
</div>
|
| 126 |
+
{% endfor %}
|
| 127 |
+
</div>
|
| 128 |
+
{% endif %}
|
| 129 |
+
{% endwith %}
|
| 130 |
+
|
| 131 |
+
<!-- Main Content -->
|
| 132 |
+
<main>
|
| 133 |
+
{% block content %}{% endblock %}
|
| 134 |
+
</main>
|
| 135 |
+
|
| 136 |
+
<!-- Footer -->
|
| 137 |
+
<footer class="footer mt-auto">
|
| 138 |
+
<div class="container">
|
| 139 |
+
<div class="row">
|
| 140 |
+
<div class="col-md-6">
|
| 141 |
+
<h6><i class="fas fa-seedling me-2"></i>Farm Management Portal</h6>
|
| 142 |
+
<p class="mb-0">AI-powered farming solutions with daily recommendations and SMS alerts.</p>
|
| 143 |
+
</div>
|
| 144 |
+
<div class="col-md-6 text-md-end">
|
| 145 |
+
<p class="mb-0">© 2025 Farm Management Portal. All rights reserved.</p>
|
| 146 |
+
<small>Powered by Gemini AI & Twilio SMS</small>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
</footer>
|
| 151 |
+
|
| 152 |
+
<!-- Bootstrap JS -->
|
| 153 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
| 154 |
+
|
| 155 |
+
{% block extra_js %}{% endblock %}
|
| 156 |
+
</body>
|
| 157 |
+
</html>
|
templates/edit_farm.html
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Edit Farm</title>
|
| 7 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
#map {
|
| 10 |
+
height: 50vh;
|
| 11 |
+
width: 100%;
|
| 12 |
+
border-radius: 8px;
|
| 13 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 14 |
+
margin-bottom: 20px;
|
| 15 |
+
}
|
| 16 |
+
.form-section {
|
| 17 |
+
background-color: #f8f9fa;
|
| 18 |
+
padding: 20px;
|
| 19 |
+
border-radius: 8px;
|
| 20 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 21 |
+
margin-bottom: 20px;
|
| 22 |
+
}
|
| 23 |
+
</style>
|
| 24 |
+
</head>
|
| 25 |
+
<body class="bg-light">
|
| 26 |
+
<div class="container py-5">
|
| 27 |
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
| 28 |
+
<h1>Edit Farm</h1>
|
| 29 |
+
<div>
|
| 30 |
+
<a href="/farm_details/{{ farm.id }}" class="btn btn-secondary">Back to Farm Details</a>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<form id="editFarmForm" action="/edit_farm/{{ farm.id }}" method="POST">
|
| 35 |
+
<div class="row">
|
| 36 |
+
<!-- Farmer Details Section -->
|
| 37 |
+
<div class="col-lg-6">
|
| 38 |
+
<div class="form-section">
|
| 39 |
+
<h3 class="mb-3">Farmer Details</h3>
|
| 40 |
+
|
| 41 |
+
<div class="mb-3">
|
| 42 |
+
<label for="farmer_name" class="form-label">Farmer Name *</label>
|
| 43 |
+
<input type="text" class="form-control" id="farmer_name" name="farmer_name" value="{{ farm.farmer_name }}" required>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<div class="mb-3">
|
| 47 |
+
<label for="contact" class="form-label">Contact Number *</label>
|
| 48 |
+
<input type="text" class="form-control" id="contact" name="contact" value="{{ farm.contact }}" required>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<div class="mb-3">
|
| 52 |
+
<label for="address" class="form-label">Address *</label>
|
| 53 |
+
<textarea class="form-control" id="address" name="address" rows="3" required>{{ farm.address }}</textarea>
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
<div class="mb-3">
|
| 57 |
+
<label for="crop_type" class="form-label">Main Crop Type (For compatibility)</label>
|
| 58 |
+
<select class="form-control" id="crop_type" name="crop_type">
|
| 59 |
+
<option value="">Select Crop Type</option>
|
| 60 |
+
<option value="Rice" {% if farm.crop_type == 'Rice' %}selected{% endif %}>Rice</option>
|
| 61 |
+
<option value="Wheat" {% if farm.crop_type == 'Wheat' %}selected{% endif %}>Wheat</option>
|
| 62 |
+
<option value="Corn" {% if farm.crop_type == 'Corn' %}selected{% endif %}>Corn</option>
|
| 63 |
+
<option value="Cotton" {% if farm.crop_type == 'Cotton' %}selected{% endif %}>Cotton</option>
|
| 64 |
+
<option value="Sugarcane" {% if farm.crop_type == 'Sugarcane' %}selected{% endif %}>Sugarcane</option>
|
| 65 |
+
<option value="Vegetables" {% if farm.crop_type == 'Vegetables' %}selected{% endif %}>Vegetables</option>
|
| 66 |
+
<option value="Fruits" {% if farm.crop_type == 'Fruits' %}selected{% endif %}>Fruits</option>
|
| 67 |
+
<option value="Other" {% if farm.crop_type == 'Other' %}selected{% endif %}>Other</option>
|
| 68 |
+
</select>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
<div class="mb-3">
|
| 72 |
+
<h4>Crop Details</h4>
|
| 73 |
+
<div class="table-responsive">
|
| 74 |
+
<table class="table table-bordered" id="crops-table">
|
| 75 |
+
<thead>
|
| 76 |
+
<tr>
|
| 77 |
+
<th>Crop Name</th>
|
| 78 |
+
<th>Area (acres)</th>
|
| 79 |
+
<th>Sowing Month</th>
|
| 80 |
+
<th>Actions</th>
|
| 81 |
+
</tr>
|
| 82 |
+
</thead>
|
| 83 |
+
<tbody id="crops-tbody">
|
| 84 |
+
{% if crops_data and crops_data|length > 0 %}
|
| 85 |
+
{% for crop in crops_data %}
|
| 86 |
+
<tr>
|
| 87 |
+
<td>
|
| 88 |
+
<input type="text" class="form-control" name="crop_name[]" value="{{ crop.name }}" required>
|
| 89 |
+
</td>
|
| 90 |
+
<td>
|
| 91 |
+
<input type="number" class="form-control" name="crop_area[]" step="0.01" min="0" value="{{ crop.area }}" required>
|
| 92 |
+
</td>
|
| 93 |
+
<td>
|
| 94 |
+
<select class="form-control" name="crop_month[]" required>
|
| 95 |
+
<option value="">Select Month</option>
|
| 96 |
+
<option value="January" {% if crop.sowing_month == 'January' %}selected{% endif %}>January</option>
|
| 97 |
+
<option value="February" {% if crop.sowing_month == 'February' %}selected{% endif %}>February</option>
|
| 98 |
+
<option value="March" {% if crop.sowing_month == 'March' %}selected{% endif %}>March</option>
|
| 99 |
+
<option value="April" {% if crop.sowing_month == 'April' %}selected{% endif %}>April</option>
|
| 100 |
+
<option value="May" {% if crop.sowing_month == 'May' %}selected{% endif %}>May</option>
|
| 101 |
+
<option value="June" {% if crop.sowing_month == 'June' %}selected{% endif %}>June</option>
|
| 102 |
+
<option value="July" {% if crop.sowing_month == 'July' %}selected{% endif %}>July</option>
|
| 103 |
+
<option value="August" {% if crop.sowing_month == 'August' %}selected{% endif %}>August</option>
|
| 104 |
+
<option value="September" {% if crop.sowing_month == 'September' %}selected{% endif %}>September</option>
|
| 105 |
+
<option value="October" {% if crop.sowing_month == 'October' %}selected{% endif %}>October</option>
|
| 106 |
+
<option value="November" {% if crop.sowing_month == 'November' %}selected{% endif %}>November</option>
|
| 107 |
+
<option value="December" {% if crop.sowing_month == 'December' %}selected{% endif %}>December</option>
|
| 108 |
+
</select>
|
| 109 |
+
</td>
|
| 110 |
+
<td>
|
| 111 |
+
<button type="button" class="btn btn-danger btn-sm remove-crop">Remove</button>
|
| 112 |
+
</td>
|
| 113 |
+
</tr>
|
| 114 |
+
{% endfor %}
|
| 115 |
+
{% else %}
|
| 116 |
+
<tr>
|
| 117 |
+
<td>
|
| 118 |
+
<input type="text" class="form-control" name="crop_name[]" required>
|
| 119 |
+
</td>
|
| 120 |
+
<td>
|
| 121 |
+
<input type="number" class="form-control" name="crop_area[]" step="0.01" min="0" required>
|
| 122 |
+
</td>
|
| 123 |
+
<td>
|
| 124 |
+
<select class="form-control" name="crop_month[]" required>
|
| 125 |
+
<option value="">Select Month</option>
|
| 126 |
+
<option value="January">January</option>
|
| 127 |
+
<option value="February">February</option>
|
| 128 |
+
<option value="March">March</option>
|
| 129 |
+
<option value="April">April</option>
|
| 130 |
+
<option value="May">May</option>
|
| 131 |
+
<option value="June">June</option>
|
| 132 |
+
<option value="July">July</option>
|
| 133 |
+
<option value="August">August</option>
|
| 134 |
+
<option value="September">September</option>
|
| 135 |
+
<option value="October">October</option>
|
| 136 |
+
<option value="November">November</option>
|
| 137 |
+
<option value="December">December</option>
|
| 138 |
+
</select>
|
| 139 |
+
</td>
|
| 140 |
+
<td>
|
| 141 |
+
<button type="button" class="btn btn-danger btn-sm remove-crop">Remove</button>
|
| 142 |
+
</td>
|
| 143 |
+
</tr>
|
| 144 |
+
{% endif %}
|
| 145 |
+
</tbody>
|
| 146 |
+
</table>
|
| 147 |
+
<button type="button" id="add-crop" class="btn btn-success">Add Another Crop</button>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
|
| 153 |
+
<!-- Farm Geolocation Section -->
|
| 154 |
+
<div class="col-lg-6">
|
| 155 |
+
<div class="form-section">
|
| 156 |
+
<h3 class="mb-3">Farm Location</h3>
|
| 157 |
+
|
| 158 |
+
<div class="mb-3">
|
| 159 |
+
<div id="map"></div>
|
| 160 |
+
</div>
|
| 161 |
+
|
| 162 |
+
<div class="row mb-3">
|
| 163 |
+
<div class="col-6">
|
| 164 |
+
<button type="button" id="startDrawingBtn" class="btn btn-success w-100">Edit Drawing</button>
|
| 165 |
+
</div>
|
| 166 |
+
<div class="col-6">
|
| 167 |
+
<button type="button" id="clearDrawingBtn" class="btn btn-danger w-100">Clear Drawing</button>
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
|
| 171 |
+
<div class="alert alert-info" id="drawingStatus">
|
| 172 |
+
Edit the farm boundary on the map if needed.
|
| 173 |
+
</div>
|
| 174 |
+
|
| 175 |
+
<input type="hidden" id="field_coordinates" name="field_coordinates" value="{{ farm.field_coordinates }}">
|
| 176 |
+
<input type="hidden" id="center_lat" name="center_lat" value="{{ farm.center_lat }}">
|
| 177 |
+
<input type="hidden" id="center_lng" name="center_lng" value="{{ farm.center_lng }}">
|
| 178 |
+
|
| 179 |
+
<div class="mb-3">
|
| 180 |
+
<label for="area" class="form-label">Farm Area (acres)</label>
|
| 181 |
+
<input type="number" class="form-control" id="area" name="area" step="0.01" min="0" value="{{ farm.area }}">
|
| 182 |
+
<small class="text-muted">Leave empty to calculate automatically from the drawn boundary.</small>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
<div class="d-grid">
|
| 187 |
+
<button type="submit" id="submitBtn" class="btn btn-primary btn-lg">Save Changes</button>
|
| 188 |
+
</div>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
</form>
|
| 192 |
+
</div>
|
| 193 |
+
|
| 194 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
| 195 |
+
<script>
|
| 196 |
+
let map;
|
| 197 |
+
let polygon = null;
|
| 198 |
+
let drawingManager;
|
| 199 |
+
|
| 200 |
+
function initMap() {
|
| 201 |
+
// Center the map on farm coordinates
|
| 202 |
+
const farmCenter = {
|
| 203 |
+
lat: {{ farm.latitude }},
|
| 204 |
+
lng: {{ farm.longitude }}
|
| 205 |
+
};
|
| 206 |
+
|
| 207 |
+
map = new google.maps.Map(document.getElementById('map'), {
|
| 208 |
+
zoom: 16,
|
| 209 |
+
center: farmCenter,
|
| 210 |
+
mapTypeId: 'satellite',
|
| 211 |
+
mapTypeControl: true,
|
| 212 |
+
streetViewControl: false,
|
| 213 |
+
fullscreenControl: true,
|
| 214 |
+
});
|
| 215 |
+
|
| 216 |
+
// Initialize drawing tools
|
| 217 |
+
drawingManager = new google.maps.drawing.DrawingManager({
|
| 218 |
+
drawingMode: null,
|
| 219 |
+
drawingControl: false,
|
| 220 |
+
polygonOptions: {
|
| 221 |
+
fillColor: '#4CAF50',
|
| 222 |
+
fillOpacity: 0.4,
|
| 223 |
+
strokeWeight: 2,
|
| 224 |
+
strokeColor: '#4CAF50',
|
| 225 |
+
editable: true
|
| 226 |
+
}
|
| 227 |
+
});
|
| 228 |
+
drawingManager.setMap(map);
|
| 229 |
+
|
| 230 |
+
// Load existing farm boundary if exists
|
| 231 |
+
{% if farm.field_coordinates %}
|
| 232 |
+
try {
|
| 233 |
+
const fieldCoords = JSON.parse('{{ farm.field_coordinates|safe }}');
|
| 234 |
+
const paths = fieldCoords.map(coord => new google.maps.LatLng(coord.lat, coord.lng));
|
| 235 |
+
|
| 236 |
+
polygon = new google.maps.Polygon({
|
| 237 |
+
paths: paths,
|
| 238 |
+
fillColor: '#4CAF50',
|
| 239 |
+
fillOpacity: 0.4,
|
| 240 |
+
strokeWeight: 2,
|
| 241 |
+
strokeColor: '#4CAF50',
|
| 242 |
+
editable: true
|
| 243 |
+
});
|
| 244 |
+
|
| 245 |
+
polygon.setMap(map);
|
| 246 |
+
|
| 247 |
+
// Add listener for polygon changes
|
| 248 |
+
google.maps.event.addListener(polygon.getPath(), 'set_at', updateCoordinatesFields);
|
| 249 |
+
google.maps.event.addListener(polygon.getPath(), 'insert_at', updateCoordinatesFields);
|
| 250 |
+
|
| 251 |
+
// Update status
|
| 252 |
+
document.getElementById('drawingStatus').className = 'alert alert-success';
|
| 253 |
+
document.getElementById('drawingStatus').textContent = 'Farm boundary loaded successfully!';
|
| 254 |
+
} catch (e) {
|
| 255 |
+
console.error('Error parsing farm boundary:', e);
|
| 256 |
+
}
|
| 257 |
+
{% endif %}
|
| 258 |
+
|
| 259 |
+
// Setup event listeners for drawing
|
| 260 |
+
google.maps.event.addListener(drawingManager, 'polygoncomplete', function(poly) {
|
| 261 |
+
// Remove old polygon if exists
|
| 262 |
+
if (polygon !== null) {
|
| 263 |
+
polygon.setMap(null);
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
polygon = poly;
|
| 267 |
+
drawingManager.setDrawingMode(null);
|
| 268 |
+
document.getElementById('startDrawingBtn').textContent = "Edit Drawing";
|
| 269 |
+
|
| 270 |
+
// Update hidden fields with polygon data
|
| 271 |
+
updateCoordinatesFields();
|
| 272 |
+
|
| 273 |
+
// Add listener for polygon changes
|
| 274 |
+
google.maps.event.addListener(polygon.getPath(), 'set_at', updateCoordinatesFields);
|
| 275 |
+
google.maps.event.addListener(polygon.getPath(), 'insert_at', updateCoordinatesFields);
|
| 276 |
+
|
| 277 |
+
// Update status
|
| 278 |
+
document.getElementById('drawingStatus').className = 'alert alert-success';
|
| 279 |
+
document.getElementById('drawingStatus').textContent = 'Farm boundary updated successfully!';
|
| 280 |
+
});
|
| 281 |
+
|
| 282 |
+
// Setup buttons
|
| 283 |
+
document.getElementById('startDrawingBtn').addEventListener('click', function() {
|
| 284 |
+
if (drawingManager.getDrawingMode() == google.maps.drawing.OverlayType.POLYGON) {
|
| 285 |
+
drawingManager.setDrawingMode(null);
|
| 286 |
+
this.textContent = polygon ? "Edit Drawing" : "Start Drawing";
|
| 287 |
+
} else {
|
| 288 |
+
drawingManager.setDrawingMode(google.maps.drawing.OverlayType.POLYGON);
|
| 289 |
+
this.textContent = "Cancel Drawing";
|
| 290 |
+
}
|
| 291 |
+
});
|
| 292 |
+
|
| 293 |
+
document.getElementById('clearDrawingBtn').addEventListener('click', function() {
|
| 294 |
+
if (polygon) {
|
| 295 |
+
polygon.setMap(null);
|
| 296 |
+
polygon = null;
|
| 297 |
+
}
|
| 298 |
+
document.getElementById('startDrawingBtn').textContent = "Start Drawing";
|
| 299 |
+
document.getElementById('drawingStatus').className = 'alert alert-warning';
|
| 300 |
+
document.getElementById('drawingStatus').textContent = 'Please draw your farm boundary on the map.';
|
| 301 |
+
|
| 302 |
+
// Clear hidden fields
|
| 303 |
+
document.getElementById('field_coordinates').value = '';
|
| 304 |
+
document.getElementById('center_lat').value = '';
|
| 305 |
+
document.getElementById('center_lng').value = '';
|
| 306 |
+
});
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
function updateCoordinatesFields() {
|
| 310 |
+
if (!polygon) return;
|
| 311 |
+
|
| 312 |
+
// Get polygon path and convert to array of coordinates
|
| 313 |
+
const path = polygon.getPath();
|
| 314 |
+
const coordinates = [];
|
| 315 |
+
for (let i = 0; i < path.getLength(); i++) {
|
| 316 |
+
const point = path.getAt(i);
|
| 317 |
+
coordinates.push({
|
| 318 |
+
lat: point.lat(),
|
| 319 |
+
lng: point.lng()
|
| 320 |
+
});
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
// Calculate center of polygon
|
| 324 |
+
const bounds = new google.maps.LatLngBounds();
|
| 325 |
+
path.forEach(latlng => bounds.extend(latlng));
|
| 326 |
+
const center = bounds.getCenter();
|
| 327 |
+
|
| 328 |
+
// Update hidden fields
|
| 329 |
+
document.getElementById('field_coordinates').value = JSON.stringify(coordinates);
|
| 330 |
+
document.getElementById('center_lat').value = center.lat();
|
| 331 |
+
document.getElementById('center_lng').value = center.lng();
|
| 332 |
+
|
| 333 |
+
// If area input is empty, compute and populate approximate area (acres)
|
| 334 |
+
try {
|
| 335 |
+
// compute approximate area using same method as add_farm
|
| 336 |
+
if (coordinates.length >= 3) {
|
| 337 |
+
let latSum = 0;
|
| 338 |
+
coordinates.forEach(p => latSum += p.lat);
|
| 339 |
+
const latAvg = latSum / coordinates.length;
|
| 340 |
+
const latAvgRad = latAvg * Math.PI / 180.0;
|
| 341 |
+
|
| 342 |
+
const metersPerDegLat = 111132.92;
|
| 343 |
+
const metersPerDegLon = 111320.0 * Math.cos(latAvgRad);
|
| 344 |
+
|
| 345 |
+
const pts = coordinates.map(p => ({ x: p.lng * metersPerDegLon, y: p.lat * metersPerDegLat }));
|
| 346 |
+
let area = 0;
|
| 347 |
+
for (let i = 0; i < pts.length; i++) {
|
| 348 |
+
const j = (i + 1) % pts.length;
|
| 349 |
+
area += pts[i].x * pts[j].y - pts[j].x * pts[i].y;
|
| 350 |
+
}
|
| 351 |
+
area = Math.abs(area) / 2.0; // m^2
|
| 352 |
+
const acres = Math.round((area / 4046.8564224) * 10000) / 10000;
|
| 353 |
+
|
| 354 |
+
const areaInput = document.getElementById('area');
|
| 355 |
+
if (areaInput && (!areaInput.value || Number(areaInput.value) === 0)) {
|
| 356 |
+
areaInput.value = acres;
|
| 357 |
+
}
|
| 358 |
+
}
|
| 359 |
+
} catch (e) {
|
| 360 |
+
console.error('Error computing area:', e);
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// Crop management
|
| 365 |
+
document.getElementById('add-crop').addEventListener('click', function() {
|
| 366 |
+
const tbody = document.getElementById('crops-tbody');
|
| 367 |
+
const newRow = document.createElement('tr');
|
| 368 |
+
newRow.innerHTML = `
|
| 369 |
+
<td>
|
| 370 |
+
<input type="text" class="form-control" name="crop_name[]" required>
|
| 371 |
+
</td>
|
| 372 |
+
<td>
|
| 373 |
+
<input type="number" class="form-control" name="crop_area[]" step="0.01" min="0" required>
|
| 374 |
+
</td>
|
| 375 |
+
<td>
|
| 376 |
+
<select class="form-control" name="crop_month[]" required>
|
| 377 |
+
<option value="">Select Month</option>
|
| 378 |
+
<option value="January">January</option>
|
| 379 |
+
<option value="February">February</option>
|
| 380 |
+
<option value="March">March</option>
|
| 381 |
+
<option value="April">April</option>
|
| 382 |
+
<option value="May">May</option>
|
| 383 |
+
<option value="June">June</option>
|
| 384 |
+
<option value="July">July</option>
|
| 385 |
+
<option value="August">August</option>
|
| 386 |
+
<option value="September">September</option>
|
| 387 |
+
<option value="October">October</option>
|
| 388 |
+
<option value="November">November</option>
|
| 389 |
+
<option value="December">December</option>
|
| 390 |
+
</select>
|
| 391 |
+
</td>
|
| 392 |
+
<td>
|
| 393 |
+
<button type="button" class="btn btn-danger btn-sm remove-crop">Remove</button>
|
| 394 |
+
</td>
|
| 395 |
+
`;
|
| 396 |
+
tbody.appendChild(newRow);
|
| 397 |
+
|
| 398 |
+
// Add event listener to new remove button
|
| 399 |
+
newRow.querySelector('.remove-crop').addEventListener('click', function() {
|
| 400 |
+
if (tbody.children.length > 1) {
|
| 401 |
+
tbody.removeChild(newRow);
|
| 402 |
+
} else {
|
| 403 |
+
alert('You must have at least one crop entry.');
|
| 404 |
+
}
|
| 405 |
+
});
|
| 406 |
+
});
|
| 407 |
+
|
| 408 |
+
// Add event listeners to existing remove buttons
|
| 409 |
+
document.querySelectorAll('.remove-crop').forEach(button => {
|
| 410 |
+
button.addEventListener('click', function() {
|
| 411 |
+
const tbody = document.getElementById('crops-tbody');
|
| 412 |
+
if (tbody.children.length > 1) {
|
| 413 |
+
tbody.removeChild(this.closest('tr'));
|
| 414 |
+
} else {
|
| 415 |
+
alert('You must have at least one crop entry.');
|
| 416 |
+
}
|
| 417 |
+
});
|
| 418 |
+
});
|
| 419 |
+
</script>
|
| 420 |
+
<script async defer src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBvVLjWmCja331H8SuIZ4UlJdZytuYkC6Y&libraries=drawing&callback=initMap"></script>
|
| 421 |
+
</body>
|
| 422 |
+
</html>
|
templates/error.html
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Error {{ error_code }} - Farm Management Portal{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="container py-5">
|
| 7 |
+
<div class="row justify-content-center">
|
| 8 |
+
<div class="col-md-6 text-center">
|
| 9 |
+
<div class="card shadow">
|
| 10 |
+
<div class="card-body py-5">
|
| 11 |
+
<div class="mb-4">
|
| 12 |
+
<i class="fas fa-exclamation-triangle fa-5x text-warning"></i>
|
| 13 |
+
</div>
|
| 14 |
+
<h1 class="display-1 text-muted">{{ error_code }}</h1>
|
| 15 |
+
<h3 class="mb-3">{{ error_message }}</h3>
|
| 16 |
+
|
| 17 |
+
{% if error_code == 404 %}
|
| 18 |
+
<p class="text-muted mb-4">
|
| 19 |
+
The page you're looking for doesn't exist or has been moved.
|
| 20 |
+
</p>
|
| 21 |
+
{% elif error_code == 500 %}
|
| 22 |
+
<p class="text-muted mb-4">
|
| 23 |
+
Something went wrong on our end. We're working to fix it.
|
| 24 |
+
</p>
|
| 25 |
+
{% else %}
|
| 26 |
+
<p class="text-muted mb-4">
|
| 27 |
+
An error occurred while processing your request.
|
| 28 |
+
</p>
|
| 29 |
+
{% endif %}
|
| 30 |
+
|
| 31 |
+
<div class="d-flex justify-content-center gap-3">
|
| 32 |
+
<a href="{{ url_for('index') }}" class="btn btn-success">
|
| 33 |
+
<i class="fas fa-home me-2"></i>Go Home
|
| 34 |
+
</a>
|
| 35 |
+
<button onclick="history.back()" class="btn btn-outline-secondary">
|
| 36 |
+
<i class="fas fa-arrow-left me-2"></i>Go Back
|
| 37 |
+
</button>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
{% endblock %}
|
templates/farm_details.html
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Farm Details</title>
|
| 7 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
#map {
|
| 10 |
+
height: 400px;
|
| 11 |
+
width: 100%;
|
| 12 |
+
border-radius: 8px;
|
| 13 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 14 |
+
margin-bottom: 20px;
|
| 15 |
+
}
|
| 16 |
+
.card {
|
| 17 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 18 |
+
margin-bottom: 20px;
|
| 19 |
+
}
|
| 20 |
+
.weather-icon {
|
| 21 |
+
width: 64px;
|
| 22 |
+
height: 64px;
|
| 23 |
+
}
|
| 24 |
+
</style>
|
| 25 |
+
</head>
|
| 26 |
+
<body class="bg-light">
|
| 27 |
+
<div class="container py-5">
|
| 28 |
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
| 29 |
+
<h1>Farm Details</h1>
|
| 30 |
+
<div>
|
| 31 |
+
<a href="/farms" class="btn btn-secondary me-2">Back to Farms</a>
|
| 32 |
+
<a href="/edit_farm/{{ farm.id }}" class="btn btn-primary">Edit Farm</a>
|
| 33 |
+
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#deleteFarmModal">
|
| 34 |
+
Delete Farm
|
| 35 |
+
</button>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
|
| 39 |
+
<div class="row">
|
| 40 |
+
<!-- Farm Information -->
|
| 41 |
+
<div class="col-lg-6">
|
| 42 |
+
<div class="card">
|
| 43 |
+
<div class="card-header bg-primary text-white">
|
| 44 |
+
<h3 class="card-title">Farm Information</h3>
|
| 45 |
+
</div>
|
| 46 |
+
<div class="card-body">
|
| 47 |
+
<table class="table table-striped">
|
| 48 |
+
<tr>
|
| 49 |
+
<th>Farmer Name</th>
|
| 50 |
+
<td>{{ farm.owner.name }}</td>
|
| 51 |
+
</tr>
|
| 52 |
+
<tr>
|
| 53 |
+
<th>Contact</th>
|
| 54 |
+
<td>{{ farm.owner.contact_number }}</td>
|
| 55 |
+
</tr>
|
| 56 |
+
<tr>
|
| 57 |
+
<th>Address</th>
|
| 58 |
+
<td>{{ farm.owner.address }}</td>
|
| 59 |
+
</tr>
|
| 60 |
+
<tr>
|
| 61 |
+
<th>Total Farm Area</th>
|
| 62 |
+
<td>{{ farm.farm_size|round(2) if farm.farm_size else 'Not specified' }} acres</td>
|
| 63 |
+
</tr>
|
| 64 |
+
<tr>
|
| 65 |
+
<th>Registration Date</th>
|
| 66 |
+
<td>{{ farm.created_at.strftime('%d %b %Y') }}</td>
|
| 67 |
+
</tr>
|
| 68 |
+
</table>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
<!-- Crops Information -->
|
| 73 |
+
<div class="card">
|
| 74 |
+
<div class="card-header bg-success text-white">
|
| 75 |
+
<h3 class="card-title">Crops Information</h3>
|
| 76 |
+
</div>
|
| 77 |
+
<div class="card-body">
|
| 78 |
+
{% if crops_data and crops_data|length > 0 %}
|
| 79 |
+
<table class="table table-bordered">
|
| 80 |
+
<thead>
|
| 81 |
+
<tr>
|
| 82 |
+
<th>Crop Name</th>
|
| 83 |
+
<th>Area (acres)</th>
|
| 84 |
+
<th>Sowing Month</th>
|
| 85 |
+
</tr>
|
| 86 |
+
</thead>
|
| 87 |
+
<tbody>
|
| 88 |
+
{% for crop in crops_data %}
|
| 89 |
+
<tr>
|
| 90 |
+
<td>{{ crop.name }}</td>
|
| 91 |
+
<td>{{ crop.area }}</td>
|
| 92 |
+
<td>{{ crop.sowing_month }}</td>
|
| 93 |
+
</tr>
|
| 94 |
+
{% endfor %}
|
| 95 |
+
</tbody>
|
| 96 |
+
</table>
|
| 97 |
+
{% else %}
|
| 98 |
+
<p class="text-muted">No crop details available.</p>
|
| 99 |
+
{% endif %}
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
|
| 104 |
+
<!-- Map and Weather -->
|
| 105 |
+
<div class="col-lg-6">
|
| 106 |
+
<div class="card">
|
| 107 |
+
<div class="card-header bg-info text-white">
|
| 108 |
+
<h3 class="card-title">Farm Location</h3>
|
| 109 |
+
</div>
|
| 110 |
+
<div class="card-body">
|
| 111 |
+
<div id="map"></div>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<div class="card">
|
| 116 |
+
<div class="card-header bg-warning">
|
| 117 |
+
<h3 class="card-title">Current Weather</h3>
|
| 118 |
+
</div>
|
| 119 |
+
<div class="card-body">
|
| 120 |
+
{% if weather and not weather.get('error', False) %}
|
| 121 |
+
<div class="row align-items-center">
|
| 122 |
+
<div class="col-md-6">
|
| 123 |
+
<h4>{{ weather.name }}</h4>
|
| 124 |
+
<div class="d-flex align-items-center">
|
| 125 |
+
<img src="http://openweathermap.org/img/wn/{{ weather.weather[0].icon }}@2x.png" alt="{{ weather.weather[0].description }}" class="weather-icon me-3">
|
| 126 |
+
<div>
|
| 127 |
+
<h2>{{ weather.main.temp|round(1) }}°C</h2>
|
| 128 |
+
<p class="mb-0">{{ weather.weather[0].description|capitalize }}</p>
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
<div class="col-md-6">
|
| 133 |
+
<ul class="list-unstyled">
|
| 134 |
+
<li><strong>Humidity:</strong> {{ weather.main.humidity }}%</li>
|
| 135 |
+
<li><strong>Wind:</strong> {{ weather.wind.speed }} m/s</li>
|
| 136 |
+
{% if weather.main.get('feels_like') %}
|
| 137 |
+
<li><strong>Feels like:</strong> {{ weather.main.feels_like|round(1) }}°C</li>
|
| 138 |
+
{% endif %}
|
| 139 |
+
</ul>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
{% else %}
|
| 143 |
+
<p>Weather data unavailable.</p>
|
| 144 |
+
{% endif %}
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
|
| 150 |
+
<!-- Agriculture News Section -->
|
| 151 |
+
{% if news and news|length > 0 %}
|
| 152 |
+
<div class="card mt-4">
|
| 153 |
+
<div class="card-header bg-secondary text-white">
|
| 154 |
+
<h3 class="card-title">Agriculture News</h3>
|
| 155 |
+
</div>
|
| 156 |
+
<div class="card-body">
|
| 157 |
+
<div class="row">
|
| 158 |
+
{% for article in news %}
|
| 159 |
+
<div class="col-md-4 mb-3">
|
| 160 |
+
<div class="card h-100">
|
| 161 |
+
{% if article.urlToImage %}
|
| 162 |
+
<img src="{{ article.urlToImage }}" class="card-img-top" alt="{{ article.title }}">
|
| 163 |
+
{% endif %}
|
| 164 |
+
<div class="card-body">
|
| 165 |
+
<h5 class="card-title">{{ article.title }}</h5>
|
| 166 |
+
<p class="card-text small">{{ article.description|truncate(100) }}</p>
|
| 167 |
+
</div>
|
| 168 |
+
<div class="card-footer">
|
| 169 |
+
<a href="{{ article.url }}" target="_blank" class="btn btn-sm btn-primary">Read More</a>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
</div>
|
| 173 |
+
{% endfor %}
|
| 174 |
+
</div>
|
| 175 |
+
</div>
|
| 176 |
+
</div>
|
| 177 |
+
{% endif %}
|
| 178 |
+
</div>
|
| 179 |
+
|
| 180 |
+
<!-- Delete Farm Modal -->
|
| 181 |
+
<div class="modal fade" id="deleteFarmModal" tabindex="-1" aria-hidden="true">
|
| 182 |
+
<div class="modal-dialog">
|
| 183 |
+
<div class="modal-content">
|
| 184 |
+
<div class="modal-header bg-danger text-white">
|
| 185 |
+
<h5 class="modal-title">Confirm Deletion</h5>
|
| 186 |
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
| 187 |
+
</div>
|
| 188 |
+
<div class="modal-body">
|
| 189 |
+
<p>Are you sure you want to delete this farm? This action cannot be undone.</p>
|
| 190 |
+
</div>
|
| 191 |
+
<div class="modal-footer">
|
| 192 |
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
| 193 |
+
<form action="/delete_farm/{{ farm.id }}" method="POST">
|
| 194 |
+
<button type="submit" class="btn btn-danger">Delete Farm</button>
|
| 195 |
+
</form>
|
| 196 |
+
</div>
|
| 197 |
+
</div>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
|
| 201 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
| 202 |
+
<script>
|
| 203 |
+
let map;
|
| 204 |
+
let farmBoundary;
|
| 205 |
+
|
| 206 |
+
function initMap() {
|
| 207 |
+
// Center the map on farm coordinates
|
| 208 |
+
const farmCenter = {lat: {{ farm.latitude }}, lng: {{ farm.longitude }}};
|
| 209 |
+
|
| 210 |
+
map = new google.maps.Map(document.getElementById('map'), {
|
| 211 |
+
zoom: 15,
|
| 212 |
+
center: farmCenter,
|
| 213 |
+
mapTypeId: 'satellite'
|
| 214 |
+
});
|
| 215 |
+
|
| 216 |
+
// Add marker for farm center
|
| 217 |
+
const marker = new google.maps.Marker({
|
| 218 |
+
position: farmCenter,
|
| 219 |
+
map: map,
|
| 220 |
+
title: "{{ farm.farm_name }}"
|
| 221 |
+
});
|
| 222 |
+
|
| 223 |
+
// Add farm boundary if coordinates exist
|
| 224 |
+
{% if farm.field_coordinates %}
|
| 225 |
+
try {
|
| 226 |
+
const coordinates = JSON.parse('{{ farm.field_coordinates|safe }}');
|
| 227 |
+
const farmBoundaryPath = coordinates.map(coord => ({lat: coord.lat, lng: coord.lng}));
|
| 228 |
+
|
| 229 |
+
farmBoundary = new google.maps.Polygon({
|
| 230 |
+
paths: farmBoundaryPath,
|
| 231 |
+
strokeColor: '#4CAF50',
|
| 232 |
+
strokeOpacity: 0.8,
|
| 233 |
+
strokeWeight: 3,
|
| 234 |
+
fillColor: '#4CAF50',
|
| 235 |
+
fillOpacity: 0.35
|
| 236 |
+
});
|
| 237 |
+
|
| 238 |
+
farmBoundary.setMap(map);
|
| 239 |
+
|
| 240 |
+
// Adjust bounds to fit the farm boundary
|
| 241 |
+
const bounds = new google.maps.LatLngBounds();
|
| 242 |
+
farmBoundaryPath.forEach(coord => bounds.extend(coord));
|
| 243 |
+
map.fitBounds(bounds);
|
| 244 |
+
} catch (e) {
|
| 245 |
+
console.error("Error parsing farm boundary coordinates:", e);
|
| 246 |
+
}
|
| 247 |
+
{% endif %}
|
| 248 |
+
}
|
| 249 |
+
</script>
|
| 250 |
+
<script async defer src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBvVLjWmCja331H8SuIZ4UlJdZytuYkC6Y&callback=initMap"></script>
|
| 251 |
+
</body>
|
| 252 |
+
</html>
|
templates/farmer_dashboard.html
ADDED
|
@@ -0,0 +1,1348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Farmer Dashboard - Farm Management Portal{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="container mt-4">
|
| 7 |
+
<!-- Welcome Header -->
|
| 8 |
+
<div class="row mb-4">
|
| 9 |
+
<div class="col-12">
|
| 10 |
+
<div class="card bg-success text-white">
|
| 11 |
+
<div class="card-body">
|
| 12 |
+
<h2><i class="fas fa-tachometer-alt me-2"></i>Welcome, {{ farmer.name }}!</h2>
|
| 13 |
+
<p class="mb-0">Manage your farms and get AI-powered daily recommendations</p>
|
| 14 |
+
</div>
|
| 15 |
+
</div>
|
| 16 |
+
</div>
|
| 17 |
+
</div>
|
| 18 |
+
|
| 19 |
+
<!-- External AI Tools Quick Access -->
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
<!-- Quick Stats -->
|
| 23 |
+
<div class="row mb-4">
|
| 24 |
+
<div class="col-md-3 mb-3">
|
| 25 |
+
<div class="card text-center">
|
| 26 |
+
<div class="card-body">
|
| 27 |
+
<i class="fas fa-seedling fa-2x text-success mb-2"></i>
|
| 28 |
+
<h5>{{ farms|length }}</h5>
|
| 29 |
+
<small class="text-muted">Total Farms</small>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
<div class="col-md-3 mb-3">
|
| 34 |
+
<div class="card text-center">
|
| 35 |
+
<div class="card-body">
|
| 36 |
+
<i class="fas fa-calendar-check fa-2x text-primary mb-2"></i>
|
| 37 |
+
<h5>{{ recent_activities|length }}</h5>
|
| 38 |
+
<small class="text-muted">Recent Activities</small>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
<div class="col-md-3 mb-3">
|
| 43 |
+
<div class="card text-center">
|
| 44 |
+
<div class="card-body">
|
| 45 |
+
<i class="fas fa-sms fa-2x text-info mb-2"></i>
|
| 46 |
+
<h5>{% if today_advisory %}Active{% else %}Pending{% endif %}</h5>
|
| 47 |
+
<small class="text-muted">Today's Advisory</small>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
<div class="col-md-3 mb-3">
|
| 52 |
+
<div class="card text-center">
|
| 53 |
+
<div class="card-body">
|
| 54 |
+
<i class="fas fa-user fa-2x text-warning mb-2"></i>
|
| 55 |
+
<h5>{{ farmer.contact_number }}</h5>
|
| 56 |
+
<small class="text-muted">Contact</small>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<div class="row">
|
| 63 |
+
<!-- Today's Advisory -->
|
| 64 |
+
<div class="col-md-8 mb-4">
|
| 65 |
+
<div class="card">
|
| 66 |
+
<div class="card-header bg-primary text-white">
|
| 67 |
+
<h5><i class="fas fa-brain me-2"></i>Today's AI Advisory</h5>
|
| 68 |
+
</div>
|
| 69 |
+
<div class="card-body">
|
| 70 |
+
{% if today_advisory %}
|
| 71 |
+
<div class="alert alert-success">
|
| 72 |
+
<h6><i class="fas fa-check-circle me-2"></i>Tasks to Do:</h6>
|
| 73 |
+
<p>{{ today_advisory.task_to_do }}</p>
|
| 74 |
+
</div>
|
| 75 |
+
<div class="alert alert-warning">
|
| 76 |
+
<h6><i class="fas fa-exclamation-triangle me-2"></i>Tasks to Avoid:</h6>
|
| 77 |
+
<p>{{ today_advisory.task_to_avoid }}</p>
|
| 78 |
+
</div>
|
| 79 |
+
{% if today_advisory.reason_explanation %}
|
| 80 |
+
<div class="alert alert-info">
|
| 81 |
+
<h6><i class="fas fa-info-circle me-2"></i>Explanation:</h6>
|
| 82 |
+
<p>{{ today_advisory.reason_explanation }}</p>
|
| 83 |
+
</div>
|
| 84 |
+
{% endif %}
|
| 85 |
+
|
| 86 |
+
{% if farms %}
|
| 87 |
+
<div class="mt-3">
|
| 88 |
+
<button class="btn btn-success" onclick="sendSMSAdvisory({{ farms[0].id }})">
|
| 89 |
+
<i class="fas fa-sms me-2"></i>Send SMS Alert
|
| 90 |
+
</button>
|
| 91 |
+
<button class="btn btn-info" onclick="sendTelegramAdvisory({{ farms[0].id }})">
|
| 92 |
+
<i class="fab fa-telegram me-2"></i>Send Telegram Alert
|
| 93 |
+
</button>
|
| 94 |
+
<button class="btn btn-primary" onclick="generateNewAdvisory({{ farms[0].id }})">
|
| 95 |
+
<i class="fas fa-sync me-2"></i>Refresh Advisory
|
| 96 |
+
</button>
|
| 97 |
+
</div>
|
| 98 |
+
{% endif %}
|
| 99 |
+
{% else %}
|
| 100 |
+
<div class="text-center text-muted py-4">
|
| 101 |
+
<i class="fas fa-robot fa-3x mb-3"></i>
|
| 102 |
+
<h6>No advisory generated yet for today</h6>
|
| 103 |
+
<p>Click below to generate AI-powered recommendations</p>
|
| 104 |
+
{% if farms %}
|
| 105 |
+
<button class="btn btn-primary" onclick="generateNewAdvisory({{ farms[0].id }})">
|
| 106 |
+
<i class="fas fa-magic me-2"></i>Generate Advisory
|
| 107 |
+
</button>
|
| 108 |
+
{% endif %}
|
| 109 |
+
</div>
|
| 110 |
+
{% endif %}
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<!-- Quick Actions -->
|
| 116 |
+
<div class="col-md-4 mb-4">
|
| 117 |
+
<div class="card">
|
| 118 |
+
<div class="card-header bg-secondary text-white">
|
| 119 |
+
<h5><i class="fas fa-bolt me-2"></i>Quick Actions</h5>
|
| 120 |
+
<small><i class="fas fa-robot me-1"></i>AI-powered tools for better farming</small>
|
| 121 |
+
</div>
|
| 122 |
+
<div class="card-body">
|
| 123 |
+
<div class="d-grid gap-2">
|
| 124 |
+
<a href="{{ url_for('add_farm') }}" class="btn btn-success">
|
| 125 |
+
<i class="fas fa-plus me-2"></i>Add New Farm
|
| 126 |
+
</a>
|
| 127 |
+
{% if farms %}
|
| 128 |
+
<a href="{{ url_for('farm_details', farm_id=farms[0].id) }}" class="btn btn-primary">
|
| 129 |
+
<i class="fas fa-eye me-2"></i>View Farm Details
|
| 130 |
+
</a>
|
| 131 |
+
<button class="btn btn-success" onclick="window.open('https://pranit144-weather-forecast-farmers.hf.space', '_blank')" title="AI-powered weather forecasting tool">
|
| 132 |
+
<i class="fas fa-cloud me-2"></i>Smart Weather Forecast
|
| 133 |
+
</button>
|
| 134 |
+
<button class="btn btn-warning" onclick="viewWeatherAlerts({{ farms[0].id }})">
|
| 135 |
+
<i class="fas fa-exclamation-triangle me-2"></i>Weather Alerts
|
| 136 |
+
</button>
|
| 137 |
+
<button class="btn btn-info" onclick="window.open('https://agri-ai-rosy.vercel.app/cropMarketTrendAnalyzer', '_blank')" title="AI market trend analyzer">
|
| 138 |
+
<i class="fas fa-chart-line me-2"></i>Market Analyzer
|
| 139 |
+
</button>
|
| 140 |
+
<button class="btn btn-danger" onclick="window.open('https://agri-ai-rosy.vercel.app/plant-disease-detector', '_blank')" title="AI plant disease detector">
|
| 141 |
+
<i class="fas fa-search me-2"></i>Disease Detector
|
| 142 |
+
</button>
|
| 143 |
+
<button class="btn btn-secondary" onclick="showSendImageModal()">
|
| 144 |
+
<i class="fas fa-paper-plane me-2"></i>Send Image via Telegram
|
| 145 |
+
</button>
|
| 146 |
+
{% endif %}
|
| 147 |
+
<a href="{{ url_for('farmer_logout') }}" class="btn btn-outline-danger">
|
| 148 |
+
<i class="fas fa-sign-out-alt me-2"></i>Logout
|
| 149 |
+
</a>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
+
|
| 156 |
+
<!-- Daily Tasks Section -->
|
| 157 |
+
<div class="row mb-4">
|
| 158 |
+
<div class="col-12">
|
| 159 |
+
<div class="card">
|
| 160 |
+
<div class="card-header bg-info text-white d-flex justify-content-between align-items-center">
|
| 161 |
+
<h5><i class="fas fa-tasks me-2"></i>Today's Daily Tasks</h5>
|
| 162 |
+
<small id="task-date">{{ today_date or 'Today' }}</small>
|
| 163 |
+
</div>
|
| 164 |
+
<div class="card-body">
|
| 165 |
+
<div id="daily-tasks-container">
|
| 166 |
+
<div class="text-center text-muted py-4" id="no-tasks-message">
|
| 167 |
+
<i class="fas fa-clipboard-list fa-3x mb-3"></i>
|
| 168 |
+
<h6>No daily tasks loaded</h6>
|
| 169 |
+
<p>Generate daily tasks to get AI-powered farming recommendations</p>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
|
| 173 |
+
<div class="mt-3 text-center">
|
| 174 |
+
<div class="btn-group" role="group">
|
| 175 |
+
<button class="btn btn-primary" onclick="loadDailyTasks()">
|
| 176 |
+
<i class="fas fa-sync me-2"></i>Load Today's Tasks
|
| 177 |
+
</button>
|
| 178 |
+
<button class="btn btn-success" onclick="generateDailyTasks()">
|
| 179 |
+
<i class="fas fa-magic me-2"></i>Generate New Tasks
|
| 180 |
+
</button>
|
| 181 |
+
<button class="btn btn-info" onclick="sendTasksTelegram()" title="Send tasks to your Telegram">
|
| 182 |
+
<i class="fab fa-telegram me-2"></i>Send to Telegram
|
| 183 |
+
</button>
|
| 184 |
+
<button class="btn btn-outline-danger" onclick="deleteAllTasks()" title="Delete all tasks for today">
|
| 185 |
+
<i class="fas fa-trash me-2"></i>Clear All
|
| 186 |
+
</button>
|
| 187 |
+
</div>
|
| 188 |
+
</div>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
</div>
|
| 193 |
+
|
| 194 |
+
<!-- Farms List -->
|
| 195 |
+
{% if farms %}
|
| 196 |
+
<div class="row mb-4">
|
| 197 |
+
<div class="col-12">
|
| 198 |
+
<div class="card">
|
| 199 |
+
<div class="card-header">
|
| 200 |
+
<h5><i class="fas fa-list me-2"></i>Your Farms</h5>
|
| 201 |
+
</div>
|
| 202 |
+
<div class="card-body">
|
| 203 |
+
<div class="table-responsive">
|
| 204 |
+
<table class="table table-striped">
|
| 205 |
+
<thead>
|
| 206 |
+
<tr>
|
| 207 |
+
<th>Farm Name</th>
|
| 208 |
+
<th>Size (Acres)</th>
|
| 209 |
+
<th>Crops</th>
|
| 210 |
+
<th>Irrigation</th>
|
| 211 |
+
<th>Actions</th>
|
| 212 |
+
</tr>
|
| 213 |
+
</thead>
|
| 214 |
+
<tbody>
|
| 215 |
+
{% for farm in farms %}
|
| 216 |
+
<tr>
|
| 217 |
+
<td>{{ farm.farm_name }}</td>
|
| 218 |
+
<td>{{ farm.farm_size }}</td>
|
| 219 |
+
<td>
|
| 220 |
+
{% for crop in farm.get_crop_types() %}
|
| 221 |
+
<span class="badge bg-success me-1">{{ crop }}</span>
|
| 222 |
+
{% endfor %}
|
| 223 |
+
</td>
|
| 224 |
+
<td>{{ farm.irrigation_type }}</td>
|
| 225 |
+
<td>
|
| 226 |
+
<div class="btn-group" role="group">
|
| 227 |
+
<!-- Primary Actions -->
|
| 228 |
+
<a href="{{ url_for('farm_details', farm_id=farm.id) }}" class="btn btn-sm btn-outline-primary" title="View Details">
|
| 229 |
+
<i class="fas fa-eye"></i>
|
| 230 |
+
</a>
|
| 231 |
+
<button class="btn btn-sm btn-outline-success" onclick="generateNewAdvisory({{ farm.id }})" title="Generate Daily Advisory">
|
| 232 |
+
<i class="fas fa-brain"></i>
|
| 233 |
+
</button>
|
| 234 |
+
</div>
|
| 235 |
+
|
| 236 |
+
<!-- Yearly Plan Actions -->
|
| 237 |
+
<div class="btn-group ms-1" role="group">
|
| 238 |
+
<button class="btn btn-sm btn-outline-info" onclick="generateYearlyPlan({{ farm.id }})" title="Generate Yearly Plan">
|
| 239 |
+
<i class="fas fa-calendar-plus"></i>
|
| 240 |
+
</button>
|
| 241 |
+
<button class="btn btn-sm btn-outline-secondary" onclick="viewYearlyPlan({{ farm.id }})" title="View Yearly Plan">
|
| 242 |
+
<i class="fas fa-calendar-alt"></i>
|
| 243 |
+
</button>
|
| 244 |
+
<button class="btn btn-sm btn-outline-warning" onclick="editYearlyPlan({{ farm.id }})" title="Edit Yearly Plan">
|
| 245 |
+
<i class="fas fa-edit"></i>
|
| 246 |
+
</button>
|
| 247 |
+
<button class="btn btn-sm btn-outline-dark" onclick="downloadYearlyPlanPDF({{ farm.id }})" title="Download Yearly Plan PDF">
|
| 248 |
+
<i class="fas fa-file-pdf"></i>
|
| 249 |
+
</button>
|
| 250 |
+
<button class="btn btn-sm btn-outline-primary" onclick="sendYearlyPlanTelegram({{ farm.id }})" title="Send Yearly Plan via Telegram">
|
| 251 |
+
<i class="fas fa-paper-plane"></i>
|
| 252 |
+
</button>
|
| 253 |
+
</div>
|
| 254 |
+
|
| 255 |
+
<!-- Smart Features -->
|
| 256 |
+
<div class="btn-group ms-1" role="group">
|
| 257 |
+
<button class="btn btn-sm btn-outline-info" onclick="window.open('https://pranit144-weather-forecast-farmers.hf.space', '_blank')" title="Weather Forecast">
|
| 258 |
+
<i class="fas fa-cloud"></i>
|
| 259 |
+
</button>
|
| 260 |
+
<button class="btn btn-sm btn-outline-success" onclick="window.open('https://agri-ai-rosy.vercel.app/cropMarketTrendAnalyzer', '_blank')" title="Market Prices">
|
| 261 |
+
<i class="fas fa-chart-line"></i>
|
| 262 |
+
</button>
|
| 263 |
+
<button class="btn btn-sm btn-outline-danger" onclick="window.open('https://agri-ai-rosy.vercel.app/plant-disease-detector', '_blank')" title="Disease Detection">
|
| 264 |
+
<i class="fas fa-bug"></i>
|
| 265 |
+
</button>
|
| 266 |
+
</div>
|
| 267 |
+
|
| 268 |
+
<!-- Delete Actions -->
|
| 269 |
+
<div class="btn-group ms-1" role="group">
|
| 270 |
+
<button class="btn btn-sm btn-outline-danger" onclick="deleteYearlyPlan({{ farm.id }})" title="Delete Yearly Plan">
|
| 271 |
+
<i class="fas fa-calendar-times"></i>
|
| 272 |
+
</button>
|
| 273 |
+
<button class="btn btn-sm btn-danger" onclick="deleteFarm({{ farm.id }})" title="Delete Farm"
|
| 274 |
+
style="opacity: 0.7;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.7'">
|
| 275 |
+
<i class="fas fa-trash-alt"></i>
|
| 276 |
+
</button>
|
| 277 |
+
</div>
|
| 278 |
+
</td>
|
| 279 |
+
</tr>
|
| 280 |
+
{% endfor %}
|
| 281 |
+
</tbody>
|
| 282 |
+
</table>
|
| 283 |
+
</div>
|
| 284 |
+
</div>
|
| 285 |
+
</div>
|
| 286 |
+
</div>
|
| 287 |
+
</div>
|
| 288 |
+
{% endif %}
|
| 289 |
+
|
| 290 |
+
<!-- Recent Activities -->
|
| 291 |
+
{% if recent_activities %}
|
| 292 |
+
<div class="row">
|
| 293 |
+
<div class="col-12">
|
| 294 |
+
<div class="card">
|
| 295 |
+
<div class="card-header">
|
| 296 |
+
<h5><i class="fas fa-history me-2"></i>Recent Activities</h5>
|
| 297 |
+
</div>
|
| 298 |
+
<div class="card-body">
|
| 299 |
+
<div class="timeline">
|
| 300 |
+
{% for activity in recent_activities %}
|
| 301 |
+
<div class="timeline-item mb-3">
|
| 302 |
+
<div class="d-flex">
|
| 303 |
+
<div class="flex-shrink-0">
|
| 304 |
+
<i class="fas fa-circle text-success"></i>
|
| 305 |
+
</div>
|
| 306 |
+
<div class="flex-grow-1 ms-3">
|
| 307 |
+
<h6 class="mb-1">{{ activity.activity_type|title }}</h6>
|
| 308 |
+
<p class="mb-1">{{ activity.activity_description }}</p>
|
| 309 |
+
<small class="text-muted">
|
| 310 |
+
Scheduled: {{ activity.scheduled_date.strftime('%d %b %Y') }}
|
| 311 |
+
| Status: <span class="badge bg-{{ 'success' if activity.status == 'completed' else 'warning' }}">{{ activity.status|title }}</span>
|
| 312 |
+
</small>
|
| 313 |
+
</div>
|
| 314 |
+
</div>
|
| 315 |
+
</div>
|
| 316 |
+
{% endfor %}
|
| 317 |
+
</div>
|
| 318 |
+
</div>
|
| 319 |
+
</div>
|
| 320 |
+
</div>
|
| 321 |
+
</div>
|
| 322 |
+
{% endif %}
|
| 323 |
+
</div>
|
| 324 |
+
|
| 325 |
+
<!-- Loading Modal -->
|
| 326 |
+
<div class="modal fade" id="loadingModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1">
|
| 327 |
+
<div class="modal-dialog modal-dialog-centered">
|
| 328 |
+
<div class="modal-content">
|
| 329 |
+
<div class="modal-body text-center">
|
| 330 |
+
<div class="spinner-border text-primary" role="status">
|
| 331 |
+
<span class="visually-hidden">Loading...</span>
|
| 332 |
+
</div>
|
| 333 |
+
<p class="mt-3 mb-0">Processing your request...</p>
|
| 334 |
+
</div>
|
| 335 |
+
</div>
|
| 336 |
+
</div>
|
| 337 |
+
</div>
|
| 338 |
+
|
| 339 |
+
<!-- Yearly Plan Modal -->
|
| 340 |
+
<div class="modal fade" id="yearlyPlanModal" tabindex="-1">
|
| 341 |
+
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
| 342 |
+
<div class="modal-content">
|
| 343 |
+
<div class="modal-header">
|
| 344 |
+
<h5 class="modal-title">Yearly Plan</h5>
|
| 345 |
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
| 346 |
+
</div>
|
| 347 |
+
<div class="modal-body">
|
| 348 |
+
<div id="yearlyPlanContent">Loading...</div>
|
| 349 |
+
</div>
|
| 350 |
+
<div class="modal-footer">
|
| 351 |
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
| 352 |
+
<button type="button" class="btn btn-primary" id="saveYearlyPlanBtn" style="display:none;">Save</button>
|
| 353 |
+
</div>
|
| 354 |
+
</div>
|
| 355 |
+
</div>
|
| 356 |
+
</div>
|
| 357 |
+
{% endblock %}
|
| 358 |
+
|
| 359 |
+
{% block extra_js %}
|
| 360 |
+
<script>
|
| 361 |
+
// Helper to show/hide the Bootstrap 5 modal using vanilla JS
|
| 362 |
+
function _getLoadingModalInstance(){
|
| 363 |
+
const modalEl = document.getElementById('loadingModal');
|
| 364 |
+
return bootstrap.Modal.getOrCreateInstance(modalEl);
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
function _showYearlyPlanModal(html, isEdit = false) {
|
| 368 |
+
const el = document.getElementById('yearlyPlanContent');
|
| 369 |
+
el.innerHTML = html;
|
| 370 |
+
const saveBtn = document.getElementById('saveYearlyPlanBtn');
|
| 371 |
+
if (isEdit) {
|
| 372 |
+
saveBtn.style.display = 'inline-block';
|
| 373 |
+
} else {
|
| 374 |
+
saveBtn.style.display = 'none';
|
| 375 |
+
}
|
| 376 |
+
const m = new bootstrap.Modal(document.getElementById('yearlyPlanModal'));
|
| 377 |
+
m.show();
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
function generateYearlyPlan(farmId) {
|
| 381 |
+
const modal = _getLoadingModalInstance();
|
| 382 |
+
modal.show();
|
| 383 |
+
|
| 384 |
+
fetch(`/farmer/farm/${farmId}/yearly_plan/generate`, {method: 'POST'})
|
| 385 |
+
.then(response => response.json())
|
| 386 |
+
.then(data => {
|
| 387 |
+
modal.hide();
|
| 388 |
+
if (data.success) {
|
| 389 |
+
alert('Yearly plan generated successfully!');
|
| 390 |
+
location.reload();
|
| 391 |
+
} else {
|
| 392 |
+
alert('Failed to generate plan: ' + (data.error || 'Unknown error'));
|
| 393 |
+
}
|
| 394 |
+
})
|
| 395 |
+
.catch(error => {
|
| 396 |
+
modal.hide();
|
| 397 |
+
alert('Error: ' + error.message);
|
| 398 |
+
});
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
function viewYearlyPlan(farmId) {
|
| 402 |
+
const modal = _getLoadingModalInstance();
|
| 403 |
+
modal.show();
|
| 404 |
+
|
| 405 |
+
fetch(`/farmer/farm/${farmId}/yearly_plan`)
|
| 406 |
+
.then(response => response.json())
|
| 407 |
+
.then(data => {
|
| 408 |
+
modal.hide();
|
| 409 |
+
if (data.success && data.plan) {
|
| 410 |
+
if (data.is_html && data.html_content) {
|
| 411 |
+
// Open comprehensive HTML plan in a new window for better viewing
|
| 412 |
+
const newWindow = window.open('', '_blank', 'width=1200,height=800,scrollbars=yes,resizable=yes');
|
| 413 |
+
newWindow.document.write(data.html_content);
|
| 414 |
+
newWindow.document.close();
|
| 415 |
+
newWindow.document.title = `Yearly Plan - ${data.plan.farm_name}`;
|
| 416 |
+
} else {
|
| 417 |
+
// Fallback to modal for simple plans
|
| 418 |
+
const html = `
|
| 419 |
+
<div class="card">
|
| 420 |
+
<div class="card-header bg-success text-white">
|
| 421 |
+
<h6><i class="fas fa-seedling me-2"></i>${data.plan.farm_name || 'Farm'}</h6>
|
| 422 |
+
</div>
|
| 423 |
+
<div class="card-body">
|
| 424 |
+
<div class="mb-2">
|
| 425 |
+
<span class="badge bg-info">Year: ${data.plan.year || new Date().getFullYear()}</span>
|
| 426 |
+
${data.plan.ai_generated ? '<span class="badge bg-success ms-2">AI Generated</span>' : '<span class="badge bg-secondary ms-2">Basic Plan</span>'}
|
| 427 |
+
</div>
|
| 428 |
+
<h6>Summary:</h6>
|
| 429 |
+
<p class="text-muted">${data.plan.summary_text || 'No summary available'}</p>
|
| 430 |
+
<h6>Plan Details:</h6>
|
| 431 |
+
<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>
|
| 432 |
+
<small class="text-muted">Generated: ${new Date(data.plan.generated_at || data.plan.created_at).toLocaleDateString()}</small>
|
| 433 |
+
</div>
|
| 434 |
+
<div class="card-footer text-center">
|
| 435 |
+
<button class="btn btn-primary btn-sm" onclick="generateYearlyPlan(${farmId})">
|
| 436 |
+
<i class="fas fa-sync-alt me-1"></i>Regenerate with AI
|
| 437 |
+
</button>
|
| 438 |
+
</div>
|
| 439 |
+
</div>
|
| 440 |
+
`;
|
| 441 |
+
_showYearlyPlanModal(html);
|
| 442 |
+
}
|
| 443 |
+
} else {
|
| 444 |
+
alert('No yearly plan found for this farm. Generate one first.');
|
| 445 |
+
}
|
| 446 |
+
})
|
| 447 |
+
.catch(error => {
|
| 448 |
+
modal.hide();
|
| 449 |
+
alert('Error loading plan: ' + error.message);
|
| 450 |
+
});
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
function editYearlyPlan(farmId) {
|
| 454 |
+
// First load the existing plan
|
| 455 |
+
fetch(`/farmer/farm/${farmId}/yearly_plan`)
|
| 456 |
+
.then(response => response.json())
|
| 457 |
+
.then(data => {
|
| 458 |
+
if (!data.success || !data.plan) {
|
| 459 |
+
alert('No plan to edit. Generate one first.');
|
| 460 |
+
return;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
const planData = data.plan;
|
| 464 |
+
const html = `
|
| 465 |
+
<div class="mb-3">
|
| 466 |
+
<label class="form-label"><strong>Edit Plan for ${planData.farm_name}</strong></label>
|
| 467 |
+
</div>
|
| 468 |
+
<div class="mb-3">
|
| 469 |
+
<label for="planSummary" class="form-label">Summary</label>
|
| 470 |
+
<textarea id="planSummary" class="form-control" rows="3">${planData.summary_text || ''}</textarea>
|
| 471 |
+
</div>
|
| 472 |
+
<div class="mb-3">
|
| 473 |
+
<label for="planJson" class="form-label">Plan Details (JSON)</label>
|
| 474 |
+
<textarea id="planJson" class="form-control" rows="10">${JSON.stringify(planData.plan_json, null, 2)}</textarea>
|
| 475 |
+
</div>
|
| 476 |
+
`;
|
| 477 |
+
|
| 478 |
+
_showYearlyPlanModal(html, true);
|
| 479 |
+
|
| 480 |
+
// Set up save button handler
|
| 481 |
+
document.getElementById('saveYearlyPlanBtn').onclick = function() {
|
| 482 |
+
const summary = document.getElementById('planSummary').value;
|
| 483 |
+
const jsonText = document.getElementById('planJson').value;
|
| 484 |
+
|
| 485 |
+
let planJson;
|
| 486 |
+
try {
|
| 487 |
+
planJson = JSON.parse(jsonText);
|
| 488 |
+
} catch (err) {
|
| 489 |
+
alert('Invalid JSON format: ' + err.message);
|
| 490 |
+
return;
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
const modal = _getLoadingModalInstance();
|
| 494 |
+
modal.show();
|
| 495 |
+
|
| 496 |
+
fetch(`/farmer/farm/${farmId}/yearly_plan`, {
|
| 497 |
+
method: 'POST',
|
| 498 |
+
headers: {'Content-Type': 'application/json'},
|
| 499 |
+
body: JSON.stringify({
|
| 500 |
+
summary_text: summary,
|
| 501 |
+
plan_json: planJson
|
| 502 |
+
})
|
| 503 |
+
})
|
| 504 |
+
.then(response => response.json())
|
| 505 |
+
.then(result => {
|
| 506 |
+
modal.hide();
|
| 507 |
+
if (result.success) {
|
| 508 |
+
alert('Plan updated successfully!');
|
| 509 |
+
bootstrap.Modal.getInstance(document.getElementById('yearlyPlanModal')).hide();
|
| 510 |
+
location.reload();
|
| 511 |
+
} else {
|
| 512 |
+
alert('Failed to save: ' + (result.error || 'Unknown error'));
|
| 513 |
+
}
|
| 514 |
+
})
|
| 515 |
+
.catch(error => {
|
| 516 |
+
modal.hide();
|
| 517 |
+
alert('Error saving plan: ' + error.message);
|
| 518 |
+
});
|
| 519 |
+
};
|
| 520 |
+
})
|
| 521 |
+
.catch(error => {
|
| 522 |
+
alert('Error loading plan: ' + error.message);
|
| 523 |
+
});
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
function deleteYearlyPlan(farmId) {
|
| 527 |
+
if (!confirm('Are you sure you want to delete the yearly plan for this farm?')) {
|
| 528 |
+
return;
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
const modal = _getLoadingModalInstance();
|
| 532 |
+
modal.show();
|
| 533 |
+
|
| 534 |
+
fetch(`/farmer/farm/${farmId}/yearly_plan/delete`, {method: 'POST'})
|
| 535 |
+
.then(response => response.json())
|
| 536 |
+
.then(data => {
|
| 537 |
+
modal.hide();
|
| 538 |
+
if (data.success) {
|
| 539 |
+
alert('Yearly plan deleted successfully!');
|
| 540 |
+
location.reload();
|
| 541 |
+
} else {
|
| 542 |
+
alert('Failed to delete plan: ' + (data.error || 'Unknown error'));
|
| 543 |
+
}
|
| 544 |
+
})
|
| 545 |
+
.catch(error => {
|
| 546 |
+
modal.hide();
|
| 547 |
+
alert('Error deleting plan: ' + error.message);
|
| 548 |
+
});
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
function deleteFarm(farmId) {
|
| 552 |
+
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.')) {
|
| 553 |
+
return;
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
const modal = _getLoadingModalInstance();
|
| 557 |
+
modal.show();
|
| 558 |
+
|
| 559 |
+
fetch(`/farmer/farm/${farmId}/delete`, {method: 'POST'})
|
| 560 |
+
.then(response => response.json())
|
| 561 |
+
.then(data => {
|
| 562 |
+
modal.hide();
|
| 563 |
+
if (data.success) {
|
| 564 |
+
alert('Farm deleted successfully!');
|
| 565 |
+
location.reload();
|
| 566 |
+
} else {
|
| 567 |
+
alert('Failed to delete farm: ' + (data.error || 'Unknown error'));
|
| 568 |
+
}
|
| 569 |
+
})
|
| 570 |
+
.catch(error => {
|
| 571 |
+
modal.hide();
|
| 572 |
+
alert('Error deleting farm: ' + error.message);
|
| 573 |
+
});
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
function generateNewAdvisory(farmId) {
|
| 577 |
+
const modal = _getLoadingModalInstance();
|
| 578 |
+
modal.show();
|
| 579 |
+
|
| 580 |
+
fetch(`/generate_advisory/${farmId}`)
|
| 581 |
+
.then(response => {
|
| 582 |
+
if (!response.ok) {
|
| 583 |
+
return response.text().then(text => { throw new Error(text || 'Server error'); });
|
| 584 |
+
}
|
| 585 |
+
return response.json();
|
| 586 |
+
})
|
| 587 |
+
.then(data => {
|
| 588 |
+
modal.hide();
|
| 589 |
+
if (data && data.success) {
|
| 590 |
+
alert('Daily advisory generated successfully!');
|
| 591 |
+
location.reload();
|
| 592 |
+
} else {
|
| 593 |
+
alert('Failed to generate advisory: ' + (data && data.error ? data.error : 'Unknown error'));
|
| 594 |
+
}
|
| 595 |
+
})
|
| 596 |
+
.catch(error => {
|
| 597 |
+
modal.hide();
|
| 598 |
+
alert('Error generating advisory: ' + (error && error.message ? error.message : String(error)));
|
| 599 |
+
});
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
function sendSMSAdvisory(farmId) {
|
| 603 |
+
const modal = _getLoadingModalInstance();
|
| 604 |
+
modal.show();
|
| 605 |
+
|
| 606 |
+
fetch(`/send_sms_advisory/${farmId}`)
|
| 607 |
+
.then(response => {
|
| 608 |
+
if (!response.ok) {
|
| 609 |
+
return response.text().then(text => { throw new Error(text || 'Server error'); });
|
| 610 |
+
}
|
| 611 |
+
return response.json();
|
| 612 |
+
})
|
| 613 |
+
.then(data => {
|
| 614 |
+
modal.hide();
|
| 615 |
+
if (data && data.success) {
|
| 616 |
+
alert('SMS sent successfully!');
|
| 617 |
+
} else {
|
| 618 |
+
alert('Failed to send SMS: ' + (data && data.error ? data.error : 'Unknown error'));
|
| 619 |
+
}
|
| 620 |
+
})
|
| 621 |
+
.catch(error => {
|
| 622 |
+
modal.hide();
|
| 623 |
+
alert('Error sending SMS: ' + (error && error.message ? error.message : String(error)));
|
| 624 |
+
});
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
function sendTelegramAdvisory(farmId) {
|
| 628 |
+
const modal = _getLoadingModalInstance();
|
| 629 |
+
modal.show();
|
| 630 |
+
|
| 631 |
+
fetch(`/send_telegram_advisory/${farmId}`)
|
| 632 |
+
.then(response => {
|
| 633 |
+
if (!response.ok) {
|
| 634 |
+
return response.text().then(text => { throw new Error(text || 'Server error'); });
|
| 635 |
+
}
|
| 636 |
+
return response.json();
|
| 637 |
+
})
|
| 638 |
+
.then(data => {
|
| 639 |
+
modal.hide();
|
| 640 |
+
if (data && data.success) {
|
| 641 |
+
alert('Telegram message sent successfully!');
|
| 642 |
+
} else {
|
| 643 |
+
alert('Failed to send Telegram message: ' + (data && data.error ? data.error : 'Unknown error'));
|
| 644 |
+
}
|
| 645 |
+
})
|
| 646 |
+
.catch(error => {
|
| 647 |
+
modal.hide();
|
| 648 |
+
alert('Error sending Telegram message: ' + (error && error.message ? error.message : String(error)));
|
| 649 |
+
});
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
function checkWeather(farmId) {
|
| 653 |
+
const modal = _getLoadingModalInstance();
|
| 654 |
+
modal.show();
|
| 655 |
+
|
| 656 |
+
fetch(`/api/weather/${farmId}`)
|
| 657 |
+
.then(response => {
|
| 658 |
+
if (!response.ok) {
|
| 659 |
+
return response.text().then(text => { throw new Error(text || 'Server error'); });
|
| 660 |
+
}
|
| 661 |
+
return response.json();
|
| 662 |
+
})
|
| 663 |
+
.then(data => {
|
| 664 |
+
modal.hide();
|
| 665 |
+
if (data.error) {
|
| 666 |
+
alert('Weather data unavailable: ' + data.error);
|
| 667 |
+
} else {
|
| 668 |
+
let weatherInfo = `Current Weather:\n`;
|
| 669 |
+
weatherInfo += `Temperature: ${data.main?.temp || 'N/A'}°C\n`;
|
| 670 |
+
weatherInfo += `Humidity: ${data.main?.humidity || 'N/A'}%\n`;
|
| 671 |
+
weatherInfo += `Condition: ${data.weather?.[0]?.description || 'N/A'}\n`;
|
| 672 |
+
weatherInfo += `Wind: ${data.wind?.speed ? (data.wind.speed * 3.6).toFixed(1) : 'N/A'} km/h\n`;
|
| 673 |
+
alert(weatherInfo);
|
| 674 |
+
}
|
| 675 |
+
})
|
| 676 |
+
.catch(error => {
|
| 677 |
+
modal.hide();
|
| 678 |
+
alert('Error fetching weather: ' + (error && error.message ? error.message : String(error)));
|
| 679 |
+
});
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
// New feature functions
|
| 683 |
+
function downloadYearlyPlanPDF(farmId) {
|
| 684 |
+
const modal = _getLoadingModalInstance();
|
| 685 |
+
modal.show();
|
| 686 |
+
|
| 687 |
+
fetch(`/farmer/farm/${farmId}/yearly_plan/pdf`)
|
| 688 |
+
.then(response => {
|
| 689 |
+
modal.hide();
|
| 690 |
+
if (response.ok) {
|
| 691 |
+
// Trigger download
|
| 692 |
+
const link = document.createElement('a');
|
| 693 |
+
link.href = `/farmer/farm/${farmId}/yearly_plan/pdf`;
|
| 694 |
+
link.download = `yearly_plan_farm_${farmId}.pdf`;
|
| 695 |
+
document.body.appendChild(link);
|
| 696 |
+
link.click();
|
| 697 |
+
document.body.removeChild(link);
|
| 698 |
+
} else {
|
| 699 |
+
alert('Failed to generate PDF. Please ensure you have a yearly plan first.');
|
| 700 |
+
}
|
| 701 |
+
})
|
| 702 |
+
.catch(error => {
|
| 703 |
+
modal.hide();
|
| 704 |
+
alert('Error generating PDF: ' + error.message);
|
| 705 |
+
});
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
function sendYearlyPlanTelegram(farmId) {
|
| 709 |
+
const modal = _getLoadingModalInstance();
|
| 710 |
+
modal.show();
|
| 711 |
+
|
| 712 |
+
fetch(`/farmer/farm/${farmId}/yearly_plan/send_telegram`, {
|
| 713 |
+
method: 'POST',
|
| 714 |
+
headers: {'Content-Type': 'application/json'}
|
| 715 |
+
})
|
| 716 |
+
.then(response => response.json())
|
| 717 |
+
.then(data => {
|
| 718 |
+
modal.hide();
|
| 719 |
+
if (data.success) {
|
| 720 |
+
alert('✅ ' + data.message);
|
| 721 |
+
} else {
|
| 722 |
+
alert('❌ ' + (data.error || 'Failed to send via Telegram'));
|
| 723 |
+
}
|
| 724 |
+
})
|
| 725 |
+
.catch(error => {
|
| 726 |
+
modal.hide();
|
| 727 |
+
alert('Error sending via Telegram: ' + error.message);
|
| 728 |
+
});
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
function viewWeatherAlerts(farmId) {
|
| 732 |
+
const modal = _getLoadingModalInstance();
|
| 733 |
+
modal.show();
|
| 734 |
+
|
| 735 |
+
fetch(`/farmer/weather_alerts/${farmId}`)
|
| 736 |
+
.then(response => response.json())
|
| 737 |
+
.then(data => {
|
| 738 |
+
modal.hide();
|
| 739 |
+
if (data.success) {
|
| 740 |
+
let alertsHtml = '<h5><i class="fas fa-cloud-rain me-2"></i>Weather Alerts</h5>';
|
| 741 |
+
if (data.alerts && data.alerts.length > 0) {
|
| 742 |
+
alertsHtml += '<div class="list-group">';
|
| 743 |
+
data.alerts.forEach(alert => {
|
| 744 |
+
const severityClass = alert.severity === 'high' ? 'danger' :
|
| 745 |
+
alert.severity === 'medium' ? 'warning' : 'info';
|
| 746 |
+
alertsHtml += `
|
| 747 |
+
<div class="list-group-item list-group-item-${severityClass}">
|
| 748 |
+
<div class="d-flex w-100 justify-content-between">
|
| 749 |
+
<h6 class="mb-1">${alert.alert_type}</h6>
|
| 750 |
+
<small>${new Date(alert.created_at).toLocaleDateString()}</small>
|
| 751 |
+
</div>
|
| 752 |
+
<p class="mb-1">${alert.message}</p>
|
| 753 |
+
<small>Severity: <span class="badge bg-${severityClass}">${alert.severity}</span></small>
|
| 754 |
+
</div>
|
| 755 |
+
`;
|
| 756 |
+
});
|
| 757 |
+
alertsHtml += '</div>';
|
| 758 |
+
alertsHtml += `
|
| 759 |
+
<div class="mt-3">
|
| 760 |
+
<button class="btn btn-primary" onclick="enableWeatherAlerts(${farmId})">
|
| 761 |
+
<i class="fas fa-bell me-2"></i>Enable Alerts
|
| 762 |
+
</button>
|
| 763 |
+
<button class="btn btn-danger" onclick="disableWeatherAlerts(${farmId})">
|
| 764 |
+
<i class="fas fa-bell-slash me-2"></i>Disable Alerts
|
| 765 |
+
</button>
|
| 766 |
+
</div>
|
| 767 |
+
`;
|
| 768 |
+
} else {
|
| 769 |
+
alertsHtml += '<p class="text-muted">No weather alerts at this time.</p>';
|
| 770 |
+
alertsHtml += `
|
| 771 |
+
<button class="btn btn-primary" onclick="enableWeatherAlerts(${farmId})">
|
| 772 |
+
<i class="fas fa-bell me-2"></i>Enable Weather Alerts
|
| 773 |
+
</button>
|
| 774 |
+
`;
|
| 775 |
+
}
|
| 776 |
+
_showYearlyPlanModal(alertsHtml);
|
| 777 |
+
} else {
|
| 778 |
+
alert('Failed to load weather alerts: ' + (data.error || 'Unknown error'));
|
| 779 |
+
}
|
| 780 |
+
})
|
| 781 |
+
.catch(error => {
|
| 782 |
+
modal.hide();
|
| 783 |
+
alert('Error loading weather alerts: ' + error.message);
|
| 784 |
+
});
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
function enableWeatherAlerts(farmId) {
|
| 788 |
+
fetch(`/farmer/weather_alerts/${farmId}`, {
|
| 789 |
+
method: 'POST',
|
| 790 |
+
headers: {'Content-Type': 'application/json'},
|
| 791 |
+
body: JSON.stringify({action: 'enable'})
|
| 792 |
+
})
|
| 793 |
+
.then(response => response.json())
|
| 794 |
+
.then(data => {
|
| 795 |
+
if (data.success) {
|
| 796 |
+
alert('Weather alerts enabled successfully!');
|
| 797 |
+
viewWeatherAlerts(farmId); // Refresh the view
|
| 798 |
+
} else {
|
| 799 |
+
alert('Failed to enable alerts: ' + (data.error || 'Unknown error'));
|
| 800 |
+
}
|
| 801 |
+
})
|
| 802 |
+
.catch(error => alert('Error: ' + error.message));
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
function disableWeatherAlerts(farmId) {
|
| 806 |
+
fetch(`/farmer/weather_alerts/${farmId}`, {
|
| 807 |
+
method: 'POST',
|
| 808 |
+
headers: {'Content-Type': 'application/json'},
|
| 809 |
+
body: JSON.stringify({action: 'disable'})
|
| 810 |
+
})
|
| 811 |
+
.then(response => response.json())
|
| 812 |
+
.then(data => {
|
| 813 |
+
if (data.success) {
|
| 814 |
+
alert('Weather alerts disabled successfully!');
|
| 815 |
+
viewWeatherAlerts(farmId); // Refresh the view
|
| 816 |
+
} else {
|
| 817 |
+
alert('Failed to disable alerts: ' + (data.error || 'Unknown error'));
|
| 818 |
+
}
|
| 819 |
+
})
|
| 820 |
+
.catch(error => alert('Error: ' + error.message));
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
function viewMarketPrices(farmId) {
|
| 824 |
+
const modal = _getLoadingModalInstance();
|
| 825 |
+
modal.show();
|
| 826 |
+
|
| 827 |
+
fetch(`/farmer/market_prices`)
|
| 828 |
+
.then(response => response.json())
|
| 829 |
+
.then(data => {
|
| 830 |
+
modal.hide();
|
| 831 |
+
if (data.success) {
|
| 832 |
+
let pricesHtml = '<h5><i class="fas fa-chart-line me-2"></i>Market Prices</h5>';
|
| 833 |
+
if (data.prices && data.prices.length > 0) {
|
| 834 |
+
pricesHtml += '<div class="table-responsive">';
|
| 835 |
+
pricesHtml += '<table class="table table-striped">';
|
| 836 |
+
pricesHtml += '<thead><tr><th>Crop</th><th>Market</th><th>Price</th><th>Trend</th><th>Date</th></tr></thead><tbody>';
|
| 837 |
+
data.prices.forEach(price => {
|
| 838 |
+
const trendIcon = price.trend === 'up' ? '📈' : price.trend === 'down' ? '📉' : '➡️';
|
| 839 |
+
pricesHtml += `
|
| 840 |
+
<tr>
|
| 841 |
+
<td><span class="badge bg-success">${price.crop_type}</span></td>
|
| 842 |
+
<td>${price.market_name}</td>
|
| 843 |
+
<td>₹${price.price_per_unit}/${price.unit}</td>
|
| 844 |
+
<td>${trendIcon} ${price.trend}</td>
|
| 845 |
+
<td>${new Date(price.date).toLocaleDateString()}</td>
|
| 846 |
+
</tr>
|
| 847 |
+
`;
|
| 848 |
+
});
|
| 849 |
+
pricesHtml += '</tbody></table></div>';
|
| 850 |
+
pricesHtml += `
|
| 851 |
+
<div class="mt-3">
|
| 852 |
+
<button class="btn btn-primary" onclick="refreshMarketPrices(${farmId})">
|
| 853 |
+
<i class="fas fa-sync me-2"></i>Refresh Prices
|
| 854 |
+
</button>
|
| 855 |
+
<button class="btn btn-success" onclick="subscribeToMarketAlerts(${farmId})">
|
| 856 |
+
<i class="fas fa-bell me-2"></i>Subscribe to Alerts
|
| 857 |
+
</button>
|
| 858 |
+
</div>
|
| 859 |
+
`;
|
| 860 |
+
} else {
|
| 861 |
+
pricesHtml += '<p class="text-muted">No market price data available.</p>';
|
| 862 |
+
pricesHtml += `
|
| 863 |
+
<button class="btn btn-primary" onclick="refreshMarketPrices(${farmId})">
|
| 864 |
+
<i class="fas fa-sync me-2"></i>Fetch Market Prices
|
| 865 |
+
</button>
|
| 866 |
+
`;
|
| 867 |
+
}
|
| 868 |
+
_showYearlyPlanModal(pricesHtml);
|
| 869 |
+
} else {
|
| 870 |
+
alert('Failed to load market prices: ' + (data.error || 'Unknown error'));
|
| 871 |
+
}
|
| 872 |
+
})
|
| 873 |
+
.catch(error => {
|
| 874 |
+
modal.hide();
|
| 875 |
+
alert('Error loading market prices: ' + error.message);
|
| 876 |
+
});
|
| 877 |
+
}
|
| 878 |
+
|
| 879 |
+
function refreshMarketPrices(farmId) {
|
| 880 |
+
const modal = _getLoadingModalInstance();
|
| 881 |
+
modal.show();
|
| 882 |
+
|
| 883 |
+
fetch(`/farmer/market_prices`, {
|
| 884 |
+
method: 'POST',
|
| 885 |
+
headers: {'Content-Type': 'application/json'},
|
| 886 |
+
body: JSON.stringify({action: 'refresh'})
|
| 887 |
+
})
|
| 888 |
+
.then(response => response.json())
|
| 889 |
+
.then(data => {
|
| 890 |
+
modal.hide();
|
| 891 |
+
if (data.success) {
|
| 892 |
+
alert('Market prices updated successfully!');
|
| 893 |
+
viewMarketPrices(farmId); // Refresh the view
|
| 894 |
+
} else {
|
| 895 |
+
alert('Failed to refresh prices: ' + (data.error || 'Unknown error'));
|
| 896 |
+
}
|
| 897 |
+
})
|
| 898 |
+
.catch(error => {
|
| 899 |
+
modal.hide();
|
| 900 |
+
alert('Error refreshing prices: ' + error.message);
|
| 901 |
+
});
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
function subscribeToMarketAlerts(farmId) {
|
| 905 |
+
fetch(`/farmer/market_prices`, {
|
| 906 |
+
method: 'POST',
|
| 907 |
+
headers: {'Content-Type': 'application/json'},
|
| 908 |
+
body: JSON.stringify({action: 'subscribe'})
|
| 909 |
+
})
|
| 910 |
+
.then(response => response.json())
|
| 911 |
+
.then(data => {
|
| 912 |
+
if (data.success) {
|
| 913 |
+
alert('Subscribed to market price alerts successfully!');
|
| 914 |
+
} else {
|
| 915 |
+
alert('Failed to subscribe: ' + (data.error || 'Unknown error'));
|
| 916 |
+
}
|
| 917 |
+
})
|
| 918 |
+
.catch(error => alert('Error: ' + error.message));
|
| 919 |
+
}
|
| 920 |
+
|
| 921 |
+
function detectDisease(farmId) {
|
| 922 |
+
const modal = _getLoadingModalInstance();
|
| 923 |
+
modal.show();
|
| 924 |
+
|
| 925 |
+
fetch(`/farmer/disease_detection/${farmId}`)
|
| 926 |
+
.then(response => response.json())
|
| 927 |
+
.then(data => {
|
| 928 |
+
modal.hide();
|
| 929 |
+
if (data.success) {
|
| 930 |
+
let diseaseHtml = '<h5><i class="fas fa-bug me-2"></i>Disease Detection</h5>';
|
| 931 |
+
if (data.detections && data.detections.length > 0) {
|
| 932 |
+
diseaseHtml += '<div class="row">';
|
| 933 |
+
data.detections.forEach(detection => {
|
| 934 |
+
const severityClass = detection.severity === 'high' ? 'danger' :
|
| 935 |
+
detection.severity === 'medium' ? 'warning' : 'success';
|
| 936 |
+
diseaseHtml += `
|
| 937 |
+
<div class="col-md-6 mb-3">
|
| 938 |
+
<div class="card border-${severityClass}">
|
| 939 |
+
<div class="card-header bg-${severityClass} text-white">
|
| 940 |
+
<h6 class="mb-0">${detection.disease_name}</h6>
|
| 941 |
+
</div>
|
| 942 |
+
<div class="card-body">
|
| 943 |
+
<p><strong>Confidence:</strong> ${Math.round(detection.confidence * 100)}%</p>
|
| 944 |
+
<p><strong>Severity:</strong> <span class="badge bg-${severityClass}">${detection.severity}</span></p>
|
| 945 |
+
<p><strong>Treatment:</strong> ${detection.treatment_recommendation}</p>
|
| 946 |
+
<small class="text-muted">Detected: ${new Date(detection.detection_date).toLocaleDateString()}</small>
|
| 947 |
+
</div>
|
| 948 |
+
</div>
|
| 949 |
+
</div>
|
| 950 |
+
`;
|
| 951 |
+
});
|
| 952 |
+
diseaseHtml += '</div>';
|
| 953 |
+
} else {
|
| 954 |
+
diseaseHtml += '<p class="text-muted">No disease detections recorded.</p>';
|
| 955 |
+
}
|
| 956 |
+
diseaseHtml += `
|
| 957 |
+
<div class="mt-3">
|
| 958 |
+
<label for="diseaseImageUpload" class="btn btn-primary">
|
| 959 |
+
<i class="fas fa-camera me-2"></i>Upload Crop Image
|
| 960 |
+
</label>
|
| 961 |
+
<input type="file" id="diseaseImageUpload" accept="image/*" style="display: none;" onchange="uploadDiseaseImage(${farmId}, this)">
|
| 962 |
+
<button class="btn btn-success ms-2" onclick="viewDiseaseHistory(${farmId})">
|
| 963 |
+
<i class="fas fa-history me-2"></i>View History
|
| 964 |
+
</button>
|
| 965 |
+
</div>
|
| 966 |
+
`;
|
| 967 |
+
_showYearlyPlanModal(diseaseHtml);
|
| 968 |
+
} else {
|
| 969 |
+
alert('Failed to load disease detection data: ' + (data.error || 'Unknown error'));
|
| 970 |
+
}
|
| 971 |
+
})
|
| 972 |
+
.catch(error => {
|
| 973 |
+
modal.hide();
|
| 974 |
+
alert('Error loading disease detection: ' + error.message);
|
| 975 |
+
});
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
function uploadDiseaseImage(farmId, input) {
|
| 979 |
+
if (input.files && input.files[0]) {
|
| 980 |
+
const formData = new FormData();
|
| 981 |
+
formData.append('image', input.files[0]);
|
| 982 |
+
|
| 983 |
+
const modal = _getLoadingModalInstance();
|
| 984 |
+
modal.show();
|
| 985 |
+
|
| 986 |
+
fetch(`/farmer/disease_detection/${farmId}`, {
|
| 987 |
+
method: 'POST',
|
| 988 |
+
body: formData
|
| 989 |
+
})
|
| 990 |
+
.then(response => response.json())
|
| 991 |
+
.then(data => {
|
| 992 |
+
modal.hide();
|
| 993 |
+
if (data.success) {
|
| 994 |
+
alert('Image uploaded and analyzed successfully!');
|
| 995 |
+
detectDisease(farmId); // Refresh the view
|
| 996 |
+
} else {
|
| 997 |
+
alert('Failed to analyze image: ' + (data.error || 'Unknown error'));
|
| 998 |
+
}
|
| 999 |
+
})
|
| 1000 |
+
.catch(error => {
|
| 1001 |
+
modal.hide();
|
| 1002 |
+
alert('Error uploading image: ' + error.message);
|
| 1003 |
+
});
|
| 1004 |
+
}
|
| 1005 |
+
}
|
| 1006 |
+
|
| 1007 |
+
function viewDiseaseHistory(farmId) {
|
| 1008 |
+
window.open(`/farmer/disease_detection/${farmId}`, '_blank');
|
| 1009 |
+
}
|
| 1010 |
+
|
| 1011 |
+
function showSendImageModal() {
|
| 1012 |
+
const modalHtml = `
|
| 1013 |
+
<div class="modal fade" id="sendImageModal" tabindex="-1">
|
| 1014 |
+
<div class="modal-dialog">
|
| 1015 |
+
<div class="modal-content">
|
| 1016 |
+
<div class="modal-header">
|
| 1017 |
+
<h5 class="modal-title"><i class="fas fa-paper-plane me-2"></i>Send Image via Telegram</h5>
|
| 1018 |
+
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
| 1019 |
+
</div>
|
| 1020 |
+
<div class="modal-body">
|
| 1021 |
+
<form id="sendImageForm" enctype="multipart/form-data">
|
| 1022 |
+
<div class="mb-3">
|
| 1023 |
+
<label for="imageFile" class="form-label">Select Image:</label>
|
| 1024 |
+
<input type="file" class="form-control" id="imageFile" name="image" accept="image/*" required>
|
| 1025 |
+
<div class="form-text">Supported formats: JPG, PNG, GIF, BMP</div>
|
| 1026 |
+
</div>
|
| 1027 |
+
<div class="mb-3">
|
| 1028 |
+
<label for="imageCaption" class="form-label">Caption (optional):</label>
|
| 1029 |
+
<textarea class="form-control" id="imageCaption" name="caption" rows="3" placeholder="Add a description for your image..."></textarea>
|
| 1030 |
+
</div>
|
| 1031 |
+
</form>
|
| 1032 |
+
</div>
|
| 1033 |
+
<div class="modal-footer">
|
| 1034 |
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
| 1035 |
+
<button type="button" class="btn btn-primary" onclick="sendImageTelegram()">
|
| 1036 |
+
<i class="fas fa-paper-plane me-2"></i>Send via Telegram
|
| 1037 |
+
</button>
|
| 1038 |
+
</div>
|
| 1039 |
+
</div>
|
| 1040 |
+
</div>
|
| 1041 |
+
</div>
|
| 1042 |
+
`;
|
| 1043 |
+
|
| 1044 |
+
// Remove existing modal if any
|
| 1045 |
+
const existingModal = document.getElementById('sendImageModal');
|
| 1046 |
+
if (existingModal) {
|
| 1047 |
+
existingModal.remove();
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
// Add modal to body
|
| 1051 |
+
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
| 1052 |
+
|
| 1053 |
+
// Show modal
|
| 1054 |
+
const modal = new bootstrap.Modal(document.getElementById('sendImageModal'));
|
| 1055 |
+
modal.show();
|
| 1056 |
+
}
|
| 1057 |
+
|
| 1058 |
+
function sendImageTelegram() {
|
| 1059 |
+
const form = document.getElementById('sendImageForm');
|
| 1060 |
+
const fileInput = document.getElementById('imageFile');
|
| 1061 |
+
const captionInput = document.getElementById('imageCaption');
|
| 1062 |
+
|
| 1063 |
+
if (!fileInput.files[0]) {
|
| 1064 |
+
alert('Please select an image file');
|
| 1065 |
+
return;
|
| 1066 |
+
}
|
| 1067 |
+
|
| 1068 |
+
const formData = new FormData();
|
| 1069 |
+
formData.append('image', fileInput.files[0]);
|
| 1070 |
+
formData.append('caption', captionInput.value || `📷 Image from {{ current_user.name if current_user else 'Farmer' }}`);
|
| 1071 |
+
|
| 1072 |
+
const modal = bootstrap.Modal.getInstance(document.getElementById('sendImageModal'));
|
| 1073 |
+
modal.hide();
|
| 1074 |
+
|
| 1075 |
+
const loadingModal = _getLoadingModalInstance();
|
| 1076 |
+
loadingModal.show();
|
| 1077 |
+
|
| 1078 |
+
fetch('/farmer/send_image_telegram', {
|
| 1079 |
+
method: 'POST',
|
| 1080 |
+
body: formData
|
| 1081 |
+
})
|
| 1082 |
+
.then(response => response.json())
|
| 1083 |
+
.then(data => {
|
| 1084 |
+
loadingModal.hide();
|
| 1085 |
+
if (data.success) {
|
| 1086 |
+
alert('✅ ' + data.message);
|
| 1087 |
+
} else {
|
| 1088 |
+
alert('❌ ' + (data.error || 'Failed to send image via Telegram'));
|
| 1089 |
+
}
|
| 1090 |
+
})
|
| 1091 |
+
.catch(error => {
|
| 1092 |
+
loadingModal.hide();
|
| 1093 |
+
alert('Error sending image: ' + error.message);
|
| 1094 |
+
});
|
| 1095 |
+
}
|
| 1096 |
+
|
| 1097 |
+
// ==================== DAILY TASKS FUNCTIONS ====================
|
| 1098 |
+
|
| 1099 |
+
function loadDailyTasks(date = null) {
|
| 1100 |
+
const targetDate = date || new Date().toISOString().split('T')[0];
|
| 1101 |
+
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>';
|
| 1102 |
+
|
| 1103 |
+
document.getElementById('daily-tasks-container').innerHTML = loadingSpinner;
|
| 1104 |
+
document.getElementById('task-date').textContent = targetDate;
|
| 1105 |
+
|
| 1106 |
+
fetch(`/farmer/daily_tasks?date=${targetDate}`)
|
| 1107 |
+
.then(response => response.json())
|
| 1108 |
+
.then(data => {
|
| 1109 |
+
if (data.success) {
|
| 1110 |
+
renderDailyTasks(data.tasks);
|
| 1111 |
+
} else {
|
| 1112 |
+
showNoTasksMessage('Failed to load tasks: ' + (data.error || 'Unknown error'));
|
| 1113 |
+
}
|
| 1114 |
+
})
|
| 1115 |
+
.catch(error => {
|
| 1116 |
+
console.error('Error loading daily tasks:', error);
|
| 1117 |
+
showNoTasksMessage('Error loading tasks. Please try again.');
|
| 1118 |
+
});
|
| 1119 |
+
}
|
| 1120 |
+
|
| 1121 |
+
function renderDailyTasks(tasks) {
|
| 1122 |
+
const container = document.getElementById('daily-tasks-container');
|
| 1123 |
+
|
| 1124 |
+
if (!tasks || tasks.length === 0) {
|
| 1125 |
+
showNoTasksMessage();
|
| 1126 |
+
return;
|
| 1127 |
+
}
|
| 1128 |
+
|
| 1129 |
+
let html = '<div class="row">';
|
| 1130 |
+
tasks.forEach((task, index) => {
|
| 1131 |
+
const priorityClass = task.priority === 'high' ? 'danger' : task.priority === 'medium' ? 'warning' : 'success';
|
| 1132 |
+
const priorityIcon = task.priority === 'high' ? '🔴' : task.priority === 'medium' ? '🟡' : '🟢';
|
| 1133 |
+
const completedClass = task.is_completed ? 'border-success bg-light' : '';
|
| 1134 |
+
const completedIcon = task.is_completed ? '✅' : '⏳';
|
| 1135 |
+
|
| 1136 |
+
html += `
|
| 1137 |
+
<div class="col-md-6 col-lg-4 mb-3">
|
| 1138 |
+
<div class="card h-100 ${completedClass}">
|
| 1139 |
+
<div class="card-header d-flex justify-content-between align-items-center">
|
| 1140 |
+
<small class="text-${priorityClass}">
|
| 1141 |
+
${priorityIcon} ${task.priority.toUpperCase()} Priority
|
| 1142 |
+
</small>
|
| 1143 |
+
<small>${completedIcon} ${task.estimated_duration} min</small>
|
| 1144 |
+
</div>
|
| 1145 |
+
<div class="card-body">
|
| 1146 |
+
<h6 class="card-title">${task.task_title}</h6>
|
| 1147 |
+
<p class="card-text small">${task.task_description}</p>
|
| 1148 |
+
${task.crop_specific ? `<span class="badge bg-info mb-2">🌾 ${task.crop_specific}</span>` : ''}
|
| 1149 |
+
${task.weather_dependent ? '<span class="badge bg-warning mb-2">🌤️ Weather Dependent</span>' : ''}
|
| 1150 |
+
</div>
|
| 1151 |
+
<div class="card-footer bg-transparent">
|
| 1152 |
+
<div class="d-grid gap-1">
|
| 1153 |
+
${task.is_completed ? `
|
| 1154 |
+
<button class="btn btn-outline-secondary btn-sm" onclick="uncompleteTask(${task.id})">
|
| 1155 |
+
<i class="fas fa-undo me-1"></i>Mark as Incomplete
|
| 1156 |
+
</button>
|
| 1157 |
+
${task.rating ? `<small class="text-muted">Rating: ${'⭐'.repeat(task.rating)}</small>` : ''}
|
| 1158 |
+
` : `
|
| 1159 |
+
<button class="btn btn-success btn-sm" onclick="completeTask(${task.id})">
|
| 1160 |
+
<i class="fas fa-check me-1"></i>Mark as Complete
|
| 1161 |
+
</button>
|
| 1162 |
+
`}
|
| 1163 |
+
</div>
|
| 1164 |
+
</div>
|
| 1165 |
+
</div>
|
| 1166 |
+
</div>`;
|
| 1167 |
+
});
|
| 1168 |
+
html += '</div>';
|
| 1169 |
+
|
| 1170 |
+
container.innerHTML = html;
|
| 1171 |
+
}
|
| 1172 |
+
|
| 1173 |
+
function showNoTasksMessage(message = null) {
|
| 1174 |
+
const defaultMessage = `
|
| 1175 |
+
<div class="text-center text-muted py-4" id="no-tasks-message">
|
| 1176 |
+
<i class="fas fa-clipboard-list fa-3x mb-3"></i>
|
| 1177 |
+
<h6>No daily tasks available</h6>
|
| 1178 |
+
<p>Generate daily tasks to get AI-powered farming recommendations</p>
|
| 1179 |
+
</div>`;
|
| 1180 |
+
|
| 1181 |
+
const errorMessage = `
|
| 1182 |
+
<div class="text-center text-danger py-4">
|
| 1183 |
+
<i class="fas fa-exclamation-triangle fa-3x mb-3"></i>
|
| 1184 |
+
<h6>Error Loading Tasks</h6>
|
| 1185 |
+
<p>${message}</p>
|
| 1186 |
+
</div>`;
|
| 1187 |
+
|
| 1188 |
+
document.getElementById('daily-tasks-container').innerHTML = message ? errorMessage : defaultMessage;
|
| 1189 |
+
}
|
| 1190 |
+
|
| 1191 |
+
function generateDailyTasks() {
|
| 1192 |
+
const today = new Date().toISOString().split('T')[0];
|
| 1193 |
+
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>';
|
| 1194 |
+
|
| 1195 |
+
document.getElementById('daily-tasks-container').innerHTML = loadingSpinner;
|
| 1196 |
+
|
| 1197 |
+
fetch('/farmer/daily_tasks/generate', {
|
| 1198 |
+
method: 'POST',
|
| 1199 |
+
headers: {
|
| 1200 |
+
'Content-Type': 'application/json',
|
| 1201 |
+
},
|
| 1202 |
+
body: JSON.stringify({
|
| 1203 |
+
date: today
|
| 1204 |
+
})
|
| 1205 |
+
})
|
| 1206 |
+
.then(response => response.json())
|
| 1207 |
+
.then(data => {
|
| 1208 |
+
if (data.success) {
|
| 1209 |
+
alert('✅ ' + data.message);
|
| 1210 |
+
loadDailyTasks(); // Reload tasks to show the generated ones
|
| 1211 |
+
} else {
|
| 1212 |
+
alert('❌ ' + (data.error || 'Failed to generate daily tasks'));
|
| 1213 |
+
showNoTasksMessage();
|
| 1214 |
+
}
|
| 1215 |
+
})
|
| 1216 |
+
.catch(error => {
|
| 1217 |
+
console.error('Error generating daily tasks:', error);
|
| 1218 |
+
alert('Error generating daily tasks. Please try again.');
|
| 1219 |
+
showNoTasksMessage();
|
| 1220 |
+
});
|
| 1221 |
+
}
|
| 1222 |
+
|
| 1223 |
+
function completeTask(taskId) {
|
| 1224 |
+
// Show rating modal
|
| 1225 |
+
const rating = prompt('Rate this task completion (1-5 stars):');
|
| 1226 |
+
const feedback = prompt('Any feedback about this task? (optional):');
|
| 1227 |
+
|
| 1228 |
+
let ratingNum = null;
|
| 1229 |
+
if (rating && !isNaN(rating)) {
|
| 1230 |
+
ratingNum = Math.max(1, Math.min(5, parseInt(rating)));
|
| 1231 |
+
}
|
| 1232 |
+
|
| 1233 |
+
fetch(`/farmer/daily_tasks/${taskId}/complete`, {
|
| 1234 |
+
method: 'POST',
|
| 1235 |
+
headers: {
|
| 1236 |
+
'Content-Type': 'application/json',
|
| 1237 |
+
},
|
| 1238 |
+
body: JSON.stringify({
|
| 1239 |
+
rating: ratingNum,
|
| 1240 |
+
feedback: feedback || ''
|
| 1241 |
+
})
|
| 1242 |
+
})
|
| 1243 |
+
.then(response => response.json())
|
| 1244 |
+
.then(data => {
|
| 1245 |
+
if (data.success) {
|
| 1246 |
+
alert('✅ Task completed successfully!');
|
| 1247 |
+
loadDailyTasks(); // Reload tasks to update status
|
| 1248 |
+
} else {
|
| 1249 |
+
alert('❌ ' + (data.error || 'Failed to complete task'));
|
| 1250 |
+
}
|
| 1251 |
+
})
|
| 1252 |
+
.catch(error => {
|
| 1253 |
+
console.error('Error completing task:', error);
|
| 1254 |
+
alert('Error completing task. Please try again.');
|
| 1255 |
+
});
|
| 1256 |
+
}
|
| 1257 |
+
|
| 1258 |
+
function uncompleteTask(taskId) {
|
| 1259 |
+
if (!confirm('Are you sure you want to mark this task as incomplete?')) {
|
| 1260 |
+
return;
|
| 1261 |
+
}
|
| 1262 |
+
|
| 1263 |
+
fetch(`/farmer/daily_tasks/${taskId}/uncomplete`, {
|
| 1264 |
+
method: 'POST',
|
| 1265 |
+
headers: {
|
| 1266 |
+
'Content-Type': 'application/json',
|
| 1267 |
+
}
|
| 1268 |
+
})
|
| 1269 |
+
.then(response => response.json())
|
| 1270 |
+
.then(data => {
|
| 1271 |
+
if (data.success) {
|
| 1272 |
+
alert('✅ Task marked as incomplete');
|
| 1273 |
+
loadDailyTasks(); // Reload tasks to update status
|
| 1274 |
+
} else {
|
| 1275 |
+
alert('❌ ' + (data.error || 'Failed to uncomplete task'));
|
| 1276 |
+
}
|
| 1277 |
+
})
|
| 1278 |
+
.catch(error => {
|
| 1279 |
+
console.error('Error uncompleting task:', error);
|
| 1280 |
+
alert('Error updating task. Please try again.');
|
| 1281 |
+
});
|
| 1282 |
+
}
|
| 1283 |
+
|
| 1284 |
+
function sendTasksTelegram() {
|
| 1285 |
+
const today = new Date().toISOString().split('T')[0];
|
| 1286 |
+
|
| 1287 |
+
fetch('/farmer/daily_tasks/send_telegram', {
|
| 1288 |
+
method: 'POST',
|
| 1289 |
+
headers: {
|
| 1290 |
+
'Content-Type': 'application/json',
|
| 1291 |
+
},
|
| 1292 |
+
body: JSON.stringify({
|
| 1293 |
+
date: today
|
| 1294 |
+
})
|
| 1295 |
+
})
|
| 1296 |
+
.then(response => response.json())
|
| 1297 |
+
.then(data => {
|
| 1298 |
+
if (data.success) {
|
| 1299 |
+
alert('✅ ' + data.message);
|
| 1300 |
+
} else {
|
| 1301 |
+
alert('❌ ' + (data.error || 'Failed to send tasks via Telegram'));
|
| 1302 |
+
}
|
| 1303 |
+
})
|
| 1304 |
+
.catch(error => {
|
| 1305 |
+
console.error('Error sending tasks via Telegram:', error);
|
| 1306 |
+
alert('Error sending tasks. Please try again.');
|
| 1307 |
+
});
|
| 1308 |
+
}
|
| 1309 |
+
|
| 1310 |
+
function deleteAllTasks() {
|
| 1311 |
+
if (!confirm('Are you sure you want to delete all tasks for today? This action cannot be undone.')) {
|
| 1312 |
+
return;
|
| 1313 |
+
}
|
| 1314 |
+
|
| 1315 |
+
const today = new Date().toISOString().split('T')[0];
|
| 1316 |
+
|
| 1317 |
+
fetch('/farmer/daily_tasks/delete_all', {
|
| 1318 |
+
method: 'POST',
|
| 1319 |
+
headers: {
|
| 1320 |
+
'Content-Type': 'application/json',
|
| 1321 |
+
},
|
| 1322 |
+
body: JSON.stringify({
|
| 1323 |
+
date: today
|
| 1324 |
+
})
|
| 1325 |
+
})
|
| 1326 |
+
.then(response => response.json())
|
| 1327 |
+
.then(data => {
|
| 1328 |
+
if (data.success) {
|
| 1329 |
+
alert('✅ ' + data.message);
|
| 1330 |
+
showNoTasksMessage(); // Show the no tasks message
|
| 1331 |
+
} else {
|
| 1332 |
+
alert('❌ ' + (data.error || 'Failed to delete tasks'));
|
| 1333 |
+
}
|
| 1334 |
+
})
|
| 1335 |
+
.catch(error => {
|
| 1336 |
+
console.error('Error deleting tasks:', error);
|
| 1337 |
+
alert('Error deleting tasks. Please try again.');
|
| 1338 |
+
});
|
| 1339 |
+
}
|
| 1340 |
+
|
| 1341 |
+
// Auto-load today's tasks when page loads
|
| 1342 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 1343 |
+
// Load today's tasks automatically
|
| 1344 |
+
loadDailyTasks();
|
| 1345 |
+
});
|
| 1346 |
+
</script>
|
| 1347 |
+
</script>
|
| 1348 |
+
{% endblock %}
|
templates/farmer_dashboard_fixed.html
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Farmer Dashboard - Farm Management Portal{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="container mt-4">
|
| 7 |
+
<!-- Welcome Header -->
|
| 8 |
+
<div class="row mb-4">
|
| 9 |
+
<div class="col-12">
|
| 10 |
+
<div class="card bg-success text-white">
|
| 11 |
+
<div class="card-body">
|
| 12 |
+
<h2><i class="fas fa-tachometer-alt me-2"></i>Welcome, {{ farmer.name }}!</h2>
|
| 13 |
+
<p class="mb-0">Manage your farms and get AI-powered daily recommendations</p>
|
| 14 |
+
</div>
|
| 15 |
+
</div>
|
| 16 |
+
</div>
|
| 17 |
+
</div>
|
| 18 |
+
|
| 19 |
+
<!-- Quick Stats -->
|
| 20 |
+
<div class="row mb-4">
|
| 21 |
+
<div class="col-md-3 mb-3">
|
| 22 |
+
<div class="card text-center">
|
| 23 |
+
<div class="card-body">
|
| 24 |
+
<i class="fas fa-seedling fa-2x text-success mb-2"></i>
|
| 25 |
+
<h5>{{ farms|length }}</h5>
|
| 26 |
+
<small class="text-muted">Total Farms</small>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="col-md-3 mb-3">
|
| 31 |
+
<div class="card text-center">
|
| 32 |
+
<div class="card-body">
|
| 33 |
+
<i class="fas fa-calendar-check fa-2x text-primary mb-2"></i>
|
| 34 |
+
<h5>{{ recent_activities|length }}</h5>
|
| 35 |
+
<small class="text-muted">Recent Activities</small>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
<div class="col-md-3 mb-3">
|
| 40 |
+
<div class="card text-center">
|
| 41 |
+
<div class="card-body">
|
| 42 |
+
<i class="fas fa-sms fa-2x text-info mb-2"></i>
|
| 43 |
+
<h5>{% if today_advisory %}Active{% else %}Pending{% endif %}</h5>
|
| 44 |
+
<small class="text-muted">Today's Advisory</small>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
<div class="col-md-3 mb-3">
|
| 49 |
+
<div class="card text-center">
|
| 50 |
+
<div class="card-body">
|
| 51 |
+
<i class="fas fa-user fa-2x text-warning mb-2"></i>
|
| 52 |
+
<h5>{{ farmer.contact_number }}</h5>
|
| 53 |
+
<small class="text-muted">Contact</small>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
<div class="row">
|
| 60 |
+
<!-- Today's Advisory -->
|
| 61 |
+
<div class="col-md-8 mb-4">
|
| 62 |
+
<div class="card">
|
| 63 |
+
<div class="card-header bg-primary text-white">
|
| 64 |
+
<h5><i class="fas fa-brain me-2"></i>Today's AI Advisory</h5>
|
| 65 |
+
</div>
|
| 66 |
+
<div class="card-body">
|
| 67 |
+
{% if today_advisory %}
|
| 68 |
+
<div class="alert alert-success">
|
| 69 |
+
<h6><i class="fas fa-check-circle me-2"></i>Tasks to Do:</h6>
|
| 70 |
+
<p>{{ today_advisory.task_to_do }}</p>
|
| 71 |
+
</div>
|
| 72 |
+
<div class="alert alert-warning">
|
| 73 |
+
<h6><i class="fas fa-exclamation-triangle me-2"></i>Tasks to Avoid:</h6>
|
| 74 |
+
<p>{{ today_advisory.task_to_avoid }}</p>
|
| 75 |
+
</div>
|
| 76 |
+
{% if today_advisory.reason_explanation %}
|
| 77 |
+
<div class="alert alert-info">
|
| 78 |
+
<h6><i class="fas fa-info-circle me-2"></i>Explanation:</h6>
|
| 79 |
+
<p>{{ today_advisory.reason_explanation }}</p>
|
| 80 |
+
</div>
|
| 81 |
+
{% endif %}
|
| 82 |
+
|
| 83 |
+
{% if farms %}
|
| 84 |
+
<div class="mt-3">
|
| 85 |
+
<button class="btn btn-success" onclick="sendSMSAdvisory({{ farms[0].id }})">
|
| 86 |
+
<i class="fas fa-sms me-2"></i>Send SMS Alert
|
| 87 |
+
</button>
|
| 88 |
+
<button class="btn btn-primary" onclick="generateNewAdvisory({{ farms[0].id }})">
|
| 89 |
+
<i class="fas fa-sync me-2"></i>Refresh Advisory
|
| 90 |
+
</button>
|
| 91 |
+
</div>
|
| 92 |
+
{% endif %}
|
| 93 |
+
{% else %}
|
| 94 |
+
<div class="text-center text-muted py-4">
|
| 95 |
+
<i class="fas fa-robot fa-3x mb-3"></i>
|
| 96 |
+
<h6>No advisory generated yet for today</h6>
|
| 97 |
+
<p>Click below to generate AI-powered recommendations</p>
|
| 98 |
+
{% if farms %}
|
| 99 |
+
<button class="btn btn-primary" onclick="generateNewAdvisory({{ farms[0].id }})">
|
| 100 |
+
<i class="fas fa-magic me-2"></i>Generate Advisory
|
| 101 |
+
</button>
|
| 102 |
+
{% endif %}
|
| 103 |
+
</div>
|
| 104 |
+
{% endif %}
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
<!-- Quick Actions -->
|
| 110 |
+
<div class="col-md-4 mb-4">
|
| 111 |
+
<div class="card">
|
| 112 |
+
<div class="card-header bg-secondary text-white">
|
| 113 |
+
<h5><i class="fas fa-bolt me-2"></i>Quick Actions</h5>
|
| 114 |
+
</div>
|
| 115 |
+
<div class="card-body">
|
| 116 |
+
<div class="d-grid gap-2">
|
| 117 |
+
<a href="{{ url_for('add_farm') }}" class="btn btn-success">
|
| 118 |
+
<i class="fas fa-plus me-2"></i>Add New Farm
|
| 119 |
+
</a>
|
| 120 |
+
{% if farms %}
|
| 121 |
+
<a href="{{ url_for('farm_details', farm_id=farms[0].id) }}" class="btn btn-primary">
|
| 122 |
+
<i class="fas fa-eye me-2"></i>View Farm Details
|
| 123 |
+
</a>
|
| 124 |
+
<button class="btn btn-info" onclick="checkWeather({{ farms[0].id }})">
|
| 125 |
+
<i class="fas fa-cloud me-2"></i>Check Weather
|
| 126 |
+
</button>
|
| 127 |
+
{% endif %}
|
| 128 |
+
<a href="{{ url_for('farmer_logout') }}" class="btn btn-outline-danger">
|
| 129 |
+
<i class="fas fa-sign-out-alt me-2"></i>Logout
|
| 130 |
+
</a>
|
| 131 |
+
</div>
|
| 132 |
+
</div>
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
+
<!-- Farms List -->
|
| 138 |
+
{% if farms %}
|
| 139 |
+
<div class="row mb-4">
|
| 140 |
+
<div class="col-12">
|
| 141 |
+
<div class="card">
|
| 142 |
+
<div class="card-header">
|
| 143 |
+
<h5><i class="fas fa-list me-2"></i>Your Farms</h5>
|
| 144 |
+
</div>
|
| 145 |
+
<div class="card-body">
|
| 146 |
+
<div class="table-responsive">
|
| 147 |
+
<table class="table table-striped">
|
| 148 |
+
<thead>
|
| 149 |
+
<tr>
|
| 150 |
+
<th>Farm Name</th>
|
| 151 |
+
<th>Size (Acres)</th>
|
| 152 |
+
<th>Crops</th>
|
| 153 |
+
<th>Irrigation</th>
|
| 154 |
+
<th>Actions</th>
|
| 155 |
+
</tr>
|
| 156 |
+
</thead>
|
| 157 |
+
<tbody>
|
| 158 |
+
{% for farm in farms %}
|
| 159 |
+
<tr>
|
| 160 |
+
<td>{{ farm.farm_name }}</td>
|
| 161 |
+
<td>{{ farm.farm_size }}</td>
|
| 162 |
+
<td>
|
| 163 |
+
{% for crop in farm.get_crop_types() %}
|
| 164 |
+
<span class="badge bg-success me-1">{{ crop }}</span>
|
| 165 |
+
{% endfor %}
|
| 166 |
+
</td>
|
| 167 |
+
<td>{{ farm.irrigation_type }}</td>
|
| 168 |
+
<td>
|
| 169 |
+
<a href="{{ url_for('farm_details', farm_id=farm.id) }}" class="btn btn-sm btn-primary">
|
| 170 |
+
<i class="fas fa-eye"></i>
|
| 171 |
+
</a>
|
| 172 |
+
<button class="btn btn-sm btn-success" onclick="generateNewAdvisory({{ farm.id }})">
|
| 173 |
+
<i class="fas fa-brain"></i>
|
| 174 |
+
</button>
|
| 175 |
+
</td>
|
| 176 |
+
</tr>
|
| 177 |
+
{% endfor %}
|
| 178 |
+
</tbody>
|
| 179 |
+
</table>
|
| 180 |
+
</div>
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
{% endif %}
|
| 186 |
+
|
| 187 |
+
<!-- Recent Activities -->
|
| 188 |
+
{% if recent_activities %}
|
| 189 |
+
<div class="row">
|
| 190 |
+
<div class="col-12">
|
| 191 |
+
<div class="card">
|
| 192 |
+
<div class="card-header">
|
| 193 |
+
<h5><i class="fas fa-history me-2"></i>Recent Activities</h5>
|
| 194 |
+
</div>
|
| 195 |
+
<div class="card-body">
|
| 196 |
+
<div class="timeline">
|
| 197 |
+
{% for activity in recent_activities %}
|
| 198 |
+
<div class="timeline-item mb-3">
|
| 199 |
+
<div class="d-flex">
|
| 200 |
+
<div class="flex-shrink-0">
|
| 201 |
+
<i class="fas fa-circle text-success"></i>
|
| 202 |
+
</div>
|
| 203 |
+
<div class="flex-grow-1 ms-3">
|
| 204 |
+
<h6 class="mb-1">{{ activity.activity_type|title }}</h6>
|
| 205 |
+
<p class="mb-1">{{ activity.activity_description }}</p>
|
| 206 |
+
<small class="text-muted">
|
| 207 |
+
Scheduled: {{ activity.scheduled_date.strftime('%d %b %Y') }}
|
| 208 |
+
| Status: <span class="badge bg-{{ 'success' if activity.status == 'completed' else 'warning' }}">{{ activity.status|title }}</span>
|
| 209 |
+
</small>
|
| 210 |
+
</div>
|
| 211 |
+
</div>
|
| 212 |
+
</div>
|
| 213 |
+
{% endfor %}
|
| 214 |
+
</div>
|
| 215 |
+
</div>
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
{% endif %}
|
| 220 |
+
</div>
|
| 221 |
+
|
| 222 |
+
<!-- Loading Modal -->
|
| 223 |
+
<div class="modal fade" id="loadingModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1">
|
| 224 |
+
<div class="modal-dialog modal-dialog-centered">
|
| 225 |
+
<div class="modal-content">
|
| 226 |
+
<div class="modal-body text-center">
|
| 227 |
+
<div class="spinner-border text-primary" role="status">
|
| 228 |
+
<span class="visually-hidden">Loading...</span>
|
| 229 |
+
</div>
|
| 230 |
+
<p class="mt-3 mb-0">Processing your request...</p>
|
| 231 |
+
</div>
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
{% endblock %}
|
| 236 |
+
|
| 237 |
+
{% block extra_js %}
|
| 238 |
+
<script>
|
| 239 |
+
// Helper to show/hide the Bootstrap 5 modal using vanilla JS
|
| 240 |
+
function _getLoadingModalInstance(){
|
| 241 |
+
const modalEl = document.getElementById('loadingModal');
|
| 242 |
+
return bootstrap.Modal.getOrCreateInstance(modalEl);
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
function generateNewAdvisory(farmId) {
|
| 246 |
+
const modal = _getLoadingModalInstance();
|
| 247 |
+
modal.show();
|
| 248 |
+
|
| 249 |
+
fetch(`/generate_advisory/${farmId}`)
|
| 250 |
+
.then(response => response.json())
|
| 251 |
+
.then(data => {
|
| 252 |
+
modal.hide();
|
| 253 |
+
if (data.success) {
|
| 254 |
+
// Reload to show updated advisory
|
| 255 |
+
location.reload();
|
| 256 |
+
} else {
|
| 257 |
+
alert('Failed to generate advisory: ' + (data.error || 'Unknown error'));
|
| 258 |
+
}
|
| 259 |
+
})
|
| 260 |
+
.catch(error => {
|
| 261 |
+
modal.hide();
|
| 262 |
+
alert('Error: ' + error.message);
|
| 263 |
+
});
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
function sendSMSAdvisory(farmId) {
|
| 267 |
+
const modal = _getLoadingModalInstance();
|
| 268 |
+
modal.show();
|
| 269 |
+
|
| 270 |
+
fetch(`/send_sms_advisory/${farmId}`)
|
| 271 |
+
.then(response => response.json())
|
| 272 |
+
.then(data => {
|
| 273 |
+
modal.hide();
|
| 274 |
+
if (data.success) {
|
| 275 |
+
alert('SMS sent successfully!');
|
| 276 |
+
} else {
|
| 277 |
+
alert('Failed to send SMS: ' + (data.error || 'Unknown error'));
|
| 278 |
+
}
|
| 279 |
+
})
|
| 280 |
+
.catch(error => {
|
| 281 |
+
modal.hide();
|
| 282 |
+
alert('Error: ' + error.message);
|
| 283 |
+
});
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
function checkWeather(farmId) {
|
| 287 |
+
const modal = _getLoadingModalInstance();
|
| 288 |
+
modal.show();
|
| 289 |
+
|
| 290 |
+
fetch(`/api/weather/${farmId}`)
|
| 291 |
+
.then(response => response.json())
|
| 292 |
+
.then(data => {
|
| 293 |
+
modal.hide();
|
| 294 |
+
if (data.error) {
|
| 295 |
+
alert('Weather data unavailable: ' + data.error);
|
| 296 |
+
} else {
|
| 297 |
+
let weatherInfo = `Current Weather:\n`;
|
| 298 |
+
weatherInfo += `Temperature: ${data.main?.temp || 'N/A'}°C\n`;
|
| 299 |
+
weatherInfo += `Humidity: ${data.main?.humidity || 'N/A'}%\n`;
|
| 300 |
+
weatherInfo += `Condition: ${data.weather?.[0]?.description || 'N/A'}\n`;
|
| 301 |
+
weatherInfo += `Wind: ${data.wind?.speed ? (data.wind.speed * 3.6).toFixed(1) : 'N/A'} km/h\n`;
|
| 302 |
+
alert(weatherInfo);
|
| 303 |
+
}
|
| 304 |
+
})
|
| 305 |
+
.catch(error => {
|
| 306 |
+
modal.hide();
|
| 307 |
+
alert('Error: ' + error.message);
|
| 308 |
+
});
|
| 309 |
+
}
|
| 310 |
+
</script>
|
| 311 |
+
{% endblock %}
|
templates/farmer_login.html
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Farmer Login - Farm Management Portal{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="container">
|
| 7 |
+
<div class="row justify-content-center mt-5">
|
| 8 |
+
<div class="col-md-6 col-lg-4">
|
| 9 |
+
<div class="card">
|
| 10 |
+
<div class="card-header bg-success text-white text-center">
|
| 11 |
+
<h4><i class="fas fa-user me-2"></i>Farmer Login</h4>
|
| 12 |
+
</div>
|
| 13 |
+
<div class="card-body">
|
| 14 |
+
<form method="POST">
|
| 15 |
+
<div class="mb-3">
|
| 16 |
+
<label for="aadhaar_id" class="form-label">Aadhaar ID</label>
|
| 17 |
+
<input type="text" class="form-control" id="aadhaar_id" name="aadhaar_id"
|
| 18 |
+
placeholder="Enter 12-digit Aadhaar ID" maxlength="12" required>
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<div class="mb-3">
|
| 22 |
+
<label for="password" class="form-label">Password</label>
|
| 23 |
+
<input type="password" class="form-control" id="password" name="password"
|
| 24 |
+
placeholder="Enter password" required>
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
<div class="d-grid">
|
| 28 |
+
<button type="submit" class="btn btn-success">
|
| 29 |
+
<i class="fas fa-sign-in-alt me-2"></i>Login
|
| 30 |
+
</button>
|
| 31 |
+
</div>
|
| 32 |
+
</form>
|
| 33 |
+
|
| 34 |
+
<hr>
|
| 35 |
+
|
| 36 |
+
<div class="text-center">
|
| 37 |
+
<p class="mb-0">Don't have an account?</p>
|
| 38 |
+
<a href="{{ url_for('farmer_register') }}" class="btn btn-outline-success btn-sm">
|
| 39 |
+
<i class="fas fa-user-plus me-1"></i>Register Now
|
| 40 |
+
</a>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
{% endblock %}
|
| 48 |
+
|
| 49 |
+
{% block extra_js %}
|
| 50 |
+
<script>
|
| 51 |
+
// Format Aadhaar ID input
|
| 52 |
+
document.getElementById('aadhaar_id').addEventListener('input', function(e) {
|
| 53 |
+
let value = e.target.value.replace(/\D/g, '');
|
| 54 |
+
if (value.length > 12) {
|
| 55 |
+
value = value.slice(0, 12);
|
| 56 |
+
}
|
| 57 |
+
e.target.value = value;
|
| 58 |
+
});
|
| 59 |
+
</script>
|
| 60 |
+
{% endblock %}
|
templates/farmer_register.html
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Farmer Registration - Farm Management Portal{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="container">
|
| 7 |
+
<div class="row justify-content-center mt-4">
|
| 8 |
+
<div class="col-md-8 col-lg-6">
|
| 9 |
+
<div class="card">
|
| 10 |
+
<div class="card-header bg-success text-white text-center">
|
| 11 |
+
<h4><i class="fas fa-user-plus me-2"></i>Farmer Registration</h4>
|
| 12 |
+
</div>
|
| 13 |
+
<div class="card-body">
|
| 14 |
+
<form method="POST">
|
| 15 |
+
<div class="row">
|
| 16 |
+
<div class="col-md-12 mb-3">
|
| 17 |
+
<label for="name" class="form-label">Full Name *</label>
|
| 18 |
+
<input type="text" class="form-control" id="name" name="name" required>
|
| 19 |
+
</div>
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<div class="row">
|
| 23 |
+
<div class="col-md-6 mb-3">
|
| 24 |
+
<label for="age" class="form-label">Age</label>
|
| 25 |
+
<input type="number" class="form-control" id="age" name="age" min="18" max="100">
|
| 26 |
+
</div>
|
| 27 |
+
<div class="col-md-6 mb-3">
|
| 28 |
+
<label for="gender" class="form-label">Gender</label>
|
| 29 |
+
<select class="form-select" id="gender" name="gender">
|
| 30 |
+
<option value="">Select Gender</option>
|
| 31 |
+
<option value="Male">Male</option>
|
| 32 |
+
<option value="Female">Female</option>
|
| 33 |
+
<option value="Other">Other</option>
|
| 34 |
+
</select>
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
+
<div class="mb-3">
|
| 39 |
+
<label for="aadhaar_id" class="form-label">Aadhaar ID *</label>
|
| 40 |
+
<input type="text" class="form-control" id="aadhaar_id" name="aadhaar_id"
|
| 41 |
+
placeholder="Enter 12-digit Aadhaar ID" maxlength="12" required>
|
| 42 |
+
<small class="form-text text-muted">This will be used as your login ID</small>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<div class="mb-3">
|
| 46 |
+
<label for="contact_number" class="form-label">Contact Number *</label>
|
| 47 |
+
<input type="tel" class="form-control" id="contact_number" name="contact_number"
|
| 48 |
+
placeholder="Enter 10-digit mobile number" maxlength="10" required>
|
| 49 |
+
<small class="form-text text-muted">SMS alerts will be sent to this number</small>
|
| 50 |
+
</div>
|
| 51 |
+
|
| 52 |
+
<div class="mb-3">
|
| 53 |
+
<label for="address" class="form-label">Address *</label>
|
| 54 |
+
<textarea class="form-control" id="address" name="address" rows="3"
|
| 55 |
+
placeholder="Enter complete address" required></textarea>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<div class="mb-3">
|
| 59 |
+
<label for="password" class="form-label">Password *</label>
|
| 60 |
+
<input type="password" class="form-control" id="password" name="password"
|
| 61 |
+
placeholder="Enter password" minlength="6" required>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
<div class="mb-3">
|
| 65 |
+
<label for="confirm_password" class="form-label">Confirm Password *</label>
|
| 66 |
+
<input type="password" class="form-control" id="confirm_password" name="confirm_password"
|
| 67 |
+
placeholder="Confirm password" required>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<div class="form-check mb-3">
|
| 71 |
+
<input class="form-check-input" type="checkbox" id="terms" required>
|
| 72 |
+
<label class="form-check-label" for="terms">
|
| 73 |
+
I agree to the Terms and Conditions and Privacy Policy
|
| 74 |
+
</label>
|
| 75 |
+
</div>
|
| 76 |
+
|
| 77 |
+
<div class="d-grid">
|
| 78 |
+
<button type="submit" class="btn btn-success">
|
| 79 |
+
<i class="fas fa-user-plus me-2"></i>Register
|
| 80 |
+
</button>
|
| 81 |
+
</div>
|
| 82 |
+
</form>
|
| 83 |
+
|
| 84 |
+
<hr>
|
| 85 |
+
|
| 86 |
+
<div class="text-center">
|
| 87 |
+
<p class="mb-0">Already have an account?</p>
|
| 88 |
+
<a href="{{ url_for('farmer_login') }}" class="btn btn-outline-success btn-sm">
|
| 89 |
+
<i class="fas fa-sign-in-alt me-1"></i>Login Here
|
| 90 |
+
</a>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
{% endblock %}
|
| 98 |
+
|
| 99 |
+
{% block extra_js %}
|
| 100 |
+
<script>
|
| 101 |
+
// Format Aadhaar ID input
|
| 102 |
+
document.getElementById('aadhaar_id').addEventListener('input', function(e) {
|
| 103 |
+
let value = e.target.value.replace(/\D/g, '');
|
| 104 |
+
if (value.length > 12) {
|
| 105 |
+
value = value.slice(0, 12);
|
| 106 |
+
}
|
| 107 |
+
e.target.value = value;
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
// Format contact number input
|
| 111 |
+
document.getElementById('contact_number').addEventListener('input', function(e) {
|
| 112 |
+
let value = e.target.value.replace(/\D/g, '');
|
| 113 |
+
if (value.length > 10) {
|
| 114 |
+
value = value.slice(0, 10);
|
| 115 |
+
}
|
| 116 |
+
e.target.value = value;
|
| 117 |
+
});
|
| 118 |
+
|
| 119 |
+
// Password confirmation validation
|
| 120 |
+
document.getElementById('confirm_password').addEventListener('input', function(e) {
|
| 121 |
+
const password = document.getElementById('password').value;
|
| 122 |
+
const confirmPassword = e.target.value;
|
| 123 |
+
|
| 124 |
+
if (password !== confirmPassword) {
|
| 125 |
+
e.target.setCustomValidity('Passwords do not match');
|
| 126 |
+
} else {
|
| 127 |
+
e.target.setCustomValidity('');
|
| 128 |
+
}
|
| 129 |
+
});
|
| 130 |
+
</script>
|
| 131 |
+
{% endblock %}
|