feat: detailed pet profile attributes and grooming visit history (closes #13)

- Add cut_style, shampoo_preference, special_care_notes, custom_fields columns to pets table
- Add grooming_visit_logs table to track per-visit grooming details (cut, products, notes)
- Extend pets API to accept and return new profile fields
- Add /api/grooming-logs endpoint (GET by petId, POST, DELETE)
- Update Pet type with new fields; add GroomingVisitLog type
- Update Clients page: grooming preferences section in pet card, "Log visit" button,
  visit history panel showing last 3 visits, expanded pet form with grooming preferences

Co-authored-by: Groom Book CTO <cto@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit was merged in pull request #32.
This commit is contained in:
groombook-paperclip[bot]
2026-03-17 21:46:40 +00:00
committed by GitHub
parent f47717dfd8
commit 14ed19497f
8 changed files with 392 additions and 12 deletions
+23
View File
@@ -1,6 +1,7 @@
import {
boolean,
integer,
jsonb,
numeric,
pgEnum,
pgTable,
@@ -68,6 +69,10 @@ export const pets = pgTable("pets", {
dateOfBirth: timestamp("date_of_birth"),
healthAlerts: text("health_alerts"),
groomingNotes: text("grooming_notes"),
cutStyle: text("cut_style"),
shampooPreference: text("shampoo_preference"),
specialCareNotes: text("special_care_notes"),
customFields: jsonb("custom_fields").$type<Record<string, string>>().notNull().default({}),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});
@@ -194,3 +199,21 @@ export const reminderLogs = pgTable(
},
(t) => [unique().on(t.appointmentId, t.reminderType)]
);
export const groomingVisitLogs = pgTable("grooming_visit_logs", {
id: uuid("id").primaryKey().defaultRandom(),
petId: uuid("pet_id")
.notNull()
.references(() => pets.id, { onDelete: "cascade" }),
appointmentId: uuid("appointment_id").references(() => appointments.id, {
onDelete: "set null",
}),
staffId: uuid("staff_id").references(() => staff.id, {
onDelete: "set null",
}),
cutStyle: text("cut_style"),
productsUsed: text("products_used"),
notes: text("notes"),
groomedAt: timestamp("groomed_at").notNull().defaultNow(),
createdAt: timestamp("created_at").notNull().defaultNow(),
});