updated medicine schedules

This commit is contained in:
blaisadmin
2026-04-19 19:42:01 -04:00
parent 872b6c8663
commit d1657ef7ed
7 changed files with 363 additions and 66 deletions
+17 -1
View File
@@ -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,
+15 -2
View File
@@ -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);
+22 -17
View File
@@ -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;
+8
View File
@@ -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
View File
@@ -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`
+181 -30
View File
@@ -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,36 +3402,52 @@ 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>
<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, 'administered')}
onClick={() => handleMedicationAdministrationSubmit(medication.id, slot.key, 'administered')}
type="button"
disabled={Boolean(savingMedicationAdministrationId)}
>
{savingMedicationAdministrationId === givenActionId ? 'Saving...' : 'Given today'}
{savingMedicationAdministrationId === givenActionId ? 'Saving...' : 'Administered'}
</button>
<button
className="secondary-button"
onClick={() => handleMedicationAdministrationSubmit(medication.id, 'missed')}
onClick={() => handleMedicationAdministrationSubmit(medication.id, slot.key, 'missed')}
type="button"
disabled={Boolean(savingMedicationAdministrationId)}
>
{savingMedicationAdministrationId === missedActionId ? 'Saving...' : 'Missed today'}
{savingMedicationAdministrationId === missedActionId ? 'Saving...' : 'Not administered'}
</button>
</div>
</div>
);
})}
</div>
) : null}
{options.showActions ? (
<div className="button-row">
@@ -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
+33
View File
@@ -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;