feat/improve-public-booking #6
@@ -202,6 +202,10 @@ async function getComplexWithBookingData(complexSlug: string) {
|
|||||||
availabilities: {
|
availabilities: {
|
||||||
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
|
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
|
||||||
},
|
},
|
||||||
|
priceRules: {
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: 'asc' },
|
orderBy: { createdAt: 'asc' },
|
||||||
},
|
},
|
||||||
@@ -226,6 +230,37 @@ async function getComplexWithBookingData(complexSlug: string) {
|
|||||||
return complex;
|
return complex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveSlotPrice(
|
||||||
|
court: ComplexWithPublicBookingData['courts'][number],
|
||||||
|
dayOfWeek: DayOfWeek,
|
||||||
|
slot: Slot
|
||||||
|
): number {
|
||||||
|
const slotStart = toMinutes(slot.startTime);
|
||||||
|
const slotEnd = toMinutes(slot.endTime);
|
||||||
|
const matchingRules = court.priceRules
|
||||||
|
.filter((rule) => {
|
||||||
|
if (rule.dayOfWeek && rule.dayOfWeek !== dayOfWeek) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rule.startTime || !rule.endTime) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return slotStart >= toMinutes(rule.startTime) && slotEnd <= toMinutes(rule.endTime);
|
||||||
|
})
|
||||||
|
.sort((first, second) => {
|
||||||
|
const firstSpecificity =
|
||||||
|
(first.dayOfWeek ? 1 : 0) + (first.startTime && first.endTime ? 1 : 0);
|
||||||
|
const secondSpecificity =
|
||||||
|
(second.dayOfWeek ? 1 : 0) + (second.startTime && second.endTime ? 1 : 0);
|
||||||
|
|
||||||
|
return secondSpecificity - firstSpecificity;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Number(matchingRules[0]?.price ?? court.basePrice);
|
||||||
|
}
|
||||||
|
|
||||||
function mapBookingResponse(input: {
|
function mapBookingResponse(input: {
|
||||||
bookingId: string;
|
bookingId: string;
|
||||||
bookingCode: string;
|
bookingCode: string;
|
||||||
@@ -432,7 +467,10 @@ export async function listPublicAvailability(complexSlug: string, query: PublicA
|
|||||||
},
|
},
|
||||||
slotDurationMinutes: court.slotDurationMinutes,
|
slotDurationMinutes: court.slotDurationMinutes,
|
||||||
availabilityDay: dayOfWeek,
|
availabilityDay: dayOfWeek,
|
||||||
availableSlots,
|
availableSlots: availableSlots.map((slot) => ({
|
||||||
|
...slot,
|
||||||
|
price: resolveSlotPrice(court, dayOfWeek, slot),
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((court) => court.availableSlots.length > 0);
|
.filter((court) => court.availableSlots.length > 0);
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ type SelectedSlot = {
|
|||||||
sportName: string;
|
sportName: string;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
|
price: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BookingShellProps = {
|
type BookingShellProps = {
|
||||||
@@ -179,6 +180,18 @@ function extractMessage(error: unknown, fallback: string) {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBookingPrice(price: number): string {
|
||||||
|
if (price === 0) {
|
||||||
|
return 'Sin cargo';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.NumberFormat('es-AR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'ARS',
|
||||||
|
maximumFractionDigits: Number.isInteger(price) ? 0 : 2,
|
||||||
|
}).format(price);
|
||||||
|
}
|
||||||
|
|
||||||
function getStep(selectedSlot: SelectedSlot | null, isValid: boolean) {
|
function getStep(selectedSlot: SelectedSlot | null, isValid: boolean) {
|
||||||
if (!selectedSlot) return 1;
|
if (!selectedSlot) return 1;
|
||||||
if (!isValid) return 3;
|
if (!isValid) return 3;
|
||||||
@@ -966,6 +979,7 @@ function MobileAvailableSlots({
|
|||||||
sportName: court.sport.name,
|
sportName: court.sport.name,
|
||||||
startTime: slot.startTime,
|
startTime: slot.startTime,
|
||||||
endTime: slot.endTime,
|
endTime: slot.endTime,
|
||||||
|
price: slot.price,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
aria-pressed={selected}
|
aria-pressed={selected}
|
||||||
@@ -1093,6 +1107,7 @@ function BookingTimeline({
|
|||||||
sportName: court.sport.name,
|
sportName: court.sport.name,
|
||||||
startTime: slot.startTime,
|
startTime: slot.startTime,
|
||||||
endTime: slot.endTime,
|
endTime: slot.endTime,
|
||||||
|
price: slot.price,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className={`absolute top-7 flex h-12 min-w-[50px] items-center justify-center rounded border text-xs transition ${
|
className={`absolute top-7 flex h-12 min-w-[50px] items-center justify-center rounded border text-xs transition ${
|
||||||
@@ -1237,6 +1252,13 @@ function SelectedSlotCard(props: BookingShellProps & { compact?: boolean }) {
|
|||||||
className={compact ? 'mt-4 space-y-3' : 'mt-5 grid gap-3'}
|
className={compact ? 'mt-4 space-y-3' : 'mt-5 grid gap-3'}
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
>
|
>
|
||||||
|
<div className="flex items-center justify-between gap-3 rounded-md border border-emerald-400/20 bg-emerald-400/[0.06] px-3 py-2.5">
|
||||||
|
<span className="text-sm text-white/72">Precio del turno</span>
|
||||||
|
<span className="shrink-0 text-base font-bold text-white">
|
||||||
|
{formatBookingPrice(selectedSlot.price)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={compact ? 'space-y-3' : 'grid grid-cols-2 gap-3'}>
|
<div className={compact ? 'space-y-3' : 'grid grid-cols-2 gap-3'}>
|
||||||
<Field data-invalid={Boolean(errors.customerName)}>
|
<Field data-invalid={Boolean(errors.customerName)}>
|
||||||
<FieldLabel htmlFor="customerName" className="text-white/76">
|
<FieldLabel htmlFor="customerName" className="text-white/76">
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const publicBookingSportSchema = z.object({
|
|||||||
export const publicBookingSlotSchema = z.object({
|
export const publicBookingSlotSchema = z.object({
|
||||||
startTime: z.string().regex(TIME_REGEX),
|
startTime: z.string().regex(TIME_REGEX),
|
||||||
endTime: z.string().regex(TIME_REGEX),
|
endTime: z.string().regex(TIME_REGEX),
|
||||||
|
price: z.number().nonnegative(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const publicAvailabilityCourtSchema = z.object({
|
export const publicAvailabilityCourtSchema = z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user