feat: widget rental tab — real objects, time slots, actual rental_bookings
- Add rental_objects to /api/widget/:slug/config response - Add POST /api/widget/:slug/rental-bookings endpoint with availability check - Widget rental tab now shows real rental objects (not additionalServices) - Date picker, hourly or full-day toggle, start/end hour selectors - Booking submit creates actual rental_booking record in DB - Success screen shows rental object, date and time slot - WidgetRentalObject + WidgetRentalBookingPayload types added to api.ts - BookingWidgetStandalonePage passes rentalObjects to WidgetPreview Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,16 @@ const publicWidget: FastifyPluginAsync = async (fastify) => {
|
||||
[hotel.id],
|
||||
)
|
||||
|
||||
// Load rental objects (only if widget_show_rental = true)
|
||||
const { rows: rentalObjects } = await db.query(
|
||||
`SELECT id, name, icon, price_per_hour, price_per_day,
|
||||
open_hour, close_hour, max_hours_per_slot, buffer_minutes, sort_order
|
||||
FROM rental_objects
|
||||
WHERE hotel_id = $1
|
||||
ORDER BY sort_order, name`,
|
||||
[hotel.id],
|
||||
)
|
||||
|
||||
// Check if YooKassa gateway is configured for booking-widget
|
||||
const gateway = await getGatewayForModule(hotel.id, 'booking-widget')
|
||||
|
||||
@@ -92,9 +102,79 @@ const publicWidget: FastifyPluginAsync = async (fastify) => {
|
||||
minPrice: Number(c.min_price),
|
||||
maxGuests: Number(c.max_guests),
|
||||
})),
|
||||
rentalObjects: rentalObjects.map((o: any) => ({
|
||||
id: o.id,
|
||||
name: o.name,
|
||||
icon: o.icon ?? '🏨',
|
||||
pricePerHour: Number(o.price_per_hour ?? 0),
|
||||
pricePerDay: Number(o.price_per_day ?? 0),
|
||||
openHour: Number(o.open_hour ?? 8),
|
||||
closeHour: Number(o.close_hour ?? 22),
|
||||
maxHoursPerSlot: o.max_hours_per_slot ? Number(o.max_hours_per_slot) : null,
|
||||
bufferMinutes: Number(o.buffer_minutes ?? 0),
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
// ── POST /api/widget/:slug/rental-bookings ────────────────────────────────
|
||||
fastify.post<SlugParam & {
|
||||
Body: {
|
||||
objectId: string
|
||||
date: string
|
||||
isFullDay?: boolean
|
||||
startHour?: number
|
||||
endHour?: number
|
||||
guestName: string
|
||||
guestEmail?: string
|
||||
guestPhone?: string
|
||||
totalAmount: number
|
||||
notes?: string
|
||||
}
|
||||
}>('/api/widget/:slug/rental-bookings', async (req, reply) => {
|
||||
const hotel = await getHotelId(req.params.slug)
|
||||
if (!hotel) return reply.code(404).send({ error: 'Hotel not found' })
|
||||
|
||||
const { objectId, date, isFullDay, startHour, endHour, guestName, guestEmail, guestPhone, totalAmount, notes } = req.body
|
||||
|
||||
if (!objectId || !date || !guestName) {
|
||||
return reply.code(400).send({ error: 'Missing required fields' })
|
||||
}
|
||||
|
||||
// Check object exists
|
||||
const { rows: objRows } = await db.query(
|
||||
`SELECT id FROM rental_objects WHERE id = $1 AND hotel_id = $2`,
|
||||
[objectId, hotel.id],
|
||||
)
|
||||
if (!objRows[0]) return reply.code(404).send({ error: 'Rental object not found' })
|
||||
|
||||
// Availability check (hourly bookings)
|
||||
if (!isFullDay && startHour !== undefined && endHour !== undefined) {
|
||||
const { rows: conflicts } = await db.query(
|
||||
`SELECT id FROM rental_bookings
|
||||
WHERE object_id = $1 AND date = $2 AND status != 'cancelled'
|
||||
AND NOT (end_hour <= $3 OR start_hour >= $4)`,
|
||||
[objectId, date, startHour, endHour],
|
||||
)
|
||||
if (conflicts.length > 0) {
|
||||
return reply.code(409).send({ error: 'This time slot is already booked' })
|
||||
}
|
||||
}
|
||||
|
||||
// Create rental booking
|
||||
const { rows } = await db.query(
|
||||
`INSERT INTO rental_bookings
|
||||
(hotel_id, object_id, date, is_full_day, start_hour, end_hour,
|
||||
guest_name, guest_phone, total_amount, notes, status)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'confirmed') RETURNING id`,
|
||||
[hotel.id, objectId, date, isFullDay ?? false,
|
||||
startHour ?? 0, endHour ?? 0,
|
||||
guestName, guestPhone ?? '', Math.round(totalAmount * 100),
|
||||
notes ?? null],
|
||||
)
|
||||
|
||||
return { bookingId: rows[0].id, status: 'confirmed' }
|
||||
})
|
||||
|
||||
// ── GET /api/widget/:slug/guests/lookup ───────────────────────────────────
|
||||
// Lookup existing guest by email or phone (for auto-fill, no auth)
|
||||
fastify.get<SlugParam & { Querystring: { email?: string; phone?: string } }>(
|
||||
|
||||
Reference in New Issue
Block a user