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 { 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user