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