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() { } /> } /> } /> + } /> } /> } /> diff --git a/client/src/components/LitterForm.jsx b/client/src/components/LitterForm.jsx index a86430a..ca921bd 100644 --- a/client/src/components/LitterForm.jsx +++ b/client/src/components/LitterForm.jsx @@ -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 }) {
e.stopPropagation()}>
-

{litter ? 'Edit Litter' : 'Create New Litter'}

+

{litter ? 'Edit Litter' : prefill ? `Record Litter โ€” ${prefill.dam_name || 'Dam pre-selected'}` : 'Create New Litter'}

@@ -78,6 +86,20 @@ function LitterForm({ litter, onClose, onSave }) {
{error &&
{error}
} + {prefill && !litter && ( +
+ ๐Ÿพ Pre-filled from heat cycle โ€” select a sire to complete the litter record. +
+ )} +
@@ -111,6 +133,11 @@ function LitterForm({ litter, onClose, onSave }) { ))} + {prefill?.dam_name && !litter && ( +

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

+ )}
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 }) {
setSelectedPuppyId(e.target.value)} + > + + {unlinkedDogs.map(d => ( + + ))} + + {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 diff --git a/client/src/pages/LitterList.jsx b/client/src/pages/LitterList.jsx index a3720d4..5d3c709 100644 --- a/client/src/pages/LitterList.jsx +++ b/client/src/pages/LitterList.jsx @@ -1,62 +1,158 @@ 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
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