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' })
|
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) {
|
if (categoryId && !roomId) {
|
||||||
const { rows: available } = await db.query(
|
// Read assignment strategy
|
||||||
`SELECT id FROM rooms
|
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'
|
WHERE hotel_id = $1 AND category_id = $2 AND status = 'available'
|
||||||
AND id NOT IN (
|
AND id NOT IN (
|
||||||
SELECT room_id FROM bookings
|
SELECT room_id FROM bookings
|
||||||
@@ -155,11 +163,50 @@ const publicWidget: FastifyPluginAsync = async (fastify) => {
|
|||||||
AND status NOT IN ('cancelled','no_show','checked_out')
|
AND status NOT IN ('cancelled','no_show','checked_out')
|
||||||
AND check_in < $4 AND check_out > $3
|
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],
|
[hotel.id, categoryId, checkIn, checkOut],
|
||||||
)
|
)
|
||||||
if (!available[0]) return reply.code(409).send({ error: 'No rooms available in this category for selected dates' })
|
if (!candRows.length) return reply.code(409).send({ error: 'No rooms available in this category for selected dates' })
|
||||||
roomId = available[0].id
|
|
||||||
|
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
|
// Check availability for specific room
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export function SettingsPage() {
|
|||||||
if (s.auto_cancel_noshow_hours) setAutoCancelNoShowHours(Number(s.auto_cancel_noshow_hours))
|
if (s.auto_cancel_noshow_hours) setAutoCancelNoShowHours(Number(s.auto_cancel_noshow_hours))
|
||||||
setAutoCheckout(Boolean(s.auto_checkout_enabled))
|
setAutoCheckout(Boolean(s.auto_checkout_enabled))
|
||||||
if (s.auto_checkout_hours) setAutoCheckoutHours(Number(s.auto_checkout_hours))
|
if (s.auto_checkout_hours) setAutoCheckoutHours(Number(s.auto_checkout_hours))
|
||||||
|
if (s.assignment_strategy) setAssignmentStrategy(s.assignment_strategy as any)
|
||||||
setSmtp({
|
setSmtp({
|
||||||
host: String(s.smtp_host ?? ''),
|
host: String(s.smtp_host ?? ''),
|
||||||
port: String(s.smtp_port ?? '587'),
|
port: String(s.smtp_port ?? '587'),
|
||||||
@@ -269,6 +270,13 @@ export function SettingsPage() {
|
|||||||
console.error('Failed to save hotel settings', err)
|
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') {
|
if (section === 'notify') {
|
||||||
try {
|
try {
|
||||||
await api.hotelSettings.update(slug, {
|
await api.hotelSettings.update(slug, {
|
||||||
|
|||||||
Reference in New Issue
Block a user