feat: add Record Litter CTA in CycleDetailModal when breeding date is logged

This commit is contained in:
2026-03-09 20:52:57 -05:00
parent 49d2851532
commit 7a6b770999

View File

@@ -1,15 +1,17 @@
import { useEffect, useState, useCallback } from 'react' import { useEffect, useState, useCallback } from 'react'
import { import {
Heart, ChevronLeft, ChevronRight, Plus, X, Heart, ChevronLeft, ChevronRight, Plus, X,
CalendarDays, FlaskConical, Baby, AlertCircle, CheckCircle2 CalendarDays, FlaskConical, Baby, AlertCircle, CheckCircle2, Activity
} from 'lucide-react' } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import axios from 'axios'
// ─── Date helpers ──────────────────────────────────────────────────────────── // ─── Date helpers ────────────────────────────────────────────────────────────
const toISO = d => d.toISOString().split('T')[0] const toISO = d => d.toISOString().split('T')[0]
const addDays = (dateStr, n) => { const addDays = (dateStr, n) => {
const d = new Date(dateStr); d.setDate(d.getDate() + n); return toISO(d) 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()) const today = toISO(new Date())
// ─── Cycle window classifier ───────────────────────────────────────────────── // ─── Cycle window classifier ─────────────────────────────────────────────────
@@ -74,7 +76,7 @@ function StartCycleModal({ females, onClose, onSaved }) {
<div className="form-group"> <div className="form-group">
<label className="label">Female Dog *</label> <label className="label">Female Dog *</label>
<select value={dogId} onChange={e => setDogId(e.target.value)} required> <select value={dogId} onChange={e => setDogId(e.target.value)} required>
<option value=""> Select Female </option> <option value=""> Select Female </option>
{females.map(d => ( {females.map(d => (
<option key={d.id} value={d.id}> <option key={d.id} value={d.id}>
{d.name}{d.breed ? ` · ${d.breed}` : ''} {d.name}{d.breed ? ` · ${d.breed}` : ''}
@@ -105,7 +107,7 @@ function StartCycleModal({ females, onClose, onSaved }) {
} }
// ─── Cycle Detail Modal ─────────────────────────────────────────────────────── // ─── Cycle Detail Modal ───────────────────────────────────────────────────────
function CycleDetailModal({ cycle, onClose, onDeleted }) { function CycleDetailModal({ cycle, onClose, onDeleted, onRecordLitter }) {
const [suggestions, setSuggestions] = useState(null) const [suggestions, setSuggestions] = useState(null)
const [breedingDate, setBreedingDate] = useState(cycle.breeding_date || '') const [breedingDate, setBreedingDate] = useState(cycle.breeding_date || '')
const [savingBreed, setSavingBreed] = useState(false) const [savingBreed, setSavingBreed] = useState(false)
@@ -146,6 +148,7 @@ function CycleDetailModal({ cycle, onClose, onDeleted }) {
} }
const whelp = suggestions?.whelping const whelp = suggestions?.whelping
const hasBreedingDate = !!(breedingDate && breedingDate === cycle.breeding_date)
return ( return (
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}> <div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
@@ -221,7 +224,7 @@ function CycleDetailModal({ cycle, onClose, onDeleted }) {
{/* Whelping estimate */} {/* Whelping estimate */}
{whelp && ( {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)' }}> <h3 style={{ fontSize: '0.9375rem', marginBottom: '0.75rem', display: 'flex', alignItems: 'center', gap: '0.4rem', color: 'var(--success)' }}>
<Baby size={16} /> Whelping Estimate <Baby size={16} /> Whelping Estimate
</h3> </h3>
@@ -235,6 +238,39 @@ function CycleDetailModal({ cycle, onClose, onDeleted }) {
</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>
<div className="modal-footer" style={{ justifyContent: 'space-between' }}> <div className="modal-footer" style={{ justifyContent: 'space-between' }}>
<button className="btn btn-danger" onClick={deleteCycle} disabled={deleting}> <button className="btn btn-danger" onClick={deleteCycle} disabled={deleting}>
@@ -265,6 +301,8 @@ export default function BreedingCalendar() {
const [showStartModal, setShowStartModal] = useState(false) const [showStartModal, setShowStartModal] = useState(false)
const [selectedCycle, setSelectedCycle] = useState(null) const [selectedCycle, setSelectedCycle] = useState(null)
const [selectedDay, setSelectedDay] = useState(null) const [selectedDay, setSelectedDay] = useState(null)
const [pendingLitterCycle, setPendingLitterCycle] = useState(null)
const navigate = useNavigate()
const load = useCallback(async () => { const load = useCallback(async () => {
setLoading(true) setLoading(true)
@@ -287,6 +325,23 @@ export default function BreedingCalendar() {
useEffect(() => { load() }, [load]) 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 ── // ── Build calendar grid ──
const firstDay = new Date(year, month, 1) const firstDay = new Date(year, month, 1)
const lastDay = new Date(year, month + 1, 0) const lastDay = new Date(year, month + 1, 0)
@@ -306,7 +361,6 @@ export default function BreedingCalendar() {
else setMonth(m => m + 1) else setMonth(m => m + 1)
} }
// Find cycles that overlap a given date
function cyclesForDate(dateStr) { function cyclesForDate(dateStr) {
return cycles.filter(c => { return cycles.filter(c => {
const s = c.start_date const s = c.start_date
@@ -321,16 +375,12 @@ export default function BreedingCalendar() {
if (dayCycles.length === 1) { if (dayCycles.length === 1) {
setSelectedCycle(dayCycles[0]) setSelectedCycle(dayCycles[0])
} else if (dayCycles.length > 1) { } else if (dayCycles.length > 1) {
// show first — could be upgraded to a picker
setSelectedCycle(dayCycles[0]) setSelectedCycle(dayCycles[0])
} else { } 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) setShowStartModal(true)
} }
} }
// Active cycles (in current month or ongoing)
const activeCycles = cycles.filter(c => { const activeCycles = cycles.filter(c => {
const s = c.start_date; if (!s) return false const s = c.start_date; if (!s) return false
const end = c.end_date || addDays(s, 28) const end = c.end_date || addDays(s, 28)
@@ -394,7 +444,6 @@ export default function BreedingCalendar() {
const dayCycles = dateStr ? cyclesForDate(dateStr) : [] const dayCycles = dateStr ? cyclesForDate(dateStr) : []
const isToday = dateStr === today const isToday = dateStr === today
// Pick dominant window color for background
let cellBg = 'transparent' let cellBg = 'transparent'
let cellBorder = 'var(--border)' let cellBorder = 'var(--border)'
if (dayCycles.length > 0) { if (dayCycles.length > 0) {
@@ -522,6 +571,7 @@ export default function BreedingCalendar() {
cycle={selectedCycle} cycle={selectedCycle}
onClose={() => setSelectedCycle(null)} onClose={() => setSelectedCycle(null)}
onDeleted={() => { setSelectedCycle(null); load() }} onDeleted={() => { setSelectedCycle(null); load() }}
onRecordLitter={handleRecordLitter}
/> />
)} )}
</div> </div>