Files
smoothschedule/frontend/src/types.ts
poduck 4a66246708 Add booking flow, business hours, and dark mode support
Features:
- Complete multi-step booking flow with service selection, date/time picker,
  auth (login/signup with email verification), payment, and confirmation
- Business hours settings page for defining when business is open
- TimeBlock purpose field (BUSINESS_HOURS, CLOSURE, UNAVAILABLE)
- Service resource assignment with prep/takedown time buffers
- Availability checking respects business hours and service buffers
- Customer registration via email verification code

UI/UX:
- Full dark mode support for all booking components
- Separate first/last name fields in signup form
- Back buttons on each wizard step
- Removed auto-redirect from confirmation page

API:
- Public endpoints for services, availability, business hours
- Customer verification and registration endpoints
- Tenant lookup from X-Business-Subdomain header

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 20:20:18 -05:00

698 lines
18 KiB
TypeScript

// Domain models based on the Game Plan
// FIX: Added PageComponent types and updated Business interface for website editor feature.
export type PageComponentType = 'HEADING' | 'TEXT' | 'IMAGE' | 'BUTTON' | 'SERVICE' | 'COLUMNS';
export interface PageComponent {
id: string;
type: PageComponentType;
content?: {
text?: string;
level?: 1 | 2 | 3;
src?: string;
alt?: string;
buttonText?: string;
href?: string;
serviceId?: string;
};
children?: PageComponent[][];
}
export interface CustomDomain {
id: number;
domain: string;
is_verified: boolean;
ssl_provisioned: boolean;
is_primary: boolean;
verification_token: string;
dns_txt_record: string;
dns_txt_record_name: string;
created_at: string;
verified_at?: string;
}
export interface PlanPermissions {
sms_reminders: boolean;
webhooks: boolean;
api_access: boolean;
custom_domain: boolean;
white_label: boolean;
custom_oauth: boolean;
plugins: boolean;
can_create_plugins: boolean;
tasks: boolean;
export_data: boolean;
video_conferencing: boolean;
two_factor_auth: boolean;
masked_calling: boolean;
pos_system: boolean;
mobile_app: boolean;
contracts: boolean;
}
export interface Business {
id: string;
name: string;
subdomain: string;
primaryColor: string;
secondaryColor: string;
logoUrl?: string;
emailLogoUrl?: string;
logoDisplayMode?: 'logo-only' | 'text-only' | 'logo-and-text'; // How to display branding
timezone?: string; // IANA timezone (e.g., 'America/New_York')
timezoneDisplayMode?: 'business' | 'viewer'; // How times are displayed to users
whitelabelEnabled: boolean;
plan?: 'Free' | 'Professional' | 'Business' | 'Enterprise';
status?: 'Active' | 'Suspended' | 'Trial';
joinedAt?: Date;
resourcesCanReschedule?: boolean;
paymentsEnabled: boolean;
requirePaymentMethodToBook: boolean;
cancellationWindowHours: number;
lateCancellationFeePercent: number;
initialSetupComplete?: boolean;
customDomain?: string;
customDomainVerified?: boolean;
bookingReturnUrl?: string; // URL to redirect customers after booking completion
stripeConnectAccountId?: string;
websitePages?: Record<string, { name: string; content: PageComponent[] }>;
customerDashboardContent?: PageComponent[];
// Booking page customization
serviceSelectionHeading?: string; // Custom heading for service selection (default: "Choose your experience")
serviceSelectionSubheading?: string; // Custom subheading (default: "Select a service to begin your booking.")
trialStart?: string;
trialEnd?: string;
isTrialActive?: boolean;
isTrialExpired?: boolean;
daysLeftInTrial?: number;
resourceTypes?: ResourceTypeDefinition[]; // Custom resource types
// Platform-controlled permissions
canManageOAuthCredentials?: boolean;
// Plan permissions (what features are available based on subscription)
planPermissions?: PlanPermissions;
}
export type UserRole = 'superuser' | 'platform_manager' | 'platform_support' | 'owner' | 'manager' | 'staff' | 'resource' | 'customer';
export interface NotificationPreferences {
email: boolean;
sms: boolean;
in_app: boolean;
appointment_reminders: boolean;
marketing: boolean;
}
export interface QuotaOverage {
id: number;
quota_type: string;
display_name: string;
current_usage: number;
allowed_limit: number;
overage_amount: number;
days_remaining: number;
grace_period_ends_at: string;
}
export interface User {
id: string | number;
username?: string;
name: string;
email: string;
role: UserRole;
avatarUrl?: string;
phone?: string;
email_verified?: boolean;
two_factor_enabled?: boolean;
timezone?: string;
locale?: string;
notification_preferences?: NotificationPreferences;
can_invite_staff?: boolean;
can_access_tickets?: boolean;
can_send_messages?: boolean;
can_edit_schedule?: boolean;
linked_resource_id?: number;
linked_resource_name?: string;
permissions?: Record<string, boolean>;
quota_overages?: QuotaOverage[];
}
export type ResourceType = 'STAFF' | 'ROOM' | 'EQUIPMENT';
export type ResourceTypeCategory = 'STAFF' | 'OTHER';
export interface ResourceTypeDefinition {
id: string;
name: string; // User-facing name like "Stylist", "Massage Therapist", "Treatment Room"
description?: string; // Description of this resource type
category: ResourceTypeCategory; // STAFF (requires staff assignment) or OTHER
isDefault: boolean; // Cannot be deleted
iconName?: string; // Optional icon identifier
}
export interface Resource {
id: string;
name: string;
type: ResourceType; // Legacy field - will be deprecated
typeId?: string; // New field - references ResourceTypeDefinition
userId?: string;
maxConcurrentEvents: number;
savedLaneCount?: number; // Remembered lane count when multilane is disabled
created_at?: string; // Used for quota overage calculation (oldest archived first)
is_archived_by_quota?: boolean; // True if archived due to quota overage
userCanEditSchedule?: boolean; // Allow linked user to edit their schedule regardless of role
}
// Backend uses: SCHEDULED, EN_ROUTE, IN_PROGRESS, CANCELED, COMPLETED, AWAITING_PAYMENT, PAID, NOSHOW
// Frontend aliases: PENDING (for SCHEDULED), CONFIRMED (for SCHEDULED), CANCELLED (for CANCELED), NO_SHOW (for NOSHOW)
export type AppointmentStatus =
| 'SCHEDULED' | 'EN_ROUTE' | 'IN_PROGRESS' | 'CANCELED' | 'COMPLETED' | 'AWAITING_PAYMENT' | 'PAID' | 'NOSHOW'
// Legacy aliases for frontend compatibility
| 'PENDING' | 'CONFIRMED' | 'CANCELLED' | 'NO_SHOW';
export interface Appointment {
id: string;
resourceId: string | null; // null if unassigned
customerId?: string; // optional for walk-in appointments
customerName: string;
serviceId: string;
startTime: Date; // For MVP, we will assume a specific date
durationMinutes: number;
status: AppointmentStatus;
notes?: string;
}
export interface Blocker {
id: string;
resourceId: string;
startTime: Date;
durationMinutes: number;
title: string;
}
export interface PaymentMethod {
id: string;
brand: 'Visa' | 'Mastercard' | 'Amex';
last4: string;
isDefault: boolean;
}
export interface Customer {
id: string;
name: string;
email: string;
phone: string;
city?: string;
state?: string;
zip?: string;
totalSpend: number;
lastVisit: Date | null;
status: 'Active' | 'Inactive' | 'Blocked';
avatarUrl?: string;
tags?: string[];
userId?: string;
paymentMethods: PaymentMethod[];
}
export interface Service {
id: string;
name: string;
durationMinutes: number;
duration?: number; // Duration in minutes (backend field name)
price: number;
price_cents?: number; // Price in cents
description: string;
displayOrder: number;
display_order?: number;
photos?: string[];
is_active?: boolean;
created_at?: string; // Used for quota overage calculation (oldest archived first)
updated_at?: string;
is_archived_by_quota?: boolean; // True if archived due to quota overage
// Pricing fields
variable_pricing?: boolean; // If true, final price is determined after service completion
deposit_amount?: number | null; // Fixed deposit amount in dollars
deposit_amount_cents?: number | null; // Fixed deposit amount in cents
deposit_percent?: number | null; // Deposit as percentage (only for fixed pricing)
requires_deposit?: boolean; // True if deposit configured (computed)
requires_saved_payment_method?: boolean; // True if deposit > 0 or variable pricing (computed)
deposit_display?: string | null; // Human-readable deposit description
// Resource assignment
all_resources?: boolean;
resource_ids?: string[];
resource_names?: string[];
// Buffer time (frontend-only for now)
prep_time?: number;
takedown_time?: number;
// Notification settings (frontend-only for now)
reminder_enabled?: boolean;
reminder_hours_before?: number;
reminder_email?: boolean;
reminder_sms?: boolean;
thank_you_email_enabled?: boolean;
// Category (future feature)
category?: string | null;
}
export interface Metric {
label: string;
value: string;
change: string;
trend: 'up' | 'down' | 'neutral';
}
// --- Platform Types ---
export type TicketType = 'PLATFORM' | 'CUSTOMER' | 'STAFF_REQUEST' | 'INTERNAL';
export type TicketStatus = 'OPEN' | 'IN_PROGRESS' | 'RESOLVED' | 'CLOSED' | 'AWAITING_RESPONSE';
export type TicketPriority = 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
export type TicketCategory =
| 'BILLING'
| 'TECHNICAL'
| 'FEATURE_REQUEST'
| 'ACCOUNT'
| 'APPOINTMENT'
| 'REFUND'
| 'COMPLAINT'
| 'GENERAL_INQUIRY'
| 'TIME_OFF'
| 'SCHEDULE_CHANGE'
| 'EQUIPMENT'
| 'OTHER';
export interface TicketComment {
id: string;
ticket: string; // Ticket ID
author: string; // User ID
authorEmail: string;
authorFullName: string;
commentText: string;
createdAt: string; // Date string
isInternal: boolean;
}
export interface TicketEmailAddressListItem {
id: number;
display_name: string;
email_address: string;
color: string;
is_active: boolean;
is_default: boolean;
}
export interface Ticket {
id: string;
tenant?: string; // Tenant ID, optional for platform tickets
creator: string; // User ID
creatorEmail: string;
creatorFullName: string;
assignee?: string; // User ID, optional
assigneeEmail?: string;
assigneeFullName?: string;
ticketType: TicketType;
status: TicketStatus;
priority: TicketPriority;
subject: string;
description: string;
category: TicketCategory;
relatedAppointmentId?: string; // Appointment ID, optional
dueAt?: string; // Date string
firstResponseAt?: string; // Date string
isOverdue?: boolean;
createdAt: string; // Date string
updatedAt: string; // Date string
resolvedAt?: string; // Date string
comments?: TicketComment[]; // Nested comments
// External sender info (for tickets from non-registered users via email)
externalEmail?: string;
externalName?: string;
// Source email address (which email address received/sent this ticket)
source_email_address?: TicketEmailAddressListItem;
}
export interface TicketTemplate {
id: string;
tenant?: string; // Tenant ID, optional for platform templates
name: string;
description: string;
ticketType: TicketType;
category: TicketCategory;
defaultPriority: TicketPriority;
subjectTemplate: string;
descriptionTemplate: string;
isActive: boolean;
createdAt: string; // Date string
}
export interface CannedResponse {
id: string;
tenant?: string; // Tenant ID, optional for platform canned responses
title: string;
content: string;
category?: TicketCategory;
isActive: boolean;
useCount: number;
createdBy?: string; // User ID
createdAt: string; // Date string
}
export interface PlatformMetric {
label: string;
value: string;
change: string;
trend: 'up' | 'down' | 'neutral';
color: 'blue' | 'green' | 'purple' | 'orange';
}
// --- OAuth Settings Types ---
export interface OAuthProvider {
id: string;
name: string;
icon: string;
description: string;
}
export interface BusinessOAuthSettings {
enabledProviders: string[];
allowRegistration: boolean;
autoLinkByEmail: boolean;
useCustomCredentials: boolean;
}
export interface BusinessOAuthSettingsResponse {
settings: BusinessOAuthSettings;
availableProviders: OAuthProvider[];
}
// --- OAuth Credentials Types ---
export interface OAuthProviderCredential {
client_id: string;
client_secret: string;
has_secret: boolean;
}
export interface BusinessOAuthCredentialsResponse {
credentials: Record<string, OAuthProviderCredential>;
useCustomCredentials: boolean;
}
// --- Plugin Types ---
export type PluginCategory = 'EMAIL' | 'REPORTS' | 'CUSTOMER' | 'BOOKING' | 'INTEGRATION' | 'AUTOMATION' | 'OTHER';
export interface PluginTemplate {
id: string;
name: string;
description: string;
category: PluginCategory;
version: string;
author: string;
logoUrl?: string;
pluginCode?: string;
rating: number;
ratingCount: number;
installCount: number;
isVerified: boolean;
isFeatured: boolean;
createdAt: string;
updatedAt: string;
}
export interface PluginInstallation {
id: string;
template: string;
templateName: string;
templateDescription: string;
category: PluginCategory;
version: string;
authorName?: string;
logoUrl?: string;
templateVariables?: Record<string, any>;
configValues?: Record<string, any>;
isActive: boolean;
installedAt: string;
hasUpdate: boolean;
rating?: number;
review?: string;
scheduledTaskId?: string;
}
// --- Email Template Types ---
export type EmailTemplateScope = 'BUSINESS' | 'PLATFORM';
export type EmailTemplateCategory =
| 'APPOINTMENT'
| 'REMINDER'
| 'CONFIRMATION'
| 'MARKETING'
| 'NOTIFICATION'
| 'REPORT'
| 'OTHER';
export interface EmailTemplate {
id: string;
name: string;
description: string;
subject: string;
htmlContent: string;
textContent: string;
scope: EmailTemplateScope;
isDefault: boolean;
category: EmailTemplateCategory;
previewContext?: Record<string, any>;
createdBy?: number;
createdByName?: string;
createdAt: string;
updatedAt: string;
}
export interface EmailTemplatePreview {
subject: string;
htmlContent: string;
textContent: string;
forceFooter: boolean;
}
export interface EmailTemplateVariable {
code: string;
description: string;
}
export interface EmailTemplateVariableGroup {
category: string;
items: EmailTemplateVariable[];
}
// --- Contract Types ---
export type ContractScope = 'CUSTOMER' | 'APPOINTMENT';
export type ContractStatus = 'PENDING' | 'SIGNED' | 'EXPIRED' | 'VOIDED';
export type ContractTemplateStatus = 'DRAFT' | 'ACTIVE' | 'ARCHIVED';
export interface ContractTemplate {
id: string;
name: string;
description: string;
content: string;
scope: ContractScope;
status: ContractTemplateStatus;
expires_after_days: number | null;
version: number;
version_notes: string;
services: { id: string; name: string }[];
created_by: string | null;
created_by_name: string | null;
created_at: string;
updated_at: string;
}
export interface Contract {
id: string;
template: string;
template_name: string;
template_version: number;
scope: ContractScope;
status: ContractStatus;
content: string;
customer?: string;
customer_name?: string;
customer_email?: string;
appointment?: string;
appointment_service_name?: string;
appointment_start_time?: string;
service?: string;
service_name?: string;
sent_at: string | null;
signed_at: string | null;
expires_at: string | null;
voided_at: string | null;
voided_reason: string | null;
public_token: string;
created_at: string;
updated_at: string;
}
export interface ContractSignature {
id: string;
contract: string;
signer_name: string;
signer_email: string;
signature_data: string;
ip_address: string;
user_agent: string;
signed_at: string;
}
export interface ContractPublicView {
contract: Contract;
template: {
name: string;
content: string;
};
business: {
name: string;
logo_url?: string;
};
customer?: {
name: string;
email: string;
};
appointment?: {
service_name: string;
start_time: string;
};
is_expired: boolean;
can_sign: boolean;
signature?: ContractSignature;
}
// --- Time Blocking Types ---
export type BlockType = 'HARD' | 'SOFT';
export type BlockPurpose = 'CLOSURE' | 'UNAVAILABLE' | 'BUSINESS_HOURS' | 'OTHER';
export type RecurrenceType = 'NONE' | 'WEEKLY' | 'MONTHLY' | 'YEARLY' | 'HOLIDAY';
export type TimeBlockLevel = 'business' | 'resource';
export type HolidayType = 'FIXED' | 'FLOATING' | 'CALCULATED';
export interface Holiday {
code: string;
name: string;
country: string;
holiday_type?: HolidayType;
month?: number;
day?: number;
week_of_month?: number;
day_of_week?: number;
calculation_rule?: string;
is_active?: boolean;
next_occurrence?: string; // ISO date string
}
export interface RecurrencePattern {
days_of_week?: number[]; // 0=Mon, 6=Sun (for WEEKLY)
days_of_month?: number[]; // 1-31 (for MONTHLY)
month?: number; // 1-12 (for YEARLY)
day?: number; // 1-31 (for YEARLY)
holiday_code?: string; // holiday code (for HOLIDAY)
}
export interface TimeBlock {
id: string;
title: string;
description?: string;
resource?: string | null; // Resource ID or null for business-level
resource_name?: string;
level: TimeBlockLevel;
block_type: BlockType;
purpose: BlockPurpose;
recurrence_type: RecurrenceType;
start_date?: string; // ISO date string (for NONE type)
end_date?: string; // ISO date string (for NONE type)
all_day: boolean;
start_time?: string; // HH:MM:SS (if not all_day)
end_time?: string; // HH:MM:SS (if not all_day)
recurrence_pattern?: RecurrencePattern;
pattern_display?: string; // Human-readable pattern description
holiday_name?: string; // Holiday name if HOLIDAY type
recurrence_start?: string; // ISO date string
recurrence_end?: string; // ISO date string
is_active: boolean;
created_by?: string;
created_by_name?: string;
conflict_count?: number;
created_at: string;
updated_at?: string;
}
export type ApprovalStatus = 'APPROVED' | 'PENDING' | 'DENIED';
export interface TimeBlockListItem {
id: string;
title: string;
description?: string;
resource?: string | null;
resource_name?: string;
level: TimeBlockLevel;
block_type: BlockType;
purpose: BlockPurpose;
recurrence_type: RecurrenceType;
start_date?: string;
end_date?: string;
all_day?: boolean;
start_time?: string;
end_time?: string;
recurrence_pattern?: RecurrencePattern;
recurrence_start?: string;
recurrence_end?: string;
pattern_display?: string;
is_active: boolean;
created_at: string;
approval_status?: ApprovalStatus;
reviewed_by?: number;
reviewed_by_name?: string;
reviewed_at?: string;
review_notes?: string;
created_by_name?: string;
}
export interface BlockedDate {
date: string; // ISO date string
block_type: BlockType;
purpose: BlockPurpose;
title: string;
resource_id: string | null;
all_day: boolean;
start_time: string | null;
end_time: string | null;
time_block_id: string;
}
export interface TimeBlockConflict {
event_id: string;
title: string;
start_time: string;
end_time: string;
}
export interface TimeBlockConflictCheck {
has_conflicts: boolean;
conflict_count: number;
conflicts: TimeBlockConflict[];
}
export interface MyBlocksResponse {
business_blocks: TimeBlockListItem[];
my_blocks: TimeBlockListItem[];
resource_id: string | null;
resource_name: string | null;
can_self_approve: boolean;
}