reverse pedigree

This commit is contained in:
2026-03-12 07:27:41 -05:00
parent 5ca594fdc7
commit 42bab14ac3
2 changed files with 143 additions and 67 deletions

View File

@@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'
import { ArrowLeft, GitBranch, AlertCircle, Loader } from 'lucide-react'
import axios from 'axios'
import PedigreeTree from '../components/PedigreeTree'
import { transformPedigreeData, formatCOI, getPedigreeCompleteness } from '../utils/pedigreeHelpers'
import { transformPedigreeData, transformDescendantData, formatCOI, getPedigreeCompleteness } from '../utils/pedigreeHelpers'
function PedigreeView() {
const { id } = useParams()
@@ -14,16 +14,18 @@ function PedigreeView() {
const [pedigreeData, setPedigreeData] = useState(null)
const [coiData, setCoiData] = useState(null)
const [generations, setGenerations] = useState(5)
const [viewMode, setViewMode] = useState('ancestors')
useEffect(() => {
fetchPedigreeData()
}, [id, generations])
}, [id, generations, viewMode])
const fetchPedigreeData = async () => {
setLoading(true)
setError('')
try {
if (viewMode === 'ancestors') {
const pedigreeRes = await axios.get(`/api/pedigree/${id}`)
const dogData = pedigreeRes.data
setDog(dogData)
@@ -38,6 +40,15 @@ function PedigreeView() {
console.warn('COI calculation unavailable:', coiError)
setCoiData(null)
}
} else {
const descendantRes = await axios.get(`/api/pedigree/${id}/descendants?generations=${generations}`)
const dogData = descendantRes.data
setDog(dogData)
const treeData = transformDescendantData(dogData, generations)
setPedigreeData(treeData)
setCoiData(null)
}
setLoading(false)
} catch (err) {
@@ -86,7 +97,7 @@ function PedigreeView() {
return (
<div className="container">
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1.5rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
<button
className="btn btn-secondary"
onClick={() => navigate(`/dogs/${id}`)}
@@ -96,10 +107,10 @@ function PedigreeView() {
Back to Profile
</button>
<div style={{ flex: 1 }}>
<div style={{ flex: 1, minWidth: '200px' }}>
<h1 style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<GitBranch size={32} style={{ color: 'var(--primary)' }} />
{dog?.name}'s Pedigree
{dog?.name}'s {viewMode === 'ancestors' ? 'Pedigree' : 'Descendants'}
</h1>
{dog?.registration_number && (
<p style={{ color: 'var(--text-secondary)', margin: '0.25rem 0 0 0' }}>
@@ -107,12 +118,31 @@ function PedigreeView() {
</p>
)}
</div>
<div style={{ display: 'flex', background: 'var(--bg-tertiary)', padding: '4px', borderRadius: 'var(--radius)' }}>
<button
className={`btn ${viewMode === 'ancestors' ? 'btn-primary' : 'btn-ghost'}`}
onClick={() => setViewMode('ancestors')}
style={{ padding: '0.5rem 1rem' }}
>
Ancestors
</button>
<button
className={`btn ${viewMode === 'descendants' ? 'btn-primary' : 'btn-ghost'}`}
onClick={() => setViewMode('descendants')}
style={{ padding: '0.5rem 1rem' }}
>
Descendants
</button>
</div>
</div>
{/* Stats Bar */}
<div className="card" style={{ marginBottom: '1rem', padding: '1rem' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1.5rem' }}>
{viewMode === 'ancestors' && (
<>
{/* COI */}
<div>
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: '0.25rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }}>
@@ -166,6 +196,8 @@ function PedigreeView() {
</div>
</div>
</div>
</>
)}
{/* Generations */}
<div>

View File

@@ -181,3 +181,47 @@ export const getPedigreeCompleteness = (treeData, targetGenerations = 5) => {
const actualCount = countAncestors(treeData)
return Math.min(100, Math.round((actualCount / expectedTotal) * 100))
}
/**
* Transform API descendant data to react-d3-tree format
* @param {Object} dog - Dog object from API with nested offspring array
* @param {number} maxGenerations - Maximum generations to display (default 3)
* @returns {Object} Tree data in react-d3-tree format
*/
export const transformDescendantData = (dog, maxGenerations = 3) => {
if (!dog) return null
const buildTree = (dogData, generation = 0) => {
if (!dogData || generation >= maxGenerations) {
return null
}
const node = {
name: dogData.name || 'Unknown',
attributes: {
id: dogData.id,
sex: dogData.sex,
registration: dogData.registration_number || '',
birth_year: dogData.birth_date ? new Date(dogData.birth_date).getFullYear() : ''
},
children: []
}
if (dogData.offspring && dogData.offspring.length > 0) {
dogData.offspring.forEach(child => {
const childNode = buildTree(child, generation + 1)
if (childNode) {
node.children.push(childNode)
}
})
}
if (node.children.length === 0) {
delete node.children
}
return node
}
return buildTree(dog)
}