feat: add PuppyLogPanel weight/health logs + projected whelping window to LitterDetail
This commit is contained in:
@@ -1,9 +1,254 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
import { ArrowLeft, Plus, X, ExternalLink, Dog } from 'lucide-react'
|
import { ArrowLeft, Plus, X, ExternalLink, Dog, Weight, ChevronDown, ChevronUp, Trash2 } from 'lucide-react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import LitterForm from '../components/LitterForm'
|
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() {
|
function LitterDetail() {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -14,14 +259,11 @@ function LitterDetail() {
|
|||||||
const [allDogs, setAllDogs] = useState([])
|
const [allDogs, setAllDogs] = useState([])
|
||||||
const [selectedPuppyId, setSelectedPuppyId] = useState('')
|
const [selectedPuppyId, setSelectedPuppyId] = useState('')
|
||||||
const [newPuppy, setNewPuppy] = useState({ name: '', sex: 'male', color: '', dob: '' })
|
const [newPuppy, setNewPuppy] = useState({ name: '', sex: 'male', color: '', dob: '' })
|
||||||
const [addMode, setAddMode] = useState('existing') // 'existing' | 'new'
|
const [addMode, setAddMode] = useState('existing')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { fetchLitter(); fetchAllDogs() }, [id])
|
||||||
fetchLitter()
|
|
||||||
fetchAllDogs()
|
|
||||||
}, [id])
|
|
||||||
|
|
||||||
const fetchLitter = async () => {
|
const fetchLitter = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -38,9 +280,7 @@ function LitterDetail() {
|
|||||||
try {
|
try {
|
||||||
const res = await axios.get('/api/dogs')
|
const res = await axios.get('/api/dogs')
|
||||||
setAllDogs(res.data)
|
setAllDogs(res.data)
|
||||||
} catch (err) {
|
} catch (err) { console.error('Error fetching dogs:', err) }
|
||||||
console.error('Error fetching dogs:', err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const unlinkedDogs = allDogs.filter(d => {
|
const unlinkedDogs = allDogs.filter(d => {
|
||||||
@@ -52,47 +292,31 @@ function LitterDetail() {
|
|||||||
|
|
||||||
const handleLinkPuppy = async () => {
|
const handleLinkPuppy = async () => {
|
||||||
if (!selectedPuppyId) return
|
if (!selectedPuppyId) return
|
||||||
setSaving(true)
|
setSaving(true); setError('')
|
||||||
setError('')
|
|
||||||
try {
|
try {
|
||||||
await axios.post(`/api/litters/${id}/puppies/${selectedPuppyId}`)
|
await axios.post(`/api/litters/${id}/puppies/${selectedPuppyId}`)
|
||||||
setSelectedPuppyId('')
|
setSelectedPuppyId(''); setShowAddPuppy(false); fetchLitter()
|
||||||
setShowAddPuppy(false)
|
|
||||||
fetchLitter()
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.response?.data?.error || 'Failed to link puppy')
|
setError(err.response?.data?.error || 'Failed to link puppy')
|
||||||
} finally {
|
} finally { setSaving(false) }
|
||||||
setSaving(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreateAndLink = async () => {
|
const handleCreateAndLink = async () => {
|
||||||
if (!newPuppy.name) {
|
if (!newPuppy.name) { setError('Puppy name is required'); return }
|
||||||
setError('Puppy name is required')
|
setSaving(true); setError('')
|
||||||
return
|
|
||||||
}
|
|
||||||
setSaving(true)
|
|
||||||
setError('')
|
|
||||||
try {
|
try {
|
||||||
const dob = newPuppy.dob || litter.whelping_date || litter.breeding_date
|
const dob = newPuppy.dob || litter.whelping_date || litter.breeding_date
|
||||||
const res = await axios.post('/api/dogs', {
|
const res = await axios.post('/api/dogs', {
|
||||||
name: newPuppy.name,
|
name: newPuppy.name, sex: newPuppy.sex,
|
||||||
sex: newPuppy.sex,
|
color: newPuppy.color, date_of_birth: dob,
|
||||||
color: newPuppy.color,
|
|
||||||
date_of_birth: dob,
|
|
||||||
breed: litter.dam_breed || '',
|
breed: litter.dam_breed || '',
|
||||||
})
|
})
|
||||||
const createdDog = res.data
|
await axios.post(`/api/litters/${id}/puppies/${res.data.id}`)
|
||||||
await axios.post(`/api/litters/${id}/puppies/${createdDog.id}`)
|
|
||||||
setNewPuppy({ name: '', sex: 'male', color: '', dob: '' })
|
setNewPuppy({ name: '', sex: 'male', color: '', dob: '' })
|
||||||
setShowAddPuppy(false)
|
setShowAddPuppy(false); fetchLitter(); fetchAllDogs()
|
||||||
fetchLitter()
|
|
||||||
fetchAllDogs()
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.response?.data?.error || 'Failed to create puppy')
|
setError(err.response?.data?.error || 'Failed to create puppy')
|
||||||
} finally {
|
} finally { setSaving(false) }
|
||||||
setSaving(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUnlinkPuppy = async (puppyId) => {
|
const handleUnlinkPuppy = async (puppyId) => {
|
||||||
@@ -100,9 +324,7 @@ function LitterDetail() {
|
|||||||
try {
|
try {
|
||||||
await axios.delete(`/api/litters/${id}/puppies/${puppyId}`)
|
await axios.delete(`/api/litters/${id}/puppies/${puppyId}`)
|
||||||
fetchLitter()
|
fetchLitter()
|
||||||
} catch (err) {
|
} catch (err) { console.error('Error unlinking puppy:', err) }
|
||||||
console.error('Error unlinking puppy:', err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) return <div className="container loading">Loading litter...</div>
|
if (loading) return <div className="container loading">Loading litter...</div>
|
||||||
@@ -120,13 +342,11 @@ function LitterDetail() {
|
|||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<h1 style={{ margin: 0 }}>🐾 {litter.sire_name} × {litter.dam_name}</h1>
|
<h1 style={{ margin: 0 }}>🐾 {litter.sire_name} × {litter.dam_name}</h1>
|
||||||
<p style={{ color: 'var(--text-secondary)', margin: '0.25rem 0 0' }}>
|
<p style={{ color: 'var(--text-secondary)', margin: '0.25rem 0 0' }}>
|
||||||
Bred: {new Date(litter.breeding_date).toLocaleDateString()}
|
Bred: {new Date(litter.breeding_date + 'T00:00:00').toLocaleDateString()}
|
||||||
{litter.whelping_date && ` • Whelped: ${new Date(litter.whelping_date).toLocaleDateString()}`}
|
{litter.whelping_date && ` • Whelped: ${new Date(litter.whelping_date + 'T00:00:00').toLocaleDateString()}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button className="btn btn-secondary" onClick={() => setShowEditForm(true)}>
|
<button className="btn btn-secondary" onClick={() => setShowEditForm(true)}>Edit Litter</button>
|
||||||
Edit Litter
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats row */}
|
{/* Stats row */}
|
||||||
@@ -155,6 +375,9 @@ function LitterDetail() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Projected whelping window */}
|
||||||
|
<WhelpingBanner breedingDate={litter.breeding_date} whelpingDate={litter.whelping_date} />
|
||||||
|
|
||||||
{/* Notes */}
|
{/* Notes */}
|
||||||
{litter.notes && (
|
{litter.notes && (
|
||||||
<div className="card" style={{ marginBottom: '2rem', padding: '1rem', borderLeft: '3px solid var(--accent)' }}>
|
<div className="card" style={{ marginBottom: '2rem', padding: '1rem', borderLeft: '3px solid var(--accent)' }}>
|
||||||
@@ -177,7 +400,7 @@ function LitterDetail() {
|
|||||||
<p style={{ color: 'var(--text-secondary)' }}>No puppies linked yet. Add puppies to this litter.</p>
|
<p style={{ color: 'var(--text-secondary)' }}>No puppies linked yet. Add puppies to this litter.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: '1rem' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))', gap: '1rem' }}>
|
||||||
{litter.puppies.map(puppy => (
|
{litter.puppies.map(puppy => (
|
||||||
<div key={puppy.id} className="card" style={{ position: 'relative' }}>
|
<div key={puppy.id} className="card" style={{ position: 'relative' }}>
|
||||||
<button
|
<button
|
||||||
@@ -197,7 +420,7 @@ function LitterDetail() {
|
|||||||
</div>
|
</div>
|
||||||
{puppy.date_of_birth && (
|
{puppy.date_of_birth && (
|
||||||
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
|
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
|
||||||
Born: {new Date(puppy.date_of_birth).toLocaleDateString()}
|
Born: {new Date(puppy.date_of_birth + 'T00:00:00').toLocaleDateString()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
@@ -208,6 +431,13 @@ function LitterDetail() {
|
|||||||
<ExternalLink size={12} style={{ marginRight: '0.3rem' }} />
|
<ExternalLink size={12} style={{ marginRight: '0.3rem' }} />
|
||||||
View Profile
|
View Profile
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Weight / Health Log collapsible */}
|
||||||
|
<PuppyLogPanel
|
||||||
|
litterId={id}
|
||||||
|
puppy={puppy}
|
||||||
|
whelpingDate={litter.whelping_date || litter.breeding_date}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -223,37 +453,22 @@ function LitterDetail() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
{error && <div className="error" style={{ marginBottom: '1rem' }}>{error}</div>}
|
{error && <div className="error" style={{ marginBottom: '1rem' }}>{error}</div>}
|
||||||
|
|
||||||
{/* Mode toggle */}
|
|
||||||
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1.5rem' }}>
|
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1.5rem' }}>
|
||||||
<button
|
<button className={`btn ${addMode === 'existing' ? 'btn-primary' : 'btn-secondary'}`}
|
||||||
className={`btn ${addMode === 'existing' ? 'btn-primary' : 'btn-secondary'}`}
|
onClick={() => setAddMode('existing')} style={{ flex: 1 }}>Link Existing Dog</button>
|
||||||
onClick={() => setAddMode('existing')}
|
<button className={`btn ${addMode === 'new' ? 'btn-primary' : 'btn-secondary'}`}
|
||||||
style={{ flex: 1 }}
|
onClick={() => setAddMode('new')} style={{ flex: 1 }}>Create New Puppy</button>
|
||||||
>
|
|
||||||
Link Existing Dog
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`btn ${addMode === 'new' ? 'btn-primary' : 'btn-secondary'}`}
|
|
||||||
onClick={() => setAddMode('new')}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
>
|
|
||||||
Create New Puppy
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{addMode === 'existing' ? (
|
{addMode === 'existing' ? (
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Select Dog</label>
|
<label className="label">Select Dog</label>
|
||||||
<select
|
<select className="input" value={selectedPuppyId}
|
||||||
className="input"
|
onChange={e => setSelectedPuppyId(e.target.value)}>
|
||||||
value={selectedPuppyId}
|
|
||||||
onChange={e => setSelectedPuppyId(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="">-- Select a dog --</option>
|
<option value="">-- Select a dog --</option>
|
||||||
{unlinkedDogs.map(d => (
|
{unlinkedDogs.map(d => (
|
||||||
<option key={d.id} value={d.id}>
|
<option key={d.id} value={d.id}>
|
||||||
{d.name} ({d.sex}{d.date_of_birth ? `, born ${new Date(d.date_of_birth).toLocaleDateString()}` : ''})
|
{d.name} ({d.sex}{d.date_of_birth ? `, born ${new Date(d.date_of_birth + 'T00:00:00').toLocaleDateString()}` : ''})
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -267,46 +482,33 @@ function LitterDetail() {
|
|||||||
<div style={{ display: 'grid', gap: '1rem' }}>
|
<div style={{ display: 'grid', gap: '1rem' }}>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Puppy Name *</label>
|
<label className="label">Puppy Name *</label>
|
||||||
<input
|
<input className="input" value={newPuppy.name}
|
||||||
className="input"
|
|
||||||
value={newPuppy.name}
|
|
||||||
onChange={e => setNewPuppy(p => ({ ...p, name: e.target.value }))}
|
onChange={e => setNewPuppy(p => ({ ...p, name: e.target.value }))}
|
||||||
placeholder="e.g. Blue Collar"
|
placeholder="e.g. Blue Collar" />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-grid">
|
<div className="form-grid">
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Sex</label>
|
<label className="label">Sex</label>
|
||||||
<select
|
<select className="input" value={newPuppy.sex}
|
||||||
className="input"
|
onChange={e => setNewPuppy(p => ({ ...p, sex: e.target.value }))}>
|
||||||
value={newPuppy.sex}
|
|
||||||
onChange={e => setNewPuppy(p => ({ ...p, sex: e.target.value }))}
|
|
||||||
>
|
|
||||||
<option value="male">Male</option>
|
<option value="male">Male</option>
|
||||||
<option value="female">Female</option>
|
<option value="female">Female</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Color / Markings</label>
|
<label className="label">Color / Markings</label>
|
||||||
<input
|
<input className="input" value={newPuppy.color}
|
||||||
className="input"
|
|
||||||
value={newPuppy.color}
|
|
||||||
onChange={e => setNewPuppy(p => ({ ...p, color: e.target.value }))}
|
onChange={e => setNewPuppy(p => ({ ...p, color: e.target.value }))}
|
||||||
placeholder="e.g. Black & Tan"
|
placeholder="e.g. Black & Tan" />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label className="label">Date of Birth</label>
|
<label className="label">Date of Birth</label>
|
||||||
<input
|
<input type="date" className="input" value={newPuppy.dob}
|
||||||
type="date"
|
onChange={e => setNewPuppy(p => ({ ...p, dob: e.target.value }))} />
|
||||||
className="input"
|
|
||||||
value={newPuppy.dob}
|
|
||||||
onChange={e => setNewPuppy(p => ({ ...p, dob: e.target.value }))}
|
|
||||||
/>
|
|
||||||
{litter.whelping_date && !newPuppy.dob && (
|
{litter.whelping_date && !newPuppy.dob && (
|
||||||
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginTop: '0.25rem' }}>
|
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)', marginTop: '0.25rem' }}>
|
||||||
Will default to whelping date: {new Date(litter.whelping_date).toLocaleDateString()}
|
Will default to whelping date: {new Date(litter.whelping_date + 'T00:00:00').toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -314,9 +516,7 @@ function LitterDetail() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
<button className="btn btn-secondary" onClick={() => setShowAddPuppy(false)} disabled={saving}>
|
<button className="btn btn-secondary" onClick={() => setShowAddPuppy(false)} disabled={saving}>Cancel</button>
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
disabled={saving || (addMode === 'existing' && !selectedPuppyId)}
|
disabled={saving || (addMode === 'existing' && !selectedPuppyId)}
|
||||||
@@ -329,7 +529,6 @@ function LitterDetail() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Edit Litter Modal */}
|
|
||||||
{showEditForm && (
|
{showEditForm && (
|
||||||
<LitterForm
|
<LitterForm
|
||||||
litter={litter}
|
litter={litter}
|
||||||
|
|||||||
Reference in New Issue
Block a user