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

580 lines
27 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, useCallback } from 'react'
import {
Heart, ChevronLeft, ChevronRight, Plus, X,
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 today = toISO(new Date())
// ─── Cycle window classifier ─────────────────────────────────────────────────
function getWindowForDate(cycle, dateStr) {
if (!cycle?.start_date) return null
const start = new Date(cycle.start_date + 'T00:00:00')
const check = new Date(dateStr + 'T00:00:00')
const day = Math.round((check - start) / 86400000)
if (day < 0 || day > 28) return null
if (day <= 8) return 'proestrus'
if (day <= 15) return 'optimal'
if (day <= 21) return 'late'
return 'diestrus'
}
const WINDOW_STYLES = {
proestrus: { bg: 'rgba(244,114,182,0.18)', border: '#f472b6', label: 'Proestrus', dot: '#f472b6' },
optimal: { bg: 'rgba(16,185,129,0.22)', border: '#10b981', label: 'Optimal Breeding', dot: '#10b981' },
late: { bg: 'rgba(245,158,11,0.18)', border: '#f59e0b', label: 'Late Estrus', dot: '#f59e0b' },
diestrus: { bg: 'rgba(148,163,184,0.12)', border: '#64748b', label: 'Diestrus', dot: '#64748b' },
}
// ─── Start Heat Cycle Modal ───────────────────────────────────────────────────
function StartCycleModal({ females, onClose, onSaved }) {
const [dogId, setDogId] = useState('')
const [startDate, setStartDate] = useState(today)
const [notes, setNotes] = useState('')
const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
async function handleSubmit(e) {
e.preventDefault()
if (!dogId || !startDate) return
setSaving(true); setError(null)
try {
const res = await fetch('/api/breeding/heat-cycles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dog_id: parseInt(dogId), start_date: startDate, notes: notes || null })
})
if (!res.ok) { const e = await res.json(); throw new Error(e.error || 'Failed to save') }
onSaved()
} catch (err) {
setError(err.message)
setSaving(false)
}
}
return (
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
<div className="modal-content" style={{ maxWidth: '480px' }}>
<div className="modal-header">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
<Heart size={18} style={{ color: '#f472b6' }} />
<h2>Start Heat Cycle</h2>
</div>
<button className="btn-icon" onClick={onClose}><X size={20} /></button>
</div>
<form onSubmit={handleSubmit}>
<div className="modal-body">
{error && <div className="error" style={{ marginBottom: '1rem' }}>{error}</div>}
<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>
{females.map(d => (
<option key={d.id} value={d.id}>
{d.name}{d.breed ? ` · ${d.breed}` : ''}
</option>
))}
</select>
{females.length === 0 && <p style={{ color: 'var(--text-muted)', fontSize: '0.8rem', marginTop: '0.4rem' }}>No female dogs registered.</p>}
</div>
<div className="form-group">
<label className="label">Heat Start Date *</label>
<input type="date" className="input" value={startDate} onChange={e => setStartDate(e.target.value)} required />
</div>
<div className="form-group" style={{ marginBottom: 0 }}>
<label className="label">Notes</label>
<textarea className="input" value={notes} onChange={e => setNotes(e.target.value)} placeholder="Optional notes..." rows={3} />
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" onClick={onClose}>Cancel</button>
<button type="submit" className="btn btn-primary" disabled={saving || !dogId}>
{saving ? 'Saving…' : <><Heart size={15} /> Start Cycle</>}
</button>
</div>
</form>
</div>
</div>
)
}
// ─── Cycle Detail Modal ───────────────────────────────────────────────────────
function CycleDetailModal({ cycle, onClose, onDeleted, onRecordLitter }) {
const [suggestions, setSuggestions] = useState(null)
const [breedingDate, setBreedingDate] = useState(cycle.breeding_date || '')
const [savingBreed, setSavingBreed] = useState(false)
const [deleting, setDeleting] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
fetch(`/api/breeding/heat-cycles/${cycle.id}/suggestions`)
.then(r => r.json())
.then(setSuggestions)
.catch(() => {})
}, [cycle.id])
async function saveBreedingDate() {
if (!breedingDate) return
setSavingBreed(true); setError(null)
try {
const res = await fetch(`/api/breeding/heat-cycles/${cycle.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...cycle, breeding_date: breedingDate })
})
if (!res.ok) { const e = await res.json(); throw new Error(e.error) }
// Refresh suggestions
const s = await fetch(`/api/breeding/heat-cycles/${cycle.id}/suggestions`).then(r => r.json())
setSuggestions(s)
} catch (err) { setError(err.message) }
finally { setSavingBreed(false) }
}
async function deleteCycle() {
if (!window.confirm(`Delete heat cycle for ${cycle.dog_name}? This cannot be undone.`)) return
setDeleting(true)
try {
await fetch(`/api/breeding/heat-cycles/${cycle.id}`, { method: 'DELETE' })
onDeleted()
} catch (err) { setError(err.message); setDeleting(false) }
}
const whelp = suggestions?.whelping
const hasBreedingDate = !!(breedingDate && breedingDate === cycle.breeding_date)
return (
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
<div className="modal-content" style={{ maxWidth: '560px' }}>
<div className="modal-header">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
<Heart size={18} style={{ color: '#f472b6' }} />
<h2>{cycle.dog_name}</h2>
</div>
<button className="btn-icon" onClick={onClose}><X size={20} /></button>
</div>
<div className="modal-body">
{error && <div className="error">{error}</div>}
{/* Cycle meta */}
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1.5rem', flexWrap: 'wrap' }}>
<div style={infoChip}>
<span style={{ color: 'var(--text-muted)', fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Started</span>
<span style={{ fontWeight: 600 }}>{fmt(cycle.start_date)}</span>
</div>
{cycle.breed && (
<div style={infoChip}>
<span style={{ color: 'var(--text-muted)', fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Breed</span>
<span style={{ fontWeight: 600 }}>{cycle.breed}</span>
</div>
)}
</div>
{/* Breeding date windows */}
{suggestions && (
<>
<h3 style={{ fontSize: '0.9375rem', marginBottom: '0.75rem', display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
<FlaskConical size={16} style={{ color: 'var(--accent)' }} /> Breeding Date Windows
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', marginBottom: '1.5rem' }}>
{suggestions.windows.map(w => (
<div key={w.type} style={{
display: 'flex', alignItems: 'flex-start', gap: '0.75rem',
padding: '0.625rem 0.875rem',
background: WINDOW_STYLES[w.type]?.bg,
border: `1px solid ${WINDOW_STYLES[w.type]?.border}`,
borderRadius: 'var(--radius-sm)'
}}>
<div style={{ width: 10, height: 10, borderRadius: '50%', background: WINDOW_STYLES[w.type]?.dot, marginTop: 4, flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
<span style={{ fontWeight: 600, fontSize: '0.875rem' }}>{w.label}</span>
<span style={{ fontSize: '0.8125rem', color: 'var(--text-secondary)', whiteSpace: 'nowrap' }}>{fmt(w.start)} {fmt(w.end)}</span>
</div>
<p style={{ fontSize: '0.8rem', color: 'var(--text-muted)', margin: '0.15rem 0 0' }}>{w.description}</p>
</div>
</div>
))}
</div>
</>
)}
{/* Log breeding date */}
<div style={{ background: 'var(--bg-tertiary)', borderRadius: 'var(--radius)', padding: '1rem', marginBottom: '1.25rem' }}>
<h3 style={{ fontSize: '0.9375rem', marginBottom: '0.75rem', display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
<CalendarDays size={16} style={{ color: 'var(--primary)' }} /> Log Breeding Date
</h3>
<div style={{ display: 'flex', gap: '0.75rem', alignItems: 'flex-end', flexWrap: 'wrap' }}>
<div style={{ flex: 1, minWidth: 160 }}>
<label className="label" style={{ marginBottom: '0.4rem' }}>Breeding Date</label>
<input type="date" className="input" value={breedingDate} onChange={e => setBreedingDate(e.target.value)} />
</div>
<button className="btn btn-primary" onClick={saveBreedingDate} disabled={savingBreed || !breedingDate} style={{ marginBottom: 0 }}>
{savingBreed ? 'Saving…' : 'Save'}
</button>
</div>
</div>
{/* 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', 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>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '0.75rem', textAlign: 'center' }}>
{[['Earliest', whelp.earliest], ['Expected', whelp.expected], ['Latest', whelp.latest]].map(([label, date]) => (
<div key={label}>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '0.2rem' }}>{label}</div>
<div style={{ fontWeight: 700, fontSize: '0.9375rem' }}>{fmt(date)}</div>
</div>
))}
</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}>
{deleting ? 'Deleting…' : 'Delete Cycle'}
</button>
<button className="btn btn-secondary" onClick={onClose}>Close</button>
</div>
</div>
</div>
)
}
const infoChip = {
display: 'flex', flexDirection: 'column', gap: '0.15rem',
padding: '0.5rem 0.875rem',
background: 'var(--bg-tertiary)',
borderRadius: 'var(--radius-sm)'
}
// ─── Main Calendar ────────────────────────────────────────────────────────────
export default function BreedingCalendar() {
const now = new Date()
const [year, setYear] = useState(now.getFullYear())
const [month, setMonth] = useState(now.getMonth()) // 0-indexed
const [cycles, setCycles] = useState([])
const [females, setFemales] = useState([])
const [loading, setLoading] = useState(true)
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)
try {
const [cyclesRes, dogsRes] = await Promise.all([
fetch('/api/breeding/heat-cycles'),
fetch('/api/dogs')
])
const allCycles = await cyclesRes.json()
const dogsData = await dogsRes.json()
const allDogs = Array.isArray(dogsData) ? dogsData : (dogsData.dogs || [])
setCycles(Array.isArray(allCycles) ? allCycles : [])
setFemales(allDogs.filter(d => d.sex === 'female'))
} catch (e) {
console.error(e)
} finally {
setLoading(false)
}
}, [])
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)
const startPad = firstDay.getDay() // 0=Sun
const totalCells = startPad + lastDay.getDate()
const rows = Math.ceil(totalCells / 7)
const MONTH_NAMES = ['January','February','March','April','May','June','July','August','September','October','November','December']
const DAY_NAMES = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
function prevMonth() {
if (month === 0) { setMonth(11); setYear(y => y - 1) }
else setMonth(m => m - 1)
}
function nextMonth() {
if (month === 11) { setMonth(0); setYear(y => y + 1) }
else setMonth(m => m + 1)
}
function cyclesForDate(dateStr) {
return cycles.filter(c => {
const s = c.start_date
if (!s) return false
const end = c.end_date || addDays(s, 28)
return dateStr >= s && dateStr <= end
})
}
function handleDayClick(dateStr, dayCycles) {
setSelectedDay(dateStr)
if (dayCycles.length === 1) {
setSelectedCycle(dayCycles[0])
} else if (dayCycles.length > 1) {
setSelectedCycle(dayCycles[0])
} else {
setShowStartModal(true)
}
}
const activeCycles = cycles.filter(c => {
const s = c.start_date; if (!s) return false
const end = c.end_date || addDays(s, 28)
const mStart = toISO(new Date(year, month, 1))
const mEnd = toISO(new Date(year, month + 1, 0))
return s <= mEnd && end >= mStart
})
return (
<div className="container" style={{ paddingTop: '2rem', paddingBottom: '3rem' }}>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '1.5rem', flexWrap: 'wrap', gap: '1rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<div style={{ width: '2.5rem', height: '2.5rem', borderRadius: 'var(--radius)', background: 'rgba(244,114,182,0.2)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#f472b6' }}>
<Heart size={20} />
</div>
<div>
<h1 style={{ fontSize: '1.75rem', margin: 0 }}>Heat Cycle Calendar</h1>
<p style={{ color: 'var(--text-muted)', margin: 0, fontSize: '0.875rem' }}>Track heat cycles and optimal breeding windows</p>
</div>
</div>
<button className="btn btn-primary" onClick={() => setShowStartModal(true)}>
<Plus size={16} /> Start Heat Cycle
</button>
</div>
{/* Legend */}
<div style={{ display: 'flex', gap: '0.75rem', marginBottom: '1.25rem', flexWrap: 'wrap' }}>
{Object.entries(WINDOW_STYLES).map(([key, s]) => (
<div key={key} style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', fontSize: '0.8125rem', color: 'var(--text-secondary)' }}>
<div style={{ width: 10, height: 10, borderRadius: '50%', background: s.dot }} />
{s.label}
</div>
))}
</div>
{/* Month navigator */}
<div className="card" style={{ marginBottom: '1rem', padding: '0' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0.875rem 1rem', borderBottom: '1px solid var(--border)' }}>
<button className="btn-icon" onClick={prevMonth}><ChevronLeft size={20} /></button>
<h2 style={{ margin: 0, fontSize: '1.1rem' }}>{MONTH_NAMES[month]} {year}</h2>
<button className="btn-icon" onClick={nextMonth}><ChevronRight size={20} /></button>
</div>
{/* Day headers */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', borderBottom: '1px solid var(--border)' }}>
{DAY_NAMES.map(d => (
<div key={d} style={{ padding: '0.5rem', textAlign: 'center', fontSize: '0.75rem', fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>{d}</div>
))}
</div>
{/* Calendar cells */}
{loading ? (
<div className="loading" style={{ minHeight: 280 }}>Loading calendar</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)' }}>
{Array.from({ length: rows * 7 }).map((_, idx) => {
const dayNum = idx - startPad + 1
const isValid = dayNum >= 1 && dayNum <= lastDay.getDate()
const dateStr = isValid ? toISO(new Date(year, month, dayNum)) : null
const dayCycles = dateStr ? cyclesForDate(dateStr) : []
const isToday = dateStr === today
let cellBg = 'transparent'
let cellBorder = 'var(--border)'
if (dayCycles.length > 0) {
const win = getWindowForDate(dayCycles[0], dateStr)
if (win && WINDOW_STYLES[win]) {
cellBg = WINDOW_STYLES[win].bg
cellBorder = WINDOW_STYLES[win].border
}
}
return (
<div
key={idx}
onClick={() => isValid && handleDayClick(dateStr, dayCycles)}
style={{
minHeight: 72,
padding: '0.375rem 0.5rem',
borderRight: '1px solid var(--border)',
borderBottom: '1px solid var(--border)',
background: cellBg,
cursor: isValid ? 'pointer' : 'default',
position: 'relative',
transition: 'filter 0.15s',
opacity: isValid ? 1 : 0.3,
outline: isToday ? `2px solid var(--primary)` : 'none',
outlineOffset: -2,
}}
onMouseEnter={e => { if (isValid) e.currentTarget.style.filter = 'brightness(1.15)' }}
onMouseLeave={e => { e.currentTarget.style.filter = 'none' }}
>
{isValid && (
<>
<div style={{
fontSize: '0.8125rem', fontWeight: isToday ? 700 : 500,
color: isToday ? 'var(--primary)' : 'var(--text-primary)',
marginBottom: '0.25rem'
}}>{dayNum}</div>
{dayCycles.map((c, i) => {
const win = getWindowForDate(c, dateStr)
const dot = win ? WINDOW_STYLES[win]?.dot : '#94a3b8'
return (
<div key={i} style={{
fontSize: '0.7rem', color: dot, fontWeight: 600,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
lineHeight: 1.3
}}>
{c.dog_name}
</div>
)
})}
{/* Breeding date marker */}
{dayCycles.some(c => c.breeding_date === dateStr) && (
<div style={{ position: 'absolute', top: 4, right: 4, width: 8, height: 8, borderRadius: '50%', background: 'var(--success)', border: '1.5px solid var(--bg-primary)' }} title="Breeding date logged" />
)}
</>
)}
</div>
)
})}
</div>
)}
</div>
{/* Active cycles list */}
<div style={{ marginTop: '1.5rem' }}>
<h3 style={{ fontSize: '1rem', marginBottom: '0.875rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<AlertCircle size={16} style={{ color: '#f472b6' }} />
Active Cycles This Month
<span className="badge badge-primary">{activeCycles.length}</span>
</h3>
{activeCycles.length === 0 ? (
<div className="card" style={{ textAlign: 'center', padding: '2rem', color: 'var(--text-muted)' }}>
<Heart size={32} style={{ margin: '0 auto 0.75rem', opacity: 0.4 }} />
<p>No active heat cycles this month.</p>
<button className="btn btn-primary" style={{ marginTop: '1rem' }} onClick={() => setShowStartModal(true)}>
<Plus size={15} /> Start First Cycle
</button>
</div>
) : (
<div style={{ display: 'grid', gap: '0.75rem', gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))' }}>
{activeCycles.map(c => {
const win = getWindowForDate(c, today)
const ws = win ? WINDOW_STYLES[win] : null
const daysSince = Math.round((new Date(today) - new Date(c.start_date + 'T00:00:00')) / 86400000)
return (
<div
key={c.id}
className="card"
style={{ cursor: 'pointer', borderColor: ws?.border || 'var(--border)', background: ws?.bg || 'var(--bg-secondary)' }}
onClick={() => setSelectedCycle(c)}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<h4 style={{ margin: '0 0 0.2rem', fontSize: '1rem' }}>{c.dog_name}</h4>
{c.breed && <p style={{ color: 'var(--text-muted)', fontSize: '0.8rem', margin: 0 }}>{c.breed}</p>}
</div>
{ws && <span className="badge" style={{ background: ws.bg, color: ws.dot, border: `1px solid ${ws.border}`, flexShrink: 0 }}>{ws.label}</span>}
</div>
<div style={{ marginTop: '0.75rem', display: 'flex', gap: '1rem', fontSize: '0.8125rem', color: 'var(--text-secondary)' }}>
<span>Started {fmt(c.start_date)}</span>
<span>Day {daysSince + 1}</span>
</div>
{c.breeding_date && (
<div style={{ marginTop: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.4rem', fontSize: '0.8rem', color: 'var(--success)' }}>
<CheckCircle2 size={13} /> Bred {fmt(c.breeding_date)}
</div>
)}
</div>
)
})}
</div>
)}
</div>
{/* Modals */}
{showStartModal && (
<StartCycleModal
females={females}
onClose={() => setShowStartModal(false)}
onSaved={() => { setShowStartModal(false); load() }}
/>
)}
{selectedCycle && (
<CycleDetailModal
cycle={selectedCycle}
onClose={() => setSelectedCycle(null)}
onDeleted={() => { setSelectedCycle(null); load() }}
onRecordLitter={handleRecordLitter}
/>
)}
</div>
)
}