feat: tariff selector in BookingModal

- Migration 050: add tariff_id FK to bookings table
- Backend: accept tariff_id in POST/PATCH bookings
- Frontend: load active tariffs in BookingModal, show tariff dropdown
- Price calculation applies tariff modifier (percent or fixed) before discount
- tariffId forwarded through CalendarPage → api.bookings.create/update

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-31 21:02:56 +03:00
parent 249492cf3a
commit 948744e077
6 changed files with 60 additions and 10 deletions

View File

@@ -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<TariffApi[]>([])
const [selectedTariffId, setSelectedTariffId] = useState<string>(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
</div>
</div>
{/* Тариф */}
{tariffs.length > 0 && (
<div className="shrink-0 min-w-[160px]">
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1.5 flex items-center gap-1"><Tag size={11} /> Тариф</p>
<select
className="input text-sm"
value={selectedTariffId}
onChange={e => setSelectedTariffId(e.target.value)}
>
<option value=""> Базовый </option>
{tariffs.map(t => {
const sign = t.modifier_value >= 0 ? '+' : ''
const mod = t.modifier_type === 'percent'
? `${sign}${t.modifier_value}%`
: `${sign}${t.modifier_value.toLocaleString('ru-RU')}`
return (
<option key={t.id} value={t.id}>{t.name} ({mod})</option>
)
})}
</select>
</div>
)}
{/* Скидка */}
<div className="shrink-0 min-w-[160px]">
<p className="text-xs text-slate-500 dark:text-slate-400 mb-1.5 flex items-center gap-1"><Tag size={11} /> Скидка</p>
@@ -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')}`}
</p>
)}

View File

@@ -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<BookingPayload>): Record<string, unknown> {
@@ -708,6 +709,7 @@ function toBookingPayload(b: Partial<BookingPayload>): Record<string, unknown> {
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
}

View File

@@ -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 })

View File

@@ -169,6 +169,7 @@ export interface Booking {
adults: number
children: number
notes?: string
tariffId?: string | null
createdAt: string
}