diff --git a/client/src/App.jsx b/client/src/App.jsx index 24285e0..42b5d3f 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,11 +1,12 @@ -import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom' -import { Home, Users, Activity, Heart } from 'lucide-react' +import { BrowserRouter as Router, Routes, Route, Link} from 'react-router-dom' +import { Home, Users, Activity, Heart, FlaskConical } from 'lucide-react' import Dashboard from './pages/Dashboard' import DogList from './pages/DogList' import DogDetail from './pages/DogDetail' import PedigreeView from './pages/PedigreeView' import LitterList from './pages/LitterList' import BreedingCalendar from './pages/BreedingCalendar' +import PairingSimulator from './pages/PairingSimulator' import './App.css' function App() { @@ -39,6 +40,10 @@ function App() { Breeding + + + Pairing + @@ -51,6 +56,7 @@ function App() { } /> } /> } /> + } /> diff --git a/client/src/index.css b/client/src/index.css index ee74197..11f6955 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -461,4 +461,33 @@ select { color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; -} \ No newline at end of file +} + +/* Risk Badge - Pairing Simulator */ +.risk-badge { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 1.25rem; + border-radius: var(--radius); + font-size: 0.9375rem; + font-weight: 600; +} + +.risk-low { + background: rgba(16, 185, 129, 0.15); + color: var(--success); + border: 1px solid rgba(16, 185, 129, 0.3); +} + +.risk-med { + background: rgba(245, 158, 11, 0.15); + color: var(--warning); + border: 1px solid rgba(245, 158, 11, 0.3); +} + +.risk-high { + background: rgba(239, 68, 68, 0.15); + color: var(--danger); + border: 1px solid rgba(239, 68, 68, 0.3); +} diff --git a/client/src/pages/PairingSimulator.jsx b/client/src/pages/PairingSimulator.jsx new file mode 100644 index 0000000..9633f3e --- /dev/null +++ b/client/src/pages/PairingSimulator.jsx @@ -0,0 +1,219 @@ +import { useState, useEffect } from 'react' +import { FlaskConical, AlertTriangle, CheckCircle, XCircle, GitMerge } from 'lucide-react' + +export default function PairingSimulator() { + const [dogs, setDogs] = useState([]) + const [sireId, setSireId] = useState('') + const [damId, setDamId] = useState('') + const [result, setResult] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [dogsLoading, setDogsLoading] = useState(true) + + useEffect(() => { + fetch('/api/dogs') + .then(r => r.json()) + .then(data => { + setDogs(Array.isArray(data) ? data : (data.dogs || [])) + setDogsLoading(false) + }) + .catch(() => setDogsLoading(false)) + }, []) + + const males = dogs.filter(d => d.sex === 'male') + const females = dogs.filter(d => d.sex === 'female') + + async function handleSimulate(e) { + e.preventDefault() + if (!sireId || !damId) return + setLoading(true) + setError(null) + setResult(null) + try { + const res = await fetch('/api/pedigree/trial-pairing', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sire_id: parseInt(sireId), dam_id: parseInt(damId) }) + }) + if (!res.ok) { + const err = await res.json() + throw new Error(err.error || 'Failed to calculate') + } + setResult(await res.json()) + } catch (err) { + setError(err.message) + } finally { + setLoading(false) + } + } + + function RiskBadge({ coi, recommendation }) { + const isLow = coi < 5 + const isMed = coi >= 5 && coi < 10 + const isHigh = coi >= 10 + return ( +
+ {isLow && } + {isMed && } + {isHigh && } + {recommendation} +
+ ) + } + + return ( +
+ {/* Header */} +
+
+
+ +
+

Trial Pairing Simulator

+
+

+ Select a sire and dam to calculate the estimated inbreeding coefficient (COI) and view common ancestors. +

+
+ + {/* Selector Card */} +
+
+
+
+ + + {!dogsLoading && males.length === 0 && ( +

No male dogs registered.

+ )} +
+ +
+ + + {!dogsLoading && females.length === 0 && ( +

No female dogs registered.

+ )} +
+
+ + +
+
+ + {/* Error */} + {error &&
{error}
} + + {/* Results */} + {result && ( +
+ {/* COI Summary */} +
+
+
+

Pairing

+

+ {result.sire.name} + × + {result.dam.name} +

+
+
+

COI

+

+ {result.coi.toFixed(2)}% +

+
+
+ +
+ +
+ +
+ COI Guide: <5% Low risk · 5–10% Moderate risk · >10% High risk +
+
+ + {/* Common Ancestors */} +
+
+ +

Common Ancestors

+ + {result.commonAncestors.length} found + +
+ + {result.commonAncestors.length === 0 ? ( +

+ No common ancestors found within 5 generations. This pairing has excellent genetic diversity. +

+ ) : ( +
+ + + + + + + + + + {result.commonAncestors.map((anc, i) => ( + + + + + + ))} + +
AncestorSire GenDam Gen
{anc.name} + Gen {anc.sireGen} + + Gen {anc.damGen} +
+
+ )} +
+
+ )} +
+ ) +}