From d7bad192755df2f9572642515ce94ee6fcaa5bf6 Mon Sep 17 00:00:00 2001 From: jason Date: Mon, 9 Mar 2026 20:30:42 -0500 Subject: [PATCH] feat: Add breeding date suggestion window endpoint --- server/routes/breeding.js | 157 ++++++++++++++++++++++++++++---------- 1 file changed, 118 insertions(+), 39 deletions(-) diff --git a/server/routes/breeding.js b/server/routes/breeding.js index 0bec351..32efd36 100644 --- a/server/routes/breeding.js +++ b/server/routes/breeding.js @@ -11,55 +11,149 @@ router.get('/heat-cycles/dog/:dogId', (req, res) => { WHERE dog_id = ? ORDER BY start_date DESC `).all(req.params.dogId); - res.json(cycles); } catch (error) { res.status(500).json({ error: error.message }); } }); -// GET all active heat cycles +// GET all active heat cycles (with dog info) router.get('/heat-cycles/active', (req, res) => { try { const db = getDatabase(); const cycles = db.prepare(` - SELECT hc.*, d.name as dog_name, d.registration_number + SELECT hc.*, d.name as dog_name, d.registration_number, d.breed, d.birth_date FROM heat_cycles hc JOIN dogs d ON hc.dog_id = d.id WHERE hc.end_date IS NULL OR hc.end_date >= date('now', '-30 days') ORDER BY hc.start_date DESC `).all(); - res.json(cycles); } catch (error) { res.status(500).json({ error: error.message }); } }); +// GET all heat cycles (all dogs, for calendar population) +router.get('/heat-cycles', (req, res) => { + try { + const db = getDatabase(); + const { year, month } = req.query; + let query = ` + SELECT hc.*, d.name as dog_name, d.registration_number, d.breed + FROM heat_cycles hc + JOIN dogs d ON hc.dog_id = d.id + `; + const params = []; + if (year && month) { + query += ` WHERE strftime('%Y', hc.start_date) = ? AND strftime('%m', hc.start_date) = ?`; + params.push(year, month.toString().padStart(2, '0')); + } + query += ' ORDER BY hc.start_date DESC'; + const cycles = db.prepare(query).all(...params); + res.json(cycles); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// GET breeding date suggestions for a heat cycle +// Returns optimal breeding window based on start_date (days 9-15 of cycle) +router.get('/heat-cycles/:id/suggestions', (req, res) => { + try { + const db = getDatabase(); + const cycle = db.prepare(` + SELECT hc.*, d.name as dog_name + FROM heat_cycles hc + JOIN dogs d ON hc.dog_id = d.id + WHERE hc.id = ? + `).get(req.params.id); + + if (!cycle) return res.status(404).json({ error: 'Heat cycle not found' }); + + const start = new Date(cycle.start_date); + + const addDays = (d, n) => { + const r = new Date(d); + r.setDate(r.getDate() + n); + return r.toISOString().split('T')[0]; + }; + + // Standard canine heat cycle windows + res.json({ + cycle_id: cycle.id, + dog_name: cycle.dog_name, + start_date: cycle.start_date, + windows: [ + { + label: 'Proestrus', + description: 'Bleeding begins, not yet receptive', + start: addDays(start, 0), + end: addDays(start, 8), + color: 'pink', + type: 'proestrus' + }, + { + label: 'Optimal Breeding Window', + description: 'Estrus — highest fertility, best time to breed', + start: addDays(start, 9), + end: addDays(start, 15), + color: 'green', + type: 'optimal' + }, + { + label: 'Late Estrus', + description: 'Fertility declining but breeding still possible', + start: addDays(start, 16), + end: addDays(start, 21), + color: 'yellow', + type: 'late' + }, + { + label: 'Diestrus', + description: 'Cycle ending, not receptive', + start: addDays(start, 22), + end: addDays(start, 28), + color: 'gray', + type: 'diestrus' + } + ], + // If a breeding_date was logged, compute whelping estimate + whelping: cycle.breeding_date ? { + breeding_date: cycle.breeding_date, + earliest: addDays(new Date(cycle.breeding_date), 58), + expected: addDays(new Date(cycle.breeding_date), 63), + latest: addDays(new Date(cycle.breeding_date), 68) + } : null + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + // POST create heat cycle router.post('/heat-cycles', (req, res) => { try { - const { dog_id, start_date, end_date, progesterone_peak_date, breeding_date, breeding_successful, notes } = req.body; - + const { dog_id, start_date, end_date, breeding_date, breeding_successful, notes } = req.body; + if (!dog_id || !start_date) { return res.status(400).json({ error: 'Dog ID and start date are required' }); } - + const db = getDatabase(); - + // Verify dog is female const dog = db.prepare('SELECT sex FROM dogs WHERE id = ?').get(dog_id); if (!dog || dog.sex !== 'female') { return res.status(400).json({ error: 'Dog must be female' }); } - + const result = db.prepare(` - INSERT INTO heat_cycles (dog_id, start_date, end_date, progesterone_peak_date, breeding_date, breeding_successful, notes) - VALUES (?, ?, ?, ?, ?, ?, ?) - `).run(dog_id, start_date, end_date, progesterone_peak_date, breeding_date, breeding_successful || 0, notes); - + INSERT INTO heat_cycles (dog_id, start_date, end_date, breeding_date, breeding_successful, notes) + VALUES (?, ?, ?, ?, ?, ?) + `).run(dog_id, start_date, end_date || null, breeding_date || null, breeding_successful || 0, notes || null); + const cycle = db.prepare('SELECT * FROM heat_cycles WHERE id = ?').get(result.lastInsertRowid); - res.status(201).json(cycle); } catch (error) { res.status(500).json({ error: error.message }); @@ -69,16 +163,13 @@ router.post('/heat-cycles', (req, res) => { // PUT update heat cycle router.put('/heat-cycles/:id', (req, res) => { try { - const { start_date, end_date, progesterone_peak_date, breeding_date, breeding_successful, notes } = req.body; - + const { start_date, end_date, breeding_date, breeding_successful, notes } = req.body; const db = getDatabase(); db.prepare(` UPDATE heat_cycles - SET start_date = ?, end_date = ?, progesterone_peak_date = ?, - breeding_date = ?, breeding_successful = ?, notes = ? + SET start_date = ?, end_date = ?, breeding_date = ?, breeding_successful = ?, notes = ? WHERE id = ? - `).run(start_date, end_date, progesterone_peak_date, breeding_date, breeding_successful, notes, req.params.id); - + `).run(start_date, end_date || null, breeding_date || null, breeding_successful || 0, notes || null, req.params.id); const cycle = db.prepare('SELECT * FROM heat_cycles WHERE id = ?').get(req.params.id); res.json(cycle); } catch (error) { @@ -97,32 +188,20 @@ router.delete('/heat-cycles/:id', (req, res) => { } }); -// GET calculate expected whelping date +// GET whelping calculator (standalone) router.get('/whelping-calculator', (req, res) => { try { const { breeding_date } = req.query; - if (!breeding_date) { return res.status(400).json({ error: 'Breeding date is required' }); } - const breedDate = new Date(breeding_date); - - // Average gestation: 63 days, range 58-68 days - const expectedDate = new Date(breedDate); - expectedDate.setDate(expectedDate.getDate() + 63); - - const earliestDate = new Date(breedDate); - earliestDate.setDate(earliestDate.getDate() + 58); - - const latestDate = new Date(breedDate); - latestDate.setDate(latestDate.getDate() + 68); - + const addDays = (d, n) => { const r = new Date(d); r.setDate(r.getDate() + n); return r.toISOString().split('T')[0]; }; res.json({ - breeding_date: breeding_date, - expected_whelping_date: expectedDate.toISOString().split('T')[0], - earliest_date: earliestDate.toISOString().split('T')[0], - latest_date: latestDate.toISOString().split('T')[0], + breeding_date, + expected_whelping_date: addDays(breedDate, 63), + earliest_date: addDays(breedDate, 58), + latest_date: addDays(breedDate, 68), gestation_days: 63 }); } catch (error) { @@ -130,4 +209,4 @@ router.get('/whelping-calculator', (req, res) => { } }); -module.exports = router; \ No newline at end of file +module.exports = router;