feat(shared): add types and Zod validations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ivan
2026-02-01 06:15:04 +00:00
parent 039c17352b
commit b042e14fc7
6 changed files with 1188 additions and 0 deletions

41
packages/shared/package-lock.json generated Normal file
View 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"
}
}
}
}

View 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"
}
}

View File

@@ -0,0 +1,5 @@
// Re-export all types
export * from './types/index';
// Re-export all validations
export * from './validations/index';

View 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>;
};
};

View 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>;

View 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"]
}