medication tracking started

This commit is contained in:
blaisadmin
2026-04-19 02:30:22 -04:00
parent 1ff8100b2c
commit 263b98d3d8
6 changed files with 624 additions and 3 deletions
+135
View File
@@ -30,18 +30,22 @@ import {
import {
completePendingBirdTransfersForOwner,
createBird,
createMedicationForBird,
createPendingBirdTransfer,
findBirdsByBandId,
createVetVisitForBird,
createWeightForBird,
deleteBird,
deleteMedicationForBird,
deleteVetVisitForBird,
getBirdById,
listBirds,
listMedicationsForBird,
listVetVisitsForBird,
listWeightsForBird,
transferBirdToWorkspace,
updateBird,
updateMedicationForBird,
updateVetVisitForBird,
} from './repositories/birdRepository.js';
import { createIntegrationTokenRecord, listIntegrationTokens, revokeIntegrationToken } from './repositories/integrationTokenRepository.js';
@@ -78,6 +82,7 @@ import type {
BirdRow,
IntegrationTokenRow,
LostBirdMatchRow,
MedicationRow,
ProviderKey,
RescueVerificationStatus,
SubscriptionStatus,
@@ -218,6 +223,21 @@ const vetVisitSchema = z.object({
notes: z.string().trim().max(1000).optional().or(z.literal('')),
});
const medicationSchema = z
.object({
name: z.string().trim().min(1).max(160),
dosage: z.string().trim().min(1).max(160),
frequency: z.string().trim().min(1).max(160),
route: z.string().trim().max(80).optional().or(z.literal('')),
startDate: dateStringSchema,
endDate: dateStringSchema.optional().or(z.literal('')),
notes: z.string().trim().max(1000).optional().or(z.literal('')),
})
.refine((value) => !value.endDate || value.endDate >= value.startDate, {
message: 'End date must be on or after start date.',
path: ['endDate'],
});
const integrationTokenCreateSchema = z.object({
name: z.string().trim().min(1).max(160),
scope: integrationTokenScopeSchema.default('read_write'),
@@ -409,6 +429,18 @@ const normalizeVetVisit = (row: VetVisitRow) => ({
notes: row.notes,
});
const normalizeMedication = (row: MedicationRow) => ({
id: row.id,
birdId: row.bird_id,
name: row.name,
dosage: row.dosage,
frequency: row.frequency,
route: row.route,
startDate: row.start_date,
endDate: row.end_date,
notes: row.notes,
});
const normalizeIntegrationToken = (row: IntegrationTokenRow) => ({
id: row.id,
userId: row.user_id,
@@ -2147,6 +2179,109 @@ app.delete('/api/birds/:birdId/vet-visits/:visitId', requireAuth, requireWriteAc
}
});
app.get('/api/birds/:birdId/medications', requireAuth, async (req: Request, res: Response, next: NextFunction) => {
try {
const medications = await listMedicationsForBird(req.params.birdId, req.auth!.workspace.id);
res.json({ medications: medications.map(normalizeMedication) });
} catch (error) {
next(error);
}
});
app.post('/api/birds/:birdId/medications', requireAuth, requireWriteAccess, requireWorkspaceRole(['owner', 'assistant', 'caregiver']), async (req: Request, res: Response, next: NextFunction) => {
const parsed = medicationSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ error: 'Invalid medication payload', details: parsed.error.flatten() });
return;
}
try {
const bird = await getBirdById(req.params.birdId, req.auth!.workspace.id);
if (!bird) {
res.status(404).json({ error: 'Bird not found.' });
return;
}
const medication = await createMedicationForBird(
req.params.birdId,
parsed.data.name,
parsed.data.dosage,
parsed.data.frequency,
emptyToNull(parsed.data.route),
parsed.data.startDate,
emptyToNull(parsed.data.endDate),
emptyToNull(parsed.data.notes),
);
res.status(201).json({ medication: normalizeMedication(medication!) });
} catch (error) {
next(error);
}
});
app.put('/api/birds/:birdId/medications/:medicationId', requireAuth, requireWriteAccess, requireWorkspaceRole(['owner', 'assistant', 'caregiver']), async (req: Request, res: Response, next: NextFunction) => {
const parsed = medicationSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ error: 'Invalid medication payload', details: parsed.error.flatten() });
return;
}
try {
const bird = await getBirdById(req.params.birdId, req.auth!.workspace.id);
if (!bird) {
res.status(404).json({ error: 'Bird not found.' });
return;
}
const medication = await updateMedicationForBird(
req.params.medicationId,
req.params.birdId,
parsed.data.name,
parsed.data.dosage,
parsed.data.frequency,
emptyToNull(parsed.data.route),
parsed.data.startDate,
emptyToNull(parsed.data.endDate),
emptyToNull(parsed.data.notes),
);
if (!medication) {
res.status(404).json({ error: 'Medication not found.' });
return;
}
res.json({ medication: normalizeMedication(medication) });
} catch (error) {
next(error);
}
});
app.delete('/api/birds/:birdId/medications/:medicationId', requireAuth, requireWriteAccess, requireWorkspaceRole(['owner', 'assistant', 'caregiver']), async (req: Request, res: Response, next: NextFunction) => {
try {
const bird = await getBirdById(req.params.birdId, req.auth!.workspace.id);
if (!bird) {
res.status(404).json({ error: 'Bird not found.' });
return;
}
const deleted = await deleteMedicationForBird(req.params.medicationId, req.params.birdId);
if (!deleted) {
res.status(404).json({ error: 'Medication not found.' });
return;
}
res.status(204).send();
} catch (error) {
next(error);
}
});
app.use((error: unknown, _req: Request, res: Response, _next: NextFunction) => {
console.error(error);
res.status(500).json({ error: error instanceof Error ? error.message : 'Internal server error' });