feat(shared): add types and Zod validations
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
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>;
|
||||
Reference in New Issue
Block a user