Files
hotelsync/backend/src/routes/housekeeping-settings.ts

178 lines
6.8 KiB
TypeScript

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
require_completion_photo: boolean
emergency_close_days: number
auto_assign: boolean
auto_assign_strategy: string
}
const DEFAULT_SETTINGS: HkSettings = {
checkout_auto: true,
checkout_priority: 'high',
inspection_after_clean: true,
require_completion_photo: false,
emergency_close_days: 1,
auto_assign: false,
auto_assign_strategy: 'least_loaded',
}
export async function getHkSettings(hotelId: string): Promise<HkSettings> {
const { rows } = await db.query(
'SELECT checkout_auto, checkout_priority, inspection_after_clean, require_completion_photo, emergency_close_days, auto_assign, auto_assign_strategy FROM housekeeping_settings WHERE hotel_id = $1',
[hotelId],
)
return rows[0] ?? DEFAULT_SETTINGS
}
// Pick a housekeeper based on strategy
export async function autoAssignHousekeeper(
hotelId: string,
strategy: string,
): Promise<string | null> {
if (strategy === 'round_robin') {
// Housekeeper with fewest tasks assigned today
const { rows } = await db.query(
`SELECT u.id
FROM users u
WHERE u.hotel_id = $1 AND u.role = 'housekeeper' AND u.active = true
ORDER BY (
SELECT COUNT(*) FROM housekeeping_tasks t
WHERE t.assignee_id = u.id
AND t.created_at::date = CURRENT_DATE
) ASC, RANDOM()
LIMIT 1`,
[hotelId],
)
return rows[0]?.id ?? null
}
if (strategy === 'least_loaded') {
// Housekeeper with fewest active (pending/in_progress) tasks right now
const { rows } = await db.query(
`SELECT u.id
FROM users u
WHERE u.hotel_id = $1 AND u.role = 'housekeeper' AND u.active = true
ORDER BY (
SELECT COUNT(*) FROM housekeeping_tasks t
WHERE t.assignee_id = u.id
AND t.status IN ('pending', 'in_progress')
) ASC, RANDOM()
LIMIT 1`,
[hotelId],
)
return rows[0]?.id ?? null
}
if (strategy === 'first_free') {
// Housekeeper who most recently completed a task (or has never had one) and has no active tasks
const { rows } = await db.query(
`SELECT u.id
FROM users u
WHERE u.hotel_id = $1 AND u.role = 'housekeeper' AND u.active = true
AND NOT EXISTS (
SELECT 1 FROM housekeeping_tasks t
WHERE t.assignee_id = u.id AND t.status IN ('pending', 'in_progress')
)
ORDER BY (
SELECT MAX(t.completed_at) FROM housekeeping_tasks t
WHERE t.assignee_id = u.id AND t.status = 'done'
) DESC NULLS LAST
LIMIT 1`,
[hotelId],
)
// If nobody is free, fall back to least_loaded
if (rows[0]) return rows[0].id
const { rows: fallback } = await db.query(
`SELECT u.id
FROM users u
WHERE u.hotel_id = $1 AND u.role = 'housekeeper' AND u.active = true
ORDER BY (
SELECT COUNT(*) FROM housekeeping_tasks t
WHERE t.assignee_id = u.id AND t.status IN ('pending', 'in_progress')
) ASC, RANDOM()
LIMIT 1`,
[hotelId],
)
return fallback[0]?.id ?? null
}
return null
}
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,
require_completion_photo, emergency_close_days,
auto_assign, auto_assign_strategy,
} = request.body
await db.query(
`INSERT INTO housekeeping_settings
(hotel_id, checkout_auto, checkout_priority, inspection_after_clean, require_completion_photo, emergency_close_days, auto_assign, auto_assign_strategy)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
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),
require_completion_photo = COALESCE(EXCLUDED.require_completion_photo, housekeeping_settings.require_completion_photo),
emergency_close_days = COALESCE(EXCLUDED.emergency_close_days, housekeeping_settings.emergency_close_days),
auto_assign = COALESCE(EXCLUDED.auto_assign, housekeeping_settings.auto_assign),
auto_assign_strategy = COALESCE(EXCLUDED.auto_assign_strategy, housekeeping_settings.auto_assign_strategy),
updated_at = NOW()`,
[
hotelId,
checkout_auto ?? DEFAULT_SETTINGS.checkout_auto,
checkout_priority ?? DEFAULT_SETTINGS.checkout_priority,
inspection_after_clean ?? DEFAULT_SETTINGS.inspection_after_clean,
require_completion_photo ?? DEFAULT_SETTINGS.require_completion_photo,
emergency_close_days ?? DEFAULT_SETTINGS.emergency_close_days,
auto_assign ?? DEFAULT_SETTINGS.auto_assign,
auto_assign_strategy ?? DEFAULT_SETTINGS.auto_assign_strategy,
],
)
return getHkSettings(hotelId)
},
)
}
export default housekeepingSettings