Finished medication tracking and UI enhancements

This commit is contained in:
Corey Blais
2026-04-19 13:20:02 -04:00
parent 263b98d3d8
commit 872b6c8663
6 changed files with 589 additions and 253 deletions
+59
View File
@@ -30,6 +30,7 @@ import {
import {
completePendingBirdTransfersForOwner,
createBird,
upsertMedicationAdministrationForBird,
createMedicationForBird,
createPendingBirdTransfer,
findBirdsByBandId,
@@ -40,6 +41,7 @@ import {
deleteVetVisitForBird,
getBirdById,
listBirds,
listMedicationAdministrationsForBird,
listMedicationsForBird,
listVetVisitsForBird,
listWeightsForBird,
@@ -83,6 +85,7 @@ import type {
IntegrationTokenRow,
LostBirdMatchRow,
MedicationRow,
MedicationAdministrationRow,
ProviderKey,
RescueVerificationStatus,
SubscriptionStatus,
@@ -238,6 +241,12 @@ const medicationSchema = z
path: ['endDate'],
});
const medicationAdministrationSchema = z.object({
administeredOn: dateStringSchema,
status: z.enum(['administered', 'missed']),
notes: z.string().trim().max(500).optional().or(z.literal('')),
});
const integrationTokenCreateSchema = z.object({
name: z.string().trim().min(1).max(160),
scope: integrationTokenScopeSchema.default('read_write'),
@@ -441,6 +450,17 @@ const normalizeMedication = (row: MedicationRow) => ({
notes: row.notes,
});
const normalizeMedicationAdministration = (row: MedicationAdministrationRow) => ({
id: row.id,
medicationId: row.medication_id,
birdId: row.bird_id,
administeredOn: row.administered_on,
status: row.status,
notes: row.notes,
createdByUserId: row.created_by_user_id,
createdAt: row.created_at,
});
const normalizeIntegrationToken = (row: IntegrationTokenRow) => ({
id: row.id,
userId: row.user_id,
@@ -2282,6 +2302,45 @@ app.delete('/api/birds/:birdId/medications/:medicationId', requireAuth, requireW
}
});
app.get('/api/birds/:birdId/medication-administrations', requireAuth, async (req: Request, res: Response, next: NextFunction) => {
try {
const administrations = await listMedicationAdministrationsForBird(req.params.birdId, req.auth!.workspace.id);
res.json({ administrations: administrations.map(normalizeMedicationAdministration) });
} catch (error) {
next(error);
}
});
app.post('/api/birds/:birdId/medications/:medicationId/administrations', requireAuth, requireWriteAccess, requireWorkspaceRole(['owner', 'assistant', 'caregiver']), async (req: Request, res: Response, next: NextFunction) => {
const parsed = medicationAdministrationSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ error: 'Invalid medication administration payload', details: parsed.error.flatten() });
return;
}
try {
const administration = await upsertMedicationAdministrationForBird(
req.params.medicationId,
req.params.birdId,
req.auth!.workspace.id,
parsed.data.administeredOn,
parsed.data.status,
emptyToNull(parsed.data.notes),
req.auth!.user.id,
);
if (!administration) {
res.status(404).json({ error: 'Medication not found.' });
return;
}
res.status(201).json({ administration: normalizeMedicationAdministration(administration) });
} 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' });