feat(shared): add types and Zod validations
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
41
packages/shared/package-lock.json
generated
Normal file
41
packages/shared/package-lock.json
generated
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "@padel-pro/shared",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@padel-pro/shared",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.22.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.25.76",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
packages/shared/package.json
Normal file
17
packages/shared/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "@padel-pro/shared",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"lint": "eslint src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.22.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/shared/src/index.ts
Normal file
5
packages/shared/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Re-export all types
|
||||||
|
export * from './types/index';
|
||||||
|
|
||||||
|
// Re-export all validations
|
||||||
|
export * from './validations/index';
|
||||||
531
packages/shared/src/types/index.ts
Normal file
531
packages/shared/src/types/index.ts
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
// ==========================================
|
||||||
|
// Enums
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export type UserRole = 'super_admin' | 'org_admin' | 'site_admin' | 'staff' | 'coach' | 'client';
|
||||||
|
|
||||||
|
export type CourtType = 'indoor' | 'outdoor' | 'covered';
|
||||||
|
|
||||||
|
export type CourtStatus = 'active' | 'maintenance' | 'inactive';
|
||||||
|
|
||||||
|
export type BookingStatus = 'pending' | 'confirmed' | 'cancelled' | 'completed' | 'no_show';
|
||||||
|
|
||||||
|
export type PaymentType = 'cash' | 'card' | 'transfer' | 'membership' | 'pending';
|
||||||
|
|
||||||
|
export type MembershipStatus = 'active' | 'expired' | 'cancelled' | 'suspended';
|
||||||
|
|
||||||
|
export type TournamentType = 'single_elimination' | 'double_elimination' | 'round_robin' | 'swiss';
|
||||||
|
|
||||||
|
export type TournamentStatus = 'draft' | 'registration_open' | 'registration_closed' | 'in_progress' | 'completed' | 'cancelled';
|
||||||
|
|
||||||
|
export type MatchStatus = 'scheduled' | 'in_progress' | 'completed' | 'cancelled' | 'walkover';
|
||||||
|
|
||||||
|
export type CashRegisterStatus = 'open' | 'closed';
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Core Interfaces
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export interface Organization {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
logo?: string;
|
||||||
|
primaryColor?: string;
|
||||||
|
secondaryColor?: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
address?: string;
|
||||||
|
taxId?: string;
|
||||||
|
settings: OrganizationSettings;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrganizationSettings {
|
||||||
|
timezone: string;
|
||||||
|
currency: string;
|
||||||
|
dateFormat: string;
|
||||||
|
timeFormat: '12h' | '24h';
|
||||||
|
defaultBookingDuration: number; // in minutes
|
||||||
|
cancellationPolicy: {
|
||||||
|
allowCancellation: boolean;
|
||||||
|
hoursBeforeBooking: number;
|
||||||
|
refundPercentage: number;
|
||||||
|
};
|
||||||
|
bookingRules: {
|
||||||
|
minAdvanceBooking: number; // hours
|
||||||
|
maxAdvanceBooking: number; // days
|
||||||
|
requirePayment: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Site {
|
||||||
|
id: string;
|
||||||
|
organizationId: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
address: string;
|
||||||
|
city: string;
|
||||||
|
state?: string;
|
||||||
|
country: string;
|
||||||
|
postalCode?: string;
|
||||||
|
latitude?: number;
|
||||||
|
longitude?: number;
|
||||||
|
phone?: string;
|
||||||
|
email?: string;
|
||||||
|
openingHours: WeeklySchedule;
|
||||||
|
amenities: string[];
|
||||||
|
images: string[];
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeeklySchedule {
|
||||||
|
monday: DaySchedule;
|
||||||
|
tuesday: DaySchedule;
|
||||||
|
wednesday: DaySchedule;
|
||||||
|
thursday: DaySchedule;
|
||||||
|
friday: DaySchedule;
|
||||||
|
saturday: DaySchedule;
|
||||||
|
sunday: DaySchedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DaySchedule {
|
||||||
|
isOpen: boolean;
|
||||||
|
openTime?: string; // HH:mm format
|
||||||
|
closeTime?: string; // HH:mm format
|
||||||
|
breaks?: TimeSlot[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeSlot {
|
||||||
|
startTime: string; // HH:mm format
|
||||||
|
endTime: string; // HH:mm format
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Court {
|
||||||
|
id: string;
|
||||||
|
siteId: string;
|
||||||
|
name: string;
|
||||||
|
type: CourtType;
|
||||||
|
status: CourtStatus;
|
||||||
|
description?: string;
|
||||||
|
features: string[];
|
||||||
|
hourlyRate: number;
|
||||||
|
peakHourlyRate?: number;
|
||||||
|
peakHours?: TimeSlot[];
|
||||||
|
images: string[];
|
||||||
|
position?: number; // for ordering
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// User & Client Interfaces
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
passwordHash?: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
phone?: string;
|
||||||
|
avatar?: string;
|
||||||
|
role: UserRole;
|
||||||
|
organizationId?: string;
|
||||||
|
siteIds: string[]; // sites user has access to
|
||||||
|
permissions: string[];
|
||||||
|
isActive: boolean;
|
||||||
|
emailVerified: boolean;
|
||||||
|
lastLoginAt?: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Client {
|
||||||
|
id: string;
|
||||||
|
userId?: string; // linked user account (optional)
|
||||||
|
organizationId: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
phone?: string;
|
||||||
|
dateOfBirth?: Date;
|
||||||
|
gender?: 'male' | 'female' | 'other' | 'prefer_not_to_say';
|
||||||
|
avatar?: string;
|
||||||
|
address?: string;
|
||||||
|
city?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
country?: string;
|
||||||
|
notes?: string;
|
||||||
|
tags: string[];
|
||||||
|
preferredSiteId?: string;
|
||||||
|
skillLevel?: 'beginner' | 'intermediate' | 'advanced' | 'professional';
|
||||||
|
emergencyContact?: EmergencyContact;
|
||||||
|
marketingConsent: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmergencyContact {
|
||||||
|
name: string;
|
||||||
|
relationship: string;
|
||||||
|
phone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Booking Interfaces
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export interface Booking {
|
||||||
|
id: string;
|
||||||
|
courtId: string;
|
||||||
|
siteId: string;
|
||||||
|
organizationId: string;
|
||||||
|
clientId: string;
|
||||||
|
createdByUserId?: string;
|
||||||
|
startTime: Date;
|
||||||
|
endTime: Date;
|
||||||
|
duration: number; // in minutes
|
||||||
|
status: BookingStatus;
|
||||||
|
paymentType: PaymentType;
|
||||||
|
paymentStatus: 'pending' | 'paid' | 'refunded' | 'partial';
|
||||||
|
amount: number;
|
||||||
|
paidAmount: number;
|
||||||
|
currency: string;
|
||||||
|
participants: BookingParticipant[];
|
||||||
|
notes?: string;
|
||||||
|
internalNotes?: string;
|
||||||
|
isRecurring: boolean;
|
||||||
|
recurringId?: string;
|
||||||
|
cancellationReason?: string;
|
||||||
|
cancelledAt?: Date;
|
||||||
|
cancelledByUserId?: string;
|
||||||
|
checkInAt?: Date;
|
||||||
|
checkOutAt?: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BookingParticipant {
|
||||||
|
clientId?: string;
|
||||||
|
name: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
isPayer: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecurringBooking {
|
||||||
|
id: string;
|
||||||
|
courtId: string;
|
||||||
|
siteId: string;
|
||||||
|
organizationId: string;
|
||||||
|
clientId: string;
|
||||||
|
createdByUserId?: string;
|
||||||
|
dayOfWeek: number; // 0-6 (Sunday-Saturday)
|
||||||
|
startTime: string; // HH:mm format
|
||||||
|
endTime: string; // HH:mm format
|
||||||
|
startDate: Date;
|
||||||
|
endDate?: Date;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Membership Interfaces
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export interface MembershipPlan {
|
||||||
|
id: string;
|
||||||
|
organizationId: string;
|
||||||
|
siteIds: string[]; // applicable sites
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
durationMonths: number;
|
||||||
|
price: number;
|
||||||
|
currency: string;
|
||||||
|
benefits: MembershipBenefit[];
|
||||||
|
bookingDiscount?: number; // percentage
|
||||||
|
maxActiveBookings?: number;
|
||||||
|
priorityBooking: boolean;
|
||||||
|
guestPasses?: number;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MembershipBenefit {
|
||||||
|
type: 'discount' | 'free_hours' | 'priority' | 'guest_pass' | 'product_discount' | 'custom';
|
||||||
|
description: string;
|
||||||
|
value?: number;
|
||||||
|
conditions?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Membership {
|
||||||
|
id: string;
|
||||||
|
clientId: string;
|
||||||
|
planId: string;
|
||||||
|
organizationId: string;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
status: MembershipStatus;
|
||||||
|
autoRenew: boolean;
|
||||||
|
paymentMethod?: string;
|
||||||
|
remainingGuestPasses: number;
|
||||||
|
usedHours: number;
|
||||||
|
notes?: string;
|
||||||
|
suspendedAt?: Date;
|
||||||
|
suspensionReason?: string;
|
||||||
|
cancelledAt?: Date;
|
||||||
|
cancellationReason?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Product & Sales Interfaces
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export interface ProductCategory {
|
||||||
|
id: string;
|
||||||
|
organizationId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
parentId?: string;
|
||||||
|
position: number;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Product {
|
||||||
|
id: string;
|
||||||
|
organizationId: string;
|
||||||
|
siteIds: string[]; // available at these sites
|
||||||
|
categoryId?: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
sku?: string;
|
||||||
|
barcode?: string;
|
||||||
|
price: number;
|
||||||
|
cost?: number;
|
||||||
|
currency: string;
|
||||||
|
taxRate: number;
|
||||||
|
trackInventory: boolean;
|
||||||
|
images: string[];
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductInventory {
|
||||||
|
id: string;
|
||||||
|
productId: string;
|
||||||
|
siteId: string;
|
||||||
|
quantity: number;
|
||||||
|
minQuantity?: number;
|
||||||
|
location?: string;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Sale {
|
||||||
|
id: string;
|
||||||
|
siteId: string;
|
||||||
|
organizationId: string;
|
||||||
|
clientId?: string;
|
||||||
|
userId: string; // staff who made the sale
|
||||||
|
cashRegisterId?: string;
|
||||||
|
items: SaleItem[];
|
||||||
|
subtotal: number;
|
||||||
|
taxAmount: number;
|
||||||
|
discountAmount: number;
|
||||||
|
total: number;
|
||||||
|
currency: string;
|
||||||
|
paymentType: PaymentType;
|
||||||
|
paymentReference?: string;
|
||||||
|
notes?: string;
|
||||||
|
voidedAt?: Date;
|
||||||
|
voidedByUserId?: string;
|
||||||
|
voidReason?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaleItem {
|
||||||
|
productId?: string;
|
||||||
|
bookingId?: string;
|
||||||
|
membershipId?: string;
|
||||||
|
description: string;
|
||||||
|
quantity: number;
|
||||||
|
unitPrice: number;
|
||||||
|
taxRate: number;
|
||||||
|
discountAmount: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CashRegister {
|
||||||
|
id: string;
|
||||||
|
siteId: string;
|
||||||
|
name: string;
|
||||||
|
status: CashRegisterStatus;
|
||||||
|
openedAt?: Date;
|
||||||
|
openedByUserId?: string;
|
||||||
|
openingBalance?: number;
|
||||||
|
closedAt?: Date;
|
||||||
|
closedByUserId?: string;
|
||||||
|
closingBalance?: number;
|
||||||
|
expectedBalance?: number;
|
||||||
|
notes?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Tournament Interfaces
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export interface Tournament {
|
||||||
|
id: string;
|
||||||
|
siteId: string;
|
||||||
|
organizationId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
type: TournamentType;
|
||||||
|
status: TournamentStatus;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
registrationStartDate: Date;
|
||||||
|
registrationEndDate: Date;
|
||||||
|
maxTeams: number;
|
||||||
|
minTeams: number;
|
||||||
|
teamSize: number; // players per team (1 for singles, 2 for doubles)
|
||||||
|
entryFee: number;
|
||||||
|
currency: string;
|
||||||
|
prizePool?: number;
|
||||||
|
prizes?: TournamentPrize[];
|
||||||
|
rules?: string;
|
||||||
|
courtIds: string[];
|
||||||
|
categories: TournamentCategory[];
|
||||||
|
images: string[];
|
||||||
|
isPublic: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TournamentCategory {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
minSkillLevel?: string;
|
||||||
|
maxSkillLevel?: string;
|
||||||
|
minAge?: number;
|
||||||
|
maxAge?: number;
|
||||||
|
gender?: 'male' | 'female' | 'mixed' | 'any';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TournamentPrize {
|
||||||
|
position: number;
|
||||||
|
description: string;
|
||||||
|
amount?: number;
|
||||||
|
currency?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TournamentTeam {
|
||||||
|
id: string;
|
||||||
|
tournamentId: string;
|
||||||
|
categoryId: string;
|
||||||
|
name: string;
|
||||||
|
players: TournamentPlayer[];
|
||||||
|
seed?: number;
|
||||||
|
status: 'registered' | 'confirmed' | 'withdrawn' | 'disqualified';
|
||||||
|
paidAt?: Date;
|
||||||
|
notes?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TournamentPlayer {
|
||||||
|
clientId: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
isCaptain: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TournamentMatch {
|
||||||
|
id: string;
|
||||||
|
tournamentId: string;
|
||||||
|
categoryId: string;
|
||||||
|
round: number;
|
||||||
|
matchNumber: number;
|
||||||
|
courtId?: string;
|
||||||
|
scheduledAt?: Date;
|
||||||
|
team1Id?: string;
|
||||||
|
team2Id?: string;
|
||||||
|
winnerId?: string;
|
||||||
|
score?: MatchScore;
|
||||||
|
status: MatchStatus;
|
||||||
|
notes?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MatchScore {
|
||||||
|
sets: SetScore[];
|
||||||
|
winner: 'team1' | 'team2';
|
||||||
|
duration?: number; // in minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetScore {
|
||||||
|
team1Score: number;
|
||||||
|
team2Score: number;
|
||||||
|
tiebreak?: TiebreakScore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TiebreakScore {
|
||||||
|
team1Score: number;
|
||||||
|
team2Score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Utility Types
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export type WithTimestamps<T> = T & {
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateInput<T> = Omit<T, 'id' | 'createdAt' | 'updatedAt'>;
|
||||||
|
|
||||||
|
export type UpdateInput<T> = Partial<Omit<T, 'id' | 'createdAt' | 'updatedAt'>>;
|
||||||
|
|
||||||
|
export type PaginationParams = {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder?: 'asc' | 'desc';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PaginatedResponse<T> = {
|
||||||
|
data: T[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiResponse<T> = {
|
||||||
|
success: boolean;
|
||||||
|
data?: T;
|
||||||
|
error?: {
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
details?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
};
|
||||||
575
packages/shared/src/validations/index.ts
Normal file
575
packages/shared/src/validations/index.ts
Normal file
@@ -0,0 +1,575 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Common Schemas
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export const emailSchema = z.string().email('Invalid email address');
|
||||||
|
|
||||||
|
export const phoneSchema = z.string().regex(
|
||||||
|
/^[+]?[(]?[0-9]{1,4}[)]?[-\s./0-9]*$/,
|
||||||
|
'Invalid phone number'
|
||||||
|
).optional();
|
||||||
|
|
||||||
|
export const passwordSchema = z
|
||||||
|
.string()
|
||||||
|
.min(8, 'Password must be at least 8 characters')
|
||||||
|
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
||||||
|
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
||||||
|
.regex(/[0-9]/, 'Password must contain at least one number');
|
||||||
|
|
||||||
|
export const timeSchema = z.string().regex(
|
||||||
|
/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/,
|
||||||
|
'Invalid time format (HH:mm)'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const slugSchema = z
|
||||||
|
.string()
|
||||||
|
.min(2, 'Slug must be at least 2 characters')
|
||||||
|
.max(50, 'Slug must be at most 50 characters')
|
||||||
|
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Slug must be lowercase and contain only letters, numbers, and hyphens');
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Auth Schemas
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export const loginSchema = z.object({
|
||||||
|
email: emailSchema,
|
||||||
|
password: z.string().min(1, 'Password is required'),
|
||||||
|
rememberMe: z.boolean().optional().default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type LoginInput = z.infer<typeof loginSchema>;
|
||||||
|
|
||||||
|
export const registerClientSchema = z.object({
|
||||||
|
email: emailSchema,
|
||||||
|
password: passwordSchema,
|
||||||
|
confirmPassword: z.string(),
|
||||||
|
firstName: z.string().min(1, 'First name is required').max(50),
|
||||||
|
lastName: z.string().min(1, 'Last name is required').max(50),
|
||||||
|
phone: phoneSchema,
|
||||||
|
dateOfBirth: z.coerce.date().optional(),
|
||||||
|
gender: z.enum(['male', 'female', 'other', 'prefer_not_to_say']).optional(),
|
||||||
|
marketingConsent: z.boolean().optional().default(false),
|
||||||
|
termsAccepted: z.boolean().refine(val => val === true, {
|
||||||
|
message: 'You must accept the terms and conditions',
|
||||||
|
}),
|
||||||
|
}).refine(data => data.password === data.confirmPassword, {
|
||||||
|
message: 'Passwords do not match',
|
||||||
|
path: ['confirmPassword'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RegisterClientInput = z.infer<typeof registerClientSchema>;
|
||||||
|
|
||||||
|
export const forgotPasswordSchema = z.object({
|
||||||
|
email: emailSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ForgotPasswordInput = z.infer<typeof forgotPasswordSchema>;
|
||||||
|
|
||||||
|
export const resetPasswordSchema = z.object({
|
||||||
|
token: z.string().min(1, 'Reset token is required'),
|
||||||
|
password: passwordSchema,
|
||||||
|
confirmPassword: z.string(),
|
||||||
|
}).refine(data => data.password === data.confirmPassword, {
|
||||||
|
message: 'Passwords do not match',
|
||||||
|
path: ['confirmPassword'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ResetPasswordInput = z.infer<typeof resetPasswordSchema>;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Booking Schemas
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export const createBookingSchema = z.object({
|
||||||
|
courtId: z.string().uuid('Invalid court ID'),
|
||||||
|
clientId: z.string().uuid('Invalid client ID'),
|
||||||
|
startTime: z.coerce.date(),
|
||||||
|
endTime: z.coerce.date(),
|
||||||
|
participants: z.array(z.object({
|
||||||
|
clientId: z.string().uuid().optional(),
|
||||||
|
name: z.string().min(1, 'Participant name is required'),
|
||||||
|
email: emailSchema.optional(),
|
||||||
|
phone: phoneSchema,
|
||||||
|
isPayer: z.boolean().default(false),
|
||||||
|
})).optional().default([]),
|
||||||
|
paymentType: z.enum(['cash', 'card', 'transfer', 'membership', 'pending']).default('pending'),
|
||||||
|
notes: z.string().max(500).optional(),
|
||||||
|
}).refine(data => data.endTime > data.startTime, {
|
||||||
|
message: 'End time must be after start time',
|
||||||
|
path: ['endTime'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateBookingInput = z.infer<typeof createBookingSchema>;
|
||||||
|
|
||||||
|
export const updateBookingStatusSchema = z.object({
|
||||||
|
bookingId: z.string().uuid('Invalid booking ID'),
|
||||||
|
status: z.enum(['pending', 'confirmed', 'cancelled', 'completed', 'no_show']),
|
||||||
|
cancellationReason: z.string().max(500).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UpdateBookingStatusInput = z.infer<typeof updateBookingStatusSchema>;
|
||||||
|
|
||||||
|
export const createRecurringBookingSchema = z.object({
|
||||||
|
courtId: z.string().uuid('Invalid court ID'),
|
||||||
|
clientId: z.string().uuid('Invalid client ID'),
|
||||||
|
dayOfWeek: z.number().int().min(0).max(6),
|
||||||
|
startTime: timeSchema,
|
||||||
|
endTime: timeSchema,
|
||||||
|
startDate: z.coerce.date(),
|
||||||
|
endDate: z.coerce.date().optional(),
|
||||||
|
paymentType: z.enum(['cash', 'card', 'transfer', 'membership', 'pending']).default('pending'),
|
||||||
|
notes: z.string().max(500).optional(),
|
||||||
|
}).refine(data => {
|
||||||
|
const [startH, startM] = data.startTime.split(':').map(Number);
|
||||||
|
const [endH, endM] = data.endTime.split(':').map(Number);
|
||||||
|
return endH * 60 + endM > startH * 60 + startM;
|
||||||
|
}, {
|
||||||
|
message: 'End time must be after start time',
|
||||||
|
path: ['endTime'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateRecurringBookingInput = z.infer<typeof createRecurringBookingSchema>;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Site & Court Schemas
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
const dayScheduleSchema = z.object({
|
||||||
|
isOpen: z.boolean(),
|
||||||
|
openTime: timeSchema.optional(),
|
||||||
|
closeTime: timeSchema.optional(),
|
||||||
|
breaks: z.array(z.object({
|
||||||
|
startTime: timeSchema,
|
||||||
|
endTime: timeSchema,
|
||||||
|
})).optional(),
|
||||||
|
}).refine(data => {
|
||||||
|
if (data.isOpen) {
|
||||||
|
return data.openTime !== undefined && data.closeTime !== undefined;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, {
|
||||||
|
message: 'Open and close times are required when the day is open',
|
||||||
|
});
|
||||||
|
|
||||||
|
const weeklyScheduleSchema = z.object({
|
||||||
|
monday: dayScheduleSchema,
|
||||||
|
tuesday: dayScheduleSchema,
|
||||||
|
wednesday: dayScheduleSchema,
|
||||||
|
thursday: dayScheduleSchema,
|
||||||
|
friday: dayScheduleSchema,
|
||||||
|
saturday: dayScheduleSchema,
|
||||||
|
sunday: dayScheduleSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createSiteSchema = z.object({
|
||||||
|
name: z.string().min(1, 'Site name is required').max(100),
|
||||||
|
slug: slugSchema,
|
||||||
|
address: z.string().min(1, 'Address is required').max(200),
|
||||||
|
city: z.string().min(1, 'City is required').max(100),
|
||||||
|
state: z.string().max(100).optional(),
|
||||||
|
country: z.string().min(1, 'Country is required').max(100),
|
||||||
|
postalCode: z.string().max(20).optional(),
|
||||||
|
latitude: z.number().min(-90).max(90).optional(),
|
||||||
|
longitude: z.number().min(-180).max(180).optional(),
|
||||||
|
phone: phoneSchema,
|
||||||
|
email: emailSchema.optional(),
|
||||||
|
openingHours: weeklyScheduleSchema,
|
||||||
|
amenities: z.array(z.string()).optional().default([]),
|
||||||
|
images: z.array(z.string().url()).optional().default([]),
|
||||||
|
isActive: z.boolean().optional().default(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateSiteInput = z.infer<typeof createSiteSchema>;
|
||||||
|
|
||||||
|
export const updateSiteSchema = createSiteSchema.partial();
|
||||||
|
|
||||||
|
export type UpdateSiteInput = z.infer<typeof updateSiteSchema>;
|
||||||
|
|
||||||
|
export const createCourtSchema = z.object({
|
||||||
|
siteId: z.string().uuid('Invalid site ID'),
|
||||||
|
name: z.string().min(1, 'Court name is required').max(50),
|
||||||
|
type: z.enum(['indoor', 'outdoor', 'covered']),
|
||||||
|
status: z.enum(['active', 'maintenance', 'inactive']).default('active'),
|
||||||
|
description: z.string().max(500).optional(),
|
||||||
|
features: z.array(z.string()).optional().default([]),
|
||||||
|
hourlyRate: z.number().positive('Hourly rate must be positive'),
|
||||||
|
peakHourlyRate: z.number().positive().optional(),
|
||||||
|
peakHours: z.array(z.object({
|
||||||
|
startTime: timeSchema,
|
||||||
|
endTime: timeSchema,
|
||||||
|
})).optional(),
|
||||||
|
images: z.array(z.string().url()).optional().default([]),
|
||||||
|
position: z.number().int().min(0).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateCourtInput = z.infer<typeof createCourtSchema>;
|
||||||
|
|
||||||
|
export const updateCourtSchema = createCourtSchema.partial().omit({ siteId: true });
|
||||||
|
|
||||||
|
export type UpdateCourtInput = z.infer<typeof updateCourtSchema>;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Product & Sales Schemas
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export const createProductCategorySchema = z.object({
|
||||||
|
name: z.string().min(1, 'Category name is required').max(100),
|
||||||
|
description: z.string().max(500).optional(),
|
||||||
|
parentId: z.string().uuid().optional(),
|
||||||
|
position: z.number().int().min(0).optional().default(0),
|
||||||
|
isActive: z.boolean().optional().default(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateProductCategoryInput = z.infer<typeof createProductCategorySchema>;
|
||||||
|
|
||||||
|
export const createProductSchema = z.object({
|
||||||
|
categoryId: z.string().uuid().optional(),
|
||||||
|
siteIds: z.array(z.string().uuid()).min(1, 'At least one site is required'),
|
||||||
|
name: z.string().min(1, 'Product name is required').max(100),
|
||||||
|
description: z.string().max(1000).optional(),
|
||||||
|
sku: z.string().max(50).optional(),
|
||||||
|
barcode: z.string().max(50).optional(),
|
||||||
|
price: z.number().nonnegative('Price must be non-negative'),
|
||||||
|
cost: z.number().nonnegative().optional(),
|
||||||
|
currency: z.string().length(3, 'Currency must be a 3-letter code').default('EUR'),
|
||||||
|
taxRate: z.number().min(0).max(100).default(21),
|
||||||
|
trackInventory: z.boolean().optional().default(false),
|
||||||
|
images: z.array(z.string().url()).optional().default([]),
|
||||||
|
isActive: z.boolean().optional().default(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateProductInput = z.infer<typeof createProductSchema>;
|
||||||
|
|
||||||
|
export const updateProductSchema = createProductSchema.partial();
|
||||||
|
|
||||||
|
export type UpdateProductInput = z.infer<typeof updateProductSchema>;
|
||||||
|
|
||||||
|
const saleItemSchema = z.object({
|
||||||
|
productId: z.string().uuid().optional(),
|
||||||
|
bookingId: z.string().uuid().optional(),
|
||||||
|
membershipId: z.string().uuid().optional(),
|
||||||
|
description: z.string().min(1, 'Item description is required').max(200),
|
||||||
|
quantity: z.number().int().positive('Quantity must be positive'),
|
||||||
|
unitPrice: z.number().nonnegative('Unit price must be non-negative'),
|
||||||
|
taxRate: z.number().min(0).max(100).default(21),
|
||||||
|
discountAmount: z.number().nonnegative().optional().default(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createSaleSchema = z.object({
|
||||||
|
siteId: z.string().uuid('Invalid site ID'),
|
||||||
|
clientId: z.string().uuid().optional(),
|
||||||
|
cashRegisterId: z.string().uuid().optional(),
|
||||||
|
items: z.array(saleItemSchema).min(1, 'At least one item is required'),
|
||||||
|
discountAmount: z.number().nonnegative().optional().default(0),
|
||||||
|
paymentType: z.enum(['cash', 'card', 'transfer', 'membership', 'pending']),
|
||||||
|
paymentReference: z.string().max(100).optional(),
|
||||||
|
notes: z.string().max(500).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateSaleInput = z.infer<typeof createSaleSchema>;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Tournament Schemas
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
const tournamentCategorySchema = z.object({
|
||||||
|
id: z.string().uuid().optional(),
|
||||||
|
name: z.string().min(1, 'Category name is required').max(50),
|
||||||
|
description: z.string().max(200).optional(),
|
||||||
|
minSkillLevel: z.enum(['beginner', 'intermediate', 'advanced', 'professional']).optional(),
|
||||||
|
maxSkillLevel: z.enum(['beginner', 'intermediate', 'advanced', 'professional']).optional(),
|
||||||
|
minAge: z.number().int().min(0).max(100).optional(),
|
||||||
|
maxAge: z.number().int().min(0).max(100).optional(),
|
||||||
|
gender: z.enum(['male', 'female', 'mixed', 'any']).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tournamentPrizeSchema = z.object({
|
||||||
|
position: z.number().int().positive(),
|
||||||
|
description: z.string().min(1).max(200),
|
||||||
|
amount: z.number().nonnegative().optional(),
|
||||||
|
currency: z.string().length(3).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createTournamentSchema = z.object({
|
||||||
|
siteId: z.string().uuid('Invalid site ID'),
|
||||||
|
name: z.string().min(1, 'Tournament name is required').max(100),
|
||||||
|
description: z.string().max(2000).optional(),
|
||||||
|
type: z.enum(['single_elimination', 'double_elimination', 'round_robin', 'swiss']),
|
||||||
|
startDate: z.coerce.date(),
|
||||||
|
endDate: z.coerce.date(),
|
||||||
|
registrationStartDate: z.coerce.date(),
|
||||||
|
registrationEndDate: z.coerce.date(),
|
||||||
|
maxTeams: z.number().int().min(2, 'Minimum 2 teams required').max(256),
|
||||||
|
minTeams: z.number().int().min(2, 'Minimum 2 teams required').default(2),
|
||||||
|
teamSize: z.number().int().min(1).max(4).default(2),
|
||||||
|
entryFee: z.number().nonnegative().default(0),
|
||||||
|
currency: z.string().length(3, 'Currency must be a 3-letter code').default('EUR'),
|
||||||
|
prizePool: z.number().nonnegative().optional(),
|
||||||
|
prizes: z.array(tournamentPrizeSchema).optional(),
|
||||||
|
rules: z.string().max(5000).optional(),
|
||||||
|
courtIds: z.array(z.string().uuid()).min(1, 'At least one court is required'),
|
||||||
|
categories: z.array(tournamentCategorySchema).min(1, 'At least one category is required'),
|
||||||
|
images: z.array(z.string().url()).optional().default([]),
|
||||||
|
isPublic: z.boolean().optional().default(true),
|
||||||
|
}).refine(data => data.endDate >= data.startDate, {
|
||||||
|
message: 'End date must be on or after start date',
|
||||||
|
path: ['endDate'],
|
||||||
|
}).refine(data => data.registrationEndDate >= data.registrationStartDate, {
|
||||||
|
message: 'Registration end date must be on or after registration start date',
|
||||||
|
path: ['registrationEndDate'],
|
||||||
|
}).refine(data => data.registrationEndDate <= data.startDate, {
|
||||||
|
message: 'Registration must end before or on tournament start date',
|
||||||
|
path: ['registrationEndDate'],
|
||||||
|
}).refine(data => data.minTeams <= data.maxTeams, {
|
||||||
|
message: 'Minimum teams cannot exceed maximum teams',
|
||||||
|
path: ['minTeams'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateTournamentInput = z.infer<typeof createTournamentSchema>;
|
||||||
|
|
||||||
|
// Base tournament schema without refinements for partial updates
|
||||||
|
const baseTournamentSchema = z.object({
|
||||||
|
name: z.string().min(1, 'Tournament name is required').max(100),
|
||||||
|
description: z.string().max(2000).optional(),
|
||||||
|
type: z.enum(['single_elimination', 'double_elimination', 'round_robin', 'swiss']),
|
||||||
|
startDate: z.coerce.date(),
|
||||||
|
endDate: z.coerce.date(),
|
||||||
|
registrationStartDate: z.coerce.date(),
|
||||||
|
registrationEndDate: z.coerce.date(),
|
||||||
|
maxTeams: z.number().int().min(2, 'Minimum 2 teams required').max(256),
|
||||||
|
minTeams: z.number().int().min(2, 'Minimum 2 teams required').default(2),
|
||||||
|
teamSize: z.number().int().min(1).max(4).default(2),
|
||||||
|
entryFee: z.number().nonnegative().default(0),
|
||||||
|
currency: z.string().length(3, 'Currency must be a 3-letter code').default('EUR'),
|
||||||
|
prizePool: z.number().nonnegative().optional(),
|
||||||
|
prizes: z.array(tournamentPrizeSchema).optional(),
|
||||||
|
rules: z.string().max(5000).optional(),
|
||||||
|
courtIds: z.array(z.string().uuid()).min(1, 'At least one court is required'),
|
||||||
|
categories: z.array(tournamentCategorySchema).min(1, 'At least one category is required'),
|
||||||
|
images: z.array(z.string().url()).optional().default([]),
|
||||||
|
isPublic: z.boolean().optional().default(true),
|
||||||
|
status: z.enum(['draft', 'registration_open', 'registration_closed', 'in_progress', 'completed', 'cancelled']).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateTournamentSchema = baseTournamentSchema.partial();
|
||||||
|
|
||||||
|
export type UpdateTournamentInput = z.infer<typeof updateTournamentSchema>;
|
||||||
|
|
||||||
|
export const registerTournamentTeamSchema = z.object({
|
||||||
|
tournamentId: z.string().uuid('Invalid tournament ID'),
|
||||||
|
categoryId: z.string().uuid('Invalid category ID'),
|
||||||
|
name: z.string().min(1, 'Team name is required').max(50),
|
||||||
|
players: z.array(z.object({
|
||||||
|
clientId: z.string().uuid('Invalid client ID'),
|
||||||
|
firstName: z.string().min(1).max(50),
|
||||||
|
lastName: z.string().min(1).max(50),
|
||||||
|
email: emailSchema.optional(),
|
||||||
|
phone: phoneSchema,
|
||||||
|
isCaptain: z.boolean().default(false),
|
||||||
|
})).min(1, 'At least one player is required'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RegisterTournamentTeamInput = z.infer<typeof registerTournamentTeamSchema>;
|
||||||
|
|
||||||
|
export const updateMatchScoreSchema = z.object({
|
||||||
|
matchId: z.string().uuid('Invalid match ID'),
|
||||||
|
score: z.object({
|
||||||
|
sets: z.array(z.object({
|
||||||
|
team1Score: z.number().int().min(0),
|
||||||
|
team2Score: z.number().int().min(0),
|
||||||
|
tiebreak: z.object({
|
||||||
|
team1Score: z.number().int().min(0),
|
||||||
|
team2Score: z.number().int().min(0),
|
||||||
|
}).optional(),
|
||||||
|
})).min(1, 'At least one set is required'),
|
||||||
|
winner: z.enum(['team1', 'team2']),
|
||||||
|
duration: z.number().int().positive().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UpdateMatchScoreInput = z.infer<typeof updateMatchScoreSchema>;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Membership Schemas
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
const membershipBenefitSchema = z.object({
|
||||||
|
type: z.enum(['discount', 'free_hours', 'priority', 'guest_pass', 'product_discount', 'custom']),
|
||||||
|
description: z.string().min(1, 'Benefit description is required').max(200),
|
||||||
|
value: z.number().optional(),
|
||||||
|
conditions: z.string().max(500).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createMembershipPlanSchema = z.object({
|
||||||
|
siteIds: z.array(z.string().uuid()).min(1, 'At least one site is required'),
|
||||||
|
name: z.string().min(1, 'Plan name is required').max(100),
|
||||||
|
description: z.string().max(1000).optional(),
|
||||||
|
durationMonths: z.number().int().positive('Duration must be at least 1 month'),
|
||||||
|
price: z.number().nonnegative('Price must be non-negative'),
|
||||||
|
currency: z.string().length(3, 'Currency must be a 3-letter code').default('EUR'),
|
||||||
|
benefits: z.array(membershipBenefitSchema).optional().default([]),
|
||||||
|
bookingDiscount: z.number().min(0).max(100).optional(),
|
||||||
|
maxActiveBookings: z.number().int().positive().optional(),
|
||||||
|
priorityBooking: z.boolean().optional().default(false),
|
||||||
|
guestPasses: z.number().int().nonnegative().optional(),
|
||||||
|
isActive: z.boolean().optional().default(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateMembershipPlanInput = z.infer<typeof createMembershipPlanSchema>;
|
||||||
|
|
||||||
|
export const updateMembershipPlanSchema = createMembershipPlanSchema.partial();
|
||||||
|
|
||||||
|
export type UpdateMembershipPlanInput = z.infer<typeof updateMembershipPlanSchema>;
|
||||||
|
|
||||||
|
export const createMembershipSchema = z.object({
|
||||||
|
clientId: z.string().uuid('Invalid client ID'),
|
||||||
|
planId: z.string().uuid('Invalid plan ID'),
|
||||||
|
startDate: z.coerce.date(),
|
||||||
|
autoRenew: z.boolean().optional().default(false),
|
||||||
|
paymentMethod: z.string().max(50).optional(),
|
||||||
|
notes: z.string().max(500).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateMembershipInput = z.infer<typeof createMembershipSchema>;
|
||||||
|
|
||||||
|
export const updateMembershipStatusSchema = z.object({
|
||||||
|
membershipId: z.string().uuid('Invalid membership ID'),
|
||||||
|
status: z.enum(['active', 'expired', 'cancelled', 'suspended']),
|
||||||
|
reason: z.string().max(500).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UpdateMembershipStatusInput = z.infer<typeof updateMembershipStatusSchema>;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Client Schemas
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export const createClientSchema = z.object({
|
||||||
|
firstName: z.string().min(1, 'First name is required').max(50),
|
||||||
|
lastName: z.string().min(1, 'Last name is required').max(50),
|
||||||
|
email: emailSchema,
|
||||||
|
phone: phoneSchema,
|
||||||
|
dateOfBirth: z.coerce.date().optional(),
|
||||||
|
gender: z.enum(['male', 'female', 'other', 'prefer_not_to_say']).optional(),
|
||||||
|
address: z.string().max(200).optional(),
|
||||||
|
city: z.string().max(100).optional(),
|
||||||
|
postalCode: z.string().max(20).optional(),
|
||||||
|
country: z.string().max(100).optional(),
|
||||||
|
notes: z.string().max(1000).optional(),
|
||||||
|
tags: z.array(z.string()).optional().default([]),
|
||||||
|
preferredSiteId: z.string().uuid().optional(),
|
||||||
|
skillLevel: z.enum(['beginner', 'intermediate', 'advanced', 'professional']).optional(),
|
||||||
|
emergencyContact: z.object({
|
||||||
|
name: z.string().min(1).max(100),
|
||||||
|
relationship: z.string().min(1).max(50),
|
||||||
|
phone: z.string().min(1),
|
||||||
|
}).optional(),
|
||||||
|
marketingConsent: z.boolean().optional().default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateClientInput = z.infer<typeof createClientSchema>;
|
||||||
|
|
||||||
|
export const updateClientSchema = createClientSchema.partial();
|
||||||
|
|
||||||
|
export type UpdateClientInput = z.infer<typeof updateClientSchema>;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Organization Schemas
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export const createOrganizationSchema = z.object({
|
||||||
|
name: z.string().min(1, 'Organization name is required').max(100),
|
||||||
|
slug: slugSchema,
|
||||||
|
logo: z.string().url().optional(),
|
||||||
|
primaryColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/, 'Invalid color format').optional(),
|
||||||
|
secondaryColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/, 'Invalid color format').optional(),
|
||||||
|
email: emailSchema.optional(),
|
||||||
|
phone: phoneSchema,
|
||||||
|
address: z.string().max(200).optional(),
|
||||||
|
taxId: z.string().max(50).optional(),
|
||||||
|
settings: z.object({
|
||||||
|
timezone: z.string().default('Europe/Madrid'),
|
||||||
|
currency: z.string().length(3).default('EUR'),
|
||||||
|
dateFormat: z.string().default('DD/MM/YYYY'),
|
||||||
|
timeFormat: z.enum(['12h', '24h']).default('24h'),
|
||||||
|
defaultBookingDuration: z.number().int().positive().default(90),
|
||||||
|
cancellationPolicy: z.object({
|
||||||
|
allowCancellation: z.boolean().default(true),
|
||||||
|
hoursBeforeBooking: z.number().int().nonnegative().default(24),
|
||||||
|
refundPercentage: z.number().min(0).max(100).default(100),
|
||||||
|
}).optional(),
|
||||||
|
bookingRules: z.object({
|
||||||
|
minAdvanceBooking: z.number().int().nonnegative().default(0),
|
||||||
|
maxAdvanceBooking: z.number().int().positive().default(14),
|
||||||
|
requirePayment: z.boolean().default(false),
|
||||||
|
}).optional(),
|
||||||
|
}).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateOrganizationInput = z.infer<typeof createOrganizationSchema>;
|
||||||
|
|
||||||
|
export const updateOrganizationSchema = createOrganizationSchema.partial().omit({ slug: true });
|
||||||
|
|
||||||
|
export type UpdateOrganizationInput = z.infer<typeof updateOrganizationSchema>;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// User Schemas
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export const createUserSchema = z.object({
|
||||||
|
email: emailSchema,
|
||||||
|
password: passwordSchema,
|
||||||
|
firstName: z.string().min(1, 'First name is required').max(50),
|
||||||
|
lastName: z.string().min(1, 'Last name is required').max(50),
|
||||||
|
phone: phoneSchema,
|
||||||
|
role: z.enum(['super_admin', 'org_admin', 'site_admin', 'staff', 'coach', 'client']),
|
||||||
|
organizationId: z.string().uuid().optional(),
|
||||||
|
siteIds: z.array(z.string().uuid()).optional().default([]),
|
||||||
|
permissions: z.array(z.string()).optional().default([]),
|
||||||
|
isActive: z.boolean().optional().default(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateUserInput = z.infer<typeof createUserSchema>;
|
||||||
|
|
||||||
|
export const updateUserSchema = createUserSchema.partial().omit({ password: true });
|
||||||
|
|
||||||
|
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
|
||||||
|
|
||||||
|
export const changePasswordSchema = z.object({
|
||||||
|
currentPassword: z.string().min(1, 'Current password is required'),
|
||||||
|
newPassword: passwordSchema,
|
||||||
|
confirmPassword: z.string(),
|
||||||
|
}).refine(data => data.newPassword === data.confirmPassword, {
|
||||||
|
message: 'Passwords do not match',
|
||||||
|
path: ['confirmPassword'],
|
||||||
|
}).refine(data => data.currentPassword !== data.newPassword, {
|
||||||
|
message: 'New password must be different from current password',
|
||||||
|
path: ['newPassword'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ChangePasswordInput = z.infer<typeof changePasswordSchema>;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// Pagination & Filter Schemas
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
export const paginationSchema = z.object({
|
||||||
|
page: z.coerce.number().int().positive().optional().default(1),
|
||||||
|
limit: z.coerce.number().int().positive().max(100).optional().default(20),
|
||||||
|
sortBy: z.string().optional(),
|
||||||
|
sortOrder: z.enum(['asc', 'desc']).optional().default('desc'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type PaginationInput = z.infer<typeof paginationSchema>;
|
||||||
|
|
||||||
|
export const dateRangeSchema = z.object({
|
||||||
|
startDate: z.coerce.date(),
|
||||||
|
endDate: z.coerce.date(),
|
||||||
|
}).refine(data => data.endDate >= data.startDate, {
|
||||||
|
message: 'End date must be on or after start date',
|
||||||
|
path: ['endDate'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DateRangeInput = z.infer<typeof dateRangeSchema>;
|
||||||
19
packages/shared/tsconfig.json
Normal file
19
packages/shared/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user