medication tracking started
This commit is contained in:
@@ -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' });
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user