Added vet info
This commit is contained in:
@@ -262,6 +262,10 @@ const birdSchema = z.object({
|
||||
motivators: birdProfileListSchema,
|
||||
demotivators: birdProfileListSchema,
|
||||
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(),
|
||||
dateOfBirth: dateStringSchema.optional().or(z.literal('')),
|
||||
gotchaDay: dateStringSchema.optional().or(z.literal('')),
|
||||
@@ -617,6 +621,10 @@ const normalizeBird = (row: BirdRow) => ({
|
||||
motivators: row.motivators,
|
||||
demotivators: row.demotivators,
|
||||
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,
|
||||
dateOfBirth: row.date_of_birth,
|
||||
gotchaDay: row.gotcha_day,
|
||||
@@ -3118,6 +3126,10 @@ app.post('/api/birds', requireAuth, requireWriteAccess, requireWorkspaceRole(['o
|
||||
motivators: emptyToNull(parsed.data.motivators),
|
||||
demotivators: emptyToNull(parsed.data.demotivators),
|
||||
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,
|
||||
dateOfBirth: emptyToNull(parsed.data.dateOfBirth),
|
||||
gotchaDay: emptyToNull(parsed.data.gotchaDay),
|
||||
@@ -3271,6 +3283,10 @@ app.put('/api/birds/:birdId', requireAuth, requireWriteAccess, requireWorkspaceR
|
||||
motivators: emptyToNull(parsed.data.motivators),
|
||||
demotivators: emptyToNull(parsed.data.demotivators),
|
||||
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,
|
||||
dateOfBirth: emptyToNull(parsed.data.dateOfBirth),
|
||||
gotchaDay: emptyToNull(parsed.data.gotchaDay),
|
||||
|
||||
@@ -215,6 +215,10 @@ export const ensureSchema = async (database: DatabaseClient = db) => {
|
||||
motivators VARCHAR(1000),
|
||||
demotivators VARCHAR(1000),
|
||||
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',
|
||||
date_of_birth 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 demotivators VARCHAR(1000),
|
||||
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 date_of_birth DATE,
|
||||
ADD COLUMN IF NOT EXISTS gotcha_day DATE,
|
||||
|
||||
@@ -23,6 +23,10 @@ const birdSelectFields = `
|
||||
birds.motivators,
|
||||
birds.demotivators,
|
||||
birds.favorite_snack,
|
||||
birds.vet_clinic_name,
|
||||
birds.vet_clinic_address,
|
||||
birds.vet_account_number,
|
||||
birds.vet_doctor_name,
|
||||
birds.gender,
|
||||
birds.date_of_birth::text,
|
||||
birds.gotcha_day::text,
|
||||
@@ -287,6 +291,10 @@ export const createBird = async ({
|
||||
motivators,
|
||||
demotivators,
|
||||
favoriteSnack,
|
||||
vetClinicName = null,
|
||||
vetClinicAddress = null,
|
||||
vetAccountNumber = null,
|
||||
vetDoctorName = null,
|
||||
gender,
|
||||
dateOfBirth,
|
||||
gotchaDay,
|
||||
@@ -308,6 +316,10 @@ export const createBird = async ({
|
||||
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;
|
||||
@@ -322,9 +334,9 @@ export const createBird = async ({
|
||||
publicProfileEnabled?: boolean;
|
||||
}) => {
|
||||
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)
|
||||
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)
|
||||
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`,
|
||||
`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, $21, $22, $23, $24)
|
||||
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,
|
||||
workspaceId,
|
||||
@@ -334,6 +346,10 @@ export const createBird = async ({
|
||||
motivators,
|
||||
demotivators,
|
||||
favoriteSnack,
|
||||
vetClinicName,
|
||||
vetClinicAddress,
|
||||
vetAccountNumber,
|
||||
vetDoctorName,
|
||||
gender,
|
||||
dateOfBirth,
|
||||
gotchaDay,
|
||||
@@ -361,6 +377,10 @@ export const updateBird = async ({
|
||||
motivators,
|
||||
demotivators,
|
||||
favoriteSnack,
|
||||
vetClinicName,
|
||||
vetClinicAddress,
|
||||
vetAccountNumber,
|
||||
vetDoctorName,
|
||||
gender,
|
||||
dateOfBirth,
|
||||
gotchaDay,
|
||||
@@ -382,6 +402,10 @@ export const updateBird = async ({
|
||||
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;
|
||||
@@ -403,22 +427,26 @@ export const updateBird = async ({
|
||||
motivators = $5,
|
||||
demotivators = $6,
|
||||
favorite_snack = $7,
|
||||
gender = $8,
|
||||
date_of_birth = $9,
|
||||
gotcha_day = $10,
|
||||
chart_color = $11,
|
||||
photo_data_url = $12,
|
||||
photo_object_key = $13,
|
||||
photo_content_type = $14,
|
||||
photo_updated_at = $15,
|
||||
notify_on_dob = $16,
|
||||
notify_on_gotcha_day = $17,
|
||||
public_profile_code = $18,
|
||||
public_profile_enabled = $19
|
||||
vet_clinic_name = $8,
|
||||
vet_clinic_address = $9,
|
||||
vet_account_number = $10,
|
||||
vet_doctor_name = $11,
|
||||
gender = $12,
|
||||
date_of_birth = $13,
|
||||
gotcha_day = $14,
|
||||
chart_color = $15,
|
||||
photo_data_url = $16,
|
||||
photo_object_key = $17,
|
||||
photo_content_type = $18,
|
||||
photo_updated_at = $19,
|
||||
notify_on_dob = $20,
|
||||
notify_on_gotcha_day = $21,
|
||||
public_profile_code = $22,
|
||||
public_profile_enabled = $23
|
||||
WHERE id = $1
|
||||
AND workspace_id = $20
|
||||
AND workspace_id = $24
|
||||
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
|
||||
FROM weight_records
|
||||
@@ -441,6 +469,10 @@ export const updateBird = async ({
|
||||
motivators,
|
||||
demotivators,
|
||||
favoriteSnack,
|
||||
vetClinicName,
|
||||
vetClinicAddress,
|
||||
vetAccountNumber,
|
||||
vetDoctorName,
|
||||
gender,
|
||||
dateOfBirth,
|
||||
gotchaDay,
|
||||
@@ -482,7 +514,7 @@ export const memorializeBird = async ({
|
||||
WHERE id = $1
|
||||
AND workspace_id = $2
|
||||
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
|
||||
FROM weight_records
|
||||
@@ -518,7 +550,7 @@ export const updateMemorialReminderPreference = async ({
|
||||
WHERE id = $1
|
||||
AND workspace_id = $2
|
||||
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
|
||||
FROM weight_records
|
||||
@@ -558,7 +590,7 @@ export const transferBirdToWorkspace = async (birdId: string, sourceWorkspaceId:
|
||||
WHERE id = $1
|
||||
AND workspace_id = $2
|
||||
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
|
||||
FROM weight_records
|
||||
|
||||
@@ -101,6 +101,10 @@ export type BirdRow = {
|
||||
motivators: string | null;
|
||||
demotivators: 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;
|
||||
date_of_birth: string | null;
|
||||
gotcha_day: string | null;
|
||||
|
||||
@@ -208,6 +208,10 @@ Role requirements are called out per endpoint below. If the signed-in member lac
|
||||
"name": "Kiwi",
|
||||
"tagId": "FP-001",
|
||||
"species": "Cockatiel",
|
||||
"vetClinicName": "Avian Care Center",
|
||||
"vetClinicAddress": "123 Feather Lane, Raleigh, NC",
|
||||
"vetAccountNumber": "FP-1001",
|
||||
"vetDoctorName": "Dr. Rivera",
|
||||
"gender": "female",
|
||||
"dateOfBirth": "2023-05-10",
|
||||
"gotchaDay": "2023-08-21",
|
||||
@@ -793,6 +797,10 @@ Request body:
|
||||
"name": "Kiwi",
|
||||
"tagId": "FP-001",
|
||||
"species": "Cockatiel",
|
||||
"vetClinicName": "Avian Care Center",
|
||||
"vetClinicAddress": "123 Feather Lane, Raleigh, NC",
|
||||
"vetAccountNumber": "FP-1001",
|
||||
"vetDoctorName": "Dr. Rivera",
|
||||
"gender": "female",
|
||||
"dateOfBirth": "2023-05-10",
|
||||
"gotchaDay": "2023-08-21",
|
||||
@@ -805,7 +813,7 @@ Request body:
|
||||
|
||||
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`
|
||||
|
||||
Response `201`:
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user