diff --git a/backend/src/routes/bookings.ts b/backend/src/routes/bookings.ts index 2d168cf..f77e8ab 100644 --- a/backend/src/routes/bookings.ts +++ b/backend/src/routes/bookings.ts @@ -58,7 +58,7 @@ const bookings: FastifyPluginAsync = async (fastify) => { fastify.post( '/api/hotels/:slug/bookings', { onRequest: [fastify.authenticate] }, @@ -76,7 +76,7 @@ const bookings: FastifyPluginAsync = async (fastify) => { const { room_id, guest_name, guest_email, guest_phone, check_in, check_out, adults = 1, children = 0, - status = 'confirmed', source = 'direct', total_amount, notes, + status = 'confirmed', source = 'direct', total_amount, paid_amount = 0, notes, } = request.body // Check for conflicts @@ -88,17 +88,17 @@ const bookings: FastifyPluginAsync = async (fastify) => { [room_id, check_out, check_in], ) if (conflicts.length > 0) { - return reply.code(409).send({ error: 'Room already booked for these dates' }) + return reply.code(409).send({ error: 'Номер уже занят на эти даты' }) } const { rows } = await db.query( `INSERT INTO bookings (hotel_id, room_id, guest_name, guest_email, guest_phone, check_in, check_out, - adults, children, status, source, total_amount, notes) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13) RETURNING *`, + adults, children, status, source, total_amount, paid_amount, notes) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING *`, [hotelId, room_id, guest_name, guest_email ?? null, guest_phone ?? null, check_in, check_out, adults, children, status, source, - total_amount ?? null, notes ?? null], + total_amount ?? 0, paid_amount, notes ?? null], ) return reply.code(201).send(rows[0]) }, diff --git a/src/components/bookings/BookingModal.tsx b/src/components/bookings/BookingModal.tsx index 634a4d7..c9b63db 100644 --- a/src/components/bookings/BookingModal.tsx +++ b/src/components/bookings/BookingModal.tsx @@ -59,12 +59,27 @@ interface BookingModalProps { open: boolean draft: DraftBooking rooms: Room[] + bookings?: Booking[] onClose: () => void onSave: (data: Partial) => 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 }: ))} + {/* 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 ( +
+
+ + Номер занят на эти даты. +
+ {free.length > 0 ? ( +
+ Свободны: + {free.map(r => ( + + ))} +
+ ) : ( +

Нет свободных номеров на эти даты.

+ )} +
+ ) + })() + } {/* Guest name + email */} diff --git a/src/components/calendar/BookingCalendar.tsx b/src/components/calendar/BookingCalendar.tsx index bd3ae46..baf053c 100644 --- a/src/components/calendar/BookingCalendar.tsx +++ b/src/components/calendar/BookingCalendar.tsx @@ -651,6 +651,7 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd open={true} draft={bookingModalDraft} rooms={rooms} + bookings={bookings} onClose={() => { if (bookingModalDraft) onDraftCancel?.(bookingModalDraft.roomId) setBookingModalDraft(null) diff --git a/src/lib/api.ts b/src/lib/api.ts index 4ca9566..d715bbb 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -16,7 +16,7 @@ export class ApiError extends Error { function getToken(): string | null { try { - const s = sessionStorage.getItem('hotelsync-session') + const s = localStorage.getItem('hotelsync-session') return s ? (JSON.parse(s) as { token: string }).token : null } catch { return null @@ -25,11 +25,11 @@ function getToken(): string | null { function saveToken(token: string) { try { - const s = sessionStorage.getItem('hotelsync-session') + const s = localStorage.getItem('hotelsync-session') if (!s) return const parsed = JSON.parse(s) as Record parsed.token = token - sessionStorage.setItem('hotelsync-session', JSON.stringify(parsed)) + localStorage.setItem('hotelsync-session', JSON.stringify(parsed)) } catch { // ignore } @@ -86,7 +86,7 @@ async function req( saveToken(access_token) res = await doFetch(access_token) } else { - sessionStorage.removeItem('hotelsync-session') + localStorage.removeItem('hotelsync-session') window.location.href = '/login' throw new ApiError(401, 'Session expired') } diff --git a/src/pages/CalendarPage.tsx b/src/pages/CalendarPage.tsx index e0fab0f..1895c1c 100644 --- a/src/pages/CalendarPage.tsx +++ b/src/pages/CalendarPage.tsx @@ -58,13 +58,15 @@ export function CalendarPage() { checkIn: data.checkIn, checkOut: data.checkOut, adults: data.adults, children: data.children, status: data.status, source: data.source, - totalAmount: data.totalAmount, notes: data.notes, + totalAmount: data.totalAmount, paidAmount: data.paidAmount, notes: data.notes, }) setBookings(prev => [...prev, created]) send({ type: 'booking:created', booking: created }) if (data.roomId) send({ type: 'unlock', roomId: data.roomId }) } catch (err) { console.error('Failed to create booking:', err) + const msg = err instanceof Error ? err.message : 'Ошибка создания бронирования' + alert(msg) } }