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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -169,6 +169,7 @@ export interface Booking {
|
||||
adults: number
|
||||
children: number
|
||||
notes?: string
|
||||
tariffId?: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user