feat: persist assignment_strategy + apply to widget category bookings

This commit is contained in:
2026-04-07 21:45:12 +03:00
parent ab37430243
commit 328753fc1e
2 changed files with 61 additions and 6 deletions

View File

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

View File

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