feat(ui): integrate ClearanceSummaryCard and HealthRecordForm into DogDetail
This commit is contained in:
@@ -4,6 +4,8 @@ import { Dog, GitBranch, Edit, Upload, Trash2, ArrowLeft, Calendar, Hash, Award
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import DogForm from '../components/DogForm'
|
import DogForm from '../components/DogForm'
|
||||||
import { ChampionBadge, ChampionBloodlineBadge } from '../components/ChampionBadge'
|
import { ChampionBadge, ChampionBloodlineBadge } from '../components/ChampionBadge'
|
||||||
|
import ClearanceSummaryCard from '../components/ClearanceSummaryCard'
|
||||||
|
import HealthRecordForm from '../components/HealthRecordForm'
|
||||||
|
|
||||||
function DogDetail() {
|
function DogDetail() {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
@@ -15,7 +17,13 @@ function DogDetail() {
|
|||||||
const [selectedPhoto, setSelectedPhoto] = useState(0)
|
const [selectedPhoto, setSelectedPhoto] = useState(0)
|
||||||
const fileInputRef = useRef(null)
|
const fileInputRef = useRef(null)
|
||||||
|
|
||||||
|
// Health records state
|
||||||
|
const [healthRecords, setHealthRecords] = useState([])
|
||||||
|
const [showHealthForm, setShowHealthForm] = useState(false)
|
||||||
|
const [editingRecord, setEditingRecord] = useState(null)
|
||||||
|
|
||||||
useEffect(() => { fetchDog() }, [id])
|
useEffect(() => { fetchDog() }, [id])
|
||||||
|
useEffect(() => { fetchHealth() }, [id])
|
||||||
|
|
||||||
const fetchDog = async () => {
|
const fetchDog = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -28,6 +36,12 @@ function DogDetail() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchHealth = () => {
|
||||||
|
axios.get(`/api/health/dog/${id}`)
|
||||||
|
.then(r => setHealthRecords(r.data))
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
const handlePhotoUpload = async (e) => {
|
const handlePhotoUpload = async (e) => {
|
||||||
const file = e.target.files[0]
|
const file = e.target.files[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
@@ -66,7 +80,7 @@ function DogDetail() {
|
|||||||
if (!birthDate) return null
|
if (!birthDate) return null
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
const birth = new Date(birthDate)
|
const birth = new Date(birthDate)
|
||||||
let years = today.getFullYear() - birth.getFullYear()
|
let years = today.getFullYear() - birth.getFullYear()
|
||||||
let months = today.getMonth() - birth.getMonth()
|
let months = today.getMonth() - birth.getMonth()
|
||||||
if (months < 0) { years--; months += 12 }
|
if (months < 0) { years--; months += 12 }
|
||||||
if (years === 0) return `${months} month${months !== 1 ? 's' : ''}`
|
if (years === 0) return `${months} month${months !== 1 ? 's' : ''}`
|
||||||
@@ -77,10 +91,15 @@ function DogDetail() {
|
|||||||
const hasChampionBlood = (d) =>
|
const hasChampionBlood = (d) =>
|
||||||
(d.sire && d.sire.is_champion) || (d.dam && d.dam.is_champion)
|
(d.sire && d.sire.is_champion) || (d.dam && d.dam.is_champion)
|
||||||
|
|
||||||
if (loading) return <div className="container loading">Loading...</div>
|
const openAddHealth = () => { setEditingRecord(null); setShowHealthForm(true) }
|
||||||
if (!dog) return <div className="container">Dog not found</div>
|
const openEditHealth = (rec) => { setEditingRecord(rec); setShowHealthForm(true) }
|
||||||
|
const closeHealthForm = () => { setShowHealthForm(false); setEditingRecord(null) }
|
||||||
|
const handleHealthSaved = () => { closeHealthForm(); fetchHealth() }
|
||||||
|
|
||||||
const isChampion = !!dog.is_champion
|
if (loading) return <div className="container loading">Loading...</div>
|
||||||
|
if (!dog) return <div className="container">Dog not found</div>
|
||||||
|
|
||||||
|
const isChampion = !!dog.is_champion
|
||||||
const hasBloodline = !isChampion && hasChampionBlood(dog)
|
const hasBloodline = !isChampion && hasChampionBlood(dog)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -93,13 +112,13 @@ function DogDetail() {
|
|||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.625rem', flexWrap: 'wrap', marginBottom: '0.25rem' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.625rem', flexWrap: 'wrap', marginBottom: '0.25rem' }}>
|
||||||
<h1 style={{ margin: 0 }}>{dog.name}</h1>
|
<h1 style={{ margin: 0 }}>{dog.name}</h1>
|
||||||
{isChampion && <ChampionBadge size="lg" />}
|
{isChampion && <ChampionBadge size="lg" />}
|
||||||
{hasBloodline && <ChampionBloodlineBadge size="lg" />}
|
{hasBloodline && <ChampionBloodlineBadge size="lg" />}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', color: 'var(--text-secondary)' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', color: 'var(--text-secondary)' }}>
|
||||||
<span>{dog.breed}</span>
|
<span>{dog.breed}</span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span>{dog.sex === 'male' ? 'Male ♂' : 'Female ♀'}</span>
|
<span>{dog.sex === 'male' ? 'Male' : 'Female'}</span>
|
||||||
{dog.birth_date && (
|
{dog.birth_date && (
|
||||||
<>
|
<>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
@@ -180,8 +199,7 @@ function DogDetail() {
|
|||||||
onClick={() => setSelectedPhoto(index)}
|
onClick={() => setSelectedPhoto(index)}
|
||||||
style={{
|
style={{
|
||||||
width: '60px', height: '60px', objectFit: 'cover',
|
width: '60px', height: '60px', objectFit: 'cover',
|
||||||
borderRadius: 'var(--radius-sm)',
|
borderRadius: 'var(--radius-sm)', cursor: 'pointer',
|
||||||
cursor: 'pointer',
|
|
||||||
border: selectedPhoto === index ? '2px solid var(--primary)' : '1px solid var(--border)',
|
border: selectedPhoto === index ? '2px solid var(--primary)' : '1px solid var(--border)',
|
||||||
opacity: selectedPhoto === index ? 1 : 0.6,
|
opacity: selectedPhoto === index ? 1 : 0.6,
|
||||||
transition: 'all 0.2s'
|
transition: 'all 0.2s'
|
||||||
@@ -210,7 +228,7 @@ function DogDetail() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="info-row">
|
<div className="info-row">
|
||||||
<span className="info-label">Sex</span>
|
<span className="info-label">Sex</span>
|
||||||
<span className="info-value">{dog.sex === 'male' ? 'Male ♂' : 'Female ♀'}</span>
|
<span className="info-value">{dog.sex === 'male' ? 'Male' : 'Female'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-row">
|
<div className="info-row">
|
||||||
<span className="info-label">Champion</span>
|
<span className="info-label">Champion</span>
|
||||||
@@ -219,7 +237,7 @@ function DogDetail() {
|
|||||||
? <ChampionBadge size="lg" />
|
? <ChampionBadge size="lg" />
|
||||||
: hasBloodline
|
: hasBloodline
|
||||||
? <ChampionBloodlineBadge size="lg" />
|
? <ChampionBloodlineBadge size="lg" />
|
||||||
: <span style={{ color: 'var(--text-muted)', fontStyle: 'italic' }}>—</span>
|
: <span style={{ color: 'var(--text-muted)', fontStyle: 'italic' }}>—</span>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -296,6 +314,49 @@ function DogDetail() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* OFA Clearance Summary */}
|
||||||
|
<ClearanceSummaryCard dogId={id} onAddRecord={openAddHealth} />
|
||||||
|
|
||||||
|
{/* Health Records List */}
|
||||||
|
{healthRecords.length > 0 && (
|
||||||
|
<div className="card" style={{ marginBottom: '1.5rem' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||||
|
<h2 style={{ fontSize: '1rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', margin: 0 }}>
|
||||||
|
Health Records ({healthRecords.length})
|
||||||
|
</h2>
|
||||||
|
<button className="btn btn-ghost" style={{ fontSize: '0.8rem', padding: '0.35rem 0.75rem' }} onClick={openAddHealth}>
|
||||||
|
+ Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||||
|
{healthRecords.map(rec => (
|
||||||
|
<div key={rec.id} style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: '0.75rem',
|
||||||
|
padding: '0.6rem 0.75rem', background: 'var(--bg-primary)',
|
||||||
|
borderRadius: 'var(--radius-sm)', border: '1px solid var(--border)',
|
||||||
|
}}>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<span style={{ fontWeight: 500, fontSize: '0.875rem' }}>
|
||||||
|
{rec.test_name || (rec.test_type ? rec.test_type.replace(/_/g, ' ') : rec.record_type)}
|
||||||
|
</span>
|
||||||
|
{rec.ofa_result && (
|
||||||
|
<span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', color: 'var(--text-muted)' }}>
|
||||||
|
{rec.ofa_result}{rec.ofa_number ? ` · ${rec.ofa_number}` : ''}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: '0.8rem', color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>
|
||||||
|
{rec.test_date ? new Date(rec.test_date).toLocaleDateString() : ''}
|
||||||
|
</span>
|
||||||
|
<button className="btn-icon" style={{ padding: '0.2rem' }} onClick={() => openEditHealth(rec)}>
|
||||||
|
<Edit size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Offspring */}
|
{/* Offspring */}
|
||||||
{dog.offspring && dog.offspring.length > 0 && (
|
{dog.offspring && dog.offspring.length > 0 && (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
@@ -317,19 +378,19 @@ function DogDetail() {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '0.5rem'
|
gap: '0.5rem'
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={e => {
|
||||||
e.currentTarget.style.borderColor = 'var(--primary)'
|
e.currentTarget.style.borderColor = 'var(--primary)'
|
||||||
e.currentTarget.style.background = 'var(--bg-tertiary)'
|
e.currentTarget.style.background = 'var(--bg-tertiary)'
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={e => {
|
||||||
e.currentTarget.style.borderColor = 'var(--border)'
|
e.currentTarget.style.borderColor = 'var(--border)'
|
||||||
e.currentTarget.style.background = 'var(--bg-primary)'
|
e.currentTarget.style.background = 'var(--bg-primary)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ color: 'var(--text-primary)', fontWeight: 500 }}>{child.name}</span>
|
<span style={{ color: 'var(--text-primary)', fontWeight: 500 }}>{child.name}</span>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.35rem' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.35rem' }}>
|
||||||
{child.is_champion && <ChampionBadge />}
|
{child.is_champion && <ChampionBadge />}
|
||||||
<span style={{ fontSize: '1.125rem' }}>{child.sex === 'male' ? '♂' : '♀'}</span>
|
<span style={{ fontSize: '1.125rem' }}>{child.sex === 'male' ? '' : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
@@ -337,6 +398,7 @@ function DogDetail() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Edit Dog Modal */}
|
||||||
{showEditModal && (
|
{showEditModal && (
|
||||||
<DogForm
|
<DogForm
|
||||||
dog={dog}
|
dog={dog}
|
||||||
@@ -344,6 +406,16 @@ function DogDetail() {
|
|||||||
onSave={() => { fetchDog(); setShowEditModal(false) }}
|
onSave={() => { fetchDog(); setShowEditModal(false) }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Health Record Form Modal */}
|
||||||
|
{showHealthForm && (
|
||||||
|
<HealthRecordForm
|
||||||
|
dogId={id}
|
||||||
|
record={editingRecord}
|
||||||
|
onClose={closeHealthForm}
|
||||||
|
onSave={handleHealthSaved}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user