feat: rental drafts, room modal cleanup, tariff auto-code
- New hotels get 3 rental object templates (tennis/sauna/conference) as drafts (is_active=false) - RentalPage: draft templates shown separately with Activate/Delete buttons - Migration 047: adds is_active column to rental_objects - RoomModal: hide bed type/status/housekeeping in create mode; optional price (from category); restructured Places tab with extra beds constructor - TariffsPage: auto-generate tariff code from name initials if code field is empty - types: ExtraPlace.beds for extra place bed configuration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -60,7 +60,7 @@ export function RoomModal({ open, room, existingNumbers = [], categories = [], s
|
||||
type: room?.type ?? (categories[0]?.name ?? ''),
|
||||
bedType: (room?.bedType ?? 'double') as BedType,
|
||||
maxGuests: room?.maxGuests ?? 2,
|
||||
baseRate: room?.baseRate ?? 5000,
|
||||
baseRate: room?.baseRate != null ? String(room.baseRate) : '',
|
||||
status: (room?.status ?? 'available') as RoomStatus,
|
||||
housekeepingStatus: (room?.housekeepingStatus ?? 'clean') as HousekeepingStatus,
|
||||
sortOrder: room?.sortOrder ?? 99,
|
||||
@@ -84,8 +84,14 @@ export function RoomModal({ open, room, existingNumbers = [], categories = [], s
|
||||
room?.beds ?? [{ type: 'double', count: 1 }]
|
||||
)
|
||||
const [extraPlace, setExtraPlace] = useState<ExtraPlace>(
|
||||
room?.extraPlace ?? { enabled: false, count: 1, price: 1500 }
|
||||
room?.extraPlace ?? { enabled: false, count: 1, price: 1500, beds: [] }
|
||||
)
|
||||
|
||||
// Extra beds constructor helpers
|
||||
const addExtraBed = () => setExtraPlace(p => ({ ...p, beds: [...(p.beds ?? []), { type: 'single', count: 1 }] }))
|
||||
const removeExtraBed = (i: number) => setExtraPlace(p => ({ ...p, beds: (p.beds ?? []).filter((_, idx) => idx !== i) }))
|
||||
const updateExtraBed = (i: number, patch: Partial<BedItem>) =>
|
||||
setExtraPlace(p => ({ ...p, beds: (p.beds ?? []).map((b, idx) => idx === i ? { ...b, ...patch } : b) }))
|
||||
const [childPolicy, setChildPolicy] = useState<ChildPolicy>(
|
||||
room?.childPolicy ?? { enabled: false, freeUnderAge: 12, chargeFromAge: 12 }
|
||||
)
|
||||
@@ -151,6 +157,11 @@ export function RoomModal({ open, room, existingNumbers = [], categories = [], s
|
||||
|
||||
const handleSave = () => {
|
||||
if (!form.number || numberTaken) return
|
||||
// Derive extra place count from beds sum (or keep manual count if no beds)
|
||||
const extraBeds = extraPlace.beds ?? []
|
||||
const extraCount = extraBeds.length > 0
|
||||
? extraBeds.reduce((s, b) => s + b.count, 0)
|
||||
: extraPlace.count
|
||||
onSave({
|
||||
id: room?.id ?? `r-${Date.now()}`,
|
||||
hotelId: room?.hotelId ?? 'hotel-1',
|
||||
@@ -160,7 +171,7 @@ export function RoomModal({ open, room, existingNumbers = [], categories = [], s
|
||||
type: form.type,
|
||||
bedType: form.bedType,
|
||||
maxGuests: form.maxGuests,
|
||||
baseRate: form.baseRate,
|
||||
baseRate: form.baseRate !== '' ? Number(form.baseRate) : 0,
|
||||
status: form.status,
|
||||
housekeepingStatus: form.housekeepingStatus,
|
||||
amenities,
|
||||
@@ -171,7 +182,7 @@ export function RoomModal({ open, room, existingNumbers = [], categories = [], s
|
||||
description: form.description || undefined,
|
||||
photos: photos.length > 0 ? photos : undefined,
|
||||
beds: beds.length > 0 ? beds : undefined,
|
||||
extraPlace: extraPlace.enabled ? extraPlace : undefined,
|
||||
extraPlace: extraPlace.enabled ? { ...extraPlace, count: extraCount, beds: extraBeds.length > 0 ? extraBeds : undefined } : undefined,
|
||||
childPolicy: childPolicy.enabled ? childPolicy : undefined,
|
||||
earlyCheckinFee: form.earlyCheckinFee !== '' ? Number(form.earlyCheckinFee) : undefined,
|
||||
lateCheckoutFee: form.lateCheckoutFee !== '' ? Number(form.lateCheckoutFee) : undefined,
|
||||
@@ -290,36 +301,41 @@ export function RoomModal({ open, room, existingNumbers = [], categories = [], s
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Тип кровати</label>
|
||||
<select className="input" value={form.bedType} onChange={e => set('bedType', e.target.value as BedType)}>
|
||||
{BED_TYPES.map(b => <option key={b.value} value={b.value}>{b.label}</option>)}
|
||||
</select>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Основных гостей</label>
|
||||
<input type="number" min={1} max={20} className="input" value={form.maxGuests} onChange={e => set('maxGuests', parseInt(e.target.value) || 1)} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Макс. гостей</label>
|
||||
<input type="number" min={1} max={10} className="input" value={form.maxGuests} onChange={e => set('maxGuests', parseInt(e.target.value) || 1)} />
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Доп. мест</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExtraPlace(p => ({ ...p, enabled: p.count > 0, count: Math.max(0, p.count - 1) }))}
|
||||
className="w-8 h-8 rounded-lg border border-slate-200 dark:border-slate-600 flex items-center justify-center text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700"
|
||||
><Minus size={12} /></button>
|
||||
<span className="w-8 text-center text-sm font-semibold text-slate-800 dark:text-slate-200">
|
||||
{extraPlace.enabled ? extraPlace.count : 0}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExtraPlace(p => ({ ...p, enabled: true, count: Math.min(10, p.count + 1) }))}
|
||||
className="w-8 h-8 rounded-lg border border-slate-200 dark:border-slate-600 flex items-center justify-center text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700"
|
||||
><Plus size={12} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Цена ₽/ночь</label>
|
||||
<input type="number" min={0} className="input" value={form.baseRate} onChange={e => set('baseRate', parseInt(e.target.value) || 0)} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Статус номера</label>
|
||||
<select className="input" value={form.status} onChange={e => set('status', e.target.value as RoomStatus)}>
|
||||
{ROOM_STATUSES.map(s => <option key={s.value} value={s.value}>{s.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Статус уборки</label>
|
||||
<select className="input" value={form.housekeepingStatus} onChange={e => set('housekeepingStatus', e.target.value as HousekeepingStatus)}>
|
||||
{HK_STATUSES.map(s => <option key={s.value} value={s.value}>{s.label}</option>)}
|
||||
</select>
|
||||
<input
|
||||
type="number" min={0} className="input"
|
||||
placeholder="из категории"
|
||||
value={form.baseRate}
|
||||
onChange={e => set('baseRate', e.target.value)}
|
||||
/>
|
||||
{!form.baseRate && (
|
||||
<p className="text-xs text-slate-400 mt-1">Будет взята из категории</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Порядок сортировки</label>
|
||||
@@ -327,6 +343,24 @@ export function RoomModal({ open, room, existingNumbers = [], categories = [], s
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Статус и уборка — только при редактировании */}
|
||||
{isEdit && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Статус номера</label>
|
||||
<select className="input" value={form.status} onChange={e => set('status', e.target.value as RoomStatus)}>
|
||||
{ROOM_STATUSES.map(s => <option key={s.value} value={s.value}>{s.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Статус уборки</label>
|
||||
<select className="input" value={form.housekeepingStatus} onChange={e => set('housekeepingStatus', e.target.value as HousekeepingStatus)}>
|
||||
{HK_STATUSES.map(s => <option key={s.value} value={s.value}>{s.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hourly section */}
|
||||
<div className="rounded-xl border border-slate-200 dark:border-slate-600 overflow-hidden">
|
||||
<div className="px-4 py-3 bg-slate-50 dark:bg-slate-700/40 border-b border-slate-200 dark:border-slate-600 flex items-center justify-between">
|
||||
@@ -413,8 +447,8 @@ export function RoomModal({ open, room, existingNumbers = [], categories = [], s
|
||||
{/* Beds constructor */}
|
||||
<div className="rounded-xl border border-slate-200 dark:border-slate-600 overflow-hidden">
|
||||
<div className="px-4 py-3 bg-slate-50 dark:bg-slate-700/40 border-b border-slate-200 dark:border-slate-600">
|
||||
<p className="text-sm font-semibold text-slate-700 dark:text-slate-300">Спальные места</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Кровати, диваны и другая мебель для сна</p>
|
||||
<p className="text-sm font-semibold text-slate-700 dark:text-slate-300">Основные спальные места</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Постоянные кровати, включённые в стоимость</p>
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
{beds.map((bed, i) => (
|
||||
@@ -470,12 +504,12 @@ export function RoomModal({ open, room, existingNumbers = [], categories = [], s
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Extra places */}
|
||||
{/* Extra beds */}
|
||||
<div className="rounded-xl border border-slate-200 dark:border-slate-600 overflow-hidden">
|
||||
<div className="px-4 py-3 bg-slate-50 dark:bg-slate-700/40 border-b border-slate-200 dark:border-slate-600 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-slate-700 dark:text-slate-300">Дополнительные места</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Раскладные кровати, кушетки за доп. плату</p>
|
||||
<p className="text-sm font-semibold text-slate-700 dark:text-slate-300">Дополнительные спальные места</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Раскладные кровати, кушетки — за доп. плату</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -486,25 +520,45 @@ export function RoomModal({ open, room, existingNumbers = [], categories = [], s
|
||||
</button>
|
||||
</div>
|
||||
{extraPlace.enabled && (
|
||||
<div className="p-4 grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1.5">Кол-во мест</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<button type="button" onClick={() => setExtraPlace(p => ({ ...p, count: Math.max(1, p.count - 1) }))}
|
||||
className="w-8 h-8 rounded-lg border border-slate-200 dark:border-slate-600 flex items-center justify-center text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700">
|
||||
<Minus size={13} />
|
||||
</button>
|
||||
<span className="w-8 text-center font-semibold text-slate-800 dark:text-slate-200">{extraPlace.count}</span>
|
||||
<button type="button" onClick={() => setExtraPlace(p => ({ ...p, count: Math.min(5, p.count + 1) }))}
|
||||
className="w-8 h-8 rounded-lg border border-slate-200 dark:border-slate-600 flex items-center justify-center text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700">
|
||||
<Plus size={13} />
|
||||
<div className="p-4 space-y-3">
|
||||
{(extraPlace.beds ?? []).map((bed, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<span className="text-lg w-7 text-center shrink-0">
|
||||
{bedTypes.find(b => b.value === bed.type)?.icon ?? '🛏'}
|
||||
</span>
|
||||
<select
|
||||
className="input flex-1"
|
||||
value={bed.type}
|
||||
onChange={e => updateExtraBed(i, { type: e.target.value })}
|
||||
>
|
||||
{bedTypes.map(b => <option key={b.value} value={b.value}>{b.label}</option>)}
|
||||
</select>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button type="button"
|
||||
onClick={() => updateExtraBed(i, { count: Math.max(1, bed.count - 1) })}
|
||||
className="w-7 h-7 rounded-lg border border-slate-200 dark:border-slate-600 flex items-center justify-center text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700">
|
||||
<Minus size={12} />
|
||||
</button>
|
||||
<span className="w-6 text-center text-sm font-medium text-slate-700 dark:text-slate-300">{bed.count}</span>
|
||||
<button type="button"
|
||||
onClick={() => updateExtraBed(i, { count: Math.min(6, bed.count + 1) })}
|
||||
className="w-7 h-7 rounded-lg border border-slate-200 dark:border-slate-600 flex items-center justify-center text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700">
|
||||
<Plus size={12} />
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" onClick={() => removeExtraBed(i)}
|
||||
className="w-7 h-7 rounded-lg flex items-center justify-center text-slate-400 hover:text-red-500">
|
||||
<XIcon size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button type="button" onClick={addExtraBed}
|
||||
className="btn-secondary w-full justify-center text-xs py-1.5">
|
||||
<Plus size={13} /> Добавить доп. кровать
|
||||
</button>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1.5">Цена ₽/ночь</label>
|
||||
<input
|
||||
type="number" min={0} className="input"
|
||||
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1.5">Цена ₽/место/ночь</label>
|
||||
<input type="number" min={0} className="input w-40"
|
||||
value={extraPlace.price}
|
||||
onChange={e => setExtraPlace(p => ({ ...p, price: parseInt(e.target.value) || 0 }))}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface RentalObject {
|
||||
closeHour: number // 22
|
||||
maxHoursPerSlot?: number
|
||||
bufferMinutes?: number // tech break between bookings
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
export interface RentalBooking {
|
||||
|
||||
@@ -817,6 +817,7 @@ export interface RentalObjectApi {
|
||||
maxHoursPerSlot: number | null
|
||||
bufferMinutes: number
|
||||
sortOrder: number
|
||||
isActive: boolean
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Plus, Pencil, Trash2, CalendarDays, X, Save } from 'lucide-react'
|
||||
import { Plus, Pencil, Trash2, CalendarDays, X, Save, Sparkles } from 'lucide-react'
|
||||
import { format } from 'date-fns'
|
||||
import { ru } from 'date-fns/locale'
|
||||
import { cn, formatCurrency } from '../lib/utils'
|
||||
@@ -267,6 +267,7 @@ function fromObjApi(o: RentalObjectApi): RentalObject {
|
||||
openHour: o.openHour, closeHour: o.closeHour,
|
||||
maxHoursPerSlot: o.maxHoursPerSlot ?? undefined,
|
||||
bufferMinutes: o.bufferMinutes ?? undefined,
|
||||
isActive: o.isActive,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,6 +335,12 @@ export function RentalPage() {
|
||||
if (selectedObj?.id === id) setSelectedObj(null)
|
||||
}
|
||||
|
||||
const handleActivateObject = async (id: string) => {
|
||||
if (!slug) return
|
||||
const saved = await api.rental.updateObject(slug, id, { is_active: true })
|
||||
setObjects(prev => prev.map(o => o.id === id ? fromObjApi(saved) : o))
|
||||
}
|
||||
|
||||
const handleBookingSave = async (b: RentalBooking) => {
|
||||
if (!slug) return
|
||||
const payload = {
|
||||
@@ -357,13 +364,16 @@ export function RentalPage() {
|
||||
.filter(b => b.status === 'confirmed')
|
||||
.reduce((s, b) => s + b.totalAmount, 0)
|
||||
|
||||
const activeObjects = objects.filter(o => o.isActive !== false)
|
||||
const draftObjects = objects.filter(o => o.isActive === false)
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-6 space-y-5">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-100">Аренда объектов</h1>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">{objects.length} объектов · управление и расписание</p>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">{activeObjects.length} объектов · управление и расписание</p>
|
||||
</div>
|
||||
<button onClick={() => setCreateModal(true)} className="btn-primary">
|
||||
<Plus size={15} />
|
||||
@@ -374,7 +384,7 @@ export function RentalPage() {
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{[
|
||||
{ label: 'Объектов', value: objects.length, color: 'text-slate-900 dark:text-slate-100' },
|
||||
{ label: 'Объектов', value: activeObjects.length, color: 'text-slate-900 dark:text-slate-100' },
|
||||
{ label: 'Броней сегодня', value: bookings.filter(b => b.date === today).length, color: 'text-brand-600 dark:text-brand-400' },
|
||||
{ label: 'Всего броней', value: bookings.filter(b => b.status === 'confirmed').length, color: 'text-emerald-600 dark:text-emerald-400' },
|
||||
{ label: 'Общая выручка', value: formatCurrency(totalRevenue), color: 'text-emerald-600 dark:text-emerald-400' },
|
||||
@@ -390,7 +400,7 @@ export function RentalPage() {
|
||||
{/* Objects list */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide">Объекты аренды</p>
|
||||
{objects.map(obj => {
|
||||
{activeObjects.map(obj => {
|
||||
const objBookings = bookings.filter(b => b.objectId === obj.id && b.status === 'confirmed')
|
||||
const todayB = objBookings.filter(b => b.date === today)
|
||||
const isSelected = selectedObj?.id === obj.id
|
||||
@@ -443,7 +453,7 @@ export function RentalPage() {
|
||||
)
|
||||
})}
|
||||
|
||||
{objects.length === 0 && (
|
||||
{activeObjects.length === 0 && draftObjects.length === 0 && (
|
||||
<div className="card p-8 text-center text-slate-400">
|
||||
<p className="text-sm">Нет объектов аренды</p>
|
||||
<button onClick={() => setCreateModal(true)} className="btn-secondary text-xs mt-3">
|
||||
@@ -451,6 +461,43 @@ export function RentalPage() {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Draft templates */}
|
||||
{draftObjects.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles size={13} className="text-amber-500" />
|
||||
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Шаблоны</p>
|
||||
</div>
|
||||
{draftObjects.map(obj => (
|
||||
<div key={obj.id} className="rounded-xl border border-dashed border-slate-300 dark:border-slate-600 p-3 bg-slate-50 dark:bg-slate-800/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn('w-8 h-8 rounded-lg flex items-center justify-center text-lg shrink-0 text-white opacity-60', obj.color)}>
|
||||
{obj.icon}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-slate-600 dark:text-slate-400 truncate">{obj.name}</p>
|
||||
<p className="text-xs text-slate-400 dark:text-slate-500">Черновик · не активен</p>
|
||||
</div>
|
||||
<div className="flex gap-1 shrink-0">
|
||||
<button
|
||||
onClick={() => handleActivateObject(obj.id)}
|
||||
className="text-xs px-2.5 py-1 rounded-lg bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400 dark:hover:bg-emerald-900/50 font-medium transition-colors"
|
||||
>
|
||||
Активировать
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteObject(obj.id)}
|
||||
className="p-1.5 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 text-slate-400 hover:text-red-600 transition-colors"
|
||||
>
|
||||
<Trash2 size={13} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: bookings for selected object */}
|
||||
|
||||
@@ -166,6 +166,18 @@ function TariffModal({
|
||||
const set = <K extends keyof typeof form>(k: K, v: typeof form[K]) =>
|
||||
setForm(prev => ({ ...prev, [k]: v }))
|
||||
|
||||
// Авто-генерация кода из названия (если поле кода пустое)
|
||||
const autoCode = (name: string) =>
|
||||
name.trim().split(/\s+/).map(w => w[0]?.toUpperCase() ?? '').join('').slice(0, 6) || ''
|
||||
|
||||
const handleNameChange = (v: string) => {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
name: v,
|
||||
code: prev.code || !tariff ? autoCode(v) : prev.code,
|
||||
}))
|
||||
}
|
||||
|
||||
const toggleInclusion = (id: string) =>
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
@@ -204,9 +216,13 @@ function TariffModal({
|
||||
<>
|
||||
<button onClick={onClose} className="btn-secondary">Отмена</button>
|
||||
<button
|
||||
onClick={() => { if (form.name && form.code) onSave(form) }}
|
||||
onClick={() => {
|
||||
if (!form.name) return
|
||||
const code = form.code || autoCode(form.name) || `T${Date.now().toString(36).toUpperCase().slice(-4)}`
|
||||
onSave({ ...form, code })
|
||||
}}
|
||||
className="btn-primary"
|
||||
disabled={!form.name || !form.code}
|
||||
disabled={!form.name}
|
||||
>
|
||||
{tariff ? 'Сохранить' : 'Создать тариф'}
|
||||
</button>
|
||||
@@ -221,13 +237,13 @@ function TariffModal({
|
||||
Название тарифа *
|
||||
</label>
|
||||
<input type="text" className="input" placeholder="Стандарт с завтраком"
|
||||
value={form.name} onChange={e => set('name', e.target.value)} />
|
||||
value={form.name} onChange={e => handleNameChange(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">
|
||||
Код *
|
||||
Код <span className="text-slate-400 font-normal text-xs">(необязательно)</span>
|
||||
</label>
|
||||
<input type="text" className="input font-mono uppercase" placeholder="STD-BB"
|
||||
<input type="text" className="input font-mono uppercase" placeholder="авто"
|
||||
value={form.code}
|
||||
onChange={e => set('code', e.target.value.toUpperCase())} />
|
||||
</div>
|
||||
|
||||
@@ -60,8 +60,9 @@ export interface BedItem {
|
||||
|
||||
export interface ExtraPlace {
|
||||
enabled: boolean
|
||||
count: number // max extra places available
|
||||
price: number // price per extra place per night
|
||||
count: number // max extra places available (derived from beds sum)
|
||||
price: number // price per extra place per night
|
||||
beds?: BedItem[] // bed types for extra places
|
||||
}
|
||||
|
||||
export interface ChildPolicy {
|
||||
|
||||
Reference in New Issue
Block a user