reverse pedigree
This commit is contained in:
@@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'
|
|||||||
import { ArrowLeft, GitBranch, AlertCircle, Loader } from 'lucide-react'
|
import { ArrowLeft, GitBranch, AlertCircle, Loader } from 'lucide-react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import PedigreeTree from '../components/PedigreeTree'
|
import PedigreeTree from '../components/PedigreeTree'
|
||||||
import { transformPedigreeData, formatCOI, getPedigreeCompleteness } from '../utils/pedigreeHelpers'
|
import { transformPedigreeData, transformDescendantData, formatCOI, getPedigreeCompleteness } from '../utils/pedigreeHelpers'
|
||||||
|
|
||||||
function PedigreeView() {
|
function PedigreeView() {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
@@ -14,16 +14,18 @@ function PedigreeView() {
|
|||||||
const [pedigreeData, setPedigreeData] = useState(null)
|
const [pedigreeData, setPedigreeData] = useState(null)
|
||||||
const [coiData, setCoiData] = useState(null)
|
const [coiData, setCoiData] = useState(null)
|
||||||
const [generations, setGenerations] = useState(5)
|
const [generations, setGenerations] = useState(5)
|
||||||
|
const [viewMode, setViewMode] = useState('ancestors')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchPedigreeData()
|
fetchPedigreeData()
|
||||||
}, [id, generations])
|
}, [id, generations, viewMode])
|
||||||
|
|
||||||
const fetchPedigreeData = async () => {
|
const fetchPedigreeData = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (viewMode === 'ancestors') {
|
||||||
const pedigreeRes = await axios.get(`/api/pedigree/${id}`)
|
const pedigreeRes = await axios.get(`/api/pedigree/${id}`)
|
||||||
const dogData = pedigreeRes.data
|
const dogData = pedigreeRes.data
|
||||||
setDog(dogData)
|
setDog(dogData)
|
||||||
@@ -38,6 +40,15 @@ function PedigreeView() {
|
|||||||
console.warn('COI calculation unavailable:', coiError)
|
console.warn('COI calculation unavailable:', coiError)
|
||||||
setCoiData(null)
|
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)
|
setLoading(false)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -86,7 +97,7 @@ function PedigreeView() {
|
|||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
{/* Header */}
|
{/* 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
|
<button
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
onClick={() => navigate(`/dogs/${id}`)}
|
onClick={() => navigate(`/dogs/${id}`)}
|
||||||
@@ -96,10 +107,10 @@ function PedigreeView() {
|
|||||||
Back to Profile
|
Back to Profile
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1, minWidth: '200px' }}>
|
||||||
<h1 style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
<h1 style={{ margin: 0, display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||||
<GitBranch size={32} style={{ color: 'var(--primary)' }} />
|
<GitBranch size={32} style={{ color: 'var(--primary)' }} />
|
||||||
{dog?.name}'s Pedigree
|
{dog?.name}'s {viewMode === 'ancestors' ? 'Pedigree' : 'Descendants'}
|
||||||
</h1>
|
</h1>
|
||||||
{dog?.registration_number && (
|
{dog?.registration_number && (
|
||||||
<p style={{ color: 'var(--text-secondary)', margin: '0.25rem 0 0 0' }}>
|
<p style={{ color: 'var(--text-secondary)', margin: '0.25rem 0 0 0' }}>
|
||||||
@@ -107,12 +118,31 @@ function PedigreeView() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Stats Bar */}
|
{/* Stats Bar */}
|
||||||
<div className="card" style={{ marginBottom: '1rem', padding: '1rem' }}>
|
<div className="card" style={{ marginBottom: '1rem', padding: '1rem' }}>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1.5rem' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1.5rem' }}>
|
||||||
|
|
||||||
|
{viewMode === 'ancestors' && (
|
||||||
|
<>
|
||||||
{/* COI */}
|
{/* COI */}
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginBottom: '0.25rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500 }}>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Generations */}
|
{/* Generations */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -181,3 +181,47 @@ export const getPedigreeCompleteness = (treeData, targetGenerations = 5) => {
|
|||||||
const actualCount = countAncestors(treeData)
|
const actualCount = countAncestors(treeData)
|
||||||
return Math.min(100, Math.round((actualCount / expectedTotal) * 100))
|
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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user