feat: early check-in / late checkout fees with room-level configuration
- DB migration 016: add early_checkin_fee, late_checkout_fee to rooms table - Backend rooms: support new fee columns in POST/PATCH - Backend hotel-settings GET: also returns check_in_time, check_out_time - Room type + RoomPayload: add earlyCheckinFee, lateCheckoutFee fields - RoomModal: add fee amount inputs in pricing section - SettingsPage: add toggles for early_checkin_enabled / late_checkout_enabled - BookingDetailPanel: on check-in shows fee warning + adds payment record if current time is before checkInTime and fee is set on the room; same logic for check-out after checkOutTime Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
4
backend/migrations/016_checkinout_fees.sql
Normal file
4
backend/migrations/016_checkinout_fees.sql
Normal file
@@ -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;
|
||||||
@@ -24,14 +24,18 @@ const hotelSettings: FastifyPluginAsync = async (fastify) => {
|
|||||||
const hotelId = await getHotelId(slug)
|
const hotelId = await getHotelId(slug)
|
||||||
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
||||||
|
|
||||||
const { rows } = await db.query(
|
const [{ rows: settingsRows }, { rows: hotelRows }] = await Promise.all([
|
||||||
'SELECT key, value FROM hotel_settings WHERE hotel_id = $1',
|
db.query('SELECT key, value FROM hotel_settings WHERE hotel_id = $1', [hotelId]),
|
||||||
[hotelId],
|
db.query('SELECT check_in_time, check_out_time FROM hotels WHERE id = $1', [hotelId]),
|
||||||
)
|
])
|
||||||
const out: Record<string, unknown> = {}
|
const out: Record<string, unknown> = {}
|
||||||
for (const row of rows) {
|
for (const row of settingsRows) {
|
||||||
out[row.key] = row.value
|
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
|
return out
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const rooms: FastifyPluginAsync = async (fastify) => {
|
|||||||
beds?: unknown; housekeeping_status?: string; sort_order?: number
|
beds?: unknown; housekeeping_status?: string; sort_order?: number
|
||||||
allow_hourly?: boolean; hourly_rate?: number; extra_place?: unknown
|
allow_hourly?: boolean; hourly_rate?: number; extra_place?: unknown
|
||||||
child_policy?: unknown; description?: string; photos?: string[]
|
child_policy?: unknown; description?: string; photos?: string[]
|
||||||
|
early_checkin_fee?: number; late_checkout_fee?: number
|
||||||
} }>(
|
} }>(
|
||||||
'/api/hotels/:slug/rooms',
|
'/api/hotels/:slug/rooms',
|
||||||
{ onRequest: [fastify.authenticate] },
|
{ onRequest: [fastify.authenticate] },
|
||||||
@@ -74,15 +75,16 @@ const rooms: FastifyPluginAsync = async (fastify) => {
|
|||||||
amenities = [], name, category_id, bed_type = 'double',
|
amenities = [], name, category_id, bed_type = 'double',
|
||||||
beds, housekeeping_status = 'clean', sort_order = 99,
|
beds, housekeeping_status = 'clean', sort_order = 99,
|
||||||
allow_hourly = false, hourly_rate, extra_place, child_policy,
|
allow_hourly = false, hourly_rate, extra_place, child_policy,
|
||||||
description, photos = [],
|
description, photos = [], early_checkin_fee, late_checkout_fee,
|
||||||
} = request.body
|
} = request.body
|
||||||
|
|
||||||
const { rows } = await db.query(
|
const { rows } = await db.query(
|
||||||
`INSERT INTO rooms
|
`INSERT INTO rooms
|
||||||
(hotel_id, number, type, floor, max_guests, base_rate, amenities, name,
|
(hotel_id, number, type, floor, max_guests, base_rate, amenities, name,
|
||||||
category_id, bed_type, beds, housekeeping_status, sort_order,
|
category_id, bed_type, beds, housekeeping_status, sort_order,
|
||||||
allow_hourly, hourly_rate, extra_place, child_policy, description, photos)
|
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)
|
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 *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
hotelId, number, type, floor, max_guests, base_rate,
|
hotelId, number, type, floor, max_guests, base_rate,
|
||||||
@@ -92,6 +94,7 @@ const rooms: FastifyPluginAsync = async (fastify) => {
|
|||||||
extra_place ? JSON.stringify(extra_place) : null,
|
extra_place ? JSON.stringify(extra_place) : null,
|
||||||
child_policy ? JSON.stringify(child_policy) : null,
|
child_policy ? JSON.stringify(child_policy) : null,
|
||||||
description ?? null, photos,
|
description ?? null, photos,
|
||||||
|
early_checkin_fee ?? null, late_checkout_fee ?? null,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
return reply.code(201).send(rows[0])
|
return reply.code(201).send(rows[0])
|
||||||
@@ -139,6 +142,7 @@ const rooms: FastifyPluginAsync = async (fastify) => {
|
|||||||
'amenities', 'name', 'category_id', 'bed_type', 'beds',
|
'amenities', 'name', 'category_id', 'bed_type', 'beds',
|
||||||
'housekeeping_status', 'sort_order', 'allow_hourly', 'hourly_rate',
|
'housekeeping_status', 'sort_order', 'allow_hourly', 'hourly_rate',
|
||||||
'extra_place', 'child_policy', 'description', 'photos',
|
'extra_place', 'child_policy', 'description', 'photos',
|
||||||
|
'early_checkin_fee', 'late_checkout_fee',
|
||||||
]
|
]
|
||||||
const updates: string[] = []
|
const updates: string[] = []
|
||||||
const values: unknown[] = []
|
const values: unknown[] = []
|
||||||
|
|||||||
@@ -85,6 +85,27 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
|
|||||||
const acTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const acTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
const acWrapRef = useRef<HTMLDivElement | null>(null)
|
const acWrapRef = useRef<HTMLDivElement | null>(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<string, unknown>
|
||||||
|
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
|
// Load booking guests + settings when tab is activated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tab !== 'guest' || !slug || bgLoaded) return
|
if (tab !== 'guest' || !slug || bgLoaded) return
|
||||||
@@ -1272,19 +1293,73 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{booking.status === 'confirmed' && booking.checkIn <= today && (
|
{booking.status === 'confirmed' && booking.checkIn <= today && (() => {
|
||||||
<button onClick={() => setStatus('checked_in')} className="w-full btn-primary justify-center">
|
const nowTime = format(new Date(), 'HH:mm')
|
||||||
<CheckCircle size={15} /> Заселить
|
const isEarlyArrival = earlyCheckinEnabled && nowTime < hotelCheckInTime && (room?.earlyCheckinFee ?? 0) > 0
|
||||||
</button>
|
return (
|
||||||
|
<>
|
||||||
|
{isEarlyArrival && (
|
||||||
|
<div className="flex items-center gap-2 p-2.5 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700">
|
||||||
|
<AlertTriangle size={14} className="text-blue-500 shrink-0" />
|
||||||
|
<p className="text-xs text-blue-700 dark:text-blue-400">
|
||||||
|
Ранний заезд (до {hotelCheckInTime}) — доп. оплата {formatCurrency(room!.earlyCheckinFee!)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{booking.status === 'checked_in' && !earlyOutOpen && (
|
|
||||||
<button
|
<button
|
||||||
onClick={() => { setEarlyOutOpen(true); setEarlyOutDate(today) }}
|
onClick={() => {
|
||||||
|
if (isEarlyArrival) {
|
||||||
|
setPayments(prev => [...prev, {
|
||||||
|
id: `early-${Date.now()}`,
|
||||||
|
date: new Date().toLocaleDateString('ru-RU'),
|
||||||
|
amount: room!.earlyCheckinFee!,
|
||||||
|
method: 'cash' as const,
|
||||||
|
note: `Ранний заезд (заезд до ${hotelCheckInTime})`,
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
setStatus('checked_in')
|
||||||
|
}}
|
||||||
|
className="w-full btn-primary justify-center"
|
||||||
|
>
|
||||||
|
<CheckCircle size={15} /> {isEarlyArrival ? `Заселить (+ ${formatCurrency(room!.earlyCheckinFee!)})` : 'Заселить'}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
{booking.status === 'checked_in' && !earlyOutOpen && (() => {
|
||||||
|
const nowTime = format(new Date(), 'HH:mm')
|
||||||
|
const isLateCheckout = lateCheckoutEnabled && nowTime > hotelCheckOutTime && (room?.lateCheckoutFee ?? 0) > 0
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLateCheckout && (
|
||||||
|
<div className="flex items-center gap-2 p-2.5 rounded-xl bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-700">
|
||||||
|
<AlertTriangle size={14} className="text-orange-500 shrink-0" />
|
||||||
|
<p className="text-xs text-orange-700 dark:text-orange-400">
|
||||||
|
Поздний выезд (после {hotelCheckOutTime}) — доп. оплата {formatCurrency(room!.lateCheckoutFee!)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (isLateCheckout) {
|
||||||
|
setPayments(prev => [...prev, {
|
||||||
|
id: `late-${Date.now()}`,
|
||||||
|
date: new Date().toLocaleDateString('ru-RU'),
|
||||||
|
amount: room!.lateCheckoutFee!,
|
||||||
|
method: 'cash' as const,
|
||||||
|
note: `Поздний выезд (выезд после ${hotelCheckOutTime})`,
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
setEarlyOutOpen(true)
|
||||||
|
setEarlyOutDate(today)
|
||||||
|
}}
|
||||||
className="w-full btn-primary justify-center bg-emerald-600 hover:bg-emerald-700"
|
className="w-full btn-primary justify-center bg-emerald-600 hover:bg-emerald-700"
|
||||||
>
|
>
|
||||||
<CheckCircle size={15} /> Выселить
|
<CheckCircle size={15} /> {isLateCheckout ? `Выселить (+ ${formatCurrency(room!.lateCheckoutFee!)})` : 'Выселить'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
</>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
{booking.status === 'checked_in' && earlyOutOpen && (() => {
|
{booking.status === 'checked_in' && earlyOutOpen && (() => {
|
||||||
const origNights = differenceInDays(new Date(booking.checkOut), new Date(booking.checkIn))
|
const origNights = differenceInDays(new Date(booking.checkOut), new Date(booking.checkIn))
|
||||||
const actualNights = Math.max(1, differenceInDays(new Date(earlyOutDate), new Date(booking.checkIn)))
|
const actualNights = Math.max(1, differenceInDays(new Date(earlyOutDate), new Date(booking.checkIn)))
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ export function RoomModal({ open, room, categories = [], onClose, onSave, onDele
|
|||||||
hourlyRate: room?.hourlyRate ?? 1000,
|
hourlyRate: room?.hourlyRate ?? 1000,
|
||||||
categoryId: room?.categoryId ?? '',
|
categoryId: room?.categoryId ?? '',
|
||||||
description: room?.description ?? '',
|
description: room?.description ?? '',
|
||||||
|
earlyCheckinFee: room?.earlyCheckinFee ?? '',
|
||||||
|
lateCheckoutFee: room?.lateCheckoutFee ?? '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { amenities: allAmenities } = useAmenities()
|
const { amenities: allAmenities } = useAmenities()
|
||||||
@@ -148,6 +150,8 @@ export function RoomModal({ open, room, categories = [], onClose, onSave, onDele
|
|||||||
beds: beds.length > 0 ? beds : undefined,
|
beds: beds.length > 0 ? beds : undefined,
|
||||||
extraPlace: extraPlace.enabled ? extraPlace : undefined,
|
extraPlace: extraPlace.enabled ? extraPlace : undefined,
|
||||||
childPolicy: childPolicy.enabled ? childPolicy : 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
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Early check-in / late checkout fees */}
|
||||||
|
<div className="rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||||
|
<div className="px-4 py-3 bg-slate-50 dark:bg-slate-700/40 border-b border-slate-200 dark:border-slate-700">
|
||||||
|
<p className="text-sm font-medium text-slate-700 dark:text-slate-300">Тарифы за ранний/поздний заезд</p>
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Применяются если включено в настройках отеля</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Ранний заезд ₽</label>
|
||||||
|
<input
|
||||||
|
type="number" min={0} className="input"
|
||||||
|
placeholder="0 = бесплатно"
|
||||||
|
value={form.earlyCheckinFee}
|
||||||
|
onChange={e => set('earlyCheckinFee', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Поздний выезд ₽</label>
|
||||||
|
<input
|
||||||
|
type="number" min={0} className="input"
|
||||||
|
placeholder="0 = бесплатно"
|
||||||
|
value={form.lateCheckoutFee}
|
||||||
|
onChange={e => set('lateCheckoutFee', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Amenities */}
|
{/* Amenities */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Удобства</label>
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Удобства</label>
|
||||||
|
|||||||
@@ -381,6 +381,7 @@ export interface RoomPayload {
|
|||||||
sortOrder?: number; allowHourly?: boolean; hourlyRate?: number
|
sortOrder?: number; allowHourly?: boolean; hourlyRate?: number
|
||||||
extraPlace?: unknown; childPolicy?: unknown
|
extraPlace?: unknown; childPolicy?: unknown
|
||||||
description?: string; photos?: string[]
|
description?: string; photos?: string[]
|
||||||
|
earlyCheckinFee?: number | null; lateCheckoutFee?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function toRoomPayload(r: Partial<RoomPayload>): Record<string, unknown> {
|
function toRoomPayload(r: Partial<RoomPayload>): Record<string, unknown> {
|
||||||
@@ -404,6 +405,8 @@ function toRoomPayload(r: Partial<RoomPayload>): Record<string, unknown> {
|
|||||||
if (r.childPolicy !== undefined) out.child_policy = r.childPolicy
|
if (r.childPolicy !== undefined) out.child_policy = r.childPolicy
|
||||||
if (r.description !== undefined) out.description = r.description
|
if (r.description !== undefined) out.description = r.description
|
||||||
if (r.photos !== undefined) out.photos = r.photos
|
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
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ export function SettingsPage() {
|
|||||||
const [assignmentStrategy, setAssignmentStrategy] = useState<'spread' | 'together' | 'sequential' | 'manual'>('spread')
|
const [assignmentStrategy, setAssignmentStrategy] = useState<'spread' | 'together' | 'sequential' | 'manual'>('spread')
|
||||||
const [showBookingSource, setShowBookingSource] = useState(false)
|
const [showBookingSource, setShowBookingSource] = useState(false)
|
||||||
const [requireGuestDocs, setRequireGuestDocs] = useState(false)
|
const [requireGuestDocs, setRequireGuestDocs] = useState(false)
|
||||||
|
const [earlyCheckinEnabled, setEarlyCheckinEnabled] = useState(false)
|
||||||
|
const [lateCheckoutEnabled, setLateCheckoutEnabled] = useState(false)
|
||||||
const [hotelSettingsLoaded, setHotelSettingsLoaded] = useState(false)
|
const [hotelSettingsLoaded, setHotelSettingsLoaded] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -83,6 +85,8 @@ export function SettingsPage() {
|
|||||||
api.hotelSettings.get(slug)
|
api.hotelSettings.get(slug)
|
||||||
.then(s => {
|
.then(s => {
|
||||||
setRequireGuestDocs(Boolean(s.require_guest_docs))
|
setRequireGuestDocs(Boolean(s.require_guest_docs))
|
||||||
|
setEarlyCheckinEnabled(Boolean(s.early_checkin_enabled))
|
||||||
|
setLateCheckoutEnabled(Boolean(s.late_checkout_enabled))
|
||||||
setHotelSettingsLoaded(true)
|
setHotelSettingsLoaded(true)
|
||||||
})
|
})
|
||||||
.catch(console.error)
|
.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
|
// Guest settings
|
||||||
const [guestTags, setGuestTags] = useState([
|
const [guestTags, setGuestTags] = useState([
|
||||||
{ id: 'vip', label: 'VIP', color: '#F59E0B' },
|
{ id: 'vip', label: 'VIP', color: '#F59E0B' },
|
||||||
@@ -299,6 +323,28 @@ export function SettingsPage() {
|
|||||||
<Toggle on={requireGuestDocs} onChange={toggleRequireGuestDocs} />
|
<Toggle on={requireGuestDocs} onChange={toggleRequireGuestDocs} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Early check-in fee */}
|
||||||
|
<div className="flex items-center justify-between p-3 rounded-lg bg-slate-50 dark:bg-slate-700/40">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">Платный ранний заезд</p>
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
Предлагать доп. оплату при заселении до официального времени заезда. Сумма задаётся в настройках каждого номера.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle on={earlyCheckinEnabled} onChange={toggleEarlyCheckin} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Late checkout fee */}
|
||||||
|
<div className="flex items-center justify-between p-3 rounded-lg bg-slate-50 dark:bg-slate-700/40">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-900 dark:text-slate-100">Платный поздний выезд</p>
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
Предлагать доп. оплату при выезде после официального времени выезда. Сумма задаётся в настройках каждого номера.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle on={lateCheckoutEnabled} onChange={toggleLateCheckout} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
Стратегия автоматического расселения
|
Стратегия автоматического расселения
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ export interface Room {
|
|||||||
beds?: BedItem[]
|
beds?: BedItem[]
|
||||||
extraPlace?: ExtraPlace
|
extraPlace?: ExtraPlace
|
||||||
childPolicy?: ChildPolicy
|
childPolicy?: ChildPolicy
|
||||||
|
earlyCheckinFee?: number
|
||||||
|
lateCheckoutFee?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentTemplate {
|
export interface DocumentTemplate {
|
||||||
|
|||||||
Reference in New Issue
Block a user