NTEIGBANAM OKE commited on
Commit
95e5a9a
Β·
verified Β·
1 Parent(s): 4023b58

Update app.py

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