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
+16
View File
@@ -262,6 +262,10 @@ const birdSchema = z.object({
motivators: birdProfileListSchema, motivators: birdProfileListSchema,
demotivators: birdProfileListSchema, demotivators: birdProfileListSchema,
favoriteSnack: z.string().trim().max(160).optional().or(z.literal('')), favoriteSnack: z.string().trim().max(160).optional().or(z.literal('')),
vetClinicName: z.string().trim().max(160).optional().or(z.literal('')),
vetClinicAddress: z.string().trim().max(500).optional().or(z.literal('')),
vetAccountNumber: z.string().trim().max(120).optional().or(z.literal('')),
vetDoctorName: z.string().trim().max(160).optional().or(z.literal('')),
gender: birdGenderSchema.optional(), gender: birdGenderSchema.optional(),
dateOfBirth: dateStringSchema.optional().or(z.literal('')), dateOfBirth: dateStringSchema.optional().or(z.literal('')),
gotchaDay: dateStringSchema.optional().or(z.literal('')), gotchaDay: dateStringSchema.optional().or(z.literal('')),
@@ -617,6 +621,10 @@ const normalizeBird = (row: BirdRow) => ({
motivators: row.motivators, motivators: row.motivators,
demotivators: row.demotivators, demotivators: row.demotivators,
favoriteSnack: row.favorite_snack, favoriteSnack: row.favorite_snack,
vetClinicName: row.vet_clinic_name,
vetClinicAddress: row.vet_clinic_address,
vetAccountNumber: row.vet_account_number,
vetDoctorName: row.vet_doctor_name,
gender: row.gender, gender: row.gender,
dateOfBirth: row.date_of_birth, dateOfBirth: row.date_of_birth,
gotchaDay: row.gotcha_day, gotchaDay: row.gotcha_day,
@@ -3118,6 +3126,10 @@ app.post('/api/birds', requireAuth, requireWriteAccess, requireWorkspaceRole(['o
motivators: emptyToNull(parsed.data.motivators), motivators: emptyToNull(parsed.data.motivators),
demotivators: emptyToNull(parsed.data.demotivators), demotivators: emptyToNull(parsed.data.demotivators),
favoriteSnack: emptyToNull(parsed.data.favoriteSnack), favoriteSnack: emptyToNull(parsed.data.favoriteSnack),
vetClinicName: emptyToNull(parsed.data.vetClinicName),
vetClinicAddress: emptyToNull(parsed.data.vetClinicAddress),
vetAccountNumber: emptyToNull(parsed.data.vetAccountNumber),
vetDoctorName: emptyToNull(parsed.data.vetDoctorName),
gender: (parsed.data.gender ?? 'unknown') as BirdGender, gender: (parsed.data.gender ?? 'unknown') as BirdGender,
dateOfBirth: emptyToNull(parsed.data.dateOfBirth), dateOfBirth: emptyToNull(parsed.data.dateOfBirth),
gotchaDay: emptyToNull(parsed.data.gotchaDay), gotchaDay: emptyToNull(parsed.data.gotchaDay),
@@ -3271,6 +3283,10 @@ app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceR
motivators: emptyToNull(parsed.data.motivators), motivators: emptyToNull(parsed.data.motivators),
demotivators: emptyToNull(parsed.data.demotivators), demotivators: emptyToNull(parsed.data.demotivators),
favoriteSnack: emptyToNull(parsed.data.favoriteSnack), favoriteSnack: emptyToNull(parsed.data.favoriteSnack),
vetClinicName: emptyToNull(parsed.data.vetClinicName),
vetClinicAddress: emptyToNull(parsed.data.vetClinicAddress),
vetAccountNumber: emptyToNull(parsed.data.vetAccountNumber),
vetDoctorName: emptyToNull(parsed.data.vetDoctorName),
gender: (parsed.data.gender ?? 'unknown') as BirdGender, gender: (parsed.data.gender ?? 'unknown') as BirdGender,
dateOfBirth: emptyToNull(parsed.data.dateOfBirth), dateOfBirth: emptyToNull(parsed.data.dateOfBirth),
gotchaDay: emptyToNull(parsed.data.gotchaDay), gotchaDay: emptyToNull(parsed.data.gotchaDay),
+8
View File
@@ -215,6 +215,10 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
motivators VARCHAR(1000), motivators VARCHAR(1000),
demotivators VARCHAR(1000), demotivators VARCHAR(1000),
favorite_snack VARCHAR(160), favorite_snack VARCHAR(160),
vet_clinic_name VARCHAR(160),
vet_clinic_address VARCHAR(500),
vet_account_number VARCHAR(120),
vet_doctor_name VARCHAR(160),
gender VARCHAR(16) NOT NULL DEFAULT 'unknown', gender VARCHAR(16) NOT NULL DEFAULT 'unknown',
date_of_birth DATE, date_of_birth DATE,
gotcha_day DATE, gotcha_day DATE,
@@ -239,6 +243,10 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
ADD COLUMN IF NOT EXISTS motivators VARCHAR(1000), ADD COLUMN IF NOT EXISTS motivators VARCHAR(1000),
ADD COLUMN IF NOT EXISTS demotivators VARCHAR(1000), ADD COLUMN IF NOT EXISTS demotivators VARCHAR(1000),
ADD COLUMN IF NOT EXISTS favorite_snack VARCHAR(160), ADD COLUMN IF NOT EXISTS favorite_snack VARCHAR(160),
ADD COLUMN IF NOT EXISTS vet_clinic_name VARCHAR(160),
ADD COLUMN IF NOT EXISTS vet_clinic_address VARCHAR(500),
ADD COLUMN IF NOT EXISTS vet_account_number VARCHAR(120),
ADD COLUMN IF NOT EXISTS vet_doctor_name VARCHAR(160),
ADD COLUMN IF NOT EXISTS gender VARCHAR(16) NOT NULL DEFAULT 'unknown', ADD COLUMN IF NOT EXISTS gender VARCHAR(16) NOT NULL DEFAULT 'unknown',
ADD COLUMN IF NOT EXISTS date_of_birth DATE, ADD COLUMN IF NOT EXISTS date_of_birth DATE,
ADD COLUMN IF NOT EXISTS gotcha_day DATE, ADD COLUMN IF NOT EXISTS gotcha_day DATE,
+52 -20
View File
@@ -23,6 +23,10 @@ const birdSelectFields = `
birds.motivators, birds.motivators,
birds.demotivators, birds.demotivators,
birds.favorite_snack, birds.favorite_snack,
birds.vet_clinic_name,
birds.vet_clinic_address,
birds.vet_account_number,
birds.vet_doctor_name,
birds.gender, birds.gender,
birds.date_of_birth::text, birds.date_of_birth::text,
birds.gotcha_day::text, birds.gotcha_day::text,
@@ -287,6 +291,10 @@ export const createBird = async ({
motivators, motivators,
demotivators, demotivators,
favoriteSnack, favoriteSnack,
vetClinicName = null,
vetClinicAddress = null,
vetAccountNumber = null,
vetDoctorName = null,
gender, gender,
dateOfBirth, dateOfBirth,
gotchaDay, gotchaDay,
@@ -308,6 +316,10 @@ export const createBird = async ({
motivators: string | null; motivators: string | null;
demotivators: string | null; demotivators: string | null;
favoriteSnack: string | null; favoriteSnack: string | null;
vetClinicName?: string | null;
vetClinicAddress?: string | null;
vetAccountNumber?: string | null;
vetDoctorName?: string | null;
gender: BirdGender; gender: BirdGender;
dateOfBirth: string | null; dateOfBirth: string | null;
gotchaDay: string | null; gotchaDay: string | null;
@@ -322,9 +334,9 @@ export const createBird = async ({
publicProfileEnabled?: boolean; publicProfileEnabled?: boolean;
}) => { }) => {
const result = await db.query<BirdRow>( const result = await db.query<BirdRow>(
`INSERT INTO birds (id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth, gotcha_day, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled) `INSERT INTO birds (id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth, gotcha_day, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled)
VALUES (COALESCE($1::uuid, gen_random_uuid()), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) VALUES (COALESCE($1::uuid, gen_random_uuid()), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`, RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`,
[ [
birdId ?? null, birdId ?? null,
workspaceId, workspaceId,
@@ -334,6 +346,10 @@ export const createBird = async ({
motivators, motivators,
demotivators, demotivators,
favoriteSnack, favoriteSnack,
vetClinicName,
vetClinicAddress,
vetAccountNumber,
vetDoctorName,
gender, gender,
dateOfBirth, dateOfBirth,
gotchaDay, gotchaDay,
@@ -361,6 +377,10 @@ export const updateBird = async ({
motivators, motivators,
demotivators, demotivators,
favoriteSnack, favoriteSnack,
vetClinicName,
vetClinicAddress,
vetAccountNumber,
vetDoctorName,
gender, gender,
dateOfBirth, dateOfBirth,
gotchaDay, gotchaDay,
@@ -382,6 +402,10 @@ export const updateBird = async ({
motivators: string | null; motivators: string | null;
demotivators: string | null; demotivators: string | null;
favoriteSnack: string | null; favoriteSnack: string | null;
vetClinicName: string | null;
vetClinicAddress: string | null;
vetAccountNumber: string | null;
vetDoctorName: string | null;
gender: BirdGender; gender: BirdGender;
dateOfBirth: string | null; dateOfBirth: string | null;
gotchaDay: string | null; gotchaDay: string | null;
@@ -403,22 +427,26 @@ export const updateBird = async ({
motivators = $5, motivators = $5,
demotivators = $6, demotivators = $6,
favorite_snack = $7, favorite_snack = $7,
gender = $8, vet_clinic_name = $8,
date_of_birth = $9, vet_clinic_address = $9,
gotcha_day = $10, vet_account_number = $10,
chart_color = $11, vet_doctor_name = $11,
photo_data_url = $12, gender = $12,
photo_object_key = $13, date_of_birth = $13,
photo_content_type = $14, gotcha_day = $14,
photo_updated_at = $15, chart_color = $15,
notify_on_dob = $16, photo_data_url = $16,
notify_on_gotcha_day = $17, photo_object_key = $17,
public_profile_code = $18, photo_content_type = $18,
public_profile_enabled = $19 photo_updated_at = $19,
notify_on_dob = $20,
notify_on_gotcha_day = $21,
public_profile_code = $22,
public_profile_enabled = $23
WHERE id = $1 WHERE id = $1
AND workspace_id = $20 AND workspace_id = $24
AND memorialized_at IS NULL AND memorialized_at IS NULL
RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
( (
SELECT weight_grams::text SELECT weight_grams::text
FROM weight_records FROM weight_records
@@ -441,6 +469,10 @@ export const updateBird = async ({
motivators, motivators,
demotivators, demotivators,
favoriteSnack, favoriteSnack,
vetClinicName,
vetClinicAddress,
vetAccountNumber,
vetDoctorName,
gender, gender,
dateOfBirth, dateOfBirth,
gotchaDay, gotchaDay,
@@ -482,7 +514,7 @@ export const memorializeBird = async ({
WHERE id = $1 WHERE id = $1
AND workspace_id = $2 AND workspace_id = $2
AND memorialized_at IS NULL AND memorialized_at IS NULL
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
( (
SELECT weight_grams::text SELECT weight_grams::text
FROM weight_records FROM weight_records
@@ -518,7 +550,7 @@ export const updateMemorialReminderPreference = async ({
WHERE id = $1 WHERE id = $1
AND workspace_id = $2 AND workspace_id = $2
AND memorialized_at IS NOT NULL AND memorialized_at IS NOT NULL
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
( (
SELECT weight_grams::text SELECT weight_grams::text
FROM weight_records FROM weight_records
@@ -558,7 +590,7 @@ export const transferBirdToWorkspace = async (birdId: string, sourceWorkspaceId:
WHERE id = $1 WHERE id = $1
AND workspace_id = $2 AND workspace_id = $2
AND memorialized_at IS NULL AND memorialized_at IS NULL
RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at,
( (
SELECT weight_grams::text SELECT weight_grams::text
FROM weight_records FROM weight_records
+4
View File
@@ -101,6 +101,10 @@ export type BirdRow = {
motivators: string | null; motivators: string | null;
demotivators: string | null; demotivators: string | null;
favorite_snack: string | null; favorite_snack: string | null;
vet_clinic_name: string | null;
vet_clinic_address: string | null;
vet_account_number: string | null;
vet_doctor_name: string | null;
gender: BirdGender; gender: BirdGender;
date_of_birth: string | null; date_of_birth: string | null;
gotcha_day: string | null; gotcha_day: string | null;
+9 -1
View File
@@ -208,6 +208,10 @@ Role requirements are called out per endpoint below. If the signed-in member lac
"name": "Kiwi", "name": "Kiwi",
"tagId": "FP-001", "tagId": "FP-001",
"species": "Cockatiel", "species": "Cockatiel",
"vetClinicName": "Avian Care Center",
"vetClinicAddress": "123 Feather Lane, Raleigh, NC",
"vetAccountNumber": "FP-1001",
"vetDoctorName": "Dr. Rivera",
"gender": "female", "gender": "female",
"dateOfBirth": "2023-05-10", "dateOfBirth": "2023-05-10",
"gotchaDay": "2023-08-21", "gotchaDay": "2023-08-21",
@@ -793,6 +797,10 @@ Request body:
"name": "Kiwi", "name": "Kiwi",
"tagId": "FP-001", "tagId": "FP-001",
"species": "Cockatiel", "species": "Cockatiel",
"vetClinicName": "Avian Care Center",
"vetClinicAddress": "123 Feather Lane, Raleigh, NC",
"vetAccountNumber": "FP-1001",
"vetDoctorName": "Dr. Rivera",
"gender": "female", "gender": "female",
"dateOfBirth": "2023-05-10", "dateOfBirth": "2023-05-10",
"gotchaDay": "2023-08-21", "gotchaDay": "2023-08-21",
@@ -805,7 +813,7 @@ Request body:
Notes: Notes:
- `dateOfBirth`, `gotchaDay`, and `photoDataUrl` may be omitted or sent as empty strings - `dateOfBirth`, `gotchaDay`, `photoDataUrl`, and veterinary info fields may be omitted or sent as empty strings
- `chartColor` defaults to `#cb3a35` - `chartColor` defaults to `#cb3a35`
Response `201`: Response `201`:
+261
View File
@@ -24,6 +24,10 @@ type Bird = {
motivators: string | null; motivators: string | null;
demotivators: string | null; demotivators: string | null;
favoriteSnack: string | null; favoriteSnack: string | null;
vetClinicName: string | null;
vetClinicAddress: string | null;
vetAccountNumber: string | null;
vetDoctorName: string | null;
gender: BirdGender; gender: BirdGender;
dateOfBirth: string | null; dateOfBirth: string | null;
gotchaDay: string | null; gotchaDay: string | null;
@@ -219,6 +223,10 @@ type BirdFormState = {
motivators: string; motivators: string;
demotivators: string; demotivators: string;
favoriteSnack: string; favoriteSnack: string;
vetClinicName: string;
vetClinicAddress: string;
vetAccountNumber: string;
vetDoctorName: string;
gender: BirdGender; gender: BirdGender;
dateOfBirth: string; dateOfBirth: string;
gotchaDay: string; gotchaDay: string;
@@ -229,6 +237,8 @@ type BirdFormState = {
publicProfileEnabled: boolean; publicProfileEnabled: boolean;
}; };
type VeterinaryInfoFormState = Pick<BirdFormState, 'vetClinicName' | 'vetClinicAddress' | 'vetAccountNumber' | 'vetDoctorName'>;
type BirdImportWeight = { type BirdImportWeight = {
weightGrams: number; weightGrams: number;
recordedOn: string; recordedOn: string;
@@ -627,6 +637,10 @@ const emptyBirdForm: BirdFormState = {
motivators: '', motivators: '',
demotivators: '', demotivators: '',
favoriteSnack: '', favoriteSnack: '',
vetClinicName: '',
vetClinicAddress: '',
vetAccountNumber: '',
vetDoctorName: '',
gender: 'unknown', gender: 'unknown',
dateOfBirth: '', dateOfBirth: '',
gotchaDay: '', gotchaDay: '',
@@ -637,6 +651,13 @@ const emptyBirdForm: BirdFormState = {
publicProfileEnabled: false, publicProfileEnabled: false,
}; };
const emptyVeterinaryInfoForm: VeterinaryInfoFormState = {
vetClinicName: '',
vetClinicAddress: '',
vetAccountNumber: '',
vetDoctorName: '',
};
const emptyMemorializeBirdForm = (): MemorializeBirdFormState => ({ const emptyMemorializeBirdForm = (): MemorializeBirdFormState => ({
memorializedOn: new Date().toISOString().slice(0, 10), memorializedOn: new Date().toISOString().slice(0, 10),
memorialNote: '', memorialNote: '',
@@ -758,6 +779,10 @@ const toBirdForm = (bird: Bird): BirdFormState => ({
motivators: parseBirdProfileList(bird.motivators).join('\n'), motivators: parseBirdProfileList(bird.motivators).join('\n'),
demotivators: parseBirdProfileList(bird.demotivators).join('\n'), demotivators: parseBirdProfileList(bird.demotivators).join('\n'),
favoriteSnack: bird.favoriteSnack ?? '', favoriteSnack: bird.favoriteSnack ?? '',
vetClinicName: bird.vetClinicName ?? '',
vetClinicAddress: bird.vetClinicAddress ?? '',
vetAccountNumber: bird.vetAccountNumber ?? '',
vetDoctorName: bird.vetDoctorName ?? '',
gender: bird.gender, gender: bird.gender,
dateOfBirth: bird.dateOfBirth ?? '', dateOfBirth: bird.dateOfBirth ?? '',
gotchaDay: bird.gotchaDay ?? '', gotchaDay: bird.gotchaDay ?? '',
@@ -768,6 +793,13 @@ const toBirdForm = (bird: Bird): BirdFormState => ({
publicProfileEnabled: bird.publicProfileEnabled, 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) => { const formatDate = (value: string | null) => {
if (!value) { if (!value) {
return 'Not set'; return 'Not set';
@@ -1538,6 +1570,7 @@ function App() {
reason: '', reason: '',
notes: '', notes: '',
}); });
const [veterinaryInfoForm, setVeterinaryInfoForm] = useState<VeterinaryInfoFormState>(emptyVeterinaryInfoForm);
const [medicationForm, setMedicationForm] = useState({ const [medicationForm, setMedicationForm] = useState({
name: '', name: '',
dosage: '', dosage: '',
@@ -1562,6 +1595,8 @@ function App() {
const [memorializingBird, setMemorializingBird] = useState(false); const [memorializingBird, setMemorializingBird] = useState(false);
const [savingMemorialReminderBirdId, setSavingMemorialReminderBirdId] = useState(''); const [savingMemorialReminderBirdId, setSavingMemorialReminderBirdId] = useState('');
const [editingVetVisitId, setEditingVetVisitId] = useState(''); const [editingVetVisitId, setEditingVetVisitId] = useState('');
const [editingVeterinaryInfo, setEditingVeterinaryInfo] = useState(false);
const [savingVeterinaryInfo, setSavingVeterinaryInfo] = useState(false);
const [deletingVetVisitId, setDeletingVetVisitId] = useState(''); const [deletingVetVisitId, setDeletingVetVisitId] = useState('');
const [editingMedicationId, setEditingMedicationId] = useState(''); const [editingMedicationId, setEditingMedicationId] = useState('');
const [deletingMedicationId, setDeletingMedicationId] = useState(''); const [deletingMedicationId, setDeletingMedicationId] = useState('');
@@ -1602,6 +1637,15 @@ function App() {
setSelectedBirdTab('info'); setSelectedBirdTab('info');
}, [selectedBirdId]); }, [selectedBirdId]);
useEffect(() => {
if (selectedBird) {
setVeterinaryInfoForm(toVeterinaryInfoForm(selectedBird));
} else {
setVeterinaryInfoForm(emptyVeterinaryInfoForm);
}
setEditingVeterinaryInfo(false);
}, [selectedBird]);
const overviewWindowStartDate = useMemo(() => { const overviewWindowStartDate = useMemo(() => {
const startDate = new Date(); const startDate = new Date();
startDate.setHours(0, 0, 0, 0); startDate.setHours(0, 0, 0, 0);
@@ -2345,6 +2389,7 @@ function App() {
setVetVisits([]); setVetVisits([]);
setMedications([]); setMedications([]);
setMedicationAdministrations([]); setMedicationAdministrations([]);
setVeterinaryInfoForm(emptyVeterinaryInfoForm);
return; 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>) => { const handleWeightSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
@@ -5129,6 +5217,43 @@ function App() {
placeholder="Optional" placeholder="Optional"
/> />
</label> </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"> <label className="wide-field">
Motivators Motivators
<div className="profile-list-fields"> <div className="profile-list-fields">
@@ -5845,6 +5970,105 @@ function App() {
<section className="panel inset-panel"> <section className="panel inset-panel">
<div className="panel-header"> <div className="panel-header">
<div> <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> <p className="eyebrow">Vet visits</p>
<h2>Care history and notes</h2> <h2>Care history and notes</h2>
</div> </div>
@@ -6894,6 +7118,43 @@ function App() {
placeholder="Optional" placeholder="Optional"
/> />
</label> </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"> <label className="wide-field">
Motivators Motivators
<div className="profile-list-fields"> <div className="profile-list-fields">