From 0e8b875a4cbf3282e4aad5a1fd82fdf734af0653 Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 9 Mar 2026 20:49:34 -0500 Subject: [PATCH 1/6] feat: rebuild LitterList with create/edit/delete and LitterForm integration --- client/src/pages/LitterList.jsx | 121 +++++++++++++++++++++++++++----- 1 file changed, 103 insertions(+), 18 deletions(-) diff --git a/client/src/pages/LitterList.jsx b/client/src/pages/LitterList.jsx index a3720d4..fc056f1 100644 --- a/client/src/pages/LitterList.jsx +++ b/client/src/pages/LitterList.jsx @@ -1,10 +1,16 @@ 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() @@ -14,49 +20,128 @@ function LitterList() { 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
Loading litters...
} return (
-

Litters

+
+

Litters

+ +
{litters.length === 0 ? (

No litters recorded yet

-

Start tracking breeding records

+

Create a litter after a breeding cycle to track puppies

+
) : (
{litters.map(litter => ( -
-

{litter.sire_name} ร— {litter.dam_name}

-

- Breeding Date: {new Date(litter.breeding_date).toLocaleDateString()} -

- {litter.whelping_date && ( -

- Whelping Date: {new Date(litter.whelping_date).toLocaleDateString()} -

- )} -

- Puppies: {litter.puppy_count || litter.puppies?.length || 0} -

+
navigate(`/litters/${litter.id}`)} + > +
+
+

+ ๐Ÿพ {litter.sire_name} ร— {litter.dam_name} +

+
+ ๐Ÿ“… Bred: {new Date(litter.breeding_date).toLocaleDateString()} + {litter.whelping_date && ( + ๐Ÿ• Whelped: {new Date(litter.whelping_date).toLocaleDateString()} + )} + + {litter.actual_puppy_count ?? litter.puppies?.length ?? litter.puppy_count ?? 0} puppies + +
+ {litter.notes && ( +

+ {litter.notes} +

+ )} +
+
+ + + +
+
))}
)} + + {showForm && ( + setShowForm(false)} + onSave={handleSave} + /> + )}
) } -export default LitterList \ No newline at end of file +export default LitterList From 15aa8713332847d94cee4b729488e7c38121df48 Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 9 Mar 2026 20:50:25 -0500 Subject: [PATCH 2/6] feat: add LitterDetail page with puppy roster and add/remove/create puppy --- client/src/pages/LitterDetail.jsx | 344 ++++++++++++++++++++++++++++++ 1 file changed, 344 insertions(+) create mode 100644 client/src/pages/LitterDetail.jsx diff --git a/client/src/pages/LitterDetail.jsx b/client/src/pages/LitterDetail.jsx new file mode 100644 index 0000000..ee58a88 --- /dev/null +++ b/client/src/pages/LitterDetail.jsx @@ -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
Loading litter...
+ if (!litter) return

Litter not found.

+ + const puppyCount = litter.puppies?.length ?? 0 + + return ( +
+ {/* Header */} +
+ +
+

๐Ÿพ {litter.sire_name} ร— {litter.dam_name}

+

+ Bred: {new Date(litter.breeding_date).toLocaleDateString()} + {litter.whelping_date && ` โ€ข Whelped: ${new Date(litter.whelping_date).toLocaleDateString()}`} +

+
+ +
+ + {/* Stats row */} +
+
+
{puppyCount}
+
Puppies Linked
+
+
+
+ {litter.puppies?.filter(p => p.sex === 'male').length ?? 0} +
+
Males
+
+
+
+ {litter.puppies?.filter(p => p.sex === 'female').length ?? 0} +
+
Females
+
+ {litter.puppy_count > 0 && ( +
+
{litter.puppy_count}
+
Expected
+
+ )} +
+ + {/* Notes */} + {litter.notes && ( +
+

{litter.notes}

+
+ )} + + {/* Puppies section */} +
+

Puppies

+ +
+ + {puppyCount === 0 ? ( +
+ +

No puppies linked yet. Add puppies to this litter.

+
+ ) : ( +
+ {litter.puppies.map(puppy => ( +
+ +
+ {puppy.sex === 'male' ? '๐Ÿฆ' : '๐Ÿฅ'} +
+
{puppy.name}
+
+ {puppy.sex} {puppy.color && `โ€ข ${puppy.color}`} +
+ {puppy.date_of_birth && ( +
+ Born: {new Date(puppy.date_of_birth).toLocaleDateString()} +
+ )} + +
+ ))} +
+ )} + + {/* Add Puppy Modal */} + {showAddPuppy && ( +
setShowAddPuppy(false)}> +
e.stopPropagation()} style={{ maxWidth: '480px' }}> +
+

Add Puppy to Litter

+ +
+
+ {error &&
{error}
} + + {/* Mode toggle */} +
+ + +
+ + {addMode === 'existing' ? ( +
+ + + {unlinkedDogs.length === 0 && ( +

+ No unlinked dogs available. Use "Create New Puppy" instead. +

+ )} +
+ ) : ( +
+
+ + setNewPuppy(p => ({ ...p, name: e.target.value }))} + placeholder="e.g. Blue Collar" + /> +
+
+
+ + +
+
+ + setNewPuppy(p => ({ ...p, color: e.target.value }))} + placeholder="e.g. Black & Tan" + /> +
+
+
+ + setNewPuppy(p => ({ ...p, dob: e.target.value }))} + /> + {litter.whelping_date && !newPuppy.dob && ( +

+ Will default to whelping date: {new Date(litter.whelping_date).toLocaleDateString()} +

+ )} +
+
+ )} +
+
+ + +
+
+
+ )} + + {/* Edit Litter Modal */} + {showEditForm && ( + setShowEditForm(false)} + onSave={fetchLitter} + /> + )} +
+ ) +} + +export default LitterDetail From 49d28515324d180f40e09d7655b95ab27119bab6 Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 9 Mar 2026 20:50:42 -0500 Subject: [PATCH 3/6] feat: add /litters/:id route for LitterDetail page --- client/src/App.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/App.jsx b/client/src/App.jsx index 42b5d3f..bddf6bc 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -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() { } /> } /> } /> + } /> } /> } /> From 7a6b77099904b5832a71e3b08b3ac5f797ccf18b Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 9 Mar 2026 20:52:57 -0500 Subject: [PATCH 4/6] feat: add Record Litter CTA in CycleDetailModal when breeding date is logged --- client/src/pages/BreedingCalendar.jsx | 72 +++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/client/src/pages/BreedingCalendar.jsx b/client/src/pages/BreedingCalendar.jsx index ccd33c0..e413033 100644 --- a/client/src/pages/BreedingCalendar.jsx +++ b/client/src/pages/BreedingCalendar.jsx @@ -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 }) {
+ {prefill?.dam_name && !litter && ( +

+ โœ“ Pre-selected: {prefill.dam_name} +

+ )}