GoogleCalc commited on
Commit
da4e598
Β·
verified Β·
1 Parent(s): d6790bc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1292 -614
app.py CHANGED
@@ -4,7 +4,7 @@ import re
4
  import sqlite3
5
  import json
6
  import urllib.parse
7
- from datetime import datetime
8
  from pathlib import Path
9
  import threading
10
 
@@ -28,85 +28,98 @@ if db_dir and not os.path.exists(db_dir):
28
 
29
  STARTER_END_DAY = 21
30
 
31
-
32
  # ============================================================
33
- # DATABASE
34
  # ============================================================
35
 
36
- def init_db():
37
- conn = sqlite3.connect(DB_PATH)
38
- cur = conn.cursor()
39
-
40
- cur.execute("""
41
- CREATE TABLE IF NOT EXISTS leads (
42
- id INTEGER PRIMARY KEY AUTOINCREMENT,
43
- email TEXT UNIQUE,
44
- country TEXT,
45
- currency TEXT,
46
- breed TEXT,
47
- created_at TEXT
48
- )
49
- """)
50
-
51
- cur.execute("""
52
- CREATE TABLE IF NOT EXISTS farmer_questions (
53
- id INTEGER PRIMARY KEY AUTOINCREMENT,
54
- email TEXT,
55
- question TEXT,
56
- breed TEXT,
57
- created_at TEXT
58
- )
59
- """)
60
-
61
- cur.execute("""
62
- CREATE TABLE IF NOT EXISTS calculations (
63
- id INTEGER PRIMARY KEY AUTOINCREMENT,
64
- email TEXT,
65
- country TEXT,
66
- currency TEXT,
67
- breed TEXT,
68
- birds INTEGER,
69
- weeks INTEGER,
70
- days INTEGER,
71
- total_days INTEGER,
72
- bag_size REAL,
73
- price_per_bag REAL,
74
- total_feed_kg REAL,
75
- starter_kg REAL,
76
- finisher_kg REAL,
77
- starter_bags REAL,
78
- finisher_bags REAL,
79
- total_bags REAL,
80
- rounded_bags INTEGER,
81
- total_cost REAL,
82
- created_at TEXT
83
- )
84
- """)
85
-
86
- cur.execute("""
87
- CREATE TABLE IF NOT EXISTS analytics (
88
- id INTEGER PRIMARY KEY AUTOINCREMENT,
89
- visit_date TEXT,
90
- ip TEXT,
91
- country TEXT,
92
- breed TEXT,
93
- action TEXT,
94
- created_at TEXT
95
- )
96
- """)
97
-
98
- conn.commit()
99
- conn.close()
100
- print(f"βœ… Database initialized at: {DB_PATH}")
101
-
102
-
103
- init_db()
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
 
106
  # ============================================================
107
  # VERIFIED BREED DATA - From Official PDFs
108
  # ============================================================
109
 
 
110
  BREED_WEIGHT_DATA = {
111
  "Ross 308": {
112
  0: 0.044, 7: 0.213, 14: 0.533, 21: 1.012,
@@ -122,6 +135,7 @@ BREED_WEIGHT_DATA = {
122
  }
123
  }
124
 
 
125
  BREED_FEED_DATA = {
126
  "Ross 308": {
127
  0: 0.000, 7: 0.166, 14: 0.535, 21: 1.155,
@@ -137,6 +151,25 @@ BREED_FEED_DATA = {
137
  }
138
  }
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
  def get_breed_weight_data(breed):
142
  return BREED_WEIGHT_DATA.get(breed, BREED_WEIGHT_DATA["Ross 308"])
@@ -147,33 +180,128 @@ def get_breed_feed_data(breed):
147
 
148
 
149
  def interpolate_weight(day, breed):
 
150
  weight_data = get_breed_weight_data(breed)
 
151
  if day <= 0:
152
  return weight_data[0]
153
  if day >= 56:
154
  return weight_data[56]
 
155
  keys = sorted(weight_data.keys())
156
  for i in range(len(keys) - 1):
157
  if keys[i] <= day <= keys[i + 1]:
158
  ratio = (day - keys[i]) / (keys[i + 1] - keys[i])
159
  return weight_data[keys[i]] + (weight_data[keys[i + 1]] - weight_data[keys[i]]) * ratio
 
160
  return weight_data[56]
161
 
162
 
163
  def interpolate_feed(day, breed):
 
164
  feed_data = get_breed_feed_data(breed)
 
165
  if day <= 0:
166
  return 0.0
167
  if day >= 56:
168
  return feed_data[56]
 
169
  keys = sorted(feed_data.keys())
170
  for i in range(len(keys) - 1):
171
  if keys[i] <= day <= keys[i + 1]:
172
  ratio = (day - keys[i]) / (keys[i + 1] - keys[i])
173
  return feed_data[keys[i]] + (feed_data[keys[i + 1]] - feed_data[keys[i]]) * ratio
 
174
  return feed_data[56]
175
 
176
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  # ============================================================
178
  # CURRENCY DATA
179
  # ============================================================
@@ -210,21 +338,25 @@ DEFAULT_PRICES = {
210
 
211
 
212
  def detect_country_from_env():
 
213
  candidates = [
214
  os.environ.get("COUNTRY_CODE"),
215
  os.environ.get("CF-IPCOUNTRY"),
216
  os.environ.get("GEOIP_COUNTRY"),
217
  os.environ.get("HTTP_CF_IPCOUNTRY"),
218
  ]
 
219
  for value in candidates:
220
  if value:
221
  value = value.strip().upper()
222
  if value in CURRENCIES:
223
  return value
 
224
  return None
225
 
226
 
227
  def get_country_info(country_code):
 
228
  if country_code in CURRENCIES:
229
  name, symbol, code = CURRENCIES[country_code]
230
  return country_code, symbol, code
@@ -238,6 +370,80 @@ def get_default_price(currency_code):
238
  COUNTRY_LIST = sorted([(code, f"{name} ({symbol})") for code, (name, symbol, curr) in CURRENCIES.items()])
239
 
240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  # ============================================================
242
  # DATABASE HELPERS
243
  # ============================================================
@@ -253,34 +459,51 @@ def valid_email(email):
253
  def save_lead(email, country, currency, breed):
254
  if not valid_email(email):
255
  return
 
 
256
  conn = sqlite3.connect(DB_PATH)
257
  cur = conn.cursor()
258
  cur.execute("""
259
  INSERT OR IGNORE INTO leads (email, country, currency, breed, created_at)
260
  VALUES (?, ?, ?, ?, ?)
261
- """, (email.strip().lower(), country, currency, breed, datetime.utcnow().isoformat()))
262
  conn.commit()
263
  conn.close()
264
 
 
 
 
 
 
 
265
 
266
  def save_question(email, question, breed):
267
  if not question or not question.strip():
268
  return
 
 
269
  conn = sqlite3.connect(DB_PATH)
270
  cur = conn.cursor()
271
  cur.execute("""
272
  INSERT INTO farmer_questions (email, question, breed, created_at)
273
  VALUES (?, ?, ?, ?)
274
- """, (email.strip().lower() if email else "", question.strip(), breed, datetime.utcnow().isoformat()))
275
  conn.commit()
276
  conn.close()
277
 
 
 
 
 
 
 
278
 
279
  def save_calculation(email, country, currency, breed, birds, weeks, days, total_days,
280
  bag_size, price_per_bag, total_feed_kg, starter_kg, finisher_kg,
281
  starter_bags, finisher_bags, total_bags, rounded_bags, total_cost):
282
  conn = sqlite3.connect(DB_PATH)
283
  cur = conn.cursor()
 
284
  cur.execute("""
285
  INSERT INTO calculations (
286
  email, country, currency, breed, birds, weeks, days, total_days,
@@ -291,16 +514,20 @@ def save_calculation(email, country, currency, breed, birds, weeks, days, total_
291
  """, (email.strip().lower() if email else "", country, currency, breed, birds, weeks, days, total_days,
292
  bag_size, price_per_bag, total_feed_kg, starter_kg, finisher_kg,
293
  starter_bags, finisher_bags, total_bags, rounded_bags, total_cost,
294
- datetime.utcnow().isoformat()))
 
295
  conn.commit()
296
  conn.close()
297
 
298
 
299
- def track_visit_async(request: gr.Request):
 
300
  try:
301
  client_ip = request.client.host if request and hasattr(request, 'client') else "unknown"
 
302
  conn = sqlite3.connect(DB_PATH)
303
  cur = conn.cursor()
 
304
  cur.execute("""
305
  INSERT INTO analytics (visit_date, ip, country, action, created_at)
306
  VALUES (?, ?, ?, ?, ?)
@@ -311,6 +538,7 @@ def track_visit_async(request: gr.Request):
311
  "page_visit",
312
  datetime.now().isoformat()
313
  ))
 
314
  conn.commit()
315
  conn.close()
316
  except Exception as e:
@@ -318,9 +546,11 @@ def track_visit_async(request: gr.Request):
318
 
319
 
320
  def track_calculation(breed, birds, days):
 
321
  try:
322
  conn = sqlite3.connect(DB_PATH)
323
  cur = conn.cursor()
 
324
  cur.execute("""
325
  INSERT INTO analytics (visit_date, country, breed, action, created_at)
326
  VALUES (?, ?, ?, ?, ?)
@@ -331,6 +561,7 @@ def track_calculation(breed, birds, days):
331
  f"calculation_{birds}_{days}",
332
  datetime.now().isoformat()
333
  ))
 
334
  conn.commit()
335
  conn.close()
336
  return True
@@ -339,22 +570,32 @@ def track_calculation(breed, birds, days):
339
 
340
 
341
  def get_analytics_summary():
 
342
  conn = sqlite3.connect(DB_PATH)
343
  cur = conn.cursor()
 
344
  cur.execute("SELECT COUNT(*) FROM analytics WHERE action = 'page_visit'")
345
  total_visits = cur.fetchone()[0] or 0
 
346
  cur.execute("SELECT COUNT(*) FROM analytics WHERE action = 'page_visit' AND visit_date = ?",
347
  (datetime.now().strftime("%Y-%m-%d"),))
348
  today_visits = cur.fetchone()[0] or 0
 
349
  cur.execute("SELECT COUNT(*) FROM analytics WHERE action LIKE 'calculation_%'")
350
  total_calculations = cur.fetchone()[0] or 0
 
351
  cur.execute("SELECT COUNT(DISTINCT ip) FROM analytics WHERE action = 'page_visit' AND ip != 'unknown'")
352
  unique_visitors = cur.fetchone()[0] or 0
 
353
  cur.execute("SELECT breed, COUNT(*) FROM analytics WHERE action LIKE 'calculation_%' GROUP BY breed")
354
  breed_stats = cur.fetchall()
 
 
355
  cur.execute("SELECT COUNT(*) FROM leads")
356
  total_leads = cur.fetchone()[0] or 0
 
357
  conn.close()
 
358
  return {
359
  "total_visits": total_visits,
360
  "today_visits": today_visits,
@@ -370,254 +611,202 @@ def get_analytics_summary():
370
  # ============================================================
371
 
372
  def get_share_links(app_url="https://howmuchfeed.com"):
 
 
373
  share_text = f"πŸ” Calculate your poultry feed needs at {app_url}"
374
  encoded = urllib.parse.quote(share_text)
375
- return {
 
376
  "whatsapp": f"https://wa.me/?text={encoded}",
377
  "email": f"mailto:?subject=How Much Feed? - Poultry Feed Calculator&body={encoded}",
378
  "facebook": f"https://www.facebook.com/sharer/sharer.php?u={urllib.parse.quote(app_url)}",
379
  "twitter": f"https://twitter.com/intent/tweet?text={encoded}",
380
  }
 
 
381
 
382
 
383
  # ============================================================
384
- # BENCHMARK HELPERS
385
  # ============================================================
386
 
387
- def get_benchmark_html(breed):
388
- weight_data = get_breed_weight_data(breed)
389
- feed_data = get_breed_feed_data(breed)
390
- days = [7, 14, 21, 28, 35, 42, 49, 56]
391
- rows = []
392
- for day in days:
393
- weight = weight_data.get(day, 0)
394
- feed = feed_data.get(day, 0)
395
- fcr = feed / weight if weight > 0 else 0
396
- rows.append(f"<tr><td>{day}</td><td>{weight:.3f} kg</td><td>{feed:.3f} kg</td><td>{fcr:.3f}</td></tr>")
397
- return f"""
398
- <div class="table-wrap"><table class="feed-table">
399
- <thead><tr><th>Days</th><th>Body Weight</th><th>Cum Feed</th><th>FCR</th></tr></thead>
400
- <tbody>{''.join(rows)}</tbody>
401
- </table></div>
402
- """
403
 
404
-
405
- def get_comparison_html():
406
- breeds = ["Ross 308", "Cobb 500", "Arbor Acres"]
407
- days = [7, 14, 21, 28, 35, 42, 49, 56]
408
- rows = []
409
- for day in days:
410
- cells = [f'<td>{day}</td>']
411
- for breed in breeds:
412
- weight_data = get_breed_weight_data(breed)
413
- feed_data = get_breed_feed_data(breed)
414
- weight = weight_data.get(day, 0)
415
- feed = feed_data.get(day, 0)
416
- cells.append(f'<td>{weight:.3f} kg</td>')
417
- cells.append(f'<td>{feed:.3f} kg</td>')
418
- rows.append('<tr>' + ''.join(cells) + '</tr>')
419
- headers = ['Days'] + [f'{b} Wt' for b in breeds] + [f'{b} Feed' for b in breeds]
420
- head = ''.join(f'<th>{h}</th>' for h in headers)
421
- return f"""
422
- <div class="table-wrap"><table class="feed-table comparison-table">
423
- <thead><tr>{head}</tr></thead>
424
- <tbody>{''.join(rows)}</tbody>
425
- </table></div>
426
- """
427
-
428
-
429
- # ============================================================
430
- # QUESTION / ANSWER
431
- # ============================================================
432
-
433
- def answer_farmer_question(question, email, breed):
434
- if not question or not question.strip():
435
- return "Please type or speak your question first."
436
- save_question(email or "", question, breed)
437
- q = question.lower().strip()
438
- if "bag" in q and ("feed" in q or "birds" in q):
439
- return """
440
- ### Feed-bag questions
441
- For an exact feed estimate, use the calculator above.
442
- Enter: number of birds, age, bag size, price, email, and breed.
443
- """
444
- elif "breed" in q or "ross" in q or "cobb" in q or "arbor" in q:
445
- return """
446
- ### Common Broiler Breeds in Nigeria
447
- | Breed | Characteristics | Available At |
448
- |-------|-----------------|--------------|
449
- | **Ross 308** | Big chest, thick legs, very fast growth | Agrited, Sayed, Valentine |
450
- | **Cobb 500** | Good early growth, strong legs | Chikun, Zartech |
451
- | **Arbor Acres** | Balanced growth, hardy, adaptable | Supreme, CHI, GS, Vertex, Fidan, Cascada, Orobor, Sunchi, Farm Support, Ajila |
452
- """
453
- elif "starter" in q:
454
- return f"Starter feed is used during the early growth period (Day 0–{STARTER_END_DAY})."
455
- elif "finisher" in q:
456
- return f"Finisher feed is used after day {STARTER_END_DAY}."
457
- elif "cost" in q or "price" in q:
458
- return "Enter the current price of one feed bag. The app calculates: bags Γ— price = cost."
459
- else:
460
- return f"**β€œ{question.strip()}”** β€” Your question has been saved."
461
-
462
-
463
- # ============================================================
464
- # FEED CALCULATION - CLEAN OUTPUT
465
- # ============================================================
466
-
467
- def calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_code, breed, request: gr.Request):
468
- try:
469
- birds = int(birds)
470
- weeks = int(weeks)
471
- days = int(days)
472
- bag_size = float(bag_size)
473
- price_per_bag = float(price_per_bag)
474
- except Exception:
475
- return "⚠️ Please enter valid numbers.", ""
476
-
477
- if birds <= 0:
478
- return "⚠️ Number of birds must be greater than zero.", ""
479
- if weeks < 0:
480
- return "⚠️ Weeks cannot be negative.", ""
481
- if days < 0 or days > 6:
482
- return "⚠️ Days must be between 0 and 6.", ""
483
- if bag_size <= 0:
484
- return "⚠️ Feed bag size must be greater than zero.", ""
485
- if price_per_bag < 0:
486
- return "⚠️ Price per bag cannot be negative.", ""
487
 
488
  if not valid_email(email):
489
  return "πŸ“§ **Email required**\n\nPlease enter your email address before calculating.", ""
490
 
 
491
  country_name, symbol, currency_code = get_country_info(country_code)
492
  currency_symbol = symbol
493
 
 
494
  save_lead(email, country_code, currency_code, breed)
 
 
495
  track_calculation(breed, birds, (weeks * 7 + days))
496
 
 
497
  total_days = (weeks * 7) + days
 
 
498
  total_feed_per_bird = interpolate_feed(total_days, breed)
499
  total_feed_kg = birds * total_feed_per_bird
500
 
 
501
  starter_days = min(total_days, STARTER_END_DAY)
502
  starter_per_bird = interpolate_feed(starter_days, breed)
503
  starter_kg = birds * starter_per_bird
504
  finisher_kg = max(total_feed_kg - starter_kg, 0)
505
 
 
506
  starter_bags = starter_kg / bag_size
507
  finisher_bags = finisher_kg / bag_size
508
  total_bags = total_feed_kg / bag_size
509
  rounded_bags = math.ceil(total_bags)
510
  total_cost = rounded_bags * price_per_bag
511
 
 
512
  expected_weight = interpolate_weight(total_days, breed)
513
  flock_weight = expected_weight * birds
514
 
 
515
  save_calculation(email, country_code, currency_code, breed, birds, weeks, days, total_days,
516
  bag_size, price_per_bag, total_feed_kg, starter_kg, finisher_kg,
517
  starter_bags, finisher_bags, total_bags, rounded_bags, total_cost)
518
 
 
 
 
 
 
519
  share_links = get_share_links()
 
 
520
  starter_bags_display = f"{starter_bags:.1f}"
521
  finisher_bags_display = f"{finisher_bags:.1f}"
 
522
  age_text = f"{weeks} week{'s' if weeks != 1 else ''}"
523
  if days:
524
  age_text += f" + {days} day{'s' if days != 1 else ''}"
525
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  html = f"""
527
- <div class="result-clean">
528
-
529
- <!-- Hero -->
530
- <div class="hero-box">
531
- <div class="hero-badge">⚑ QUICK ANSWER</div>
532
- <div class="hero-number">{rounded_bags}</div>
533
- <div class="hero-label">BAGS OF FEED</div>
534
- <div class="hero-sub">
535
- To raise <strong>{birds:,}</strong> {breed} broilers for <strong>{age_text}</strong>
536
- </div>
537
- <div class="hero-note">Recommended amount, rounded up to the next whole bag.</div>
538
  </div>
539
 
540
- <!-- Summary -->
541
- <div class="summary-box">
542
  You need <strong>{total_feed_kg:,.1f} kg</strong> of feed, equal to about
543
  <strong>{total_bags:.1f} bags</strong> of {bag_size:g} kg feed.
544
- </div>
545
 
546
- <!-- Metrics -->
547
- <div class="metrics-grid">
548
- <div class="metric-item">
549
- <div class="metric-icon">🌾</div>
550
- <div class="metric-value">{total_feed_kg:,.1f} kg</div>
551
- <div class="metric-label">Feed needed</div>
552
  </div>
553
- <div class="metric-item">
554
- <div class="metric-icon">πŸ“¦</div>
555
- <div class="metric-value">{total_bags:.1f}</div>
556
- <div class="metric-label">Exact amount</div>
557
  </div>
558
- <div class="metric-item">
559
- <div class="metric-icon">πŸ’°</div>
560
- <div class="metric-value">{currency_symbol}{total_cost:,.0f}</div>
561
- <div class="metric-label">Estimated cost</div>
562
  </div>
563
- <div class="metric-item">
564
- <div class="metric-icon">πŸ“…</div>
565
- <div class="metric-value">{total_days}</div>
566
- <div class="metric-label">Growing period</div>
567
  </div>
568
  </div>
569
 
570
- <!-- Breakdown -->
571
- <div class="breakdown-box">
572
  <div class="section-title">🌾 Feed Breakdown</div>
573
- <div class="breakdown-grid">
574
- <div class="breakdown-item starter">
575
- <div class="bd-label">🌱 Starter</div>
576
- <div class="bd-value">{starter_kg:,.1f} kg</div>
577
- <div class="bd-bags">{starter_bags_display} bags</div>
578
- </div>
579
- <div class="breakdown-item finisher">
580
- <div class="bd-label">🌽 Finisher</div>
581
- <div class="bd-value">{finisher_kg:,.1f} kg</div>
582
- <div class="bd-bags">{finisher_bags_display} bags</div>
583
- </div>
584
- <div class="breakdown-item total">
585
- <div class="bd-label">πŸ“Š TOTAL</div>
586
- <div class="bd-value">{total_feed_kg:,.1f} kg</div>
587
- <div class="bd-bags">{total_bags:.1f} bags</div>
588
- </div>
589
  </div>
590
- </div>
591
 
592
- <!-- Cost -->
593
- <div class="cost-box">
594
  <div class="section-title">πŸ’° Feed Cost Estimate</div>
595
- <div class="cost-amount">{currency_symbol}{total_cost:,.0f}</div>
596
  <div class="cost-note">Based on {rounded_bags} bags Γ— {currency_symbol}{price_per_bag:,.0f} per bag.</div>
597
- </div>
598
 
599
- <!-- Performance -->
600
- <div class="perf-box">
601
  <div class="section-title">πŸ“ˆ Expected Performance</div>
602
- <div class="perf-grid">
603
- <div><span>Body weight / bird</span><strong>{expected_weight:.3f} kg</strong></div>
604
  <div><span>Flock live weight</span><strong>{flock_weight:,.1f} kg</strong></div>
605
  </div>
606
- </div>
607
 
608
- <!-- Share -->
609
- <div class="share-box">
610
- <div class="section-title">πŸ“€ Share Results</div>
611
  <div class="share-grid">
612
- <a class="share-btn whatsapp" href="{share_links['whatsapp']}" target="_blank">πŸ’¬ WhatsApp</a>
613
- <a class="share-btn email" href="{share_links['email']}" target="_blank">πŸ“§ Email</a>
614
- <a class="share-btn facebook" href="{share_links['facebook']}" target="_blank">πŸ“˜ Facebook</a>
615
- <a class="share-btn twitter" href="{share_links['twitter']}" target="_blank">🐦 X</a>
616
  </div>
617
- </div>
618
 
619
- <div class="footnote">
620
- ⚠️ Estimate based on {breed} benchmark. Actual results may vary.
621
  </div>
622
  </div>
623
  """
@@ -626,14 +815,107 @@ def calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_c
626
 
627
 
628
  # ============================================================
629
- # COUNTRY CHANGE HANDLER
630
  # ============================================================
631
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
  def on_country_change_handler(country_selection):
 
633
  if country_selection == "Auto-Detect":
634
  country_code = detect_country_from_env() or "NG"
635
  else:
636
  country_code = country_selection
 
637
  _, symbol, currency_code = get_country_info(country_code)
638
  default_price = get_default_price(currency_code)
639
  return country_code, default_price
@@ -643,408 +925,685 @@ def on_country_change_handler(country_selection):
643
  # ADMIN FUNCTIONS
644
  # ============================================================
645
 
646
- def admin_login(password):
647
- if password == ADMIN_PASSWORD:
648
- stats = get_analytics_summary()
649
- breed_stats_html = ""
650
- for breed, count in stats["breed_stats"]:
651
- breed_stats_html += f"<li><strong>{breed}</strong>: {count} calculations</li>"
652
- return f"""
653
- ### Analytics
654
- | Metric | Value |
655
- |--------|-------|
656
- | Total Visits | **{stats['total_visits']:,}** |
657
- | Today's Visits | **{stats['today_visits']}** |
658
- | Unique Visitors | **{stats['unique_visitors']:,}** |
659
- | Calculations | **{stats['total_calculations']:,}** |
660
- | Leads | **{stats['total_leads']:,}** |
661
- ### Popular Breeds
662
- <ul>{breed_stats_html}</ul>
663
- """, gr.update(visible=True), gr.update(visible=False)
664
- else:
665
- return "❌ Incorrect password.", gr.update(visible=False), gr.update(visible=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
666
 
667
 
668
- def logout_admin():
669
- return "πŸ”’ Enter password to view analytics.", gr.update(visible=True), gr.update(visible=False)
 
670
 
671
 
672
- def export_leads_wrapper():
673
- import tempfile
674
  conn = sqlite3.connect(DB_PATH)
675
  cur = conn.cursor()
676
  cur.execute("SELECT email, country, currency, breed, created_at FROM leads ORDER BY created_at DESC")
677
  rows = cur.fetchall()
678
  conn.close()
 
679
  import csv
680
  from io import StringIO
 
681
  output = StringIO()
682
  writer = csv.writer(output)
683
  writer.writerow(["Email", "Country", "Currency", "Breed", "Created At"])
684
  writer.writerows(rows)
 
 
 
 
 
 
 
 
685
  temp = tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False, encoding='utf-8')
686
- temp.write(output.getvalue())
687
  temp.close()
688
  return temp.name
689
 
690
 
691
  def export_calculations_wrapper():
692
- import tempfile
693
  conn = sqlite3.connect(DB_PATH)
694
  cur = conn.cursor()
695
- cur.execute("SELECT email, breed, birds, total_days, rounded_bags, bag_size, total_cost, created_at FROM calculations ORDER BY created_at DESC")
 
 
 
 
696
  rows = cur.fetchall()
697
  conn.close()
 
698
  import csv
699
- from io import StringIO
700
- output = StringIO()
701
- writer = csv.writer(output)
702
- writer.writerow(["Email", "Breed", "Birds", "Days", "Bags", "Bag Size", "Cost", "Created"])
703
- writer.writerows(rows)
704
  temp = tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False, encoding='utf-8')
705
- temp.write(output.getvalue())
 
 
706
  temp.close()
 
707
  return temp.name
708
 
709
 
710
  # ============================================================
711
- # CSS - CLEAN & PROFESSIONAL
712
  # ============================================================
713
 
714
  CSS = """
715
- /* ============================================================
716
- CLEAN & PROFESSIONAL DESIGN - FORCED LIGHT THEME
717
- ============================================================ */
718
-
719
- :root {
720
- --mint: #00A86B;
721
- --mint-dark: #0B3022;
722
- --mint-soft: #E8F5EE;
723
- --yellow: #FFB800;
724
- --yellow-soft: #FFF8E1;
725
- --red: #EA4335;
726
- --bg: #F5F7F6;
727
- --surface: #FFFFFF;
728
- --border: #E2E8E6;
729
- --text: #1A2E26;
730
- --text-muted: #5A6E65;
731
- }
732
-
733
- html, body, .gradio-container, .gradio-container * {
734
- background: var(--bg) !important;
735
- color: var(--text) !important;
 
 
 
 
 
 
 
 
 
736
  color-scheme: light !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
737
  }
738
 
739
  .gradio-container {
740
- max-width: 1000px !important;
 
741
  margin: 0 auto !important;
742
- padding: 0 12px 32px !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
743
  }
744
 
745
- /* Remove HF header */
746
- #huggingface-space-header, .hf-header, [data-testid="header"] {
747
- display: none !important;
748
  }
749
 
750
- /* APP HEADER */
 
 
751
  .app-header {
752
- background: var(--mint-dark) !important;
753
- border-radius: 0 0 20px 20px;
754
- padding: 16px 20px 18px;
755
- margin: 0 -12px 14px;
756
- }
757
- .app-header .brand-row { display: flex; align-items: center; gap: 10px; }
758
- .app-header .brand-icon { font-size: 28px; }
759
- .app-header h1 { color: #fff !important; margin: 0; font-size: 24px; font-weight: 800; }
760
- .app-header p { color: rgba(255,255,255,.85) !important; margin: 4px 0 0 38px; font-size: 13px; }
761
-
762
- /* FORM CARDS */
763
- .form-card {
764
- background: var(--surface) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
765
  border: 1px solid var(--border);
 
766
  border-radius: 12px;
767
  padding: 12px 14px;
768
- margin-bottom: 8px;
769
- }
770
- .form-card .label {
771
- font-weight: 700;
772
  font-size: 13px;
773
- color: var(--text);
774
- margin-bottom: 6px;
 
 
 
 
 
 
 
 
 
 
 
 
775
  display: flex;
776
  align-items: center;
777
- gap: 8px;
 
 
 
 
778
  }
779
- .form-card .label .num {
780
- background: var(--mint-dark);
781
- color: #fff;
782
  border-radius: 50%;
783
- width: 20px;
784
- height: 20px;
785
  display: inline-flex;
786
  align-items: center;
787
  justify-content: center;
788
- font-size: 10px;
789
  font-weight: 800;
 
790
  }
791
 
792
- .gradio-container input, .gradio-container select, .gradio-container textarea {
 
 
 
 
793
  background: #fff !important;
794
- border: 1px solid var(--border) !important;
795
- border-radius: 8px !important;
796
- min-height: 40px !important;
797
- font-size: 14px !important;
798
- color: var(--text) !important;
 
799
  }
800
- .gradio-container input:focus, .gradio-container select:focus {
 
 
 
801
  border-color: var(--mint) !important;
802
- box-shadow: 0 0 0 3px rgba(0,168,107,.12) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
803
  }
804
- .gradio-container label { color: var(--text) !important; font-weight: 600 !important; }
805
- .gradio-container .info { color: var(--text-muted) !important; font-size: 12px !important; }
806
 
807
- .calc-btn button {
 
 
 
808
  width: 100% !important;
809
- min-height: 48px !important;
 
 
810
  background: var(--mint) !important;
811
  color: #fff !important;
812
- border: 0 !important;
813
- border-radius: 10px !important;
814
- font-size: 15px !important;
815
  font-weight: 800 !important;
816
- box-shadow: 0 4px 12px rgba(0,168,107,.2) !important;
817
- }
818
- .calc-btn button:hover { background: #00955E !important; }
819
-
820
- /* ============================================================
821
- CLEAN RESULTS
822
- ============================================================ */
823
-
824
- .result-clean {
825
- max-width: 100%;
826
- animation: fadeUp 0.3s ease;
827
  }
828
-
829
- @keyframes fadeUp {
830
- from { opacity: 0; transform: translateY(8px); }
831
- to { opacity: 1; transform: translateY(0); }
 
 
 
 
 
832
  }
833
-
834
- /* Hero */
835
- .hero-box {
836
- background: var(--mint-dark);
837
- border-radius: 14px;
838
- padding: 18px 16px 16px;
839
  text-align: center;
840
- margin-bottom: 12px;
841
  }
842
- .hero-badge {
843
- color: rgba(255,255,255,0.6);
844
- font-size: 10px;
845
- font-weight: 700;
846
- letter-spacing: 1px;
847
- text-transform: uppercase;
848
  }
849
- .hero-number {
850
- color: #fff;
851
- font-size: clamp(48px, 15vw, 72px);
 
 
 
 
 
 
852
  font-weight: 900;
853
- line-height: 1;
854
- margin: 2px 0;
855
  }
856
- .hero-label {
857
- color: rgba(255,255,255,0.7);
858
- font-size: 14px;
859
- font-weight: 600;
860
- letter-spacing: 1.5px;
861
  }
862
- .hero-sub {
863
- color: rgba(255,255,255,0.85);
864
- font-size: 13px;
865
- margin: 6px 0 4px;
866
  }
867
- .hero-sub strong { color: #fff !important; }
868
- .hero-note {
869
- color: rgba(255,255,255,0.5);
870
- font-size: 11px;
 
871
  }
872
-
873
- /* Summary */
874
- .summary-box {
875
- background: var(--yellow-soft);
876
  border-left: 4px solid var(--yellow);
877
- border-radius: 8px;
878
- padding: 10px 14px;
879
  margin: 10px 0 12px;
880
  font-size: 14px;
881
  line-height: 1.5;
882
- color: var(--text);
883
  }
884
- .summary-box strong { color: var(--text); }
885
 
886
- /* Metrics */
887
- .metrics-grid {
888
  display: grid;
889
- grid-template-columns: repeat(2, 1fr);
890
- gap: 8px;
891
- margin-bottom: 12px;
892
  }
893
- .metric-item {
894
- background: var(--surface);
895
  border: 1px solid var(--border);
896
- border-radius: 10px;
897
- padding: 10px 8px;
898
- text-align: center;
899
  }
900
- .metric-item .metric-icon { font-size: 18px; }
901
- .metric-item .metric-value {
 
902
  font-size: 18px;
903
- font-weight: 800;
904
- color: var(--mint-dark);
905
- margin: 2px 0;
906
  }
907
- .metric-item .metric-label {
908
- font-size: 10px;
909
- color: var(--text-muted);
910
- font-weight: 500;
 
911
  }
912
-
913
- /* Breakdown */
914
- .breakdown-box {
915
- background: var(--surface);
916
- border: 1px solid var(--border);
917
- border-radius: 10px;
918
- padding: 12px 12px 10px;
919
- margin-bottom: 12px;
920
  }
 
921
  .section-title {
922
- font-weight: 700;
923
- font-size: 13px;
 
924
  margin-bottom: 8px;
925
- color: var(--text);
926
  }
927
- .breakdown-grid {
928
- display: grid;
929
- grid-template-columns: 1fr 1fr 1fr;
930
- gap: 6px;
931
- }
932
- .breakdown-item {
933
- text-align: center;
934
- padding: 8px 4px;
935
- border-radius: 8px;
 
 
936
  }
937
- .breakdown-item.starter { background: #E3F2ED; }
938
- .breakdown-item.finisher { background: #F5F0E8; }
939
- .breakdown-item.total { background: var(--mint-soft); }
940
- .breakdown-item .bd-label {
941
- font-size: 10px;
942
- font-weight: 600;
943
- color: var(--text-muted);
944
  }
945
- .breakdown-item .bd-value {
946
- font-size: 14px;
947
- font-weight: 700;
948
- color: var(--text);
 
 
 
949
  }
950
- .breakdown-item .bd-bags {
951
- font-size: 11px;
952
- color: var(--text-muted);
 
953
  }
 
954
 
955
- /* Cost */
956
- .cost-box {
957
  background: var(--yellow-soft);
958
  border: 1px solid #EBC34A;
959
- border-radius: 10px;
960
- padding: 10px 14px;
961
- margin-bottom: 12px;
 
962
  }
963
- .cost-amount {
964
- font-size: clamp(24px, 7vw, 34px);
 
 
965
  font-weight: 900;
966
- color: var(--mint-dark);
967
- }
968
- .cost-note {
969
- font-size: 11px;
970
- color: var(--text-muted);
971
- margin-top: 2px;
972
  }
 
973
 
974
- /* Performance */
975
- .perf-box {
976
- background: var(--surface);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
977
  border: 1px solid var(--border);
978
  border-radius: 10px;
979
- padding: 10px 14px;
980
- margin-bottom: 12px;
 
 
 
981
  }
982
- .perf-grid {
983
- display: grid;
984
- grid-template-columns: 1fr 1fr;
985
- gap: 4px;
 
 
 
 
 
 
 
986
  }
987
- .perf-grid div {
988
  display: flex;
 
989
  justify-content: space-between;
990
- padding: 4px 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
991
  font-size: 13px;
992
- border-bottom: 1px solid var(--border);
 
 
 
 
 
 
 
993
  }
994
- .perf-grid div:last-child { border-bottom: 0; }
995
- .perf-grid span { color: var(--text-muted); }
996
- .perf-grid strong { color: var(--mint-dark); }
997
 
998
- /* Share */
999
- .share-box {
1000
- background: var(--surface);
1001
- border: 1px solid var(--border);
1002
- border-radius: 10px;
1003
- padding: 10px 14px;
1004
- margin-bottom: 10px;
 
1005
  }
1006
- .share-grid {
1007
- display: grid;
1008
- grid-template-columns: repeat(2, 1fr);
1009
- gap: 6px;
 
1010
  }
1011
- .share-btn {
1012
- display: block;
1013
- text-align: center;
1014
- text-decoration: none !important;
1015
- border-radius: 8px;
1016
- padding: 8px;
1017
  font-size: 12px;
1018
- font-weight: 700;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1019
  color: #fff !important;
 
 
 
 
 
1020
  }
1021
- .share-btn.whatsapp { background: #25D366; }
1022
- .share-btn.email { background: #EA4335; }
1023
- .share-btn.facebook { background: #1877F2; }
1024
- .share-btn.twitter { background: #000; }
1025
 
1026
- .footnote {
1027
- font-size: 11px;
1028
- color: var(--text-muted);
1029
- text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1030
  padding-top: 10px;
1031
- border-top: 1px solid var(--border);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1032
  }
1033
 
1034
- /* Dark mode override */
1035
- .dark, .dark * {
1036
- background: var(--bg) !important;
1037
- color: var(--text) !important;
1038
- color-scheme: light !important;
 
 
 
 
 
 
1039
  }
1040
 
1041
- /* Mobile */
1042
  @media (max-width: 480px) {
1043
- .metrics-grid { grid-template-columns: repeat(2, 1fr); gap: 6px; }
1044
- .breakdown-grid { grid-template-columns: 1fr; gap: 4px; }
1045
- .perf-grid { grid-template-columns: 1fr; }
1046
- .hero-number { font-size: 42px; }
1047
- .metric-item .metric-value { font-size: 16px; }
 
 
1048
  }
1049
  """
1050
 
@@ -1058,119 +1617,197 @@ with gr.Blocks(title=APP_NAME, analytics_enabled=False) as demo:
1058
  <header class="app-header">
1059
  <div class="brand-row">
1060
  <div class="brand-icon">πŸ”</div>
1061
- <div><h1>How Much Feed?</h1></div>
 
 
1062
  </div>
1063
- <p>Broiler feed calculator β€” know exactly how many bags of feed you need.</p>
1064
  </header>
1065
  """)
1066
 
1067
- with gr.Column():
1068
- with gr.Group(elem_classes=["form-card"]):
1069
- gr.HTML('<div class="label"><span class="num">1</span> πŸ” Breed & Birds</div>')
1070
- breed = gr.Dropdown(choices=["Ross 308", "Cobb 500", "Arbor Acres"], value="Ross 308", label="Breed")
1071
- birds = gr.Dropdown(choices=[25, 50, 75, 100, 150, 200, 500, 1000], value=50, label="Number of birds", allow_custom_value=True)
1072
-
1073
- with gr.Group(elem_classes=["form-card"]):
1074
- gr.HTML('<div class="label"><span class="num">2</span> πŸ“… Expected Age</div>')
1075
- with gr.Row():
1076
- weeks = gr.Dropdown(choices=list(range(0, 21)), value=8, label="Weeks")
1077
- days = gr.Dropdown(choices=list(range(0, 7)), value=0, label="Days")
1078
-
1079
- with gr.Group(elem_classes=["form-card"]):
1080
- gr.HTML('<div class="label"><span class="num">3</span> 🌾 Feed Details</div>')
1081
- with gr.Row():
1082
- bag_size = gr.Dropdown(choices=[10, 20, 25, 50, 100], value=25, label="Bag size (kg)")
1083
- price = gr.Number(value=24000, label="Price per bag (₦)", minimum=0)
1084
-
1085
- with gr.Group(elem_classes=["form-card"]):
1086
- gr.HTML('<div class="label"><span class="num">4</span> 🌍 Your Location</div>')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1087
  detected_country = detect_country_from_env() or "NG"
1088
  country_choices = ["Auto-Detect"] + [code for code, _ in COUNTRY_LIST]
1089
- country_dropdown = gr.Dropdown(choices=country_choices, value="Auto-Detect", label="Country")
 
 
 
 
 
1090
  country_code_state = gr.State(detected_country)
1091
 
1092
- with gr.Group(elem_classes=["form-card"]):
1093
- gr.HTML('<div class="label"><span class="num">5</span> πŸ“§ Your Email</div>')
1094
  email = gr.Textbox(
1095
  label="Email address",
1096
  placeholder="farmer@example.com",
1097
  type="email",
1098
- info="We save your calculation so you can access it later.",
 
1099
  )
1100
 
1101
- calculate_btn = gr.Button("πŸ” Calculate Feed Requirement", variant="primary", size="lg", elem_classes=["calc-btn"])
 
 
 
 
 
1102
  save_status = gr.Markdown("")
1103
 
1104
- # Results
 
 
1105
  gr.Markdown("## πŸ“Š Your Feed Result")
1106
  result = gr.HTML(
1107
  value="""
1108
- <div class="result-clean">
1109
- <div class="hero-box">
1110
- <div class="hero-badge">⚑ QUICK ANSWER</div>
1111
- <div class="hero-number">β€”</div>
1112
- <div class="hero-label">BAGS OF FEED</div>
1113
- <div class="hero-sub">Enter your flock details to calculate</div>
1114
- </div>
 
1115
  </div>
1116
  """,
1117
  elem_classes=["answer-box"],
1118
  )
1119
 
1120
- # Reference / Benchmarks
1121
- with gr.Group(elem_classes=["benchmark-card"]):
1122
- with gr.Row():
1123
- gr.HTML('<div style="font-weight:700;font-size:15px;">πŸ“Š Selected Breed Benchmark</div>')
1124
- selected_breed_badge = gr.HTML('<span style="background:#0B3022;color:#fff;padding:2px 14px;border-radius:20px;font-size:12px;font-weight:700;">Ross 308</span>')
1125
-
1126
- with gr.Accordion("πŸ“Š Weekly benchmark data", open=False):
1127
- benchmark_table = gr.HTML(value=get_benchmark_html("Ross 308"))
1128
-
1129
- with gr.Accordion("πŸ” Breed Comparison", open=False):
1130
- comparison_table = gr.HTML(value=get_comparison_html())
1131
-
1132
- with gr.Accordion("πŸ” Common Broiler Breeds in Nigeria", open=False):
1133
- gr.Markdown("""
1134
- | Breed | Characteristics | Available At |
1135
- |---|---|---|
1136
- | **Ross 308** | Big chest, thick legs, very fast growth. | Agrited, Sayed, Valentine |
1137
- | **Cobb 500** | Early growth, strong legs, solid feed conversion. | Chikun, Zartech |
1138
- | **Arbor Acres** | Balanced growth, hardy and adaptable. | Supreme, CHI, GS, Vertex, Fidan, Cascada, Orobor, Sunchi, Farm Support, Ajila |
1139
- """)
1140
-
1141
- with gr.Accordion("πŸ“Œ How It Works", open=False):
1142
- gr.Markdown(f"""
1143
- - Weeks + days β†’ total days
1144
- - Starter feed: Day 0–{STARTER_END_DAY}
1145
- - Finisher feed: Day {STARTER_END_DAY+1}+
1146
- - Exact bags = total feed Γ· bag size
1147
- - Bags to buy = rounded up to next whole bag
1148
- - Feed cost = bags to buy Γ— price per bag
1149
- """)
1150
-
1151
- # Ask a Question
1152
  with gr.Group(elem_classes=["benchmark-card"]):
1153
- gr.HTML('<div style="font-weight:700;font-size:15px;margin-bottom:4px;">❓ Ask a Question</div>')
1154
- gr.HTML('<div style="color:#5A6E65;font-size:12px;margin-bottom:8px;">Ask a simple poultry-feed question.</div>')
1155
- question = gr.Textbox(label="Your question", placeholder="How many bags for 100 broilers for 6 weeks?", lines=2)
1156
- ask_btn = gr.Button("πŸ’¬ Get Answer", variant="secondary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1157
  question_answer = gr.Markdown("")
1158
 
 
 
 
1159
  gr.HTML("""
1160
- <div style="text-align:center;font-size:11px;color:#5A6E65;margin-top:16px;border-top:1px solid #E2E8E6;padding-top:12px;">
1161
  πŸ” <strong>How Much Feed?</strong> β€” A simple broiler feed planning tool.<br>
1162
  Feed estimates are guides and should be adjusted to actual flock performance.
1163
  </div>
1164
  """)
1165
 
1166
- # Admin (Collapsed)
1167
- with gr.Accordion("πŸ”’ Admin", open=False):
1168
- with gr.Group():
1169
- admin_password_input = gr.Textbox(label="Admin Password", placeholder="Enter password...", type="password")
1170
- with gr.Row():
1171
- login_btn = gr.Button("Login", variant="primary")
1172
- logout_btn = gr.Button("Logout", variant="secondary", visible=False)
1173
  admin_output = gr.Markdown("πŸ”’ Enter the admin password to view analytics.")
 
1174
  with gr.Row():
1175
  export_leads_btn = gr.Button("πŸ“₯ Export Leads (CSV)", variant="secondary")
1176
  export_leads_output = gr.File(label="Leads CSV")
@@ -1181,28 +1818,67 @@ with gr.Blocks(title=APP_NAME, analytics_enabled=False) as demo:
1181
  # ============================================================
1182
  # EVENTS
1183
  # ============================================================
 
 
 
 
 
1184
 
1185
- breed.change(fn=get_benchmark_html, inputs=[breed], outputs=[benchmark_table])
1186
- breed.change(lambda b: f'<span style="background:#0B3022;color:#fff;padding:2px 14px;border-radius:20px;font-size:12px;font-weight:700;">{b}</span>', inputs=[breed], outputs=[selected_breed_badge])
1187
- country_dropdown.change(fn=on_country_change_handler, inputs=[country_dropdown], outputs=[country_code_state, price])
 
 
 
 
 
 
 
 
1188
 
1189
  def calculate_wrapper(birds, weeks, days, bag_size, price_per_bag, email, country_code_state, breed, request: gr.Request):
1190
  if not country_code_state:
1191
  country_code_state = "NG"
1192
- return calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_code_state, breed, request)
 
 
 
1193
 
1194
- calculate_btn.click(fn=calculate_wrapper, inputs=[birds, weeks, days, bag_size, price, email, country_code_state, breed], outputs=[result, save_status])
1195
- ask_btn.click(fn=answer_farmer_question, inputs=[question, email, breed], outputs=[question_answer])
 
 
 
 
 
 
 
 
 
1196
 
1197
- login_btn.click(fn=admin_login, inputs=[admin_password_input], outputs=[admin_output, admin_password_input, logout_btn])
1198
- logout_btn.click(fn=logout_admin, inputs=[], outputs=[admin_output, admin_password_input, logout_btn])
1199
- export_leads_btn.click(fn=export_leads_wrapper, inputs=[], outputs=[export_leads_output])
1200
- export_calc_btn.click(fn=export_calculations_wrapper, inputs=[], outputs=[export_calc_output])
 
 
 
 
 
 
 
 
 
 
 
 
 
1201
 
1202
- def track_visit_async_wrapper(request: gr.Request):
1203
- threading.Thread(target=track_visit_async, args=(request,), daemon=True).start()
 
1204
 
1205
- demo.load(fn=track_visit_async_wrapper, inputs=None, outputs=None)
1206
 
1207
 
1208
  # ============================================================
@@ -1211,10 +1887,12 @@ with gr.Blocks(title=APP_NAME, analytics_enabled=False) as demo:
1211
 
1212
  if __name__ == "__main__":
1213
  port = int(os.environ.get("PORT", 7860))
 
1214
  demo.launch(
1215
  server_name="0.0.0.0",
1216
  server_port=port,
1217
  ssr_mode=False,
1218
  theme=gr.themes.Base(),
 
1219
  footer_links=[],
1220
  )
 
4
  import sqlite3
5
  import json
6
  import urllib.parse
7
+ from datetime import datetime, timezone
8
  from pathlib import Path
9
  import threading
10
 
 
28
 
29
  STARTER_END_DAY = 21
30
 
 
31
  # ============================================================
32
+ # GOOGLE SHEETS INTEGRATION
33
  # ============================================================
34
 
35
+ def init_google_sheets():
36
+ """Initialize Google Sheets connection."""
37
+ try:
38
+ import gspread
39
+ from oauth2client.service_account import ServiceAccountCredentials
40
+
41
+ creds_json = os.environ.get("GOOGLE_SHEETS_CREDENTIALS")
42
+ sheet_id = os.environ.get("GOOGLE_SHEETS_ID")
43
+
44
+ if not creds_json or not sheet_id:
45
+ print("⚠️ Google Sheets not configured. Using local database only.")
46
+ return None, None
47
+
48
+ creds_dict = json.loads(creds_json)
49
+ scope = [
50
+ "https://spreadsheets.google.com/feeds",
51
+ "https://www.googleapis.com/auth/drive"
52
+ ]
53
+
54
+ creds = ServiceAccountCredentials.from_json_keyfile_dict(creds_dict, scope)
55
+ client = gspread.authorize(creds)
56
+
57
+ try:
58
+ sheet = client.open_by_key(sheet_id)
59
+ print(f"βœ… Google Sheets connected: {sheet.title}")
60
+ except:
61
+ sheet = client.create("How Much Feed Calculator Leads")
62
+ print(f"βœ… New Google Sheet created")
63
+
64
+ return client, sheet
65
+
66
+ except Exception as e:
67
+ print(f"⚠️ Google Sheets error: {e}")
68
+ return None, None
69
+
70
+
71
+ # Google Sheets is initialized lazily on first sync.
72
+ # This keeps the public calculator fast to load even when Sheets is not configured.
73
+ gs_client = None
74
+ gs_sheet = None
75
+ gs_initialized = False
76
+ gs_init_lock = threading.Lock()
77
+
78
+ def get_google_sheets():
79
+ global gs_client, gs_sheet, gs_initialized
80
+ if gs_initialized:
81
+ return gs_client, gs_sheet
82
+ with gs_init_lock:
83
+ if not gs_initialized:
84
+ gs_client, gs_sheet = init_google_sheets()
85
+ gs_initialized = True
86
+ return gs_client, gs_sheet
87
+
88
+
89
+ def sync_to_google_sheets(email, country, currency, breed, action="lead", question=""):
90
+ """Sync a lead or farmer question to Google Sheets, if configured."""
91
+ client, sheet = get_google_sheets()
92
+ if not client or not sheet:
93
+ return False
94
+ try:
95
+ if action == "lead":
96
+ worksheet_name = "Leads"
97
+ headers = ["Email", "Country", "Currency", "Breed", "Created At"]
98
+ row_data = [email, country, currency, breed, datetime.now(timezone.utc).isoformat()]
99
+ elif action == "question":
100
+ worksheet_name = "Questions"
101
+ headers = ["Email", "Question", "Breed", "Created At"]
102
+ row_data = [email or "", question or "", breed or "", datetime.now(timezone.utc).isoformat()]
103
+ else:
104
+ return False
105
+ try:
106
+ worksheet = sheet.worksheet(worksheet_name)
107
+ except Exception:
108
+ worksheet = sheet.add_worksheet(title=worksheet_name, rows=1000, cols=20)
109
+ worksheet.append_row(headers)
110
+ worksheet.append_row(row_data)
111
+ print(f"βœ… Synced to Google Sheets: {email or 'anonymous'}")
112
+ return True
113
+ except Exception as e:
114
+ print(f"⚠️ Google Sheets sync error: {e}")
115
+ return False
116
 
117
 
118
  # ============================================================
119
  # VERIFIED BREED DATA - From Official PDFs
120
  # ============================================================
121
 
122
+ # Body Weight (kg) at key days - As-Hatched Performance
123
  BREED_WEIGHT_DATA = {
124
  "Ross 308": {
125
  0: 0.044, 7: 0.213, 14: 0.533, 21: 1.012,
 
135
  }
136
  }
137
 
138
+ # Cumulative Feed (kg) at key days - As-Hatched Performance
139
  BREED_FEED_DATA = {
140
  "Ross 308": {
141
  0: 0.000, 7: 0.166, 14: 0.535, 21: 1.155,
 
151
  }
152
  }
153
 
154
+ # Breed descriptions and availability
155
+ BREED_INFO = {
156
+ "Ross 308": {
157
+ "description": "Big chest, thick legs, very fast growth. Excellent for meat production.",
158
+ "available_at": "Agrited, Sayed, Valentine",
159
+ "color": "#00A86B"
160
+ },
161
+ "Cobb 500": {
162
+ "description": "Good early growth, strong legs, solid feed conversion.",
163
+ "available_at": "Chikun, Zartech",
164
+ "color": "#00A86B"
165
+ },
166
+ "Arbor Acres": {
167
+ "description": "Balanced growth, hardy and adaptable. Great survival rate.",
168
+ "available_at": "Supreme, CHI, GS, Vertex, Fidan, Cascada, Orobor, Sunchi, Farm Support, Ajila",
169
+ "color": "#00A86B"
170
+ }
171
+ }
172
+
173
 
174
  def get_breed_weight_data(breed):
175
  return BREED_WEIGHT_DATA.get(breed, BREED_WEIGHT_DATA["Ross 308"])
 
180
 
181
 
182
  def interpolate_weight(day, breed):
183
+ """Interpolate body weight between known checkpoints."""
184
  weight_data = get_breed_weight_data(breed)
185
+
186
  if day <= 0:
187
  return weight_data[0]
188
  if day >= 56:
189
  return weight_data[56]
190
+
191
  keys = sorted(weight_data.keys())
192
  for i in range(len(keys) - 1):
193
  if keys[i] <= day <= keys[i + 1]:
194
  ratio = (day - keys[i]) / (keys[i + 1] - keys[i])
195
  return weight_data[keys[i]] + (weight_data[keys[i + 1]] - weight_data[keys[i]]) * ratio
196
+
197
  return weight_data[56]
198
 
199
 
200
  def interpolate_feed(day, breed):
201
+ """Interpolate cumulative feed between known checkpoints."""
202
  feed_data = get_breed_feed_data(breed)
203
+
204
  if day <= 0:
205
  return 0.0
206
  if day >= 56:
207
  return feed_data[56]
208
+
209
  keys = sorted(feed_data.keys())
210
  for i in range(len(keys) - 1):
211
  if keys[i] <= day <= keys[i + 1]:
212
  ratio = (day - keys[i]) / (keys[i + 1] - keys[i])
213
  return feed_data[keys[i]] + (feed_data[keys[i + 1]] - feed_data[keys[i]]) * ratio
214
+
215
  return feed_data[56]
216
 
217
 
218
+ # ============================================================
219
+ # BENCHMARK TABLE GENERATOR
220
+ # ============================================================
221
+
222
+ def get_benchmark_table(breed):
223
+ """Generate benchmark table for the selected breed."""
224
+ weight_data = get_breed_weight_data(breed)
225
+ feed_data = get_breed_feed_data(breed)
226
+
227
+ days = [7, 14, 21, 28, 35, 42, 49, 56]
228
+
229
+ table_data = []
230
+ for day in days:
231
+ weight = weight_data.get(day, 0)
232
+ feed = feed_data.get(day, 0)
233
+ fcr = feed / weight if weight > 0 else 0
234
+ table_data.append([day, f"{weight:.3f}", f"{feed:.3f}", f"{fcr:.3f}"])
235
+
236
+ return table_data, days
237
+
238
+
239
+ def get_benchmark_html(breed):
240
+ """Render the selected breed benchmark as lightweight HTML instead of a JS dataframe."""
241
+ table_data, _ = get_benchmark_table(breed)
242
+ rows = []
243
+ for day, weight, feed, fcr in table_data:
244
+ rows.append(
245
+ f"<tr><td>{day}</td><td>{weight} kg</td><td>{feed} kg</td><td>{fcr}</td></tr>"
246
+ )
247
+ return f"""
248
+ <div class="table-wrap benchmark-html">
249
+ <table class="feed-table benchmark-table">
250
+ <thead><tr><th>Days</th><th>Body Weight</th><th>Cumulative Feed</th><th>FCR</th></tr></thead>
251
+ <tbody>{''.join(rows)}</tbody>
252
+ </table>
253
+ </div>
254
+ """
255
+
256
+
257
+ def get_comparison_html():
258
+ """Render the breed comparison as lightweight HTML."""
259
+ data, breeds, _ = get_breed_comparison_table()
260
+ headers = ['Days'] + [f'{b} Weight' for b in breeds] + [f'{b} Feed' for b in breeds]
261
+ # Keep the comparison compact on phones: weight and feed are the most useful fields.
262
+ rows = []
263
+ for row in data:
264
+ day = row[0]
265
+ vals = row[1:]
266
+ weights = vals[0::3]
267
+ feeds = vals[1::3]
268
+ cells = [f'<td>{day}</td>']
269
+ cells += [f'<td>{v} kg</td>' for v in weights]
270
+ cells += [f'<td>{v} kg</td>' for v in feeds]
271
+ rows.append('<tr>' + ''.join(cells) + '</tr>')
272
+ head = ''.join(f'<th>{h}</th>' for h in headers)
273
+ return f"""
274
+ <div class="table-wrap benchmark-html">
275
+ <table class="feed-table benchmark-table comparison-table">
276
+ <thead><tr>{head}</tr></thead>
277
+ <tbody>{''.join(rows)}</tbody>
278
+ </table>
279
+ </div>
280
+ """
281
+
282
+
283
+ def get_breed_comparison_table():
284
+ """Generate comparison table for all three breeds."""
285
+ breeds = ["Ross 308", "Cobb 500", "Arbor Acres"]
286
+ days = [7, 14, 21, 28, 35, 42, 49, 56]
287
+
288
+ table_data = []
289
+ for day in days:
290
+ row = [day]
291
+ for breed in breeds:
292
+ weight_data = get_breed_weight_data(breed)
293
+ feed_data = get_breed_feed_data(breed)
294
+ weight = weight_data.get(day, 0)
295
+ feed = feed_data.get(day, 0)
296
+ fcr = feed / weight if weight > 0 else 0
297
+ row.append(f"{weight:.3f}")
298
+ row.append(f"{feed:.3f}")
299
+ row.append(f"{fcr:.3f}")
300
+ table_data.append(row)
301
+
302
+ return table_data, breeds, days
303
+
304
+
305
  # ============================================================
306
  # CURRENCY DATA
307
  # ============================================================
 
338
 
339
 
340
  def detect_country_from_env():
341
+ """Best-effort country detection from environment variables."""
342
  candidates = [
343
  os.environ.get("COUNTRY_CODE"),
344
  os.environ.get("CF-IPCOUNTRY"),
345
  os.environ.get("GEOIP_COUNTRY"),
346
  os.environ.get("HTTP_CF_IPCOUNTRY"),
347
  ]
348
+
349
  for value in candidates:
350
  if value:
351
  value = value.strip().upper()
352
  if value in CURRENCIES:
353
  return value
354
+
355
  return None
356
 
357
 
358
  def get_country_info(country_code):
359
+ """Get currency info for a country code."""
360
  if country_code in CURRENCIES:
361
  name, symbol, code = CURRENCIES[country_code]
362
  return country_code, symbol, code
 
370
  COUNTRY_LIST = sorted([(code, f"{name} ({symbol})") for code, (name, symbol, curr) in CURRENCIES.items()])
371
 
372
 
373
+ # ============================================================
374
+ # DATABASE
375
+ # ============================================================
376
+
377
+ def init_db():
378
+ conn = sqlite3.connect(DB_PATH)
379
+ cur = conn.cursor()
380
+
381
+ cur.execute("""
382
+ CREATE TABLE IF NOT EXISTS leads (
383
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
384
+ email TEXT UNIQUE,
385
+ country TEXT,
386
+ currency TEXT,
387
+ breed TEXT,
388
+ created_at TEXT
389
+ )
390
+ """)
391
+
392
+ cur.execute("""
393
+ CREATE TABLE IF NOT EXISTS farmer_questions (
394
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
395
+ email TEXT,
396
+ question TEXT,
397
+ breed TEXT,
398
+ created_at TEXT
399
+ )
400
+ """)
401
+
402
+ cur.execute("""
403
+ CREATE TABLE IF NOT EXISTS calculations (
404
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
405
+ email TEXT,
406
+ country TEXT,
407
+ currency TEXT,
408
+ breed TEXT,
409
+ birds INTEGER,
410
+ weeks INTEGER,
411
+ days INTEGER,
412
+ total_days INTEGER,
413
+ bag_size REAL,
414
+ price_per_bag REAL,
415
+ total_feed_kg REAL,
416
+ starter_kg REAL,
417
+ finisher_kg REAL,
418
+ starter_bags REAL,
419
+ finisher_bags REAL,
420
+ total_bags REAL,
421
+ rounded_bags INTEGER,
422
+ total_cost REAL,
423
+ created_at TEXT
424
+ )
425
+ """)
426
+
427
+ cur.execute("""
428
+ CREATE TABLE IF NOT EXISTS analytics (
429
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
430
+ visit_date TEXT,
431
+ ip TEXT,
432
+ country TEXT,
433
+ breed TEXT,
434
+ action TEXT,
435
+ created_at TEXT
436
+ )
437
+ """)
438
+
439
+ conn.commit()
440
+ conn.close()
441
+ print(f"βœ… Database initialized at: {DB_PATH}")
442
+
443
+
444
+ init_db()
445
+
446
+
447
  # ============================================================
448
  # DATABASE HELPERS
449
  # ============================================================
 
459
  def save_lead(email, country, currency, breed):
460
  if not valid_email(email):
461
  return
462
+
463
+ # Save to SQLite
464
  conn = sqlite3.connect(DB_PATH)
465
  cur = conn.cursor()
466
  cur.execute("""
467
  INSERT OR IGNORE INTO leads (email, country, currency, breed, created_at)
468
  VALUES (?, ?, ?, ?, ?)
469
+ """, (email.strip().lower(), country, currency, breed, datetime.now(timezone.utc).isoformat()))
470
  conn.commit()
471
  conn.close()
472
 
473
+ # Sync to Google Sheets
474
+ try:
475
+ sync_to_google_sheets(email, country, currency, breed, "lead")
476
+ except Exception as e:
477
+ print(f"Google Sheets sync error: {e}")
478
+
479
 
480
  def save_question(email, question, breed):
481
  if not question or not question.strip():
482
  return
483
+
484
+ # Save to SQLite
485
  conn = sqlite3.connect(DB_PATH)
486
  cur = conn.cursor()
487
  cur.execute("""
488
  INSERT INTO farmer_questions (email, question, breed, created_at)
489
  VALUES (?, ?, ?, ?)
490
+ """, (email.strip().lower() if email else "", question.strip(), breed, datetime.now(timezone.utc).isoformat()))
491
  conn.commit()
492
  conn.close()
493
 
494
+ # Sync to Google Sheets
495
+ try:
496
+ sync_to_google_sheets(email, "", "", breed, "question", question)
497
+ except Exception as e:
498
+ print(f"Google Sheets sync error: {e}")
499
+
500
 
501
  def save_calculation(email, country, currency, breed, birds, weeks, days, total_days,
502
  bag_size, price_per_bag, total_feed_kg, starter_kg, finisher_kg,
503
  starter_bags, finisher_bags, total_bags, rounded_bags, total_cost):
504
  conn = sqlite3.connect(DB_PATH)
505
  cur = conn.cursor()
506
+
507
  cur.execute("""
508
  INSERT INTO calculations (
509
  email, country, currency, breed, birds, weeks, days, total_days,
 
514
  """, (email.strip().lower() if email else "", country, currency, breed, birds, weeks, days, total_days,
515
  bag_size, price_per_bag, total_feed_kg, starter_kg, finisher_kg,
516
  starter_bags, finisher_bags, total_bags, rounded_bags, total_cost,
517
+ datetime.now(timezone.utc).isoformat()))
518
+
519
  conn.commit()
520
  conn.close()
521
 
522
 
523
+ def track_visit(request: gr.Request):
524
+ """Track page visit."""
525
  try:
526
  client_ip = request.client.host if request and hasattr(request, 'client') else "unknown"
527
+
528
  conn = sqlite3.connect(DB_PATH)
529
  cur = conn.cursor()
530
+
531
  cur.execute("""
532
  INSERT INTO analytics (visit_date, ip, country, action, created_at)
533
  VALUES (?, ?, ?, ?, ?)
 
538
  "page_visit",
539
  datetime.now().isoformat()
540
  ))
541
+
542
  conn.commit()
543
  conn.close()
544
  except Exception as e:
 
546
 
547
 
548
  def track_calculation(breed, birds, days):
549
+ """Track when a calculation is performed."""
550
  try:
551
  conn = sqlite3.connect(DB_PATH)
552
  cur = conn.cursor()
553
+
554
  cur.execute("""
555
  INSERT INTO analytics (visit_date, country, breed, action, created_at)
556
  VALUES (?, ?, ?, ?, ?)
 
561
  f"calculation_{birds}_{days}",
562
  datetime.now().isoformat()
563
  ))
564
+
565
  conn.commit()
566
  conn.close()
567
  return True
 
570
 
571
 
572
  def get_analytics_summary():
573
+ """Get analytics summary."""
574
  conn = sqlite3.connect(DB_PATH)
575
  cur = conn.cursor()
576
+
577
  cur.execute("SELECT COUNT(*) FROM analytics WHERE action = 'page_visit'")
578
  total_visits = cur.fetchone()[0] or 0
579
+
580
  cur.execute("SELECT COUNT(*) FROM analytics WHERE action = 'page_visit' AND visit_date = ?",
581
  (datetime.now().strftime("%Y-%m-%d"),))
582
  today_visits = cur.fetchone()[0] or 0
583
+
584
  cur.execute("SELECT COUNT(*) FROM analytics WHERE action LIKE 'calculation_%'")
585
  total_calculations = cur.fetchone()[0] or 0
586
+
587
  cur.execute("SELECT COUNT(DISTINCT ip) FROM analytics WHERE action = 'page_visit' AND ip != 'unknown'")
588
  unique_visitors = cur.fetchone()[0] or 0
589
+
590
  cur.execute("SELECT breed, COUNT(*) FROM analytics WHERE action LIKE 'calculation_%' GROUP BY breed")
591
  breed_stats = cur.fetchall()
592
+
593
+ # Total leads
594
  cur.execute("SELECT COUNT(*) FROM leads")
595
  total_leads = cur.fetchone()[0] or 0
596
+
597
  conn.close()
598
+
599
  return {
600
  "total_visits": total_visits,
601
  "today_visits": today_visits,
 
611
  # ============================================================
612
 
613
  def get_share_links(app_url="https://howmuchfeed.com"):
614
+ """Generate share links for WhatsApp, Email, Facebook, Twitter."""
615
+
616
  share_text = f"πŸ” Calculate your poultry feed needs at {app_url}"
617
  encoded = urllib.parse.quote(share_text)
618
+
619
+ links = {
620
  "whatsapp": f"https://wa.me/?text={encoded}",
621
  "email": f"mailto:?subject=How Much Feed? - Poultry Feed Calculator&body={encoded}",
622
  "facebook": f"https://www.facebook.com/sharer/sharer.php?u={urllib.parse.quote(app_url)}",
623
  "twitter": f"https://twitter.com/intent/tweet?text={encoded}",
624
  }
625
+
626
+ return links
627
 
628
 
629
  # ============================================================
630
+ # FEED CALCULATION - MINT GREEN & EGG YELLOW DESIGN
631
  # ============================================================
632
 
633
+ def calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_code, breed, request: gr.Request):
634
+ # Validation
635
+ try:
636
+ birds = int(birds)
637
+ weeks = int(weeks)
638
+ days = int(days)
639
+ bag_size = float(bag_size)
640
+ price_per_bag = float(price_per_bag)
641
+ except Exception:
642
+ return "⚠️ Please enter valid numbers.", ""
 
 
 
 
 
 
643
 
644
+ if birds <= 0:
645
+ return "⚠️ Number of birds must be greater than zero.", ""
646
+ if weeks < 0:
647
+ return "⚠️ Weeks cannot be negative.", ""
648
+ if days < 0 or days > 6:
649
+ return "⚠️ Days must be between 0 and 6.", ""
650
+ if bag_size <= 0:
651
+ return "⚠️ Feed bag size must be greater than zero.", ""
652
+ if price_per_bag < 0:
653
+ return "⚠️ Price per bag cannot be negative.", ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
 
655
  if not valid_email(email):
656
  return "πŸ“§ **Email required**\n\nPlease enter your email address before calculating.", ""
657
 
658
+ # Country / Currency
659
  country_name, symbol, currency_code = get_country_info(country_code)
660
  currency_symbol = symbol
661
 
662
+ # Save lead with breed
663
  save_lead(email, country_code, currency_code, breed)
664
+
665
+ # Track calculation
666
  track_calculation(breed, birds, (weeks * 7 + days))
667
 
668
+ # Total days
669
  total_days = (weeks * 7) + days
670
+
671
+ # Total feed
672
  total_feed_per_bird = interpolate_feed(total_days, breed)
673
  total_feed_kg = birds * total_feed_per_bird
674
 
675
+ # Starter / Finisher
676
  starter_days = min(total_days, STARTER_END_DAY)
677
  starter_per_bird = interpolate_feed(starter_days, breed)
678
  starter_kg = birds * starter_per_bird
679
  finisher_kg = max(total_feed_kg - starter_kg, 0)
680
 
681
+ # Bags
682
  starter_bags = starter_kg / bag_size
683
  finisher_bags = finisher_kg / bag_size
684
  total_bags = total_feed_kg / bag_size
685
  rounded_bags = math.ceil(total_bags)
686
  total_cost = rounded_bags * price_per_bag
687
 
688
+ # Expected performance
689
  expected_weight = interpolate_weight(total_days, breed)
690
  flock_weight = expected_weight * birds
691
 
692
+ # Save calculation
693
  save_calculation(email, country_code, currency_code, breed, birds, weeks, days, total_days,
694
  bag_size, price_per_bag, total_feed_kg, starter_kg, finisher_kg,
695
  starter_bags, finisher_bags, total_bags, rounded_bags, total_cost)
696
 
697
+ # Breed info
698
+ breed_info = BREED_INFO.get(breed, BREED_INFO["Ross 308"])
699
+ breed_color = breed_info["color"]
700
+
701
+ # Generate share links
702
  share_links = get_share_links()
703
+
704
+ # Output HTML - mobile-first farmer-friendly result
705
  starter_bags_display = f"{starter_bags:.1f}"
706
  finisher_bags_display = f"{finisher_bags:.1f}"
707
+
708
  age_text = f"{weeks} week{'s' if weeks != 1 else ''}"
709
  if days:
710
  age_text += f" + {days} day{'s' if days != 1 else ''}"
711
 
712
+ share_text = (
713
+ f"How Much Feed?\n"
714
+ f"{breed} broilers: {birds:,} birds for {age_text}.\n"
715
+ f"Feed needed: {total_feed_kg:,.1f} kg ({total_bags:.1f} bags).\n"
716
+ f"Buy {rounded_bags} bags."
717
+ )
718
+ share_encoded = urllib.parse.quote(share_text)
719
+ share_links = {
720
+ "whatsapp": f"https://wa.me/?text={share_encoded}",
721
+ "email": f"mailto:?subject=My How Much Feed result&body={share_encoded}",
722
+ "facebook": f"https://www.facebook.com/sharer/sharer.php?u={urllib.parse.quote('https://howmuchfeed.com')}",
723
+ "twitter": f"https://twitter.com/intent/tweet?text={share_encoded}",
724
+ }
725
+
726
  html = f"""
727
+ <div class="results-shell">
728
+ <section class="buy-card">
729
+ <div class="eyebrow">YOUR FEED RESULT</div>
730
+ <div class="buy-label">BUY</div>
731
+ <div class="buy-number">{rounded_bags}</div>
732
+ <div class="buy-unit">BAGS</div>
733
+ <div class="buy-note">Recommended amount, rounded up to the next whole bag.</div>
734
+ </section>
735
+
736
+ <div class="result-context">
737
+ <strong>{birds:,} {breed} broilers</strong> Β· {age_text} ({total_days} days)
738
  </div>
739
 
740
+ <p class="plain-answer">
 
741
  You need <strong>{total_feed_kg:,.1f} kg</strong> of feed, equal to about
742
  <strong>{total_bags:.1f} bags</strong> of {bag_size:g} kg feed.
743
+ </p>
744
 
745
+ <div class="metric-grid">
746
+ <div class="metric-card">
747
+ <span class="metric-icon">βš–οΈ</span>
748
+ <span class="metric-label">Feed needed</span>
749
+ <strong>{total_feed_kg:,.1f} kg</strong>
 
750
  </div>
751
+ <div class="metric-card">
752
+ <span class="metric-icon">βœ“</span>
753
+ <span class="metric-label">Exact amount</span>
754
+ <strong>{total_bags:.1f} bags</strong>
755
  </div>
756
+ <div class="metric-card">
757
+ <span class="metric-icon">₦</span>
758
+ <span class="metric-label">Estimated cost</span>
759
+ <strong>{currency_symbol}{total_cost:,.0f}</strong>
760
  </div>
761
+ <div class="metric-card">
762
+ <span class="metric-icon">πŸ“…</span>
763
+ <span class="metric-label">Growing period</span>
764
+ <strong>{total_days} days</strong>
765
  </div>
766
  </div>
767
 
768
+ <section class="result-section">
 
769
  <div class="section-title">🌾 Feed Breakdown</div>
770
+ <div class="table-wrap">
771
+ <table class="feed-table">
772
+ <thead>
773
+ <tr><th>Stage</th><th>Feed</th><th>Bags</th></tr>
774
+ </thead>
775
+ <tbody>
776
+ <tr><td>Starter <span class="muted">(0–21 days)</span></td><td>{starter_kg:,.1f} kg</td><td>{starter_bags_display}</td></tr>
777
+ <tr><td>Finisher <span class="muted">(Day 22+)</span></td><td>{finisher_kg:,.1f} kg</td><td>{finisher_bags_display}</td></tr>
778
+ <tr class="total-row"><td>TOTAL</td><td>{total_feed_kg:,.1f} kg</td><td>{total_bags:.1f} bags</td></tr>
779
+ </tbody>
780
+ </table>
 
 
 
 
 
781
  </div>
782
+ </section>
783
 
784
+ <section class="cost-card">
 
785
  <div class="section-title">πŸ’° Feed Cost Estimate</div>
786
+ <div class="cost-main">{currency_symbol}{total_cost:,.0f}</div>
787
  <div class="cost-note">Based on {rounded_bags} bags Γ— {currency_symbol}{price_per_bag:,.0f} per bag.</div>
788
+ </section>
789
 
790
+ <section class="result-section performance-section">
 
791
  <div class="section-title">πŸ“ˆ Expected Performance</div>
792
+ <div class="performance-grid">
793
+ <div><span>Body weight per bird</span><strong>{expected_weight:.3f} kg</strong></div>
794
  <div><span>Flock live weight</span><strong>{flock_weight:,.1f} kg</strong></div>
795
  </div>
796
+ </section>
797
 
798
+ <section class="result-section share-section">
799
+ <div class="section-title">πŸ“€ Share Result</div>
 
800
  <div class="share-grid">
801
+ <a class="share-btn whatsapp" href="{share_links['whatsapp']}" target="_blank" rel="noopener">WhatsApp</a>
802
+ <a class="share-btn email" href="{share_links['email']}" target="_blank" rel="noopener">Email</a>
803
+ <a class="share-btn facebook" href="{share_links['facebook']}" target="_blank" rel="noopener">Facebook</a>
804
+ <a class="share-btn twitter" href="{share_links['twitter']}" target="_blank" rel="noopener">X</a>
805
  </div>
806
+ </section>
807
 
808
+ <div class="result-disclaimer">
809
+ ⚠️ This is an estimate based on the selected {breed} benchmark. Actual feed use can vary with feed quality, management, environment and bird performance.
810
  </div>
811
  </div>
812
  """
 
815
 
816
 
817
  # ============================================================
818
+ # QUESTION / ANSWER
819
  # ============================================================
820
 
821
+ def answer_farmer_question(question, email, breed):
822
+ if not question or not question.strip():
823
+ return "Please type or speak your question first."
824
+
825
+ save_question(email or "", question, breed)
826
+
827
+ q = question.lower().strip()
828
+
829
+ if "bag" in q and ("feed" in q or "birds" in q):
830
+ answer = """
831
+ ### Feed-bag questions
832
+
833
+ For an exact feed estimate, use the calculator above.
834
+
835
+ Enter:
836
+ 1. Number of birds
837
+ 2. Age in weeks + days
838
+ 3. Feed bag size
839
+ 4. Price per bag
840
+ 5. Your email
841
+ 6. Breed
842
+
843
+ The calculator will give you the estimated total bags, starter/finisher breakdown and feed cost.
844
+ """
845
+ elif "breed" in q or "ross" in q or "cobb" in q or "arbor" in q:
846
+ answer = """
847
+ ### Common Broiler Breeds in Nigeria
848
+
849
+ | Breed | Characteristics | Available At |
850
+ |-------|-----------------|--------------|
851
+ | **Ross 308** | Big chest, thick legs, very fast growth | Agrited, Sayed, Valentine |
852
+ | **Cobb 500** | Good early growth, strong legs | Chikun, Zartech |
853
+ | **Arbor Acres** | Balanced growth, hardy, adaptable | Supreme, CHI, GS, Vertex, Fidan, Cascada, Orobor, Sunchi, Farm Support, Ajila |
854
+
855
+ Tip: Before buying chicks, ask about the breed and hatchery. Not all broilers are the same!
856
+ """
857
+ elif "starter" in q:
858
+ answer = f"""
859
+ ### Starter feed
860
+
861
+ Starter feed is used during the early growth period (Day 0–{STARTER_END_DAY}).
862
+
863
+ The calculator separates the estimated feed requirement into **Starter** and **Finisher** based on the selected age.
864
+ """
865
+ elif "finisher" in q:
866
+ answer = f"""
867
+ ### Finisher feed
868
+
869
+ Finisher feed is used after day {STARTER_END_DAY}.
870
+
871
+ Your calculator automatically estimates the finisher requirement from the selected number of birds and total age.
872
+ """
873
+ elif "cost" in q or "price" in q:
874
+ answer = """
875
+ ### Feed cost
876
+
877
+ Enter the current price of one feed bag in the calculator.
878
+
879
+ The app calculates: **Number of bags Γ— price per bag = estimated feed cost**
880
+
881
+ The result is for **feed alone**.
882
+ """
883
+ else:
884
+ answer = f"""
885
+ ### Question received
886
+
887
+ **β€œ{question.strip()}”**
888
+
889
+ Your question has been saved.
890
+
891
+ We can use the questions farmers submit to decide which answers, calculators and features should be added to the app next.
892
+ """
893
+
894
+ return answer
895
+
896
+
897
+ # ============================================================
898
+ # UPDATE FUNCTIONS
899
+ # ============================================================
900
+
901
+ def update_benchmark(breed):
902
+ """Update the benchmark table when breed changes."""
903
+ return get_benchmark_html(breed)
904
+
905
+
906
+ def update_selected_breed_badge(breed):
907
+ """Update the always-visible selected-breed badge."""
908
+ safe_breed = breed or "Ross 308"
909
+ return f'<span class="selected-breed-badge">{safe_breed}</span>'
910
+
911
+
912
  def on_country_change_handler(country_selection):
913
+ """Update country state and default feed price when country changes."""
914
  if country_selection == "Auto-Detect":
915
  country_code = detect_country_from_env() or "NG"
916
  else:
917
  country_code = country_selection
918
+
919
  _, symbol, currency_code = get_country_info(country_code)
920
  default_price = get_default_price(currency_code)
921
  return country_code, default_price
 
925
  # ADMIN FUNCTIONS
926
  # ============================================================
927
 
928
+ def show_analytics(password):
929
+ """Display analytics summary - only with correct password."""
930
+ if password != ADMIN_PASSWORD:
931
+ return """
932
+ ### Admin Access Required
933
+
934
+ Please enter the correct admin password to view analytics.
935
+ """
936
+
937
+ stats = get_analytics_summary()
938
+
939
+ breed_stats_html = ""
940
+ for breed, count in stats["breed_stats"]:
941
+ breed_stats_html += f"<li><strong>{breed}</strong>: {count} calculations</li>"
942
+
943
+ if not breed_stats_html:
944
+ breed_stats_html = "<li>No calculations yet</li>"
945
+
946
+ return f"""
947
+ ### How Much Feed? - Analytics
948
+
949
+ | Metric | Value |
950
+ |--------|-------|
951
+ | **Total Visits** | **{stats['total_visits']:,}** |
952
+ | **Today's Visits** | **{stats['today_visits']}** |
953
+ | **Unique Visitors** | **{stats['unique_visitors']:,}** |
954
+ | **Total Calculations** | **{stats['total_calculations']:,}** |
955
+ | **Total Leads (Emails)** | **{stats['total_leads']:,}** |
956
+
957
+ ### Most Popular Breeds
958
+
959
+ <ul>{breed_stats_html}</ul>
960
+
961
+ ---
962
+ *Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
963
+ """
964
 
965
 
966
+ def admin_login(password):
967
+ """Check admin password and return the dashboard text."""
968
+ return show_analytics(password)
969
 
970
 
971
+ def export_leads_to_csv():
972
+ """Export all leads to CSV format."""
973
  conn = sqlite3.connect(DB_PATH)
974
  cur = conn.cursor()
975
  cur.execute("SELECT email, country, currency, breed, created_at FROM leads ORDER BY created_at DESC")
976
  rows = cur.fetchall()
977
  conn.close()
978
+
979
  import csv
980
  from io import StringIO
981
+
982
  output = StringIO()
983
  writer = csv.writer(output)
984
  writer.writerow(["Email", "Country", "Currency", "Breed", "Created At"])
985
  writer.writerows(rows)
986
+
987
+ return output.getvalue()
988
+
989
+
990
+ def export_leads_wrapper():
991
+ """Export leads to CSV file for download."""
992
+ import tempfile
993
+ csv_data = export_leads_to_csv()
994
  temp = tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False, encoding='utf-8')
995
+ temp.write(csv_data)
996
  temp.close()
997
  return temp.name
998
 
999
 
1000
  def export_calculations_wrapper():
1001
+ """Export calculations to CSV file for download."""
1002
  conn = sqlite3.connect(DB_PATH)
1003
  cur = conn.cursor()
1004
+ cur.execute("""
1005
+ SELECT email, breed, birds, total_days, rounded_bags, bag_size, total_cost, created_at
1006
+ FROM calculations
1007
+ ORDER BY created_at DESC
1008
+ """)
1009
  rows = cur.fetchall()
1010
  conn.close()
1011
+
1012
  import csv
1013
+ import tempfile
1014
+
 
 
 
1015
  temp = tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False, encoding='utf-8')
1016
+ writer = csv.writer(temp)
1017
+ writer.writerow(["Email", "Breed", "Birds", "Total Days", "Bags", "Bag Size (kg)", "Total Cost", "Created At"])
1018
+ writer.writerows(rows)
1019
  temp.close()
1020
+
1021
  return temp.name
1022
 
1023
 
1024
  # ============================================================
1025
+ # CSS - MINT GREEN & EGG YELLOW DESIGN SYSTEM
1026
  # ============================================================
1027
 
1028
  CSS = """
1029
+ :root {
1030
+ --app-bg-main: #f4f7f5;
1031
+ --app-card-surface: #ffffff;
1032
+ --brand-primary-mint: #00a86b;
1033
+ --brand-dark-charcoal: #0b3022;
1034
+ --brand-accent-yellow: #ffb800;
1035
+ --ui-border-muted: #d6e0db;
1036
+ --text-helper: #5a6e65;
1037
+
1038
+ /* Compatibility aliases used throughout the app */
1039
+ --mint: var(--brand-primary-mint);
1040
+ --mint-dark: var(--brand-dark-charcoal);
1041
+ --mint-soft: #eaf8f1;
1042
+ --yellow: var(--brand-accent-yellow);
1043
+ --yellow-soft: #fff7db;
1044
+ --surface: var(--app-card-surface);
1045
+ --page: var(--app-bg-main);
1046
+ --border: var(--ui-border-muted);
1047
+ --muted: var(--text-helper);
1048
+ --danger: #b42318;
1049
+ }
1050
+
1051
+ /* =========================
1052
+ GLOBAL MOBILE-FIRST UI
1053
+ ========================= */
1054
+ html, body {
1055
+ margin: 0 !important;
1056
+ padding: 0 !important;
1057
+ background: var(--page) !important;
1058
+ color: var(--mint-dark) !important;
1059
  color-scheme: light !important;
1060
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif !important;
1061
+ -webkit-text-size-adjust: 100%;
1062
+ }
1063
+
1064
+ body,
1065
+ .gradio-container,
1066
+ .gradio-container > .main,
1067
+ .gradio-container .block,
1068
+ .gradio-container .gr-group,
1069
+ .gradio-container .gr-box,
1070
+ .gradio-container .gr-panel {
1071
+ background: var(--page) !important;
1072
+ color: var(--mint-dark) !important;
1073
  }
1074
 
1075
  .gradio-container {
1076
+ width: 100% !important;
1077
+ max-width: 1180px !important;
1078
  margin: 0 auto !important;
1079
+ padding: 0 12px 48px !important;
1080
+ box-sizing: border-box !important;
1081
+ color-scheme: light !important;
1082
+ --body-background-fill: #f4f7f5 !important;
1083
+ --body-background-fill-dark: #f4f7f5 !important;
1084
+ --body-text-color: #0b3022 !important;
1085
+ --body-text-color-dark: #0b3022 !important;
1086
+ --body-text-color-subdued: #5a6e65 !important;
1087
+ --body-text-color-subdued-dark: #5a6e65 !important;
1088
+ --background-fill-primary: #ffffff !important;
1089
+ --background-fill-primary-dark: #ffffff !important;
1090
+ --background-fill-secondary: #f4f7f5 !important;
1091
+ --background-fill-secondary-dark: #f4f7f5 !important;
1092
+ --border-color-primary: #d6e0db !important;
1093
+ --border-color-primary-dark: #d6e0db !important;
1094
+ }
1095
+
1096
+ /* Never force a dark theme onto the mobile browser. */
1097
+ .dark body,
1098
+ .dark .gradio-container,
1099
+ .dark .gradio-container .block,
1100
+ .dark .gradio-container .gr-group,
1101
+ .dark .gradio-container .gr-box,
1102
+ .dark .gradio-container .gr-panel {
1103
+ background: var(--page) !important;
1104
+ color: var(--mint-dark) !important;
1105
  }
1106
 
1107
+ h1, h2, h3, h4, p, label, span, div {
1108
+ color: inherit;
 
1109
  }
1110
 
1111
+ /* =========================
1112
+ HEADER
1113
+ ========================= */
1114
  .app-header {
1115
+ background: var(--mint-dark);
1116
+ color: #fff;
1117
+ border-radius: 0 0 18px 18px;
1118
+ padding: 18px 16px 20px;
1119
+ margin: 0 -12px 16px;
1120
+ }
1121
+ .app-header .brand-row {
1122
+ display: flex;
1123
+ align-items: center;
1124
+ gap: 10px;
1125
+ }
1126
+ .app-header .brand-icon { font-size: 30px; line-height: 1; }
1127
+ .app-header h1 {
1128
+ color: #fff !important;
1129
+ margin: 0;
1130
+ font-size: clamp(23px, 6vw, 32px);
1131
+ line-height: 1.1;
1132
+ font-weight: 800;
1133
+ }
1134
+ .app-header p {
1135
+ color: rgba(255,255,255,.88) !important;
1136
+ margin: 6px 0 0 40px;
1137
+ font-size: 14px;
1138
+ line-height: 1.45;
1139
+ }
1140
+
1141
+ /* =========================
1142
+ STATUS / FORM SECTIONS
1143
+ ========================= */
1144
+ .status-card {
1145
+ background: var(--surface);
1146
  border: 1px solid var(--border);
1147
+ border-left: 4px solid var(--mint);
1148
  border-radius: 12px;
1149
  padding: 12px 14px;
1150
+ margin-bottom: 12px;
 
 
 
1151
  font-size: 13px;
1152
+ }
1153
+ .status-card strong { color: var(--mint-dark) !important; }
1154
+ .status-card a { color: var(--mint) !important; font-weight: 700; }
1155
+
1156
+ .form-section {
1157
+ background: var(--surface) !important;
1158
+ border: 1px solid var(--border) !important;
1159
+ border-radius: 14px !important;
1160
+ margin-bottom: 12px !important;
1161
+ overflow: visible !important;
1162
+ padding: 14px !important;
1163
+ box-shadow: 0 2px 8px rgba(11,48,34,.04) !important;
1164
+ }
1165
+ .section-heading {
1166
  display: flex;
1167
  align-items: center;
1168
+ gap: 9px;
1169
+ margin: 0 0 12px;
1170
+ color: var(--mint-dark) !important;
1171
+ font-size: 17px;
1172
+ font-weight: 800;
1173
  }
1174
+ .step-number {
1175
+ width: 28px;
1176
+ height: 28px;
1177
  border-radius: 50%;
1178
+ background: var(--mint-dark);
1179
+ color: #fff !important;
1180
  display: inline-flex;
1181
  align-items: center;
1182
  justify-content: center;
1183
+ font-size: 13px;
1184
  font-weight: 800;
1185
+ flex: 0 0 28px;
1186
  }
1187
 
1188
+ /* Gradio field controls */
1189
+ .gradio-container input,
1190
+ .gradio-container textarea,
1191
+ .gradio-container select,
1192
+ .gradio-container [role="combobox"] {
1193
  background: #fff !important;
1194
+ color: var(--mint-dark) !important;
1195
+ border: 1px solid #C8D5CE !important;
1196
+ border-radius: 10px !important;
1197
+ min-height: 48px !important;
1198
+ box-sizing: border-box !important;
1199
+ font-size: 16px !important;
1200
  }
1201
+ .gradio-container input:focus,
1202
+ .gradio-container textarea:focus,
1203
+ .gradio-container select:focus,
1204
+ .gradio-container [role="combobox"]:focus-within {
1205
  border-color: var(--mint) !important;
1206
+ box-shadow: 0 0 0 3px rgba(0,168,107,.16) !important;
1207
+ }
1208
+ .gradio-container label {
1209
+ color: var(--mint-dark) !important;
1210
+ font-weight: 650 !important;
1211
+ }
1212
+ .gradio-container .info,
1213
+ .gradio-container small {
1214
+ color: var(--muted) !important;
1215
+ }
1216
+
1217
+ /* Give Weeks and Days a real touch gap on mobile. */
1218
+ .age-row,
1219
+ .feed-row {
1220
+ gap: 10px !important;
1221
+ }
1222
+ .age-row > *,
1223
+ .feed-row > * {
1224
+ min-width: 0 !important;
1225
+ }
1226
+
1227
+ .email-box textarea,
1228
+ .email-box input {
1229
+ background: #fff !important;
1230
+ border: 2px solid var(--mint) !important;
1231
  }
 
 
1232
 
1233
+ /* =========================
1234
+ PRIMARY ACTION
1235
+ ========================= */
1236
+ .calculate-btn button {
1237
  width: 100% !important;
1238
+ min-height: 52px !important;
1239
+ border: 0 !important;
1240
+ border-radius: 12px !important;
1241
  background: var(--mint) !important;
1242
  color: #fff !important;
1243
+ font-size: 16px !important;
 
 
1244
  font-weight: 800 !important;
1245
+ box-shadow: 0 5px 14px rgba(0,168,107,.20) !important;
 
 
 
 
 
 
 
 
 
 
1246
  }
1247
+ .calculate-btn button:hover { background: #008F5A !important; }
1248
+
1249
+ /* =========================
1250
+ RESULT HTML
1251
+ ========================= */
1252
+ .results-shell {
1253
+ width: 100%;
1254
+ box-sizing: border-box;
1255
+ color: var(--mint-dark);
1256
  }
1257
+ .buy-card {
1258
+ background: linear-gradient(145deg, #F1FBF6 0%, #FFFFFF 100%);
1259
+ border: 2px solid var(--mint);
1260
+ border-radius: 16px;
1261
+ padding: 18px 16px;
 
1262
  text-align: center;
1263
+ box-shadow: 0 5px 16px rgba(11,48,34,.08);
1264
  }
1265
+ .eyebrow {
1266
+ color: var(--mint) !important;
1267
+ font-size: 12px;
1268
+ font-weight: 800;
1269
+ letter-spacing: .09em;
 
1270
  }
1271
+ .buy-label {
1272
+ margin-top: 7px;
1273
+ color: var(--mint-dark) !important;
1274
+ font-size: 13px;
1275
+ font-weight: 800;
1276
+ }
1277
+ .buy-number {
1278
+ color: var(--mint) !important;
1279
+ font-size: clamp(62px, 18vw, 92px);
1280
  font-weight: 900;
1281
+ line-height: .9;
1282
+ margin-top: 2px;
1283
  }
1284
+ .buy-unit {
1285
+ color: var(--mint-dark) !important;
1286
+ font-size: 21px;
1287
+ font-weight: 900;
1288
+ letter-spacing: .08em;
1289
  }
1290
+ .buy-note {
1291
+ margin-top: 8px;
1292
+ color: var(--muted) !important;
1293
+ font-size: 12px;
1294
  }
1295
+ .result-context {
1296
+ text-align: center;
1297
+ margin: 12px 2px 6px;
1298
+ color: var(--muted) !important;
1299
+ font-size: 13px;
1300
  }
1301
+ .result-context strong { color: var(--mint-dark) !important; }
1302
+ .plain-answer {
1303
+ background: #fff;
 
1304
  border-left: 4px solid var(--yellow);
1305
+ border-radius: 10px;
1306
+ padding: 11px 12px;
1307
  margin: 10px 0 12px;
1308
  font-size: 14px;
1309
  line-height: 1.5;
1310
+ color: var(--mint-dark) !important;
1311
  }
1312
+ .plain-answer strong { color: var(--mint-dark) !important; }
1313
 
1314
+ .metric-grid {
 
1315
  display: grid;
1316
+ grid-template-columns: repeat(2, minmax(0,1fr));
1317
+ gap: 9px;
1318
+ margin-bottom: 14px;
1319
  }
1320
+ .metric-card {
1321
+ background: #fff;
1322
  border: 1px solid var(--border);
1323
+ border-radius: 12px;
1324
+ padding: 11px 9px;
1325
+ min-width: 0;
1326
  }
1327
+ .metric-icon {
1328
+ display: block;
1329
+ color: var(--mint) !important;
1330
  font-size: 18px;
1331
+ font-weight: 900;
 
 
1332
  }
1333
+ .metric-label {
1334
+ display: block;
1335
+ color: var(--muted) !important;
1336
+ font-size: 11px;
1337
+ margin: 2px 0 3px;
1338
  }
1339
+ .metric-card strong {
1340
+ display: block;
1341
+ color: var(--mint-dark) !important;
1342
+ font-size: 17px;
1343
+ line-height: 1.2;
1344
+ overflow-wrap: anywhere;
 
 
1345
  }
1346
+ .result-section { margin: 14px 0; }
1347
  .section-title {
1348
+ color: var(--mint-dark) !important;
1349
+ font-size: 14px;
1350
+ font-weight: 850;
1351
  margin-bottom: 8px;
 
1352
  }
1353
+ .table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
1354
+ .feed-table {
1355
+ width: 100%;
1356
+ min-width: 330px;
1357
+ border-collapse: separate;
1358
+ border-spacing: 0;
1359
+ background: #fff;
1360
+ border: 1px solid var(--border);
1361
+ border-radius: 11px;
1362
+ overflow: hidden;
1363
+ font-size: 12px;
1364
  }
1365
+ .feed-table th {
1366
+ background: var(--mint) !important;
1367
+ color: #fff !important;
1368
+ padding: 9px 8px;
1369
+ text-align: left;
1370
+ font-weight: 800;
 
1371
  }
1372
+ .feed-table th:not(:first-child),
1373
+ .feed-table td:not(:first-child) { text-align: right; }
1374
+ .feed-table td {
1375
+ padding: 9px 8px;
1376
+ border-top: 1px solid var(--border);
1377
+ color: var(--mint-dark) !important;
1378
+ white-space: nowrap;
1379
  }
1380
+ .feed-table .muted { color: var(--muted) !important; font-size: 10px; }
1381
+ .feed-table .total-row td {
1382
+ background: var(--mint-soft) !important;
1383
+ font-weight: 900 !important;
1384
  }
1385
+ .feed-table .total-row td:last-child { font-size: 14px; }
1386
 
1387
+ .cost-card {
 
1388
  background: var(--yellow-soft);
1389
  border: 1px solid #EBC34A;
1390
+ border-left: 4px solid var(--yellow);
1391
+ border-radius: 12px;
1392
+ padding: 13px 14px;
1393
+ margin: 14px 0;
1394
  }
1395
+ .cost-main {
1396
+ color: var(--mint-dark) !important;
1397
+ font-size: clamp(27px, 8vw, 38px);
1398
+ line-height: 1.1;
1399
  font-weight: 900;
 
 
 
 
 
 
1400
  }
1401
+ .cost-note { color: var(--muted) !important; font-size: 11px; margin-top: 5px; }
1402
 
1403
+ .performance-grid { display: grid; gap: 0; background: #fff; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
1404
+ .performance-grid > div { display: flex; justify-content: space-between; gap: 12px; padding: 11px; border-top: 1px solid var(--border); font-size: 13px; }
1405
+ .performance-grid > div:first-child { border-top: 0; }
1406
+ .performance-grid span { color: var(--muted) !important; }
1407
+ .performance-grid strong { color: var(--mint) !important; white-space: nowrap; }
1408
+
1409
+ .share-grid { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 8px; }
1410
+ .share-btn {
1411
+ display: block;
1412
+ text-align: center;
1413
+ text-decoration: none !important;
1414
+ background: var(--mint);
1415
+ color: #fff !important;
1416
+ border-radius: 9px;
1417
+ padding: 11px 8px;
1418
+ font-size: 12px;
1419
+ font-weight: 800;
1420
+ min-height: 44px;
1421
+ box-sizing: border-box;
1422
+ }
1423
+ .share-btn:hover { background: #008F5A; }
1424
+ .result-disclaimer {
1425
+ background: #F1F6F3;
1426
  border: 1px solid var(--border);
1427
  border-radius: 10px;
1428
+ padding: 10px 11px;
1429
+ color: var(--muted) !important;
1430
+ font-size: 10px;
1431
+ line-height: 1.45;
1432
+ margin-top: 14px;
1433
  }
1434
+
1435
+ /* =========================
1436
+ BENCHMARK / REFERENCE CONTENT
1437
+ ========================= */
1438
+ .benchmark-card {
1439
+ background: var(--app-card-surface);
1440
+ border: 1px solid var(--ui-border-muted);
1441
+ border-radius: 14px;
1442
+ padding: 14px;
1443
+ margin: 16px 0 12px;
1444
+ box-shadow: 0 2px 8px rgba(11,48,34,.04);
1445
  }
1446
+ .benchmark-heading {
1447
  display: flex;
1448
+ align-items: center;
1449
  justify-content: space-between;
1450
+ gap: 12px;
1451
+ flex-wrap: wrap;
1452
+ margin-bottom: 10px;
1453
+ }
1454
+ .benchmark-title {
1455
+ color: var(--brand-dark-charcoal) !important;
1456
+ font-size: 16px;
1457
+ font-weight: 850;
1458
+ }
1459
+ .selected-breed-badge {
1460
+ display: inline-flex;
1461
+ align-items: center;
1462
+ min-height: 34px;
1463
+ padding: 6px 12px;
1464
+ border-radius: 999px;
1465
+ background: var(--brand-dark-charcoal);
1466
+ color: #fff !important;
1467
  font-size: 13px;
1468
+ font-weight: 800;
1469
+ white-space: nowrap;
1470
+ }
1471
+ .benchmark-help {
1472
+ color: var(--text-helper) !important;
1473
+ font-size: 12px;
1474
+ line-height: 1.45;
1475
+ margin: 0 0 10px;
1476
  }
1477
+ .benchmark-accordion { margin-top: 10px !important; }
 
 
1478
 
1479
+ /* Ask a Question stays visible because it is a farmer-facing feature. */
1480
+ .question-card {
1481
+ background: var(--app-card-surface);
1482
+ border: 1px solid var(--ui-border-muted);
1483
+ border-radius: 14px;
1484
+ padding: 14px;
1485
+ margin: 14px 0;
1486
+ box-shadow: 0 2px 8px rgba(11,48,34,.04);
1487
  }
1488
+ .question-heading {
1489
+ color: var(--brand-dark-charcoal) !important;
1490
+ font-size: 16px;
1491
+ font-weight: 850;
1492
+ margin-bottom: 5px;
1493
  }
1494
+ .question-help {
1495
+ color: var(--text-helper) !important;
 
 
 
 
1496
  font-size: 12px;
1497
+ margin-bottom: 10px;
1498
+ }
1499
+
1500
+ /* Traditional social colours: recognizable at a glance. */
1501
+ .share-btn.whatsapp { background: #25D366 !important; }
1502
+ .share-btn.email { background: #EA4335 !important; }
1503
+ .share-btn.facebook { background: #1877F2 !important; }
1504
+ .share-btn.twitter { background: #000000 !important; }
1505
+ .share-btn.whatsapp:hover { background: #1ebe5d !important; }
1506
+ .share-btn.email:hover { background: #d93025 !important; }
1507
+ .share-btn.facebook:hover { background: #166fe5 !important; }
1508
+ .share-btn.twitter:hover { background: #222 !important; }
1509
+
1510
+ /* Replace Gradio's default blue emphasis with the brand system. */
1511
+ .gradio-container button.primary,
1512
+ .gradio-container button[data-variant="primary"] {
1513
+ background: var(--brand-primary-mint) !important;
1514
+ color: #fff !important;
1515
+ border-color: var(--brand-primary-mint) !important;
1516
+ }
1517
+ .gradio-container button.secondary,
1518
+ .gradio-container button[data-variant="secondary"] {
1519
+ background: var(--brand-dark-charcoal) !important;
1520
  color: #fff !important;
1521
+ border-color: var(--brand-dark-charcoal) !important;
1522
+ }
1523
+ .gradio-container button:focus-visible {
1524
+ outline: 3px solid rgba(255,184,0,.45) !important;
1525
+ outline-offset: 2px;
1526
  }
 
 
 
 
1527
 
1528
+ /* =========================
1529
+ COLLAPSIBLE LOWER CONTENT
1530
+ ========================= */
1531
+ .gradio-container details,
1532
+ .gradio-container .gr-accordion {
1533
+ background: #fff !important;
1534
+ border: 1px solid var(--border) !important;
1535
+ border-radius: 12px !important;
1536
+ margin-bottom: 10px !important;
1537
+ overflow: hidden !important;
1538
+ }
1539
+ .gradio-container summary,
1540
+ .gradio-container .gr-accordion > button {
1541
+ color: var(--mint-dark) !important;
1542
+ font-weight: 800 !important;
1543
+ }
1544
+
1545
+ /* Dataframes: allow horizontal scrolling on phones. */
1546
+ .benchmark-wrap { overflow-x: auto !important; -webkit-overflow-scrolling: touch !important; }
1547
+ .benchmark-wrap .table-wrap { overflow-x: auto !important; }
1548
+
1549
+ /* =========================
1550
+ ADMIN / FOOTER
1551
+ ========================= */
1552
+ .admin-login {
1553
+ background: #fff !important;
1554
+ border: 1px solid var(--border) !important;
1555
+ border-left: 4px solid var(--yellow) !important;
1556
+ border-radius: 12px !important;
1557
+ padding: 12px !important;
1558
+ }
1559
+ .admin-footer {
1560
+ margin-top: 24px;
1561
  padding-top: 10px;
1562
+ border-top: 1px solid var(--ui-border-muted);
1563
+ }
1564
+ .admin-footer summary,
1565
+ .admin-footer .gr-accordion > button {
1566
+ color: var(--text-helper) !important;
1567
+ font-size: 12px !important;
1568
+ font-weight: 650 !important;
1569
+ }
1570
+ .footer-note { color: var(--muted) !important; font-size: 11px; line-height: 1.5; }
1571
+
1572
+ /* =========================
1573
+ PERFORMANCE / MOBILE POLISH
1574
+ ========================= */
1575
+ * { box-sizing: border-box; }
1576
+ img, svg { max-width: 100%; }
1577
+ button, input, select, textarea { font: inherit; }
1578
+ @media (prefers-reduced-motion: reduce) {
1579
+ *, *::before, *::after {
1580
+ scroll-behavior: auto !important;
1581
+ animation-duration: 0.01ms !important;
1582
+ transition-duration: 0.01ms !important;
1583
+ }
1584
  }
1585
 
1586
+ .benchmark-html { width: 100%; }
1587
+ .benchmark-table { min-width: 0; font-size: 11px; }
1588
+ .benchmark-table th, .benchmark-table td { padding: 8px 7px; }
1589
+ .comparison-table { min-width: 620px; }
1590
+
1591
+ /* Desktop gets breathing room; mobile stays single-column. */
1592
+ @media (min-width: 900px) {
1593
+ .gradio-container { padding: 0 22px 60px !important; }
1594
+ .app-header { margin-left: -22px; margin-right: -22px; padding-left: 28px; padding-right: 28px; }
1595
+ .buy-number { font-size: 84px; }
1596
+ .metric-grid { grid-template-columns: repeat(4,minmax(0,1fr)); }
1597
  }
1598
 
 
1599
  @media (max-width: 480px) {
1600
+ .gradio-container { padding-left: 10px !important; padding-right: 10px !important; }
1601
+ .app-header { margin-left: -10px; margin-right: -10px; }
1602
+ .age-row, .feed-row { gap: 12px !important; }
1603
+ .metric-grid { gap: 7px; }
1604
+ .metric-card { padding: 10px 8px; }
1605
+ .metric-card strong { font-size: 15px; }
1606
+ .performance-grid > div { font-size: 12px; }
1607
  }
1608
  """
1609
 
 
1617
  <header class="app-header">
1618
  <div class="brand-row">
1619
  <div class="brand-icon">πŸ”</div>
1620
+ <div>
1621
+ <h1>How Much Feed?</h1>
1622
+ </div>
1623
  </div>
1624
+ <p>Broiler feed calculator β€” find out how much feed to buy for your birds.</p>
1625
  </header>
1626
  """)
1627
 
1628
+ gr.HTML("""
1629
+ <div class="status-card">
1630
+ <strong>βœ“ Simple farmer estimate</strong><br>
1631
+ Enter your breed, flock size, age and feed price. The calculator shows the feed you need and the number of bags to buy.
1632
+ </div>
1633
+ """)
1634
+
1635
+ # ============================================================
1636
+ # MOBILE-FIRST INPUT FORM
1637
+ # ============================================================
1638
+ with gr.Column(elem_classes=["mobile-form"]):
1639
+ with gr.Group(elem_classes=["form-section"]):
1640
+ gr.HTML('<div class="section-heading"><span class="step-number">1</span><span>πŸ” Breed & Birds</span></div>')
1641
+ breed = gr.Dropdown(
1642
+ choices=["Ross 308", "Cobb 500", "Arbor Acres"],
1643
+ value="Ross 308",
1644
+ label="Breed",
1645
+ info="Choose the breed of your broilers.",
1646
+ )
1647
+ birds = gr.Dropdown(
1648
+ choices=[25, 50, 75, 100, 150, 200, 500, 1000],
1649
+ value=50,
1650
+ label="Number of birds",
1651
+ allow_custom_value=True,
1652
+ )
1653
+
1654
+ with gr.Group(elem_classes=["form-section"]):
1655
+ gr.HTML('<div class="section-heading"><span class="step-number">2</span><span>πŸ“… Expected Age</span></div>')
1656
+ with gr.Row(elem_classes=["age-row"]):
1657
+ weeks = gr.Dropdown(
1658
+ choices=list(range(0, 21)), value=8, label="Weeks", scale=1
1659
+ )
1660
+ days = gr.Dropdown(
1661
+ choices=list(range(0, 7)), value=0, label="Days", scale=1
1662
+ )
1663
+
1664
+ with gr.Group(elem_classes=["form-section"]):
1665
+ gr.HTML('<div class="section-heading"><span class="step-number">3</span><span>🌾 Feed Details</span></div>')
1666
+ with gr.Row(elem_classes=["feed-row"]):
1667
+ bag_size = gr.Dropdown(
1668
+ choices=[10, 20, 25, 50, 100], value=25, label="Bag size (kg)", scale=1
1669
+ )
1670
+ price = gr.Number(
1671
+ value=24000, label="Price per bag (₦)", minimum=0, scale=1
1672
+ )
1673
+
1674
+ with gr.Group(elem_classes=["form-section"]):
1675
+ gr.HTML('<div class="section-heading"><span class="step-number">4</span><span>🌍 Your Location</span></div>')
1676
  detected_country = detect_country_from_env() or "NG"
1677
  country_choices = ["Auto-Detect"] + [code for code, _ in COUNTRY_LIST]
1678
+ country_dropdown = gr.Dropdown(
1679
+ choices=country_choices,
1680
+ value="Auto-Detect",
1681
+ label="Country",
1682
+ info="Auto-Detect uses your connection location when available.",
1683
+ )
1684
  country_code_state = gr.State(detected_country)
1685
 
1686
+ with gr.Group(elem_classes=["form-section"]):
1687
+ gr.HTML('<div class="section-heading"><span class="step-number">5</span><span>πŸ“§ Email for your result</span></div>')
1688
  email = gr.Textbox(
1689
  label="Email address",
1690
  placeholder="farmer@example.com",
1691
  type="email",
1692
+ elem_classes=["email-box"],
1693
+ info="We'll save your calculation so you can access it later.",
1694
  )
1695
 
1696
+ calculate_btn = gr.Button(
1697
+ "πŸ” Calculate Feed Requirement",
1698
+ variant="primary",
1699
+ size="lg",
1700
+ elem_classes=["calculate-btn"],
1701
+ )
1702
  save_status = gr.Markdown("")
1703
 
1704
+ # ============================================================
1705
+ # RESULTS
1706
+ # ============================================================
1707
  gr.Markdown("## πŸ“Š Your Feed Result")
1708
  result = gr.HTML(
1709
  value="""
1710
+ <div class="results-shell">
1711
+ <section class="buy-card">
1712
+ <div class="eyebrow">YOUR FEED RESULT</div>
1713
+ <div class="buy-label">BUY</div>
1714
+ <div class="buy-number">β€”</div>
1715
+ <div class="buy-unit">BAGS</div>
1716
+ <div class="buy-note">Enter your flock details above to calculate.</div>
1717
+ </section>
1718
  </div>
1719
  """,
1720
  elem_classes=["answer-box"],
1721
  )
1722
 
1723
+ # ============================================================
1724
+ # REFERENCE / BENCHMARKS
1725
+ # ============================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1726
  with gr.Group(elem_classes=["benchmark-card"]):
1727
+ with gr.Row(elem_classes=["benchmark-heading"]):
1728
+ gr.HTML('<div class="benchmark-title">πŸ“Š Selected Breed Benchmark</div>')
1729
+ selected_breed_badge = gr.HTML('<span class="selected-breed-badge">Ross 308</span>')
1730
+
1731
+ with gr.Accordion(
1732
+ "πŸ“Š Weekly benchmark data used by the calculator",
1733
+ open=False,
1734
+ elem_classes=["benchmark-accordion"],
1735
+ ):
1736
+ gr.HTML('<p class="benchmark-help">Weekly benchmark data used by the calculator for the selected breed.</p>')
1737
+ benchmark_table = gr.HTML(
1738
+ value=get_benchmark_html("Ross 308"),
1739
+ elem_classes=["benchmark-wrap"],
1740
+ )
1741
+
1742
+ with gr.Accordion("πŸ” Breed Comparison", open=False):
1743
+ gr.Markdown("Compare Ross 308, Cobb 500 and Arbor Acres at the same age checkpoints.")
1744
+ comparison_table = gr.HTML(
1745
+ value=get_comparison_html(),
1746
+ elem_classes=["benchmark-wrap"],
1747
+ )
1748
+
1749
+ with gr.Accordion("πŸ” Common Broiler Breeds in Nigeria", open=False):
1750
+ gr.Markdown("""
1751
+ | Breed | Characteristics | Available At |
1752
+ |---|---|---|
1753
+ | **Ross 308** | Big chest, thick legs, very fast growth. | Agrited, Sayed, Valentine |
1754
+ | **Cobb 500** | Good early growth, strong legs, solid feed conversion. | Chikun, Zartech |
1755
+ | **Arbor Acres** | Balanced growth, hardy and adaptable. | Supreme, CHI, GS, Vertex, Fidan, Cascada, Orobor, Sunchi, Farm Support, Ajila |
1756
+
1757
+ **Tip:** Ask the hatchery which breed you are buying before using the benchmark.
1758
+ """)
1759
+
1760
+ with gr.Accordion("πŸ“Œ How It Works & Data Sources", open=False):
1761
+ gr.Markdown(f"""
1762
+ **How the estimate works**
1763
+ - Your weeks + days are converted to total days.
1764
+ - Breed-specific cumulative feed benchmarks estimate total feed per bird.
1765
+ - Starter feed is estimated through Day {STARTER_END_DAY}; feed after that is treated as finisher.
1766
+ - Exact bags = total feed Γ· bag size.
1767
+ - Bags to buy = exact bags rounded up to the next whole bag.
1768
+ - Feed cost = bags to buy Γ— your current price per bag.
1769
+
1770
+ **Data sources**
1771
+ - Ross 308 β€” Aviagen (2022)
1772
+ - Cobb 500 β€” Cobb-Vantress
1773
+ - Arbor Acres β€” Aviagen (2022)
1774
+
1775
+ **Important:** This is a benchmark estimate. Actual feed use varies with management, feed quality, environment and bird performance.
1776
+ """)
1777
+
1778
+ # ============================================================
1779
+ # ASK A QUESTION
1780
+ # ============================================================
1781
+ with gr.Column(elem_classes=["question-card"]):
1782
+ gr.HTML('<div class="question-heading">❓ Ask a Question</div>')
1783
+ gr.HTML('<div class="question-help">Ask a simple poultry-feed question. Your question can also help us improve the calculator.</div>')
1784
+ question = gr.Textbox(
1785
+ label="Your question",
1786
+ placeholder="How many bags of feed for 100 broilers for 6 weeks?",
1787
+ lines=3,
1788
+ )
1789
+ ask_btn = gr.Button("πŸ’¬ Get Answer", variant="secondary", size="lg")
1790
  question_answer = gr.Markdown("")
1791
 
1792
+ # ============================================================
1793
+ # FOOTER + LOW-PROMINENCE ADMIN
1794
+ # ============================================================
1795
  gr.HTML("""
1796
+ <div class="footer-note" style="margin-top:18px; text-align:center;">
1797
  πŸ” <strong>How Much Feed?</strong> β€” A simple broiler feed planning tool.<br>
1798
  Feed estimates are guides and should be adjusted to actual flock performance.
1799
  </div>
1800
  """)
1801
 
1802
+ with gr.Accordion("πŸ”’ Admin", open=False, elem_classes=["admin-footer"]):
1803
+ gr.Markdown("Password-protected analytics and CSV exports.")
1804
+ with gr.Column(elem_classes=["admin-login"]):
1805
+ admin_password_input = gr.Textbox(
1806
+ label="Admin Password", placeholder="Enter password...", type="password"
1807
+ )
1808
+ login_btn = gr.Button("View Dashboard", variant="secondary")
1809
  admin_output = gr.Markdown("πŸ”’ Enter the admin password to view analytics.")
1810
+
1811
  with gr.Row():
1812
  export_leads_btn = gr.Button("πŸ“₯ Export Leads (CSV)", variant="secondary")
1813
  export_leads_output = gr.File(label="Leads CSV")
 
1818
  # ============================================================
1819
  # EVENTS
1820
  # ============================================================
1821
+ breed.change(
1822
+ fn=update_benchmark,
1823
+ inputs=[breed],
1824
+ outputs=[benchmark_table],
1825
+ )
1826
 
1827
+ breed.change(
1828
+ fn=update_selected_breed_badge,
1829
+ inputs=[breed],
1830
+ outputs=[selected_breed_badge],
1831
+ )
1832
+
1833
+ country_dropdown.change(
1834
+ fn=on_country_change_handler,
1835
+ inputs=[country_dropdown],
1836
+ outputs=[country_code_state, price],
1837
+ )
1838
 
1839
  def calculate_wrapper(birds, weeks, days, bag_size, price_per_bag, email, country_code_state, breed, request: gr.Request):
1840
  if not country_code_state:
1841
  country_code_state = "NG"
1842
+ return calculate_feed(
1843
+ birds, weeks, days, bag_size, price_per_bag,
1844
+ email, country_code_state, breed, request
1845
+ )
1846
 
1847
+ calculate_btn.click(
1848
+ fn=calculate_wrapper,
1849
+ inputs=[birds, weeks, days, bag_size, price, email, country_code_state, breed],
1850
+ outputs=[result, save_status],
1851
+ )
1852
+
1853
+ ask_btn.click(
1854
+ fn=answer_farmer_question,
1855
+ inputs=[question, email, breed],
1856
+ outputs=[question_answer],
1857
+ )
1858
 
1859
+ login_btn.click(
1860
+ fn=admin_login,
1861
+ inputs=[admin_password_input],
1862
+ outputs=[admin_output],
1863
+ )
1864
+
1865
+ export_leads_btn.click(
1866
+ fn=export_leads_wrapper,
1867
+ inputs=[],
1868
+ outputs=[export_leads_output],
1869
+ )
1870
+
1871
+ export_calc_btn.click(
1872
+ fn=export_calculations_wrapper,
1873
+ inputs=[],
1874
+ outputs=[export_calc_output],
1875
+ )
1876
 
1877
+ def track_visit_async(request: gr.Request):
1878
+ # Do not make the first page render wait for analytics I/O.
1879
+ threading.Thread(target=track_visit, args=(request,), daemon=True).start()
1880
 
1881
+ demo.load(fn=track_visit_async, inputs=None, outputs=None)
1882
 
1883
 
1884
  # ============================================================
 
1887
 
1888
  if __name__ == "__main__":
1889
  port = int(os.environ.get("PORT", 7860))
1890
+
1891
  demo.launch(
1892
  server_name="0.0.0.0",
1893
  server_port=port,
1894
  ssr_mode=False,
1895
  theme=gr.themes.Base(),
1896
+ css=CSS,
1897
  footer_links=[],
1898
  )