feat: housekeeping automation — auto-task on checkout + room status on done

Backend:
- Migration 023: housekeeping_settings table (checkout_auto, checkout_priority, inspection_after_clean)
- New route: GET/PATCH /api/hotels/:slug/housekeeping-settings
- bookings.ts: on checked_out → auto-create turnover task + mark room dirty + broadcast WS
- housekeeping.ts: on task done → update room status (inspect|clean per setting) + broadcast WS
- ws.ts: export broadcast() for use in other routes

Frontend:
- HousekeepingPage: load/save settings to API, live Loader on save button
- useHotelSocket: add housekeeping WS message types
- HousekeepingPage: subscribe to WS — new tasks appear instantly without refresh
- BookingModal: show total section when room + nights selected (not just when total > 0)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 22:27:18 +03:00
parent ce20c6545d
commit 9af11df1b0
10 changed files with 205 additions and 12 deletions

View File

@@ -0,0 +1,81 @@
import { FastifyPluginAsync } from 'fastify'
import { db } from '../db'
type SlugParam = { Params: { slug: string } }
export interface HkSettings {
checkout_auto: boolean
checkout_priority: string
inspection_after_clean: boolean
}
const DEFAULT_SETTINGS: HkSettings = {
checkout_auto: true,
checkout_priority: 'high',
inspection_after_clean: true,
}
export async function getHkSettings(hotelId: string): Promise<HkSettings> {
const { rows } = await db.query(
'SELECT checkout_auto, checkout_priority, inspection_after_clean FROM housekeeping_settings WHERE hotel_id = $1',
[hotelId],
)
return rows[0] ?? DEFAULT_SETTINGS
}
const housekeepingSettings: FastifyPluginAsync = async (fastify) => {
const getHotelId = async (slug: string): Promise<string | null> => {
const { rows } = await db.query('SELECT id FROM hotels WHERE slug = $1', [slug])
return rows[0]?.id ?? null
}
const canAccess = (userSlug: string | null, role: string, slug: string) =>
role === 'super_admin' || userSlug === slug
// GET /api/hotels/:slug/housekeeping-settings
fastify.get<SlugParam>(
'/api/hotels/:slug/housekeeping-settings',
{ onRequest: [fastify.authenticate] },
async (request, reply) => {
const { slug } = request.params
if (!canAccess(request.user.hotelSlug, request.user.role, slug))
return reply.code(403).send({ error: 'Forbidden' })
const hotelId = await getHotelId(slug)
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
return getHkSettings(hotelId)
},
)
// PATCH /api/hotels/:slug/housekeeping-settings
fastify.patch<SlugParam & { Body: Partial<HkSettings> }>(
'/api/hotels/:slug/housekeeping-settings',
{ onRequest: [fastify.authenticate] },
async (request, reply) => {
const { slug } = request.params
if (!canAccess(request.user.hotelSlug, request.user.role, slug))
return reply.code(403).send({ error: 'Forbidden' })
const hotelId = await getHotelId(slug)
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
const { checkout_auto, checkout_priority, inspection_after_clean } = request.body
await db.query(
`INSERT INTO housekeeping_settings (hotel_id, checkout_auto, checkout_priority, inspection_after_clean)
VALUES ($1, $2, $3, $4)
ON CONFLICT (hotel_id) DO UPDATE SET
checkout_auto = COALESCE(EXCLUDED.checkout_auto, housekeeping_settings.checkout_auto),
checkout_priority = COALESCE(EXCLUDED.checkout_priority, housekeeping_settings.checkout_priority),
inspection_after_clean = COALESCE(EXCLUDED.inspection_after_clean, housekeeping_settings.inspection_after_clean),
updated_at = NOW()`,
[
hotelId,
checkout_auto ?? DEFAULT_SETTINGS.checkout_auto,
checkout_priority ?? DEFAULT_SETTINGS.checkout_priority,
inspection_after_clean ?? DEFAULT_SETTINGS.inspection_after_clean,
],
)
return getHkSettings(hotelId)
},
)
}
export default housekeepingSettings