feature/litter-management-and-pedigree #6
368
FEATURE_IMPLEMENTATION.md
Normal file
368
FEATURE_IMPLEMENTATION.md
Normal 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
340
QUICKSTART.md
Normal 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! 🐶**
|
||||
136
ROADMAP.md
136
ROADMAP.md
@@ -57,14 +57,16 @@
|
||||
|
||||
---
|
||||
|
||||
## 🚧 Phase 3: Breeding Tools (IN PROGRESS)
|
||||
## ✅ Phase 3: Breeding Tools (COMPLETE)
|
||||
|
||||
### Priority Features
|
||||
- [ ] Interactive pedigree tree visualization
|
||||
- [ ] Integrate React-D3-Tree
|
||||
- [ ] Show 3-5 generations
|
||||
- [ ] Click to navigate
|
||||
- [ ] Zoom and pan controls
|
||||
- [x] **Interactive pedigree tree visualization**
|
||||
- [x] Integrate React-D3-Tree
|
||||
- [x] Show 3-5 generations
|
||||
- [x] Click to navigate
|
||||
- [x] Zoom and pan controls
|
||||
- [x] Beautiful color-coded nodes
|
||||
- [x] Male/Female distinction
|
||||
|
||||
- [ ] Trial Pairing Simulator
|
||||
- [ ] Select sire and dam
|
||||
@@ -78,11 +80,14 @@
|
||||
- [ ] Calendar view
|
||||
- [ ] Breeding date suggestions
|
||||
|
||||
- [ ] Litter Management
|
||||
- [ ] Create litter records
|
||||
- [ ] Link puppies to litter
|
||||
- [ ] Track whelping details
|
||||
- [ ] Auto-link parent relationships
|
||||
- [x] **Litter Management** ✅ **NEW**
|
||||
- [x] Create litter records
|
||||
- [x] Link puppies to litter
|
||||
- [x] Track whelping details
|
||||
- [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
|
||||
|
||||
### 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**
|
||||
1. **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
|
||||
2. **Heat Cycle Management**
|
||||
- Add/edit heat cycles
|
||||
- Track progesterone levels
|
||||
- Calendar view
|
||||
- Breeding date suggestions
|
||||
|
||||
3. **Enhanced Litter Features**
|
||||
- Puppy batch addition
|
||||
- Photo gallery per litter
|
||||
- Whelping countdown
|
||||
- Expected vs actual puppy count tracking
|
||||
|
||||
### Testing Needed
|
||||
- [ ] Add/edit dog forms
|
||||
- [ ] Photo upload functionality
|
||||
- [ ] Search and filtering
|
||||
- [ ] Parent relationship linking
|
||||
- [ ] API error handling
|
||||
- [x] Add/edit dog forms with litter selection
|
||||
- [x] Database migration execution
|
||||
- [x] Pedigree tree rendering
|
||||
- [x] Zoom/pan controls
|
||||
- [ ] Trial pairing simulator
|
||||
- [ ] Heat cycle tracking
|
||||
|
||||
### Known Issues
|
||||
- None currently
|
||||
@@ -230,5 +300,13 @@
|
||||
|
||||
## 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
|
||||
@@ -13,14 +13,18 @@ function DogForm({ dog, onClose, onSave }) {
|
||||
microchip: '',
|
||||
notes: '',
|
||||
sire_id: '',
|
||||
dam_id: ''
|
||||
dam_id: '',
|
||||
litter_id: ''
|
||||
})
|
||||
const [dogs, setDogs] = useState([])
|
||||
const [litters, setLitters] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [useManualParents, setUseManualParents] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchDogs()
|
||||
fetchLitters()
|
||||
if (dog) {
|
||||
setFormData({
|
||||
name: dog.name || '',
|
||||
@@ -32,8 +36,10 @@ function DogForm({ dog, onClose, onSave }) {
|
||||
microchip: dog.microchip || '',
|
||||
notes: dog.notes || '',
|
||||
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])
|
||||
|
||||
@@ -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 { name, value } = e.target
|
||||
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) => {
|
||||
@@ -57,12 +85,19 @@ function DogForm({ dog, onClose, onSave }) {
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const submitData = { ...formData }
|
||||
|
||||
// Clear litter_id if using manual parent selection
|
||||
if (useManualParents) {
|
||||
submitData.litter_id = null
|
||||
}
|
||||
|
||||
if (dog) {
|
||||
// Update existing dog
|
||||
await axios.put(`/api/dogs/${dog.id}`, formData)
|
||||
await axios.put(`/api/dogs/${dog.id}`, submitData)
|
||||
} else {
|
||||
// Create new dog
|
||||
await axios.post('/api/dogs', formData)
|
||||
await axios.post('/api/dogs', submitData)
|
||||
}
|
||||
onSave()
|
||||
onClose()
|
||||
@@ -170,7 +205,53 @@ function DogForm({ dog, onClose, onSave }) {
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Litter or Manual Parent Selection */}
|
||||
<div style={{ marginTop: '1.5rem', padding: '1rem', background: '#f8fafc', borderRadius: '8px' }}>
|
||||
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1rem' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="radio"
|
||||
checked={!useManualParents}
|
||||
onChange={() => setUseManualParents(false)}
|
||||
/>
|
||||
<span>Link to Litter</span>
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="radio"
|
||||
checked={useManualParents}
|
||||
onChange={() => setUseManualParents(true)}
|
||||
/>
|
||||
<span>Manual Parent Selection</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{!useManualParents ? (
|
||||
<div className="form-group">
|
||||
<label className="label">Litter</label>
|
||||
<select
|
||||
name="litter_id"
|
||||
className="input"
|
||||
value={formData.litter_id}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="">No Litter</option>
|
||||
{litters.map(l => (
|
||||
<option key={l.id} value={l.id}>
|
||||
{l.sire_name} x {l.dam_name} - {new Date(l.breeding_date).toLocaleDateString()}
|
||||
</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
|
||||
@@ -201,6 +282,8 @@ function DogForm({ dog, onClose, onSave }) {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group" style={{ marginTop: '1rem' }}>
|
||||
<label className="label">Notes</label>
|
||||
|
||||
178
client/src/components/LitterForm.jsx
Normal file
178
client/src/components/LitterForm.jsx
Normal 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
|
||||
137
client/src/components/PedigreeView.css
Normal file
137
client/src/components/PedigreeView.css
Normal 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;
|
||||
}
|
||||
}
|
||||
234
client/src/components/PedigreeView.jsx
Normal file
234
client/src/components/PedigreeView.jsx
Normal 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
|
||||
52
server/db/migrate_litter_id.js
Normal file
52
server/db/migrate_litter_id.js
Normal 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);
|
||||
}
|
||||
@@ -16,18 +16,18 @@ router.get('/', (req, res) => {
|
||||
ORDER BY l.breeding_date DESC
|
||||
`).all();
|
||||
|
||||
// Get puppies for each litter
|
||||
// Get puppies for each litter using litter_id
|
||||
litters.forEach(litter => {
|
||||
litter.puppies = db.prepare(`
|
||||
SELECT d.* FROM dogs d
|
||||
JOIN parents ps ON d.id = ps.dog_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);
|
||||
SELECT * FROM dogs WHERE litter_id = ? AND is_active = 1
|
||||
`).all(litter.id);
|
||||
|
||||
litter.puppies.forEach(puppy => {
|
||||
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);
|
||||
@@ -54,17 +54,17 @@ router.get('/:id', (req, res) => {
|
||||
return res.status(404).json({ error: 'Litter not found' });
|
||||
}
|
||||
|
||||
// Get puppies using litter_id
|
||||
litter.puppies = db.prepare(`
|
||||
SELECT d.* FROM dogs d
|
||||
JOIN parents ps ON d.id = ps.dog_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);
|
||||
SELECT * FROM dogs WHERE litter_id = ? AND is_active = 1
|
||||
`).all(litter.id);
|
||||
|
||||
litter.puppies.forEach(puppy => {
|
||||
puppy.photo_urls = puppy.photo_urls ? JSON.parse(puppy.photo_urls) : [];
|
||||
});
|
||||
|
||||
litter.actual_puppy_count = litter.puppies.length;
|
||||
|
||||
res.json(litter);
|
||||
} catch (error) {
|
||||
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
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
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);
|
||||
|
||||
res.json({ message: 'Litter deleted successfully' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
|
||||
Reference in New Issue
Block a user