diff --git a/backend/migrations/016_checkinout_fees.sql b/backend/migrations/016_checkinout_fees.sql new file mode 100644 index 0000000..27c72ce --- /dev/null +++ b/backend/migrations/016_checkinout_fees.sql @@ -0,0 +1,4 @@ +-- Add early check-in and late checkout fee fields to rooms +ALTER TABLE rooms + ADD COLUMN IF NOT EXISTS early_checkin_fee DECIMAL(10,2) DEFAULT NULL, + ADD COLUMN IF NOT EXISTS late_checkout_fee DECIMAL(10,2) DEFAULT NULL; diff --git a/backend/src/routes/hotel-settings.ts b/backend/src/routes/hotel-settings.ts index a5b6374..015601f 100644 --- a/backend/src/routes/hotel-settings.ts +++ b/backend/src/routes/hotel-settings.ts @@ -24,14 +24,18 @@ const hotelSettings: FastifyPluginAsync = async (fastify) => { const hotelId = await getHotelId(slug) if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) - const { rows } = await db.query( - 'SELECT key, value FROM hotel_settings WHERE hotel_id = $1', - [hotelId], - ) + const [{ rows: settingsRows }, { rows: hotelRows }] = await Promise.all([ + db.query('SELECT key, value FROM hotel_settings WHERE hotel_id = $1', [hotelId]), + db.query('SELECT check_in_time, check_out_time FROM hotels WHERE id = $1', [hotelId]), + ]) const out: Record = {} - for (const row of rows) { + for (const row of settingsRows) { out[row.key] = row.value } + if (hotelRows[0]) { + out.check_in_time = hotelRows[0].check_in_time + out.check_out_time = hotelRows[0].check_out_time + } return out }, ) diff --git a/backend/src/routes/rooms.ts b/backend/src/routes/rooms.ts index 6cbbab3..e72b42e 100644 --- a/backend/src/routes/rooms.ts +++ b/backend/src/routes/rooms.ts @@ -55,6 +55,7 @@ const rooms: FastifyPluginAsync = async (fastify) => { beds?: unknown; housekeeping_status?: string; sort_order?: number allow_hourly?: boolean; hourly_rate?: number; extra_place?: unknown child_policy?: unknown; description?: string; photos?: string[] + early_checkin_fee?: number; late_checkout_fee?: number } }>( '/api/hotels/:slug/rooms', { onRequest: [fastify.authenticate] }, @@ -74,15 +75,16 @@ const rooms: FastifyPluginAsync = async (fastify) => { amenities = [], name, category_id, bed_type = 'double', beds, housekeeping_status = 'clean', sort_order = 99, allow_hourly = false, hourly_rate, extra_place, child_policy, - description, photos = [], + description, photos = [], early_checkin_fee, late_checkout_fee, } = request.body const { rows } = await db.query( `INSERT INTO rooms (hotel_id, number, type, floor, max_guests, base_rate, amenities, name, category_id, bed_type, beds, housekeeping_status, sort_order, - allow_hourly, hourly_rate, extra_place, child_policy, description, photos) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) + allow_hourly, hourly_rate, extra_place, child_policy, description, photos, + early_checkin_fee, late_checkout_fee) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21) RETURNING *`, [ hotelId, number, type, floor, max_guests, base_rate, @@ -92,6 +94,7 @@ const rooms: FastifyPluginAsync = async (fastify) => { extra_place ? JSON.stringify(extra_place) : null, child_policy ? JSON.stringify(child_policy) : null, description ?? null, photos, + early_checkin_fee ?? null, late_checkout_fee ?? null, ], ) return reply.code(201).send(rows[0]) @@ -139,6 +142,7 @@ const rooms: FastifyPluginAsync = async (fastify) => { 'amenities', 'name', 'category_id', 'bed_type', 'beds', 'housekeeping_status', 'sort_order', 'allow_hourly', 'hourly_rate', 'extra_place', 'child_policy', 'description', 'photos', + 'early_checkin_fee', 'late_checkout_fee', ] const updates: string[] = [] const values: unknown[] = [] diff --git a/src/components/bookings/BookingDetailPanel.tsx b/src/components/bookings/BookingDetailPanel.tsx index bc323ab..94290b4 100644 --- a/src/components/bookings/BookingDetailPanel.tsx +++ b/src/components/bookings/BookingDetailPanel.tsx @@ -85,6 +85,27 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on const acTimerRef = useRef | null>(null) const acWrapRef = useRef(null) + // Hotel timing settings (for early/late fee warnings) + const [hotelCheckInTime, setHotelCheckInTime] = useState('14:00') + const [hotelCheckOutTime, setHotelCheckOutTime] = useState('12:00') + const [earlyCheckinEnabled, setEarlyCheckinEnabled] = useState(false) + const [lateCheckoutEnabled, setLateCheckoutEnabled] = useState(false) + const [hotelTimingsLoaded, setHotelTimingsLoaded] = useState(false) + + useEffect(() => { + if (!slug || hotelTimingsLoaded) return + api.hotelSettings.get(slug) + .then(s => { + const st = s as Record + if (st.check_in_time) setHotelCheckInTime(String(st.check_in_time).slice(0, 5)) + if (st.check_out_time) setHotelCheckOutTime(String(st.check_out_time).slice(0, 5)) + setEarlyCheckinEnabled(Boolean(st.early_checkin_enabled)) + setLateCheckoutEnabled(Boolean(st.late_checkout_enabled)) + setHotelTimingsLoaded(true) + }) + .catch(console.error) + }, [slug, hotelTimingsLoaded]) + // Load booking guests + settings when tab is activated useEffect(() => { if (tab !== 'guest' || !slug || bgLoaded) return @@ -1272,19 +1293,73 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on )} - {booking.status === 'confirmed' && booking.checkIn <= today && ( - - )} - {booking.status === 'checked_in' && !earlyOutOpen && ( - - )} + {booking.status === 'confirmed' && booking.checkIn <= today && (() => { + const nowTime = format(new Date(), 'HH:mm') + const isEarlyArrival = earlyCheckinEnabled && nowTime < hotelCheckInTime && (room?.earlyCheckinFee ?? 0) > 0 + return ( + <> + {isEarlyArrival && ( +
+ +

+ Ранний заезд (до {hotelCheckInTime}) — доп. оплата {formatCurrency(room!.earlyCheckinFee!)} +

+
+ )} + + + ) + })()} + {booking.status === 'checked_in' && !earlyOutOpen && (() => { + const nowTime = format(new Date(), 'HH:mm') + const isLateCheckout = lateCheckoutEnabled && nowTime > hotelCheckOutTime && (room?.lateCheckoutFee ?? 0) > 0 + return ( + <> + {isLateCheckout && ( +
+ +

+ Поздний выезд (после {hotelCheckOutTime}) — доп. оплата {formatCurrency(room!.lateCheckoutFee!)} +

+
+ )} + + + ) + })()} {booking.status === 'checked_in' && earlyOutOpen && (() => { const origNights = differenceInDays(new Date(booking.checkOut), new Date(booking.checkIn)) const actualNights = Math.max(1, differenceInDays(new Date(earlyOutDate), new Date(booking.checkIn))) diff --git a/src/components/rooms/RoomModal.tsx b/src/components/rooms/RoomModal.tsx index 4569bc2..4e58875 100644 --- a/src/components/rooms/RoomModal.tsx +++ b/src/components/rooms/RoomModal.tsx @@ -72,6 +72,8 @@ export function RoomModal({ open, room, categories = [], onClose, onSave, onDele hourlyRate: room?.hourlyRate ?? 1000, categoryId: room?.categoryId ?? '', description: room?.description ?? '', + earlyCheckinFee: room?.earlyCheckinFee ?? '', + lateCheckoutFee: room?.lateCheckoutFee ?? '', }) const { amenities: allAmenities } = useAmenities() @@ -148,6 +150,8 @@ export function RoomModal({ open, room, categories = [], onClose, onSave, onDele beds: beds.length > 0 ? beds : undefined, extraPlace: extraPlace.enabled ? extraPlace : undefined, childPolicy: childPolicy.enabled ? childPolicy : undefined, + earlyCheckinFee: form.earlyCheckinFee !== '' ? Number(form.earlyCheckinFee) : undefined, + lateCheckoutFee: form.lateCheckoutFee !== '' ? Number(form.lateCheckoutFee) : undefined, }) } @@ -303,6 +307,34 @@ export function RoomModal({ open, room, categories = [], onClose, onSave, onDele )} + {/* Early check-in / late checkout fees */} +
+
+

Тарифы за ранний/поздний заезд

+

Применяются если включено в настройках отеля

+
+
+
+ + set('earlyCheckinFee', e.target.value)} + /> +
+
+ + set('lateCheckoutFee', e.target.value)} + /> +
+
+
+ {/* Amenities */}
diff --git a/src/lib/api.ts b/src/lib/api.ts index 4ad2b1a..77a2cdd 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -381,6 +381,7 @@ export interface RoomPayload { sortOrder?: number; allowHourly?: boolean; hourlyRate?: number extraPlace?: unknown; childPolicy?: unknown description?: string; photos?: string[] + earlyCheckinFee?: number | null; lateCheckoutFee?: number | null } function toRoomPayload(r: Partial): Record { @@ -402,8 +403,10 @@ function toRoomPayload(r: Partial): Record { if (r.hourlyRate !== undefined) out.hourly_rate = r.hourlyRate if (r.extraPlace !== undefined) out.extra_place = r.extraPlace if (r.childPolicy !== undefined) out.child_policy = r.childPolicy - if (r.description !== undefined) out.description = r.description - if (r.photos !== undefined) out.photos = r.photos + if (r.description !== undefined) out.description = r.description + if (r.photos !== undefined) out.photos = r.photos + if (r.earlyCheckinFee !== undefined) out.early_checkin_fee = r.earlyCheckinFee + if (r.lateCheckoutFee !== undefined) out.late_checkout_fee = r.lateCheckoutFee return out } diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 913bfb2..57ac9d0 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -75,14 +75,18 @@ export function SettingsPage() { // Booking / assignment settings const [assignmentStrategy, setAssignmentStrategy] = useState<'spread' | 'together' | 'sequential' | 'manual'>('spread') const [showBookingSource, setShowBookingSource] = useState(false) - const [requireGuestDocs, setRequireGuestDocs] = useState(false) - const [hotelSettingsLoaded, setHotelSettingsLoaded] = useState(false) + const [requireGuestDocs, setRequireGuestDocs] = useState(false) + const [earlyCheckinEnabled, setEarlyCheckinEnabled] = useState(false) + const [lateCheckoutEnabled, setLateCheckoutEnabled] = useState(false) + const [hotelSettingsLoaded, setHotelSettingsLoaded] = useState(false) useEffect(() => { if (!slug || hotelSettingsLoaded) return api.hotelSettings.get(slug) .then(s => { setRequireGuestDocs(Boolean(s.require_guest_docs)) + setEarlyCheckinEnabled(Boolean(s.early_checkin_enabled)) + setLateCheckoutEnabled(Boolean(s.late_checkout_enabled)) setHotelSettingsLoaded(true) }) .catch(console.error) @@ -98,6 +102,26 @@ export function SettingsPage() { } } + const toggleEarlyCheckin = async () => { + const next = !earlyCheckinEnabled + setEarlyCheckinEnabled(next) + try { + await api.hotelSettings.update(slug, { early_checkin_enabled: next }) + } catch { + setEarlyCheckinEnabled(!next) + } + } + + const toggleLateCheckout = async () => { + const next = !lateCheckoutEnabled + setLateCheckoutEnabled(next) + try { + await api.hotelSettings.update(slug, { late_checkout_enabled: next }) + } catch { + setLateCheckoutEnabled(!next) + } + } + // Guest settings const [guestTags, setGuestTags] = useState([ { id: 'vip', label: 'VIP', color: '#F59E0B' }, @@ -299,6 +323,28 @@ export function SettingsPage() {
+ {/* Early check-in fee */} +
+
+

Платный ранний заезд

+

+ Предлагать доп. оплату при заселении до официального времени заезда. Сумма задаётся в настройках каждого номера. +

+
+ +
+ + {/* Late checkout fee */} +
+
+

Платный поздний выезд

+

+ Предлагать доп. оплату при выезде после официального времени выезда. Сумма задаётся в настройках каждого номера. +

+
+ +
+