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:
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
|
### 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
|
||||||
@@ -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' }}>
|
||||||
|
|||||||
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
|
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 });
|
||||||
|
|||||||
Reference in New Issue
Block a user