diff --git a/backend/migrations/050_booking_tariff_id.sql b/backend/migrations/050_booking_tariff_id.sql new file mode 100644 index 0000000..e3fe986 --- /dev/null +++ b/backend/migrations/050_booking_tariff_id.sql @@ -0,0 +1,3 @@ +-- Add tariff reference to bookings +ALTER TABLE bookings + ADD COLUMN IF NOT EXISTS tariff_id UUID REFERENCES tariffs(id) ON DELETE SET NULL; diff --git a/backend/src/routes/bookings.ts b/backend/src/routes/bookings.ts index ecd0e1d..c1cf436 100644 --- a/backend/src/routes/bookings.ts +++ b/backend/src/routes/bookings.ts @@ -61,7 +61,7 @@ const bookings: FastifyPluginAsync = async (fastify) => { fastify.post( '/api/hotels/:slug/bookings', { onRequest: [fastify.authenticate] }, @@ -79,7 +79,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, paid_amount = 0, notes, + status = 'confirmed', source = 'direct', total_amount, paid_amount = 0, notes, tariff_id, } = request.body // Check for conflicts (checked_out = early departure, doesn't block new booking) @@ -97,11 +97,11 @@ const bookings: FastifyPluginAsync = async (fastify) => { 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, paid_amount, notes) - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING *`, + adults, children, status, source, total_amount, paid_amount, notes, tariff_id) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) RETURNING *`, [hotelId, room_id, guest_name, guest_email ?? null, guest_phone ?? null, check_in, check_out, adults, children, status, source, - total_amount ?? 0, paid_amount, notes ?? null], + total_amount ?? 0, paid_amount, notes ?? null, tariff_id ?? null], ) return reply.code(201).send(rows[0]) }, @@ -147,7 +147,7 @@ const bookings: FastifyPluginAsync = async (fastify) => { if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) const allowed = ['room_id','guest_name','guest_email','guest_phone','check_in','check_out', - 'adults','children','status','source','total_amount','paid_amount','notes'] + 'adults','children','status','source','total_amount','paid_amount','notes','tariff_id'] const updates: string[] = [] const values: unknown[] = [] let idx = 1 diff --git a/src/components/bookings/BookingModal.tsx b/src/components/bookings/BookingModal.tsx index 0c36e8f..fff9f73 100644 --- a/src/components/bookings/BookingModal.tsx +++ b/src/components/bookings/BookingModal.tsx @@ -10,7 +10,7 @@ import type { RentalObject, RentalBooking } from '../../data/rentalData' import { FloorMapModal } from '../floormap/FloorMapModal' import { useModules } from '../../contexts/ModulesContext' import { useAuth } from '../../contexts/AuthContext' -import { api } from '../../lib/api' +import { api, type TariffApi } from '../../lib/api' const GUEST_TAGS = ['VIP', 'Постоянный гость', 'Корпоративный', 'Медовый месяц', 'День рождения', 'Особые пожелания'] @@ -174,6 +174,12 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav return () => document.removeEventListener('mousedown', handler) }, []) + // Load tariffs + useEffect(() => { + if (!slug || !open) return + api.tariffs.list(slug).then(list => setTariffs(list.filter(t => t.is_active))).catch(() => {}) + }, [slug, open]) + // Guest suggestions (room tab) const guestSugg = [ ...(bookings ?? []).filter(b => b.status === 'checked_in'), @@ -291,6 +297,10 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav const activeDiscounts = MOCK_DISCOUNTS.filter(d => d.isActive) const selectedDiscount: Discount | undefined = activeDiscounts.find(d => d.id === selectedDiscountId) + // Tariffs + const [tariffs, setTariffs] = useState([]) + const [selectedTariffId, setSelectedTariffId] = useState(existing?.tariffId ?? '') + // Hourly booking state const [isHourly, setIsHourly] = useState(false) const [hourlyDate, setHourlyDate] = useState(draft.checkIn) @@ -365,12 +375,19 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav const roomBaseTotal = isHourly && room?.allowHourly ? hourlyRateForDate * hourlyHours : roomNightlyTotal + const selectedTariff = tariffs.find(t => t.id === selectedTariffId) + const tariffModifier = selectedTariff + ? selectedTariff.modifier_type === 'percent' + ? Math.round(roomBaseTotal * selectedTariff.modifier_value / 100) + : selectedTariff.modifier_value + : 0 + const roomAfterTariff = Math.max(0, roomBaseTotal + tariffModifier) const discountAmount = selectedDiscount ? selectedDiscount.valueType === 'percent' - ? Math.round(roomBaseTotal * Math.min(100, selectedDiscount.value) / 100) - : Math.min(roomBaseTotal, selectedDiscount.value) + ? Math.round(roomAfterTariff * Math.min(100, selectedDiscount.value) / 100) + : Math.min(roomAfterTariff, selectedDiscount.value) : 0 - const roomTotal = Math.max(0, roomBaseTotal - discountAmount) + const roomTotal = Math.max(0, roomAfterTariff - discountAmount) const servicesTotal = addedServices.reduce((s, sv) => s + sv.price * sv.qty, 0) const extraBedsTotal = extraBeds * EXTRA_BED_PRICE * (isHourly ? 1 : Math.max(1, nights)) const total = roomTotal + servicesTotal + extraBedsTotal @@ -427,6 +444,7 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav notes: hourlyPrefix + extraBedsPrefix + form.notes, totalAmount: total, paidAmount, + tariffId: selectedTariffId || null, id: existing?.id ?? `b-${Date.now()}`, hotelId: 'hotel-1', guestId: existing?.guestId ?? `g-${Date.now()}`, @@ -1239,6 +1257,29 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav + {/* Тариф */} + {tariffs.length > 0 && ( +
+

Тариф

+ +
+ )} + {/* Скидка */}

Скидка

@@ -1310,6 +1351,7 @@ export function BookingModal({ open, draft, rooms, bookings = [], onClose, onSav ? `${hourlyHours} ч × ${(room.hourlyRate ?? 0).toLocaleString('ru-RU')} ₽` : `${nightCount} ${nightCount === 1 ? 'ночь' : nightCount < 5 ? 'ночи' : 'ночей'} · ${roomNightlyTotal.toLocaleString('ru-RU')} ₽` } + {tariffModifier !== 0 && ` ${tariffModifier > 0 ? '+' : '−'} ${Math.abs(tariffModifier).toLocaleString('ru-RU')} ₽`} {discountAmount > 0 && ` − ${discountAmount.toLocaleString('ru-RU')} ₽`}

)} diff --git a/src/lib/api.ts b/src/lib/api.ts index c622e5e..9bd83cf 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -691,6 +691,7 @@ export interface BookingPayload { roomId?: string; guestName?: string; guestEmail?: string; guestPhone?: string checkIn?: string; checkOut?: string; adults?: number; children?: number status?: string; source?: string; totalAmount?: number; paidAmount?: number; notes?: string + tariffId?: string | null } function toBookingPayload(b: Partial): Record { @@ -708,6 +709,7 @@ function toBookingPayload(b: Partial): Record { if (b.totalAmount !== undefined) out.total_amount = b.totalAmount if (b.paidAmount !== undefined) out.paid_amount = b.paidAmount if (b.notes !== undefined) out.notes = b.notes + if (b.tariffId !== undefined) out.tariff_id = b.tariffId return out } diff --git a/src/pages/CalendarPage.tsx b/src/pages/CalendarPage.tsx index 170243c..da36ffc 100644 --- a/src/pages/CalendarPage.tsx +++ b/src/pages/CalendarPage.tsx @@ -166,6 +166,7 @@ export function CalendarPage() { adults: data.adults, children: data.children, status: data.status, source: data.source, totalAmount: data.totalAmount, paidAmount: data.paidAmount, notes: data.notes, + tariffId: data.tariffId, }) setBookings(prev => [...prev, created]) send({ type: 'booking:created', booking: created }) @@ -186,6 +187,7 @@ export function CalendarPage() { adults: data.adults, children: data.children, status: data.status, source: data.source, totalAmount: data.totalAmount, notes: data.notes, + tariffId: data.tariffId, }) setBookings(prev => prev.map(b => b.id === id ? updated : b)) send({ type: 'booking:updated', booking: updated }) diff --git a/src/types/index.ts b/src/types/index.ts index 3668529..169c29d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -169,6 +169,7 @@ export interface Booking { adults: number children: number notes?: string + tariffId?: string | null createdAt: string }