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:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user