From 2c458dfc097a571b98bc26094fe1be027926b488 Mon Sep 17 00:00:00 2001 From: HotelSync Date: Sat, 21 Mar 2026 19:11:29 +0300 Subject: [PATCH] feat: early checkout with refund calculation for any checked_in booking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces simple 'Выселить' button with a checkout form: - Date picker (min=checkIn, max=original checkOut) - If leaving early: shows refund nights, refund amount, and payment method selector - Adds negative payment record to local payment history - On confirm: sets status=checked_out and updates checkOut date Co-Authored-By: Claude Sonnet 4.6 --- .../bookings/BookingDetailPanel.tsx | 106 +++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/src/components/bookings/BookingDetailPanel.tsx b/src/components/bookings/BookingDetailPanel.tsx index bec3e67..977f5fc 100644 --- a/src/components/bookings/BookingDetailPanel.tsx +++ b/src/components/bookings/BookingDetailPanel.tsx @@ -264,6 +264,11 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on const [earlyRoomId, setEarlyRoomId] = useState(booking.roomId) const [earlySaving, setEarlySaving] = useState(false) + const [earlyOutOpen, setEarlyOutOpen] = useState(false) + const [earlyOutDate, setEarlyOutDate] = useState(today) + const [earlyOutMethod, setEarlyOutMethod] = useState<'cash' | 'card' | 'transfer'>('cash') + const [earlyOutSaving, setEarlyOutSaving] = useState(false) + const freeRoomsForRelocate = (rooms ?? []).filter(r => r.id !== booking.roomId && !(allBookings ?? []).some(b => @@ -297,6 +302,31 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on } } + const handleEarlyCheckOut = async () => { + if (earlyOutDate > booking.checkOut) { showToast('Дата не может быть позже плановой'); return } + setEarlyOutSaving(true) + try { + const origNights = differenceInDays(new Date(booking.checkOut), new Date(booking.checkIn)) + const actualNights = differenceInDays(new Date(earlyOutDate), new Date(booking.checkIn)) + const refundNights = origNights - actualNights + if (refundNights > 0) { + const refundAmt = Math.round(refundNights * booking.totalAmount / origNights) + setPayments(prev => [...prev, { + id: `refund-${Date.now()}`, + date: new Date().toLocaleDateString('ru-RU'), + amount: -refundAmt, + method: earlyOutMethod, + note: `Возврат за ${refundNights} неиспользованных ${refundNights === 1 ? 'ночь' : refundNights < 5 ? 'ночи' : 'ночей'}`, + }]) + } + await onUpdate(booking.id, { checkOut: earlyOutDate, status: 'checked_out' }) + setEarlyOutOpen(false) + showToast('Гость выселен') + } finally { + setEarlyOutSaving(false) + } + } + const findConflict = (ci: string, co: string, rid: string): Booking | null => (allBookings ?? []).find(b => b.id !== booking.id && @@ -1247,11 +1277,83 @@ export function BookingDetailPanel({ booking, room, rooms, allBookings, slug, on Заселить )} - {booking.status === 'checked_in' && ( - )} + {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))) + const refundNights = origNights - actualNights + const refundAmt = refundNights > 0 ? Math.round(refundNights * booking.totalAmount / origNights) : 0 + return ( +
+

+ Выселение гостя +

+
+ + setEarlyOutDate(e.target.value)} + /> +
+ {refundNights > 0 && ( +
+

+ Возврат за {refundNights} {refundNights === 1 ? 'ночь' : refundNights < 5 ? 'ночи' : 'ночей'}: {formatCurrency(refundAmt)} +

+
+ {METHODS.map(m => ( + + ))} +
+

+ Выдайте гостю {formatCurrency(refundAmt)} {earlyOutMethod === 'cash' ? 'наличными' : earlyOutMethod === 'card' ? 'на карту' : 'переводом'} +

+
+ )} + {refundNights <= 0 && ( +

Возврат не требуется

+ )} +
+ + +
+
+ ) + })()} {(booking.status === 'confirmed' || booking.status === 'inquiry') && (