updated medicine schedules
This commit is contained in:
+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