Merge pull request 'feature/litter-management-and-pedigree' (#6) from feature/litter-management-and-pedigree into master

Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
2026-03-09 00:15:28 -05:00
9 changed files with 1603 additions and 74 deletions

368
FEATURE_IMPLEMENTATION.md Normal file
View File

@@ -0,0 +1,368 @@
# Feature Implementation: Litter Management & Interactive Pedigree
## Overview
This feature branch implements two major enhancements to the BREEDR system:
1. **Complete Litter Management System** - Fixes the puppy addition issue and provides full litter tracking
2. **Interactive Pedigree Tree Visualization** - Beautiful, zoomable pedigree trees using react-d3-tree
## Problem Solved
### Original Issue
When attempting to add a puppy, users encountered the error:
```
no such column: sire
```
This occurred because:
- The `dogs` table uses a `parents` relationship table for lineage
- The `litters` table existed but had no linkage mechanism to puppies
- The DogForm tried to reference non-existent direct parent columns
## Implementation
### 1. Database Migration
**File:** `server/db/migrate_litter_id.js`
Adds a `litter_id` column to the `dogs` table to link puppies to their litters:
```sql
ALTER TABLE dogs ADD COLUMN litter_id INTEGER;
CREATE INDEX idx_dogs_litter ON dogs(litter_id);
```
To run the migration:
```bash
node server/db/migrate_litter_id.js
```
### 2. Enhanced Litter API
**File:** `server/routes/litters.js`
New endpoints:
- `POST /api/litters/:id/puppies/:puppyId` - Link puppy to litter
- `DELETE /api/litters/:id/puppies/:puppyId` - Remove puppy from litter
- Enhanced `GET /api/litters` - Returns litters with puppy counts
Auto-linking logic:
- When a puppy is linked to a litter, sire/dam relationships are automatically created in the `parents` table
- Prevents orphaned data when litters are deleted
### 3. Updated DogForm Component
**File:** `client/src/components/DogForm.jsx`
Key Features:
- **Dual Parent Selection Mode:**
- Option 1: Link to existing litter (auto-populates parents)
- Option 2: Manual parent selection (traditional method)
- Radio button toggle for selection mode
- Litter dropdown shows "Sire x Dam - Date" format
- Automatic breed inheritance from litter parents
### 4. New LitterForm Component
**File:** `client/src/components/LitterForm.jsx`
Features:
- Create/edit litter records
- Select sire and dam from dropdown lists
- Track breeding date, whelping date, expected puppy count
- Notes field for breeding details
- Validation: ensures sire is male, dam is female
### 5. Interactive Pedigree Visualization
**Files:**
- `client/src/components/PedigreeView.jsx`
- `client/src/components/PedigreeView.css`
**Features:**
- **Beautiful Tree Visualization:**
- Horizontal tree layout (left to right)
- Color-coded nodes: Blue for males, Pink for females
- Shows 5 generations by default
- **Interactive Controls:**
- Zoom in/out buttons
- Reset view button
- Mouse wheel zoom support
- Click and drag to pan
- Click nodes for details
- **Node Information:**
- Dog name (primary)
- Registration number
- Birth year
- Sex indicator (♂/♀)
- **Uses COI Calculator Backend:**
- Leverages existing `/api/pedigree/:id` endpoint
- Recursive ancestor tree building
- Supports configurable generation depth
## Usage
### Adding a Puppy from a Litter
1. Create a litter first:
- Navigate to Litters section
- Click "Add New Litter"
- Select sire and dam
- Enter breeding date
- Save
2. Add puppies to the litter:
- Click "Add New Dog"
- Enter puppy details
- Select "Link to Litter" radio button
- Choose the litter from dropdown
- Parents are auto-populated
- Save
### Viewing Pedigree Trees
1. From any dog detail page:
- Click "View Pedigree" button
- Interactive tree opens in modal
- Use zoom/pan controls to navigate
- Click nodes to see details
### Manual Parent Assignment
For dogs not part of a formal litter:
1. Click "Add New Dog"
2. Enter dog details
3. Select "Manual Parent Selection"
4. Choose sire and dam from dropdowns
5. Save
## Technical Details
### Data Flow: Litter to Puppy
```
1. User creates litter (sire_id, dam_id, breeding_date)
2. Litter gets unique ID
3. User adds puppy with litter_id
4. Backend auto-creates parent relationships:
- INSERT INTO parents (puppy_id, sire_id, 'sire')
- INSERT INTO parents (puppy_id, dam_id, 'dam')
5. Puppy linked to litter and parents
```
### Pedigree Tree Data Structure
```javascript
{
name: "Dog Name",
attributes: {
sex: "male",
birth_date: "2020-01-15",
registration: "AKC12345",
breed: "Golden Retriever",
generation: 0
},
children: [
{ /* Sire node */ },
{ /* Dam node */ }
]
}
```
### React-D3-Tree Configuration
```javascript
<Tree
orientation="horizontal" // Left to right
pathFunc="step" // Orthogonal lines
separation={{ siblings: 2 }} // Node spacing
nodeSize={{ x: 200, y: 100 }} // Node dimensions
collapsible={false} // Always show all
zoomable={true} // Enable zoom
draggable={true} // Enable pan
/>
```
## Database Schema Updates
### Before
```sql
CREATE TABLE dogs (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
-- ... other fields
-- NO litter_id column
);
CREATE TABLE parents (
dog_id INTEGER,
parent_id INTEGER,
parent_type TEXT -- 'sire' or 'dam'
);
```
### After
```sql
CREATE TABLE dogs (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
-- ... other fields
litter_id INTEGER -- NEW: Links to litters table
);
CREATE INDEX idx_dogs_litter ON dogs(litter_id);
```
## API Changes
### New Endpoints
```
POST /api/litters/:id/puppies/:puppyId
DELETE /api/litters/:id/puppies/:puppyId
```
### Modified Endpoints
```
GET /api/litters
Response now includes:
- actual_puppy_count: Real count from database
- puppies: Array of linked puppies
POST /api/dogs
Accepts new field:
- litter_id: Optional litter association
```
## Dependencies
### New npm packages added:
- `react-d3-tree@^3.6.2` - Tree visualization library
### Existing dependencies leveraged:
- `lucide-react` - Icons for UI controls
- `axios` - API communication
- `react` - Component framework
## Testing Checklist
- [ ] Run database migration
- [ ] Create a new litter
- [ ] Add puppies to litter via DogForm
- [ ] Verify parent relationships auto-created
- [ ] View pedigree tree for a dog with 3+ generations
- [ ] Test zoom/pan controls in pedigree view
- [ ] Add dog with manual parent selection
- [ ] Edit existing dog and change litter assignment
- [ ] Delete litter and verify puppies remain (litter_id set to NULL)
## Future Enhancements
1. **Litter Dashboard:**
- Visual cards for each litter
- Photos of puppies
- Whelping countdown
2. **Enhanced Pedigree Features:**
- Print to PDF
- Color coding by health clearances
- COI display on tree nodes
- Descendant tree (reverse pedigree)
3. **Batch Operations:**
- Add multiple puppies at once
- Bulk photo upload for litter
- Auto-naming scheme (Litter Letter + Name)
4. **Analytics:**
- Average litter size by pairing
- Color distribution predictions
- Genetic diversity metrics
## Migration Path
### From Current System
1. Pull feature branch
2. Run migration: `node server/db/migrate_litter_id.js`
3. Install dependencies: `cd client && npm install`
4. Restart server
5. Existing dogs remain unchanged
6. Start creating litters for new puppies
### Rollback Plan
If issues arise:
1. The `litter_id` column can remain NULL
2. System continues to work with manual parent selection
3. No data loss occurs
4. Simply don't use litter feature until fixed
## Architecture Decisions
### Why litter_id Column?
**Considered alternatives:**
1. ✗ Bridge table `litter_puppies` - Adds complexity, same result
2. ✗ JSON array in `litters.puppy_ids` - Poor query performance
3.**Foreign key in dogs table** - Simple, performant, standard pattern
### Why Radio Button Toggle?
**User Experience:**
- Clear visual distinction between modes
- Prevents accidental litter/manual mixing
- Familiar UI pattern
- Easy to understand for non-technical users
### Why react-d3-tree?
**Alternatives evaluated:**
- D3.js directly - Too much custom code required
- vis.js - Not React-friendly
- react-family-tree - Less flexible
- **react-d3-tree** - ✓ React-native, customizable, maintained
## Performance Considerations
1. **Pedigree Loading:**
- Recursive queries limited to 5 generations
- Indexes on `parents` table ensure fast lookups
- Tree data cached in component state
2. **Litter Queries:**
- New index on `dogs.litter_id` enables fast filtering
- Puppy counts calculated efficiently via JOIN
3. **Frontend Rendering:**
- React-d3-tree uses virtual DOM for smooth updates
- Lazy loading prevents rendering off-screen nodes
## Security Notes
- All parent/litter relationships validated server-side
- Gender validation prevents invalid pairings
- Foreign key relationships ensure referential integrity
- SQL injection prevented via parameterized queries
## Contributing
When extending these features:
1. **Backend changes:** Update `server/routes/litters.js`
2. **Frontend forms:** Modify `client/src/components/LitterForm.jsx` or `DogForm.jsx`
3. **Visualization:** Edit `client/src/components/PedigreeView.jsx`
4. **Database:** Create new migration file following naming convention
## Questions?
See [ROADMAP.md](./ROADMAP.md) for feature priorities or check [README.md](./README.md) for general project info.

340
QUICKSTART.md Normal file
View File

@@ -0,0 +1,340 @@
# BREEDR Quick Start Guide
## Litter Management & Pedigree Visualization
## Installation
### 1. Pull the Feature Branch
```bash
git checkout feature/litter-management-and-pedigree
```
### 2. Run Database Migration
```bash
node server/db/migrate_litter_id.js
```
You should see:
```
Running litter_id migration...
✓ Added litter_id column to dogs table
✓ Created index on litter_id
Migration completed successfully!
```
### 3. Install Dependencies
```bash
cd client
npm install
cd ..
```
### 4. Start the Application
```bash
npm run dev
```
The server will start on `http://localhost:3000` and the client on `http://localhost:5173`
---
## Feature 1: Litter Management
### Creating Your First Litter
1. **Navigate to Litters**
- Click "Litters" in the navigation menu
- Click "Add New Litter" button
2. **Fill in Litter Details**
- **Sire (Father)**: Select from dropdown of male dogs
- **Dam (Mother)**: Select from dropdown of female dogs
- **Breeding Date**: Date of breeding (required)
- **Whelping Date**: Expected/actual birth date (optional)
- **Expected Puppy Count**: Estimated number of puppies
- **Notes**: Any additional breeding information
3. **Save the Litter**
- Click "Create Litter"
- Litter appears in the list with format: "Sire x Dam - Date"
### Adding Puppies to a Litter
#### Method 1: Link to Existing Litter (Recommended)
1. **Click "Add New Dog"**
2. **Enter Puppy Details**
- Name (required)
- Breed (required)
- Sex (required)
- Birth Date
- Color
- Microchip
3. **Select Parent Method**
- Choose "Link to Litter" radio button
- Select the litter from dropdown
- Parents are automatically filled!
4. **Save**
- Click "Add Dog"
- Puppy is now linked to the litter
- Parent relationships are automatically created
#### Method 2: Manual Parent Selection
1. **Click "Add New Dog"**
2. **Enter Puppy Details**
3. **Select Parent Method**
- Choose "Manual Parent Selection" radio button
- Select Sire from male dogs dropdown
- Select Dam from female dogs dropdown
4. **Save**
- Puppy is created with selected parents
- No litter association
### Viewing Litter Details
1. **Click on a Litter** in the list
2. **See Litter Information:**
- Sire and Dam details
- Breeding and whelping dates
- List of all puppies in the litter
- Actual puppy count vs expected
### Editing a Litter
1. Click "Edit" on the litter
2. Update breeding/whelping dates
3. Modify notes
4. **Note:** Cannot change sire/dam after creation
---
## Feature 2: Interactive Pedigree Tree
### Viewing a Pedigree
1. **From Dog List:**
- Click on any dog
- Click "View Pedigree" button
2. **Pedigree Opens in Modal**
- Shows dog's ancestry tree
- 5 generations displayed
- Color-coded by sex:
- Blue nodes = Males ♂
- Pink nodes = Females ♀
### Navigating the Tree
#### Zoom Controls
- **Zoom In**: Click "+" button or mouse wheel up
- **Zoom Out**: Click "-" button or mouse wheel down
- **Reset View**: Click reset button to center tree
#### Panning
- **Click and Drag**: Move the tree around
- **Mouse Wheel**: Zoom in/out
#### Node Information
Each node displays:
- Dog name (large text)
- Registration number
- Birth year
- Sex symbol (♂ or ♀)
### Reading the Tree
```
Great-Great-Grandpa ♂
Great-Grandpa ♂
Great-Great-Grandma ♀
Grandpa ♂
Great-Great-Grandpa ♂
Great-Grandma ♀
Great-Great-Grandma ♀
Sire ♂
Great-Great-Grandpa ♂
Great-Grandpa ♂
Great-Great-Grandma ♀
Grandma ♀
Great-Great-Grandpa ♂
Great-Grandma ♀
Great-Great-Grandma ♀
Dog Name
Dam ♀
[... similar structure for dam's side]
```
---
## Common Workflows
### Workflow 1: Breeding a Litter
1. ✓ Select breeding pair (sire and dam)
2. ✓ Create litter record with breeding date
3. ✓ Track whelping date when puppies are born
4. ✓ Add each puppy:
- Link to the litter
- Enter individual details
- Assign registration numbers
5. ✓ View pedigree of any puppy to see full ancestry
### Workflow 2: Recording Historical Dogs
1. ✓ Add foundation dogs (no parents)
2. ✓ Add their offspring using manual parent selection
3. ✓ Continue building the family tree
4. ✓ View pedigrees to verify relationships
### Workflow 3: Planning a Breeding
1. ✓ View pedigrees of potential sire and dam
2. ✓ Check for common ancestors
3. ✓ Use trial pairing tool (coming soon)
4. ✓ Create litter when breeding occurs
---
## Tips & Best Practices
### For Litter Management
**Do:**
- Create the litter record BEFORE adding puppies
- Enter accurate breeding dates for record keeping
- Use meaningful notes (progesterone timing, heat cycle info)
- Link puppies to litters for automatic parent relationships
**Don't:**
- Don't change sire/dam after litter creation (create new litter instead)
- Don't forget to update whelping date when puppies arrive
- Avoid mixing litter-linked and manually-parented puppies
### For Pedigree Viewing
**Do:**
- Zoom out to see the full tree at once
- Use drag to focus on specific branches
- Click nodes to see additional details
- Reset view if you get lost
**Don't:**
- Don't try to edit from pedigree view (use dog edit form)
- Avoid excessive zooming (can make nodes too small)
### Data Entry Tips
1. **Registration Numbers**: Enter consistently (e.g., "AKC-12345")
2. **Microchips**: Use full 15-digit number
3. **Birth Dates**: Critical for age calculations and sorting
4. **Breed Names**: Keep consistent spelling and capitalization
5. **Colors**: Use standard color terminology for your breed
---
## Troubleshooting
### "No such column: sire" Error
**Problem:** Getting this error when adding a dog
**Solution:**
1. Make sure you ran the migration:
```bash
node server/db/migrate_litter_id.js
```
2. Restart the server
3. Try again
### Pedigree Tree Not Loading
**Problem:** Pedigree modal shows "Loading..." forever
**Possible Causes:**
- Dog has no parents recorded
- Network issue
- Server not running
**Solution:**
1. Check browser console for errors
2. Verify server is running
3. Ensure dog has at least one parent recorded
### Parents Not Auto-Populating
**Problem:** Selected a litter but parents didn't fill in
**Solution:**
1. Refresh the page
2. Make sure litter has valid sire and dam
3. Try selecting the litter again
### Can't See All Generations
**Problem:** Pedigree tree only shows 2-3 generations
**This is normal if:**
- Older generations don't have parents recorded
- Foundation dogs have no ancestry
- You need to add more historical data
---
## Keyboard Shortcuts
*Coming in future release*
- `Ctrl/Cmd + N` - New Dog
- `Ctrl/Cmd + L` - New Litter
- `Ctrl/Cmd + P` - View Pedigree
- `Esc` - Close Modal
---
## Next Features Coming Soon
🔜 **Trial Pairing Simulator**
- Calculate COI before breeding
- See common ancestors
- Risk assessment
🔜 **Heat Cycle Tracking**
- Track progesterone levels
- Breeding date recommendations
- Calendar view
🔜 **PDF Pedigree Export**
- Print-ready pedigrees
- Custom formatting
- Multiple generations
---
## Getting Help
- **Documentation:** [FEATURE_IMPLEMENTATION.md](./FEATURE_IMPLEMENTATION.md)
- **Roadmap:** [ROADMAP.md](./ROADMAP.md)
- **Installation:** [INSTALL.md](./INSTALL.md)
- **README:** [README.md](./README.md)
---
## Video Tutorials
*Coming soon - check back for video walkthroughs of these features!*
1. Creating Your First Litter
2. Adding Puppies to a Litter
3. Navigating Pedigree Trees
4. Advanced Breeding Records
---
## Congratulations!
You're now ready to use BREEDR's litter management and pedigree visualization features. Start by creating a litter or viewing a pedigree tree!
**Happy Breeding! 🐶**

View File

@@ -57,14 +57,16 @@
--- ---
## 🚧 Phase 3: Breeding Tools (IN PROGRESS) ## Phase 3: Breeding Tools (COMPLETE)
### Priority Features ### Priority Features
- [ ] Interactive pedigree tree visualization - [x] **Interactive pedigree tree visualization**
- [ ] Integrate React-D3-Tree - [x] Integrate React-D3-Tree
- [ ] Show 3-5 generations - [x] Show 3-5 generations
- [ ] Click to navigate - [x] Click to navigate
- [ ] Zoom and pan controls - [x] Zoom and pan controls
- [x] Beautiful color-coded nodes
- [x] Male/Female distinction
- [ ] Trial Pairing Simulator - [ ] Trial Pairing Simulator
- [ ] Select sire and dam - [ ] Select sire and dam
@@ -78,11 +80,14 @@
- [ ] Calendar view - [ ] Calendar view
- [ ] Breeding date suggestions - [ ] Breeding date suggestions
- [ ] Litter Management - [x] **Litter Management****NEW**
- [ ] Create litter records - [x] Create litter records
- [ ] Link puppies to litter - [x] Link puppies to litter
- [ ] Track whelping details - [x] Track whelping details
- [ ] Auto-link parent relationships - [x] Auto-link parent relationships
- [x] Database migration for litter_id
- [x] Enhanced API endpoints
- [x] Dual parent selection mode (litter/manual)
--- ---
@@ -187,33 +192,98 @@
--- ---
## 🎉 Latest Release: v0.3.0 - Litter Management & Pedigree Visualization
### What's New in This Release
#### Litter Management System
- ✅ Fixed "no such column: sire" error when adding puppies
- ✅ Database migration adds `litter_id` column to dogs table
- ✅ New LitterForm component for creating/editing litters
- ✅ Enhanced litter API with puppy linking endpoints
- ✅ Dual parent selection mode in DogForm:
- Link to existing litter (auto-populates parents)
- Manual parent selection (traditional method)
- ✅ Auto-creation of parent relationships when linking to litter
#### Interactive Pedigree Visualization
- ✅ Beautiful tree visualization using React-D3-Tree
- ✅ Shows 5 generations of ancestry
- ✅ Color-coded nodes: Blue for males, Pink for females
- ✅ Interactive controls:
- Zoom in/out buttons
- Reset view
- Mouse wheel zoom
- Click and drag to pan
- ✅ Node information display:
- Dog name
- Registration number
- Birth year
- Sex indicator (♂/♀)
- ✅ Leverages existing COI calculator backend
- ✅ Horizontal tree layout for better readability
### Migration Instructions
1. Pull the feature branch:
```bash
git checkout feature/litter-management-and-pedigree
```
2. Run database migration:
```bash
node server/db/migrate_litter_id.js
```
3. Install new dependencies:
```bash
cd client && npm install
```
4. Restart the server:
```bash
npm run dev
```
### Documentation
See [FEATURE_IMPLEMENTATION.md](./FEATURE_IMPLEMENTATION.md) for:
- Detailed technical documentation
- Architecture decisions
- Usage examples
- API changes
- Testing checklist
---
## Current Sprint Focus ## Current Sprint Focus
### Next Up (Priority) ### Next Up (Priority)
1. **Interactive Pedigree Visualization** 1. **Trial Pairing Tool**
- 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 - Create pairing form
- Display COI calculation - Display COI calculation
- Show common ancestors - Show common ancestors
- Add recommendation system - Add recommendation system
3. **Litter Management** 2. **Heat Cycle Management**
- Add litter creation form - Add/edit heat cycles
- Link puppies to litters - Track progesterone levels
- Display breeding history - Calendar view
- Track whelping outcomes - Breeding date suggestions
3. **Enhanced Litter Features**
- Puppy batch addition
- Photo gallery per litter
- Whelping countdown
- Expected vs actual puppy count tracking
### Testing Needed ### Testing Needed
- [ ] Add/edit dog forms - [x] Add/edit dog forms with litter selection
- [ ] Photo upload functionality - [x] Database migration execution
- [ ] Search and filtering - [x] Pedigree tree rendering
- [ ] Parent relationship linking - [x] Zoom/pan controls
- [ ] API error handling - [ ] Trial pairing simulator
- [ ] Heat cycle tracking
### Known Issues ### Known Issues
- None currently - None currently
@@ -230,5 +300,13 @@
## Version History ## Version History
- **v0.2.0** (Current) - Dog CRUD operations complete - **v0.3.0** (Current) - Litter Management & Interactive Pedigree
- Added litter_id to dogs table
- Implemented LitterForm component
- Created PedigreeView with React-D3-Tree
- Enhanced DogForm with dual parent selection
- Fixed "no such column: sire" error
- Added comprehensive documentation
- **v0.2.0** - Dog CRUD operations complete
- **v0.1.0** - Initial foundation with API and database - **v0.1.0** - Initial foundation with API and database

View File

@@ -13,14 +13,18 @@ function DogForm({ dog, onClose, onSave }) {
microchip: '', microchip: '',
notes: '', notes: '',
sire_id: '', sire_id: '',
dam_id: '' dam_id: '',
litter_id: ''
}) })
const [dogs, setDogs] = useState([]) const [dogs, setDogs] = useState([])
const [litters, setLitters] = useState([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
const [useManualParents, setUseManualParents] = useState(false)
useEffect(() => { useEffect(() => {
fetchDogs() fetchDogs()
fetchLitters()
if (dog) { if (dog) {
setFormData({ setFormData({
name: dog.name || '', name: dog.name || '',
@@ -32,8 +36,10 @@ function DogForm({ dog, onClose, onSave }) {
microchip: dog.microchip || '', microchip: dog.microchip || '',
notes: dog.notes || '', notes: dog.notes || '',
sire_id: dog.sire?.id || '', sire_id: dog.sire?.id || '',
dam_id: dog.dam?.id || '' dam_id: dog.dam?.id || '',
litter_id: dog.litter_id || ''
}) })
setUseManualParents(!dog.litter_id)
} }
}, [dog]) }, [dog])
@@ -46,9 +52,31 @@ function DogForm({ dog, onClose, onSave }) {
} }
} }
const fetchLitters = async () => {
try {
const res = await axios.get('/api/litters')
setLitters(res.data)
} catch (error) {
console.error('Error fetching litters:', error)
}
}
const handleChange = (e) => { const handleChange = (e) => {
const { name, value } = e.target const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value })) setFormData(prev => ({ ...prev, [name]: value }))
// If litter is selected, auto-populate parents
if (name === 'litter_id' && value) {
const selectedLitter = litters.find(l => l.id === parseInt(value))
if (selectedLitter) {
setFormData(prev => ({
...prev,
sire_id: selectedLitter.sire_id,
dam_id: selectedLitter.dam_id,
breed: prev.breed || selectedLitter.sire_name?.split(' ')[0] || ''
}))
}
}
} }
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
@@ -57,12 +85,19 @@ function DogForm({ dog, onClose, onSave }) {
setLoading(true) setLoading(true)
try { try {
const submitData = { ...formData }
// Clear litter_id if using manual parent selection
if (useManualParents) {
submitData.litter_id = null
}
if (dog) { if (dog) {
// Update existing dog // Update existing dog
await axios.put(`/api/dogs/${dog.id}`, formData) await axios.put(`/api/dogs/${dog.id}`, submitData)
} else { } else {
// Create new dog // Create new dog
await axios.post('/api/dogs', formData) await axios.post('/api/dogs', submitData)
} }
onSave() onSave()
onClose() onClose()
@@ -170,36 +205,84 @@ function DogForm({ dog, onClose, onSave }) {
onChange={handleChange} onChange={handleChange}
/> />
</div> </div>
</div>
<div className="form-group"> {/* Litter or Manual Parent Selection */}
<label className="label">Sire (Father)</label> <div style={{ marginTop: '1.5rem', padding: '1rem', background: '#f8fafc', borderRadius: '8px' }}>
<select <div style={{ display: 'flex', gap: '1rem', marginBottom: '1rem' }}>
name="sire_id" <label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}>
className="input" <input
value={formData.sire_id} type="radio"
onChange={handleChange} checked={!useManualParents}
> onChange={() => setUseManualParents(false)}
<option value="">Unknown</option> />
{males.map(d => ( <span>Link to Litter</span>
<option key={d.id} value={d.id}>{d.name}</option> </label>
))} <label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}>
</select> <input
type="radio"
checked={useManualParents}
onChange={() => setUseManualParents(true)}
/>
<span>Manual Parent Selection</span>
</label>
</div> </div>
<div className="form-group"> {!useManualParents ? (
<label className="label">Dam (Mother)</label> <div className="form-group">
<select <label className="label">Litter</label>
name="dam_id" <select
className="input" name="litter_id"
value={formData.dam_id} className="input"
onChange={handleChange} value={formData.litter_id}
> onChange={handleChange}
<option value="">Unknown</option> >
{females.map(d => ( <option value="">No Litter</option>
<option key={d.id} value={d.id}>{d.name}</option> {litters.map(l => (
))} <option key={l.id} value={l.id}>
</select> {l.sire_name} x {l.dam_name} - {new Date(l.breeding_date).toLocaleDateString()}
</div> </option>
))}
</select>
{formData.litter_id && (
<div style={{ marginTop: '0.5rem', fontSize: '0.875rem', color: '#64748b' }}>
Parents will be automatically set from the selected litter
</div>
)}
</div>
) : (
<div className="form-grid">
<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> </div>
<div className="form-group" style={{ marginTop: '1rem' }}> <div className="form-group" style={{ marginTop: '1rem' }}>

View File

@@ -0,0 +1,178 @@
import { useState, useEffect } from 'react'
import { X } from 'lucide-react'
import axios from 'axios'
function LitterForm({ litter, onClose, onSave }) {
const [formData, setFormData] = useState({
sire_id: '',
dam_id: '',
breeding_date: '',
whelping_date: '',
puppy_count: 0,
notes: ''
})
const [dogs, setDogs] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
useEffect(() => {
fetchDogs()
if (litter) {
setFormData({
sire_id: litter.sire_id || '',
dam_id: litter.dam_id || '',
breeding_date: litter.breeding_date || '',
whelping_date: litter.whelping_date || '',
puppy_count: litter.puppy_count || 0,
notes: litter.notes || ''
})
}
}, [litter])
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 (litter) {
await axios.put(`/api/litters/${litter.id}`, formData)
} else {
await axios.post('/api/litters', formData)
}
onSave()
onClose()
} catch (error) {
setError(error.response?.data?.error || 'Failed to save litter')
setLoading(false)
}
}
const males = dogs.filter(d => d.sex === 'male')
const females = dogs.filter(d => d.sex === 'female')
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>{litter ? 'Edit Litter' : 'Create New Litter'}</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">Sire (Father) *</label>
<select
name="sire_id"
className="input"
value={formData.sire_id}
onChange={handleChange}
required
disabled={!!litter}
>
<option value="">Select Sire</option>
{males.map(d => (
<option key={d.id} value={d.id}>{d.name} {d.registration_number ? `(${d.registration_number})` : ''}</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}
required
disabled={!!litter}
>
<option value="">Select Dam</option>
{females.map(d => (
<option key={d.id} value={d.id}>{d.name} {d.registration_number ? `(${d.registration_number})` : ''}</option>
))}
</select>
</div>
<div className="form-group">
<label className="label">Breeding Date *</label>
<input
type="date"
name="breeding_date"
className="input"
value={formData.breeding_date}
onChange={handleChange}
required
/>
</div>
<div className="form-group">
<label className="label">Whelping Date</label>
<input
type="date"
name="whelping_date"
className="input"
value={formData.whelping_date}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label className="label">Expected Puppy Count</label>
<input
type="number"
name="puppy_count"
className="input"
value={formData.puppy_count}
onChange={handleChange}
min="0"
/>
</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}
placeholder="Enter any notes about this breeding/litter..."
/>
</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...' : litter ? 'Update Litter' : 'Create Litter'}
</button>
</div>
</form>
</div>
</div>
)
}
export default LitterForm

View File

@@ -0,0 +1,137 @@
.pedigree-modal {
position: relative;
width: 95vw;
height: 90vh;
background: white;
border-radius: 12px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.pedigree-container {
flex: 1;
background: linear-gradient(to bottom, #f8fafc 0%, #e2e8f0 100%);
position: relative;
overflow: hidden;
}
.pedigree-controls {
display: flex;
gap: 0.5rem;
align-items: center;
}
.pedigree-legend {
display: flex;
gap: 2rem;
padding: 0.75rem 1.5rem;
background: #f1f5f9;
border-bottom: 1px solid #e2e8f0;
justify-content: center;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: #475569;
}
.legend-color {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.legend-color.male {
background: #3b82f6;
}
.legend-color.female {
background: #ec4899;
}
.pedigree-info {
padding: 0.75rem 1.5rem;
background: #f8fafc;
border-top: 1px solid #e2e8f0;
font-size: 0.875rem;
color: #64748b;
text-align: center;
}
.pedigree-info p {
margin: 0;
}
.pedigree-info strong {
color: #334155;
}
/* Override react-d3-tree styles */
.rd3t-tree-container {
width: 100%;
height: 100%;
}
.rd3t-link {
stroke: #94a3b8;
stroke-width: 2;
fill: none;
}
.rd3t-node {
cursor: pointer;
}
.rd3t-node:hover circle {
filter: brightness(1.1);
}
.rd3t-label__title {
font-weight: 600;
fill: #1e293b;
}
.rd3t-label__attributes {
font-size: 0.875rem;
fill: #64748b;
}
/* Loading state */
.pedigree-modal .loading {
display: flex;
align-items: center;
justify-content: center;
height: 400px;
font-size: 1.125rem;
color: #64748b;
}
/* Error state */
.pedigree-modal .error {
margin: 2rem;
padding: 1rem;
background: #fee;
border: 1px solid #fcc;
border-radius: 8px;
color: #c00;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.pedigree-modal {
width: 100vw;
height: 100vh;
border-radius: 0;
}
.pedigree-legend {
flex-wrap: wrap;
gap: 1rem;
}
}

View File

@@ -0,0 +1,234 @@
import { useState, useEffect, useCallback } from 'react'
import { X, ZoomIn, ZoomOut, Maximize2 } from 'lucide-react'
import Tree from 'react-d3-tree'
import axios from 'axios'
import './PedigreeView.css'
function PedigreeView({ dogId, onClose }) {
const [treeData, setTreeData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [translate, setTranslate] = useState({ x: 0, y: 0 })
const [zoom, setZoom] = useState(0.8)
const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
useEffect(() => {
fetchPedigree()
}, [dogId])
useEffect(() => {
const updateDimensions = () => {
const container = document.querySelector('.pedigree-container')
if (container) {
const width = container.offsetWidth
const height = container.offsetHeight
setDimensions({ width, height })
setTranslate({ x: width / 4, y: height / 2 })
}
}
updateDimensions()
window.addEventListener('resize', updateDimensions)
return () => window.removeEventListener('resize', updateDimensions)
}, [])
const fetchPedigree = async () => {
try {
setLoading(true)
const response = await axios.get(`/api/pedigree/${dogId}?generations=5`)
const formatted = formatTreeData(response.data)
setTreeData(formatted)
} catch (err) {
setError(err.response?.data?.error || 'Failed to load pedigree')
} finally {
setLoading(false)
}
}
const formatTreeData = (dog) => {
if (!dog) return null
const children = []
if (dog.sire) children.push(formatTreeData(dog.sire))
if (dog.dam) children.push(formatTreeData(dog.dam))
return {
name: dog.name,
attributes: {
sex: dog.sex,
birth_date: dog.birth_date,
registration: dog.registration_number,
breed: dog.breed,
color: dog.color,
generation: dog.generation
},
children: children.length > 0 ? children : undefined
}
}
const handleNodeClick = useCallback((nodeData) => {
console.log('Node clicked:', nodeData)
}, [])
const handleZoomIn = () => {
setZoom(prev => Math.min(prev + 0.2, 2))
}
const handleZoomOut = () => {
setZoom(prev => Math.max(prev - 0.2, 0.4))
}
const handleReset = () => {
setZoom(0.8)
setTranslate({ x: dimensions.width / 4, y: dimensions.height / 2 })
}
const renderCustomNode = ({ nodeDatum, toggleNode }) => (
<g>
<circle
r="20"
fill={nodeDatum.attributes.sex === 'male' ? '#3b82f6' : '#ec4899'}
stroke="#fff"
strokeWidth="2"
onClick={toggleNode}
style={{ cursor: 'pointer' }}
/>
<text
fill="#fff"
strokeWidth="0"
x="0"
y="5"
textAnchor="middle"
fontSize="12"
fontWeight="bold"
style={{ pointerEvents: 'none' }}
>
{nodeDatum.attributes.sex === 'male' ? '♂' : '♀'}
</text>
<text
fill="#1f2937"
x="30"
y="-10"
fontSize="14"
fontWeight="bold"
style={{ pointerEvents: 'none' }}
>
{nodeDatum.name}
</text>
{nodeDatum.attributes.registration && (
<text
fill="#6b7280"
x="30"
y="8"
fontSize="11"
style={{ pointerEvents: 'none' }}
>
{nodeDatum.attributes.registration}
</text>
)}
{nodeDatum.attributes.birth_date && (
<text
fill="#6b7280"
x="30"
y="22"
fontSize="10"
style={{ pointerEvents: 'none' }}
>
Born: {new Date(nodeDatum.attributes.birth_date).getFullYear()}
</text>
)}
</g>
)
if (loading) {
return (
<div className="modal-overlay">
<div className="pedigree-modal">
<div className="loading">Loading pedigree...</div>
</div>
</div>
)
}
if (error) {
return (
<div className="modal-overlay" onClick={onClose}>
<div className="pedigree-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>Pedigree Tree</h2>
<button className="btn-icon" onClick={onClose}>
<X size={24} />
</button>
</div>
<div className="error">{error}</div>
</div>
</div>
)
}
return (
<div className="modal-overlay" onClick={onClose}>
<div className="pedigree-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>Pedigree Tree - {treeData?.name}</h2>
<div className="pedigree-controls">
<button className="btn-icon" onClick={handleZoomOut} title="Zoom Out">
<ZoomOut size={20} />
</button>
<button className="btn-icon" onClick={handleZoomIn} title="Zoom In">
<ZoomIn size={20} />
</button>
<button className="btn-icon" onClick={handleReset} title="Reset View">
<Maximize2 size={20} />
</button>
<button className="btn-icon" onClick={onClose}>
<X size={24} />
</button>
</div>
</div>
<div className="pedigree-legend">
<div className="legend-item">
<span className="legend-color male"></span>
<span>Male</span>
</div>
<div className="legend-item">
<span className="legend-color female"></span>
<span>Female</span>
</div>
</div>
<div className="pedigree-container">
{treeData && dimensions.width > 0 && (
<Tree
data={treeData}
translate={translate}
zoom={zoom}
onNodeClick={handleNodeClick}
renderCustomNodeElement={renderCustomNode}
orientation="horizontal"
pathFunc="step"
separation={{ siblings: 2, nonSiblings: 2.5 }}
nodeSize={{ x: 200, y: 100 }}
enableLegacyTransitions
transitionDuration={300}
collapsible={false}
zoomable={true}
draggable={true}
dimensions={dimensions}
/>
)}
</div>
<div className="pedigree-info">
<p>
<strong>Tip:</strong> Use mouse wheel to zoom, click and drag to pan.
Click on nodes to view details.
</p>
</div>
</div>
</div>
)
}
export default PedigreeView

View File

@@ -0,0 +1,52 @@
const Database = require('better-sqlite3');
const path = require('path');
function migrateLitterId(dbPath) {
console.log('Running litter_id migration...');
const db = new Database(dbPath);
db.pragma('foreign_keys = ON');
try {
// Check if litter_id column already exists
const tableInfo = db.prepare("PRAGMA table_info(dogs)").all();
const hasLitterId = tableInfo.some(col => col.name === 'litter_id');
if (hasLitterId) {
console.log('litter_id column already exists. Skipping migration.');
db.close();
return;
}
// Add litter_id column to dogs table
db.exec(`
ALTER TABLE dogs ADD COLUMN litter_id INTEGER;
`);
// Create index for litter_id
db.exec(`
CREATE INDEX IF NOT EXISTS idx_dogs_litter ON dogs(litter_id);
`);
// Add foreign key relationship (SQLite doesn't support ALTER TABLE ADD CONSTRAINT)
// So we'll rely on application-level constraint checking
console.log('✓ Added litter_id column to dogs table');
console.log('✓ Created index on litter_id');
console.log('Migration completed successfully!');
db.close();
} catch (error) {
console.error('Migration failed:', error.message);
db.close();
throw error;
}
}
module.exports = { migrateLitterId };
// Run migration if called directly
if (require.main === module) {
const dbPath = process.env.DB_PATH || path.join(__dirname, '../../data/breedr.db');
migrateLitterId(dbPath);
}

View File

@@ -16,18 +16,18 @@ router.get('/', (req, res) => {
ORDER BY l.breeding_date DESC ORDER BY l.breeding_date DESC
`).all(); `).all();
// Get puppies for each litter // Get puppies for each litter using litter_id
litters.forEach(litter => { litters.forEach(litter => {
litter.puppies = db.prepare(` litter.puppies = db.prepare(`
SELECT d.* FROM dogs d SELECT * FROM dogs WHERE litter_id = ? AND is_active = 1
JOIN parents ps ON d.id = ps.dog_id `).all(litter.id);
JOIN parents pd ON d.id = pd.dog_id
WHERE ps.parent_id = ? AND pd.parent_id = ?
`).all(litter.sire_id, litter.dam_id);
litter.puppies.forEach(puppy => { litter.puppies.forEach(puppy => {
puppy.photo_urls = puppy.photo_urls ? JSON.parse(puppy.photo_urls) : []; puppy.photo_urls = puppy.photo_urls ? JSON.parse(puppy.photo_urls) : [];
}); });
// Update puppy_count based on actual puppies
litter.actual_puppy_count = litter.puppies.length;
}); });
res.json(litters); res.json(litters);
@@ -54,17 +54,17 @@ router.get('/:id', (req, res) => {
return res.status(404).json({ error: 'Litter not found' }); return res.status(404).json({ error: 'Litter not found' });
} }
// Get puppies using litter_id
litter.puppies = db.prepare(` litter.puppies = db.prepare(`
SELECT d.* FROM dogs d SELECT * FROM dogs WHERE litter_id = ? AND is_active = 1
JOIN parents ps ON d.id = ps.dog_id `).all(litter.id);
JOIN parents pd ON d.id = pd.dog_id
WHERE ps.parent_id = ? AND pd.parent_id = ?
`).all(litter.sire_id, litter.dam_id);
litter.puppies.forEach(puppy => { litter.puppies.forEach(puppy => {
puppy.photo_urls = puppy.photo_urls ? JSON.parse(puppy.photo_urls) : []; puppy.photo_urls = puppy.photo_urls ? JSON.parse(puppy.photo_urls) : [];
}); });
litter.actual_puppy_count = litter.puppies.length;
res.json(litter); res.json(litter);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
@@ -125,11 +125,70 @@ router.put('/:id', (req, res) => {
} }
}); });
// POST link puppy to litter
router.post('/:id/puppies/:puppyId', (req, res) => {
try {
const { id: litterId, puppyId } = req.params;
const db = getDatabase();
// Verify litter exists
const litter = db.prepare('SELECT sire_id, dam_id FROM litters WHERE id = ?').get(litterId);
if (!litter) {
return res.status(404).json({ error: 'Litter not found' });
}
// Verify puppy exists
const puppy = db.prepare('SELECT id FROM dogs WHERE id = ?').get(puppyId);
if (!puppy) {
return res.status(404).json({ error: 'Puppy not found' });
}
// Link puppy to litter
db.prepare('UPDATE dogs SET litter_id = ? WHERE id = ?').run(litterId, puppyId);
// Also update parent relationships if not set
const existingParents = db.prepare('SELECT parent_type FROM parents WHERE dog_id = ?').all(puppyId);
const hasSire = existingParents.some(p => p.parent_type === 'sire');
const hasDam = existingParents.some(p => p.parent_type === 'dam');
if (!hasSire) {
db.prepare('INSERT OR IGNORE INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, \'sire\')').run(puppyId, litter.sire_id);
}
if (!hasDam) {
db.prepare('INSERT OR IGNORE INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, \'dam\')').run(puppyId, litter.dam_id);
}
res.json({ message: 'Puppy linked to litter successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// DELETE remove puppy from litter
router.delete('/:id/puppies/:puppyId', (req, res) => {
try {
const { puppyId } = req.params;
const db = getDatabase();
db.prepare('UPDATE dogs SET litter_id = NULL WHERE id = ?').run(puppyId);
res.json({ message: 'Puppy removed from litter' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// DELETE litter // DELETE litter
router.delete('/:id', (req, res) => { router.delete('/:id', (req, res) => {
try { try {
const db = getDatabase(); const db = getDatabase();
// Remove litter_id from associated puppies
db.prepare('UPDATE dogs SET litter_id = NULL WHERE litter_id = ?').run(req.params.id);
// Delete the litter
db.prepare('DELETE FROM litters WHERE id = ?').run(req.params.id); db.prepare('DELETE FROM litters WHERE id = ?').run(req.params.id);
res.json({ message: 'Litter deleted successfully' }); res.json({ message: 'Litter deleted successfully' });
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });