fix: wire external dogs end-to-end (modal, form flag, pairing simulator) #49

Merged
jason merged 4 commits from fix/external-dogs-wiring into feature/external-dogs 2026-03-11 01:01:19 -05:00
Showing only changes of commit 8cb4c773fd - Show all commits

View File

@@ -1,8 +1,8 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { X, Award } from 'lucide-react' import { X, Award, ExternalLink } from 'lucide-react'
import axios from 'axios' import axios from 'axios'
function DogForm({ dog, onClose, onSave }) { function DogForm({ dog, onClose, onSave, isExternal = false }) {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
registration_number: '', registration_number: '',
@@ -16,6 +16,7 @@ function DogForm({ dog, onClose, onSave }) {
dam_id: null, dam_id: null,
litter_id: null, litter_id: null,
is_champion: false, is_champion: false,
is_external: isExternal ? 1 : 0,
}) })
const [dogs, setDogs] = useState([]) const [dogs, setDogs] = useState([])
const [litters, setLitters] = useState([]) const [litters, setLitters] = useState([])
@@ -24,9 +25,14 @@ function DogForm({ dog, onClose, onSave }) {
const [useManualParents, setUseManualParents] = useState(true) const [useManualParents, setUseManualParents] = useState(true)
const [littersAvailable, setLittersAvailable] = useState(false) const [littersAvailable, setLittersAvailable] = useState(false)
// Derive effective external state (editing an existing external dog or explicitly flagged)
const effectiveExternal = isExternal || (dog && dog.is_external)
useEffect(() => { useEffect(() => {
fetchDogs() if (!effectiveExternal) {
fetchLitters() fetchDogs()
fetchLitters()
}
if (dog) { if (dog) {
setFormData({ setFormData({
name: dog.name || '', name: dog.name || '',
@@ -41,6 +47,7 @@ function DogForm({ dog, onClose, onSave }) {
dam_id: dog.dam?.id || null, dam_id: dog.dam?.id || null,
litter_id: dog.litter_id || null, litter_id: dog.litter_id || null,
is_champion: !!dog.is_champion, is_champion: !!dog.is_champion,
is_external: dog.is_external ?? (isExternal ? 1 : 0),
}) })
setUseManualParents(!dog.litter_id) setUseManualParents(!dog.litter_id)
} }
@@ -104,9 +111,10 @@ function DogForm({ dog, onClose, onSave }) {
const submitData = { const submitData = {
...formData, ...formData,
is_champion: formData.is_champion ? 1 : 0, is_champion: formData.is_champion ? 1 : 0,
sire_id: formData.sire_id || null, is_external: effectiveExternal ? 1 : 0,
dam_id: formData.dam_id || null, sire_id: effectiveExternal ? null : (formData.sire_id || null),
litter_id: useManualParents ? null : (formData.litter_id || null), dam_id: effectiveExternal ? null : (formData.dam_id || null),
litter_id: (effectiveExternal || useManualParents) ? null : (formData.litter_id || null),
registration_number: formData.registration_number || null, registration_number: formData.registration_number || null,
birth_date: formData.birth_date || null, birth_date: formData.birth_date || null,
color: formData.color || null, color: formData.color || null,
@@ -133,10 +141,31 @@ function DogForm({ dog, onClose, onSave }) {
<div className="modal-overlay" onClick={onClose}> <div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}> <div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header"> <div className="modal-header">
<h2>{dog ? 'Edit Dog' : 'Add New Dog'}</h2> <h2>
{effectiveExternal && <ExternalLink size={18} style={{ marginRight: '0.4rem', verticalAlign: 'middle', color: 'var(--text-muted)' }} />}
{dog ? 'Edit Dog' : effectiveExternal ? 'Add External Dog' : 'Add New Dog'}
</h2>
<button className="btn-icon" onClick={onClose}><X size={24} /></button> <button className="btn-icon" onClick={onClose}><X size={24} /></button>
</div> </div>
{effectiveExternal && (
<div style={{
margin: '0 0 1rem',
padding: '0.6rem 1rem',
background: 'rgba(99,102,241,0.08)',
border: '1px solid rgba(99,102,241,0.25)',
borderRadius: 'var(--radius)',
fontSize: '0.875rem',
color: 'var(--text-secondary)',
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
}}>
<ExternalLink size={14} />
External dog not part of your kennel roster. Litter and parent fields are not applicable.
</div>
)}
<form onSubmit={handleSubmit} className="modal-body"> <form onSubmit={handleSubmit} className="modal-body">
{error && <div className="error">{error}</div>} {error && <div className="error">{error}</div>}
@@ -221,69 +250,71 @@ function DogForm({ dog, onClose, onSave }) {
</div> </div>
</div> </div>
{/* Parent Section */} {/* Parent Section — hidden for external dogs */}
<div style={{ {!effectiveExternal && (
marginTop: '1.5rem', padding: '1rem', <div style={{
background: 'rgba(194, 134, 42, 0.04)', marginTop: '1.5rem', padding: '1rem',
borderRadius: '8px', background: 'rgba(194, 134, 42, 0.04)',
border: '1px solid rgba(194, 134, 42, 0.15)' borderRadius: '8px',
}}> border: '1px solid rgba(194, 134, 42, 0.15)'
<label className="label" style={{ marginBottom: '0.75rem', display: 'block', fontWeight: '600' }}>Parent Information</label> }}>
<label className="label" style={{ marginBottom: '0.75rem', display: 'block', fontWeight: '600' }}>Parent Information</label>
{littersAvailable && ( {littersAvailable && (
<div style={{ display: 'flex', gap: '1.5rem', marginBottom: '1rem', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '1.5rem', marginBottom: '1rem', flexWrap: 'wrap' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.95rem' }}> <label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.95rem' }}>
<input type="radio" name="parentMode" checked={!useManualParents} <input type="radio" name="parentMode" checked={!useManualParents}
onChange={() => setUseManualParents(false)} style={{ width: '16px', height: '16px' }} /> onChange={() => setUseManualParents(false)} style={{ width: '16px', height: '16px' }} />
<span>Link to Litter</span> <span>Link to Litter</span>
</label> </label>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.95rem' }}> <label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer', fontSize: '0.95rem' }}>
<input type="radio" name="parentMode" checked={useManualParents} <input type="radio" name="parentMode" checked={useManualParents}
onChange={() => setUseManualParents(true)} style={{ width: '16px', height: '16px' }} /> onChange={() => setUseManualParents(true)} style={{ width: '16px', height: '16px' }} />
<span>Manual Parent Selection</span> <span>Manual Parent Selection</span>
</label> </label>
</div> </div>
)} )}
{!useManualParents && littersAvailable ? ( {!useManualParents && littersAvailable ? (
<div className="form-group" style={{ marginTop: '0.5rem' }}> <div className="form-group" style={{ marginTop: '0.5rem' }}>
<label className="label">Select Litter</label> <label className="label">Select Litter</label>
<select name="litter_id" className="input" <select name="litter_id" className="input"
value={formData.litter_id || ''} onChange={handleChange}> value={formData.litter_id || ''} onChange={handleChange}>
<option value="">No Litter</option> <option value="">No Litter</option>
{litters.map(l => ( {litters.map(l => (
<option key={l.id} value={l.id}> <option key={l.id} value={l.id}>
{l.sire_name} x {l.dam_name} - {new Date(l.breeding_date).toLocaleDateString()} {l.sire_name} x {l.dam_name} - {new Date(l.breeding_date).toLocaleDateString()}
</option> </option>
))} ))}
</select> </select>
{formData.litter_id && ( {formData.litter_id && (
<div style={{ marginTop: '0.5rem', fontSize: '0.875rem', color: 'var(--primary)', fontStyle: 'italic' }}> <div style={{ marginTop: '0.5rem', fontSize: '0.875rem', color: 'var(--primary)', fontStyle: 'italic' }}>
Parents will be automatically set from the selected litter Parents will be automatically set from the selected litter
</div>
)}
</div>
) : (
<div className="form-grid" style={{ marginTop: '0.5rem' }}>
<div className="form-group">
<label className="label">Sire (Father)</label>
<select name="sire_id" className="input"
value={formData.sire_id || ''} onChange={handleChange}>
<option value="">Unknown</option>
{males.map(d => <option key={d.id} value={d.id}>{d.name}{d.is_champion ? ' ✪' : ''}</option>)}
</select>
</div>
<div className="form-group">
<label className="label">Dam (Mother)</label>
<select name="dam_id" className="input"
value={formData.dam_id || ''} onChange={handleChange}>
<option value="">Unknown</option>
{females.map(d => <option key={d.id} value={d.id}>{d.name}{d.is_champion ? ' ✪' : ''}</option>)}
</select>
</div> </div>
)}
</div>
) : (
<div className="form-grid" style={{ marginTop: '0.5rem' }}>
<div className="form-group">
<label className="label">Sire (Father)</label>
<select name="sire_id" className="input"
value={formData.sire_id || ''} onChange={handleChange}>
<option value="">Unknown</option>
{males.map(d => <option key={d.id} value={d.id}>{d.name}{d.is_champion ? ' ✪' : ''}</option>)}
</select>
</div> </div>
<div className="form-group"> )}
<label className="label">Dam (Mother)</label> </div>
<select name="dam_id" className="input" )}
value={formData.dam_id || ''} onChange={handleChange}>
<option value="">Unknown</option>
{females.map(d => <option key={d.id} value={d.id}>{d.name}{d.is_champion ? ' ✪' : ''}</option>)}
</select>
</div>
</div>
)}
</div>
<div className="form-group" style={{ marginTop: '1rem' }}> <div className="form-group" style={{ marginTop: '1rem' }}>
<label className="label">Notes</label> <label className="label">Notes</label>
@@ -294,7 +325,7 @@ function DogForm({ dog, onClose, onSave }) {
<div className="modal-footer"> <div className="modal-footer">
<button type="button" className="btn btn-secondary" onClick={onClose} disabled={loading}>Cancel</button> <button type="button" className="btn btn-secondary" onClick={onClose} disabled={loading}>Cancel</button>
<button type="submit" className="btn btn-primary" disabled={loading}> <button type="submit" className="btn btn-primary" disabled={loading}>
{loading ? 'Saving...' : dog ? 'Update Dog' : 'Add Dog'} {loading ? 'Saving...' : dog ? 'Update Dog' : effectiveExternal ? 'Add External Dog' : 'Add Dog'}
</button> </button>
</div> </div>
</form> </form>