diff --git a/client/src/App.tsx b/client/src/App.tsx index 60272a82..17be3339 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -164,6 +164,16 @@ const HomeDashboard = lazy(() => import("./pages/Home")); const LoginKeycloak = lazy(() => import("./pages/LoginKeycloak")); const OfflineConflictResolution = lazy(() => import("./pages/OfflineConflictResolution")); +// === Dairy Management (SmartAlex) === +const DairyDashboard = lazy(() => import("./pages/DairyDashboard")); +const DairyHerd = lazy(() => import("./pages/DairyHerd")); +const DairyMilk = lazy(() => import("./pages/DairyMilk")); +const DairyBreeding = lazy(() => import("./pages/DairyBreeding")); +const DairyHealth = lazy(() => import("./pages/DairyHealth")); +const DairySuppliers = lazy(() => import("./pages/DairySuppliers")); +const DairyMarket = lazy(() => import("./pages/DairyMarket")); +const DairyAnalytics = lazy(() => import("./pages/DairyAnalytics")); + function Router() { return ( @@ -330,6 +340,16 @@ function Router() { + {/* === Dairy Management (SmartAlex) === */} + + + + + + + + + diff --git a/client/src/components/CategoryHub.tsx b/client/src/components/CategoryHub.tsx index 0967a5ad..f9ee85cb 100644 --- a/client/src/components/CategoryHub.tsx +++ b/client/src/components/CategoryHub.tsx @@ -10,7 +10,8 @@ import { DollarSign, ArrowUpDown, Phone, Home, Settings, Trophy, Mic, Activity, Droplets, Fish, AlertTriangle, Store, RotateCcw, Apple, ChevronDown, WifiOff, CheckCircle, Mail, Workflow, - PieChart, GanttChart, Archive, Warehouse, Scale, Landmark + PieChart, GanttChart, Archive, Warehouse, Scale, Landmark, + Milk, Beef, Heart, Baby, Syringe, Factory } from "lucide-react"; interface FeatureCard { @@ -111,6 +112,19 @@ const categoryFeatures: Record = { { href: "/aquaculture/ai", label: "Aqua AI", icon: Brain, description: "AI predictions" }, ], }, + { + title: "Dairy Management", + cards: [ + { href: "/dairy", label: "Dairy Dashboard", icon: Milk, description: "SmartAlex dairy hub", badge: "NEW" }, + { href: "/dairy-herd", label: "Herd Management", icon: Beef, description: "Cow profiles & tracking", badge: "NEW" }, + { href: "/dairy-milk", label: "Milk Production", icon: Milk, description: "Daily yield recording", badge: "NEW" }, + { href: "/dairy-breeding", label: "Breeding Records", icon: Baby, description: "AI, pregnancy & calving", badge: "NEW" }, + { href: "/dairy-health", label: "Health Monitoring", icon: Heart, description: "Vaccinations & treatments", badge: "NEW" }, + { href: "/dairy-suppliers", label: "Supplier Market", icon: Store, description: "Feed, drugs & services", badge: "NEW" }, + { href: "/dairy-market", label: "Processors & Buyers", icon: Factory, description: "Sell milk, schedule pickup", badge: "NEW" }, + { href: "/dairy-analytics", label: "Dairy Analytics", icon: BarChart3, description: "Yield trends & profit", badge: "NEW" }, + ], + }, { title: "Advanced", cards: [ diff --git a/client/src/index.css b/client/src/index.css index fcc31d2a..23a640ff 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -841,3 +841,69 @@ border-radius: 4px; } } + +/* ============================================ + SMARTALEX DAIRY DESIGN SYSTEM + Warm illustrative style — sandy-brown / sky-blue + Circular teal iconography + ============================================ */ + +/* Hero banner gradient — warm sandy tones to teal */ +.dairy-hero { + background: linear-gradient(135deg, #0d9488 0%, #0f766e 40%, #115e59 100%); + position: relative; +} +.dark .dairy-hero { + background: linear-gradient(135deg, #134e4a 0%, #0f4c47 40%, #0c3e3a 100%); +} + +/* Cards with subtle warm shadow + rounded corners */ +.dairy-card { + border-radius: 12px; + border: 1px solid oklch(0.92 0.005 250); + transition: box-shadow 0.2s ease, transform 0.15s ease; +} +.dairy-card:hover { + box-shadow: 0 4px 12px rgba(13, 148, 136, 0.08); +} +.dark .dairy-card { + border-color: oklch(0.28 0.015 250); +} +.dark .dairy-card:hover { + box-shadow: 0 4px 12px rgba(13, 148, 136, 0.15); +} + +/* Challenge section — sandy/warm background */ +.dairy-challenge-bg { + background: linear-gradient(180deg, #fef3c7 0%, #fde68a20 100%); +} +.dark .dairy-challenge-bg { + background: linear-gradient(180deg, rgba(180, 83, 9, 0.12) 0%, rgba(180, 83, 9, 0.04) 100%); +} + +/* Solution section — cool blue/teal background */ +.dairy-solution-bg { + background: linear-gradient(180deg, #ccfbf1 0%, #99f6e420 100%); +} +.dark .dairy-solution-bg { + background: linear-gradient(180deg, rgba(13, 148, 136, 0.12) 0%, rgba(13, 148, 136, 0.04) 100%); +} + +/* Primary action button — teal */ +.dairy-btn-primary { + background: linear-gradient(135deg, #0d9488 0%, #0f766e 100%) !important; + color: white !important; + border: none !important; + font-weight: 600; + transition: opacity 0.15s ease; +} +.dairy-btn-primary:hover { + opacity: 0.9; +} + +/* Tabs styled for dairy section */ +.dairy-tabs [data-state="active"] { + background: linear-gradient(135deg, #0d9488 0%, #0f766e 100%); + color: white; + font-weight: 600; +} diff --git a/client/src/pages/DairyAnalytics.tsx b/client/src/pages/DairyAnalytics.tsx new file mode 100644 index 00000000..53c0babe --- /dev/null +++ b/client/src/pages/DairyAnalytics.tsx @@ -0,0 +1,260 @@ +import { useState } from "react"; +import DashboardLayout from "@/components/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { trpc } from "@/lib/trpc"; +import { + BarChart3, TrendingUp, Droplets, DollarSign, Milk, Beef, + Heart, Baby, ArrowUp, ArrowDown, Minus +} from "lucide-react"; + +function MetricCard({ icon: Icon, label, value, target, unit, color }: { + icon: typeof Milk; label: string; value: number; target?: number; unit: string; + color: string; +}) { + const pct = target ? ((value / target) * 100) : null; + const trend = pct ? (pct >= 100 ? "up" : pct >= 80 ? "neutral" : "down") : "neutral"; + const TrendIcon = trend === "up" ? ArrowUp : trend === "down" ? ArrowDown : Minus; + const trendColor = trend === "up" ? "text-emerald-600" : trend === "down" ? "text-red-600" : "text-amber-600"; + + return ( + + +
+
+ +
+ {pct !== null && ( +
+ + {pct.toFixed(0)}% +
+ )} +
+

{label}

+

+ {typeof value === "number" ? value.toLocaleString(undefined, { maximumFractionDigits: 1 }) : value} {unit} +

+ {target && ( +
+
+
= 100 ? "bg-emerald-500" : (pct ?? 0) >= 80 ? "bg-amber-500" : "bg-red-500" + }`} + style={{ width: `${Math.min(pct ?? 0, 100)}%` }} + /> +
+

Target: {target} {unit}

+
+ )} + + + ); +} + +export default function DairyAnalytics() { + const [period, setPeriod] = useState<"7d" | "30d" | "90d" | "365d">("30d"); + + const analyticsQuery = trpc.dairy.getMilkAnalytics.useQuery({ period }, { retry: false }); + const dashboardQuery = trpc.dairy.getDashboardSummary.useQuery(undefined, { retry: false }); + + const milk = analyticsQuery.data; + const dash = dashboardQuery.data; + + return ( + +
+ {/* Header */} +
+
+
+ +
+
+

Dairy Analytics

+

+ Production trends, profitability, and herd performance +

+
+
+
+ {(["7d", "30d", "90d", "365d"] as const).map(p => ( + + ))} +
+
+ + {/* Key Metrics */} +
+ + + + +
+ + {/* Herd & Revenue */} +
+ + + + + Herd Summary + + + +
+ Total Cows + {dash?.herd.totalCows ?? 0} +
+
+ Active (Milking) + {dash?.herd.activeCows ?? 0} +
+
+ Pregnant + {dash?.breeding.pregnantCows ?? 0} +
+
+ Health Alerts + {dash?.health.unresolvedAlerts ?? 0} +
+
+
+ + + + + + Revenue (30 days) + + + +
+ Total Revenue + ₦{((dash?.market.revenue30d ?? 0) / 100).toLocaleString()} +
+
+ Liters Collected + {(dash?.market.litersCollected30d ?? 0).toLocaleString()} L +
+
+ Collections + {dash?.market.collections30d ?? 0} +
+
+ Avg ₦/Liter + + {dash?.market.litersCollected30d + ? `₦${((dash.market.revenue30d / dash.market.litersCollected30d) / 100).toFixed(0)}` + : "—" + } + +
+
+
+ + + + + + Production Efficiency + + + +
+ Records ({period}) + {milk?.totalRecords ?? 0} +
+
+ Active Cows + {milk?.activeCows ?? 0} +
+
+ Liters/Cow/Day + + {milk && milk.activeCows > 0 + ? (milk.totalLiters / milk.activeCows / (period === "7d" ? 7 : period === "30d" ? 30 : period === "90d" ? 90 : 365)).toFixed(1) + : "—" + } L + +
+
+ Quality Target + = 3.5 + ? "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300" + : "bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300" + }> + {(milk?.avgFatPercentage ?? 0) >= 3.5 ? "Grade A" : "Below Target"} + +
+
+
+
+ + {/* Benchmarks */} + + + + + Industry Benchmarks + + How your farm compares to regional averages + + +
+ {[ + { label: "Milk Yield/Cow/Day", yours: milk && milk.activeCows > 0 ? (milk.totalLiters / milk.activeCows / 30).toFixed(1) : "—", benchmark: "8-12 L", unit: "L" }, + { label: "Fat Content", yours: `${(milk?.avgFatPercentage ?? 0).toFixed(1)}%`, benchmark: "3.5-4.5%", unit: "" }, + { label: "Protein Content", yours: `${(milk?.avgProteinPercentage ?? 0).toFixed(1)}%`, benchmark: "3.0-3.5%", unit: "" }, + ].map(({ label, yours, benchmark }) => ( +
+

{label}

+

{yours}

+

Benchmark: {benchmark}

+
+ ))} +
+
+
+
+
+ ); +} diff --git a/client/src/pages/DairyBreeding.tsx b/client/src/pages/DairyBreeding.tsx new file mode 100644 index 00000000..e0c297a3 --- /dev/null +++ b/client/src/pages/DairyBreeding.tsx @@ -0,0 +1,242 @@ +import { useState, useMemo } from "react"; +import DashboardLayout from "@/components/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Textarea } from "@/components/ui/textarea"; +import { trpc } from "@/lib/trpc"; +import { toast } from "sonner"; +import { Baby, Plus, CheckCircle, Clock, X, Syringe, Calendar } from "lucide-react"; + +const OUTCOME_COLORS: Record = { + pending: "bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300", + pregnant: "bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-300", + failed: "bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300", + calved: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300", + aborted: "bg-gray-100 text-gray-800 dark:bg-gray-900/40 dark:text-gray-300", +}; + +export default function DairyBreeding() { + const [dialogOpen, setDialogOpen] = useState(false); + const [form, setForm] = useState({ + cowId: "", + breedingDate: new Date().toISOString().split("T")[0], + method: "artificial_insemination" as "artificial_insemination" | "natural_mating" | "embryo_transfer", + sireBreed: "", + sireTag: "", + aiTechnicianName: "", + strawNumber: "", + notes: "", + }); + + const cowsQuery = trpc.dairy.getCows.useQuery({}, { retry: false }); + const breedingQuery = trpc.dairy.getBreedingRecords.useQuery({}, { retry: false }); + + const recordMutation = trpc.dairy.recordBreeding.useMutation({ + onSuccess: () => { + toast.success("Breeding event recorded"); + setDialogOpen(false); + breedingQuery.refetch(); + }, + onError: (err) => toast.error(err.message), + }); + + const cowMap = useMemo(() => { + const m = new Map(); + (cowsQuery.data ?? []).forEach((c) => m.set(c.id, `${c.tagNumber}${c.name ? ` (${c.name})` : ""}`)); + return m; + }, [cowsQuery.data]); + + const stats = useMemo(() => { + const records = breedingQuery.data ?? []; + return { + total: records.length, + pregnant: records.filter((r) => r.outcome === "pregnant").length, + pending: records.filter((r) => r.outcome === "pending").length, + failed: records.filter((r) => r.outcome === "failed").length, + calved: records.filter((r) => r.outcome === "calved").length, + }; + }, [breedingQuery.data]); + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + recordMutation.mutate({ + cowId: Number(form.cowId), + breedingDate: form.breedingDate, + method: form.method, + sireBreed: form.sireBreed || undefined, + sireTag: form.sireTag || undefined, + aiTechnicianName: form.aiTechnicianName || undefined, + strawNumber: form.strawNumber || undefined, + notes: form.notes || undefined, + }); + } + + return ( + +
+ {/* Header */} +
+
+
+ +
+
+

Breeding Records

+

Track AI, natural mating, pregnancy & calving

+
+
+ + + + + + + Record Breeding Event + +
+
+ + +
+
+
+ + setForm({ ...form, breedingDate: e.target.value })} required /> +
+
+ + +
+
+
+
+ + setForm({ ...form, sireBreed: e.target.value })} /> +
+
+ + setForm({ ...form, sireTag: e.target.value })} /> +
+
+ {form.method === "artificial_insemination" && ( +
+
+ + setForm({ ...form, aiTechnicianName: e.target.value })} /> +
+
+ + setForm({ ...form, strawNumber: e.target.value })} /> +
+
+ )} +
+ +