From a246e5f84f283e5e2914b44196dc8878652ff281 Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 9 Mar 2026 00:06:41 -0500 Subject: [PATCH 1/9] Add migration to add litter_id column to dogs table --- server/db/migrate_litter_id.js | 52 ++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 server/db/migrate_litter_id.js diff --git a/server/db/migrate_litter_id.js b/server/db/migrate_litter_id.js new file mode 100644 index 0000000..0fa4dcf --- /dev/null +++ b/server/db/migrate_litter_id.js @@ -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); +} From cc5af29c1111f8760251ce1711167514f4802583 Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 9 Mar 2026 00:07:26 -0500 Subject: [PATCH 2/9] Enhanced litters API with puppy linking and litter_id support --- server/routes/litters.js | 83 ++++++++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/server/routes/litters.js b/server/routes/litters.js index e7832a6..e8cf492 100644 --- a/server/routes/litters.js +++ b/server/routes/litters.js @@ -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,15 +125,74 @@ 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 }); } }); -module.exports = router; \ No newline at end of file +module.exports = router; From 4af656667d8830e2f27f404e5602a0cf31923a9a Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 9 Mar 2026 00:08:18 -0500 Subject: [PATCH 3/9] Add LitterForm component for litter management --- client/src/components/LitterForm.jsx | 178 +++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 client/src/components/LitterForm.jsx diff --git a/client/src/components/LitterForm.jsx b/client/src/components/LitterForm.jsx new file mode 100644 index 0000000..a86430a --- /dev/null +++ b/client/src/components/LitterForm.jsx @@ -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 ( +
+
e.stopPropagation()}> +
+

{litter ? 'Edit Litter' : 'Create New Litter'}

+ +
+ +
+ {error &&
{error}
} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +