Added vet info
Deploy / deploy-dev (push) Has been skipped
Deploy / deploy-prod (push) Successful in 2m14s

This commit is contained in:
Corey Blais
2026-06-01 14:59:03 -04:00
parent 095c91e56d
commit d748d2db21
6 changed files with 350 additions and 21 deletions
+261
View File
@@ -24,6 +24,10 @@ type Bird = {
motivators: string | null;
demotivators: string | null;
favoriteSnack: string | null;
vetClinicName: string | null;
vetClinicAddress: string | null;
vetAccountNumber: string | null;
vetDoctorName: string | null;
gender: BirdGender;
dateOfBirth: string | null;
gotchaDay: string | null;
@@ -219,6 +223,10 @@ type BirdFormState = {
motivators: string;
demotivators: string;
favoriteSnack: string;
vetClinicName: string;
vetClinicAddress: string;
vetAccountNumber: string;
vetDoctorName: string;
gender: BirdGender;
dateOfBirth: string;
gotchaDay: string;
@@ -229,6 +237,8 @@ type BirdFormState = {
publicProfileEnabled: boolean;
};
type VeterinaryInfoFormState = Pick<BirdFormState, 'vetClinicName' | 'vetClinicAddress' | 'vetAccountNumber' | 'vetDoctorName'>;
type BirdImportWeight = {
weightGrams: number;
recordedOn: string;
@@ -627,6 +637,10 @@ const emptyBirdForm: BirdFormState = {
motivators: '',
demotivators: '',
favoriteSnack: '',
vetClinicName: '',
vetClinicAddress: '',
vetAccountNumber: '',
vetDoctorName: '',
gender: 'unknown',
dateOfBirth: '',
gotchaDay: '',
@@ -637,6 +651,13 @@ const emptyBirdForm: BirdFormState = {
publicProfileEnabled: false,
};
const emptyVeterinaryInfoForm: VeterinaryInfoFormState = {
vetClinicName: '',
vetClinicAddress: '',
vetAccountNumber: '',
vetDoctorName: '',
};
const emptyMemorializeBirdForm = (): MemorializeBirdFormState => ({
memorializedOn: new Date().toISOString().slice(0, 10),
memorialNote: '',
@@ -758,6 +779,10 @@ const toBirdForm = (bird: Bird): BirdFormState => ({
motivators: parseBirdProfileList(bird.motivators).join('\n'),
demotivators: parseBirdProfileList(bird.demotivators).join('\n'),
favoriteSnack: bird.favoriteSnack ?? '',
vetClinicName: bird.vetClinicName ?? '',
vetClinicAddress: bird.vetClinicAddress ?? '',
vetAccountNumber: bird.vetAccountNumber ?? '',
vetDoctorName: bird.vetDoctorName ?? '',
gender: bird.gender,
dateOfBirth: bird.dateOfBirth ?? '',
gotchaDay: bird.gotchaDay ?? '',
@@ -768,6 +793,13 @@ const toBirdForm = (bird: Bird): BirdFormState => ({
publicProfileEnabled: bird.publicProfileEnabled,
});
const toVeterinaryInfoForm = (bird: Bird): VeterinaryInfoFormState => ({
vetClinicName: bird.vetClinicName ?? '',
vetClinicAddress: bird.vetClinicAddress ?? '',
vetAccountNumber: bird.vetAccountNumber ?? '',
vetDoctorName: bird.vetDoctorName ?? '',
});
const formatDate = (value: string | null) => {
if (!value) {
return 'Not set';
@@ -1538,6 +1570,7 @@ function App() {
reason: '',
notes: '',
});
const [veterinaryInfoForm, setVeterinaryInfoForm] = useState<VeterinaryInfoFormState>(emptyVeterinaryInfoForm);
const [medicationForm, setMedicationForm] = useState({
name: '',
dosage: '',
@@ -1562,6 +1595,8 @@ function App() {
const [memorializingBird, setMemorializingBird] = useState(false);
const [savingMemorialReminderBirdId, setSavingMemorialReminderBirdId] = useState('');
const [editingVetVisitId, setEditingVetVisitId] = useState('');
const [editingVeterinaryInfo, setEditingVeterinaryInfo] = useState(false);
const [savingVeterinaryInfo, setSavingVeterinaryInfo] = useState(false);
const [deletingVetVisitId, setDeletingVetVisitId] = useState('');
const [editingMedicationId, setEditingMedicationId] = useState('');
const [deletingMedicationId, setDeletingMedicationId] = useState('');
@@ -1602,6 +1637,15 @@ function App() {
setSelectedBirdTab('info');
}, [selectedBirdId]);
useEffect(() => {
if (selectedBird) {
setVeterinaryInfoForm(toVeterinaryInfoForm(selectedBird));
} else {
setVeterinaryInfoForm(emptyVeterinaryInfoForm);
}
setEditingVeterinaryInfo(false);
}, [selectedBird]);
const overviewWindowStartDate = useMemo(() => {
const startDate = new Date();
startDate.setHours(0, 0, 0, 0);
@@ -2345,6 +2389,7 @@ function App() {
setVetVisits([]);
setMedications([]);
setMedicationAdministrations([]);
setVeterinaryInfoForm(emptyVeterinaryInfoForm);
return;
}
@@ -3190,6 +3235,49 @@ function App() {
}
};
const handleVeterinaryInfoSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!selectedBird || savingVeterinaryInfo) {
return;
}
setError('');
setSavingVeterinaryInfo(true);
try {
const response = await apiFetch(`/birds/${selectedBird.id}`, authToken, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...toBirdForm(selectedBird),
...veterinaryInfoForm,
}),
});
if (!response.ok) {
throw new Error(await readErrorMessage(response, 'Unable to save veterinary info.'));
}
const data = await readJsonSafely<{ bird?: Bird }>(response);
if (!data?.bird) {
throw new Error('Unable to save veterinary info.');
}
const savedBird = data.bird;
setBirds((current) => sortBirdsByName(current.map((bird) => (bird.id === savedBird.id ? savedBird : bird))));
setVeterinaryInfoForm(toVeterinaryInfoForm(savedBird));
setEditingVeterinaryInfo(false);
if (editingBirdId === savedBird.id) {
setBirdForm(toBirdForm(savedBird));
}
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : 'Unable to save veterinary info.');
} finally {
setSavingVeterinaryInfo(false);
}
};
const handleWeightSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
@@ -5129,6 +5217,43 @@ function App() {
placeholder="Optional"
/>
</label>
<div className="settings-inline-header wide-field">
<p className="eyebrow">Veterinary</p>
<h3>Clinic account</h3>
</div>
<label>
Clinic name
<input
value={birdForm.vetClinicName}
onChange={(event) => setBirdForm({ ...birdForm, vetClinicName: event.target.value })}
placeholder="Optional"
/>
</label>
<label>
Account #
<input
value={birdForm.vetAccountNumber}
onChange={(event) => setBirdForm({ ...birdForm, vetAccountNumber: event.target.value })}
placeholder="Optional"
/>
</label>
<label>
Dr. name
<input
value={birdForm.vetDoctorName}
onChange={(event) => setBirdForm({ ...birdForm, vetDoctorName: event.target.value })}
placeholder="Optional"
/>
</label>
<label className="wide-field">
Clinic address
<textarea
rows={2}
value={birdForm.vetClinicAddress}
onChange={(event) => setBirdForm({ ...birdForm, vetClinicAddress: event.target.value })}
placeholder="Optional"
/>
</label>
<label className="wide-field">
Motivators
<div className="profile-list-fields">
@@ -5845,6 +5970,105 @@ function App() {
<section className="panel inset-panel">
<div className="panel-header">
<div>
<p className="eyebrow">Veterinary</p>
<h2>Clinic account</h2>
</div>
{!editingVeterinaryInfo ? (
<button
className="profile-icon-button"
onClick={() => {
setVeterinaryInfoForm(toVeterinaryInfoForm(selectedBird));
setEditingVeterinaryInfo(true);
}}
type="button"
aria-label={`Edit veterinary info for ${selectedBird.name}`}
title="Edit veterinary info"
>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M4 20h4l10.5-10.5-4-4L4 16v4Z" />
<path d="m13.5 6.5 4 4" />
<path d="M15 5l1.5-1.5a2.1 2.1 0 0 1 3 3L18 8" />
</svg>
</button>
) : null}
</div>
{editingVeterinaryInfo ? (
<form className="form-panel inline-form care-entry-form" onSubmit={handleVeterinaryInfoSubmit}>
<label>
Clinic name
<input
value={veterinaryInfoForm.vetClinicName}
onChange={(event) => setVeterinaryInfoForm({ ...veterinaryInfoForm, vetClinicName: event.target.value })}
placeholder="Optional"
/>
</label>
<label>
Account #
<input
value={veterinaryInfoForm.vetAccountNumber}
onChange={(event) => setVeterinaryInfoForm({ ...veterinaryInfoForm, vetAccountNumber: event.target.value })}
placeholder="Optional"
/>
</label>
<label>
Dr. name
<input
value={veterinaryInfoForm.vetDoctorName}
onChange={(event) => setVeterinaryInfoForm({ ...veterinaryInfoForm, vetDoctorName: event.target.value })}
placeholder="Optional"
/>
</label>
<label className="wide-field">
Clinic address
<textarea
rows={2}
value={veterinaryInfoForm.vetClinicAddress}
onChange={(event) => setVeterinaryInfoForm({ ...veterinaryInfoForm, vetClinicAddress: event.target.value })}
placeholder="Optional"
/>
</label>
<div className="button-row care-form-actions">
<button className="primary-button" type="submit" disabled={savingVeterinaryInfo}>
{savingVeterinaryInfo ? 'Saving veterinary info...' : 'Save veterinary info'}
</button>
<button
className="secondary-button"
onClick={() => {
setVeterinaryInfoForm(toVeterinaryInfoForm(selectedBird));
setEditingVeterinaryInfo(false);
}}
type="button"
disabled={savingVeterinaryInfo}
>
Cancel
</button>
</div>
</form>
) : (
<div className="detail-grid">
<article className="detail-card">
<span>Clinic name</span>
<strong>{selectedBird.vetClinicName || 'Not recorded'}</strong>
</article>
<article className="detail-card">
<span>Account #</span>
<strong>{selectedBird.vetAccountNumber || 'Not recorded'}</strong>
</article>
<article className="detail-card">
<span>Dr. name</span>
<strong>{selectedBird.vetDoctorName || 'Not recorded'}</strong>
</article>
<article className="detail-card wide-field">
<span>Clinic address</span>
<strong>{selectedBird.vetClinicAddress || 'Not recorded'}</strong>
</article>
</div>
)}
</section>
<section className="panel inset-panel">
<div className="panel-header">
<div>
<p className="eyebrow">Vet visits</p>
<h2>Care history and notes</h2>
</div>
@@ -6894,6 +7118,43 @@ function App() {
placeholder="Optional"
/>
</label>
<div className="settings-inline-header wide-field">
<p className="eyebrow">Veterinary</p>
<h3>Clinic account</h3>
</div>
<label>
Clinic name
<input
value={birdForm.vetClinicName}
onChange={(event) => setBirdForm({ ...birdForm, vetClinicName: event.target.value })}
placeholder="Optional"
/>
</label>
<label>
Account #
<input
value={birdForm.vetAccountNumber}
onChange={(event) => setBirdForm({ ...birdForm, vetAccountNumber: event.target.value })}
placeholder="Optional"
/>
</label>
<label>
Dr. name
<input
value={birdForm.vetDoctorName}
onChange={(event) => setBirdForm({ ...birdForm, vetDoctorName: event.target.value })}
placeholder="Optional"
/>
</label>
<label className="wide-field">
Clinic address
<textarea
rows={2}
value={birdForm.vetClinicAddress}
onChange={(event) => setBirdForm({ ...birdForm, vetClinicAddress: event.target.value })}
placeholder="Optional"
/>
</label>
<label className="wide-field">
Motivators
<div className="profile-list-fields">