Files
hotelsync/backend/src/routes/rooms.ts
HotelSync c233590c4b Major UX improvements across multiple pages
Шахматка:
- Date/period picker dropdown on navigation button (choose start date + days window)
- Cancelled bookings fade out with animation after 1 second

Бронирования:
- Clickable column headers with sort asc/desc (Гость, Заезд, Выезд, Статус, Источник, Сумма)

Страница входа:
- Removed role-based account selector — just email + password
- System auto-detects role/hotel from credentials

Настройки:
- New "Бронирование" section with room assignment strategy (spread/together/sequential/manual)
- Notifications: SMTP email config + SMS provider config (SMSC, SMS.ru, МТС, etc.)

Модули:
- Added Housekeeping and Channel Manager as proper modules
- Channel Manager lists: Booking.com, Airbnb, Яндекс Путешествия, Островок, Суточно.ру, OneTwoTrip
- Housekeeping visible to all roles (including housekeeper) via module status
- Sidebar now uses module status to show/hide Уборка and Каналы

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 15:20:05 +03:00

164 lines
6.5 KiB
TypeScript

import { FastifyPluginAsync } from 'fastify'
import { db } from '../db'
type SlugParam = { Params: { slug: string } }
type SlugIdParam = { Params: { slug: string; id: string } }
const rooms: 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/rooms ────────────────────────────────────────────
fastify.get<SlugParam>(
'/api/hotels/:slug/rooms',
{ 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 { rows } = await db.query(
`SELECT r.*,
(SELECT json_build_object(
'guest_name', b.guest_name,
'check_in', b.check_in,
'check_out', b.check_out,
'status', b.status
)
FROM bookings b
WHERE b.room_id = r.id
AND b.status = 'checked_in'
LIMIT 1
) AS current_booking
FROM rooms r
WHERE r.hotel_id = $1
ORDER BY r.floor, r.number`,
[hotelId],
)
return rows
},
)
// ── POST /api/hotels/:slug/rooms ───────────────────────────────────────────
fastify.post<SlugParam & { Body: {
number: string; type: string; floor?: number; capacity?: number
price_per_night: number; amenities?: string[]; notes?: string
} }>(
'/api/hotels/:slug/rooms',
{ onRequest: [fastify.authenticate] },
async (request, reply) => {
if (!['manager', 'super_admin'].includes(request.user.role)) {
return reply.code(403).send({ error: 'Forbidden' })
}
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 { number, type, floor = 1, capacity = 2, price_per_night, amenities = [], notes } = request.body
const { rows } = await db.query(
`INSERT INTO rooms (hotel_id, number, type, floor, capacity, price_per_night, amenities, notes)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *`,
[hotelId, number, type, floor, capacity, price_per_night, amenities, notes ?? null],
)
return reply.code(201).send(rows[0])
},
)
// ── GET /api/hotels/:slug/rooms/:id ───────────────────────────────────────
fastify.get<SlugIdParam>(
'/api/hotels/:slug/rooms/:id',
{ onRequest: [fastify.authenticate] },
async (request, reply) => {
const { slug, id } = 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 { rows } = await db.query(
'SELECT * FROM rooms WHERE id = $1 AND hotel_id = $2',
[id, hotelId],
)
if (!rows[0]) return reply.code(404).send({ error: 'Room not found' })
return rows[0]
},
)
// ── PATCH /api/hotels/:slug/rooms/:id ─────────────────────────────────────
fastify.patch<SlugIdParam & { Body: Record<string, unknown> }>(
'/api/hotels/:slug/rooms/:id',
{ onRequest: [fastify.authenticate] },
async (request, reply) => {
if (!['manager', 'super_admin'].includes(request.user.role)) {
return reply.code(403).send({ error: 'Forbidden' })
}
const { slug, id } = 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 allowed = ['number', 'type', 'floor', 'capacity', 'price_per_night', 'status', 'amenities', 'notes']
const updates: string[] = []
const values: unknown[] = []
let idx = 1
for (const key of allowed) {
if (request.body[key] !== undefined) {
updates.push(`${key} = $${idx}`)
values.push(request.body[key])
idx++
}
}
if (updates.length === 0) return reply.code(400).send({ error: 'Nothing to update' })
values.push(id, hotelId)
const { rows } = await db.query(
`UPDATE rooms SET ${updates.join(', ')} WHERE id = $${idx} AND hotel_id = $${idx + 1} RETURNING *`,
values,
)
if (!rows[0]) return reply.code(404).send({ error: 'Room not found' })
return rows[0]
},
)
// ── DELETE /api/hotels/:slug/rooms/:id ────────────────────────────────────
fastify.delete<SlugIdParam>(
'/api/hotels/:slug/rooms/:id',
{ onRequest: [fastify.authenticate] },
async (request, reply) => {
if (!['manager', 'super_admin'].includes(request.user.role)) {
return reply.code(403).send({ error: 'Forbidden' })
}
const { slug, id } = 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 { rowCount } = await db.query(
'DELETE FROM rooms WHERE id = $1 AND hotel_id = $2',
[id, hotelId],
)
if (!rowCount) return reply.code(404).send({ error: 'Room not found' })
return reply.code(204).send()
},
)
}
export default rooms