feature/dog-forms #1

Merged
jason merged 5 commits from feature/dog-forms into master 2026-03-08 23:10:59 -05:00
5 changed files with 666 additions and 9 deletions

234
ROADMAP.md Normal file
View File

@@ -0,0 +1,234 @@
# BREEDR Development Roadmap
## ✅ Phase 1: Foundation (COMPLETE)
### Infrastructure
- [x] Docker multi-stage build configuration
- [x] SQLite database with automatic initialization
- [x] Express.js API server
- [x] React 18 frontend with Vite
- [x] Git repository structure
### Database Schema
- [x] Dogs table with core fields
- [x] Parents relationship table
- [x] Litters breeding records
- [x] Health records tracking
- [x] Heat cycles management
- [x] Traits genetic mapping
- [x] Indexes and triggers
### API Endpoints
- [x] `/api/dogs` - Full CRUD operations
- [x] `/api/pedigree` - Tree generation and COI calculator
- [x] `/api/litters` - Breeding records
- [x] `/api/health` - Health tracking
- [x] `/api/breeding` - Heat cycles and whelping calculator
- [x] Photo upload with Multer
---
## ✅ Phase 2: Core Functionality (COMPLETE)
### Dog Management
- [x] Add new dogs with full form
- [x] Edit existing dogs
- [x] View dog details
- [x] List all dogs with search/filter
- [x] Upload multiple photos per dog
- [x] Delete photos
- [x] Parent selection (sire/dam)
### User Interface
- [x] Dashboard with statistics
- [x] Dog list with grid view
- [x] Dog detail pages
- [x] Modal forms for add/edit
- [x] Photo management UI
- [x] Search and sex filtering
- [x] Responsive navigation
### Features Implemented
- [x] Photo upload and storage
- [x] Parent-child relationships
- [x] Basic information tracking
- [x] Registration numbers
- [x] Microchip tracking
---
## 🚧 Phase 3: Breeding Tools (IN PROGRESS)
### Priority Features
- [ ] Interactive pedigree tree visualization
- [ ] Integrate React-D3-Tree
- [ ] Show 3-5 generations
- [ ] Click to navigate
- [ ] Zoom and pan controls
- [ ] Trial Pairing Simulator
- [ ] Select sire and dam
- [ ] Display COI calculation
- [ ] Show common ancestors
- [ ] Risk assessment display
- [ ] Heat Cycle Management
- [ ] Add/edit heat cycles
- [ ] Track progesterone levels
- [ ] Calendar view
- [ ] Breeding date suggestions
- [ ] Litter Management
- [ ] Create litter records
- [ ] Link puppies to litter
- [ ] Track whelping details
- [ ] Auto-link parent relationships
---
## 📋 Phase 4: Health & Genetics (PLANNED)
### Health Records
- [ ] Add health test results
- [ ] Vaccination tracking
- [ ] Medical history timeline
- [ ] Document uploads (PDFs, images)
- [ ] Alert for expiring vaccinations
### Genetic Tracking
- [ ] Track inherited traits
- [ ] Color genetics calculator
- [ ] Health clearance status
- [ ] Link traits to ancestors
---
## 📋 Phase 5: Advanced Features (PLANNED)
### Pedigree Tools
- [ ] Reverse pedigree (descendants view)
- [ ] PDF pedigree generation
- [ ] Export to standard formats
- [ ] Print-friendly layouts
- [ ] Multi-generation COI analysis
### Breeding Planning
- [ ] Breeding calendar
- [ ] Heat cycle predictions
- [ ] Expected whelping alerts
- [ ] Breeding history reports
### Search & Analytics
- [ ] Advanced search filters
- [ ] By breed, color, age
- [ ] By health clearances
- [ ] By registration status
- [ ] Statistics dashboard
- [ ] Breeding success rates
- [ ] Average litter sizes
- [ ] Popular pairings
---
## 📋 Phase 6: Polish & Optimization (PLANNED)
### User Experience
- [ ] Loading states for all operations
- [ ] Better error messages
- [ ] Confirmation dialogs
- [ ] Undo functionality
- [ ] Keyboard shortcuts
### Performance
- [ ] Image optimization
- [ ] Lazy loading
- [ ] API caching
- [ ] Database query optimization
### Mobile
- [ ] Touch-friendly interface
- [ ] Mobile photo capture
- [ ] Responsive tables
- [ ] Offline mode
### Documentation
- [ ] User manual
- [ ] API documentation
- [ ] Video tutorials
- [ ] FAQ section
---
## Future Enhancements (BACKLOG)
### Multi-User Support
- [ ] User authentication
- [ ] Role-based permissions
- [ ] Activity logs
- [ ] Shared access
### Integration
- [ ] Import from other systems
- [ ] Export to Excel/CSV
- [ ] Integration with kennel clubs
- [ ] Backup to cloud storage
### Advanced Genetics
- [ ] DNA test result tracking
- [ ] Genetic diversity analysis
- [ ] Breed-specific calculators
- [ ] Health risk predictions
### Kennel Management
- [ ] Breeding contracts
- [ ] Buyer tracking
- [ ] Financial records
- [ ] Stud service management
---
## Current Sprint Focus
### Next Up (Priority)
1. **Interactive Pedigree Visualization**
- Implement React-D3-Tree integration
- Connect to `/api/pedigree/:id` endpoint
- Add zoom/pan controls
- Enable click navigation
2. **Trial Pairing Tool**
- Create pairing form
- Display COI calculation
- Show common ancestors
- Add recommendation system
3. **Litter Management**
- Add litter creation form
- Link puppies to litters
- Display breeding history
- Track whelping outcomes
### Testing Needed
- [ ] Add/edit dog forms
- [ ] Photo upload functionality
- [ ] Search and filtering
- [ ] Parent relationship linking
- [ ] API error handling
### Known Issues
- None currently
---
## How to Contribute
1. Pick a feature from "Priority Features"
2. Create a feature branch: `feature/feature-name`
3. Implement with tests
4. Update this roadmap
5. Submit for review
## Version History
- **v0.2.0** (Current) - Dog CRUD operations complete
- **v0.1.0** - Initial foundation with API and database

View File

@@ -0,0 +1,230 @@
import { useState, useEffect } from 'react'
import { X } from 'lucide-react'
import axios from 'axios'
function DogForm({ dog, onClose, onSave }) {
const [formData, setFormData] = useState({
name: '',
registration_number: '',
breed: '',
sex: 'male',
birth_date: '',
color: '',
microchip: '',
notes: '',
sire_id: '',
dam_id: ''
})
const [dogs, setDogs] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
useEffect(() => {
fetchDogs()
if (dog) {
setFormData({
name: dog.name || '',
registration_number: dog.registration_number || '',
breed: dog.breed || '',
sex: dog.sex || 'male',
birth_date: dog.birth_date || '',
color: dog.color || '',
microchip: dog.microchip || '',
notes: dog.notes || '',
sire_id: dog.sire?.id || '',
dam_id: dog.dam?.id || ''
})
}
}, [dog])
const fetchDogs = async () => {
try {
const res = await axios.get('/api/dogs')
setDogs(res.data)
} catch (error) {
console.error('Error fetching dogs:', error)
}
}
const handleChange = (e) => {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
}
const handleSubmit = async (e) => {
e.preventDefault()
setError('')
setLoading(true)
try {
if (dog) {
// Update existing dog
await axios.put(`/api/dogs/${dog.id}`, formData)
} else {
// Create new dog
await axios.post('/api/dogs', formData)
}
onSave()
onClose()
} catch (error) {
setError(error.response?.data?.error || 'Failed to save dog')
setLoading(false)
}
}
const males = dogs.filter(d => d.sex === 'male' && d.id !== dog?.id)
const females = dogs.filter(d => d.sex === 'female' && d.id !== dog?.id)
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>{dog ? 'Edit Dog' : 'Add New Dog'}</h2>
<button className="btn-icon" onClick={onClose}>
<X size={24} />
</button>
</div>
<form onSubmit={handleSubmit} className="modal-body">
{error && <div className="error">{error}</div>}
<div className="form-grid">
<div className="form-group">
<label className="label">Name *</label>
<input
type="text"
name="name"
className="input"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label className="label">Registration Number</label>
<input
type="text"
name="registration_number"
className="input"
value={formData.registration_number}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label className="label">Breed *</label>
<input
type="text"
name="breed"
className="input"
value={formData.breed}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label className="label">Sex *</label>
<select
name="sex"
className="input"
value={formData.sex}
onChange={handleChange}
required
>
<option value="male">Male</option>
<option value="female">Female</option>
</select>
</div>
<div className="form-group">
<label className="label">Birth Date</label>
<input
type="date"
name="birth_date"
className="input"
value={formData.birth_date}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label className="label">Color</label>
<input
type="text"
name="color"
className="input"
value={formData.color}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label className="label">Microchip Number</label>
<input
type="text"
name="microchip"
className="input"
value={formData.microchip}
onChange={handleChange}
/>
</div>
<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}</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}</option>
))}
</select>
</div>
</div>
<div className="form-group" style={{ marginTop: '1rem' }}>
<label className="label">Notes</label>
<textarea
name="notes"
className="input"
rows="4"
value={formData.notes}
onChange={handleChange}
/>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" onClick={onClose} disabled={loading}>
Cancel
</button>
<button type="submit" className="btn btn-primary" disabled={loading}>
{loading ? 'Saving...' : dog ? 'Update Dog' : 'Add Dog'}
</button>
</div>
</form>
</div>
</div>
)
}
export default DogForm

View File

@@ -78,17 +78,22 @@ code {
text-decoration: none;
}
.btn:hover {
.btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: var(--shadow);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
.btn-primary:hover:not(:disabled) {
background: var(--primary-dark);
}
@@ -107,6 +112,24 @@ code {
color: white;
}
.btn-icon {
background: none;
border: none;
cursor: pointer;
padding: 0.5rem;
border-radius: 0.375rem;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
transition: all 0.2s;
}
.btn-icon:hover {
background: var(--bg-secondary);
color: var(--text);
}
.card {
background: var(--bg);
border-radius: 0.5rem;
@@ -114,20 +137,27 @@ code {
box-shadow: var(--shadow);
}
.input {
.input,
textarea {
width: 100%;
padding: 0.625rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
font-size: 0.875rem;
transition: border-color 0.2s;
font-family: inherit;
}
.input:focus {
.input:focus,
textarea:focus {
outline: none;
border-color: var(--primary);
}
textarea {
resize: vertical;
}
.label {
display: block;
margin-bottom: 0.5rem;
@@ -163,3 +193,70 @@ code {
border-radius: 0.375rem;
margin: 1rem 0;
}
/* Modal styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal-content {
background: var(--bg);
border-radius: 0.5rem;
box-shadow: var(--shadow-lg);
max-width: 800px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid var(--border);
}
.modal-header h2 {
font-size: 1.5rem;
font-weight: 600;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
margin-top: 1.5rem;
}
/* Form styles */
.form-group {
margin-bottom: 1rem;
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
select.input {
background: white;
cursor: pointer;
}

View File

@@ -1,12 +1,16 @@
import { useEffect, useState } from 'react'
import { useEffect, useState, useRef } from 'react'
import { useParams, Link } from 'react-router-dom'
import { Dog, GitBranch, Edit, Trash2 } from 'lucide-react'
import { Dog, GitBranch, Edit, Upload, Trash2 } from 'lucide-react'
import axios from 'axios'
import DogForm from '../components/DogForm'
function DogDetail() {
const { id } = useParams()
const [dog, setDog] = useState(null)
const [loading, setLoading] = useState(true)
const [showEditModal, setShowEditModal] = useState(false)
const [uploading, setUploading] = useState(false)
const fileInputRef = useRef(null)
useEffect(() => {
fetchDog()
@@ -23,6 +27,40 @@ function DogDetail() {
}
}
const handlePhotoUpload = async (e) => {
const file = e.target.files[0]
if (!file) return
setUploading(true)
const formData = new FormData()
formData.append('photo', file)
try {
await axios.post(`/api/dogs/${id}/photos`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
fetchDog()
} catch (error) {
console.error('Error uploading photo:', error)
alert('Failed to upload photo')
} finally {
setUploading(false)
if (fileInputRef.current) fileInputRef.current.value = ''
}
}
const handleDeletePhoto = async (photoIndex) => {
if (!confirm('Delete this photo?')) return
try {
await axios.delete(`/api/dogs/${id}/photos/${photoIndex}`)
fetchDog()
} catch (error) {
console.error('Error deleting photo:', error)
alert('Failed to delete photo')
}
}
if (loading) {
return <div className="container loading">Loading...</div>
}
@@ -40,7 +78,7 @@ function DogDetail() {
<GitBranch size={20} />
View Pedigree
</Link>
<button className="btn btn-secondary">
<button className="btn btn-secondary" onClick={() => setShowEditModal(true)}>
<Edit size={20} />
Edit
</button>
@@ -81,11 +119,46 @@ function DogDetail() {
</div>
<div className="card">
<h2 style={{ marginBottom: '1rem' }}>Photos</h2>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
<h2>Photos</h2>
<button
className="btn btn-primary"
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
>
<Upload size={18} />
{uploading ? 'Uploading...' : 'Upload'}
</button>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handlePhotoUpload}
style={{ display: 'none' }}
/>
</div>
{dog.photo_urls && dog.photo_urls.length > 0 ? (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: '0.5rem' }}>
{dog.photo_urls.map((url, index) => (
<img key={index} src={url} alt={`${dog.name} ${index + 1}`} style={{ width: '100%', aspectRatio: '1', objectFit: 'cover', borderRadius: '0.375rem' }} />
<div key={index} style={{ position: 'relative' }}>
<img
src={url}
alt={`${dog.name} ${index + 1}`}
style={{ width: '100%', aspectRatio: '1', objectFit: 'cover', borderRadius: '0.375rem' }}
/>
<button
className="btn-icon"
onClick={() => handleDeletePhoto(index)}
style={{
position: 'absolute',
top: '0.25rem',
right: '0.25rem',
background: 'rgba(255,255,255,0.9)'
}}
>
<Trash2 size={16} color="var(--danger)" />
</button>
</div>
))}
</div>
) : (
@@ -138,6 +211,17 @@ function DogDetail() {
</div>
</div>
)}
{showEditModal && (
<DogForm
dog={dog}
onClose={() => setShowEditModal(false)}
onSave={() => {
fetchDog()
setShowEditModal(false)
}}
/>
)}
</div>
)
}

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { Dog, Plus, Search } from 'lucide-react'
import axios from 'axios'
import DogForm from '../components/DogForm'
function DogList() {
const [dogs, setDogs] = useState([])
@@ -47,6 +48,10 @@ function DogList() {
setFilteredDogs(filtered)
}
const handleSave = () => {
fetchDogs()
}
if (loading) {
return <div className="container loading">Loading dogs...</div>
}
@@ -111,6 +116,13 @@ function DogList() {
<p style={{ color: 'var(--text-secondary)' }}>No dogs found matching your search criteria.</p>
</div>
)}
{showAddModal && (
<DogForm
onClose={() => setShowAddModal(false)}
onSave={handleSave}
/>
)}
</div>
)
}