544 lines
24 KiB
JavaScript
544 lines
24 KiB
JavaScript
import { useEffect, useState } from 'react'
|
||
import { useParams, useNavigate } from 'react-router-dom'
|
||
import { ArrowLeft, Plus, X, ExternalLink, Dog, Weight, ChevronDown, ChevronUp, Trash2 } from 'lucide-react'
|
||
import axios from 'axios'
|
||
import LitterForm from '../components/LitterForm'
|
||
|
||
// ─── Puppy Log Panel ────────────────────────────────────────────────────────────
|
||
function PuppyLogPanel({ litterId, puppy, whelpingDate }) {
|
||
const [open, setOpen] = useState(false)
|
||
const [logs, setLogs] = useState([])
|
||
const [loading, setLoading] = useState(false)
|
||
const [showAdd, setShowAdd] = useState(false)
|
||
const [form, setForm] = useState({
|
||
record_date: whelpingDate || '',
|
||
weight_oz: '',
|
||
weight_lbs: '',
|
||
notes: '',
|
||
record_type: 'weight_log'
|
||
})
|
||
const [saving, setSaving] = useState(false)
|
||
|
||
useEffect(() => { if (open) fetchLogs() }, [open])
|
||
|
||
const fetchLogs = async () => {
|
||
setLoading(true)
|
||
try {
|
||
const res = await axios.get(`/api/litters/${litterId}/puppies/${puppy.id}/logs`)
|
||
const parsed = res.data.map(l => {
|
||
try { return { ...l, _data: JSON.parse(l.description) } } catch { return { ...l, _data: {} } }
|
||
})
|
||
setLogs(parsed)
|
||
} catch (e) { console.error(e) }
|
||
finally { setLoading(false) }
|
||
}
|
||
|
||
const handleAdd = async () => {
|
||
if (!form.record_date) return
|
||
setSaving(true)
|
||
try {
|
||
await axios.post(`/api/litters/${litterId}/puppies/${puppy.id}/logs`, form)
|
||
setShowAdd(false)
|
||
setForm(f => ({ ...f, weight_oz: '', weight_lbs: '', notes: '' }))
|
||
fetchLogs()
|
||
} catch (e) { console.error(e) }
|
||
finally { setSaving(false) }
|
||
}
|
||
|
||
const handleDelete = async (logId) => {
|
||
if (!window.confirm('Delete this log entry?')) return
|
||
try {
|
||
await axios.delete(`/api/litters/${litterId}/puppies/${puppy.id}/logs/${logId}`)
|
||
fetchLogs()
|
||
} catch (e) { console.error(e) }
|
||
}
|
||
|
||
const TYPES = [
|
||
{ value: 'weight_log', label: '⚖️ Weight Check' },
|
||
{ value: 'health_note', label: '📝 Health Note' },
|
||
{ value: 'deworming', label: '🐛 Deworming' },
|
||
{ value: 'vaccination', label: '💉 Vaccination' },
|
||
]
|
||
|
||
return (
|
||
<div style={{ borderTop: '1px solid var(--border)', marginTop: '0.5rem' }}>
|
||
<button
|
||
onClick={() => setOpen(o => !o)}
|
||
style={{
|
||
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||
background: 'none', border: 'none', cursor: 'pointer', padding: '0.5rem 0',
|
||
color: 'var(--text-secondary)', fontSize: '0.8rem'
|
||
}}
|
||
>
|
||
<span style={{ display: 'flex', alignItems: 'center', gap: '0.3rem' }}>
|
||
<Weight size={13} /> Logs {logs.length > 0 && `(${logs.length})`}
|
||
</span>
|
||
{open ? <ChevronUp size={13} /> : <ChevronDown size={13} />}
|
||
</button>
|
||
|
||
{open && (
|
||
<div style={{ paddingBottom: '0.5rem' }}>
|
||
{loading ? (
|
||
<p style={{ fontSize: '0.78rem', color: 'var(--text-secondary)' }}>Loading...</p>
|
||
) : logs.length === 0 ? (
|
||
<p style={{ fontSize: '0.78rem', color: 'var(--text-secondary)' }}>No logs yet.</p>
|
||
) : (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem', marginBottom: '0.5rem' }}>
|
||
{logs.map(l => (
|
||
<div key={l.id} style={{
|
||
display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between',
|
||
background: 'var(--bg-tertiary)', borderRadius: 'var(--radius-sm)',
|
||
padding: '0.4rem 0.6rem', gap: '0.5rem'
|
||
}}>
|
||
<div style={{ fontSize: '0.75rem', flex: 1 }}>
|
||
<span style={{ fontWeight: 600 }}>
|
||
{new Date(l.record_date + 'T00:00:00').toLocaleDateString()}
|
||
</span>
|
||
{' • '}
|
||
<span style={{ color: 'var(--accent)' }}>
|
||
{TYPES.find(t => t.value === l.record_type)?.label || l.record_type}
|
||
</span>
|
||
{l._data?.weight_oz && <span> — {l._data.weight_oz} oz</span>}
|
||
{l._data?.weight_lbs && <span> ({l._data.weight_lbs} lbs)</span>}
|
||
{l._data?.notes && (
|
||
<div style={{ color: 'var(--text-secondary)', marginTop: '0.1rem' }}>
|
||
{l._data.notes}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<button
|
||
onClick={() => handleDelete(l.id)}
|
||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#e53e3e', padding: 0, flexShrink: 0 }}
|
||
>
|
||
<Trash2 size={12} />
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{showAdd ? (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem' }}>
|
||
<div style={{ display: 'flex', gap: '0.4rem', flexWrap: 'wrap' }}>
|
||
<input
|
||
type="date" className="input"
|
||
style={{ fontSize: '0.78rem', padding: '0.3rem 0.5rem', flex: '1 1 120px' }}
|
||
value={form.record_date}
|
||
onChange={e => setForm(f => ({ ...f, record_date: e.target.value }))}
|
||
/>
|
||
<select
|
||
className="input"
|
||
style={{ fontSize: '0.78rem', padding: '0.3rem 0.5rem', flex: '1 1 130px' }}
|
||
value={form.record_type}
|
||
onChange={e => setForm(f => ({ ...f, record_type: e.target.value }))}
|
||
>
|
||
{TYPES.map(t => <option key={t.value} value={t.value}>{t.label}</option>)}
|
||
</select>
|
||
</div>
|
||
{form.record_type === 'weight_log' && (
|
||
<div style={{ display: 'flex', gap: '0.4rem' }}>
|
||
<input
|
||
type="number" className="input" placeholder="oz" step="0.1" min="0"
|
||
style={{ fontSize: '0.78rem', padding: '0.3rem 0.5rem', flex: 1 }}
|
||
value={form.weight_oz}
|
||
onChange={e => setForm(f => ({ ...f, weight_oz: e.target.value }))}
|
||
/>
|
||
<input
|
||
type="number" className="input" placeholder="lbs" step="0.01" min="0"
|
||
style={{ fontSize: '0.78rem', padding: '0.3rem 0.5rem', flex: 1 }}
|
||
value={form.weight_lbs}
|
||
onChange={e => setForm(f => ({ ...f, weight_lbs: e.target.value }))}
|
||
/>
|
||
</div>
|
||
)}
|
||
<input
|
||
className="input" placeholder="Notes (optional)"
|
||
style={{ fontSize: '0.78rem', padding: '0.3rem 0.5rem' }}
|
||
value={form.notes}
|
||
onChange={e => setForm(f => ({ ...f, notes: e.target.value }))}
|
||
/>
|
||
<div style={{ display: 'flex', gap: '0.4rem' }}>
|
||
<button
|
||
className="btn btn-primary"
|
||
style={{ fontSize: '0.75rem', padding: '0.3rem 0.75rem' }}
|
||
onClick={handleAdd} disabled={saving}
|
||
>
|
||
{saving ? 'Saving...' : 'Save'}
|
||
</button>
|
||
<button
|
||
className="btn btn-secondary"
|
||
style={{ fontSize: '0.75rem', padding: '0.3rem 0.75rem' }}
|
||
onClick={() => setShowAdd(false)}
|
||
>
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<button
|
||
className="btn btn-secondary"
|
||
style={{ fontSize: '0.75rem', padding: '0.3rem 0.75rem', width: '100%' }}
|
||
onClick={() => setShowAdd(true)}
|
||
>
|
||
<Plus size={12} style={{ marginRight: '0.3rem' }} /> Add Log Entry
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Whelping Window Banner ───────────────────────────────────────────────
|
||
function addDays(dateStr, n) {
|
||
const d = new Date(dateStr + 'T00:00:00')
|
||
d.setDate(d.getDate() + n)
|
||
return d
|
||
}
|
||
function fmt(d) { return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) }
|
||
|
||
function WhelpingBanner({ breedingDate, whelpingDate }) {
|
||
if (whelpingDate) return null // already whelped, no need for estimate
|
||
if (!breedingDate) return null
|
||
|
||
const earliest = addDays(breedingDate, 58)
|
||
const expected = addDays(breedingDate, 63)
|
||
const latest = addDays(breedingDate, 68)
|
||
const today = new Date()
|
||
const daysUntil = Math.ceil((expected - today) / 86400000)
|
||
|
||
let urgency = 'var(--success)'
|
||
let urgencyBg = 'rgba(16,185,129,0.06)'
|
||
let statusLabel = `~${daysUntil} days away`
|
||
if (daysUntil <= 7 && daysUntil > 0) {
|
||
urgency = '#d97706'; urgencyBg = 'rgba(217,119,6,0.08)'
|
||
statusLabel = `⚠️ ${daysUntil} days — prepare whelping area!`
|
||
} else if (daysUntil <= 0) {
|
||
urgency = '#e53e3e'; urgencyBg = 'rgba(229,62,62,0.08)'
|
||
statusLabel = '🔴 Expected date has passed — confirm or update whelping date'
|
||
}
|
||
|
||
return (
|
||
<div className="card" style={{
|
||
marginBottom: '2rem', padding: '1rem',
|
||
borderLeft: `3px solid ${urgency}`,
|
||
background: urgencyBg
|
||
}}>
|
||
<div style={{ fontWeight: 600, marginBottom: '0.5rem', color: urgency }}>
|
||
💕 Projected Whelping Window
|
||
<span style={{ fontWeight: 400, fontSize: '0.82rem', marginLeft: '0.75rem', color: 'var(--text-secondary)' }}>
|
||
{statusLabel}
|
||
</span>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: '2rem', flexWrap: 'wrap', fontSize: '0.875rem' }}>
|
||
<div>
|
||
<span style={{ color: 'var(--text-secondary)', fontSize: '0.78rem' }}>Earliest (Day 58)</span>
|
||
<br /><strong>{fmt(earliest)}</strong>
|
||
</div>
|
||
<div>
|
||
<span style={{ color: 'var(--text-secondary)', fontSize: '0.78rem' }}>Expected (Day 63)</span>
|
||
<br /><strong style={{ color: urgency, fontSize: '1rem' }}>{fmt(expected)}</strong>
|
||
</div>
|
||
<div>
|
||
<span style={{ color: 'var(--text-secondary)', fontSize: '0.78rem' }}>Latest (Day 68)</span>
|
||
<br /><strong>{fmt(latest)}</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Main LitterDetail ─────────────────────────────────────────────────────────
|
||
function LitterDetail() {
|
||
const { id } = useParams()
|
||
const navigate = useNavigate()
|
||
const [litter, setLitter] = useState(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const [showEditForm, setShowEditForm] = useState(false)
|
||
const [showAddPuppy, setShowAddPuppy] = useState(false)
|
||
const [allDogs, setAllDogs] = useState([])
|
||
const [selectedPuppyId, setSelectedPuppyId] = useState('')
|
||
const [newPuppy, setNewPuppy] = useState({ name: '', sex: 'male', color: '', dob: '' })
|
||
const [addMode, setAddMode] = useState('existing')
|
||
const [error, setError] = useState('')
|
||
const [saving, setSaving] = useState(false)
|
||
|
||
useEffect(() => { fetchLitter(); fetchAllDogs() }, [id])
|
||
|
||
const fetchLitter = async () => {
|
||
try {
|
||
const res = await axios.get(`/api/litters/${id}`)
|
||
setLitter(res.data)
|
||
} catch (err) {
|
||
console.error('Error fetching litter:', err)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const fetchAllDogs = async () => {
|
||
try {
|
||
const res = await axios.get('/api/dogs')
|
||
setAllDogs(res.data)
|
||
} catch (err) { console.error('Error fetching dogs:', err) }
|
||
}
|
||
|
||
const unlinkedDogs = allDogs.filter(d => {
|
||
if (!litter) return false
|
||
const alreadyInLitter = litter.puppies?.some(p => p.id === d.id)
|
||
const isSireOrDam = d.id === litter.sire_id || d.id === litter.dam_id
|
||
return !alreadyInLitter && !isSireOrDam
|
||
})
|
||
|
||
const handleLinkPuppy = async () => {
|
||
if (!selectedPuppyId) return
|
||
setSaving(true); setError('')
|
||
try {
|
||
await axios.post(`/api/litters/${id}/puppies/${selectedPuppyId}`)
|
||
setSelectedPuppyId(''); setShowAddPuppy(false); fetchLitter()
|
||
} catch (err) {
|
||
setError(err.response?.data?.error || 'Failed to link puppy')
|
||
} finally { setSaving(false) }
|
||
}
|
||
|
||
const handleCreateAndLink = async () => {
|
||
if (!newPuppy.name) { setError('Puppy name is required'); return }
|
||
setSaving(true); setError('')
|
||
try {
|
||
const dob = newPuppy.dob || litter.whelping_date || litter.breeding_date
|
||
const res = await axios.post('/api/dogs', {
|
||
name: newPuppy.name, sex: newPuppy.sex,
|
||
color: newPuppy.color, date_of_birth: dob,
|
||
breed: litter.dam_breed || '',
|
||
})
|
||
await axios.post(`/api/litters/${id}/puppies/${res.data.id}`)
|
||
setNewPuppy({ name: '', sex: 'male', color: '', dob: '' })
|
||
setShowAddPuppy(false); fetchLitter(); fetchAllDogs()
|
||
} catch (err) {
|
||
setError(err.response?.data?.error || 'Failed to create puppy')
|
||
} finally { setSaving(false) }
|
||
}
|
||
|
||
const handleUnlinkPuppy = async (puppyId) => {
|
||
if (!window.confirm('Remove this puppy from the litter? The dog record will not be deleted.')) return
|
||
try {
|
||
await axios.delete(`/api/litters/${id}/puppies/${puppyId}`)
|
||
fetchLitter()
|
||
} catch (err) { console.error('Error unlinking puppy:', err) }
|
||
}
|
||
|
||
if (loading) return <div className="container loading">Loading litter...</div>
|
||
if (!litter) return <div className="container"><p>Litter not found.</p></div>
|
||
|
||
const puppyCount = litter.puppies?.length ?? 0
|
||
|
||
return (
|
||
<div className="container">
|
||
{/* Header */}
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '2rem' }}>
|
||
<button className="btn-icon" onClick={() => navigate('/litters')}>
|
||
<ArrowLeft size={20} />
|
||
</button>
|
||
<div style={{ flex: 1 }}>
|
||
<h1 style={{ margin: 0 }}>🐾 {litter.sire_name} × {litter.dam_name}</h1>
|
||
<p style={{ color: 'var(--text-secondary)', margin: '0.25rem 0 0' }}>
|
||
Bred: {new Date(litter.breeding_date + 'T00:00:00').toLocaleDateString()}
|
||
{litter.whelping_date && ` • Whelped: ${new Date(litter.whelping_date + 'T00:00:00').toLocaleDateString()}`}
|
||
</p>
|
||
</div>
|
||
<button className="btn btn-secondary" onClick={() => setShowEditForm(true)}>Edit Litter</button>
|
||
</div>
|
||
|
||
{/* Stats row */}
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '1rem', marginBottom: '2rem' }}>
|
||
<div className="card" style={{ textAlign: 'center', padding: '1rem' }}>
|
||
<div style={{ fontSize: '2rem', fontWeight: 700, color: 'var(--accent)' }}>{puppyCount}</div>
|
||
<div style={{ color: 'var(--text-secondary)', fontSize: '0.85rem' }}>Puppies Linked</div>
|
||
</div>
|
||
<div className="card" style={{ textAlign: 'center', padding: '1rem' }}>
|
||
<div style={{ fontSize: '2rem', fontWeight: 700, color: 'var(--accent)' }}>
|
||
{litter.puppies?.filter(p => p.sex === 'male').length ?? 0}
|
||
</div>
|
||
<div style={{ color: 'var(--text-secondary)', fontSize: '0.85rem' }}>Males</div>
|
||
</div>
|
||
<div className="card" style={{ textAlign: 'center', padding: '1rem' }}>
|
||
<div style={{ fontSize: '2rem', fontWeight: 700, color: 'var(--accent)' }}>
|
||
{litter.puppies?.filter(p => p.sex === 'female').length ?? 0}
|
||
</div>
|
||
<div style={{ color: 'var(--text-secondary)', fontSize: '0.85rem' }}>Females</div>
|
||
</div>
|
||
{litter.puppy_count > 0 && (
|
||
<div className="card" style={{ textAlign: 'center', padding: '1rem' }}>
|
||
<div style={{ fontSize: '2rem', fontWeight: 700 }}>{litter.puppy_count}</div>
|
||
<div style={{ color: 'var(--text-secondary)', fontSize: '0.85rem' }}>Expected</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Projected whelping window */}
|
||
<WhelpingBanner breedingDate={litter.breeding_date} whelpingDate={litter.whelping_date} />
|
||
|
||
{/* Notes */}
|
||
{litter.notes && (
|
||
<div className="card" style={{ marginBottom: '2rem', padding: '1rem', borderLeft: '3px solid var(--accent)' }}>
|
||
<p style={{ margin: 0, fontStyle: 'italic', color: 'var(--text-secondary)' }}>{litter.notes}</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Puppies section */}
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||
<h2 style={{ margin: 0 }}>Puppies</h2>
|
||
<button className="btn btn-primary" onClick={() => { setShowAddPuppy(true); setError('') }}>
|
||
<Plus size={16} style={{ marginRight: '0.4rem' }} />
|
||
Add Puppy
|
||
</button>
|
||
</div>
|
||
|
||
{puppyCount === 0 ? (
|
||
<div className="card" style={{ textAlign: 'center', padding: '3rem' }}>
|
||
<Dog size={48} style={{ color: 'var(--text-secondary)', margin: '0 auto 1rem' }} />
|
||
<p style={{ color: 'var(--text-secondary)' }}>No puppies linked yet. Add puppies to this litter.</p>
|
||
</div>
|
||
) : (
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: '1rem' }}>
|
||
{litter.puppies.map(puppy => (
|
||
<div key={puppy.id} className="card" style={{ position: 'relative' }}>
|
||
<button
|
||
className="btn-icon"
|
||
onClick={() => handleUnlinkPuppy(puppy.id)}
|
||
title="Remove from litter"
|
||
style={{ position: 'absolute', top: '0.75rem', right: '0.75rem', color: '#e53e3e' }}
|
||
>
|
||
<X size={14} />
|
||
</button>
|
||
<div style={{ fontSize: '2.5rem', marginBottom: '0.5rem' }}>
|
||
{puppy.sex === 'male' ? '🐦' : '🐥'}
|
||
</div>
|
||
<div style={{ fontWeight: 600, marginBottom: '0.25rem' }}>{puppy.name}</div>
|
||
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
|
||
{puppy.sex} {puppy.color && `• ${puppy.color}`}
|
||
</div>
|
||
{puppy.date_of_birth && (
|
||
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
|
||
Born: {new Date(puppy.date_of_birth + 'T00:00:00').toLocaleDateString()}
|
||
</div>
|
||
)}
|
||
<button
|
||
className="btn btn-secondary"
|
||
style={{ marginTop: '0.75rem', width: '100%', fontSize: '0.8rem', padding: '0.4rem' }}
|
||
onClick={() => navigate(`/dogs/${puppy.id}`)}
|
||
>
|
||
<ExternalLink size={12} style={{ marginRight: '0.3rem' }} />
|
||
View Profile
|
||
</button>
|
||
|
||
{/* Weight / Health Log collapsible */}
|
||
<PuppyLogPanel
|
||
litterId={id}
|
||
puppy={puppy}
|
||
whelpingDate={litter.whelping_date || litter.breeding_date}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Add Puppy Modal */}
|
||
{showAddPuppy && (
|
||
<div className="modal-overlay" onClick={() => setShowAddPuppy(false)}>
|
||
<div className="modal-content" onClick={e => e.stopPropagation()} style={{ maxWidth: '480px' }}>
|
||
<div className="modal-header">
|
||
<h2>Add Puppy to Litter</h2>
|
||
<button className="btn-icon" onClick={() => setShowAddPuppy(false)}><X size={24} /></button>
|
||
</div>
|
||
<div className="modal-body">
|
||
{error && <div className="error" style={{ marginBottom: '1rem' }}>{error}</div>}
|
||
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1.5rem' }}>
|
||
<button className={`btn ${addMode === 'existing' ? 'btn-primary' : 'btn-secondary'}`}
|
||
onClick={() => setAddMode('existing')} style={{ flex: 1 }}>Link Existing Dog</button>
|
||
<button className={`btn ${addMode === 'new' ? 'btn-primary' : 'btn-secondary'}`}
|
||
onClick={() => setAddMode('new')} style={{ flex: 1 }}>Create New Puppy</button>
|
||
</div>
|
||
|
||
{addMode === 'existing' ? (
|
||
<div className="form-group">
|
||
<label className="label">Select Dog</label>
|
||
<select className="input" value={selectedPuppyId}
|
||
onChange={e => setSelectedPuppyId(e.target.value)}>
|
||
<option value="">-- Select a dog --</option>
|
||
{unlinkedDogs.map(d => (
|
||
<option key={d.id} value={d.id}>
|
||
{d.name} ({d.sex}{d.date_of_birth ? `, born ${new Date(d.date_of_birth + 'T00:00:00').toLocaleDateString()}` : ''})
|
||
</option>
|
||
))}
|
||
</select>
|
||
{unlinkedDogs.length === 0 && (
|
||
<p style={{ color: 'var(--text-secondary)', fontSize: '0.85rem', marginTop: '0.5rem' }}>
|
||
No unlinked dogs available. Use "Create New Puppy" instead.
|
||
</p>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div style={{ display: 'grid', gap: '1rem' }}>
|
||
<div className="form-group">
|
||
<label className="label">Puppy Name *</label>
|
||
<input className="input" value={newPuppy.name}
|
||
onChange={e => setNewPuppy(p => ({ ...p, name: e.target.value }))}
|
||
placeholder="e.g. Blue Collar" />
|
||
</div>
|
||
<div className="form-grid">
|
||
<div className="form-group">
|
||
<label className="label">Sex</label>
|
||
<select className="input" value={newPuppy.sex}
|
||
onChange={e => setNewPuppy(p => ({ ...p, sex: e.target.value }))}>
|
||
<option value="male">Male</option>
|
||
<option value="female">Female</option>
|
||
</select>
|
||
</div>
|
||
<div className="form-group">
|
||
<label className="label">Color / Markings</label>
|
||
<input className="input" value={newPuppy.color}
|
||
onChange={e => setNewPuppy(p => ({ ...p, color: e.target.value }))}
|
||
placeholder="e.g. Black & Tan" />
|
||
</div>
|
||
</div>
|
||
<div className="form-group">
|
||
<label className="label">Date of Birth</label>
|
||
<input type="date" className="input" value={newPuppy.dob}
|
||
onChange={e => setNewPuppy(p => ({ ...p, dob: e.target.value }))} />
|
||
{litter.whelping_date && !newPuppy.dob && (
|
||
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginTop: '0.25rem' }}>
|
||
Will default to whelping date: {new Date(litter.whelping_date + 'T00:00:00').toLocaleDateString()}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="modal-footer">
|
||
<button className="btn btn-secondary" onClick={() => setShowAddPuppy(false)} disabled={saving}>Cancel</button>
|
||
<button
|
||
className="btn btn-primary"
|
||
disabled={saving || (addMode === 'existing' && !selectedPuppyId)}
|
||
onClick={addMode === 'existing' ? handleLinkPuppy : handleCreateAndLink}
|
||
>
|
||
{saving ? 'Saving...' : addMode === 'existing' ? 'Link Puppy' : 'Create & Link'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{showEditForm && (
|
||
<LitterForm
|
||
litter={litter}
|
||
onClose={() => setShowEditForm(false)}
|
||
onSave={fetchLitter}
|
||
/>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default LitterDetail
|