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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +548 -490
app.py CHANGED
@@ -28,6 +28,81 @@ if db_dir and not os.path.exists(db_dir):
28
 
29
  STARTER_END_DAY = 21
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  # ============================================================
32
  # VERIFIED BREED DATA - From Official PDFs
33
  # ============================================================
@@ -62,24 +137,6 @@ BREED_FEED_DATA = {
62
  }
63
  }
64
 
65
- BREED_INFO = {
66
- "Ross 308": {
67
- "description": "Big chest, thick legs, very fast growth. Excellent for meat production.",
68
- "available_at": "Agrited, Sayed, Valentine",
69
- "color": "#00A86B"
70
- },
71
- "Cobb 500": {
72
- "description": "Good early growth, strong legs, solid feed conversion.",
73
- "available_at": "Chikun, Zartech",
74
- "color": "#00A86B"
75
- },
76
- "Arbor Acres": {
77
- "description": "Balanced growth, hardy and adaptable. Great survival rate.",
78
- "available_at": "Supreme, CHI, GS, Vertex, Fidan, Cascada, Orobor, Sunchi, Farm Support, Ajila",
79
- "color": "#00A86B"
80
- }
81
- }
82
-
83
 
84
  def get_breed_weight_data(breed):
85
  return BREED_WEIGHT_DATA.get(breed, BREED_WEIGHT_DATA["Ross 308"])
@@ -181,6 +238,133 @@ def get_default_price(currency_code):
181
  COUNTRY_LIST = sorted([(code, f"{name} ({symbol})") for code, (name, symbol, curr) in CURRENCIES.items()])
182
 
183
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  # ============================================================
185
  # SHARE LINKS GENERATOR
186
  # ============================================================
@@ -197,7 +381,87 @@ def get_share_links(app_url="https://howmuchfeed.com"):
197
 
198
 
199
  # ============================================================
200
- # FEED CALCULATION - PREMIUM DESIGN
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  # ============================================================
202
 
203
  def calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_code, breed, request: gr.Request):
@@ -227,6 +491,9 @@ def calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_c
227
  country_name, symbol, currency_code = get_country_info(country_code)
228
  currency_symbol = symbol
229
 
 
 
 
230
  total_days = (weeks * 7) + days
231
  total_feed_per_bird = interpolate_feed(total_days, breed)
232
  total_feed_kg = birds * total_feed_per_bird
@@ -245,6 +512,10 @@ def calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_c
245
  expected_weight = interpolate_weight(total_days, breed)
246
  flock_weight = expected_weight * birds
247
 
 
 
 
 
248
  share_links = get_share_links()
249
  starter_bags_display = f"{starter_bags:.1f}"
250
  finisher_bags_display = f"{finisher_bags:.1f}"
@@ -253,114 +524,100 @@ def calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_c
253
  age_text += f" + {days} day{'s' if days != 1 else ''}"
254
 
255
  html = f"""
256
- <div class="result-premium">
257
 
258
- <!-- HERO SECTION -->
259
- <div class="result-hero">
260
  <div class="hero-badge">⚑ QUICK ANSWER</div>
261
  <div class="hero-number">{rounded_bags}</div>
262
  <div class="hero-label">BAGS OF FEED</div>
263
  <div class="hero-sub">
264
  To raise <strong>{birds:,}</strong> {breed} broilers for <strong>{age_text}</strong>
265
  </div>
266
- <div class="hero-cta">
267
- <span class="cta-label">Buy</span>
268
- <span class="cta-number">{rounded_bags}</span>
269
- <span class="cta-label">bags (rounded up)</span>
270
- </div>
 
 
271
  </div>
272
 
273
- <!-- METRIC CARDS -->
274
- <div class="metric-grid-premium">
275
- <div class="metric-premium feed-metric">
276
  <div class="metric-icon">🌾</div>
277
  <div class="metric-value">{total_feed_kg:,.1f} kg</div>
278
- <div class="metric-label">Total Feed Needed</div>
279
  </div>
280
- <div class="metric-premium bag-metric">
281
  <div class="metric-icon">πŸ“¦</div>
282
  <div class="metric-value">{total_bags:.1f}</div>
283
- <div class="metric-label">Exact Bags</div>
284
  </div>
285
- <div class="metric-premium cost-metric">
286
  <div class="metric-icon">πŸ’°</div>
287
  <div class="metric-value">{currency_symbol}{total_cost:,.0f}</div>
288
- <div class="metric-label">Estimated Cost</div>
289
  </div>
290
- <div class="metric-premium time-metric">
291
- <div class="metric-icon">⏱️</div>
292
  <div class="metric-value">{total_days}</div>
293
- <div class="metric-label">Days to Grow</div>
294
  </div>
295
  </div>
296
 
297
- <!-- FEED BREAKDOWN -->
298
- <div class="breakdown-section">
299
- <div class="breakdown-title">🌾 Feed Breakdown</div>
300
  <div class="breakdown-grid">
301
  <div class="breakdown-item starter">
302
- <div class="breakdown-label">🌱 Starter</div>
303
- <div class="breakdown-value">{starter_kg:,.1f} kg</div>
304
- <div class="breakdown-bags">{starter_bags_display} bags</div>
305
  </div>
306
  <div class="breakdown-item finisher">
307
- <div class="breakdown-label">🌽 Finisher</div>
308
- <div class="breakdown-value">{finisher_kg:,.1f} kg</div>
309
- <div class="breakdown-bags">{finisher_bags_display} bags</div>
310
  </div>
311
  <div class="breakdown-item total">
312
- <div class="breakdown-label">πŸ“Š TOTAL</div>
313
- <div class="breakdown-value">{total_feed_kg:,.1f} kg</div>
314
- <div class="breakdown-bags">{total_bags:.1f} bags</div>
315
  </div>
316
  </div>
317
  </div>
318
 
319
- <!-- COST BREAKDOWN -->
320
- <div class="cost-breakdown">
321
- <div class="cost-title">πŸ’° Cost Breakdown</div>
322
- <div class="cost-row">
323
- <span>Feed bags</span>
324
- <span><strong>{rounded_bags} bags</strong></span>
325
- </div>
326
- <div class="cost-row">
327
- <span>Price per bag</span>
328
- <span><strong>{currency_symbol}{price_per_bag:,.0f}</strong></span>
329
- </div>
330
- <div class="cost-row total">
331
- <span><strong>Total Feed Cost</strong></span>
332
- <span><strong>{currency_symbol}{total_cost:,.0f}</strong></span>
333
- </div>
334
  </div>
335
 
336
- <!-- PERFORMANCE -->
337
- <div class="performance-section">
338
- <div class="perf-title">πŸ“ˆ Expected Performance</div>
339
  <div class="perf-grid">
340
- <div>
341
- <span>Body weight / bird</span>
342
- <strong>{expected_weight:.3f} kg</strong>
343
- </div>
344
- <div>
345
- <span>Flock live weight</span>
346
- <strong>{flock_weight:,.1f} kg</strong>
347
- </div>
348
  </div>
349
  </div>
350
 
351
- <!-- SHARE -->
352
- <div class="share-section-premium">
353
- <div class="share-title">πŸ“€ Share Results</div>
354
- <div class="share-grid-premium">
355
- <a class="share-btn-premium whatsapp" href="{share_links['whatsapp']}" target="_blank">πŸ’¬ WhatsApp</a>
356
- <a class="share-btn-premium email" href="{share_links['email']}" target="_blank">πŸ“§ Email</a>
357
- <a class="share-btn-premium facebook" href="{share_links['facebook']}" target="_blank">πŸ“˜ Facebook</a>
358
- <a class="share-btn-premium twitter" href="{share_links['twitter']}" target="_blank">🐦 X</a>
359
  </div>
360
  </div>
361
 
362
- <div class="result-footnote">
363
- ⚠️ Estimate based on {breed} benchmark. Results may vary.
364
  </div>
365
  </div>
366
  """
@@ -368,133 +625,95 @@ def calculate_feed(birds, weeks, days, bag_size, price_per_bag, email, country_c
368
  return html, f"βœ… Saved for {email.strip().lower()}."
369
 
370
 
371
- def valid_email(email):
372
- if not email:
373
- return False
374
- email = email.strip()
375
- pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$"
376
- return bool(re.match(pattern, email))
377
-
378
-
379
- def answer_farmer_question(question, email, breed):
380
- if not question or not question.strip():
381
- return "Please type or speak your question first."
382
-
383
- q = question.lower().strip()
384
-
385
- if "bag" in q and ("feed" in q or "birds" in q):
386
- return """
387
- ### Feed-bag questions
388
-
389
- For an exact feed estimate, use the calculator above.
390
-
391
- Enter:
392
- 1. Number of birds
393
- 2. Age in weeks + days
394
- 3. Feed bag size
395
- 4. Price per bag
396
- 5. Your email
397
- 6. Breed
398
-
399
- The calculator will give you the estimated total bags, starter/finisher breakdown and feed cost.
400
- """
401
- elif "breed" in q or "ross" in q or "cobb" in q or "arbor" in q:
402
- return """
403
- ### Common Broiler Breeds in Nigeria
404
 
405
- | Breed | Characteristics | Available At |
406
- |-------|-----------------|--------------|
407
- | **Ross 308** | Big chest, thick legs, very fast growth | Agrited, Sayed, Valentine |
408
- | **Cobb 500** | Good early growth, strong legs | Chikun, Zartech |
409
- | **Arbor Acres** | Balanced growth, hardy, adaptable | Supreme, CHI, GS, Vertex, Fidan, Cascada, Orobor, Sunchi, Farm Support, Ajila |
 
 
 
410
 
411
- Tip: Before buying chicks, ask about the breed and hatchery. Not all broilers are the same!
412
- """
413
- elif "starter" in q:
414
- return f"""
415
- ### Starter feed
416
 
417
- Starter feed is used during the early growth period (Day 0–{STARTER_END_DAY}).
 
 
418
 
419
- The calculator separates the estimated feed requirement into **Starter** and **Finisher** based on the selected age.
420
- """
421
- elif "finisher" in q:
 
 
 
422
  return f"""
423
- ### Finisher feed
424
-
425
- Finisher feed is used after day {STARTER_END_DAY}.
426
-
427
- Your calculator automatically estimates the finisher requirement from the selected number of birds and total age.
428
- """
429
- elif "cost" in q or "price" in q:
430
- return """
431
- ### Feed cost
432
-
433
- Enter the current price of one feed bag in the calculator.
434
-
435
- The app calculates: **Number of bags Γ— price per bag = estimated feed cost**
436
-
437
- The result is for **feed alone**.
438
- """
439
  else:
440
- return f"""
441
- ### Question received
442
 
443
- **β€œ{question.strip()}”**
444
 
445
- Your question has been saved.
446
- """
447
 
448
 
449
- def get_benchmark_html(breed):
450
- weight_data = get_breed_weight_data(breed)
451
- feed_data = get_breed_feed_data(breed)
452
- days = [7, 14, 21, 28, 35, 42, 49, 56]
453
- rows = []
454
- for day in days:
455
- weight = weight_data.get(day, 0)
456
- feed = feed_data.get(day, 0)
457
- fcr = feed / weight if weight > 0 else 0
458
- rows.append(f"<tr><td>{day}</td><td>{weight:.3f} kg</td><td>{feed:.3f} kg</td><td>{fcr:.3f}</td></tr>")
459
- return f"""
460
- <div class="table-wrap"><table class="feed-table">
461
- <thead><tr><th>Days</th><th>Body Weight</th><th>Cum Feed</th><th>FCR</th></tr></thead>
462
- <tbody>{''.join(rows)}</tbody>
463
- </table></div>
464
- """
 
465
 
466
 
467
- def get_comparison_html():
468
- breeds = ["Ross 308", "Cobb 500", "Arbor Acres"]
469
- days = [7, 14, 21, 28, 35, 42, 49, 56]
470
- rows = []
471
- for day in days:
472
- cells = [f'<td>{day}</td>']
473
- for breed in breeds:
474
- weight_data = get_breed_weight_data(breed)
475
- feed_data = get_breed_feed_data(breed)
476
- weight = weight_data.get(day, 0)
477
- feed = feed_data.get(day, 0)
478
- cells.append(f'<td>{weight:.3f} kg</td>')
479
- cells.append(f'<td>{feed:.3f} kg</td>')
480
- rows.append('<tr>' + ''.join(cells) + '</tr>')
481
- headers = ['Days'] + [f'{b} Wt' for b in breeds] + [f'{b} Feed' for b in breeds]
482
- head = ''.join(f'<th>{h}</th>' for h in headers)
483
- return f"""
484
- <div class="table-wrap"><table class="feed-table comparison-table">
485
- <thead><tr>{head}</tr></thead>
486
- <tbody>{''.join(rows)}</tbody>
487
- </table></div>
488
- """
489
 
490
 
491
  # ============================================================
492
- # CSS - PREMIUM DESIGN
493
  # ============================================================
494
 
495
  CSS = """
496
  /* ============================================================
497
- PREMIUM FARM & FIELD DESIGN
498
  ============================================================ */
499
 
500
  :root {
@@ -532,41 +751,25 @@ html, body, .gradio-container, .gradio-container * {
532
  .app-header {
533
  background: var(--mint-dark) !important;
534
  border-radius: 0 0 20px 20px;
535
- padding: 18px 20px 20px;
536
- margin: 0 -12px 16px;
537
  }
538
  .app-header .brand-row { display: flex; align-items: center; gap: 10px; }
539
- .app-header .brand-icon { font-size: 30px; }
540
- .app-header h1 { color: #fff !important; margin: 0; font-size: 26px; font-weight: 800; letter-spacing: -0.5px; }
541
- .app-header p { color: rgba(255,255,255,.85) !important; margin: 4px 0 0 40px; font-size: 14px; }
542
-
543
- /* STATUS BAR */
544
- .status-bar {
545
- background: var(--surface) !important;
546
- border: 1px solid var(--border);
547
- border-radius: 12px;
548
- padding: 10px 14px;
549
- margin-bottom: 14px;
550
- display: flex;
551
- align-items: center;
552
- gap: 12px;
553
- flex-wrap: wrap;
554
- }
555
- .status-bar .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--mint); }
556
- .status-bar strong { color: var(--text) !important; }
557
- .status-bar .muted { color: var(--text-muted) !important; font-size: 13px; }
558
 
559
- /* FORM SECTIONS */
560
  .form-card {
561
  background: var(--surface) !important;
562
  border: 1px solid var(--border);
563
- border-radius: 14px;
564
- padding: 14px 16px;
565
- margin-bottom: 10px;
566
  }
567
  .form-card .label {
568
  font-weight: 700;
569
- font-size: 14px;
570
  color: var(--text);
571
  margin-bottom: 6px;
572
  display: flex;
@@ -577,233 +780,214 @@ html, body, .gradio-container, .gradio-container * {
577
  background: var(--mint-dark);
578
  color: #fff;
579
  border-radius: 50%;
580
- width: 22px;
581
- height: 22px;
582
  display: inline-flex;
583
  align-items: center;
584
  justify-content: center;
585
- font-size: 11px;
 
586
  }
587
 
588
  .gradio-container input, .gradio-container select, .gradio-container textarea {
589
  background: #fff !important;
590
  border: 1px solid var(--border) !important;
591
- border-radius: 10px !important;
592
- min-height: 44px !important;
593
- font-size: 15px !important;
594
  color: var(--text) !important;
595
  }
596
  .gradio-container input:focus, .gradio-container select:focus {
597
  border-color: var(--mint) !important;
598
- box-shadow: 0 0 0 3px rgba(0,168,107,.15) !important;
599
  }
600
  .gradio-container label { color: var(--text) !important; font-weight: 600 !important; }
 
601
 
602
  .calc-btn button {
603
  width: 100% !important;
604
- min-height: 52px !important;
605
  background: var(--mint) !important;
606
  color: #fff !important;
607
  border: 0 !important;
608
- border-radius: 12px !important;
609
- font-size: 16px !important;
610
  font-weight: 800 !important;
611
- box-shadow: 0 4px 14px rgba(0,168,107,.25) !important;
612
  }
613
  .calc-btn button:hover { background: #00955E !important; }
614
 
615
  /* ============================================================
616
- PREMIUM RESULTS
617
  ============================================================ */
618
 
619
- .result-premium {
620
  max-width: 100%;
621
- animation: fadeUp 0.4s ease;
622
  }
623
 
624
  @keyframes fadeUp {
625
- from { opacity: 0; transform: translateY(12px); }
626
  to { opacity: 1; transform: translateY(0); }
627
  }
628
 
629
- /* HERO */
630
- .result-hero {
631
- background: linear-gradient(145deg, var(--mint-dark) 0%, #1A4A32 100%);
632
- border-radius: 18px;
633
- padding: 22px 18px 18px;
634
  text-align: center;
635
- margin-bottom: 16px;
636
- position: relative;
637
- overflow: hidden;
638
- }
639
- .result-hero::after {
640
- content: "πŸ”";
641
- position: absolute;
642
- right: -10px;
643
- bottom: -10px;
644
- font-size: 80px;
645
- opacity: 0.08;
646
  }
647
  .hero-badge {
648
- display: inline-block;
649
- background: rgba(255,255,255,0.12);
650
- color: #fff;
651
  font-size: 10px;
652
  font-weight: 700;
653
  letter-spacing: 1px;
654
  text-transform: uppercase;
655
- padding: 3px 14px;
656
- border-radius: 20px;
657
  }
658
  .hero-number {
659
  color: #fff;
660
- font-size: clamp(56px, 18vw, 84px);
661
  font-weight: 900;
662
  line-height: 1;
663
- margin: 4px 0 2px;
664
- letter-spacing: -2px;
665
  }
666
  .hero-label {
667
  color: rgba(255,255,255,0.7);
668
  font-size: 14px;
669
  font-weight: 600;
670
- letter-spacing: 2px;
671
  }
672
  .hero-sub {
673
  color: rgba(255,255,255,0.85);
674
- font-size: 14px;
675
- margin: 8px 0 12px;
676
  }
677
  .hero-sub strong { color: #fff !important; }
678
- .hero-cta {
679
- display: inline-flex;
680
- align-items: center;
681
- gap: 8px;
682
- background: rgba(255,255,255,0.1);
683
- border: 1px solid rgba(255,255,255,0.15);
684
- border-radius: 30px;
685
- padding: 6px 16px 6px 20px;
686
  }
687
- .cta-label { color: rgba(255,255,255,0.7); font-size: 13px; }
688
- .cta-number { color: var(--yellow); font-size: 22px; font-weight: 800; }
689
 
690
- /* METRIC GRID */
691
- .metric-grid-premium {
 
 
 
 
 
 
 
 
 
 
 
 
 
692
  display: grid;
693
  grid-template-columns: repeat(2, 1fr);
694
- gap: 10px;
695
- margin-bottom: 16px;
696
  }
697
- .metric-premium {
698
  background: var(--surface);
699
- border-radius: 14px;
700
- padding: 14px 12px;
701
- text-align: center;
702
  border: 1px solid var(--border);
 
 
 
703
  }
704
- .metric-premium .metric-icon { font-size: 20px; }
705
- .metric-premium .metric-value {
706
- font-size: 22px;
707
  font-weight: 800;
 
708
  margin: 2px 0;
709
  }
710
- .metric-premium .metric-label {
711
- font-size: 11px;
712
  color: var(--text-muted);
713
  font-weight: 500;
714
  }
715
- .feed-metric .metric-value { color: var(--mint); }
716
- .bag-metric .metric-value { color: var(--yellow); }
717
- .cost-metric .metric-value { color: var(--red); }
718
- .time-metric .metric-value { color: var(--mint-dark); }
719
 
720
- /* BREAKDOWN */
721
- .breakdown-section {
722
  background: var(--surface);
723
- border-radius: 14px;
724
- padding: 14px 14px 10px;
725
  border: 1px solid var(--border);
726
- margin-bottom: 14px;
 
 
727
  }
728
- .breakdown-title {
729
  font-weight: 700;
730
- font-size: 14px;
731
- margin-bottom: 10px;
732
  color: var(--text);
733
  }
734
  .breakdown-grid {
735
  display: grid;
736
  grid-template-columns: 1fr 1fr 1fr;
737
- gap: 8px;
738
  }
739
  .breakdown-item {
740
  text-align: center;
741
- padding: 10px 4px;
742
- border-radius: 10px;
743
  }
744
  .breakdown-item.starter { background: #E3F2ED; }
745
  .breakdown-item.finisher { background: #F5F0E8; }
746
  .breakdown-item.total { background: var(--mint-soft); }
747
- .breakdown-item .breakdown-label {
748
- font-size: 11px;
749
  font-weight: 600;
750
  color: var(--text-muted);
751
  }
752
- .breakdown-item .breakdown-value {
753
- font-size: 16px;
754
  font-weight: 700;
755
  color: var(--text);
756
  }
757
- .breakdown-item .breakdown-bags {
758
- font-size: 12px;
759
  color: var(--text-muted);
760
  }
761
 
762
- /* COST BREAKDOWN */
763
- .cost-breakdown {
764
  background: var(--yellow-soft);
765
- border-radius: 14px;
766
- padding: 14px 16px;
767
  border: 1px solid #EBC34A;
768
- margin-bottom: 14px;
 
 
769
  }
770
- .cost-title {
771
- font-weight: 700;
772
- font-size: 14px;
773
- margin-bottom: 8px;
774
  }
775
- .cost-row {
776
- display: flex;
777
- justify-content: space-between;
778
- padding: 4px 0;
779
- font-size: 14px;
780
- border-bottom: 1px solid rgba(0,0,0,0.04);
781
  }
782
- .cost-row:last-child { border-bottom: 0; }
783
- .cost-row.total { border-top: 2px solid #EBC34A; padding-top: 8px; margin-top: 4px; }
784
 
785
- /* PERFORMANCE */
786
- .performance-section {
787
  background: var(--surface);
788
- border-radius: 14px;
789
- padding: 14px 16px;
790
  border: 1px solid var(--border);
791
- margin-bottom: 14px;
792
- }
793
- .perf-title {
794
- font-weight: 700;
795
- font-size: 14px;
796
- margin-bottom: 8px;
797
  }
798
  .perf-grid {
799
  display: grid;
800
  grid-template-columns: 1fr 1fr;
801
- gap: 8px;
802
  }
803
  .perf-grid div {
804
  display: flex;
805
  justify-content: space-between;
806
- padding: 6px 0;
807
  font-size: 13px;
808
  border-bottom: 1px solid var(--border);
809
  }
@@ -811,81 +995,56 @@ html, body, .gradio-container, .gradio-container * {
811
  .perf-grid span { color: var(--text-muted); }
812
  .perf-grid strong { color: var(--mint-dark); }
813
 
814
- /* SHARE */
815
- .share-section-premium {
816
  background: var(--surface);
817
- border-radius: 14px;
818
- padding: 14px 16px;
819
  border: 1px solid var(--border);
 
 
820
  margin-bottom: 10px;
821
  }
822
- .share-title {
823
- font-weight: 700;
824
- font-size: 14px;
825
- margin-bottom: 8px;
826
- }
827
- .share-grid-premium {
828
  display: grid;
829
  grid-template-columns: repeat(2, 1fr);
830
- gap: 8px;
831
  }
832
- .share-btn-premium {
833
  display: block;
834
  text-align: center;
835
  text-decoration: none !important;
836
- border-radius: 10px;
837
- padding: 10px;
838
- font-size: 13px;
839
  font-weight: 700;
840
  color: #fff !important;
841
- transition: transform 0.15s;
842
  }
843
- .share-btn-premium:hover { transform: scale(1.02); }
844
- .share-btn-premium.whatsapp { background: #25D366; }
845
- .share-btn-premium.email { background: #EA4335; }
846
- .share-btn-premium.facebook { background: #1877F2; }
847
- .share-btn-premium.twitter { background: #000; }
848
 
849
- .result-footnote {
850
  font-size: 11px;
851
  color: var(--text-muted);
852
  text-align: center;
853
- padding-top: 8px;
854
  border-top: 1px solid var(--border);
855
  }
856
 
857
- /* BENCHMARK */
858
- .benchmark-card {
859
- background: var(--surface) !important;
860
- border: 1px solid var(--border);
861
- border-radius: 14px;
862
- padding: 14px;
863
- margin: 12px 0;
864
- }
865
- .feed-table {
866
- width: 100%;
867
- border-collapse: collapse;
868
- font-size: 12px;
869
- }
870
- .feed-table th {
871
- background: var(--mint-dark) !important;
872
- color: #fff !important;
873
- padding: 8px 10px;
874
- text-align: left;
875
- }
876
- .feed-table td {
877
- padding: 7px 10px;
878
- border-bottom: 1px solid var(--border);
879
  color: var(--text) !important;
 
880
  }
881
- .feed-table .total-row td { background: var(--mint-soft) !important; font-weight: 700; }
882
 
 
883
  @media (max-width: 480px) {
884
- .metric-grid-premium { grid-template-columns: repeat(2, 1fr); gap: 8px; }
885
  .breakdown-grid { grid-template-columns: 1fr; gap: 4px; }
886
  .perf-grid { grid-template-columns: 1fr; }
887
- .share-grid-premium { grid-template-columns: repeat(2, 1fr); }
888
- .hero-number { font-size: 48px; }
889
  }
890
  """
891
 
@@ -905,14 +1064,6 @@ with gr.Blocks(title=APP_NAME, analytics_enabled=False) as demo:
905
  </header>
906
  """)
907
 
908
- gr.HTML("""
909
- <div class="status-bar">
910
- <span class="dot"></span>
911
- <strong>Simple & Fast</strong>
912
- <span class="muted">β€” Enter your flock details, get instant feed estimates.</span>
913
- </div>
914
- """)
915
-
916
  with gr.Column():
917
  with gr.Group(elem_classes=["form-card"]):
918
  gr.HTML('<div class="label"><span class="num">1</span> πŸ” Breed & Birds</div>')
@@ -954,8 +1105,8 @@ with gr.Blocks(title=APP_NAME, analytics_enabled=False) as demo:
954
  gr.Markdown("## πŸ“Š Your Feed Result")
955
  result = gr.HTML(
956
  value="""
957
- <div class="result-premium">
958
- <div class="result-hero">
959
  <div class="hero-badge">⚑ QUICK ANSWER</div>
960
  <div class="hero-number">β€”</div>
961
  <div class="hero-label">BAGS OF FEED</div>
@@ -1048,103 +1199,10 @@ with gr.Blocks(title=APP_NAME, analytics_enabled=False) as demo:
1048
  export_leads_btn.click(fn=export_leads_wrapper, inputs=[], outputs=[export_leads_output])
1049
  export_calc_btn.click(fn=export_calculations_wrapper, inputs=[], outputs=[export_calc_output])
1050
 
 
 
1051
 
1052
- # ============================================================
1053
- # ADMIN FUNCTIONS
1054
- # ============================================================
1055
-
1056
- def admin_login(password):
1057
- if password == ADMIN_PASSWORD:
1058
- stats = get_analytics_summary()
1059
- breed_stats_html = ""
1060
- for breed, count in stats["breed_stats"]:
1061
- breed_stats_html += f"<li><strong>{breed}</strong>: {count} calculations</li>"
1062
- return f"""
1063
- ### Analytics
1064
- | Metric | Value |
1065
- |--------|-------|
1066
- | Total Visits | **{stats['total_visits']:,}** |
1067
- | Today's Visits | **{stats['today_visits']}** |
1068
- | Unique Visitors | **{stats['unique_visitors']:,}** |
1069
- | Calculations | **{stats['total_calculations']:,}** |
1070
- | Leads | **{stats['total_leads']:,}** |
1071
- ### Popular Breeds
1072
- <ul>{breed_stats_html}</ul>
1073
- """, gr.update(visible=True), gr.update(visible=False)
1074
- else:
1075
- return "❌ Incorrect password.", gr.update(visible=False), gr.update(visible=True)
1076
-
1077
-
1078
- def logout_admin():
1079
- return "πŸ”’ Enter password to view analytics.", gr.update(visible=True), gr.update(visible=False)
1080
-
1081
-
1082
- def export_leads_wrapper():
1083
- import tempfile
1084
- conn = sqlite3.connect(DB_PATH)
1085
- cur = conn.cursor()
1086
- cur.execute("SELECT email, country, currency, breed, created_at FROM leads ORDER BY created_at DESC")
1087
- rows = cur.fetchall()
1088
- conn.close()
1089
- import csv
1090
- from io import StringIO
1091
- output = StringIO()
1092
- writer = csv.writer(output)
1093
- writer.writerow(["Email", "Country", "Currency", "Breed", "Created At"])
1094
- writer.writerows(rows)
1095
- temp = tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False, encoding='utf-8')
1096
- temp.write(output.getvalue())
1097
- temp.close()
1098
- return temp.name
1099
-
1100
-
1101
- def export_calculations_wrapper():
1102
- import tempfile
1103
- conn = sqlite3.connect(DB_PATH)
1104
- cur = conn.cursor()
1105
- cur.execute("SELECT email, breed, birds, total_days, rounded_bags, bag_size, total_cost, created_at FROM calculations ORDER BY created_at DESC")
1106
- rows = cur.fetchall()
1107
- conn.close()
1108
- import csv
1109
- from io import StringIO
1110
- output = StringIO()
1111
- writer = csv.writer(output)
1112
- writer.writerow(["Email", "Breed", "Birds", "Days", "Bags", "Bag Size", "Cost", "Created"])
1113
- writer.writerows(rows)
1114
- temp = tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False, encoding='utf-8')
1115
- temp.write(output.getvalue())
1116
- temp.close()
1117
- return temp.name
1118
-
1119
-
1120
- def get_analytics_summary():
1121
- conn = sqlite3.connect(DB_PATH)
1122
- cur = conn.cursor()
1123
- cur.execute("SELECT COUNT(*) FROM analytics WHERE action = 'page_visit'")
1124
- total_visits = cur.fetchone()[0] or 0
1125
- cur.execute("SELECT COUNT(*) FROM analytics WHERE action = 'page_visit' AND visit_date = ?", (datetime.now().strftime("%Y-%m-%d"),))
1126
- today_visits = cur.fetchone()[0] or 0
1127
- cur.execute("SELECT COUNT(*) FROM analytics WHERE action LIKE 'calculation_%'")
1128
- total_calculations = cur.fetchone()[0] or 0
1129
- cur.execute("SELECT COUNT(DISTINCT ip) FROM analytics WHERE action = 'page_visit' AND ip != 'unknown'")
1130
- unique_visitors = cur.fetchone()[0] or 0
1131
- cur.execute("SELECT breed, COUNT(*) FROM analytics WHERE action LIKE 'calculation_%' GROUP BY breed")
1132
- breed_stats = cur.fetchall()
1133
- cur.execute("SELECT COUNT(*) FROM leads")
1134
- total_leads = cur.fetchone()[0] or 0
1135
- conn.close()
1136
- return {"total_visits": total_visits, "today_visits": today_visits, "unique_visitors": unique_visitors,
1137
- "total_calculations": total_calculations, "total_leads": total_leads, "breed_stats": breed_stats}
1138
-
1139
-
1140
- def on_country_change_handler(country_selection):
1141
- if country_selection == "Auto-Detect":
1142
- country_code = detect_country_from_env() or "NG"
1143
- else:
1144
- country_code = country_selection
1145
- _, symbol, currency_code = get_country_info(country_code)
1146
- default_price = get_default_price(currency_code)
1147
- return country_code, default_price
1148
 
1149
 
1150
  # ============================================================
 
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
  # ============================================================
 
137
  }
138
  }
139
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
  def get_breed_weight_data(breed):
142
  return BREED_WEIGHT_DATA.get(breed, BREED_WEIGHT_DATA["Ross 308"])
 
238
  COUNTRY_LIST = sorted([(code, f"{name} ({symbol})") for code, (name, symbol, curr) in CURRENCIES.items()])
239
 
240
 
241
+ # ============================================================
242
+ # DATABASE HELPERS
243
+ # ============================================================
244
+
245
+ def valid_email(email):
246
+ if not email:
247
+ return False
248
+ email = email.strip()
249
+ pattern = r"^[^@\s]+@[^@\s]+\.[^@\s]+$"
250
+ return bool(re.match(pattern, email))
251
+
252
+
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,
287
+ bag_size, price_per_bag, total_feed_kg, starter_kg, finisher_kg,
288
+ starter_bags, finisher_bags, total_bags, rounded_bags, total_cost, created_at
289
+ )
290
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
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 (?, ?, ?, ?, ?)
307
+ """, (
308
+ datetime.now().strftime("%Y-%m-%d"),
309
+ client_ip,
310
+ detect_country_from_env() or "NG",
311
+ "page_visit",
312
+ datetime.now().isoformat()
313
+ ))
314
+ conn.commit()
315
+ conn.close()
316
+ except Exception as e:
317
+ print(f"Analytics error: {e}")
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 (?, ?, ?, ?, ?)
327
+ """, (
328
+ datetime.now().strftime("%Y-%m-%d"),
329
+ detect_country_from_env() or "NG",
330
+ breed,
331
+ f"calculation_{birds}_{days}",
332
+ datetime.now().isoformat()
333
+ ))
334
+ conn.commit()
335
+ conn.close()
336
+ return True
337
+ except:
338
+ return False
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,
361
+ "unique_visitors": unique_visitors,
362
+ "total_calculations": total_calculations,
363
+ "total_leads": total_leads,
364
+ "breed_stats": breed_stats
365
+ }
366
+
367
+
368
  # ============================================================
369
  # SHARE LINKS GENERATOR
370
  # ============================================================
 
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):
 
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
 
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}"
 
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
  """
 
625
  return html, f"βœ… Saved for {email.strip().lower()}."
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
640
 
 
 
 
 
 
641
 
642
+ # ============================================================
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 {
 
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;
 
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
  }
 
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
 
 
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>')
 
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>
 
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
  # ============================================================