Files
breedr/client/src/pages/LitterDetail.jsx

345 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { ArrowLeft, Plus, X, ExternalLink, Dog } from 'lucide-react'
import axios from 'axios'
import LitterForm from '../components/LitterForm'
function LitterDetail() {
const { id } = useParams()
const navigate = useNavigate()
const [litter, setLitter] = useState(null)
const [loading, setLoading] = useState(true)
const [showEditForm, setShowEditForm] = useState(false)
const [showAddPuppy, setShowAddPuppy] = useState(false)
const [allDogs, setAllDogs] = useState([])
const [selectedPuppyId, setSelectedPuppyId] = useState('')
const [newPuppy, setNewPuppy] = useState({ name: '', sex: 'male', color: '', dob: '' })
const [addMode, setAddMode] = useState('existing') // 'existing' | 'new'
const [error, setError] = useState('')
const [saving, setSaving] = useState(false)
useEffect(() => {
fetchLitter()
fetchAllDogs()
}, [id])
const fetchLitter = async () => {
try {
const res = await axios.get(`/api/litters/${id}`)
setLitter(res.data)
} catch (err) {
console.error('Error fetching litter:', err)
} finally {
setLoading(false)
}
}
const fetchAllDogs = async () => {
try {
const res = await axios.get('/api/dogs')
setAllDogs(res.data)
} catch (err) {
console.error('Error fetching dogs:', err)
}
}
const unlinkedDogs = allDogs.filter(d => {
if (!litter) return false
const alreadyInLitter = litter.puppies?.some(p => p.id === d.id)
const isSireOrDam = d.id === litter.sire_id || d.id === litter.dam_id
return !alreadyInLitter && !isSireOrDam
})
const handleLinkPuppy = async () => {
if (!selectedPuppyId) return
setSaving(true)
setError('')
try {
await axios.post(`/api/litters/${id}/puppies/${selectedPuppyId}`)
setSelectedPuppyId('')
setShowAddPuppy(false)
fetchLitter()
} catch (err) {
setError(err.response?.data?.error || 'Failed to link puppy')
} finally {
setSaving(false)
}
}
const handleCreateAndLink = async () => {
if (!newPuppy.name) {
setError('Puppy name is required')
return
}
setSaving(true)
setError('')
try {
const dob = newPuppy.dob || litter.whelping_date || litter.breeding_date
const res = await axios.post('/api/dogs', {
name: newPuppy.name,
sex: newPuppy.sex,
color: newPuppy.color,
date_of_birth: dob,
breed: litter.dam_breed || '',
})
const createdDog = res.data
await axios.post(`/api/litters/${id}/puppies/${createdDog.id}`)
setNewPuppy({ name: '', sex: 'male', color: '', dob: '' })
setShowAddPuppy(false)
fetchLitter()
fetchAllDogs()
} catch (err) {
setError(err.response?.data?.error || 'Failed to create puppy')
} finally {
setSaving(false)
}
}
const handleUnlinkPuppy = async (puppyId) => {
if (!window.confirm('Remove this puppy from the litter? The dog record will not be deleted.')) return
try {
await axios.delete(`/api/litters/${id}/puppies/${puppyId}`)
fetchLitter()
} catch (err) {
console.error('Error unlinking puppy:', err)
}
}
if (loading) return <div className="container loading">Loading litter...</div>
if (!litter) return <div className="container"><p>Litter not found.</p></div>
const puppyCount = litter.puppies?.length ?? 0
return (
<div className="container">
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '2rem' }}>
<button className="btn-icon" onClick={() => navigate('/litters')}>
<ArrowLeft size={20} />
</button>
<div style={{ flex: 1 }}>
<h1 style={{ margin: 0 }}>🐾 {litter.sire_name} × {litter.dam_name}</h1>
<p style={{ color: 'var(--text-secondary)', margin: '0.25rem 0 0' }}>
Bred: {new Date(litter.breeding_date).toLocaleDateString()}
{litter.whelping_date && ` • Whelped: ${new Date(litter.whelping_date).toLocaleDateString()}`}
</p>
</div>
<button className="btn btn-secondary" onClick={() => setShowEditForm(true)}>
Edit Litter
</button>
</div>
{/* Stats row */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '1rem', marginBottom: '2rem' }}>
<div className="card" style={{ textAlign: 'center', padding: '1rem' }}>
<div style={{ fontSize: '2rem', fontWeight: 700, color: 'var(--accent)' }}>{puppyCount}</div>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.85rem' }}>Puppies Linked</div>
</div>
<div className="card" style={{ textAlign: 'center', padding: '1rem' }}>
<div style={{ fontSize: '2rem', fontWeight: 700, color: 'var(--accent)' }}>
{litter.puppies?.filter(p => p.sex === 'male').length ?? 0}
</div>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.85rem' }}>Males</div>
</div>
<div className="card" style={{ textAlign: 'center', padding: '1rem' }}>
<div style={{ fontSize: '2rem', fontWeight: 700, color: 'var(--accent)' }}>
{litter.puppies?.filter(p => p.sex === 'female').length ?? 0}
</div>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.85rem' }}>Females</div>
</div>
{litter.puppy_count > 0 && (
<div className="card" style={{ textAlign: 'center', padding: '1rem' }}>
<div style={{ fontSize: '2rem', fontWeight: 700 }}>{litter.puppy_count}</div>
<div style={{ color: 'var(--text-secondary)', fontSize: '0.85rem' }}>Expected</div>
</div>
)}
</div>
{/* Notes */}
{litter.notes && (
<div className="card" style={{ marginBottom: '2rem', padding: '1rem', borderLeft: '3px solid var(--accent)' }}>
<p style={{ margin: 0, fontStyle: 'italic', color: 'var(--text-secondary)' }}>{litter.notes}</p>
</div>
)}
{/* Puppies section */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
<h2 style={{ margin: 0 }}>Puppies</h2>
<button className="btn btn-primary" onClick={() => { setShowAddPuppy(true); setError('') }}>
<Plus size={16} style={{ marginRight: '0.4rem' }} />
Add Puppy
</button>
</div>
{puppyCount === 0 ? (
<div className="card" style={{ textAlign: 'center', padding: '3rem' }}>
<Dog size={48} style={{ color: 'var(--text-secondary)', margin: '0 auto 1rem' }} />
<p style={{ color: 'var(--text-secondary)' }}>No puppies linked yet. Add puppies to this litter.</p>
</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: '1rem' }}>
{litter.puppies.map(puppy => (
<div key={puppy.id} className="card" style={{ position: 'relative' }}>
<button
className="btn-icon"
onClick={() => handleUnlinkPuppy(puppy.id)}
title="Remove from litter"
style={{ position: 'absolute', top: '0.75rem', right: '0.75rem', color: '#e53e3e' }}
>
<X size={14} />
</button>
<div style={{ fontSize: '2.5rem', marginBottom: '0.5rem' }}>
{puppy.sex === 'male' ? '🐦' : '🐥'}
</div>
<div style={{ fontWeight: 600, marginBottom: '0.25rem' }}>{puppy.name}</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
{puppy.sex} {puppy.color && `${puppy.color}`}
</div>
{puppy.date_of_birth && (
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
Born: {new Date(puppy.date_of_birth).toLocaleDateString()}
</div>
)}
<button
className="btn btn-secondary"
style={{ marginTop: '0.75rem', width: '100%', fontSize: '0.8rem', padding: '0.4rem' }}
onClick={() => navigate(`/dogs/${puppy.id}`)}
>
<ExternalLink size={12} style={{ marginRight: '0.3rem' }} />
View Profile
</button>
</div>
))}
</div>
)}
{/* Add Puppy Modal */}
{showAddPuppy && (
<div className="modal-overlay" onClick={() => setShowAddPuppy(false)}>
<div className="modal-content" onClick={e => e.stopPropagation()} style={{ maxWidth: '480px' }}>
<div className="modal-header">
<h2>Add Puppy to Litter</h2>
<button className="btn-icon" onClick={() => setShowAddPuppy(false)}><X size={24} /></button>
</div>
<div className="modal-body">
{error && <div className="error" style={{ marginBottom: '1rem' }}>{error}</div>}
{/* Mode toggle */}
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1.5rem' }}>
<button
className={`btn ${addMode === 'existing' ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setAddMode('existing')}
style={{ flex: 1 }}
>
Link Existing Dog
</button>
<button
className={`btn ${addMode === 'new' ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setAddMode('new')}
style={{ flex: 1 }}
>
Create New Puppy
</button>
</div>
{addMode === 'existing' ? (
<div className="form-group">
<label className="label">Select Dog</label>
<select
className="input"
value={selectedPuppyId}
onChange={e => setSelectedPuppyId(e.target.value)}
>
<option value="">-- Select a dog --</option>
{unlinkedDogs.map(d => (
<option key={d.id} value={d.id}>
{d.name} ({d.sex}{d.date_of_birth ? `, born ${new Date(d.date_of_birth).toLocaleDateString()}` : ''})
</option>
))}
</select>
{unlinkedDogs.length === 0 && (
<p style={{ color: 'var(--text-secondary)', fontSize: '0.85rem', marginTop: '0.5rem' }}>
No unlinked dogs available. Use "Create New Puppy" instead.
</p>
)}
</div>
) : (
<div style={{ display: 'grid', gap: '1rem' }}>
<div className="form-group">
<label className="label">Puppy Name *</label>
<input
className="input"
value={newPuppy.name}
onChange={e => setNewPuppy(p => ({ ...p, name: e.target.value }))}
placeholder="e.g. Blue Collar"
/>
</div>
<div className="form-grid">
<div className="form-group">
<label className="label">Sex</label>
<select
className="input"
value={newPuppy.sex}
onChange={e => setNewPuppy(p => ({ ...p, sex: e.target.value }))}
>
<option value="male">Male</option>
<option value="female">Female</option>
</select>
</div>
<div className="form-group">
<label className="label">Color / Markings</label>
<input
className="input"
value={newPuppy.color}
onChange={e => setNewPuppy(p => ({ ...p, color: e.target.value }))}
placeholder="e.g. Black & Tan"
/>
</div>
</div>
<div className="form-group">
<label className="label">Date of Birth</label>
<input
type="date"
className="input"
value={newPuppy.dob}
onChange={e => setNewPuppy(p => ({ ...p, dob: e.target.value }))}
/>
{litter.whelping_date && !newPuppy.dob && (
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginTop: '0.25rem' }}>
Will default to whelping date: {new Date(litter.whelping_date).toLocaleDateString()}
</p>
)}
</div>
</div>
)}
</div>
<div className="modal-footer">
<button className="btn btn-secondary" onClick={() => setShowAddPuppy(false)} disabled={saving}>
Cancel
</button>
<button
className="btn btn-primary"
disabled={saving || (addMode === 'existing' && !selectedPuppyId)}
onClick={addMode === 'existing' ? handleLinkPuppy : handleCreateAndLink}
>
{saving ? 'Saving...' : addMode === 'existing' ? 'Link Puppy' : 'Create & Link'}
</button>
</div>
</div>
</div>
)}
{/* Edit Litter Modal */}
{showEditForm && (
<LitterForm
litter={litter}
onClose={() => setShowEditForm(false)}
onSave={fetchLitter}
/>
)}
</div>
)
}
export default LitterDetail