diff --git a/backend/src/routes/publicWidget.ts b/backend/src/routes/publicWidget.ts index 3125068..77f8717 100644 --- a/backend/src/routes/publicWidget.ts +++ b/backend/src/routes/publicWidget.ts @@ -144,10 +144,18 @@ const publicWidget: FastifyPluginAsync = async (fastify) => { return reply.code(400).send({ error: 'roomId or categoryId required' }) } - // If categoryId given, find first available room in category + // If categoryId given, find a room in category using hotel's assignment strategy if (categoryId && !roomId) { - const { rows: available } = await db.query( - `SELECT id FROM rooms + // Read assignment strategy + const { rows: stratRows } = await db.query( + `SELECT value FROM hotel_settings WHERE hotel_id = $1 AND key = 'assignment_strategy'`, + [hotel.id], + ) + const strategy: string = stratRows[0]?.value?.replace(/"/g, '') ?? 'sequential' + + // Get all available rooms in category + const { rows: candRows } = await db.query( + `SELECT id, sort_order, number FROM rooms WHERE hotel_id = $1 AND category_id = $2 AND status = 'available' AND id NOT IN ( SELECT room_id FROM bookings @@ -155,11 +163,50 @@ const publicWidget: FastifyPluginAsync = async (fastify) => { AND status NOT IN ('cancelled','no_show','checked_out') AND check_in < $4 AND check_out > $3 ) - ORDER BY sort_order, number LIMIT 1`, + ORDER BY sort_order, number`, [hotel.id, categoryId, checkIn, checkOut], ) - if (!available[0]) return reply.code(409).send({ error: 'No rooms available in this category for selected dates' }) - roomId = available[0].id + if (!candRows.length) return reply.code(409).send({ error: 'No rooms available in this category for selected dates' }) + + let chosenId: string = candRows[0].id + + if (strategy === 'spread' || strategy === 'together') { + // Get sort_orders of currently occupied rooms (same dates, any category) + const { rows: occupiedRows } = await db.query( + `SELECT r.sort_order FROM rooms r + JOIN bookings b ON b.room_id = r.id + WHERE b.hotel_id = $1 + AND b.status NOT IN ('cancelled','no_show','checked_out') + AND b.check_in < $3 AND b.check_out > $2`, + [hotel.id, checkIn, checkOut], + ) + const occupiedOrders = occupiedRows.map((r: any) => Number(r.sort_order)) + + if (occupiedOrders.length === 0) { + // No occupied rooms — for spread pick the middle one, for together pick first + chosenId = strategy === 'spread' + ? candRows[Math.floor(candRows.length / 2)].id + : candRows[0].id + } else { + // Score each candidate by min distance to occupied rooms + const scored = candRows.map((r: any) => { + const order = Number(r.sort_order) + const minDist = Math.min(...occupiedOrders.map((o: number) => Math.abs(o - order))) + return { id: r.id, minDist } + }) + if (strategy === 'spread') { + // Pick room furthest from any occupied room + scored.sort((a, b) => b.minDist - a.minDist) + } else { + // together: pick room closest to occupied rooms + scored.sort((a, b) => a.minDist - b.minDist) + } + chosenId = scored[0].id + } + } + // sequential / manual / fallback: already set to candRows[0].id + + roomId = chosenId } // Check availability for specific room diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 918ba36..cc12357 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -109,6 +109,7 @@ export function SettingsPage() { if (s.auto_cancel_noshow_hours) setAutoCancelNoShowHours(Number(s.auto_cancel_noshow_hours)) setAutoCheckout(Boolean(s.auto_checkout_enabled)) if (s.auto_checkout_hours) setAutoCheckoutHours(Number(s.auto_checkout_hours)) + if (s.assignment_strategy) setAssignmentStrategy(s.assignment_strategy as any) setSmtp({ host: String(s.smtp_host ?? ''), port: String(s.smtp_port ?? '587'), @@ -269,6 +270,13 @@ export function SettingsPage() { console.error('Failed to save hotel settings', err) } } + if (section === 'booking') { + try { + await api.hotelSettings.update(slug, { assignment_strategy: assignmentStrategy }) + } catch (err) { + console.error('Failed to save booking settings', err) + } + } if (section === 'notify') { try { await api.hotelSettings.update(slug, {