feat: add Record Litter CTA in CycleDetailModal when breeding date is logged
This commit is contained in:
@@ -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 }) {
|
||||
<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>
|
||||
<option value="">– Select Female –</option>
|
||||
{females.map(d => (
|
||||
<option key={d.id} value={d.id}>
|
||||
{d.name}{d.breed ? ` · ${d.breed}` : ''}
|
||||
@@ -105,7 +107,7 @@ function StartCycleModal({ females, onClose, onSaved }) {
|
||||
}
|
||||
|
||||
// ─── Cycle Detail Modal ───────────────────────────────────────────────────────
|
||||
function CycleDetailModal({ cycle, onClose, onDeleted }) {
|
||||
function CycleDetailModal({ cycle, onClose, onDeleted, onRecordLitter }) {
|
||||
const [suggestions, setSuggestions] = useState(null)
|
||||
const [breedingDate, setBreedingDate] = useState(cycle.breeding_date || '')
|
||||
const [savingBreed, setSavingBreed] = useState(false)
|
||||
@@ -146,6 +148,7 @@ function CycleDetailModal({ cycle, onClose, onDeleted }) {
|
||||
}
|
||||
|
||||
const whelp = suggestions?.whelping
|
||||
const hasBreedingDate = !!(breedingDate && breedingDate === cycle.breeding_date)
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
|
||||
@@ -221,7 +224,7 @@ function CycleDetailModal({ cycle, onClose, onDeleted }) {
|
||||
|
||||
{/* 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' }}>
|
||||
<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>
|
||||
@@ -235,6 +238,39 @@ function CycleDetailModal({ cycle, onClose, onDeleted }) {
|
||||
</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}>
|
||||
@@ -265,6 +301,8 @@ export default function BreedingCalendar() {
|
||||
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)
|
||||
@@ -287,6 +325,23 @@ export default function BreedingCalendar() {
|
||||
|
||||
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)
|
||||
@@ -306,7 +361,6 @@ export default function BreedingCalendar() {
|
||||
else setMonth(m => m + 1)
|
||||
}
|
||||
|
||||
// Find cycles that overlap a given date
|
||||
function cyclesForDate(dateStr) {
|
||||
return cycles.filter(c => {
|
||||
const s = c.start_date
|
||||
@@ -321,16 +375,12 @@ export default function BreedingCalendar() {
|
||||
if (dayCycles.length === 1) {
|
||||
setSelectedCycle(dayCycles[0])
|
||||
} else if (dayCycles.length > 1) {
|
||||
// show first — could be upgraded to a picker
|
||||
setSelectedCycle(dayCycles[0])
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
// Active cycles (in current month or ongoing)
|
||||
const activeCycles = cycles.filter(c => {
|
||||
const s = c.start_date; if (!s) return false
|
||||
const end = c.end_date || addDays(s, 28)
|
||||
@@ -394,7 +444,6 @@ export default function BreedingCalendar() {
|
||||
const dayCycles = dateStr ? cyclesForDate(dateStr) : []
|
||||
const isToday = dateStr === today
|
||||
|
||||
// Pick dominant window color for background
|
||||
let cellBg = 'transparent'
|
||||
let cellBorder = 'var(--border)'
|
||||
if (dayCycles.length > 0) {
|
||||
@@ -522,6 +571,7 @@ export default function BreedingCalendar() {
|
||||
cycle={selectedCycle}
|
||||
onClose={() => setSelectedCycle(null)}
|
||||
onDeleted={() => { setSelectedCycle(null); load() }}
|
||||
onRecordLitter={handleRecordLitter}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user