medication tracking started

This commit is contained in:
blaisadmin
2026-04-19 02:30:22 -04:00
parent 1ff8100b2c
commit 263b98d3d8
6 changed files with 624 additions and 3 deletions
+310 -2
View File
@@ -47,6 +47,18 @@ type VetVisit = {
notes: string | null;
};
type Medication = {
id: string;
birdId: string;
name: string;
dosage: string;
frequency: string;
route: string | null;
startDate: string;
endDate: string | null;
notes: string | null;
};
type Workspace = {
id: number;
name: string;
@@ -968,6 +980,7 @@ function App() {
const [editingBirdId, setEditingBirdId] = useState<string>('');
const [weights, setWeights] = useState<WeightRecord[]>([]);
const [vetVisits, setVetVisits] = useState<VetVisit[]>([]);
const [medications, setMedications] = useState<Medication[]>([]);
const [allBirdWeights, setAllBirdWeights] = useState<Record<string, WeightRecord[]>>({});
const [allBirdVetVisits, setAllBirdVetVisits] = useState<Record<string, VetVisit[]>>({});
const [dismissedAlerts, setDismissedAlerts] = useState<DismissedAlertMap>({});
@@ -1011,6 +1024,15 @@ function App() {
reason: '',
notes: '',
});
const [medicationForm, setMedicationForm] = useState({
name: '',
dosage: '',
frequency: '',
route: '',
startDate: new Date().toISOString().slice(0, 10),
endDate: '',
notes: '',
});
const [flockTransferForm, setFlockTransferForm] = useState({
birdId: '',
destinationOwnerEmail: '',
@@ -1024,6 +1046,8 @@ function App() {
const [deletingBird, setDeletingBird] = useState(false);
const [editingVetVisitId, setEditingVetVisitId] = useState('');
const [deletingVetVisitId, setDeletingVetVisitId] = useState('');
const [editingMedicationId, setEditingMedicationId] = useState('');
const [deletingMedicationId, setDeletingMedicationId] = useState('');
const [removingWorkspaceMemberId, setRemovingWorkspaceMemberId] = useState('');
const [expandedSettingsSection, setExpandedSettingsSection] = useState<SettingsSection | null>(null);
@@ -1219,6 +1243,15 @@ function App() {
const vetVisitDueOverflowCount = Math.max(vetVisitDueBirds.length - 3, 0);
const vetVisitDueBirdIds = useMemo(() => new Set(vetVisitDueBirds.map((bird) => bird.id)), [vetVisitDueBirds]);
const activeMedications = useMemo(
() => medications.filter((medication) => !medication.endDate || parseDateValue(medication.endDate) >= parseDateValue(new Date().toISOString().slice(0, 10))),
[medications],
);
const pastMedications = useMemo(
() => medications.filter((medication) => medication.endDate && parseDateValue(medication.endDate) < parseDateValue(new Date().toISOString().slice(0, 10))),
[medications],
);
const filteredSpeciesOptions = useMemo(() => {
const query = birdForm.species.trim().toLowerCase();
@@ -1457,6 +1490,7 @@ function App() {
setBirds([]);
setWeights([]);
setVetVisits([]);
setMedications([]);
setAllBirdWeights({});
setAllBirdVetVisits({});
setSelectedBirdId('');
@@ -1624,22 +1658,25 @@ function App() {
if (!selectedBird?.id) {
setWeights([]);
setVetVisits([]);
setMedications([]);
return;
}
const loadBirdDetail = async () => {
try {
const [weightsResponse, visitsResponse] = await Promise.all([
const [weightsResponse, visitsResponse, medicationsResponse] = await Promise.all([
apiFetch(`/birds/${selectedBird.id}/weights?days=${OVERVIEW_HISTORY_DAYS}`, authToken),
apiFetch(`/birds/${selectedBird.id}/vet-visits`, authToken),
apiFetch(`/birds/${selectedBird.id}/medications`, authToken),
]);
if (!weightsResponse.ok || !visitsResponse.ok) {
if (!weightsResponse.ok || !visitsResponse.ok || !medicationsResponse.ok) {
throw new Error('Unable to load flock member details.');
}
const weightsData = (await readJsonSafely<{ weights?: WeightRecord[] }>(weightsResponse)) ?? {};
const visitsData = (await readJsonSafely<{ vetVisits?: VetVisit[] }>(visitsResponse)) ?? {};
const medicationsData = (await readJsonSafely<{ medications?: Medication[] }>(medicationsResponse)) ?? {};
setWeights(weightsData.weights ?? []);
const nextVetVisits = visitsData.vetVisits ?? [];
@@ -1648,8 +1685,11 @@ function App() {
...current,
[selectedBird.id]: nextVetVisits,
}));
setMedications(medicationsData.medications ?? []);
setEditingVetVisitId('');
setDeletingVetVisitId('');
setEditingMedicationId('');
setDeletingMedicationId('');
} catch (loadError) {
setError(loadError instanceof Error ? loadError.message : 'Unable to load flock member details.');
}
@@ -1873,6 +1913,7 @@ function App() {
setEditingBirdId('');
setWeights([]);
setVetVisits([]);
setMedications([]);
setAllBirdVetVisits({});
setActivePage('overview');
} catch (switchError) {
@@ -2477,6 +2518,116 @@ function App() {
}
};
const handleMedicationSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!selectedBird) {
return;
}
setError('');
try {
const isEditingMedication = Boolean(editingMedicationId);
const response = await apiFetch(
isEditingMedication ? `/birds/${selectedBird.id}/medications/${editingMedicationId}` : `/birds/${selectedBird.id}/medications`,
authToken,
{
method: isEditingMedication ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(medicationForm),
},
);
if (!response.ok) {
throw new Error(await readErrorMessage(response, `Unable to ${isEditingMedication ? 'update' : 'save'} medication.`));
}
const data = await readJsonSafely<{ medication: Medication }>(response);
if (!data?.medication) {
throw new Error(`Unable to ${isEditingMedication ? 'update' : 'save'} medication.`);
}
setMedications((current) =>
(isEditingMedication
? current.map((medication) => (medication.id === data.medication.id ? data.medication : medication))
: [data.medication, ...current]
).sort((left, right) => {
const leftEnd = left.endDate ?? '9999-12-31';
const rightEnd = right.endDate ?? '9999-12-31';
return rightEnd.localeCompare(leftEnd) || right.startDate.localeCompare(left.startDate);
}),
);
setMedicationForm({
name: '',
dosage: '',
frequency: '',
route: '',
startDate: new Date().toISOString().slice(0, 10),
endDate: '',
notes: '',
});
setEditingMedicationId('');
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : 'Unable to save medication.');
}
};
const handleEditMedication = (medication: Medication) => {
setEditingMedicationId(medication.id);
setMedicationForm({
name: medication.name,
dosage: medication.dosage,
frequency: medication.frequency,
route: medication.route ?? '',
startDate: medication.startDate,
endDate: medication.endDate ?? '',
notes: medication.notes ?? '',
});
setError('');
};
const handleCancelMedicationEdit = () => {
setEditingMedicationId('');
setMedicationForm({
name: '',
dosage: '',
frequency: '',
route: '',
startDate: new Date().toISOString().slice(0, 10),
endDate: '',
notes: '',
});
};
const handleDeleteMedication = async (medicationId: string) => {
if (!selectedBird || deletingMedicationId) {
return;
}
setDeletingMedicationId(medicationId);
setError('');
try {
const response = await apiFetch(`/birds/${selectedBird.id}/medications/${medicationId}`, authToken, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Unable to remove medication.'));
}
setMedications((current) => current.filter((medication) => medication.id !== medicationId));
if (editingMedicationId === medicationId) {
handleCancelMedicationEdit();
}
} catch (removeError) {
setError(removeError instanceof Error ? removeError.message : 'Unable to remove medication.');
} finally {
setDeletingMedicationId('');
}
};
const handleRemoveBird = async () => {
if (!selectedBird || deletingBird) {
return;
@@ -2517,8 +2668,11 @@ function App() {
setSelectedBirdId('');
setWeights([]);
setVetVisits([]);
setMedications([]);
setEditingVetVisitId('');
setDeletingVetVisitId('');
setEditingMedicationId('');
setDeletingMedicationId('');
if (editingBirdId === selectedBird.id) {
setEditingBirdId('');
@@ -2591,6 +2745,7 @@ function App() {
});
setWeights((current) => (selectedBird?.id === flockTransferForm.birdId ? [] : current));
setVetVisits((current) => (selectedBird?.id === flockTransferForm.birdId ? [] : current));
setMedications((current) => (selectedBird?.id === flockTransferForm.birdId ? [] : current));
if (selectedBird?.id === flockTransferForm.birdId) {
setSelectedBirdId('');
}
@@ -3820,6 +3975,159 @@ function App() {
</form>
</section>
<section className="panel inset-panel">
<div className="panel-header">
<div>
<p className="eyebrow">Medication</p>
<h2>Per-bird medication log</h2>
</div>
</div>
<form className="form-panel inline-form" onSubmit={handleMedicationSubmit}>
<label>
Medication
<input
value={medicationForm.name}
onChange={(event) => setMedicationForm({ ...medicationForm, name: event.target.value })}
placeholder="Meloxicam"
required
/>
</label>
<label>
Dosage
<input
value={medicationForm.dosage}
onChange={(event) => setMedicationForm({ ...medicationForm, dosage: event.target.value })}
placeholder="0.05 mL"
required
/>
</label>
<label>
Frequency
<input
value={medicationForm.frequency}
onChange={(event) => setMedicationForm({ ...medicationForm, frequency: event.target.value })}
placeholder="Every 12 hours"
required
/>
</label>
<label>
Route
<input
value={medicationForm.route}
onChange={(event) => setMedicationForm({ ...medicationForm, route: event.target.value })}
placeholder="Oral"
/>
</label>
<label>
Start date
<input
type="date"
value={medicationForm.startDate}
onChange={(event) => setMedicationForm({ ...medicationForm, startDate: event.target.value })}
required
/>
</label>
<label>
End date
<input
type="date"
value={medicationForm.endDate}
onChange={(event) => setMedicationForm({ ...medicationForm, endDate: event.target.value })}
/>
</label>
<label className="wide-field">
Notes
<textarea
rows={3}
value={medicationForm.notes}
onChange={(event) => setMedicationForm({ ...medicationForm, notes: event.target.value })}
placeholder="Instructions, response, side effects, or prescribing vet"
/>
</label>
<div className="button-row wide-field">
<button className="primary-button" type="submit">
{editingMedicationId ? 'Save medication changes' : 'Save medication'}
</button>
{editingMedicationId ? (
<button className="secondary-button" onClick={handleCancelMedicationEdit} type="button">
Cancel edit
</button>
) : null}
</div>
</form>
<div className="recent-list">
{medications.length ? (
<>
{activeMedications.length ? <strong>Active medication</strong> : null}
{activeMedications.map((medication) => (
<article key={medication.id} className="vet-visit-card">
<strong>{medication.name}</strong>
<span>
{medication.dosage} {medication.frequency}
{medication.route ? `${medication.route}` : ''}
</span>
<small>
{formatDate(medication.startDate)} to {formatDate(medication.endDate)}
</small>
<small>{medication.notes || 'No notes recorded.'}</small>
<div className="button-row">
<button className="secondary-button" onClick={() => handleEditMedication(medication)} type="button">
Edit
</button>
{editingMedicationId === medication.id ? (
<button
className="secondary-button"
onClick={() => handleDeleteMedication(medication.id)}
type="button"
disabled={deletingMedicationId === medication.id}
>
{deletingMedicationId === medication.id ? 'Deleting...' : 'Delete'}
</button>
) : null}
</div>
</article>
))}
{pastMedications.length ? <strong>Past medication</strong> : null}
{pastMedications.map((medication) => (
<article key={medication.id} className="vet-visit-card">
<strong>{medication.name}</strong>
<span>
{medication.dosage} {medication.frequency}
{medication.route ? `${medication.route}` : ''}
</span>
<small>
{formatDate(medication.startDate)} to {formatDate(medication.endDate)}
</small>
<small>{medication.notes || 'No notes recorded.'}</small>
<div className="button-row">
<button className="secondary-button" onClick={() => handleEditMedication(medication)} type="button">
Edit
</button>
{editingMedicationId === medication.id ? (
<button
className="secondary-button"
onClick={() => handleDeleteMedication(medication.id)}
type="button"
disabled={deletingMedicationId === medication.id}
>
{deletingMedicationId === medication.id ? 'Deleting...' : 'Delete'}
</button>
) : null}
</div>
</article>
))}
</>
) : (
<article className="vet-visit-card empty-card">
<strong>No medication logged yet</strong>
<small>Add medication above to track dosage, frequency, dates, and notes for this bird.</small>
</article>
)}
</div>
</section>
<section className="panel inset-panel">
<div className="panel-header">
<div>