feat: persist assignment_strategy + apply to widget category bookings
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user