feat: add recurring bookings feature with related handlers and services
- Introduced recurring booking groups with the ability to create and cancel them. - Added database migrations for recurring bookings, including new tables and relationships. - Implemented handlers for creating and canceling recurring bookings in the admin booking module. - Enhanced existing booking services to support recurring bookings logic. - Updated API contract to include new schemas for recurring bookings. - Refactored existing code for improved readability and maintainability.
This commit is contained in:
@@ -1,65 +1,65 @@
|
||||
import { z } from 'zod'
|
||||
import { z } from 'zod';
|
||||
|
||||
const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/
|
||||
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
|
||||
const [hours, minutes] = value.split(':').map((part) => Number(part));
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
function hasOverlaps(
|
||||
availability: Array<{
|
||||
dayOfWeek: DayOfWeek
|
||||
startTime: string
|
||||
endTime: string
|
||||
}>,
|
||||
dayOfWeek: DayOfWeek;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}>
|
||||
): boolean {
|
||||
const grouped = new Map<
|
||||
DayOfWeek,
|
||||
Array<{
|
||||
start: number
|
||||
end: number
|
||||
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)
|
||||
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)
|
||||
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]
|
||||
const previous = ranges[index - 1];
|
||||
const current = ranges[index];
|
||||
|
||||
if (previous.end > current.start) {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
function refineAvailability(
|
||||
availability: Array<{
|
||||
dayOfWeek: DayOfWeek
|
||||
startTime: string
|
||||
endTime: string
|
||||
dayOfWeek: DayOfWeek;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}>,
|
||||
context: z.RefinementCtx,
|
||||
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.`,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ function refineAvailability(
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Hay rangos horarios superpuestos para el mismo dia.',
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,22 +79,22 @@ export const dayOfWeekSchema = z.enum([
|
||||
'FRIDAY',
|
||||
'SATURDAY',
|
||||
'SUNDAY',
|
||||
])
|
||||
]);
|
||||
|
||||
export type DayOfWeek = z.infer<typeof dayOfWeekSchema>
|
||||
export type DayOfWeek = z.infer<typeof dayOfWeekSchema>;
|
||||
|
||||
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(),
|
||||
@@ -103,13 +103,13 @@ export const courtPriceRuleSchema = z.object({
|
||||
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(),
|
||||
@@ -124,7 +124,7 @@ export const courtSchema = z.object({
|
||||
hasCustomPricing: z.boolean(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
})
|
||||
});
|
||||
|
||||
export const createCourtSchema = z
|
||||
.object({
|
||||
@@ -139,9 +139,11 @@ export const createCourtSchema = z
|
||||
.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.'),
|
||||
availability: z
|
||||
.array(courtAvailabilityInputSchema)
|
||||
.min(1, 'Debes definir al menos un rango horario.'),
|
||||
})
|
||||
.superRefine((payload, context) => refineAvailability(payload.availability, context))
|
||||
.superRefine((payload, context) => refineAvailability(payload.availability, context));
|
||||
|
||||
export const updateCourtSchema = z
|
||||
.object({
|
||||
@@ -158,7 +160,10 @@ export const updateCourtSchema = z
|
||||
.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(),
|
||||
availability: z
|
||||
.array(courtAvailabilityInputSchema)
|
||||
.min(1, 'Debes definir al menos un rango horario.')
|
||||
.optional(),
|
||||
})
|
||||
.superRefine((payload, context) => {
|
||||
if (
|
||||
@@ -171,17 +176,17 @@ export const updateCourtSchema = z
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Debes enviar al menos un campo para actualizar.',
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (payload.availability) {
|
||||
refineAvailability(payload.availability, context)
|
||||
refineAvailability(payload.availability, context);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
export type Court = z.infer<typeof courtSchema>
|
||||
export type CourtAvailability = z.infer<typeof courtAvailabilitySchema>
|
||||
export type CourtAvailabilityInput = z.infer<typeof courtAvailabilityInputSchema>
|
||||
export type CourtPriceRule = z.infer<typeof courtPriceRuleSchema>
|
||||
export type CreateCourtInput = z.infer<typeof createCourtSchema>
|
||||
export type UpdateCourtInput = z.infer<typeof updateCourtSchema>
|
||||
export type Court = z.infer<typeof courtSchema>;
|
||||
export type CourtAvailability = z.infer<typeof courtAvailabilitySchema>;
|
||||
export type CourtAvailabilityInput = z.infer<typeof courtAvailabilityInputSchema>;
|
||||
export type CourtPriceRule = z.infer<typeof courtPriceRuleSchema>;
|
||||
export type CreateCourtInput = z.infer<typeof createCourtSchema>;
|
||||
export type UpdateCourtInput = z.infer<typeof updateCourtSchema>;
|
||||
|
||||
Reference in New Issue
Block a user