feat/litter-management-ui #25

Merged
jason merged 6 commits from feat/litter-management-ui into master 2026-03-09 20:55:43 -05:00
5 changed files with 551 additions and 32 deletions

View File

@@ -5,6 +5,7 @@ import DogList from './pages/DogList'
import DogDetail from './pages/DogDetail'
import PedigreeView from './pages/PedigreeView'
import LitterList from './pages/LitterList'
import LitterDetail from './pages/LitterDetail'
import BreedingCalendar from './pages/BreedingCalendar'
import PairingSimulator from './pages/PairingSimulator'
import './App.css'
@@ -55,6 +56,7 @@ function App() {
<Route path="/dogs/:id" element={<DogDetail />} />
<Route path="/pedigree/:id" element={<PedigreeView />} />
<Route path="/litters" element={<LitterList />} />
<Route path="/litters/:id" element={<LitterDetail />} />
<Route path="/breeding" element={<BreedingCalendar />} />
<Route path="/pairing" element={<PairingSimulator />} />
</Routes>

View File

@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'
import { X } from 'lucide-react'
import axios from 'axios'
function LitterForm({ litter, onClose, onSave }) {
function LitterForm({ litter, prefill, onClose, onSave }) {
const [formData, setFormData] = useState({
sire_id: '',
dam_id: '',
@@ -26,8 +26,16 @@ function LitterForm({ litter, onClose, onSave }) {
puppy_count: litter.puppy_count || 0,
notes: litter.notes || ''
})
} else if (prefill) {
// Pre-populate from BreedingCalendar "Record Litter" flow
setFormData(prev => ({
...prev,
dam_id: prefill.dam_id ? String(prefill.dam_id) : '',
breeding_date: prefill.breeding_date || '',
whelping_date: prefill.whelping_date || '',
}))
}
}, [litter])
}, [litter, prefill])
const fetchDogs = async () => {
try {
@@ -69,7 +77,7 @@ function LitterForm({ litter, onClose, onSave }) {
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>{litter ? 'Edit Litter' : 'Create New Litter'}</h2>
<h2>{litter ? 'Edit Litter' : prefill ? `Record Litter — ${prefill.dam_name || 'Dam pre-selected'}` : 'Create New Litter'}</h2>
<button className="btn-icon" onClick={onClose}>
<X size={24} />
</button>
@@ -78,6 +86,20 @@ function LitterForm({ litter, onClose, onSave }) {
<form onSubmit={handleSubmit} className="modal-body">
{error && <div className="error">{error}</div>}
{prefill && !litter && (
<div style={{
background: 'rgba(16,185,129,0.08)',
border: '1px solid rgba(16,185,129,0.3)',
borderRadius: 'var(--radius-sm)',
padding: '0.6rem 0.875rem',
marginBottom: '1rem',
fontSize: '0.85rem',
color: 'var(--success)'
}}>
🐾 Pre-filled from heat cycle select a sire to complete the litter record.
</div>
)}
<div className="form-grid">
<div className="form-group">
<label className="label">Sire (Father) *</label>
@@ -111,6 +133,11 @@ function LitterForm({ litter, onClose, onSave }) {
<option key={d.id} value={d.id}>{d.name} {d.registration_number ? `(${d.registration_number})` : ''}</option>
))}
</select>
{prefill?.dam_name && !litter && (
<p style={{ fontSize: '0.78rem', color: 'var(--success)', marginTop: '0.25rem' }}>
Pre-selected: {prefill.dam_name}
</p>
)}
</div>
<div className="form-group">

View File

@@ -1,15 +1,17 @@
import { useEffect, useState, useCallback } from 'react'
import {
Heart, ChevronLeft, ChevronRight, Plus, X,
CalendarDays, FlaskConical, Baby, AlertCircle, CheckCircle2
CalendarDays, FlaskConical, Baby, AlertCircle, CheckCircle2, Activity
} from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import axios from 'axios'
// ─── Date helpers ────────────────────────────────────────────────────────────
const toISO = d => d.toISOString().split('T')[0]
const addDays = (dateStr, n) => {
const d = new Date(dateStr); d.setDate(d.getDate() + n); return toISO(d)
}
const fmt = str => str ? new Date(str + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : ''
const fmt = str => str ? new Date(str + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : ''
const today = toISO(new Date())
// ─── Cycle window classifier ─────────────────────────────────────────────────
@@ -74,7 +76,7 @@ function StartCycleModal({ females, onClose, onSaved }) {
<div className="form-group">
<label className="label">Female Dog *</label>
<select value={dogId} onChange={e => setDogId(e.target.value)} required>
<option value=""> Select Female </option>
<option value=""> Select Female </option>
{females.map(d => (
<option key={d.id} value={d.id}>
{d.name}{d.breed ? ` · ${d.breed}` : ''}
@@ -105,7 +107,7 @@ function StartCycleModal({ females, onClose, onSaved }) {
}
// ─── Cycle Detail Modal ───────────────────────────────────────────────────────
function CycleDetailModal({ cycle, onClose, onDeleted }) {
function CycleDetailModal({ cycle, onClose, onDeleted, onRecordLitter }) {
const [suggestions, setSuggestions] = useState(null)
const [breedingDate, setBreedingDate] = useState(cycle.breeding_date || '')
const [savingBreed, setSavingBreed] = useState(false)
@@ -146,6 +148,7 @@ function CycleDetailModal({ cycle, onClose, onDeleted }) {
}
const whelp = suggestions?.whelping
const hasBreedingDate = !!(breedingDate && breedingDate === cycle.breeding_date)
return (
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
@@ -221,7 +224,7 @@ function CycleDetailModal({ cycle, onClose, onDeleted }) {
{/* Whelping estimate */}
{whelp && (
<div style={{ background: 'rgba(16,185,129,0.08)', border: '1px solid rgba(16,185,129,0.3)', borderRadius: 'var(--radius)', padding: '1rem' }}>
<div style={{ background: 'rgba(16,185,129,0.08)', border: '1px solid rgba(16,185,129,0.3)', borderRadius: 'var(--radius)', padding: '1rem', marginBottom: '1rem' }}>
<h3 style={{ fontSize: '0.9375rem', marginBottom: '0.75rem', display: 'flex', alignItems: 'center', gap: '0.4rem', color: 'var(--success)' }}>
<Baby size={16} /> Whelping Estimate
</h3>
@@ -235,6 +238,39 @@ function CycleDetailModal({ cycle, onClose, onDeleted }) {
</div>
</div>
)}
{/* Record Litter CTA — shown when breeding date is saved */}
{hasBreedingDate && (
<div style={{
background: 'rgba(16,185,129,0.06)',
border: '1px dashed rgba(16,185,129,0.5)',
borderRadius: 'var(--radius)',
padding: '0.875rem 1rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '1rem',
flexWrap: 'wrap'
}}>
<div>
<div style={{ fontWeight: 600, fontSize: '0.9rem' }}>🐾 Ready to record the litter?</div>
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginTop: '0.2rem' }}>
Breeding date logged on {fmt(cycle.breeding_date)}. Create a litter record to track puppies.
</div>
</div>
<button
className="btn btn-primary"
style={{ whiteSpace: 'nowrap', fontSize: '0.85rem' }}
onClick={() => {
onClose()
onRecordLitter(cycle)
}}
>
<Activity size={14} style={{ marginRight: '0.4rem' }} />
Record Litter
</button>
</div>
)}
</div>
<div className="modal-footer" style={{ justifyContent: 'space-between' }}>
<button className="btn btn-danger" onClick={deleteCycle} disabled={deleting}>
@@ -265,6 +301,8 @@ export default function BreedingCalendar() {
const [showStartModal, setShowStartModal] = useState(false)
const [selectedCycle, setSelectedCycle] = useState(null)
const [selectedDay, setSelectedDay] = useState(null)
const [pendingLitterCycle, setPendingLitterCycle] = useState(null)
const navigate = useNavigate()
const load = useCallback(async () => {
setLoading(true)
@@ -287,6 +325,23 @@ export default function BreedingCalendar() {
useEffect(() => { load() }, [load])
// When user clicks Record Litter from cycle detail, create litter and navigate
const handleRecordLitter = useCallback(async (cycle) => {
try {
// We need sire_id — navigate to litters page with pre-filled dam
// Store cycle info in sessionStorage so LitterList can pre-fill
sessionStorage.setItem('prefillLitter', JSON.stringify({
dam_id: cycle.dog_id,
dam_name: cycle.dog_name,
breeding_date: cycle.breeding_date,
whelping_date: cycle.whelping_date || ''
}))
navigate('/litters')
} catch (err) {
console.error(err)
}
}, [navigate])
// ── Build calendar grid ──
const firstDay = new Date(year, month, 1)
const lastDay = new Date(year, month + 1, 0)
@@ -306,7 +361,6 @@ export default function BreedingCalendar() {
else setMonth(m => m + 1)
}
// Find cycles that overlap a given date
function cyclesForDate(dateStr) {
return cycles.filter(c => {
const s = c.start_date
@@ -321,16 +375,12 @@ export default function BreedingCalendar() {
if (dayCycles.length === 1) {
setSelectedCycle(dayCycles[0])
} else if (dayCycles.length > 1) {
// show first — could be upgraded to a picker
setSelectedCycle(dayCycles[0])
} else {
// Empty day click — open start modal with date pre-filled would be nice
// but we just open start modal; user picks date
setShowStartModal(true)
}
}
// Active cycles (in current month or ongoing)
const activeCycles = cycles.filter(c => {
const s = c.start_date; if (!s) return false
const end = c.end_date || addDays(s, 28)
@@ -394,7 +444,6 @@ export default function BreedingCalendar() {
const dayCycles = dateStr ? cyclesForDate(dateStr) : []
const isToday = dateStr === today
// Pick dominant window color for background
let cellBg = 'transparent'
let cellBorder = 'var(--border)'
if (dayCycles.length > 0) {
@@ -522,6 +571,7 @@ export default function BreedingCalendar() {
cycle={selectedCycle}
onClose={() => setSelectedCycle(null)}
onDeleted={() => { setSelectedCycle(null); load() }}
onRecordLitter={handleRecordLitter}
/>
)}
</div>

View File

@@ -0,0 +1,344 @@
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

View File

@@ -1,60 +1,156 @@
import { useEffect, useState } from 'react'
import { Activity } from 'lucide-react'
import { Activity, Plus, Edit2, Trash2, ChevronRight } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import axios from 'axios'
import LitterForm from '../components/LitterForm'
function LitterList() {
const [litters, setLitters] = useState([])
const [loading, setLoading] = useState(true)
const [showForm, setShowForm] = useState(false)
const [editingLitter, setEditingLitter] = useState(null)
const [prefill, setPrefill] = useState(null)
const navigate = useNavigate()
useEffect(() => {
fetchLitters()
// Auto-open form with prefill from BreedingCalendar "Record Litter" CTA
const stored = sessionStorage.getItem('prefillLitter')
if (stored) {
try {
const data = JSON.parse(stored)
setPrefill(data)
setEditingLitter(null)
setShowForm(true)
} catch (e) { /* ignore */ }
sessionStorage.removeItem('prefillLitter')
}
}, [])
const fetchLitters = async () => {
try {
const res = await axios.get('/api/litters')
setLitters(res.data)
setLoading(false)
} catch (error) {
console.error('Error fetching litters:', error)
} finally {
setLoading(false)
}
}
const handleCreate = () => {
setEditingLitter(null)
setPrefill(null)
setShowForm(true)
}
const handleEdit = (e, litter) => {
e.stopPropagation()
setEditingLitter(litter)
setPrefill(null)
setShowForm(true)
}
const handleDelete = async (e, id) => {
e.stopPropagation()
if (!window.confirm('Delete this litter record? Puppies will be unlinked but not deleted.')) return
try {
await axios.delete(`/api/litters/${id}`)
fetchLitters()
} catch (error) {
console.error('Error deleting litter:', error)
}
}
const handleSave = () => {
fetchLitters()
}
if (loading) {
return <div className="container loading">Loading litters...</div>
}
return (
<div className="container">
<h1 style={{ marginBottom: '2rem' }}>Litters</h1>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
<h1>Litters</h1>
<button className="btn btn-primary" onClick={handleCreate}>
<Plus size={18} style={{ marginRight: '0.5rem' }} />
New Litter
</button>
</div>
{litters.length === 0 ? (
<div className="card" style={{ textAlign: 'center', padding: '4rem' }}>
<Activity size={64} style={{ color: 'var(--text-secondary)', margin: '0 auto 1rem' }} />
<h2>No litters recorded yet</h2>
<p style={{ color: 'var(--text-secondary)' }}>Start tracking breeding records</p>
<p style={{ color: 'var(--text-secondary)', marginBottom: '1.5rem' }}>Create a litter after a breeding cycle to track puppies</p>
<button className="btn btn-primary" onClick={handleCreate}>
<Plus size={18} style={{ marginRight: '0.5rem' }} />
Create First Litter
</button>
</div>
) : (
<div style={{ display: 'grid', gap: '1rem' }}>
{litters.map(litter => (
<div key={litter.id} className="card">
<h3>{litter.sire_name} × {litter.dam_name}</h3>
<p style={{ color: 'var(--text-secondary)', marginTop: '0.5rem' }}>
Breeding Date: {new Date(litter.breeding_date).toLocaleDateString()}
</p>
<div
key={litter.id}
className="card"
style={{ cursor: 'pointer', transition: 'border-color 0.2s' }}
onClick={() => navigate(`/litters/${litter.id}`)}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
<h3 style={{ marginBottom: '0.5rem' }}>
🐾 {litter.sire_name} × {litter.dam_name}
</h3>
<div style={{ display: 'flex', gap: '1.5rem', flexWrap: 'wrap', color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
<span>📅 Bred: {new Date(litter.breeding_date).toLocaleDateString()}</span>
{litter.whelping_date && (
<p style={{ color: 'var(--text-secondary)' }}>
Whelping Date: {new Date(litter.whelping_date).toLocaleDateString()}
<span>💕 Whelped: {new Date(litter.whelping_date).toLocaleDateString()}</span>
)}
<span style={{ color: 'var(--accent)', fontWeight: 600 }}>
{litter.actual_puppy_count ?? litter.puppies?.length ?? litter.puppy_count ?? 0} puppies
</span>
</div>
{litter.notes && (
<p style={{ marginTop: '0.5rem', fontSize: '0.85rem', color: 'var(--text-secondary)', fontStyle: 'italic' }}>
{litter.notes}
</p>
)}
<p style={{ marginTop: '0.5rem' }}>
<strong>Puppies:</strong> {litter.puppy_count || litter.puppies?.length || 0}
</p>
</div>
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
<button
className="btn-icon"
title="Edit litter"
onClick={(e) => handleEdit(e, litter)}
>
<Edit2 size={16} />
</button>
<button
className="btn-icon"
title="Delete litter"
onClick={(e) => handleDelete(e, litter.id)}
style={{ color: '#e53e3e' }}
>
<Trash2 size={16} />
</button>
<ChevronRight size={20} style={{ color: 'var(--text-secondary)' }} />
</div>
</div>
</div>
))}
</div>
)}
{showForm && (
<LitterForm
litter={editingLitter}
prefill={prefill}
onClose={() => setShowForm(false)}
onSave={handleSave}
/>
)}
</div>
)
}