2026-03-09 20:32:21 -05:00
import { useEffect , useState , useCallback } from 'react'
import {
Heart , ChevronLeft , ChevronRight , Plus , X ,
2026-03-09 20:52:57 -05:00
CalendarDays , FlaskConical , Baby , AlertCircle , CheckCircle2 , Activity
2026-03-09 20:32:21 -05:00
} from 'lucide-react'
2026-03-09 20:52:57 -05:00
import { useNavigate } from 'react-router-dom'
import axios from 'axios'
2026-03-08 22:54:36 -05:00
2026-03-09 20:32:21 -05:00
// ─── 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 )
}
2026-03-09 20:52:57 -05:00
const fmt = str => str ? new Date ( str + 'T00:00:00' ) . toLocaleDateString ( 'en-US' , { month : 'short' , day : 'numeric' , year : 'numeric' } ) : '– '
2026-03-09 20:32:21 -05:00
const today = toISO ( new Date ( ) )
// ─── Cycle window classifier ─────────────────────────────────────────────────
function getWindowForDate ( cycle , dateStr ) {
if ( ! cycle ? . start _date ) return null
const start = new Date ( cycle . start _date + 'T00:00:00' )
const check = new Date ( dateStr + 'T00:00:00' )
const day = Math . round ( ( check - start ) / 86400000 )
if ( day < 0 || day > 28 ) return null
if ( day <= 8 ) return 'proestrus'
if ( day <= 15 ) return 'optimal'
if ( day <= 21 ) return 'late'
return 'diestrus'
}
const WINDOW _STYLES = {
proestrus : { bg : 'rgba(244,114,182,0.18)' , border : '#f472b6' , label : 'Proestrus' , dot : '#f472b6' } ,
optimal : { bg : 'rgba(16,185,129,0.22)' , border : '#10b981' , label : 'Optimal Breeding' , dot : '#10b981' } ,
late : { bg : 'rgba(245,158,11,0.18)' , border : '#f59e0b' , label : 'Late Estrus' , dot : '#f59e0b' } ,
diestrus : { bg : 'rgba(148,163,184,0.12)' , border : '#64748b' , label : 'Diestrus' , dot : '#64748b' } ,
}
// ─── Start Heat Cycle Modal ───────────────────────────────────────────────────
function StartCycleModal ( { females , onClose , onSaved } ) {
const [ dogId , setDogId ] = useState ( '' )
const [ startDate , setStartDate ] = useState ( today )
const [ notes , setNotes ] = useState ( '' )
const [ saving , setSaving ] = useState ( false )
const [ error , setError ] = useState ( null )
async function handleSubmit ( e ) {
e . preventDefault ( )
if ( ! dogId || ! startDate ) return
setSaving ( true ) ; setError ( null )
try {
const res = await fetch ( '/api/breeding/heat-cycles' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { dog _id : parseInt ( dogId ) , start _date : startDate , notes : notes || null } )
} )
if ( ! res . ok ) { const e = await res . json ( ) ; throw new Error ( e . error || 'Failed to save' ) }
onSaved ( )
} catch ( err ) {
setError ( err . message )
setSaving ( false )
}
}
return (
< div className = "modal-overlay" onClick = { e => e . target === e . currentTarget && onClose ( ) } >
< div className = "modal-content" style = { { maxWidth : '480px' } } >
< div className = "modal-header" >
< div style = { { display : 'flex' , alignItems : 'center' , gap : '0.6rem' } } >
< Heart size = { 18 } style = { { color : '#f472b6' } } / >
< h2 > Start Heat Cycle < / h2 >
< / div >
< button className = "btn-icon" onClick = { onClose } > < X size = { 20 } / > < / button >
< / div >
< form onSubmit = { handleSubmit } >
< div className = "modal-body" >
{ error && < div className = "error" style = { { marginBottom : '1rem' } } > { error } < / div > }
< div className = "form-group" >
< label className = "label" > Female Dog * < / label >
< select value = { dogId } onChange = { e => setDogId ( e . target . value ) } required >
2026-03-09 20:52:57 -05:00
< option value = "" > – Select Female – < / option >
2026-03-09 20:32:21 -05:00
{ females . map ( d => (
< option key = { d . id } value = { d . id } >
{ d . name } { d . breed ? ` · ${ d . breed } ` : '' }
< / option >
) ) }
< / select >
{ females . length === 0 && < p style = { { color : 'var(--text-muted)' , fontSize : '0.8rem' , marginTop : '0.4rem' } } > No female dogs registered . < / p > }
< / div >
< div className = "form-group" >
< label className = "label" > Heat Start Date * < / label >
< input type = "date" className = "input" value = { startDate } onChange = { e => setStartDate ( e . target . value ) } required / >
< / div >
< div className = "form-group" style = { { marginBottom : 0 } } >
< label className = "label" > Notes < / label >
< textarea className = "input" value = { notes } onChange = { e => setNotes ( e . target . value ) } placeholder = "Optional notes..." rows = { 3 } / >
< / div >
< / div >
< div className = "modal-footer" >
< button type = "button" className = "btn btn-secondary" onClick = { onClose } > Cancel < / button >
< button type = "submit" className = "btn btn-primary" disabled = { saving || ! dogId } >
{ saving ? 'Saving…' : < > < Heart size = { 15 } / > Start Cycle < / > }
< / button >
< / div >
< / form >
< / div >
< / div >
)
}
// ─── Cycle Detail Modal ───────────────────────────────────────────────────────
2026-03-09 20:52:57 -05:00
function CycleDetailModal ( { cycle , onClose , onDeleted , onRecordLitter } ) {
2026-03-09 20:32:21 -05:00
const [ suggestions , setSuggestions ] = useState ( null )
const [ breedingDate , setBreedingDate ] = useState ( cycle . breeding _date || '' )
const [ savingBreed , setSavingBreed ] = useState ( false )
const [ deleting , setDeleting ] = useState ( false )
const [ error , setError ] = useState ( null )
2026-03-08 22:54:36 -05:00
useEffect ( ( ) => {
2026-03-09 20:32:21 -05:00
fetch ( ` /api/breeding/heat-cycles/ ${ cycle . id } /suggestions ` )
. then ( r => r . json ( ) )
. then ( setSuggestions )
. catch ( ( ) => { } )
} , [ cycle . id ] )
2026-03-08 22:54:36 -05:00
2026-03-09 20:32:21 -05:00
async function saveBreedingDate ( ) {
if ( ! breedingDate ) return
setSavingBreed ( true ) ; setError ( null )
2026-03-08 22:54:36 -05:00
try {
2026-03-09 20:32:21 -05:00
const res = await fetch ( ` /api/breeding/heat-cycles/ ${ cycle . id } ` , {
method : 'PUT' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { ... cycle , breeding _date : breedingDate } )
} )
if ( ! res . ok ) { const e = await res . json ( ) ; throw new Error ( e . error ) }
// Refresh suggestions
const s = await fetch ( ` /api/breeding/heat-cycles/ ${ cycle . id } /suggestions ` ) . then ( r => r . json ( ) )
setSuggestions ( s )
} catch ( err ) { setError ( err . message ) }
finally { setSavingBreed ( false ) }
}
async function deleteCycle ( ) {
if ( ! window . confirm ( ` Delete heat cycle for ${ cycle . dog _name } ? This cannot be undone. ` ) ) return
setDeleting ( true )
try {
await fetch ( ` /api/breeding/heat-cycles/ ${ cycle . id } ` , { method : 'DELETE' } )
onDeleted ( )
} catch ( err ) { setError ( err . message ) ; setDeleting ( false ) }
}
const whelp = suggestions ? . whelping
2026-03-09 20:52:57 -05:00
const hasBreedingDate = ! ! ( breedingDate && breedingDate === cycle . breeding _date )
2026-03-09 20:32:21 -05:00
return (
< div className = "modal-overlay" onClick = { e => e . target === e . currentTarget && onClose ( ) } >
< div className = "modal-content" style = { { maxWidth : '560px' } } >
< div className = "modal-header" >
< div style = { { display : 'flex' , alignItems : 'center' , gap : '0.6rem' } } >
< Heart size = { 18 } style = { { color : '#f472b6' } } / >
< h2 > { cycle . dog _name } < / h2 >
< / div >
< button className = "btn-icon" onClick = { onClose } > < X size = { 20 } / > < / button >
< / div >
< div className = "modal-body" >
{ error && < div className = "error" > { error } < / div > }
{ /* Cycle meta */ }
< div style = { { display : 'flex' , gap : '1rem' , marginBottom : '1.5rem' , flexWrap : 'wrap' } } >
< div style = { infoChip } >
< span style = { { color : 'var(--text-muted)' , fontSize : '0.75rem' , textTransform : 'uppercase' , letterSpacing : '0.05em' } } > Started < / span >
< span style = { { fontWeight : 600 } } > { fmt ( cycle . start _date ) } < / span >
< / div >
{ cycle . breed && (
< div style = { infoChip } >
< span style = { { color : 'var(--text-muted)' , fontSize : '0.75rem' , textTransform : 'uppercase' , letterSpacing : '0.05em' } } > Breed < / span >
< span style = { { fontWeight : 600 } } > { cycle . breed } < / span >
< / div >
) }
< / div >
{ /* Breeding date windows */ }
{ suggestions && (
< >
< h3 style = { { fontSize : '0.9375rem' , marginBottom : '0.75rem' , display : 'flex' , alignItems : 'center' , gap : '0.4rem' } } >
< FlaskConical size = { 16 } style = { { color : 'var(--accent)' } } / > Breeding Date Windows
< / h3 >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : '0.5rem' , marginBottom : '1.5rem' } } >
{ suggestions . windows . map ( w => (
< div key = { w . type } style = { {
display : 'flex' , alignItems : 'flex-start' , gap : '0.75rem' ,
padding : '0.625rem 0.875rem' ,
background : WINDOW _STYLES [ w . type ] ? . bg ,
border : ` 1px solid ${ WINDOW _STYLES [ w . type ] ? . border } ` ,
borderRadius : 'var(--radius-sm)'
} } >
< div style = { { width : 10 , height : 10 , borderRadius : '50%' , background : WINDOW _STYLES [ w . type ] ? . dot , marginTop : 4 , flexShrink : 0 } } / >
< div style = { { flex : 1 , minWidth : 0 } } >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , gap : '0.5rem' , flexWrap : 'wrap' } } >
< span style = { { fontWeight : 600 , fontSize : '0.875rem' } } > { w . label } < / span >
< span style = { { fontSize : '0.8125rem' , color : 'var(--text-secondary)' , whiteSpace : 'nowrap' } } > { fmt ( w . start ) } – { fmt ( w . end ) } < / span >
< / div >
< p style = { { fontSize : '0.8rem' , color : 'var(--text-muted)' , margin : '0.15rem 0 0' } } > { w . description } < / p >
< / div >
< / div >
) ) }
< / div >
< / >
) }
{ /* Log breeding date */ }
< div style = { { background : 'var(--bg-tertiary)' , borderRadius : 'var(--radius)' , padding : '1rem' , marginBottom : '1.25rem' } } >
< h3 style = { { fontSize : '0.9375rem' , marginBottom : '0.75rem' , display : 'flex' , alignItems : 'center' , gap : '0.4rem' } } >
< CalendarDays size = { 16 } style = { { color : 'var(--primary)' } } / > Log Breeding Date
< / h3 >
< div style = { { display : 'flex' , gap : '0.75rem' , alignItems : 'flex-end' , flexWrap : 'wrap' } } >
< div style = { { flex : 1 , minWidth : 160 } } >
< label className = "label" style = { { marginBottom : '0.4rem' } } > Breeding Date < / label >
< input type = "date" className = "input" value = { breedingDate } onChange = { e => setBreedingDate ( e . target . value ) } / >
< / div >
< button className = "btn btn-primary" onClick = { saveBreedingDate } disabled = { savingBreed || ! breedingDate } style = { { marginBottom : 0 } } >
{ savingBreed ? 'Saving…' : 'Save' }
< / button >
< / div >
< / div >
{ /* Whelping estimate */ }
{ whelp && (
2026-03-09 20:52:57 -05:00
< 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' } } >
2026-03-09 20:32:21 -05:00
< h3 style = { { fontSize : '0.9375rem' , marginBottom : '0.75rem' , display : 'flex' , alignItems : 'center' , gap : '0.4rem' , color : 'var(--success)' } } >
< Baby size = { 16 } / > Whelping Estimate
< / h3 >
< div style = { { display : 'grid' , gridTemplateColumns : 'repeat(3, 1fr)' , gap : '0.75rem' , textAlign : 'center' } } >
{ [ [ 'Earliest' , whelp . earliest ] , [ 'Expected' , whelp . expected ] , [ 'Latest' , whelp . latest ] ] . map ( ( [ label , date ] ) => (
< div key = { label } >
< div style = { { fontSize : '0.75rem' , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : '0.05em' , marginBottom : '0.2rem' } } > { label } < / div >
< div style = { { fontWeight : 700 , fontSize : '0.9375rem' } } > { fmt ( date ) } < / div >
< / div >
) ) }
< / div >
< / div >
) }
2026-03-09 20:52:57 -05:00
{ /* 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 >
) }
2026-03-09 20:32:21 -05:00
< / div >
< div className = "modal-footer" style = { { justifyContent : 'space-between' } } >
< button className = "btn btn-danger" onClick = { deleteCycle } disabled = { deleting } >
{ deleting ? 'Deleting…' : 'Delete Cycle' }
< / button >
< button className = "btn btn-secondary" onClick = { onClose } > Close < / button >
< / div >
< / div >
< / div >
)
}
const infoChip = {
display : 'flex' , flexDirection : 'column' , gap : '0.15rem' ,
padding : '0.5rem 0.875rem' ,
background : 'var(--bg-tertiary)' ,
borderRadius : 'var(--radius-sm)'
}
// ─── Main Calendar ────────────────────────────────────────────────────────────
export default function BreedingCalendar ( ) {
const now = new Date ( )
const [ year , setYear ] = useState ( now . getFullYear ( ) )
const [ month , setMonth ] = useState ( now . getMonth ( ) ) // 0-indexed
const [ cycles , setCycles ] = useState ( [ ] )
const [ females , setFemales ] = useState ( [ ] )
const [ loading , setLoading ] = useState ( true )
const [ showStartModal , setShowStartModal ] = useState ( false )
const [ selectedCycle , setSelectedCycle ] = useState ( null )
const [ selectedDay , setSelectedDay ] = useState ( null )
2026-03-09 20:52:57 -05:00
const [ pendingLitterCycle , setPendingLitterCycle ] = useState ( null )
const navigate = useNavigate ( )
2026-03-09 20:32:21 -05:00
const load = useCallback ( async ( ) => {
setLoading ( true )
try {
const [ cyclesRes , dogsRes ] = await Promise . all ( [
fetch ( '/api/breeding/heat-cycles' ) ,
fetch ( '/api/dogs' )
] )
const allCycles = await cyclesRes . json ( )
const dogsData = await dogsRes . json ( )
const allDogs = Array . isArray ( dogsData ) ? dogsData : ( dogsData . dogs || [ ] )
setCycles ( Array . isArray ( allCycles ) ? allCycles : [ ] )
setFemales ( allDogs . filter ( d => d . sex === 'female' ) )
} catch ( e ) {
console . error ( e )
} finally {
2026-03-08 22:54:36 -05:00
setLoading ( false )
}
2026-03-09 20:32:21 -05:00
} , [ ] )
useEffect ( ( ) => { load ( ) } , [ load ] )
2026-03-09 20:52:57 -05:00
// 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 ] )
2026-03-09 20:32:21 -05:00
// ── Build calendar grid ──
const firstDay = new Date ( year , month , 1 )
const lastDay = new Date ( year , month + 1 , 0 )
const startPad = firstDay . getDay ( ) // 0=Sun
const totalCells = startPad + lastDay . getDate ( )
const rows = Math . ceil ( totalCells / 7 )
const MONTH _NAMES = [ 'January' , 'February' , 'March' , 'April' , 'May' , 'June' , 'July' , 'August' , 'September' , 'October' , 'November' , 'December' ]
const DAY _NAMES = [ 'Sun' , 'Mon' , 'Tue' , 'Wed' , 'Thu' , 'Fri' , 'Sat' ]
function prevMonth ( ) {
if ( month === 0 ) { setMonth ( 11 ) ; setYear ( y => y - 1 ) }
else setMonth ( m => m - 1 )
}
function nextMonth ( ) {
if ( month === 11 ) { setMonth ( 0 ) ; setYear ( y => y + 1 ) }
else setMonth ( m => m + 1 )
2026-03-08 22:54:36 -05:00
}
2026-03-09 20:32:21 -05:00
function cyclesForDate ( dateStr ) {
return cycles . filter ( c => {
const s = c . start _date
if ( ! s ) return false
const end = c . end _date || addDays ( s , 28 )
return dateStr >= s && dateStr <= end
} )
2026-03-08 22:54:36 -05:00
}
2026-03-09 20:32:21 -05:00
function handleDayClick ( dateStr , dayCycles ) {
setSelectedDay ( dateStr )
if ( dayCycles . length === 1 ) {
setSelectedCycle ( dayCycles [ 0 ] )
} else if ( dayCycles . length > 1 ) {
setSelectedCycle ( dayCycles [ 0 ] )
} else {
setShowStartModal ( true )
}
}
const activeCycles = cycles . filter ( c => {
const s = c . start _date ; if ( ! s ) return false
const end = c . end _date || addDays ( s , 28 )
const mStart = toISO ( new Date ( year , month , 1 ) )
const mEnd = toISO ( new Date ( year , month + 1 , 0 ) )
return s <= mEnd && end >= mStart
} )
2026-03-08 22:54:36 -05:00
return (
2026-03-09 20:32:21 -05:00
< div className = "container" style = { { paddingTop : '2rem' , paddingBottom : '3rem' } } >
{ /* Header */ }
< div style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' , marginBottom : '1.5rem' , flexWrap : 'wrap' , gap : '1rem' } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : '0.75rem' } } >
< div style = { { width : '2.5rem' , height : '2.5rem' , borderRadius : 'var(--radius)' , background : 'rgba(244,114,182,0.2)' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , color : '#f472b6' } } >
< Heart size = { 20 } / >
< / div >
< div >
< h1 style = { { fontSize : '1.75rem' , margin : 0 } } > Heat Cycle Calendar < / h1 >
< p style = { { color : 'var(--text-muted)' , margin : 0 , fontSize : '0.875rem' } } > Track heat cycles and optimal breeding windows < / p >
< / div >
< / div >
< button className = "btn btn-primary" onClick = { ( ) => setShowStartModal ( true ) } >
< Plus size = { 16 } / > Start Heat Cycle
< / button >
< / div >
{ /* Legend */ }
< div style = { { display : 'flex' , gap : '0.75rem' , marginBottom : '1.25rem' , flexWrap : 'wrap' } } >
{ Object . entries ( WINDOW _STYLES ) . map ( ( [ key , s ] ) => (
< div key = { key } style = { { display : 'flex' , alignItems : 'center' , gap : '0.4rem' , fontSize : '0.8125rem' , color : 'var(--text-secondary)' } } >
< div style = { { width : 10 , height : 10 , borderRadius : '50%' , background : s . dot } } / >
{ s . label }
2026-03-08 22:54:36 -05:00
< / div >
2026-03-09 20:32:21 -05:00
) ) }
< / div >
{ /* Month navigator */ }
< div className = "card" style = { { marginBottom : '1rem' , padding : '0' } } >
< div style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' , padding : '0.875rem 1rem' , borderBottom : '1px solid var(--border)' } } >
< button className = "btn-icon" onClick = { prevMonth } > < ChevronLeft size = { 20 } / > < / button >
< h2 style = { { margin : 0 , fontSize : '1.1rem' } } > { MONTH _NAMES [ month ] } { year } < / h2 >
< button className = "btn-icon" onClick = { nextMonth } > < ChevronRight size = { 20 } / > < / button >
< / div >
{ /* Day headers */ }
< div style = { { display : 'grid' , gridTemplateColumns : 'repeat(7, 1fr)' , borderBottom : '1px solid var(--border)' } } >
{ DAY _NAMES . map ( d => (
< div key = { d } style = { { padding : '0.5rem' , textAlign : 'center' , fontSize : '0.75rem' , fontWeight : 600 , color : 'var(--text-muted)' , textTransform : 'uppercase' , letterSpacing : '0.05em' } } > { d } < / div >
) ) }
< / div >
{ /* Calendar cells */ }
{ loading ? (
< div className = "loading" style = { { minHeight : 280 } } > Loading calendar … < / div >
2026-03-08 22:54:36 -05:00
) : (
2026-03-09 20:32:21 -05:00
< div style = { { display : 'grid' , gridTemplateColumns : 'repeat(7, 1fr)' } } >
{ Array . from ( { length : rows * 7 } ) . map ( ( _ , idx ) => {
const dayNum = idx - startPad + 1
const isValid = dayNum >= 1 && dayNum <= lastDay . getDate ( )
const dateStr = isValid ? toISO ( new Date ( year , month , dayNum ) ) : null
const dayCycles = dateStr ? cyclesForDate ( dateStr ) : [ ]
const isToday = dateStr === today
let cellBg = 'transparent'
let cellBorder = 'var(--border)'
if ( dayCycles . length > 0 ) {
const win = getWindowForDate ( dayCycles [ 0 ] , dateStr )
if ( win && WINDOW _STYLES [ win ] ) {
cellBg = WINDOW _STYLES [ win ] . bg
cellBorder = WINDOW _STYLES [ win ] . border
}
}
return (
< div
key = { idx }
onClick = { ( ) => isValid && handleDayClick ( dateStr , dayCycles ) }
style = { {
minHeight : 72 ,
padding : '0.375rem 0.5rem' ,
borderRight : '1px solid var(--border)' ,
borderBottom : '1px solid var(--border)' ,
background : cellBg ,
cursor : isValid ? 'pointer' : 'default' ,
position : 'relative' ,
transition : 'filter 0.15s' ,
opacity : isValid ? 1 : 0.3 ,
outline : isToday ? ` 2px solid var(--primary) ` : 'none' ,
outlineOffset : - 2 ,
} }
onMouseEnter = { e => { if ( isValid ) e . currentTarget . style . filter = 'brightness(1.15)' } }
onMouseLeave = { e => { e . currentTarget . style . filter = 'none' } }
>
{ isValid && (
< >
< div style = { {
fontSize : '0.8125rem' , fontWeight : isToday ? 700 : 500 ,
color : isToday ? 'var(--primary)' : 'var(--text-primary)' ,
marginBottom : '0.25rem'
} } > { dayNum } < / div >
{ dayCycles . map ( ( c , i ) => {
const win = getWindowForDate ( c , dateStr )
const dot = win ? WINDOW _STYLES [ win ] ? . dot : '#94a3b8'
return (
< div key = { i } style = { {
fontSize : '0.7rem' , color : dot , fontWeight : 600 ,
whiteSpace : 'nowrap' , overflow : 'hidden' , textOverflow : 'ellipsis' ,
lineHeight : 1.3
} } >
♥ { c . dog _name }
< / div >
)
} ) }
{ /* Breeding date marker */ }
{ dayCycles . some ( c => c . breeding _date === dateStr ) && (
< div style = { { position : 'absolute' , top : 4 , right : 4 , width : 8 , height : 8 , borderRadius : '50%' , background : 'var(--success)' , border : '1.5px solid var(--bg-primary)' } } title = "Breeding date logged" / >
) }
< / >
) }
< / div >
)
} ) }
2026-03-08 22:54:36 -05:00
< / div >
) }
< / div >
2026-03-09 20:32:21 -05:00
{ /* Active cycles list */ }
< div style = { { marginTop : '1.5rem' } } >
< h3 style = { { fontSize : '1rem' , marginBottom : '0.875rem' , display : 'flex' , alignItems : 'center' , gap : '0.5rem' } } >
< AlertCircle size = { 16 } style = { { color : '#f472b6' } } / >
Active Cycles This Month
< span className = "badge badge-primary" > { activeCycles . length } < / span >
< / h3 >
{ activeCycles . length === 0 ? (
< div className = "card" style = { { textAlign : 'center' , padding : '2rem' , color : 'var(--text-muted)' } } >
< Heart size = { 32 } style = { { margin : '0 auto 0.75rem' , opacity : 0.4 } } / >
< p > No active heat cycles this month . < / p >
< button className = "btn btn-primary" style = { { marginTop : '1rem' } } onClick = { ( ) => setShowStartModal ( true ) } >
< Plus size = { 15 } / > Start First Cycle
< / button >
< / div >
) : (
< div style = { { display : 'grid' , gap : '0.75rem' , gridTemplateColumns : 'repeat(auto-fill, minmax(260px, 1fr))' } } >
{ activeCycles . map ( c => {
const win = getWindowForDate ( c , today )
const ws = win ? WINDOW _STYLES [ win ] : null
const daysSince = Math . round ( ( new Date ( today ) - new Date ( c . start _date + 'T00:00:00' ) ) / 86400000 )
return (
< div
key = { c . id }
className = "card"
style = { { cursor : 'pointer' , borderColor : ws ? . border || 'var(--border)' , background : ws ? . bg || 'var(--bg-secondary)' } }
onClick = { ( ) => setSelectedCycle ( c ) }
>
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'flex-start' } } >
< div >
< h4 style = { { margin : '0 0 0.2rem' , fontSize : '1rem' } } > { c . dog _name } < / h4 >
{ c . breed && < p style = { { color : 'var(--text-muted)' , fontSize : '0.8rem' , margin : 0 } } > { c . breed } < / p > }
< / div >
{ ws && < span className = "badge" style = { { background : ws . bg , color : ws . dot , border : ` 1px solid ${ ws . border } ` , flexShrink : 0 } } > { ws . label } < / span > }
< / div >
< div style = { { marginTop : '0.75rem' , display : 'flex' , gap : '1rem' , fontSize : '0.8125rem' , color : 'var(--text-secondary)' } } >
< span > Started { fmt ( c . start _date ) } < / span >
< span > Day { daysSince + 1 } < / span >
< / div >
{ c . breeding _date && (
< div style = { { marginTop : '0.5rem' , display : 'flex' , alignItems : 'center' , gap : '0.4rem' , fontSize : '0.8rem' , color : 'var(--success)' } } >
< CheckCircle2 size = { 13 } / > Bred { fmt ( c . breeding _date ) }
< / div >
) }
< / div >
)
} ) }
< / div >
) }
2026-03-08 22:54:36 -05:00
< / div >
2026-03-09 20:32:21 -05:00
{ /* Modals */ }
{ showStartModal && (
< StartCycleModal
females = { females }
onClose = { ( ) => setShowStartModal ( false ) }
onSaved = { ( ) => { setShowStartModal ( false ) ; load ( ) } }
/ >
) }
{ selectedCycle && (
< CycleDetailModal
cycle = { selectedCycle }
onClose = { ( ) => setSelectedCycle ( null ) }
onDeleted = { ( ) => { setSelectedCycle ( null ) ; load ( ) } }
2026-03-09 20:52:57 -05:00
onRecordLitter = { handleRecordLitter }
2026-03-09 20:32:21 -05:00
/ >
) }
2026-03-08 22:54:36 -05:00
< / div >
)
}