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:
@@ -58,7 +58,7 @@ const bookings: FastifyPluginAsync = async (fastify) => {
|
||||
fastify.post<SlugParam & { Body: {
|
||||
room_id: string; guest_name: string; guest_email?: string; guest_phone?: string
|
||||
check_in: string; check_out: string; adults?: number; children?: number
|
||||
status?: string; source?: string; total_amount?: number; notes?: string
|
||||
status?: string; source?: string; total_amount?: number; paid_amount?: number; notes?: string
|
||||
} }>(
|
||||
'/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])
|
||||
},
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<string, unknown>
|
||||
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<T>(
|
||||
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')
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user