From 421ea5cb586a6e081360ac71fca6889c6bb576af Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 9 Mar 2026 22:24:39 -0500 Subject: [PATCH] feat(api): expose is_champion on all dog queries incl sire/dam/offspring joins --- server/routes/dogs.js | 268 ++++++++++++++++++------------------------ 1 file changed, 112 insertions(+), 156 deletions(-) diff --git a/server/routes/dogs.js b/server/routes/dogs.js index 3fdf934..f5178f4 100644 --- a/server/routes/dogs.js +++ b/server/routes/dogs.js @@ -5,7 +5,6 @@ const multer = require('multer'); const path = require('path'); const fs = require('fs'); -// Configure multer for photo uploads const storage = multer.diskStorage({ destination: (req, file, cb) => { const uploadPath = process.env.UPLOAD_PATH || path.join(__dirname, '../../uploads'); @@ -19,12 +18,10 @@ const storage = multer.diskStorage({ const upload = multer({ storage, - limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit + limits: { fileSize: 10 * 1024 * 1024 }, fileFilter: (req, file, cb) => { - const allowedTypes = /jpeg|jpg|png|gif|webp/; - const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); - const mimetype = allowedTypes.test(file.mimetype); - if (extname && mimetype) { + const allowed = /jpeg|jpg|png|gif|webp/; + if (allowed.test(path.extname(file.originalname).toLowerCase()) && allowed.test(file.mimetype)) { cb(null, true); } else { cb(new Error('Only image files are allowed')); @@ -32,29 +29,41 @@ const upload = multer({ } }); -// Helper function to convert empty strings to null -const emptyToNull = (value) => { - return (value === '' || value === undefined) ? null : value; -}; +const emptyToNull = (v) => (v === '' || v === undefined) ? null : v; -// GET all dogs +// ── Shared SELECT columns ───────────────────────────────────────────── +const DOG_COLS = ` + id, name, registration_number, breed, sex, birth_date, + color, microchip, photo_urls, notes, litter_id, is_active, + is_champion, created_at, updated_at +`; + +// ── GET all dogs ─────────────────────────────────────────────────── router.get('/', (req, res) => { try { const db = getDatabase(); const dogs = db.prepare(` - SELECT id, name, registration_number, breed, sex, birth_date, - color, microchip, photo_urls, notes, litter_id, is_active, - created_at, updated_at - FROM dogs - WHERE is_active = 1 + SELECT ${DOG_COLS} + FROM dogs + WHERE is_active = 1 ORDER BY name `).all(); - - // Parse photo_urls JSON + + // Also pull sire/dam so list page can compute bloodline status + const parentStmt = db.prepare(` + SELECT p.parent_type, d.id, d.name, d.is_champion + FROM parents p + JOIN dogs d ON p.parent_id = d.id + WHERE p.dog_id = ? + `); + dogs.forEach(dog => { dog.photo_urls = dog.photo_urls ? JSON.parse(dog.photo_urls) : []; + const parents = parentStmt.all(dog.id); + dog.sire = parents.find(p => p.parent_type === 'sire') || null; + dog.dam = parents.find(p => p.parent_type === 'dam') || null; }); - + res.json(dogs); } catch (error) { console.error('Error fetching dogs:', error); @@ -62,42 +71,35 @@ router.get('/', (req, res) => { } }); -// GET single dog by ID with parents and offspring +// ── GET single dog (with parents + offspring) ─────────────────────── router.get('/:id', (req, res) => { try { const db = getDatabase(); - const dog = db.prepare(` - SELECT id, name, registration_number, breed, sex, birth_date, - color, microchip, photo_urls, notes, litter_id, is_active, - created_at, updated_at - FROM dogs - WHERE id = ? - `).get(req.params.id); - - if (!dog) { - return res.status(404).json({ error: 'Dog not found' }); - } - + const dog = db.prepare(`SELECT ${DOG_COLS} FROM dogs WHERE id = ?`).get(req.params.id); + + if (!dog) return res.status(404).json({ error: 'Dog not found' }); + dog.photo_urls = dog.photo_urls ? JSON.parse(dog.photo_urls) : []; - - // Get parents from parents table + + // Parents — include is_champion so frontend can render bloodline badge const parents = db.prepare(` - SELECT p.parent_type, d.* - FROM parents p - JOIN dogs d ON p.parent_id = d.id + SELECT p.parent_type, d.id, d.name, d.is_champion + FROM parents p + JOIN dogs d ON p.parent_id = d.id WHERE p.dog_id = ? `).all(req.params.id); - + dog.sire = parents.find(p => p.parent_type === 'sire') || null; - dog.dam = parents.find(p => p.parent_type === 'dam') || null; - - // Get offspring + dog.dam = parents.find(p => p.parent_type === 'dam') || null; + + // Offspring — include is_champion for badge on offspring cards dog.offspring = db.prepare(` - SELECT d.* FROM dogs d + SELECT d.id, d.name, d.sex, d.is_champion + FROM dogs d JOIN parents p ON d.id = p.dog_id WHERE p.parent_id = ? AND d.is_active = 1 `).all(req.params.id); - + res.json(dog); } catch (error) { console.error('Error fetching dog:', error); @@ -105,66 +107,50 @@ router.get('/:id', (req, res) => { } }); -// POST create new dog +// ── POST create dog ──────────────────────────────────────────────── router.post('/', (req, res) => { try { - const { name, registration_number, breed, sex, birth_date, color, microchip, notes, sire_id, dam_id, litter_id } = req.body; - - console.log('Creating dog with data:', { name, breed, sex, sire_id, dam_id, litter_id }); - + const { name, registration_number, breed, sex, birth_date, color, + microchip, notes, sire_id, dam_id, litter_id, is_champion } = req.body; + + console.log('Creating dog:', { name, breed, sex, sire_id, dam_id, litter_id, is_champion }); + if (!name || !breed || !sex) { return res.status(400).json({ error: 'Name, breed, and sex are required' }); } - + const db = getDatabase(); - - // Insert dog (dogs table has NO sire/dam columns) const result = db.prepare(` - INSERT INTO dogs (name, registration_number, breed, sex, birth_date, color, microchip, notes, litter_id, photo_urls) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO dogs (name, registration_number, breed, sex, birth_date, color, + microchip, notes, litter_id, photo_urls, is_champion) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( - name, - emptyToNull(registration_number), - breed, - sex, - emptyToNull(birth_date), - emptyToNull(color), + name, + emptyToNull(registration_number), + breed, sex, + emptyToNull(birth_date), + emptyToNull(color), emptyToNull(microchip), emptyToNull(notes), emptyToNull(litter_id), - '[]' + '[]', + is_champion ? 1 : 0 ); - + const dogId = result.lastInsertRowid; console.log(`✓ Dog inserted with ID: ${dogId}`); - - // Add sire relationship if provided + if (sire_id && sire_id !== '' && sire_id !== null) { - console.log(` Adding sire relationship: dog ${dogId} -> sire ${sire_id}`); - db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)'). - run(dogId, sire_id, 'sire'); - console.log(` ✓ Sire relationship added`); + db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)').run(dogId, sire_id, 'sire'); } - - // Add dam relationship if provided if (dam_id && dam_id !== '' && dam_id !== null) { - console.log(` Adding dam relationship: dog ${dogId} -> dam ${dam_id}`); - db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)'). - run(dogId, dam_id, 'dam'); - console.log(` ✓ Dam relationship added`); + db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)').run(dogId, dam_id, 'dam'); } - - // Fetch the created dog - const dog = db.prepare(` - SELECT id, name, registration_number, breed, sex, birth_date, - color, microchip, photo_urls, notes, litter_id, is_active, - created_at, updated_at - FROM dogs - WHERE id = ? - `).get(dogId); + + const dog = db.prepare(`SELECT ${DOG_COLS} FROM dogs WHERE id = ?`).get(dogId); dog.photo_urls = []; - - console.log(`✓ Dog created successfully: ${dog.name} (ID: ${dogId})`); + + console.log(`✓ Dog created: ${dog.name} (ID: ${dogId})`); res.status(201).json(dog); } catch (error) { console.error('Error creating dog:', error); @@ -172,66 +158,47 @@ router.post('/', (req, res) => { } }); -// PUT update dog +// ── PUT update dog ──────────────────────────────────────────────── router.put('/:id', (req, res) => { try { - const { name, registration_number, breed, sex, birth_date, color, microchip, notes, sire_id, dam_id, litter_id } = req.body; - - console.log(`Updating dog ${req.params.id} with data:`, { name, breed, sex, sire_id, dam_id, litter_id }); - + const { name, registration_number, breed, sex, birth_date, color, + microchip, notes, sire_id, dam_id, litter_id, is_champion } = req.body; + + console.log(`Updating dog ${req.params.id}:`, { name, breed, sex, sire_id, dam_id, is_champion }); + const db = getDatabase(); - - // Update dog record (dogs table has NO sire/dam columns) db.prepare(` - UPDATE dogs - SET name = ?, registration_number = ?, breed = ?, sex = ?, - birth_date = ?, color = ?, microchip = ?, notes = ?, litter_id = ? + UPDATE dogs + SET name = ?, registration_number = ?, breed = ?, sex = ?, + birth_date = ?, color = ?, microchip = ?, notes = ?, + litter_id = ?, is_champion = ?, updated_at = datetime('now') WHERE id = ? `).run( - name, - emptyToNull(registration_number), - breed, - sex, - emptyToNull(birth_date), - emptyToNull(color), + name, + emptyToNull(registration_number), + breed, sex, + emptyToNull(birth_date), + emptyToNull(color), emptyToNull(microchip), emptyToNull(notes), emptyToNull(litter_id), + is_champion ? 1 : 0, req.params.id ); - console.log(` ✓ Dog record updated`); - - // Remove existing parent relationships + + // Re-link parents db.prepare('DELETE FROM parents WHERE dog_id = ?').run(req.params.id); - console.log(` ✓ Old parent relationships removed`); - - // Add new sire relationship if provided if (sire_id && sire_id !== '' && sire_id !== null) { - console.log(` Adding sire relationship: dog ${req.params.id} -> sire ${sire_id}`); - db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)'). - run(req.params.id, sire_id, 'sire'); - console.log(` ✓ Sire relationship added`); + db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)').run(req.params.id, sire_id, 'sire'); } - - // Add new dam relationship if provided if (dam_id && dam_id !== '' && dam_id !== null) { - console.log(` Adding dam relationship: dog ${req.params.id} -> dam ${dam_id}`); - db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)'). - run(req.params.id, dam_id, 'dam'); - console.log(` ✓ Dam relationship added`); + db.prepare('INSERT INTO parents (dog_id, parent_id, parent_type) VALUES (?, ?, ?)').run(req.params.id, dam_id, 'dam'); } - - // Fetch updated dog - const dog = db.prepare(` - SELECT id, name, registration_number, breed, sex, birth_date, - color, microchip, photo_urls, notes, litter_id, is_active, - created_at, updated_at - FROM dogs - WHERE id = ? - `).get(req.params.id); + + const dog = db.prepare(`SELECT ${DOG_COLS} FROM dogs WHERE id = ?`).get(req.params.id); dog.photo_urls = dog.photo_urls ? JSON.parse(dog.photo_urls) : []; - - console.log(`✓ Dog updated successfully: ${dog.name} (ID: ${req.params.id})`); + + console.log(`✓ Dog updated: ${dog.name} (ID: ${req.params.id})`); res.json(dog); } catch (error) { console.error('Error updating dog:', error); @@ -239,7 +206,7 @@ router.put('/:id', (req, res) => { } }); -// DELETE dog (soft delete) +// ── DELETE dog (soft) ─────────────────────────────────────────────── router.delete('/:id', (req, res) => { try { const db = getDatabase(); @@ -252,25 +219,19 @@ router.delete('/:id', (req, res) => { } }); -// POST upload photo for dog +// ── POST upload photo ─────────────────────────────────────────────── router.post('/:id/photos', upload.single('photo'), (req, res) => { try { - if (!req.file) { - return res.status(400).json({ error: 'No file uploaded' }); - } - + if (!req.file) return res.status(400).json({ error: 'No file uploaded' }); + const db = getDatabase(); const dog = db.prepare('SELECT photo_urls FROM dogs WHERE id = ?').get(req.params.id); - - if (!dog) { - return res.status(404).json({ error: 'Dog not found' }); - } - + if (!dog) return res.status(404).json({ error: 'Dog not found' }); + const photoUrls = dog.photo_urls ? JSON.parse(dog.photo_urls) : []; photoUrls.push(`/uploads/${req.file.filename}`); - db.prepare('UPDATE dogs SET photo_urls = ? WHERE id = ?').run(JSON.stringify(photoUrls), req.params.id); - + res.json({ url: `/uploads/${req.file.filename}`, photos: photoUrls }); } catch (error) { console.error('Error uploading photo:', error); @@ -278,31 +239,26 @@ router.post('/:id/photos', upload.single('photo'), (req, res) => { } }); -// DELETE photo from dog +// ── DELETE photo ───────────────────────────────────────────────────── router.delete('/:id/photos/:photoIndex', (req, res) => { try { const db = getDatabase(); const dog = db.prepare('SELECT photo_urls FROM dogs WHERE id = ?').get(req.params.id); - - if (!dog) { - return res.status(404).json({ error: 'Dog not found' }); - } - - const photoUrls = dog.photo_urls ? JSON.parse(dog.photo_urls) : []; + if (!dog) return res.status(404).json({ error: 'Dog not found' }); + + const photoUrls = dog.photo_urls ? JSON.parse(dog.photo_urls) : []; const photoIndex = parseInt(req.params.photoIndex); - + if (photoIndex >= 0 && photoIndex < photoUrls.length) { - const photoPath = path.join(process.env.UPLOAD_PATH || path.join(__dirname, '../../uploads'), path.basename(photoUrls[photoIndex])); - - // Delete file from disk - if (fs.existsSync(photoPath)) { - fs.unlinkSync(photoPath); - } - + const photoPath = path.join( + process.env.UPLOAD_PATH || path.join(__dirname, '../../uploads'), + path.basename(photoUrls[photoIndex]) + ); + if (fs.existsSync(photoPath)) fs.unlinkSync(photoPath); photoUrls.splice(photoIndex, 1); db.prepare('UPDATE dogs SET photo_urls = ? WHERE id = ?').run(JSON.stringify(photoUrls), req.params.id); } - + res.json({ photos: photoUrls }); } catch (error) { console.error('Error deleting photo:', error);