diff --git a/client/src/components/DogForm.jsx b/client/src/components/DogForm.jsx
index 24b6f24..34a2b20 100644
--- a/client/src/components/DogForm.jsx
+++ b/client/src/components/DogForm.jsx
@@ -1,8 +1,8 @@
import { useState, useEffect } from 'react'
-import { X, Award } from 'lucide-react'
+import { X, Award, ExternalLink } from 'lucide-react'
import axios from 'axios'
-function DogForm({ dog, onClose, onSave }) {
+function DogForm({ dog, onClose, onSave, isExternal = false }) {
const [formData, setFormData] = useState({
name: '',
registration_number: '',
@@ -16,6 +16,7 @@ function DogForm({ dog, onClose, onSave }) {
dam_id: null,
litter_id: null,
is_champion: false,
+ is_external: isExternal ? 1 : 0,
})
const [dogs, setDogs] = useState([])
const [litters, setLitters] = useState([])
@@ -24,9 +25,14 @@ function DogForm({ dog, onClose, onSave }) {
const [useManualParents, setUseManualParents] = useState(true)
const [littersAvailable, setLittersAvailable] = useState(false)
+ // Derive effective external state (editing an existing external dog or explicitly flagged)
+ const effectiveExternal = isExternal || (dog && dog.is_external)
+
useEffect(() => {
- fetchDogs()
- fetchLitters()
+ if (!effectiveExternal) {
+ fetchDogs()
+ fetchLitters()
+ }
if (dog) {
setFormData({
name: dog.name || '',
@@ -41,6 +47,7 @@ function DogForm({ dog, onClose, onSave }) {
dam_id: dog.dam?.id || null,
litter_id: dog.litter_id || null,
is_champion: !!dog.is_champion,
+ is_external: dog.is_external ?? (isExternal ? 1 : 0),
})
setUseManualParents(!dog.litter_id)
}
@@ -104,9 +111,10 @@ function DogForm({ dog, onClose, onSave }) {
const submitData = {
...formData,
is_champion: formData.is_champion ? 1 : 0,
- sire_id: formData.sire_id || null,
- dam_id: formData.dam_id || null,
- litter_id: useManualParents ? null : (formData.litter_id || null),
+ is_external: effectiveExternal ? 1 : 0,
+ sire_id: effectiveExternal ? null : (formData.sire_id || null),
+ dam_id: effectiveExternal ? null : (formData.dam_id || null),
+ litter_id: (effectiveExternal || useManualParents) ? null : (formData.litter_id || null),
registration_number: formData.registration_number || null,
birth_date: formData.birth_date || null,
color: formData.color || null,
@@ -133,10 +141,31 @@ function DogForm({ dog, onClose, onSave }) {
e.stopPropagation()}>
-
{dog ? 'Edit Dog' : 'Add New Dog'}
+
+ {effectiveExternal && }
+ {dog ? 'Edit Dog' : effectiveExternal ? 'Add External Dog' : 'Add New Dog'}
+
+ {effectiveExternal && (
+
+
+ External dog — not part of your kennel roster. Litter and parent fields are not applicable.
+
+ )}
+
- {/* Parent Section */}
-
-
+ {/* Parent Section — hidden for external dogs */}
+ {!effectiveExternal && (
+
+
- {littersAvailable && (
-
-
-
-
- )}
+ {littersAvailable && (
+
+
+
+
+ )}
- {!useManualParents && littersAvailable ? (
-
-
-
- {formData.litter_id && (
-
- ✓ Parents will be automatically set from the selected litter
+ {!useManualParents && littersAvailable ? (
+
+
+
+ {formData.litter_id && (
+
+ ✓ Parents will be automatically set from the selected litter
+
+ )}
+
+ ) : (
+
+
+
+
+
+
+
+
- )}
-
- ) : (
-
-
-
-
-
-
-
-
-
- )}
-
+ )}
+
+ )}
@@ -294,7 +325,7 @@ function DogForm({ dog, onClose, onSave }) {
diff --git a/client/src/pages/ExternalDogs.jsx b/client/src/pages/ExternalDogs.jsx
index 383ffc6..9baa308 100644
--- a/client/src/pages/ExternalDogs.jsx
+++ b/client/src/pages/ExternalDogs.jsx
@@ -1,20 +1,24 @@
import { useState, useEffect } from 'react';
-import { useNavigate } from 'react-router-dom';
import { Users, Plus, Search, ExternalLink, Award, Filter } from 'lucide-react';
+import DogForm from '../components/DogForm';
export default function ExternalDogs() {
- const [dogs, setDogs] = useState([]);
- const [loading, setLoading] = useState(true);
- const [search, setSearch] = useState('');
+ const [dogs, setDogs] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [search, setSearch] = useState('');
const [sexFilter, setSexFilter] = useState('all');
- const navigate = useNavigate();
+ const [showAddModal, setShowAddModal] = useState(false);
useEffect(() => {
+ fetchDogs();
+ }, []);
+
+ const fetchDogs = () => {
fetch('/api/dogs/external')
.then(r => r.json())
.then(data => { setDogs(data); setLoading(false); })
.catch(() => setLoading(false));
- }, []);
+ };
const filtered = dogs.filter(d => {
const matchSearch = d.name.toLowerCase().includes(search.toLowerCase()) ||
@@ -41,7 +45,7 @@ export default function ExternalDogs() {
@@ -75,7 +79,7 @@ export default function ExternalDogs() {
No external dogs yet
Add sires, dams, or ancestors that aren't part of your kennel roster.
-
@@ -85,7 +89,7 @@ export default function ExternalDogs() {
♂ Sires ({sires.length})
- {sires.map(dog => )}
+ {sires.map(dog => )}
)}
@@ -93,25 +97,34 @@ export default function ExternalDogs() {
♀ Dams ({dams.length})
- {dams.map(dog => )}
+ {dams.map(dog => )}
)}
)}
+
+ {/* Add External Dog Modal */}
+ {showAddModal && (
+ setShowAddModal(false)}
+ onSave={() => { fetchDogs(); setShowAddModal(false); }}
+ />
+ )}
);
}
-function DogCard({ dog, navigate }) {
+function DogCard({ dog }) {
const photo = dog.photo_urls?.[0];
return (
navigate(`/dogs/${dog.id}`)}
+ onClick={() => window.location.href = `/dogs/${dog.id}`}
role="button"
tabIndex={0}
- onKeyDown={e => e.key === 'Enter' && navigate(`/dogs/${dog.id}`)}
+ onKeyDown={e => e.key === 'Enter' && (window.location.href = `/dogs/${dog.id}`)}
>
{photo
@@ -129,7 +142,7 @@ function DogCard({ dog, navigate }) {
{dog.breed}
{dog.sex === 'male' ? '\u2642 Sire' : '\u2640 Dam'}
- {dog.birth_date && <> · {dog.birth_date}>}
+ {dog.birth_date && <>· {dog.birth_date}>}
{(dog.sire || dog.dam) && (
diff --git a/client/src/pages/PairingSimulator.jsx b/client/src/pages/PairingSimulator.jsx
index 047f708..1dc8236 100644
--- a/client/src/pages/PairingSimulator.jsx
+++ b/client/src/pages/PairingSimulator.jsx
@@ -13,7 +13,8 @@ export default function PairingSimulator() {
const [relationChecking, setRelationChecking] = useState(false)
useEffect(() => {
- fetch('/api/dogs')
+ // include_external=1 ensures external sires/dams appear for pairing
+ fetch('/api/dogs?include_external=1')
.then(r => r.json())
.then(data => {
setDogs(Array.isArray(data) ? data : (data.dogs || []))
@@ -54,9 +55,6 @@ export default function PairingSimulator() {
checkRelation(sireId, val)
}
- 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
@@ -64,16 +62,14 @@ export default function PairingSimulator() {
setError(null)
setResult(null)
try {
- const res = await fetch('/api/pedigree/trial-pairing', {
+ const res = await fetch('/api/pedigree/coi', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ sire_id: parseInt(sireId), dam_id: parseInt(damId) })
+ 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())
+ const data = await res.json()
+ if (!res.ok) throw new Error(data.error || 'Simulation failed')
+ setResult(data)
} catch (err) {
setError(err.message)
} finally {
@@ -81,204 +77,164 @@ export default function PairingSimulator() {
}
}
- function RiskBadge({ coi, recommendation }) {
- const isLow = coi < 5
- const isMed = coi >= 5 && coi < 10
- const isHigh = coi >= 10
- return (
-
- {isLow &&
}
- {isMed &&
}
- {isHigh &&
}
-
{recommendation}
-
- )
+ const males = dogs.filter(d => d.sex === 'male')
+ const females = dogs.filter(d => d.sex === 'female')
+
+ const coiColor = (coi) => {
+ if (coi < 0.0625) return 'var(--success)'
+ if (coi < 0.125) return 'var(--warning)'
+ return 'var(--danger)'
+ }
+
+ const coiLabel = (coi) => {
+ if (coi < 0.0625) return 'Low'
+ if (coi < 0.125) return 'Moderate'
+ if (coi < 0.25) return 'High'
+ return 'Very High'
}
return (
-
- {/* Header */}
-
-
-
-
-
-
Trial Pairing Simulator
-
-
- Select a sire and dam to calculate the estimated inbreeding coefficient (COI) and view common ancestors.
-
+
+
+
+
Pairing Simulator
+
+ Estimate the Coefficient of Inbreeding (COI) for a hypothetical pairing before breeding.
+ Includes both kennel and external dogs.
+
- {/* Selector Card */}
-