updated medicine schedules
This commit is contained in:
+17
-1
@@ -230,7 +230,17 @@ 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),
|
||||
frequency: z.enum(['once_daily', 'twice_daily', 'every_8_hours', 'every_6_hours', 'as_needed']),
|
||||
doseSchedule: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string().trim().min(1).max(80),
|
||||
label: z.string().trim().min(1).max(80),
|
||||
time: z.string().trim().regex(/^$|^\d{2}:\d{2}$/),
|
||||
}),
|
||||
)
|
||||
.min(1)
|
||||
.max(8),
|
||||
route: z.string().trim().max(80).optional().or(z.literal('')),
|
||||
startDate: dateStringSchema,
|
||||
endDate: dateStringSchema.optional().or(z.literal('')),
|
||||
@@ -243,6 +253,7 @@ const medicationSchema = z
|
||||
|
||||
const medicationAdministrationSchema = z.object({
|
||||
administeredOn: dateStringSchema,
|
||||
administrationSlot: z.string().trim().min(1).max(80).default('dose-1'),
|
||||
status: z.enum(['administered', 'missed']),
|
||||
notes: z.string().trim().max(500).optional().or(z.literal('')),
|
||||
});
|
||||
@@ -444,6 +455,7 @@ const normalizeMedication = (row: MedicationRow) => ({
|
||||
name: row.name,
|
||||
dosage: row.dosage,
|
||||
frequency: row.frequency,
|
||||
doseSchedule: row.dose_schedule,
|
||||
route: row.route,
|
||||
startDate: row.start_date,
|
||||
endDate: row.end_date,
|
||||
@@ -455,6 +467,7 @@ const normalizeMedicationAdministration = (row: MedicationAdministrationRow) =>
|
||||
medicationId: row.medication_id,
|
||||
birdId: row.bird_id,
|
||||
administeredOn: row.administered_on,
|
||||
administrationSlot: row.administration_slot,
|
||||
status: row.status,
|
||||
notes: row.notes,
|
||||
createdByUserId: row.created_by_user_id,
|
||||
@@ -2229,6 +2242,7 @@ app.post('/api/birds/:birdId/medications', requireAuth, requireWriteAccess, requ
|
||||
parsed.data.name,
|
||||
parsed.data.dosage,
|
||||
parsed.data.frequency,
|
||||
parsed.data.doseSchedule,
|
||||
emptyToNull(parsed.data.route),
|
||||
parsed.data.startDate,
|
||||
emptyToNull(parsed.data.endDate),
|
||||
@@ -2263,6 +2277,7 @@ app.put('/api/birds/:birdId/medications/:medicationId', requireAuth, requireWrit
|
||||
parsed.data.name,
|
||||
parsed.data.dosage,
|
||||
parsed.data.frequency,
|
||||
parsed.data.doseSchedule,
|
||||
emptyToNull(parsed.data.route),
|
||||
parsed.data.startDate,
|
||||
emptyToNull(parsed.data.endDate),
|
||||
@@ -2325,6 +2340,7 @@ app.post('/api/birds/:birdId/medications/:medicationId/administrations', require
|
||||
req.params.birdId,
|
||||
req.auth!.workspace.id,
|
||||
parsed.data.administeredOn,
|
||||
parsed.data.administrationSlot,
|
||||
parsed.data.status,
|
||||
emptyToNull(parsed.data.notes),
|
||||
req.auth!.user.id,
|
||||
|
||||
@@ -287,6 +287,7 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
|
||||
name VARCHAR(160) NOT NULL,
|
||||
dosage VARCHAR(160) NOT NULL,
|
||||
frequency VARCHAR(160) NOT NULL,
|
||||
dose_schedule JSONB NOT NULL DEFAULT '[{"key":"dose-1","label":"Dose","time":""}]'::jsonb,
|
||||
route VARCHAR(80),
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE,
|
||||
@@ -295,18 +296,30 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
|
||||
CHECK (end_date IS NULL OR end_date >= start_date)
|
||||
);
|
||||
|
||||
ALTER TABLE medications
|
||||
ADD COLUMN IF NOT EXISTS dose_schedule JSONB NOT NULL DEFAULT '[{"key":"dose-1","label":"Dose","time":""}]'::jsonb;
|
||||
|
||||
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,
|
||||
administration_slot VARCHAR(80) NOT NULL DEFAULT 'dose-1',
|
||||
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)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
ALTER TABLE medication_administrations
|
||||
ADD COLUMN IF NOT EXISTS administration_slot VARCHAR(80) NOT NULL DEFAULT 'dose-1';
|
||||
|
||||
ALTER TABLE medication_administrations
|
||||
DROP CONSTRAINT IF EXISTS medication_administrations_medication_id_administered_on_key;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_medication_administrations_unique_slot
|
||||
ON medication_administrations (medication_id, administered_on, administration_slot);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_weight_records_bird_recorded_on
|
||||
ON weight_records (bird_id, recorded_on DESC);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
BirdRow,
|
||||
LostBirdMatchRow,
|
||||
MedicationAdministrationRow,
|
||||
MedicationDoseScheduleItem,
|
||||
MedicationRow,
|
||||
PendingBirdTransferRow,
|
||||
VetVisitRow,
|
||||
@@ -415,7 +416,7 @@ export const deleteVetVisitForBird = async (visitId: string, birdId: string) =>
|
||||
|
||||
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
|
||||
`SELECT id, bird_id, name, dosage, frequency, dose_schedule, route, start_date::text, end_date::text, notes
|
||||
FROM medications
|
||||
WHERE bird_id = $1
|
||||
AND EXISTS (
|
||||
@@ -436,16 +437,17 @@ export const createMedicationForBird = async (
|
||||
name: string,
|
||||
dosage: string,
|
||||
frequency: string,
|
||||
doseSchedule: MedicationDoseScheduleItem[],
|
||||
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],
|
||||
`INSERT INTO medications (bird_id, name, dosage, frequency, dose_schedule, route, start_date, end_date, notes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id, bird_id, name, dosage, frequency, dose_schedule, route, start_date::text, end_date::text, notes`,
|
||||
[birdId, name, dosage, frequency, JSON.stringify(doseSchedule), route, startDate, endDate, notes],
|
||||
);
|
||||
|
||||
return result.rows[0] ?? null;
|
||||
@@ -457,6 +459,7 @@ export const updateMedicationForBird = async (
|
||||
name: string,
|
||||
dosage: string,
|
||||
frequency: string,
|
||||
doseSchedule: MedicationDoseScheduleItem[],
|
||||
route: string | null,
|
||||
startDate: string,
|
||||
endDate: string | null,
|
||||
@@ -467,14 +470,15 @@ export const updateMedicationForBird = async (
|
||||
SET name = $3,
|
||||
dosage = $4,
|
||||
frequency = $5,
|
||||
route = $6,
|
||||
start_date = $7,
|
||||
end_date = $8,
|
||||
notes = $9
|
||||
dose_schedule = $6,
|
||||
route = $7,
|
||||
start_date = $8,
|
||||
end_date = $9,
|
||||
notes = $10
|
||||
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],
|
||||
RETURNING id, bird_id, name, dosage, frequency, dose_schedule, route, start_date::text, end_date::text, notes`,
|
||||
[medicationId, birdId, name, dosage, frequency, JSON.stringify(doseSchedule), route, startDate, endDate, notes],
|
||||
);
|
||||
|
||||
return result.rows[0] ?? null;
|
||||
@@ -494,7 +498,7 @@ export const deleteMedicationForBird = async (medicationId: string, birdId: stri
|
||||
|
||||
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
|
||||
`SELECT id, medication_id, bird_id, administered_on::text, administration_slot, status, notes, created_by_user_id, created_at
|
||||
FROM medication_administrations
|
||||
WHERE bird_id = $1
|
||||
AND EXISTS (
|
||||
@@ -515,13 +519,14 @@ export const upsertMedicationAdministrationForBird = async (
|
||||
birdId: string,
|
||||
workspaceId: number,
|
||||
administeredOn: string,
|
||||
administrationSlot: 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
|
||||
`INSERT INTO medication_administrations (medication_id, bird_id, administered_on, administration_slot, status, notes, created_by_user_id)
|
||||
SELECT $1, $2, $4, $5, $6, $7, $8
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM medications
|
||||
@@ -530,13 +535,13 @@ export const upsertMedicationAdministrationForBird = async (
|
||||
AND medications.bird_id = $2
|
||||
AND birds.workspace_id = $3
|
||||
)
|
||||
ON CONFLICT (medication_id, administered_on)
|
||||
ON CONFLICT (medication_id, administered_on, administration_slot)
|
||||
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],
|
||||
RETURNING id, medication_id, bird_id, administered_on::text, administration_slot, status, notes, created_by_user_id, created_at`,
|
||||
[medicationId, birdId, workspaceId, administeredOn, administrationSlot, status, notes, createdByUserId],
|
||||
);
|
||||
|
||||
return result.rows[0] ?? null;
|
||||
|
||||
@@ -150,17 +150,25 @@ export type MedicationRow = {
|
||||
name: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
dose_schedule: MedicationDoseScheduleItem[];
|
||||
route: string | null;
|
||||
start_date: string;
|
||||
end_date: string | null;
|
||||
notes: string | null;
|
||||
};
|
||||
|
||||
export type MedicationDoseScheduleItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
time: string;
|
||||
};
|
||||
|
||||
export type MedicationAdministrationRow = {
|
||||
id: string;
|
||||
medication_id: string;
|
||||
bird_id: string;
|
||||
administered_on: string;
|
||||
administration_slot: string;
|
||||
status: 'administered' | 'missed';
|
||||
notes: string | null;
|
||||
created_by_user_id: string | null;
|
||||
|
||||
+73
-2
@@ -254,7 +254,19 @@ Role requirements are called out per endpoint below. If the signed-in member lac
|
||||
"birdId": "uuid",
|
||||
"name": "Meloxicam",
|
||||
"dosage": "0.05 mL",
|
||||
"frequency": "Every 12 hours",
|
||||
"frequency": "twice_daily",
|
||||
"doseSchedule": [
|
||||
{
|
||||
"key": "dose-1",
|
||||
"label": "Morning",
|
||||
"time": "08:00"
|
||||
},
|
||||
{
|
||||
"key": "dose-2",
|
||||
"label": "Evening",
|
||||
"time": "20:00"
|
||||
}
|
||||
],
|
||||
"route": "Oral",
|
||||
"startDate": "2026-04-14",
|
||||
"endDate": null,
|
||||
@@ -262,6 +274,22 @@ Role requirements are called out per endpoint below. If the signed-in member lac
|
||||
}
|
||||
```
|
||||
|
||||
### Medication Administration
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"medicationId": "uuid",
|
||||
"birdId": "uuid",
|
||||
"administeredOn": "2026-04-14",
|
||||
"administrationSlot": "dose-1",
|
||||
"status": "administered",
|
||||
"notes": null,
|
||||
"createdByUserId": "uuid",
|
||||
"createdAt": "2026-04-14T12:34:56.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Common Validation Rules
|
||||
|
||||
- Dates use `YYYY-MM-DD`
|
||||
@@ -926,7 +954,19 @@ Request body:
|
||||
{
|
||||
"name": "Meloxicam",
|
||||
"dosage": "0.05 mL",
|
||||
"frequency": "Every 12 hours",
|
||||
"frequency": "twice_daily",
|
||||
"doseSchedule": [
|
||||
{
|
||||
"key": "dose-1",
|
||||
"label": "Morning",
|
||||
"time": "08:00"
|
||||
},
|
||||
{
|
||||
"key": "dose-2",
|
||||
"label": "Evening",
|
||||
"time": "20:00"
|
||||
}
|
||||
],
|
||||
"route": "Oral",
|
||||
"startDate": "2026-04-14",
|
||||
"endDate": "",
|
||||
@@ -950,6 +990,37 @@ Requires auth with write access and role `owner`, `assistant`, or `caregiver`. U
|
||||
|
||||
Requires auth with write access and role `owner`, `assistant`, or `caregiver`. Deletes a medication record.
|
||||
|
||||
#### `GET /api/birds/:birdId/medication-administrations`
|
||||
|
||||
Requires auth. Lists medication administration events for a bird in the active workspace.
|
||||
|
||||
Response `200`:
|
||||
|
||||
```json
|
||||
{
|
||||
"administrations": []
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /api/birds/:birdId/medications/:medicationId/administrations`
|
||||
|
||||
Requires auth with write access and role `owner`, `assistant`, or `caregiver`. Upserts one scheduled medication event for a date and interval slot.
|
||||
|
||||
Request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"administeredOn": "2026-04-14",
|
||||
"administrationSlot": "dose-1",
|
||||
"status": "administered",
|
||||
"notes": ""
|
||||
}
|
||||
```
|
||||
|
||||
`status` is `administered` or `missed`. `administrationSlot` identifies the interval event for that day, such as `dose-1` or `dose-2`.
|
||||
|
||||
Medication `frequency` is one of `once_daily`, `twice_daily`, `every_8_hours`, `every_6_hours`, or `as_needed`. `doseSchedule` stores the editable labels and optional 24-hour `HH:MM` times used by administration slots.
|
||||
|
||||
Possible errors:
|
||||
|
||||
- `400` if `endDate` is before `startDate`
|
||||
|
||||
+195
-44
@@ -52,18 +52,28 @@ type Medication = {
|
||||
birdId: string;
|
||||
name: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
frequency: MedicationFrequency;
|
||||
doseSchedule: MedicationDoseScheduleItem[];
|
||||
route: string | null;
|
||||
startDate: string;
|
||||
endDate: string | null;
|
||||
notes: string | null;
|
||||
};
|
||||
|
||||
type MedicationFrequency = 'once_daily' | 'twice_daily' | 'every_8_hours' | 'every_6_hours' | 'as_needed';
|
||||
|
||||
type MedicationDoseScheduleItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
time: string;
|
||||
};
|
||||
|
||||
type MedicationAdministration = {
|
||||
id: string;
|
||||
medicationId: string;
|
||||
birdId: string;
|
||||
administeredOn: string;
|
||||
administrationSlot: string;
|
||||
status: 'administered' | 'missed';
|
||||
notes: string | null;
|
||||
createdAt: string;
|
||||
@@ -471,6 +481,37 @@ const PHOTO_PREVIEW_SIZE = 112;
|
||||
const MEMBER_CHART_WIDTH = 520;
|
||||
const MEMBER_CHART_HEIGHT = 180;
|
||||
const MEMBER_CHART_PADDING = { top: 16, right: 18, bottom: 34, left: 52 };
|
||||
const medicationFrequencyOptions: { value: MedicationFrequency; label: string; doseSchedule: MedicationDoseScheduleItem[] }[] = [
|
||||
{ value: 'once_daily', label: 'Once daily', doseSchedule: [{ key: 'dose-1', label: 'Morning', time: '08:00' }] },
|
||||
{
|
||||
value: 'twice_daily',
|
||||
label: 'Twice daily',
|
||||
doseSchedule: [
|
||||
{ key: 'dose-1', label: 'Morning', time: '08:00' },
|
||||
{ key: 'dose-2', label: 'Evening', time: '20:00' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'every_8_hours',
|
||||
label: 'Every 8 hours',
|
||||
doseSchedule: [
|
||||
{ key: 'dose-1', label: 'Morning', time: '06:00' },
|
||||
{ key: 'dose-2', label: 'Afternoon', time: '14:00' },
|
||||
{ key: 'dose-3', label: 'Night', time: '22:00' },
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'every_6_hours',
|
||||
label: 'Every 6 hours',
|
||||
doseSchedule: [
|
||||
{ key: 'dose-1', label: 'Early morning', time: '06:00' },
|
||||
{ key: 'dose-2', label: 'Midday', time: '12:00' },
|
||||
{ key: 'dose-3', label: 'Evening', time: '18:00' },
|
||||
{ key: 'dose-4', label: 'Night', time: '00:00' },
|
||||
],
|
||||
},
|
||||
{ value: 'as_needed', label: 'As needed', doseSchedule: [{ key: 'dose-1', label: 'As needed', time: '' }] },
|
||||
];
|
||||
|
||||
const readJsonSafely = async <T,>(response: Response): Promise<T | null> => {
|
||||
const contentType = response.headers.get('content-type') ?? '';
|
||||
@@ -920,6 +961,46 @@ const buildMemberSeries = (
|
||||
});
|
||||
};
|
||||
|
||||
const getDefaultMedicationDoseSchedule = (frequency: MedicationFrequency) =>
|
||||
(medicationFrequencyOptions.find((option) => option.value === frequency)?.doseSchedule ?? medicationFrequencyOptions[0].doseSchedule).map((slot) => ({
|
||||
...slot,
|
||||
}));
|
||||
|
||||
const formatMedicationFrequency = (frequency: MedicationFrequency | string) =>
|
||||
medicationFrequencyOptions.find((option) => option.value === frequency)?.label ?? frequency;
|
||||
|
||||
const normalizeMedicationFrequency = (frequency: MedicationFrequency | string): MedicationFrequency => {
|
||||
if (medicationFrequencyOptions.some((option) => option.value === frequency)) {
|
||||
return frequency as MedicationFrequency;
|
||||
}
|
||||
|
||||
const normalizedFrequency = frequency.toLowerCase();
|
||||
if (normalizedFrequency.includes('12') || normalizedFrequency.includes('twice') || normalizedFrequency.includes('bid')) {
|
||||
return 'twice_daily';
|
||||
}
|
||||
if (normalizedFrequency.includes('8') || normalizedFrequency.includes('three') || normalizedFrequency.includes('tid')) {
|
||||
return 'every_8_hours';
|
||||
}
|
||||
if (normalizedFrequency.includes('6') || normalizedFrequency.includes('four') || normalizedFrequency.includes('qid')) {
|
||||
return 'every_6_hours';
|
||||
}
|
||||
if (normalizedFrequency.includes('needed') || normalizedFrequency.includes('prn')) {
|
||||
return 'as_needed';
|
||||
}
|
||||
return 'once_daily';
|
||||
};
|
||||
|
||||
const formatDoseTime = (time: string) => {
|
||||
if (!time) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const [hourValue, minuteValue] = time.split(':').map(Number);
|
||||
const date = new Date();
|
||||
date.setHours(hourValue, minuteValue, 0, 0);
|
||||
return new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: '2-digit' }).format(date);
|
||||
};
|
||||
|
||||
const assessBirdWeight = (bird: Bird): BirdWeightAssessment => {
|
||||
const reference = findParrotWeightReference(bird.species);
|
||||
|
||||
@@ -1038,7 +1119,8 @@ function App() {
|
||||
const [medicationForm, setMedicationForm] = useState({
|
||||
name: '',
|
||||
dosage: '',
|
||||
frequency: '',
|
||||
frequency: 'once_daily' as MedicationFrequency,
|
||||
doseSchedule: getDefaultMedicationDoseSchedule('once_daily'),
|
||||
route: '',
|
||||
startDate: new Date().toISOString().slice(0, 10),
|
||||
endDate: '',
|
||||
@@ -2581,7 +2663,8 @@ function App() {
|
||||
setMedicationForm({
|
||||
name: '',
|
||||
dosage: '',
|
||||
frequency: '',
|
||||
frequency: 'once_daily',
|
||||
doseSchedule: getDefaultMedicationDoseSchedule('once_daily'),
|
||||
route: '',
|
||||
startDate: new Date().toISOString().slice(0, 10),
|
||||
endDate: '',
|
||||
@@ -2594,11 +2677,13 @@ function App() {
|
||||
};
|
||||
|
||||
const handleEditMedication = (medication: Medication) => {
|
||||
const frequency = normalizeMedicationFrequency(medication.frequency);
|
||||
setEditingMedicationId(medication.id);
|
||||
setMedicationForm({
|
||||
name: medication.name,
|
||||
dosage: medication.dosage,
|
||||
frequency: medication.frequency,
|
||||
frequency,
|
||||
doseSchedule: medication.doseSchedule?.length ? medication.doseSchedule : getDefaultMedicationDoseSchedule(frequency),
|
||||
route: medication.route ?? '',
|
||||
startDate: medication.startDate,
|
||||
endDate: medication.endDate ?? '',
|
||||
@@ -2612,7 +2697,8 @@ function App() {
|
||||
setMedicationForm({
|
||||
name: '',
|
||||
dosage: '',
|
||||
frequency: '',
|
||||
frequency: 'once_daily',
|
||||
doseSchedule: getDefaultMedicationDoseSchedule('once_daily'),
|
||||
route: '',
|
||||
startDate: new Date().toISOString().slice(0, 10),
|
||||
endDate: '',
|
||||
@@ -2649,12 +2735,16 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleMedicationAdministrationSubmit = async (medicationId: string, status: MedicationAdministration['status']) => {
|
||||
const handleMedicationAdministrationSubmit = async (
|
||||
medicationId: string,
|
||||
administrationSlot: string,
|
||||
status: MedicationAdministration['status'],
|
||||
) => {
|
||||
if (!selectedBird || savingMedicationAdministrationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSavingMedicationAdministrationId(`${medicationId}-${status}`);
|
||||
setSavingMedicationAdministrationId(`${medicationId}-${administrationSlot}-${status}`);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
@@ -2662,7 +2752,7 @@ function App() {
|
||||
const response = await apiFetch(`/birds/${selectedBird.id}/medications/${medicationId}/administrations`, authToken, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ administeredOn, status }),
|
||||
body: JSON.stringify({ administeredOn, administrationSlot, status }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -2680,7 +2770,9 @@ function App() {
|
||||
(administration, index, all) =>
|
||||
all.findIndex(
|
||||
(candidate) =>
|
||||
candidate.medicationId === administration.medicationId && candidate.administeredOn === administration.administeredOn,
|
||||
candidate.medicationId === administration.medicationId &&
|
||||
candidate.administeredOn === administration.administeredOn &&
|
||||
candidate.administrationSlot === administration.administrationSlot,
|
||||
) === index,
|
||||
)
|
||||
.sort((left, right) => right.administeredOn.localeCompare(left.administeredOn) || right.createdAt.localeCompare(left.createdAt)),
|
||||
@@ -3294,18 +3386,14 @@ function App() {
|
||||
const showWorkspaceSwitcher = authSession.workspaces.length > 1;
|
||||
const todayDate = new Date().toISOString().slice(0, 10);
|
||||
const renderMedicationCard = (medication: Medication, options: { showActions?: boolean; showAdministrationControls?: boolean }) => {
|
||||
const doseSlots = medication.doseSchedule?.length ? medication.doseSchedule : getDefaultMedicationDoseSchedule(medication.frequency);
|
||||
const latestAdministration = medicationAdministrations.find((administration) => administration.medicationId === medication.id);
|
||||
const todayAdministration = medicationAdministrations.find(
|
||||
(administration) => administration.medicationId === medication.id && administration.administeredOn === todayDate,
|
||||
);
|
||||
const givenActionId = `${medication.id}-administered`;
|
||||
const missedActionId = `${medication.id}-missed`;
|
||||
|
||||
return (
|
||||
<article key={medication.id} className="vet-visit-card">
|
||||
<strong>{medication.name}</strong>
|
||||
<span>
|
||||
{medication.dosage} • {medication.frequency}
|
||||
{medication.dosage} • {formatMedicationFrequency(medication.frequency)}
|
||||
{medication.route ? ` • ${medication.route}` : ''}
|
||||
</span>
|
||||
<small>
|
||||
@@ -3314,35 +3402,51 @@ function App() {
|
||||
<small>{medication.notes || 'No notes recorded.'}</small>
|
||||
{latestAdministration ? (
|
||||
<small>
|
||||
Last update: {latestAdministration.status === 'administered' ? 'Given' : 'Missed'} on {formatShortDate(latestAdministration.administeredOn)}
|
||||
Last update: {latestAdministration.status === 'administered' ? 'Given' : 'Not administered'} on{' '}
|
||||
{formatShortDate(latestAdministration.administeredOn)}
|
||||
</small>
|
||||
) : null}
|
||||
{options.showAdministrationControls ? (
|
||||
<div className="medication-admin-actions">
|
||||
<small>
|
||||
Today:{' '}
|
||||
{todayAdministration
|
||||
? `${todayAdministration.status === 'administered' ? 'Given' : 'Missed'} on ${formatShortDate(todayAdministration.administeredOn)}`
|
||||
: 'Not updated yet'}
|
||||
</small>
|
||||
<div className="button-row">
|
||||
<button
|
||||
className="primary-button"
|
||||
onClick={() => handleMedicationAdministrationSubmit(medication.id, 'administered')}
|
||||
type="button"
|
||||
disabled={Boolean(savingMedicationAdministrationId)}
|
||||
>
|
||||
{savingMedicationAdministrationId === givenActionId ? 'Saving...' : 'Given today'}
|
||||
</button>
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={() => handleMedicationAdministrationSubmit(medication.id, 'missed')}
|
||||
type="button"
|
||||
disabled={Boolean(savingMedicationAdministrationId)}
|
||||
>
|
||||
{savingMedicationAdministrationId === missedActionId ? 'Saving...' : 'Missed today'}
|
||||
</button>
|
||||
</div>
|
||||
<small>Today's interval events</small>
|
||||
{doseSlots.map((slot) => {
|
||||
const todayAdministration = medicationAdministrations.find(
|
||||
(administration) =>
|
||||
administration.medicationId === medication.id &&
|
||||
administration.administeredOn === todayDate &&
|
||||
administration.administrationSlot === slot.key,
|
||||
);
|
||||
const givenActionId = `${medication.id}-${slot.key}-administered`;
|
||||
const missedActionId = `${medication.id}-${slot.key}-missed`;
|
||||
|
||||
return (
|
||||
<div className="medication-dose-row" key={slot.key}>
|
||||
<span>
|
||||
<strong>{slot.label}</strong>
|
||||
{slot.time ? <small>{formatDoseTime(slot.time)}</small> : null}
|
||||
<small>{todayAdministration ? (todayAdministration.status === 'administered' ? 'Administered' : 'Not administered') : 'Unmarked'}</small>
|
||||
</span>
|
||||
<div className="button-row">
|
||||
<button
|
||||
className="primary-button"
|
||||
onClick={() => handleMedicationAdministrationSubmit(medication.id, slot.key, 'administered')}
|
||||
type="button"
|
||||
disabled={Boolean(savingMedicationAdministrationId)}
|
||||
>
|
||||
{savingMedicationAdministrationId === givenActionId ? 'Saving...' : 'Administered'}
|
||||
</button>
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={() => handleMedicationAdministrationSubmit(medication.id, slot.key, 'missed')}
|
||||
type="button"
|
||||
disabled={Boolean(savingMedicationAdministrationId)}
|
||||
>
|
||||
{savingMedicationAdministrationId === missedActionId ? 'Saving...' : 'Not administered'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{options.showActions ? (
|
||||
@@ -5068,12 +5172,24 @@ function App() {
|
||||
</label>
|
||||
<label>
|
||||
Frequency
|
||||
<input
|
||||
<select
|
||||
value={medicationForm.frequency}
|
||||
onChange={(event) => setMedicationForm({ ...medicationForm, frequency: event.target.value })}
|
||||
placeholder="Every 12 hours"
|
||||
onChange={(event) => {
|
||||
const frequency = event.target.value as MedicationFrequency;
|
||||
setMedicationForm({
|
||||
...medicationForm,
|
||||
frequency,
|
||||
doseSchedule: getDefaultMedicationDoseSchedule(frequency),
|
||||
});
|
||||
}}
|
||||
required
|
||||
/>
|
||||
>
|
||||
{medicationFrequencyOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Route
|
||||
@@ -5100,6 +5216,41 @@ function App() {
|
||||
onChange={(event) => setMedicationForm({ ...medicationForm, endDate: event.target.value })}
|
||||
/>
|
||||
</label>
|
||||
<label className="wide-field">
|
||||
Dose labels and times
|
||||
<div className="dose-schedule-editor">
|
||||
{medicationForm.doseSchedule.map((slot, index) => (
|
||||
<div className="dose-schedule-row" key={slot.key}>
|
||||
<input
|
||||
value={slot.label}
|
||||
onChange={(event) =>
|
||||
setMedicationForm({
|
||||
...medicationForm,
|
||||
doseSchedule: medicationForm.doseSchedule.map((currentSlot, currentIndex) =>
|
||||
currentIndex === index ? { ...currentSlot, label: event.target.value } : currentSlot,
|
||||
),
|
||||
})
|
||||
}
|
||||
aria-label={`${slot.key} label`}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
type="time"
|
||||
value={slot.time}
|
||||
onChange={(event) =>
|
||||
setMedicationForm({
|
||||
...medicationForm,
|
||||
doseSchedule: medicationForm.doseSchedule.map((currentSlot, currentIndex) =>
|
||||
currentIndex === index ? { ...currentSlot, time: event.target.value } : currentSlot,
|
||||
),
|
||||
})
|
||||
}
|
||||
aria-label={`${slot.key} time`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</label>
|
||||
<label className="wide-field">
|
||||
Notes
|
||||
<textarea
|
||||
|
||||
@@ -1196,6 +1196,39 @@ textarea {
|
||||
padding-top: 0.35rem;
|
||||
}
|
||||
|
||||
.medication-dose-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 1fr) auto;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
padding: 0.7rem;
|
||||
border: 1px solid rgba(53, 129, 98, 0.18);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.46);
|
||||
}
|
||||
|
||||
.medication-dose-row > span {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.dose-schedule-editor {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.dose-schedule-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 150px;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dose-schedule-row input {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.form-panel {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
|
||||
Reference in New Issue
Block a user