feat/improve-public-booking #6

Merged
jselesan merged 3 commits from feat/improve-public-booking into development 2026-06-02 19:56:29 +00:00
3 changed files with 177 additions and 48 deletions
Showing only changes of commit 9045a8c017 - Show all commits

View File

@@ -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);

View File

@@ -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">

View File

@@ -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({