diff --git a/client/src/pages/LitterDetail.jsx b/client/src/pages/LitterDetail.jsx
index ee58a88..0023c01 100644
--- a/client/src/pages/LitterDetail.jsx
+++ b/client/src/pages/LitterDetail.jsx
@@ -1,9 +1,254 @@
import { useEffect, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
-import { ArrowLeft, Plus, X, ExternalLink, Dog } from 'lucide-react'
+import { ArrowLeft, Plus, X, ExternalLink, Dog, Weight, ChevronDown, ChevronUp, Trash2 } from 'lucide-react'
import axios from 'axios'
import LitterForm from '../components/LitterForm'
+// ─── Puppy Log Panel ────────────────────────────────────────────────────────────
+function PuppyLogPanel({ litterId, puppy, whelpingDate }) {
+ const [open, setOpen] = useState(false)
+ const [logs, setLogs] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [showAdd, setShowAdd] = useState(false)
+ const [form, setForm] = useState({
+ record_date: whelpingDate || '',
+ weight_oz: '',
+ weight_lbs: '',
+ notes: '',
+ record_type: 'weight_log'
+ })
+ const [saving, setSaving] = useState(false)
+
+ useEffect(() => { if (open) fetchLogs() }, [open])
+
+ const fetchLogs = async () => {
+ setLoading(true)
+ try {
+ const res = await axios.get(`/api/litters/${litterId}/puppies/${puppy.id}/logs`)
+ const parsed = res.data.map(l => {
+ try { return { ...l, _data: JSON.parse(l.description) } } catch { return { ...l, _data: {} } }
+ })
+ setLogs(parsed)
+ } catch (e) { console.error(e) }
+ finally { setLoading(false) }
+ }
+
+ const handleAdd = async () => {
+ if (!form.record_date) return
+ setSaving(true)
+ try {
+ await axios.post(`/api/litters/${litterId}/puppies/${puppy.id}/logs`, form)
+ setShowAdd(false)
+ setForm(f => ({ ...f, weight_oz: '', weight_lbs: '', notes: '' }))
+ fetchLogs()
+ } catch (e) { console.error(e) }
+ finally { setSaving(false) }
+ }
+
+ const handleDelete = async (logId) => {
+ if (!window.confirm('Delete this log entry?')) return
+ try {
+ await axios.delete(`/api/litters/${litterId}/puppies/${puppy.id}/logs/${logId}`)
+ fetchLogs()
+ } catch (e) { console.error(e) }
+ }
+
+ const TYPES = [
+ { value: 'weight_log', label: '⚖️ Weight Check' },
+ { value: 'health_note', label: '📝 Health Note' },
+ { value: 'deworming', label: '🐛 Deworming' },
+ { value: 'vaccination', label: '💉 Vaccination' },
+ ]
+
+ return (
+
+
+
+ {open && (
+
+ {loading ? (
+
Loading...
+ ) : logs.length === 0 ? (
+
No logs yet.
+ ) : (
+
+ {logs.map(l => (
+
+
+
+ {new Date(l.record_date + 'T00:00:00').toLocaleDateString()}
+
+ {' • '}
+
+ {TYPES.find(t => t.value === l.record_type)?.label || l.record_type}
+
+ {l._data?.weight_oz &&
— {l._data.weight_oz} oz}
+ {l._data?.weight_lbs &&
({l._data.weight_lbs} lbs)}
+ {l._data?.notes && (
+
+ {l._data.notes}
+
+ )}
+
+
+
+ ))}
+
+ )}
+
+ {showAdd ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ )
+}
+
+// ─── Whelping Window Banner ───────────────────────────────────────────────
+function addDays(dateStr, n) {
+ const d = new Date(dateStr + 'T00:00:00')
+ d.setDate(d.getDate() + n)
+ return d
+}
+function fmt(d) { return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) }
+
+function WhelpingBanner({ breedingDate, whelpingDate }) {
+ if (whelpingDate) return null // already whelped, no need for estimate
+ if (!breedingDate) return null
+
+ const earliest = addDays(breedingDate, 58)
+ const expected = addDays(breedingDate, 63)
+ const latest = addDays(breedingDate, 68)
+ const today = new Date()
+ const daysUntil = Math.ceil((expected - today) / 86400000)
+
+ let urgency = 'var(--success)'
+ let urgencyBg = 'rgba(16,185,129,0.06)'
+ let statusLabel = `~${daysUntil} days away`
+ if (daysUntil <= 7 && daysUntil > 0) {
+ urgency = '#d97706'; urgencyBg = 'rgba(217,119,6,0.08)'
+ statusLabel = `⚠️ ${daysUntil} days — prepare whelping area!`
+ } else if (daysUntil <= 0) {
+ urgency = '#e53e3e'; urgencyBg = 'rgba(229,62,62,0.08)'
+ statusLabel = '🔴 Expected date has passed — confirm or update whelping date'
+ }
+
+ return (
+
+
+ 💕 Projected Whelping Window
+
+ {statusLabel}
+
+
+
+
+ Earliest (Day 58)
+
{fmt(earliest)}
+
+
+ Expected (Day 63)
+
{fmt(expected)}
+
+
+ Latest (Day 68)
+
{fmt(latest)}
+
+
+
+ )
+}
+
+// ─── Main LitterDetail ─────────────────────────────────────────────────────────
function LitterDetail() {
const { id } = useParams()
const navigate = useNavigate()
@@ -14,14 +259,11 @@ function LitterDetail() {
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 [addMode, setAddMode] = useState('existing')
const [error, setError] = useState('')
const [saving, setSaving] = useState(false)
- useEffect(() => {
- fetchLitter()
- fetchAllDogs()
- }, [id])
+ useEffect(() => { fetchLitter(); fetchAllDogs() }, [id])
const fetchLitter = async () => {
try {
@@ -38,9 +280,7 @@ function LitterDetail() {
try {
const res = await axios.get('/api/dogs')
setAllDogs(res.data)
- } catch (err) {
- console.error('Error fetching dogs:', err)
- }
+ } catch (err) { console.error('Error fetching dogs:', err) }
}
const unlinkedDogs = allDogs.filter(d => {
@@ -52,47 +292,31 @@ function LitterDetail() {
const handleLinkPuppy = async () => {
if (!selectedPuppyId) return
- setSaving(true)
- setError('')
+ setSaving(true); setError('')
try {
await axios.post(`/api/litters/${id}/puppies/${selectedPuppyId}`)
- setSelectedPuppyId('')
- setShowAddPuppy(false)
- fetchLitter()
+ setSelectedPuppyId(''); setShowAddPuppy(false); fetchLitter()
} catch (err) {
setError(err.response?.data?.error || 'Failed to link puppy')
- } finally {
- setSaving(false)
- }
+ } finally { setSaving(false) }
}
const handleCreateAndLink = async () => {
- if (!newPuppy.name) {
- setError('Puppy name is required')
- return
- }
- setSaving(true)
- setError('')
+ 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,
+ 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}`)
+ await axios.post(`/api/litters/${id}/puppies/${res.data.id}`)
setNewPuppy({ name: '', sex: 'male', color: '', dob: '' })
- setShowAddPuppy(false)
- fetchLitter()
- fetchAllDogs()
+ setShowAddPuppy(false); fetchLitter(); fetchAllDogs()
} catch (err) {
setError(err.response?.data?.error || 'Failed to create puppy')
- } finally {
- setSaving(false)
- }
+ } finally { setSaving(false) }
}
const handleUnlinkPuppy = async (puppyId) => {
@@ -100,9 +324,7 @@ function LitterDetail() {
try {
await axios.delete(`/api/litters/${id}/puppies/${puppyId}`)
fetchLitter()
- } catch (err) {
- console.error('Error unlinking puppy:', err)
- }
+ } catch (err) { console.error('Error unlinking puppy:', err) }
}
if (loading) return Loading litter...
@@ -120,13 +342,11 @@ function LitterDetail() {
🐾 {litter.sire_name} × {litter.dam_name}
- Bred: {new Date(litter.breeding_date).toLocaleDateString()}
- {litter.whelping_date && ` • Whelped: ${new Date(litter.whelping_date).toLocaleDateString()}`}
+ Bred: {new Date(litter.breeding_date + 'T00:00:00').toLocaleDateString()}
+ {litter.whelping_date && ` • Whelped: ${new Date(litter.whelping_date + 'T00:00:00').toLocaleDateString()}`}
-
+
{/* Stats row */}
@@ -155,6 +375,9 @@ function LitterDetail() {
)}
+ {/* Projected whelping window */}
+
+
{/* Notes */}
{litter.notes && (
@@ -177,7 +400,7 @@ function LitterDetail() {
No puppies linked yet. Add puppies to this litter.
) : (
-
+
{litter.puppies.map(puppy => (
+
+ {/* Weight / Health Log collapsible */}
+
))}
@@ -223,37 +453,22 @@ function LitterDetail() {
{error &&
{error}
}
-
- {/* Mode toggle */}
- setAddMode('existing')}
- style={{ flex: 1 }}
- >
- Link Existing Dog
-
- setAddMode('new')}
- style={{ flex: 1 }}
- >
- Create New Puppy
-
+ setAddMode('existing')} style={{ flex: 1 }}>Link Existing Dog
+ setAddMode('new')} style={{ flex: 1 }}>Create New Puppy
{addMode === 'existing' ? (
-