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' });
+17
View File
@@ -281,12 +281,29 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS medications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bird_id UUID NOT NULL REFERENCES birds(id) ON DELETE CASCADE,
name VARCHAR(160) NOT NULL,
dosage VARCHAR(160) NOT NULL,
frequency VARCHAR(160) NOT NULL,
route VARCHAR(80),
start_date DATE NOT NULL,
end_date DATE,
notes VARCHAR(1000),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CHECK (end_date IS NULL OR end_date >= start_date)
);
CREATE INDEX IF NOT EXISTS idx_weight_records_bird_recorded_on
ON weight_records (bird_id, recorded_on DESC);
CREATE INDEX IF NOT EXISTS idx_vet_visits_bird_visited_on
ON vet_visits (bird_id, visited_on DESC);
CREATE INDEX IF NOT EXISTS idx_medications_bird_start_date
ON medications (bird_id, start_date DESC);
DO $$
BEGIN
IF EXISTS (
+80 -1
View File
@@ -1,5 +1,5 @@
import { db } from '../db/client.js';
import type { BirdGender, BirdRow, LostBirdMatchRow, PendingBirdTransferRow, VetVisitRow, WeightRow } from '../types.js';
import type { BirdGender, BirdRow, LostBirdMatchRow, MedicationRow, PendingBirdTransferRow, VetVisitRow, WeightRow } from '../types.js';
const birdSelectFields = `
birds.id,
@@ -403,3 +403,82 @@ export const deleteVetVisitForBird = async (visitId: string, birdId: string) =>
return (result.rowCount ?? 0) > 0;
};
export const listMedicationsForBird = async (birdId: string, workspaceId: number) => {
const result = await db.query<MedicationRow>(
`SELECT id, bird_id, name, dosage, frequency, route, start_date::text, end_date::text, notes
FROM medications
WHERE bird_id = $1
AND EXISTS (
SELECT 1
FROM birds
WHERE birds.id = medications.bird_id
AND birds.workspace_id = $2
)
ORDER BY COALESCE(end_date, '9999-12-31'::date) DESC, start_date DESC, created_at DESC`,
[birdId, workspaceId],
);
return result.rows;
};
export const createMedicationForBird = async (
birdId: string,
name: string,
dosage: string,
frequency: string,
route: string | null,
startDate: string,
endDate: string | null,
notes: string | null,
) => {
const result = await db.query<MedicationRow>(
`INSERT INTO medications (bird_id, name, dosage, frequency, route, start_date, end_date, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, bird_id, name, dosage, frequency, route, start_date::text, end_date::text, notes`,
[birdId, name, dosage, frequency, route, startDate, endDate, notes],
);
return result.rows[0] ?? null;
};
export const updateMedicationForBird = async (
medicationId: string,
birdId: string,
name: string,
dosage: string,
frequency: string,
route: string | null,
startDate: string,
endDate: string | null,
notes: string | null,
) => {
const result = await db.query<MedicationRow>(
`UPDATE medications
SET name = $3,
dosage = $4,
frequency = $5,
route = $6,
start_date = $7,
end_date = $8,
notes = $9
WHERE id = $1
AND bird_id = $2
RETURNING id, bird_id, name, dosage, frequency, route, start_date::text, end_date::text, notes`,
[medicationId, birdId, name, dosage, frequency, route, startDate, endDate, notes],
);
return result.rows[0] ?? null;
};
export const deleteMedicationForBird = async (medicationId: string, birdId: string) => {
const result = await db.query<{ id: string }>(
`DELETE FROM medications
WHERE id = $1
AND bird_id = $2
RETURNING id`,
[medicationId, birdId],
);
return (result.rowCount ?? 0) > 0;
};
+12
View File
@@ -144,6 +144,18 @@ export type VetVisitRow = {
notes: string | null;
};
export type MedicationRow = {
id: string;
bird_id: string;
name: string;
dosage: string;
frequency: string;
route: string | null;
start_date: string;
end_date: string | null;
notes: string | null;
};
export type AuthContext = {
user: UserRow;
session: AuthSessionRow;