Added vet info
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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`:
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user