import { z } from 'zod' const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/ function toMinutes(value: string): number { const [hours, minutes] = value.split(':').map((part) => Number(part)) return hours * 60 + minutes } function hasOverlaps( availability: Array<{ dayOfWeek: DayOfWeek startTime: string endTime: string }>, ): boolean { const grouped = new Map< DayOfWeek, Array<{ start: number end: number }> >() for (const range of availability) { const start = toMinutes(range.startTime) const end = toMinutes(range.endTime) const current = grouped.get(range.dayOfWeek) ?? [] current.push({ start, end }) grouped.set(range.dayOfWeek, current) } for (const ranges of grouped.values()) { ranges.sort((a, b) => a.start - b.start) for (let index = 1; index < ranges.length; index += 1) { const previous = ranges[index - 1] const current = ranges[index] if (previous.end > current.start) { return true } } } return false } function refineAvailability( availability: Array<{ dayOfWeek: DayOfWeek startTime: string endTime: string }>, context: z.RefinementCtx, ) { for (const range of availability) { if (toMinutes(range.startTime) >= toMinutes(range.endTime)) { context.addIssue({ code: z.ZodIssueCode.custom, message: `El rango ${range.startTime}-${range.endTime} es invalido. La hora de inicio debe ser menor a la de fin.`, }) } } if (hasOverlaps(availability)) { context.addIssue({ code: z.ZodIssueCode.custom, message: 'Hay rangos horarios superpuestos para el mismo dia.', }) } } export const dayOfWeekSchema = z.enum([ 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY', ]) export type DayOfWeek = z.infer export const courtAvailabilitySchema = z.object({ id: z.uuid(), dayOfWeek: dayOfWeekSchema, startTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'), endTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'), }) export const courtAvailabilityInputSchema = z.object({ dayOfWeek: dayOfWeekSchema, startTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'), endTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'), }) export const courtPriceRuleSchema = z.object({ id: z.uuid(), dayOfWeek: dayOfWeekSchema.nullable(), startTime: z.string().nullable(), endTime: z.string().nullable(), price: z.number().nonnegative(), isActive: z.boolean(), }) export const courtSportSchema = z.object({ id: z.uuid(), name: z.string(), slug: z.string(), }) export const courtSchema = z.object({ id: z.uuid(), complexId: z.uuid(), name: z.string(), sportId: z.uuid(), sport: courtSportSchema, slotDurationMinutes: z.int().positive(), basePrice: z.number().nonnegative(), availability: z.array(courtAvailabilitySchema), priceRules: z.array(courtPriceRuleSchema), hasCustomPricing: z.boolean(), createdAt: z.string().datetime(), updatedAt: z.string().datetime(), }) export const createCourtSchema = z .object({ name: z .string() .trim() .min(2, 'La cancha debe tener al menos 2 caracteres.') .max(120, 'La cancha no puede superar los 120 caracteres.'), sportId: z.uuid('El sportId debe ser un UUID valido.'), slotDurationMinutes: z .int() .min(15, 'La duracion minima por turno es 15 minutos.') .max(480, 'La duracion maxima por turno es 480 minutos.'), basePrice: z.number().nonnegative('El precio base no puede ser negativo.'), availability: z.array(courtAvailabilityInputSchema).min(1, 'Debes definir al menos un rango horario.'), }) .superRefine((payload, context) => refineAvailability(payload.availability, context)) export const updateCourtSchema = z .object({ name: z .string() .trim() .min(2, 'La cancha debe tener al menos 2 caracteres.') .max(120, 'La cancha no puede superar los 120 caracteres.') .optional(), sportId: z.uuid('El sportId debe ser un UUID valido.').optional(), slotDurationMinutes: z .int() .min(15, 'La duracion minima por turno es 15 minutos.') .max(480, 'La duracion maxima por turno es 480 minutos.') .optional(), basePrice: z.number().nonnegative('El precio base no puede ser negativo.').optional(), availability: z.array(courtAvailabilityInputSchema).min(1, 'Debes definir al menos un rango horario.').optional(), }) .superRefine((payload, context) => { if ( payload.name === undefined && payload.sportId === undefined && payload.slotDurationMinutes === undefined && payload.basePrice === undefined && payload.availability === undefined ) { context.addIssue({ code: z.ZodIssueCode.custom, message: 'Debes enviar al menos un campo para actualizar.', }) } if (payload.availability) { refineAvailability(payload.availability, context) } }) export type Court = z.infer export type CourtAvailability = z.infer export type CourtAvailabilityInput = z.infer export type CourtPriceRule = z.infer export type CreateCourtInput = z.infer export type UpdateCourtInput = z.infer