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:
@@ -85,6 +85,27 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
|
||||
const acTimerRef = useRef<ReturnType<typeof setTimeout> | 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
|
||||
useEffect(() => {
|
||||
if (tab !== 'guest' || !slug || bgLoaded) return
|
||||
@@ -1272,19 +1293,73 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on
|
||||
</div>
|
||||
)}
|
||||
|
||||
{booking.status === 'confirmed' && booking.checkIn <= today && (
|
||||
<button onClick={() => setStatus('checked_in')} className="w-full btn-primary justify-center">
|
||||
<CheckCircle size={15} /> Заселить
|
||||
</button>
|
||||
)}
|
||||
{booking.status === 'checked_in' && !earlyOutOpen && (
|
||||
<button
|
||||
onClick={() => { setEarlyOutOpen(true); setEarlyOutDate(today) }}
|
||||
className="w-full btn-primary justify-center bg-emerald-600 hover:bg-emerald-700"
|
||||
>
|
||||
<CheckCircle size={15} /> Выселить
|
||||
</button>
|
||||
)}
|
||||
{booking.status === 'confirmed' && booking.checkIn <= today && (() => {
|
||||
const nowTime = format(new Date(), 'HH:mm')
|
||||
const isEarlyArrival = earlyCheckinEnabled && nowTime < hotelCheckInTime && (room?.earlyCheckinFee ?? 0) > 0
|
||||
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>
|
||||
)}
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<CheckCircle size={15} /> {isLateCheckout ? `Выселить (+ ${formatCurrency(room!.lateCheckoutFee!)})` : 'Выселить'}
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
{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)))
|
||||
|
||||
Reference in New Issue
Block a user