Finished medication tracking and UI enhancements
This commit is contained in:
@@ -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' });
|
||||
|
||||
@@ -295,6 +295,18 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
|
||||
CHECK (end_date IS NULL OR end_date >= start_date)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS medication_administrations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
medication_id UUID NOT NULL REFERENCES medications(id) ON DELETE CASCADE,
|
||||
bird_id UUID NOT NULL REFERENCES birds(id) ON DELETE CASCADE,
|
||||
administered_on DATE NOT NULL,
|
||||
status VARCHAR(20) NOT NULL CHECK (status IN ('administered', 'missed')),
|
||||
notes VARCHAR(500),
|
||||
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (medication_id, administered_on)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_weight_records_bird_recorded_on
|
||||
ON weight_records (bird_id, recorded_on DESC);
|
||||
|
||||
@@ -304,6 +316,9 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
|
||||
CREATE INDEX IF NOT EXISTS idx_medications_bird_start_date
|
||||
ON medications (bird_id, start_date DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_medication_administrations_bird_administered_on
|
||||
ON medication_administrations (bird_id, administered_on DESC);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { db } from '../db/client.js';
|
||||
import type { BirdGender, BirdRow, LostBirdMatchRow, MedicationRow, PendingBirdTransferRow, VetVisitRow, WeightRow } from '../types.js';
|
||||
import type {
|
||||
BirdGender,
|
||||
BirdRow,
|
||||
LostBirdMatchRow,
|
||||
MedicationAdministrationRow,
|
||||
MedicationRow,
|
||||
PendingBirdTransferRow,
|
||||
VetVisitRow,
|
||||
WeightRow,
|
||||
} from '../types.js';
|
||||
|
||||
const birdSelectFields = `
|
||||
birds.id,
|
||||
@@ -482,3 +491,53 @@ export const deleteMedicationForBird = async (medicationId: string, birdId: stri
|
||||
|
||||
return (result.rowCount ?? 0) > 0;
|
||||
};
|
||||
|
||||
export const listMedicationAdministrationsForBird = async (birdId: string, workspaceId: number) => {
|
||||
const result = await db.query<MedicationAdministrationRow>(
|
||||
`SELECT id, medication_id, bird_id, administered_on::text, status, notes, created_by_user_id, created_at
|
||||
FROM medication_administrations
|
||||
WHERE bird_id = $1
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM birds
|
||||
WHERE birds.id = medication_administrations.bird_id
|
||||
AND birds.workspace_id = $2
|
||||
)
|
||||
ORDER BY administered_on DESC, created_at DESC`,
|
||||
[birdId, workspaceId],
|
||||
);
|
||||
|
||||
return result.rows;
|
||||
};
|
||||
|
||||
export const upsertMedicationAdministrationForBird = async (
|
||||
medicationId: string,
|
||||
birdId: string,
|
||||
workspaceId: number,
|
||||
administeredOn: string,
|
||||
status: 'administered' | 'missed',
|
||||
notes: string | null,
|
||||
createdByUserId: string | null,
|
||||
) => {
|
||||
const result = await db.query<MedicationAdministrationRow>(
|
||||
`INSERT INTO medication_administrations (medication_id, bird_id, administered_on, status, notes, created_by_user_id)
|
||||
SELECT $1, $2, $4, $5, $6, $7
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM medications
|
||||
JOIN birds ON birds.id = medications.bird_id
|
||||
WHERE medications.id = $1
|
||||
AND medications.bird_id = $2
|
||||
AND birds.workspace_id = $3
|
||||
)
|
||||
ON CONFLICT (medication_id, administered_on)
|
||||
DO UPDATE SET status = EXCLUDED.status,
|
||||
notes = EXCLUDED.notes,
|
||||
created_by_user_id = EXCLUDED.created_by_user_id,
|
||||
created_at = CURRENT_TIMESTAMP
|
||||
RETURNING id, medication_id, bird_id, administered_on::text, status, notes, created_by_user_id, created_at`,
|
||||
[medicationId, birdId, workspaceId, administeredOn, status, notes, createdByUserId],
|
||||
);
|
||||
|
||||
return result.rows[0] ?? null;
|
||||
};
|
||||
|
||||
@@ -156,6 +156,17 @@ export type MedicationRow = {
|
||||
notes: string | null;
|
||||
};
|
||||
|
||||
export type MedicationAdministrationRow = {
|
||||
id: string;
|
||||
medication_id: string;
|
||||
bird_id: string;
|
||||
administered_on: string;
|
||||
status: 'administered' | 'missed';
|
||||
notes: string | null;
|
||||
created_by_user_id: string | null;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type AuthContext = {
|
||||
user: UserRow;
|
||||
session: AuthSessionRow;
|
||||
|
||||
Reference in New Issue
Block a user