NTEIGBANAM OKE commited on
Commit
2b56ea8
Β·
verified Β·
1 Parent(s): e60f59e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1311 -11
app.py CHANGED
@@ -1,21 +1,1321 @@
1
- πŸ” How Much Feed? - Results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  🎯 Quick Answer:
4
- ⭐ 16 bags of 25 kg feed is needed
5
- To raise 100 Ross 308 broilers to 8 weeks (56 days)
6
 
7
  🌾 Feed Breakdown:
8
- β€’ Starter: 4 bags
9
- β€’ Finisher: 12 bags
10
- β€’ TOTAL: 16 bags
11
 
12
  πŸ’° Feed Cost:
13
- ₦384,000
14
- (16 bags Γ— ₦24,000 per bag)
15
 
16
  πŸ“Š Expected Performance:
17
- β€’ Body weight / bird: 4.318 kg
18
- β€’ Flock live weight: 431.8 kg
19
 
20
  πŸ”— Calculate your feed needs:
21
- https://howmuchfeed.com
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import math
3
+ import re
4
+ import sqlite3
5
+ import json
6
+ import urllib.parse
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ import gradio as gr
11
+
12
+
13
+ # ============================================================
14
+ # APP CONFIG
15
+ # ============================================================
16
+
17
+ APP_NAME = "πŸ” HowMuchFeed"
18
+ DB_PATH = os.environ.get("DATABASE_PATH", "/data/howmuchfeed.db")
19
+
20
+ # Admin password - set this in Hugging Face Secrets
21
+ ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "admin123")
22
+
23
+ # Create directory if it doesn't exist
24
+ db_dir = os.path.dirname(DB_PATH)
25
+ if db_dir and not os.path.exists(db_dir):
26
+ os.makedirs(db_dir, exist_ok=True)
27
+
28
+ STARTER_END_DAY = 21
29
+
30
+ # ============================================================
31
+ # GOOGLE SHEETS INTEGRATION
32
+ # ============================================================
33
+
34
+ def init_google_sheets():
35
+ """Initialize Google Sheets connection."""
36
+ try:
37
+ import gspread
38
+ from oauth2client.service_account import ServiceAccountCredentials
39
+
40
+ creds_json = os.environ.get("GOOGLE_SHEETS_CREDENTIALS")
41
+ sheet_id = os.environ.get("GOOGLE_SHEETS_ID")
42
+
43
+ if not creds_json or not sheet_id:
44
+ print("⚠️ Google Sheets not configured. Using local database only.")
45
+ return None, None
46
+
47
+ creds_dict = json.loads(creds_json)
48
+ scope = [
49
+ "https://spreadsheets.google.com/feeds",
50
+ "https://www.googleapis.com/auth/drive"
51
+ ]
52
+
53
+ creds = ServiceAccountCredentials.from_json_keyfile_dict(creds_dict, scope)
54
+ client = gspread.authorize(creds)
55
+
56
+ try:
57
+ sheet = client.open_by_key(sheet_id)
58
+ print(f"βœ… Google Sheets connected: {sheet.title}")
59
+ except:
60
+ sheet = client.create("How Much Feed Calculator Leads")
61
+ print(f"βœ… New Google Sheet created")
62
+
63
+ return client, sheet
64
+
65
+ except Exception as e:
66
+ print(f"⚠️ Google Sheets error: {e}")
67
+ return None, None
68
+
69
+
70
+ # Initialize Google Sheets
71
+ gs_client, gs_sheet = init_google_sheets()
72
+
73
+
74
+ def sync_to_google_sheets(email, country, currency, breed, action="lead"):
75
+ """Sync data to Google Sheets."""
76
+ if not gs_client or not gs_sheet:
77
+ return False
78
+
79
+ try:
80
+ if action == "lead":
81
+ worksheet_name = "Leads"
82
+ headers = ["Email", "Country", "Currency", "Breed", "Created At"]
83
+ row_data = [email, country, currency, breed, datetime.now().isoformat()]
84
+ elif action == "question":
85
+ worksheet_name = "Questions"
86
+ headers = ["Email", "Question", "Breed", "Created At"]
87
+ row_data = [email, "Question", breed, datetime.now().isoformat()]
88
+ else:
89
+ return False
90
+
91
+ try:
92
+ worksheet = gs_sheet.worksheet(worksheet_name)
93
+ except:
94
+ worksheet = gs_sheet.add_worksheet(title=worksheet_name, rows=1000, cols=20)
95
+ worksheet.append_row(headers)
96
+
97
+ worksheet.append_row(row_data)
98
+ print(f"βœ… Synced to Google Sheets: {email}")
99
+ return True
100
+
101
+ except Exception as e:
102
+ print(f"⚠️ Google Sheets sync error: {e}")
103
+ return False
104
+
105
+
106
+ # ============================================================
107
+ # VERIFIED BREED DATA - From Official PDFs
108
+ # ============================================================
109
+
110
+ # Body Weight (kg) at key days - As-Hatched Performance
111
+ BREED_WEIGHT_DATA = {
112
+ "Ross 308": {
113
+ 0: 0.044, 7: 0.213, 14: 0.533, 21: 1.012,
114
+ 28: 1.616, 35: 2.296, 42: 2.998, 49: 3.681, 56: 4.318,
115
+ },
116
+ "Cobb 500": {
117
+ 0: 0.042, 7: 0.177, 14: 0.459, 21: 0.891,
118
+ 28: 1.436, 35: 2.067, 42: 2.732, 49: 3.369, 56: 3.958,
119
+ },
120
+ "Arbor Acres": {
121
+ 0: 0.044, 7: 0.209, 14: 0.527, 21: 1.006,
122
+ 28: 1.611, 35: 2.287, 42: 2.964, 49: 3.662, 56: 4.318,
123
+ }
124
+ }
125
+
126
+ # Cumulative Feed (kg) at key days - As-Hatched Performance
127
+ BREED_FEED_DATA = {
128
+ "Ross 308": {
129
+ 0: 0.000, 7: 0.166, 14: 0.535, 21: 1.155,
130
+ 28: 2.051, 35: 3.211, 42: 4.586, 49: 6.115, 56: 7.733,
131
+ },
132
+ "Cobb 500": {
133
+ 0: 0.000, 7: 0.150, 14: 0.465, 21: 1.053,
134
+ 28: 1.963, 35: 3.216, 42: 4.659, 49: 6.185, 56: 7.772,
135
+ },
136
+ "Arbor Acres": {
137
+ 0: 0.000, 7: 0.207, 14: 0.638, 21: 1.163,
138
+ 28: 2.069, 35: 3.237, 42: 4.625, 49: 6.111, 56: 7.647,
139
+ }
140
+ }
141
+
142
+ # Breed descriptions and availability
143
+ BREED_INFO = {
144
+ "Ross 308": {
145
+ "description": "Big chest, thick legs, very fast growth. Excellent for meat production.",
146
+ "available_at": "Agrited, Sayed, Valentine",
147
+ "color": "#16a34a"
148
+ },
149
+ "Cobb 500": {
150
+ "description": "Good early growth, strong legs, solid feed conversion.",
151
+ "available_at": "Chikun, Zartech",
152
+ "color": "#2563eb"
153
+ },
154
+ "Arbor Acres": {
155
+ "description": "Balanced growth, hardy and adaptable. Great survival rate.",
156
+ "available_at": "Supreme, CHI, GS, Vertex, Fidan, Cascada, Orobor, Sunchi, Farm Support, Ajila",
157
+ "color": "#d97706"
158
+ }
159
+ }
160
+
161
+
162
+ def get_breed_weight_data(breed):
163
+ return BREED_WEIGHT_DATA.get(breed, BREED_WEIGHT_DATA["Ross 308"])
164
+
165
+
166
+ def get_breed_feed_data(breed):
167
+ return BREED_FEED_DATA.get(breed, BREED_FEED_DATA["Ross 308"])
168
+
169
+
170
+ def interpolate_weight(day, breed):
171
+ """Interpolate body weight between known checkpoints."""
172
+ weight_data = get_breed_weight_data(breed)
173
+
174
+ if day <= 0:
175
+ return weight_data[0]
176
+ if day >= 56:
177
+ return weight_data[56]
178
+
179
+ keys = sorted(weight_data.keys())
180
+ for i in range(len(keys) - 1):
181
+ if keys[i] <= day <= keys[i + 1]:
182
+ ratio = (day - keys[i]) / (keys[i + 1] - keys[i])
183
+ return weight_data[keys[i]] + (weight_data[keys[i + 1]] - weight_data[keys[i]]) * ratio
184
+
185
+ return weight_data[56]
186
+
187
+
188
+ def interpolate_feed(day, breed):
189
+ """Interpolate cumulative feed between known checkpoints."""
190
+ feed_data = get_breed_feed_data(breed)
191
+
192
+ if day <= 0:
193
+ return 0.0
194
+ if day >= 56:
195
+ return feed_data[56]
196
+
197
+ keys = sorted(feed_data.keys())
198
+ for i in range(len(keys) - 1):
199
+ if keys[i] <= day <= keys[i + 1]:
200
+ ratio = (day - keys[i]) / (keys[i + 1] - keys[i])
201
+ return feed_data[keys[i]] + (feed_data[keys[i + 1]] - feed_data[keys[i]]) * ratio
202
+
203
+ return feed_data[56]
204
+
205
+
206
+ # ============================================================
207
+ # BENCHMARK TABLE GENERATOR
208
+ # ============================================================
209
+
210
+ def get_benchmark_table(breed):
211
+ """Generate benchmark table for the selected breed."""
212
+ weight_data = get_breed_weight_data(breed)
213
+ feed_data = get_breed_feed_data(breed)
214
+
215
+ days = [7, 14, 21, 28, 35, 42, 49, 56]
216
+
217
+ table_data = []
218
+ for day in days:
219
+ weight = weight_data.get(day, 0)
220
+ feed = feed_data.get(day, 0)
221
+ fcr = feed / weight if weight > 0 else 0
222
+ table_data.append([day, f"{weight:.3f}", f"{feed:.3f}", f"{fcr:.3f}"])
223
+
224
+ return table_data, days
225
+
226
+
227
+ def get_breed_comparison_table():
228
+ """Generate comparison table for all three breeds."""
229
+ breeds = ["Ross 308", "Cobb 500", "Arbor Acres"]
230
+ days = [7, 14, 21, 28, 35, 42, 49, 56]
231
+
232
+ table_data = []
233
+ for day in days:
234
+ row = [day]
235
+ for breed in breeds:
236
+ weight_data = get_breed_weight_data(breed)
237
+ feed_data = get_breed_feed_data(breed)
238
+ weight = weight_data.get(day, 0)
239
+ feed = feed_data.get(day, 0)
240
+ fcr = feed / weight if weight > 0 else 0
241
+ row.append(f"{weight:.3f}")
242
+ row.append(f"{feed:.3f}")
243
+ row.append(f"{fcr:.3f}")
244
+ table_data.append(row)
245
+
246
+ return table_data, breeds, days
247
+
248
+
249
+ # ============================================================
250
+ # CURRENCY DATA
251
+ # ============================================================
252
+
253
+ CURRENCIES = {
254
+ "NG": ("Nigeria", "₦", "NGN"),
255
+ "GH": ("Ghana", "β‚΅", "GHS"),
256
+ "KE": ("Kenya", "KSh", "KES"),
257
+ "UG": ("Uganda", "USh", "UGX"),
258
+ "TZ": ("Tanzania", "TSh", "TZS"),
259
+ "ZA": ("South Africa", "R", "ZAR"),
260
+ "ZM": ("Zambia", "ZK", "ZMW"),
261
+ "ZW": ("Zimbabwe", "ZiG", "ZWG"),
262
+ "CM": ("Cameroon", "FCFA", "XAF"),
263
+ "CI": ("CΓ΄te d'Ivoire", "CFA", "XOF"),
264
+ "SN": ("Senegal", "CFA", "XOF"),
265
+ "US": ("United States", "$", "USD"),
266
+ "GB": ("United Kingdom", "Β£", "GBP"),
267
+ "CA": ("Canada", "C$", "CAD"),
268
+ "AU": ("Australia", "A$", "AUD"),
269
+ "IN": ("India", "β‚Ή", "INR"),
270
+ "PK": ("Pakistan", "₨", "PKR"),
271
+ "BD": ("Bangladesh", "ΰ§³", "BDT"),
272
+ "BR": ("Brazil", "R$", "BRL"),
273
+ }
274
+
275
+ DEFAULT_PRICES = {
276
+ "NGN": 24000, "GHS": 300, "KES": 2200, "UGX": 100000,
277
+ "TZS": 30000, "ZAR": 300, "ZMW": 350, "USD": 25,
278
+ "GBP": 20, "EUR": 23, "INR": 2200, "PKR": 7000,
279
+ "BDT": 2500, "AUD": 35, "CAD": 35, "NZD": 40,
280
+ "BRL": 130, "XAF": 15000, "XOF": 15000, "ZWG": 350,
281
+ }
282
+
283
+
284
+ def detect_country_from_env():
285
+ """Best-effort country detection from environment variables."""
286
+ candidates = [
287
+ os.environ.get("COUNTRY_CODE"),
288
+ os.environ.get("CF-IPCOUNTRY"),
289
+ os.environ.get("GEOIP_COUNTRY"),
290
+ os.environ.get("HTTP_CF_IPCOUNTRY"),
291
+ ]
292
+
293
+ for value in candidates:
294
+ if value:
295
+ value = value.strip().upper()
296
+ if value in CURRENCIES:
297
+ return value
298
+
299
+ return None
300
+
301
+
302
+ def get_country_info(country_code):
303
+ """Get currency info for a country code."""
304
+ if country_code in CURRENCIES:
305
+ name, symbol, code = CURRENCIES[country_code]
306
+ return country_code, symbol, code
307
+ return "NG", "₦", "NGN"
308
+
309
+
310
+ def get_default_price(currency_code):
311
+ return DEFAULT_PRICES.get(currency_code, 24000)
312
+
313
+
314
+ COUNTRY_LIST = sorted([(code, f"{name} ({symbol})") for code, (name, symbol, curr) in CURRENCIES.items()])
315
+
316
+
317
+ # ============================================================
318
+ # DATABASE
319
+ # ============================================================
320
+
321
+ def init_db():
322
+ conn = sqlite3.connect(DB_PATH)
323
+ cur = conn.cursor()
324
+
325
+ cur.execute("""
326
+ CREATE TABLE IF NOT EXISTS leads (
327
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
328
+ email TEXT UNIQUE,
329
+ country TEXT,
330
+ currency TEXT,
331
+ breed TEXT,
332
+ created_at TEXT
333
+ )
334
+ """)
335
+
336
+ cur.execute("""
337
+ CREATE TABLE IF NOT EXISTS farmer_questions (
338
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
339
+ email TEXT,
340
+ question TEXT,
341
+ breed TEXT,
342
+ created_at TEXT
343
+ )
344
+ """)
345
+
346
+ cur.execute("""
347
+ CREATE TABLE IF NOT EXISTS calculations (
348
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
349
+ email TEXT,
350
+ country TEXT,
351
+ currency TEXT,
352
+ breed TEXT,
353
+ birds INTEGER,
354
+ weeks INTEGER,
355
+ days INTEGER,
356
+ total_days INTEGER,
357
+ bag_size REAL,
358
+ price_per_bag REAL,
359
+ total_feed_kg REAL,
360
+ starter_kg REAL,
361
+ finisher_kg REAL,
362
+ starter_bags REAL,
363
+ finisher_bags REAL,
364
+ total_bags REAL,
365
+ rounded_bags INTEGER,
366
+ total_cost REAL,
367
+ created_at TEXT
368
+ )
369
+ """)
370
+
371
+ cur.execute("""
372
+ CREATE TABLE IF NOT EXISTS analytics (
373
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
374
+ visit_date TEXT,
375
+ ip TEXT,
376
+ country TEXT,
377
+ breed TEXT,
378
+ action TEXT,
379
+ created_at TEXT
380
+ )
381
+ """)
382
+
383
+ conn.commit()
384
+ conn.close()
385
+ print(f"βœ… Database initialized at: {DB_PATH}")
386
+
387
+
388
+ init_db()
389
+
390
+
391
+ # ============================================================
392
+ # DATABASE HELPERS
393
+ # ============================================================
394
+
395
+ def valid_email(email):
396
+ if not email:
397
+ return False
398
+ email = email.strip()
399
+ pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$"
400
+ return bool(re.match(pattern, email))
401
+
402
+
403
+ def save_lead(email, country, currency, breed):
404
+ if not valid_email(email):
405
+ return
406
+
407
+ # Save to SQLite
408
+ conn = sqlite3.connect(DB_PATH)
409
+ cur = conn.cursor()
410
+ cur.execute("""
411
+ INSERT OR IGNORE INTO leads (email, country, currency, breed, created_at)
412
+ VALUES (?, ?, ?, ?, ?)
413
+ """, (email.strip().lower(), country, currency, breed, datetime.utcnow().isoformat()))
414
+ conn.commit()
415
+ conn.close()
416
+
417
+ # Sync to Google Sheets
418
+ try:
419
+ sync_to_google_sheets(email, country, currency, breed, "lead")
420
+ except Exception as e:
421
+ print(f"Google Sheets sync error: {e}")
422
+
423
+
424
+ def save_question(email, question, breed):
425
+ if not question or not question.strip():
426
+ return
427
+
428
+ # Save to SQLite
429
+ conn = sqlite3.connect(DB_PATH)
430
+ cur = conn.cursor()
431
+ cur.execute("""
432
+ INSERT INTO farmer_questions (email, question, breed, created_at)
433
+ VALUES (?, ?, ?, ?)
434
+ """, (email.strip().lower() if email else "", question.strip(), breed, datetime.utcnow().isoformat()))
435
+ conn.commit()
436
+ conn.close()
437
+
438
+ # Sync to Google Sheets
439
+ try:
440
+ sync_to_google_sheets(email, question, breed, "question")
441
+ except Exception as e:
442
+ print(f"Google Sheets sync error: {e}")
443
+
444
+
445
+ def save_calculation(email, country, currency, breed, birds, weeks, days, total_days,
446
+ bag_size, price_per_bag, total_feed_kg, starter_kg, finisher_kg,
447
+ starter_bags, finisher_bags, total_bags, rounded_bags, total_cost):
448
+ conn = sqlite3.connect(DB_PATH)
449
+ cur = conn.cursor()
450
+
451
+ cur.execute("""
452
+ INSERT INTO calculations (
453
+ email, country, currency, breed, birds, weeks, days, total_days,
454
+ bag_size, price_per_bag, total_feed_kg, starter_kg, finisher_kg,
455
+ starter_bags, finisher_bags, total_bags, rounded_bags, total_cost, created_at
456
+ )
457
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
458
+ """, (email.strip().lower() if email else "", country, currency, breed, birds, weeks, days, total_days,
459
+ bag_size, price_per_bag, total_feed_kg, starter_kg, finisher_kg,
460
+ starter_bags, finisher_bags, total_bags, rounded_bags, total_cost,
461
+ datetime.utcnow().isoformat()))
462
+
463
+ conn.commit()
464
+ conn.close()
465
+
466
+
467
+ def track_visit(request: gr.Request):
468
+ """Track page visit."""
469
+ try:
470
+ client_ip = request.client.host if request and hasattr(request, 'client') else "unknown"
471
+
472
+ conn = sqlite3.connect(DB_PATH)
473
+ cur = conn.cursor()
474
+
475
+ cur.execute("""
476
+ INSERT INTO analytics (visit_date, ip, country, action, created_at)
477
+ VALUES (?, ?, ?, ?, ?)
478
+ """, (
479
+ datetime.now().strftime("%Y-%m-%d"),
480
+ client_ip,
481
+ detect_country_from_env() or "NG",
482
+ "page_visit",
483
+ datetime.now().isoformat()
484
+ ))
485
+
486
+ conn.commit()
487
+ conn.close()
488
+ return True
489
+ except Exception as e:
490
+ print(f"Analytics error: {e}")
491
+ return False
492
+
493
+
494
+ def track_calculation(breed, birds, days):
495
+ """Track when a calculation is performed."""
496
+ try:
497
+ conn = sqlite3.connect(DB_PATH)
498
+ cur = conn.cursor()
499
+
500
+ cur.execute("""
501
+ INSERT INTO analytics (visit_date, country, breed, action, created_at)
502
+ VALUES (?, ?, ?, ?, ?)
503
+ """, (
504
+ datetime.now().strftime("%Y-%m-%d"),
505
+ detect_country_from_env() or "NG",
506
+ breed,
507
+ f"calculation_{birds}_{days}",
508
+ datetime.now().isoformat()
509
+ ))
510
+
511
+ conn.commit()
512
+ conn.close()
513
+ return True
514
+ except:
515
+ return False
516
+
517
+
518
+ def get_analytics_summary():
519
+ """Get analytics summary."""
520
+ conn = sqlite3.connect(DB_PATH)
521
+ cur = conn.cursor()
522
+
523
+ cur.execute("SELECT COUNT(*) FROM analytics WHERE action = 'page_visit'")
524
+ total_visits = cur.fetchone()[0] or 0
525
+
526
+ cur.execute("SELECT COUNT(*) FROM analytics WHERE action = 'page_visit' AND visit_date = ?",
527
+ (datetime.now().strftime("%Y-%m-%d"),))
528
+ today_visits = cur.fetchone()[0] or 0
529
+
530
+ cur.execute("SELECT COUNT(*) FROM analytics WHERE action LIKE 'calculation_%'")
531
+ total_calculations = cur.fetchone()[0] or 0
532
+
533
+ cur.execute("SELECT COUNT(DISTINCT ip) FROM analytics WHERE action = 'page_visit' AND ip != 'unknown'")
534
+ unique_visitors = cur.fetchone()[0] or 0
535
+
536
+ cur.execute("SELECT breed, COUNT(*) FROM analytics WHERE action LIKE 'calculation_%' GROUP BY breed")
537
+ breed_stats = cur.fetchall()
538
+
539
+ # Total leads
540
+ cur.execute("SELECT COUNT(*) FROM leads")
541
+ total_leads = cur.fetchone()[0] or 0
542
+
543
+ conn.close()
544
+
545
+ return {
546
+ "total_visits": total_visits,
547
+ "today_visits": today_visits,
548
+ "unique_visitors": unique_visitors,
549
+ "total_calculations": total_calculations,
550
+ "total_leads": total_leads,
551
+ "breed_stats": breed_stats
552
+ }
553
+
554
+
555
+ # ============================================================
556
+ # WHATSAPP SHARE LINK GENERATOR
557
+ # ============================================================
558
+
559
+ def get_whatsapp_link(birds, breed, weeks, days, total_days, bag_size, rounded_bags,
560
+ starter_bags, finisher_bags, total_cost, price_per_bag,
561
+ currency_symbol, expected_weight, flock_weight, app_url="https://howmuchfeed.com"):
562
+ """Generate WhatsApp share link with calculation results."""
563
+
564
+ # Format the message
565
+ message = f"""πŸ” How Much Feed? - Results
566
 
567
  🎯 Quick Answer:
568
+ ⭐ {rounded_bags} bags of {bag_size} kg feed is needed
569
+ To raise {birds:,} {breed} broilers to {weeks} weeks{f" + {days} days" if days > 0 else ""} ({total_days} days)
570
 
571
  🌾 Feed Breakdown:
572
+ β€’ Starter: {starter_bags:.0f} bags
573
+ β€’ Finisher: {finisher_bags:.0f} bags
574
+ β€’ TOTAL: {rounded_bags} bags
575
 
576
  πŸ’° Feed Cost:
577
+ {currency_symbol}{total_cost:,.0f}
578
+ ({rounded_bags} bags Γ— {currency_symbol}{price_per_bag:,.0f} per bag)
579
 
580
  πŸ“Š Expected Performance:
581
+ β€’ Body weight / bird: {expected_weight:.3f} kg
582
+ β€’ Flock live weight: {flock_weight:,.1f} kg
583
 
584
  πŸ”— Calculate your feed needs:
585
+ {app_url}"""
586
+
587
+ # URL encode
588
+ encoded = urllib.parse.quote(message)
589
+
590
+ # WhatsApp link
591
+ return f"https://wa.me/?text={encoded}"
592
+
593
+
594
+ # ============================================================
595
+ # FEED CALCULATION
596
+ # ============================================================
597
+
598
+ def calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_code, breed, request: gr.Request):
599
+ # Validation
600
+ try:
601
+ birds = int(birds)
602
+ weeks = int(weeks)
603
+ days = int(days)
604
+ bag_size = float(bag_size)
605
+ price_per_bag = float(price_per_bag)
606
+ except Exception:
607
+ return "⚠️ Please enter valid numbers.", ""
608
+
609
+ if birds <= 0:
610
+ return "⚠️ Number of birds must be greater than zero.", ""
611
+ if weeks < 0:
612
+ return "⚠️ Weeks cannot be negative.", ""
613
+ if days < 0 or days > 6:
614
+ return "⚠️ Days must be between 0 and 6.", ""
615
+ if bag_size <= 0:
616
+ return "⚠️ Feed bag size must be greater than zero.", ""
617
+ if price_per_bag < 0:
618
+ return "⚠️ Price per bag cannot be negative.", ""
619
+
620
+ if not valid_email(email):
621
+ return "πŸ“§ **Email required**\n\nPlease enter your email address before calculating.", ""
622
+
623
+ # Country / Currency
624
+ country_name, symbol, currency_code = get_country_info(country_code)
625
+ currency_symbol = symbol
626
+
627
+ # Save lead with breed
628
+ save_lead(email, country_code, currency_code, breed)
629
+
630
+ # Track calculation
631
+ track_calculation(breed, birds, (weeks * 7 + days))
632
+
633
+ # Total days
634
+ total_days = (weeks * 7) + days
635
+
636
+ # Total feed
637
+ total_feed_per_bird = interpolate_feed(total_days, breed)
638
+ total_feed_kg = birds * total_feed_per_bird
639
+
640
+ # Starter / Finisher
641
+ starter_days = min(total_days, STARTER_END_DAY)
642
+ starter_per_bird = interpolate_feed(starter_days, breed)
643
+ starter_kg = birds * starter_per_bird
644
+ finisher_kg = max(total_feed_kg - starter_kg, 0)
645
+
646
+ # Bags
647
+ starter_bags = starter_kg / bag_size
648
+ finisher_bags = finisher_kg / bag_size
649
+ total_bags = total_feed_kg / bag_size
650
+ rounded_bags = math.ceil(total_bags)
651
+ total_cost = rounded_bags * price_per_bag
652
+
653
+ # Expected performance
654
+ expected_weight = interpolate_weight(total_days, breed)
655
+ flock_weight = expected_weight * birds
656
+
657
+ # Save calculation
658
+ save_calculation(email, country_code, currency_code, breed, birds, weeks, days, total_days,
659
+ bag_size, price_per_bag, total_feed_kg, starter_kg, finisher_kg,
660
+ starter_bags, finisher_bags, total_bags, rounded_bags, total_cost)
661
+
662
+ # Breed info
663
+ breed_info = BREED_INFO.get(breed, BREED_INFO["Ross 308"])
664
+ breed_color = breed_info["color"]
665
+
666
+ # Generate WhatsApp link
667
+ whatsapp_link = get_whatsapp_link(
668
+ birds, breed, weeks, days, total_days, bag_size, rounded_bags,
669
+ starter_bags, finisher_bags, total_cost, price_per_bag,
670
+ currency_symbol, expected_weight, flock_weight
671
+ )
672
+
673
+ # Output HTML
674
+ starter_bags_display = f"{starter_bags:.1f}"
675
+ finisher_bags_display = f"{finisher_bags:.1f}"
676
+
677
+ age_text = f"{weeks} week{'s' if weeks != 1 else ''}"
678
+ if days:
679
+ age_text += f" + {days} day{'s' if days != 1 else ''}"
680
+
681
+ html = f"""
682
+ <div style="font-size: 17px; line-height: 1.6;">
683
+
684
+ <div style="background: #f0fdf4; padding: 18px 22px; border-radius: 14px; border-left: 6px solid #22c55e; margin-bottom: 20px;">
685
+ <div style="font-size: 13px; color: #16a34a; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px;">🎯 Quick Answer</div>
686
+ <div style="font-size: 28px; font-weight: 800; color: #166534; margin: 6px 0 4px 0;">
687
+ ⭐ {rounded_bags} bags of {bag_size:g} kg feed is needed
688
+ </div>
689
+ <div style="color: #374151; font-size: 16px;">
690
+ To raise <strong>{birds:,}</strong> {breed} broilers to <strong>{age_text} ({total_days} days)</strong>
691
+ </div>
692
+ <div style="margin-top: 8px; font-size: 13px;">
693
+ <span style="background: {breed_color}; color: white; padding: 3px 12px; border-radius: 14px; font-weight: 600;">{breed}</span>
694
+ <span style="background: #6b7280; color: white; padding: 3px 12px; border-radius: 14px; font-weight: 600; margin-left: 8px;">{country_name}</span>
695
+ </div>
696
+ </div>
697
+
698
+ <div style="margin-bottom: 16px;">
699
+ <div style="font-size: 13px; color: #6b7280; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;">🌾 Feed Breakdown</div>
700
+ <table style="width: 100%; border-collapse: collapse; margin-top: 6px; font-size: 15px;">
701
+ <tr style="border-bottom: 1px solid #e5e7eb;">
702
+ <td style="padding: 8px 4px;"><strong>🌱 Starter</strong></td>
703
+ <td style="padding: 8px 4px; text-align: right;">{starter_kg:,.1f} kg</td>
704
+ <td style="padding: 8px 4px; text-align: right; font-weight: 600;">{starter_bags_display} bags</td>
705
+ </tr>
706
+ <tr style="border-bottom: 1px solid #e5e7eb;">
707
+ <td style="padding: 8px 4px;"><strong>🌽 Finisher</strong></td>
708
+ <td style="padding: 8px 4px; text-align: right;">{finisher_kg:,.1f} kg</td>
709
+ <td style="padding: 8px 4px; text-align: right; font-weight: 600;">{finisher_bags_display} bags</td>
710
+ </tr>
711
+ <tr>
712
+ <td style="padding: 8px 4px; font-weight: 700;">TOTAL</td>
713
+ <td style="padding: 8px 4px; text-align: right; font-weight: 700;">{total_feed_kg:,.1f} kg</td>
714
+ <td style="padding: 8px 4px; text-align: right; font-weight: 700;">{total_bags:.1f}</td>
715
+ </tr>
716
+ </table>
717
+ </div>
718
+
719
+ <div style="margin-bottom: 16px; background: #eff6ff; padding: 14px 18px; border-radius: 10px; border-left: 4px solid #3b82f6;">
720
+ <div style="font-size: 13px; color: #6b7280; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;">πŸ’° Feed Cost</div>
721
+ <div style="font-size: 30px; font-weight: 800; color: #1e40af;">
722
+ {currency_symbol}{total_cost:,.0f}
723
+ </div>
724
+ <div style="color: #6b7280; font-size: 14px;">
725
+ {rounded_bags} bags Γ— {currency_symbol}{price_per_bag:,.0f} per bag
726
+ </div>
727
+ </div>
728
+
729
+ <div style="border-top: 1px solid #e5e7eb; padding-top: 14px;">
730
+ <div style="font-size: 13px; color: #6b7280; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;">πŸ“Š Expected Performance</div>
731
+ <table style="width: 100%; border-collapse: collapse; margin-top: 6px; font-size: 15px;">
732
+ <tr style="border-bottom: 1px solid #e5e7eb;">
733
+ <td style="padding: 6px 4px;">Body weight / bird</td>
734
+ <td style="padding: 6px 4px; text-align: right; font-weight: 600;">{expected_weight:.3f} kg</td>
735
+ </tr>
736
+ <tr>
737
+ <td style="padding: 6px 4px;">Flock live weight</td>
738
+ <td style="padding: 6px 4px; text-align: right; font-weight: 600;">{flock_weight:,.1f} kg</td>
739
+ </tr>
740
+ </table>
741
+ </div>
742
+
743
+ <div style="margin-top: 16px; border-top: 1px solid #e5e7eb; padding-top: 14px;">
744
+ <div style="display: flex; gap: 10px; flex-wrap: wrap;">
745
+ <a href="{whatsapp_link}" target="_blank" style="text-decoration: none;">
746
+ <button style="background: #25D366; color: white; padding: 10px 18px; border: none; border-radius: 8px; cursor: pointer; font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 8px;">
747
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="white">
748
+ <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/>
749
+ </svg>
750
+ Share on WhatsApp
751
+ </button>
752
+ </a>
753
+ <button onclick="window.print()" style="background: #6b7280; color: white; padding: 10px 18px; border: none; border-radius: 8px; cursor: pointer; font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 8px;">
754
+ πŸ–¨οΈ Print Results
755
+ </button>
756
+ </div>
757
+ </div>
758
+
759
+ <div style="margin-top: 14px; font-size: 12px; color: #9ca3af; border-top: 1px solid #f3f4f6; padding-top: 10px;">
760
+ ⚠️ Estimate based on {breed} benchmark. Actual results may vary.
761
+ </div>
762
+ </div>
763
+ """
764
+
765
+ return html, f"βœ… Calculation saved for {email.strip().lower()}."
766
+
767
+
768
+ # ============================================================
769
+ # QUESTION / ANSWER
770
+ # ============================================================
771
+
772
+ def answer_farmer_question(question, email, breed):
773
+ if not question or not question.strip():
774
+ return "Please type or speak your question first."
775
+
776
+ save_question(email or "", question, breed)
777
+
778
+ q = question.lower().strip()
779
+
780
+ if "bag" in q and ("feed" in q or "birds" in q):
781
+ answer = """
782
+ ### πŸ” Feed-bag questions
783
+
784
+ For an exact feed estimate, use the calculator above.
785
+
786
+ Enter:
787
+ 1. Number of birds
788
+ 2. Age in weeks + days
789
+ 3. Feed bag size
790
+ 4. Price per bag
791
+ 5. Your email
792
+ 6. Breed
793
+
794
+ The calculator will give you the estimated total bags, starter/finisher breakdown and feed cost.
795
+ """
796
+ elif "breed" in q or "ross" in q or "cobb" in q or "arbor" in q:
797
+ answer = """
798
+ ### πŸ” Common Broiler Breeds in Nigeria
799
+
800
+ | Breed | Characteristics | Available At |
801
+ |-------|-----------------|--------------|
802
+ | **Ross 308** | Big chest, thick legs, very fast growth | Agrited, Sayed, Valentine |
803
+ | **Cobb 500** | Good early growth, strong legs | Chikun, Zartech |
804
+ | **Arbor Acres** | Balanced growth, hardy, adaptable | Supreme, CHI, GS, Vertex, Fidan, Cascada, Orobor, Sunchi, Farm Support, Ajila |
805
+
806
+ πŸ’‘ **Tip:** Before buying chicks, ask about the breed and hatchery. Not all broilers are the same!
807
+ """
808
+ elif "starter" in q:
809
+ answer = f"""
810
+ ### 🌾 Starter feed
811
+
812
+ Starter feed is used during the early growth period (Day 0–{STARTER_END_DAY}).
813
+
814
+ The calculator separates the estimated feed requirement into **Starter** and **Finisher** based on the selected age.
815
+ """
816
+ elif "finisher" in q:
817
+ answer = f"""
818
+ ### 🌾 Finisher feed
819
+
820
+ Finisher feed is used after day {STARTER_END_DAY}.
821
+
822
+ Your calculator automatically estimates the finisher requirement from the selected number of birds and total age.
823
+ """
824
+ elif "cost" in q or "price" in q:
825
+ answer = """
826
+ ### πŸ’° Feed cost
827
+
828
+ Enter the current price of one feed bag in the calculator.
829
+
830
+ The app calculates: **Number of bags Γ— price per bag = estimated feed cost**
831
+
832
+ The result is for **feed alone**.
833
+ """
834
+ else:
835
+ answer = f"""
836
+ ### πŸ’‘ Question received
837
+
838
+ **β€œ{question.strip()}”**
839
+
840
+ Your question has been saved.
841
+
842
+ We can use the questions farmers submit to decide which answers, calculators and features should be added to the app next.
843
+ """
844
+
845
+ return answer
846
+
847
+
848
+ # ============================================================
849
+ # UPDATE FUNCTIONS
850
+ # ============================================================
851
+
852
+ def update_benchmark(breed):
853
+ """Update the benchmark table when breed changes."""
854
+ table_data, days = get_benchmark_table(breed)
855
+ return table_data
856
+
857
+
858
+ def on_country_change_handler(country_selection):
859
+ """Update price and currency when country changes."""
860
+ if country_selection == "Auto-Detect":
861
+ country_code = detect_country_from_env() or "NG"
862
+ else:
863
+ country_code = country_selection
864
+
865
+ _, symbol, currency_code = get_country_info(country_code)
866
+ default_price = get_default_price(currency_code)
867
+ return country_code, default_price, symbol
868
+
869
+
870
+ # ============================================================
871
+ # ADMIN FUNCTIONS
872
+ # ============================================================
873
+
874
+ def show_analytics(password):
875
+ """Display analytics summary - only with correct password."""
876
+ if password != ADMIN_PASSWORD:
877
+ return """
878
+ ### πŸ”’ Admin Access Required
879
+
880
+ Please enter the correct admin password to view analytics.
881
+ """
882
+
883
+ stats = get_analytics_summary()
884
+
885
+ breed_stats_html = ""
886
+ for breed, count in stats["breed_stats"]:
887
+ breed_stats_html += f"<li><strong>{breed}</strong>: {count} calculations</li>"
888
+
889
+ if not breed_stats_html:
890
+ breed_stats_html = "<li>No calculations yet</li>"
891
+
892
+ return f"""
893
+ ### πŸ“Š How Much Feed? - Analytics
894
+
895
+ | Metric | Value |
896
+ |--------|-------|
897
+ | **Total Visits** | **{stats['total_visits']:,}** |
898
+ | **Today's Visits** | **{stats['today_visits']}** |
899
+ | **Unique Visitors** | **{stats['unique_visitors']:,}** |
900
+ | **Total Calculations** | **{stats['total_calculations']:,}** |
901
+ | **Total Leads (Emails)** | **{stats['total_leads']:,}** |
902
+
903
+ ### πŸ” Most Popular Breeds
904
+
905
+ <ul>{breed_stats_html}</ul>
906
+
907
+ ---
908
+ *Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
909
+ """
910
+
911
+
912
+ def admin_login(password):
913
+ """Check admin password."""
914
+ if password == ADMIN_PASSWORD:
915
+ return show_analytics(password), gr.update(visible=True), gr.update(visible=False)
916
+ else:
917
+ return "❌ Incorrect password. Please try again.", gr.update(visible=False), gr.update(visible=True)
918
+
919
+
920
+ def logout_admin():
921
+ """Logout and hide analytics."""
922
+ return "πŸ”’ Enter password to view analytics.", gr.update(visible=True), gr.update(visible=False)
923
+
924
+
925
+ def export_leads_to_csv():
926
+ """Export all leads to CSV format."""
927
+ conn = sqlite3.connect(DB_PATH)
928
+ cur = conn.cursor()
929
+ cur.execute("SELECT email, country, currency, breed, created_at FROM leads ORDER BY created_at DESC")
930
+ rows = cur.fetchall()
931
+ conn.close()
932
+
933
+ import csv
934
+ from io import StringIO
935
+
936
+ output = StringIO()
937
+ writer = csv.writer(output)
938
+ writer.writerow(["Email", "Country", "Currency", "Breed", "Created At"])
939
+ writer.writerows(rows)
940
+
941
+ return output.getvalue()
942
+
943
+
944
+ def export_leads_wrapper():
945
+ """Export leads to CSV file for download."""
946
+ import tempfile
947
+ csv_data = export_leads_to_csv()
948
+ temp = tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False, encoding='utf-8')
949
+ temp.write(csv_data)
950
+ temp.close()
951
+ return temp.name
952
+
953
+
954
+ def export_calculations_wrapper():
955
+ """Export calculations to CSV file for download."""
956
+ conn = sqlite3.connect(DB_PATH)
957
+ cur = conn.cursor()
958
+ cur.execute("""
959
+ SELECT email, breed, birds, total_days, rounded_bags, bag_size, total_cost, created_at
960
+ FROM calculations
961
+ ORDER BY created_at DESC
962
+ """)
963
+ rows = cur.fetchall()
964
+ conn.close()
965
+
966
+ import csv
967
+ import tempfile
968
+
969
+ temp = tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False, encoding='utf-8')
970
+ writer = csv.writer(temp)
971
+ writer.writerow(["Email", "Breed", "Birds", "Total Days", "Bags", "Bag Size (kg)", "Total Cost", "Created At"])
972
+ writer.writerows(rows)
973
+ temp.close()
974
+
975
+ return temp.name
976
+
977
+
978
+ # ============================================================
979
+ # CSS
980
+ # ============================================================
981
+
982
+ CSS = """
983
+ body { background: #f6f8f4; }
984
+ .gradio-container { max-width: 1200px !important; margin: auto !important; }
985
+ .main-title { text-align: center; padding: 15px 10px 5px 10px; }
986
+ .main-title h1 { font-size: 34px; margin-bottom: 4px; }
987
+ .main-title p { font-size: 17px; color: #555; }
988
+ .answer-box { background: white; border-radius: 16px; padding: 16px 20px; border: 1px solid #e5e7eb; box-shadow: 0 1px 3px rgba(0,0,0,0.05); min-height: 400px; }
989
+ .email-box { border: 2px solid #d9e7d2; border-radius: 12px; padding: 10px; background: #fbfdf9; }
990
+ button { min-height: 48px !important; }
991
+ .admin-login { border: 2px solid #d97706; border-radius: 12px; padding: 15px; background: #fffbeb; }
992
+ .whatsapp-btn { background: #25D366; color: white; padding: 10px 18px; border: none; border-radius: 8px; cursor: pointer; font-size: 15px; font-weight: 600; }
993
+ .print-btn { background: #6b7280; color: white; padding: 10px 18px; border: none; border-radius: 8px; cursor: pointer; font-size: 15px; font-weight: 600; }
994
+ """
995
+
996
+
997
+ # ============================================================
998
+ # UI
999
+ # ============================================================
1000
+
1001
+ with gr.Blocks(title=APP_NAME) as demo:
1002
+
1003
+ gr.HTML("""
1004
+ <div class="main-title">
1005
+ <h1>πŸ” How Much Feed?</h1>
1006
+ <p>How many bags of feed for broilers for 5, 6, 7, or 8 weeks? Get accurate feed needed and estimated cost for your birds.</p>
1007
+ </div>
1008
+ """)
1009
+
1010
+ with gr.Row(equal_height=True):
1011
+
1012
+ # LEFT COLUMN: INPUTS
1013
+ with gr.Column(scale=1, min_width=350):
1014
+
1015
+ with gr.Group():
1016
+ gr.Markdown("## πŸ” Breed & Birds")
1017
+
1018
+ breed = gr.Dropdown(
1019
+ choices=["Ross 308", "Cobb 500", "Arbor Acres"],
1020
+ value="Ross 308",
1021
+ label="Breed",
1022
+ info="Select your broiler breed for accurate estimates",
1023
+ )
1024
+
1025
+ birds = gr.Dropdown(
1026
+ choices=[25, 50, 75, 100, 150, 200, 500, 1000],
1027
+ value=50,
1028
+ label="Number of birds",
1029
+ allow_custom_value=True,
1030
+ )
1031
+
1032
+ gr.Markdown("## πŸ“… Expected Age of Birds")
1033
+
1034
+ with gr.Row():
1035
+ weeks = gr.Dropdown(
1036
+ choices=list(range(0, 21)),
1037
+ value=8,
1038
+ label="Weeks",
1039
+ )
1040
+ days = gr.Dropdown(
1041
+ choices=list(range(0, 7)),
1042
+ value=0,
1043
+ label="+ Days",
1044
+ )
1045
+
1046
+ gr.Markdown("## 🌾 Feed")
1047
+
1048
+ with gr.Row():
1049
+ bag_size = gr.Dropdown(
1050
+ choices=[10, 20, 25, 50, 100],
1051
+ value=25,
1052
+ label="Bag size (kg)",
1053
+ )
1054
+ price = gr.Number(
1055
+ value=24000,
1056
+ label=f"Price per bag ",
1057
+ minimum=0,
1058
+ )
1059
+
1060
+ gr.Markdown("## 🌍 Country & Currency")
1061
+
1062
+ detected_country = detect_country_from_env() or "NG"
1063
+ country_choices = ["Auto-Detect"] + [code for code, _ in COUNTRY_LIST]
1064
+
1065
+ country_dropdown = gr.Dropdown(
1066
+ choices=country_choices,
1067
+ value="Auto-Detect",
1068
+ label="Select your country (Auto-Detect uses your IP)",
1069
+ )
1070
+
1071
+ country_code_state = gr.State(detected_country)
1072
+
1073
+ gr.Markdown("## πŸ“§ Your email")
1074
+
1075
+ email = gr.Textbox(
1076
+ label="",
1077
+ placeholder="farmer@example.com",
1078
+ type="email",
1079
+ elem_classes=["email-box"],
1080
+ info="Required β€” we'll send you future updates",
1081
+ )
1082
+
1083
+ calculate_btn = gr.Button(
1084
+ "πŸ” Calculate Feed Requirement",
1085
+ variant="primary",
1086
+ size="lg",
1087
+ )
1088
+
1089
+ save_status = gr.Markdown("")
1090
+
1091
+ # RIGHT COLUMN: RESULTS
1092
+ with gr.Column(scale=1, min_width=350):
1093
+
1094
+ gr.Markdown("## 🎯 Quick Answer")
1095
+
1096
+ result = gr.HTML(
1097
+ value="""
1098
+ <div style="font-size: 16px; color: #9ca3af; text-align: center; padding: 40px 20px;">
1099
+ <div style="font-size: 48px; margin-bottom: 12px;">πŸ”</div>
1100
+ <div>Select your breed, enter details and click <strong>Calculate</strong></div>
1101
+ <div style="font-size: 14px; margin-top: 8px;">Your feed estimate will appear here</div>
1102
+ </div>
1103
+ """,
1104
+ elem_classes=["answer-box"],
1105
+ )
1106
+
1107
+ # ============================================================
1108
+ # BENCHMARK SECTION
1109
+ # ============================================================
1110
+
1111
+ gr.Markdown("---")
1112
+
1113
+ with gr.Tabs():
1114
+ with gr.TabItem("πŸ“Š Selected Breed Benchmark"):
1115
+ gr.Markdown("### πŸ“Š Weekly Benchmark for Selected Breed")
1116
+ initial_table, _ = get_benchmark_table("Ross 308")
1117
+
1118
+ benchmark_table = gr.Dataframe(
1119
+ value=initial_table,
1120
+ headers=["Days", "Body Weight (kg)", "Cumulative Feed (kg)", "FCR"],
1121
+ interactive=False,
1122
+ wrap=True,
1123
+ )
1124
+
1125
+ with gr.TabItem("πŸ“ˆ Breed Comparison"):
1126
+ gr.Markdown("### πŸ“Š Breed Comparison (As-Hatched Performance)")
1127
+ comp_data, breeds_list, days_list = get_breed_comparison_table()
1128
+
1129
+ headers = ["Days"]
1130
+ for b in breeds_list:
1131
+ headers.append(f"{b} Weight (kg)")
1132
+ headers.append(f"{b} Feed (kg)")
1133
+ headers.append(f"{b} FCR")
1134
+
1135
+ comparison_table = gr.Dataframe(
1136
+ value=comp_data,
1137
+ headers=headers,
1138
+ interactive=False,
1139
+ wrap=True,
1140
+ )
1141
+
1142
+ # ============================================================
1143
+ # ADMIN-ONLY ANALYTICS TAB
1144
+ # ============================================================
1145
+
1146
+ with gr.TabItem("πŸ”’ Admin"):
1147
+ gr.Markdown("### πŸ”’ Admin Dashboard")
1148
+
1149
+ with gr.Group(elem_classes="admin-login"):
1150
+ gr.Markdown("Enter admin password to view analytics and download data.")
1151
+
1152
+ with gr.Row():
1153
+ admin_password_input = gr.Textbox(
1154
+ label="Admin Password",
1155
+ placeholder="Enter password...",
1156
+ type="password",
1157
+ scale=2,
1158
+ )
1159
+ login_btn = gr.Button("Login", variant="primary", scale=1)
1160
+ logout_btn = gr.Button("Logout", variant="secondary", scale=1)
1161
+
1162
+ # Analytics output
1163
+ admin_output = gr.Markdown("πŸ”’ Enter password to view analytics.")
1164
+
1165
+ # Export buttons
1166
+ with gr.Row():
1167
+ export_leads_btn = gr.Button("πŸ“₯ Export Leads (CSV)", variant="secondary")
1168
+ export_leads_output = gr.File(label="Download Leads CSV")
1169
+
1170
+ with gr.Row():
1171
+ export_calc_btn = gr.Button("πŸ“₯ Export Calculations (CSV)", variant="secondary")
1172
+ export_calc_output = gr.File(label="Download Calculations CSV")
1173
+
1174
+ # Login/Logout events
1175
+ login_btn.click(
1176
+ fn=admin_login,
1177
+ inputs=[admin_password_input],
1178
+ outputs=[admin_output, admin_password_input, logout_btn],
1179
+ )
1180
+
1181
+ logout_btn.click(
1182
+ fn=logout_admin,
1183
+ inputs=[],
1184
+ outputs=[admin_output, admin_password_input, logout_btn],
1185
+ )
1186
+
1187
+ # Export events
1188
+ export_leads_btn.click(
1189
+ fn=export_leads_wrapper,
1190
+ inputs=[],
1191
+ outputs=[export_leads_output],
1192
+ )
1193
+
1194
+ export_calc_btn.click(
1195
+ fn=export_calculations_wrapper,
1196
+ inputs=[],
1197
+ outputs=[export_calc_output],
1198
+ )
1199
+
1200
+ # ============================================================
1201
+ # BREED REFERENCE
1202
+ # ============================================================
1203
+
1204
+ gr.Markdown("---")
1205
+
1206
+ gr.Markdown("""
1207
+ ### πŸ” Common Broiler Breeds in Nigeria
1208
+
1209
+ | Breed | Characteristics | Available At |
1210
+ |-------|-----------------|--------------|
1211
+ | **Ross 308** | Big chest, thick legs, very fast growth | Agrited, Sayed, Valentine |
1212
+ | **Cobb 500** | Good early growth, strong legs | Chikun, Zartech |
1213
+ | **Arbor Acres** | Balanced growth, hardy, adaptable | Supreme, CHI, GS, Vertex, Fidan, Cascada, Orobor, Sunchi, Farm Support, Ajila |
1214
+
1215
+ πŸ’‘ **Tip:** Before buying chicks, ask about the breed and hatchery. Not all broilers are the same!
1216
+ """)
1217
+
1218
+ # ============================================================
1219
+ # HOW IT WORKS
1220
+ # ============================================================
1221
+
1222
+ gr.Markdown("---")
1223
+
1224
+ with gr.Row():
1225
+ with gr.Column(scale=1):
1226
+ gr.Markdown("""
1227
+ ### πŸ“Œ How it works
1228
+
1229
+ - **Weeks + Days** β†’ total days
1230
+ - **Starter** β†’ Day 0–21
1231
+ - **Finisher** β†’ Day 22+
1232
+ - **Bags** β†’ rounded up to next whole bag
1233
+ - **Breed-specific benchmarks** used for estimates
1234
+
1235
+ ⚠️ Actual results may vary with management, feed quality, and other conditions.
1236
+ """)
1237
+
1238
+ with gr.Column(scale=1):
1239
+ gr.Markdown("""
1240
+ ### πŸ“Š Data Sources
1241
+
1242
+ - **Ross 308** β€” Aviagen (2022)
1243
+ - **Cobb 500** β€” Cobb-Vantress
1244
+ - **Arbor Acres** β€” Aviagen (2022)
1245
+
1246
+ *All data from official breed performance objectives.*
1247
+ """)
1248
+
1249
+ # ============================================================
1250
+ # QUESTION SECTION
1251
+ # ============================================================
1252
+
1253
+ gr.Markdown("---")
1254
+
1255
+ with gr.Row():
1256
+ with gr.Column(scale=2):
1257
+ gr.Markdown("""
1258
+ # ❓ Ask a question
1259
+ ### What would you like to know about raising your birds?
1260
+ """)
1261
+
1262
+ question = gr.Textbox(
1263
+ label="",
1264
+ placeholder="e.g. How many bags of feed for 100 broilers for 6 weeks?",
1265
+ lines=3,
1266
+ )
1267
+
1268
+ ask_btn = gr.Button("πŸ’¬ Get Answer", variant="secondary", size="lg")
1269
+ question_answer = gr.Markdown("")
1270
+
1271
+ # ============================================================
1272
+ # EVENTS
1273
+ # ============================================================
1274
+
1275
+ # Update benchmark table when breed changes
1276
+ breed.change(
1277
+ fn=update_benchmark,
1278
+ inputs=[breed],
1279
+ outputs=[benchmark_table],
1280
+ )
1281
+
1282
+ # Update price and currency when country changes
1283
+ country_dropdown.change(
1284
+ fn=on_country_change_handler,
1285
+ inputs=[country_dropdown],
1286
+ outputs=[country_code_state, price, price],
1287
+ )
1288
+
1289
+ # Calculate button
1290
+ def calculate_wrapper(birds, weeks, days, bag_size, price_per_bag, email, country_code_state, breed, request: gr.Request):
1291
+ if not country_code_state:
1292
+ country_code_state = "NG"
1293
+ return calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_code_state, breed, request)
1294
+
1295
+ calculate_btn.click(
1296
+ fn=calculate_wrapper,
1297
+ inputs=[birds, weeks, days, bag_size, price, email, country_code_state, breed],
1298
+ outputs=[result, save_status],
1299
+ )
1300
+
1301
+ ask_btn.click(
1302
+ fn=answer_farmer_question,
1303
+ inputs=[question, email, breed],
1304
+ outputs=[question_answer],
1305
+ )
1306
+
1307
+ # Track visits on page load
1308
+ demo.load(
1309
+ fn=track_visit,
1310
+ inputs=None,
1311
+ outputs=None,
1312
+ )
1313
+
1314
+
1315
+ # ============================================================
1316
+ # START APP
1317
+ # ============================================================
1318
+
1319
+ if __name__ == "__main__":
1320
+ port = int(os.environ.get("PORT", 7860))
1321
+ demo.launch(server_name="0.0.0.0", server_port=port, root_path="/")