feat: Add breeding date suggestion window endpoint
This commit is contained in:
@@ -11,55 +11,149 @@ router.get('/heat-cycles/dog/:dogId', (req, res) => {
|
|||||||
WHERE dog_id = ?
|
WHERE dog_id = ?
|
||||||
ORDER BY start_date DESC
|
ORDER BY start_date DESC
|
||||||
`).all(req.params.dogId);
|
`).all(req.params.dogId);
|
||||||
|
|
||||||
res.json(cycles);
|
res.json(cycles);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
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) => {
|
router.get('/heat-cycles/active', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
const cycles = db.prepare(`
|
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
|
FROM heat_cycles hc
|
||||||
JOIN dogs d ON hc.dog_id = d.id
|
JOIN dogs d ON hc.dog_id = d.id
|
||||||
WHERE hc.end_date IS NULL OR hc.end_date >= date('now', '-30 days')
|
WHERE hc.end_date IS NULL OR hc.end_date >= date('now', '-30 days')
|
||||||
ORDER BY hc.start_date DESC
|
ORDER BY hc.start_date DESC
|
||||||
`).all();
|
`).all();
|
||||||
|
|
||||||
res.json(cycles);
|
res.json(cycles);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
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
|
// POST create heat cycle
|
||||||
router.post('/heat-cycles', (req, res) => {
|
router.post('/heat-cycles', (req, res) => {
|
||||||
try {
|
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) {
|
if (!dog_id || !start_date) {
|
||||||
return res.status(400).json({ error: 'Dog ID and start date are required' });
|
return res.status(400).json({ error: 'Dog ID and start date are required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = getDatabase();
|
const db = getDatabase();
|
||||||
|
|
||||||
// Verify dog is female
|
// Verify dog is female
|
||||||
const dog = db.prepare('SELECT sex FROM dogs WHERE id = ?').get(dog_id);
|
const dog = db.prepare('SELECT sex FROM dogs WHERE id = ?').get(dog_id);
|
||||||
if (!dog || dog.sex !== 'female') {
|
if (!dog || dog.sex !== 'female') {
|
||||||
return res.status(400).json({ error: 'Dog must be female' });
|
return res.status(400).json({ error: 'Dog must be female' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = db.prepare(`
|
const result = db.prepare(`
|
||||||
INSERT INTO heat_cycles (dog_id, start_date, end_date, progesterone_peak_date, breeding_date, breeding_successful, notes)
|
INSERT INTO heat_cycles (dog_id, start_date, end_date, breeding_date, breeding_successful, notes)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`).run(dog_id, start_date, end_date, progesterone_peak_date, breeding_date, breeding_successful || 0, notes);
|
`).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);
|
const cycle = db.prepare('SELECT * FROM heat_cycles WHERE id = ?').get(result.lastInsertRowid);
|
||||||
|
|
||||||
res.status(201).json(cycle);
|
res.status(201).json(cycle);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -69,16 +163,13 @@ router.post('/heat-cycles', (req, res) => {
|
|||||||
// PUT update heat cycle
|
// PUT update heat cycle
|
||||||
router.put('/heat-cycles/:id', (req, res) => {
|
router.put('/heat-cycles/:id', (req, res) => {
|
||||||
try {
|
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();
|
const db = getDatabase();
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
UPDATE heat_cycles
|
UPDATE heat_cycles
|
||||||
SET start_date = ?, end_date = ?, progesterone_peak_date = ?,
|
SET start_date = ?, end_date = ?, breeding_date = ?, breeding_successful = ?, notes = ?
|
||||||
breeding_date = ?, breeding_successful = ?, notes = ?
|
|
||||||
WHERE id = ?
|
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);
|
const cycle = db.prepare('SELECT * FROM heat_cycles WHERE id = ?').get(req.params.id);
|
||||||
res.json(cycle);
|
res.json(cycle);
|
||||||
} catch (error) {
|
} 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) => {
|
router.get('/whelping-calculator', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { breeding_date } = req.query;
|
const { breeding_date } = req.query;
|
||||||
|
|
||||||
if (!breeding_date) {
|
if (!breeding_date) {
|
||||||
return res.status(400).json({ error: 'Breeding date is required' });
|
return res.status(400).json({ error: 'Breeding date is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const breedDate = new Date(breeding_date);
|
const breedDate = new Date(breeding_date);
|
||||||
|
const addDays = (d, n) => { const r = new Date(d); r.setDate(r.getDate() + n); return r.toISOString().split('T')[0]; };
|
||||||
// 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);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
breeding_date: breeding_date,
|
breeding_date,
|
||||||
expected_whelping_date: expectedDate.toISOString().split('T')[0],
|
expected_whelping_date: addDays(breedDate, 63),
|
||||||
earliest_date: earliestDate.toISOString().split('T')[0],
|
earliest_date: addDays(breedDate, 58),
|
||||||
latest_date: latestDate.toISOString().split('T')[0],
|
latest_date: addDays(breedDate, 68),
|
||||||
gestation_days: 63
|
gestation_days: 63
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -130,4 +209,4 @@ router.get('/whelping-calculator', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
Reference in New Issue
Block a user