Fix booking creation: localStorage token, conflict detection, paid_amount

- Fix api.ts getToken/saveToken to read from localStorage (matching AuthContext) — was reading sessionStorage causing all API requests to fail auth until refresh
- Fix session cleanup on 401 to use localStorage
- Add isConflict/availableRooms helpers to BookingModal; show amber warning when selected room is occupied with clickable free room suggestions
- Pass bookings prop to BookingModal via BookingCalendar
- Add paid_amount to backend POST /bookings INSERT
- Show alert() when booking creation fails so user sees the error
- Pass paidAmount in CalendarPage handleCreate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 19:48:37 +03:00
parent e59bc752b0
commit 684073c4ad
5 changed files with 61 additions and 12 deletions

View File

@@ -59,12 +59,27 @@ interface BookingModalProps {
open: boolean
draft: DraftBooking
rooms: Room[]
bookings?: Booking[]
onClose: () => void
onSave: (data: Partial<Booking>) => void
existing?: Booking
}
export function BookingModal({ open, draft, rooms, onClose, onSave, existing }: BookingModalProps) {
function isConflict(bookings: Booking[], roomId: string, checkIn: string, checkOut: string, excludeId?: string) {
if (!checkIn || !checkOut || checkIn >= checkOut) return false
return bookings.some(b =>
b.roomId === roomId &&
b.id !== excludeId &&
b.status !== 'cancelled' && b.status !== 'no_show' &&
b.checkIn < checkOut && b.checkOut > checkIn,
)
}
function availableRooms(rooms: Room[], bookings: Booking[], checkIn: string, checkOut: string, excludeId?: string) {
return rooms.filter(r => !isConflict(bookings, r.id, checkIn, checkOut, excludeId))
}
export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSave, existing }: BookingModalProps) {
const [form, setForm] = useState({
roomId: existing?.roomId ?? draft.roomId,
guestName: existing?.guestName ?? '',
@@ -252,6 +267,37 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
</option>
))}
</select>
{/* Conflict warning */}
{!isHourly && form.roomId && form.checkIn && form.checkOut && form.checkIn < form.checkOut &&
isConflict(bookings, form.roomId, form.checkIn, form.checkOut, existing?.id) && (() => {
const free = availableRooms(rooms, bookings, form.checkIn, form.checkOut, existing?.id)
return (
<div className="mt-2 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700">
<div className="flex items-start gap-2 text-amber-800 dark:text-amber-300 text-xs mb-2">
<AlertCircle size={13} className="shrink-0 mt-0.5" />
<span>Номер занят на эти даты.</span>
</div>
{free.length > 0 ? (
<div className="flex flex-wrap gap-1.5">
<span className="text-xs text-amber-700 dark:text-amber-400 self-center">Свободны:</span>
{free.map(r => (
<button
key={r.id}
type="button"
onClick={() => set('roomId', r.id)}
className="text-xs px-2 py-0.5 rounded-md bg-white dark:bg-slate-700 border border-amber-300 dark:border-amber-600 text-amber-800 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/30 font-medium transition-colors"
>
{r.number} ({r.type})
</button>
))}
</div>
) : (
<p className="text-xs text-amber-700 dark:text-amber-400">Нет свободных номеров на эти даты.</p>
)}
</div>
)
})()
}
</div>
{/* Guest name + email */}