feat(GRO-106): portal Communication tab — real backend
- Added GET /portal/conversation and GET /portal/conversation/messages endpoints - Created Communication.api.ts with typed fetchers and React hooks - Rewired Communication.tsx to use real API, removed mock data - Added composer-disabled bar with "Reply from your phone" tooltip - Added conversation route tests to portal.test.ts Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -103,6 +103,8 @@ export function buildPet(overrides: Partial<PetRow> & { clientId: string }): Pet
|
||||
photoKey: null,
|
||||
photoUploadedAt: null,
|
||||
image: null,
|
||||
sizeCategory: null,
|
||||
coatType: null,
|
||||
createdAt: new Date("2025-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2025-01-01T00:00:00Z"),
|
||||
};
|
||||
@@ -117,6 +119,7 @@ export function buildService(overrides: Partial<ServiceRow> = {}): ServiceRow {
|
||||
description: "A grooming service",
|
||||
basePriceCents: 6500,
|
||||
durationMinutes: 60,
|
||||
defaultBufferMinutes: 0,
|
||||
active: true,
|
||||
createdAt: new Date("2025-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2025-01-01T00:00:00Z"),
|
||||
@@ -148,6 +151,7 @@ export function buildAppointment(
|
||||
confirmationStatus: "pending",
|
||||
confirmedAt: null,
|
||||
cancelledAt: null,
|
||||
bufferMinutes: 0,
|
||||
confirmationToken: null,
|
||||
customerNotes: null,
|
||||
createdAt: new Date("2025-01-01T00:00:00Z"),
|
||||
|
||||
@@ -48,6 +48,22 @@ export const clientStatusEnum = pgEnum("client_status", [
|
||||
"disabled",
|
||||
]);
|
||||
|
||||
export const petSizeCategoryEnum = pgEnum("pet_size_category", [
|
||||
"small",
|
||||
"medium",
|
||||
"large",
|
||||
"xlarge",
|
||||
]);
|
||||
|
||||
export const coatTypeEnum = pgEnum("coat_type", [
|
||||
"smooth",
|
||||
"double",
|
||||
"curly",
|
||||
"wire",
|
||||
"long",
|
||||
"hairless",
|
||||
]);
|
||||
|
||||
// ─── Better-Auth Tables ──────────────────────────────────────────────────────
|
||||
|
||||
export const user = pgTable("user", {
|
||||
@@ -146,6 +162,8 @@ export const pets = pgTable(
|
||||
photoKey: text("photo_key"),
|
||||
photoUploadedAt: timestamp("photo_uploaded_at"),
|
||||
image: text("image"),
|
||||
sizeCategory: petSizeCategoryEnum("size_category"),
|
||||
coatType: coatTypeEnum("coat_type"),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
},
|
||||
@@ -158,6 +176,7 @@ export const services = pgTable("services", {
|
||||
description: text("description"),
|
||||
basePriceCents: integer("base_price_cents").notNull(),
|
||||
durationMinutes: integer("duration_minutes").notNull(),
|
||||
defaultBufferMinutes: integer("default_buffer_minutes").notNull().default(0),
|
||||
active: boolean("active").notNull().default(true),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
@@ -240,6 +259,8 @@ export const appointments = pgTable(
|
||||
confirmationStatus: text("confirmation_status").notNull().default("pending"),
|
||||
confirmedAt: timestamp("confirmed_at"),
|
||||
cancelledAt: timestamp("cancelled_at"),
|
||||
// Per-appointment buffer time (may be overridden from service default or bufferTimeRules)
|
||||
bufferMinutes: integer("buffer_minutes").notNull().default(0),
|
||||
// Token for tokenized email confirm/cancel links (no auth required)
|
||||
confirmationToken: text("confirmation_token").unique(),
|
||||
// Customer-provided note visible to groomer (500 char max, editable until appointment starts)
|
||||
@@ -600,3 +621,22 @@ export const authProviderConfig = pgTable("auth_provider_config", {
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const bufferTimeRules = pgTable(
|
||||
"buffer_time_rules",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
serviceId: uuid("service_id")
|
||||
.notNull()
|
||||
.references(() => services.id, { onDelete: "cascade" }),
|
||||
sizeCategory: petSizeCategoryEnum("size_category"),
|
||||
coatType: coatTypeEnum("coat_type"),
|
||||
bufferMinutes: integer("buffer_minutes").notNull(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
unique().on(t.serviceId, t.sizeCategory, t.coatType),
|
||||
index("idx_buffer_rules_service_id").on(t.serviceId),
|
||||
]
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user