Add hourly indicator in calendar, extra beds, custom amenities, meal/inclusion management

- Calendar: show clock icon + hours on hourly bookings (detect [Почасово:] prefix in notes)
- BookingModal: add extra beds counter (max 4, 1500₽/place/night) included in total
- RoomCategoriesPage: custom amenity input — type any amenity and add it alongside predefined list
- TariffsPage: collapsible Справочники section with full add/edit/delete for meal plan types and room inclusions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 15:28:43 +03:00
parent 92f0270290
commit c347c99228
4 changed files with 247 additions and 17 deletions

View File

@@ -1,6 +1,6 @@
import { useState } from 'react' import { useState } from 'react'
import { format } from 'date-fns' import { format } from 'date-fns'
import { Star, Banknote, CreditCard, AlertCircle, Map, Plus, Trash2, IdCard, CalendarDays, Tag, ScanLine } from 'lucide-react' import { Star, Banknote, CreditCard, AlertCircle, Map, Plus, Trash2, IdCard, CalendarDays, Tag, ScanLine, Minus, BedDouble } from 'lucide-react'
import { MOCK_DISCOUNTS } from '../../pages/DiscountsPage' import { MOCK_DISCOUNTS } from '../../pages/DiscountsPage'
import type { Discount } from '../../pages/DiscountsPage' import type { Discount } from '../../pages/DiscountsPage'
import { Modal } from '../ui/Modal' import { Modal } from '../ui/Modal'
@@ -99,6 +99,10 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
const [startHour, setStartHour] = useState(10) const [startHour, setStartHour] = useState(10)
const [endHour, setEndHour] = useState(12) const [endHour, setEndHour] = useState(12)
// Extra beds
const [extraBeds, setExtraBeds] = useState(0)
const EXTRA_BED_PRICE = 1500
const room = rooms.find(r => r.id === form.roomId) const room = rooms.find(r => r.id === form.roomId)
const nights = form.checkIn && form.checkOut const nights = form.checkIn && form.checkOut
? Math.max(0, (new Date(form.checkOut).getTime() - new Date(form.checkIn).getTime()) / 86400000) ? Math.max(0, (new Date(form.checkOut).getTime() - new Date(form.checkIn).getTime()) / 86400000)
@@ -115,7 +119,8 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
: 0 : 0
const roomTotal = Math.max(0, roomBaseTotal - discountAmount) const roomTotal = Math.max(0, roomBaseTotal - discountAmount)
const servicesTotal = addedServices.reduce((s, sv) => s + sv.price * sv.qty, 0) const servicesTotal = addedServices.reduce((s, sv) => s + sv.price * sv.qty, 0)
const total = roomTotal + servicesTotal const extraBedsTotal = extraBeds * EXTRA_BED_PRICE * (isHourly ? 1 : Math.max(1, nights))
const total = roomTotal + servicesTotal + extraBedsTotal
const debt = Math.max(0, total - paidAmount) const debt = Math.max(0, total - paidAmount)
@@ -150,6 +155,7 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
const hourlyPrefix = isHourly && room?.allowHourly const hourlyPrefix = isHourly && room?.allowHourly
? `[Почасово: ${String(startHour).padStart(2, '0')}:00${String(endHour).padStart(2, '0')}:00] ` ? `[Почасово: ${String(startHour).padStart(2, '0')}:00${String(endHour).padStart(2, '0')}:00] `
: '' : ''
const extraBedsPrefix = extraBeds > 0 ? `[Доп.места: ${extraBeds}] ` : ''
const checkIn = isHourly && room?.allowHourly ? hourlyDate : form.checkIn const checkIn = isHourly && room?.allowHourly ? hourlyDate : form.checkIn
const checkOut = isHourly && room?.allowHourly ? hourlyDate : form.checkOut const checkOut = isHourly && room?.allowHourly ? hourlyDate : form.checkOut
@@ -159,7 +165,7 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
source: 'direct', source: 'direct',
checkIn, checkIn,
checkOut, checkOut,
notes: hourlyPrefix + form.notes, notes: hourlyPrefix + extraBedsPrefix + form.notes,
totalAmount: total, totalAmount: total,
paidAmount, paidAmount,
id: existing?.id ?? `b-${Date.now()}`, id: existing?.id ?? `b-${Date.now()}`,
@@ -418,6 +424,37 @@ export function BookingModal({ open, draft, rooms, onClose, onSave, existing }:
</div> </div>
</div> </div>
{/* Extra beds */}
<div className="flex items-center justify-between px-3 py-2 rounded-xl bg-slate-50 dark:bg-slate-700/40 border border-slate-200 dark:border-slate-600">
<div className="flex items-center gap-2">
<BedDouble size={15} className="text-slate-400 shrink-0" />
<div>
<p className="text-sm font-medium text-slate-700 dark:text-slate-300 leading-tight">Доп. места</p>
<p className="text-[10px] text-slate-400">{EXTRA_BED_PRICE.toLocaleString('ru-RU')} /место{!isHourly && nights > 0 ? '/ночь' : ''}</p>
</div>
</div>
<div className="flex items-center gap-2">
{extraBeds > 0 && (
<span className="text-xs font-medium text-amber-600 dark:text-amber-400 mr-1">
+{extraBedsTotal.toLocaleString('ru-RU')}
</span>
)}
<button type="button" onClick={() => setExtraBeds(e => Math.max(0, e - 1))}
className="w-6 h-6 rounded-md bg-slate-200 dark:bg-slate-600 flex items-center justify-center hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors disabled:opacity-40"
disabled={extraBeds === 0}
>
<Minus size={11} />
</button>
<span className="w-5 text-center text-sm font-semibold text-slate-900 dark:text-slate-100">{extraBeds}</span>
<button type="button" onClick={() => setExtraBeds(e => Math.min(4, e + 1))}
className="w-6 h-6 rounded-md bg-slate-200 dark:bg-slate-600 flex items-center justify-center hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors disabled:opacity-40"
disabled={extraBeds >= 4}
>
<Plus size={11} />
</button>
</div>
</div>
{/* Guest tags */} {/* Guest tags */}
<div> <div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5"> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">

View File

@@ -2,7 +2,7 @@ import { useState, useRef, useCallback, useEffect } from 'react'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { addDays, format, startOfDay, differenceInDays, parseISO, isToday } from 'date-fns' import { addDays, format, startOfDay, differenceInDays, parseISO, isToday } from 'date-fns'
import { ru } from 'date-fns/locale' import { ru } from 'date-fns/locale'
import { ChevronLeft, ChevronRight, Plus, CalendarDays, ChevronDown, AlignJustify } from 'lucide-react' import { ChevronLeft, ChevronRight, Plus, CalendarDays, ChevronDown, AlignJustify, Clock } from 'lucide-react'
import { cn, BOOKING_STATUS_COLORS, BOOKING_STATUS_LABELS, SOURCE_LABELS } from '../../lib/utils' import { cn, BOOKING_STATUS_COLORS, BOOKING_STATUS_LABELS, SOURCE_LABELS } from '../../lib/utils'
import type { Room, Booking, DraftBooking } from '../../types' import type { Room, Booking, DraftBooking } from '../../types'
import type { RentalObject, RentalBooking } from '../../data/rentalData' import type { RentalObject, RentalBooking } from '../../data/rentalData'
@@ -412,6 +412,7 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd
const isFading = fadingBookingIds?.has(booking.id) const isFading = fadingBookingIds?.has(booking.id)
const isUnpaid = booking.paidAmount < booking.totalAmount const isUnpaid = booking.paidAmount < booking.totalAmount
const guestCount = (booking.adults ?? 0) + (booking.children ?? 0) const guestCount = (booking.adults ?? 0) + (booking.children ?? 0)
const hourlyMatch = booking.notes?.match(/\[Почасово: (\d+):00(\d+):00\]/)
return ( return (
<div <div
key={booking.id} key={booking.id}
@@ -428,20 +429,26 @@ export function BookingCalendar({ rooms, bookings, onBookingCreate, onBookingUpd
position: 'absolute', position: 'absolute',
}} }}
onClick={(e) => { e.stopPropagation(); setSelectedBooking(booking) }} onClick={(e) => { e.stopPropagation(); setSelectedBooking(booking) }}
title={`${booking.guestName}${booking.checkIn} ${booking.checkOut}${isUnpaid ? ' • Не оплачено' : ''}`} title={`${booking.guestName}${booking.checkIn}${hourlyMatch ? ` ${hourlyMatch[1]}:00${hourlyMatch[2]}:00` : ` ${booking.checkOut}`}${isUnpaid ? ' • Не оплачено' : ''}`}
> >
{isUnpaid && ( {isUnpaid && (
<span className="shrink-0 w-1.5 h-1.5 rounded-full bg-red-500 shadow-sm mr-1 mt-0.5" /> <span className="shrink-0 w-1.5 h-1.5 rounded-full bg-red-500 shadow-sm mr-1 mt-0.5" />
)} )}
{hourlyMatch && <Clock size={9} className="shrink-0 mr-0.5 opacity-80" />}
<span className="truncate text-xs font-semibold opacity-95 drop-shadow-sm"> <span className="truncate text-xs font-semibold opacity-95 drop-shadow-sm">
{booking.guestName} {booking.guestName}
</span> </span>
{!compact && style.width > 90 && guestCount > 0 && ( {!compact && hourlyMatch && style.width > 80 && (
<span className="ml-1 opacity-80 text-[10px] shrink-0">
{hourlyMatch[1]}{hourlyMatch[2]}ч
</span>
)}
{!compact && !hourlyMatch && style.width > 90 && guestCount > 0 && (
<span className="ml-1.5 opacity-75 text-[10px] shrink-0"> <span className="ml-1.5 opacity-75 text-[10px] shrink-0">
{guestCount}г {guestCount}г
</span> </span>
)} )}
{!compact && style.width > 130 && ( {!compact && !hourlyMatch && style.width > 130 && (
<span className="ml-1.5 opacity-75 text-[10px] shrink-0"> <span className="ml-1.5 opacity-75 text-[10px] shrink-0">
{nights}н {nights}н
</span> </span>

View File

@@ -1,5 +1,5 @@
import { useState, useRef } from 'react' import { useState, useRef } from 'react'
import { Plus, Pencil, Trash2, ImagePlus, X as XIcon, Tag, ChevronRight } from 'lucide-react' import { Plus, Pencil, Trash2, ImagePlus, X as XIcon, Tag, ChevronRight, Check } from 'lucide-react'
import { cn } from '../lib/utils' import { cn } from '../lib/utils'
import type { RoomCategory } from '../types' import type { RoomCategory } from '../types'
@@ -71,9 +71,21 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) {
const [photoIdx, setPhotoIdx] = useState(0) const [photoIdx, setPhotoIdx] = useState(0)
const fileRef = useRef<HTMLInputElement>(null) const fileRef = useRef<HTMLInputElement>(null)
const [customInput, setCustomInput] = useState('')
const toggleAmenity = (a: string) => const toggleAmenity = (a: string) =>
setAmenities(prev => prev.includes(a) ? prev.filter(x => x !== a) : [...prev, a]) setAmenities(prev => prev.includes(a) ? prev.filter(x => x !== a) : [...prev, a])
const addCustom = () => {
const v = customInput.trim()
if (!v || amenities.includes(v)) return
setAmenities(prev => [...prev, v])
setCustomInput('')
}
// All amenities to show as toggles: predefined + any custom ones already in the list
const customAmenities = amenities.filter(a => !AMENITY_LIST.includes(a))
const handlePhotoUpload = (e: React.ChangeEvent<HTMLInputElement>) => { const handlePhotoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
Array.from(e.target.files ?? []).forEach(file => { Array.from(e.target.files ?? []).forEach(file => {
const reader = new FileReader() const reader = new FileReader()
@@ -178,7 +190,7 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) {
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Базовые удобства</label> <label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Удобства в номере</label>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{AMENITY_LIST.map(a => { {AMENITY_LIST.map(a => {
const sel = amenities.includes(a) const sel = amenities.includes(a)
@@ -198,6 +210,37 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) {
</button> </button>
) )
})} })}
{/* Custom amenities */}
{customAmenities.map(a => (
<span
key={a}
className="flex items-center gap-1 pl-3 pr-1 py-1.5 rounded-lg text-xs font-medium border bg-brand-600 text-white border-brand-600"
>
{a}
<button type="button" onClick={() => toggleAmenity(a)} className="w-4 h-4 rounded-full hover:bg-brand-700 flex items-center justify-center">
<XIcon size={10} />
</button>
</span>
))}
</div>
{/* Add custom amenity */}
<div className="flex gap-2 mt-2">
<input
type="text"
className="input flex-1 text-sm"
placeholder="Добавить своё удобство..."
value={customInput}
onChange={e => setCustomInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && (e.preventDefault(), addCustom())}
/>
<button
type="button"
onClick={addCustom}
disabled={!customInput.trim() || amenities.includes(customInput.trim())}
className="btn-secondary px-3 disabled:opacity-40 disabled:cursor-not-allowed"
>
<Check size={14} />
</button>
</div> </div>
</div> </div>
</> </>

View File

@@ -1,14 +1,15 @@
import { useState } from 'react' import React, { useState } from 'react'
import { import {
Plus, Edit2, Trash2, ToggleLeft, ToggleRight, Coffee, Utensils, Plus, Edit2, Trash2, ToggleLeft, ToggleRight, Coffee, Utensils,
Wifi, Car, Waves, Sparkles, Plane, Package, Check, X, Info, Tag, Wifi, Car, Waves, Sparkles, Plane, Package, Check, X, Info, Tag,
ChevronDown, ChevronUp, Pencil,
} from 'lucide-react' } from 'lucide-react'
import { Modal } from '../components/ui/Modal' import { Modal } from '../components/ui/Modal'
import { cn } from '../lib/utils' import { cn } from '../lib/utils'
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────
type MealPlan = 'no_meals' | 'breakfast' | 'hb' | 'fb' | 'ai' type MealPlan = string
interface TariffInclusion { interface TariffInclusion {
id: string id: string
@@ -135,11 +136,13 @@ const EMPTY_TARIFF: Omit<Tariff, 'id'> = {
} }
function TariffModal({ function TariffModal({
tariff, onSave, onClose, tariff, onSave, onClose, mealPlans, allInclusions,
}: { }: {
tariff?: Tariff tariff?: Tariff
onSave: (t: Omit<Tariff, 'id'>) => void onSave: (t: Omit<Tariff, 'id'>) => void
onClose: () => void onClose: () => void
mealPlans: Array<{ id: string; label: string; description: string; inclusions: string[] }>
allInclusions: Array<{ id: string; label: string; icon: React.ElementType }>
}) { }) {
const [form, setForm] = useState<Omit<Tariff, 'id'>>( const [form, setForm] = useState<Omit<Tariff, 'id'>>(
tariff ? { ...tariff } : { ...EMPTY_TARIFF } tariff ? { ...tariff } : { ...EMPTY_TARIFF }
@@ -157,7 +160,7 @@ function TariffModal({
})) }))
const applyMealPlan = (plan: MealPlan) => { const applyMealPlan = (plan: MealPlan) => {
const mp = MEAL_PLANS.find(m => m.id === plan)! const mp = mealPlans.find(m => m.id === plan)!
setForm(prev => ({ setForm(prev => ({
...prev, ...prev,
mealPlan: plan, mealPlan: plan,
@@ -221,7 +224,7 @@ function TariffModal({
Тип питания Тип питания
</label> </label>
<div className="grid grid-cols-5 gap-2"> <div className="grid grid-cols-5 gap-2">
{MEAL_PLANS.map(mp => ( {mealPlans.map(mp => (
<button <button
key={mp.id} key={mp.id}
type="button" type="button"
@@ -248,7 +251,7 @@ function TariffModal({
Что включено Что включено
</label> </label>
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-4 gap-2">
{ALL_INCLUSIONS.map(inc => { {allInclusions.map(inc => {
const active = form.inclusions.includes(inc.id) const active = form.inclusions.includes(inc.id)
return ( return (
<button <button
@@ -375,6 +378,25 @@ export function TariffsPage() {
const [modal, setModal] = useState<'create' | Tariff | null>(null) const [modal, setModal] = useState<'create' | Tariff | null>(null)
const [deleteTarget, setDeleteTarget] = useState<Tariff | null>(null) const [deleteTarget, setDeleteTarget] = useState<Tariff | null>(null)
// Editable meal plans & inclusions
const [mealPlans, setMealPlans] = useState<Array<{ id: string; label: string; description: string; inclusions: string[] }>>(MEAL_PLANS)
const [allInclusions, setAllInclusions] = useState(ALL_INCLUSIONS)
const [showDicts, setShowDicts] = useState(false)
// Meal plan editing
const [editMeal, setEditMeal] = useState<{ id: string; label: string; description: string; inclusions: string[] } | null>(null)
const [newMealLabel, setNewMealLabel] = useState('')
const [newMealDesc, setNewMealDesc] = useState('')
const [addMealOpen, setAddMealOpen] = useState(false)
// Inclusion editing
const [editInc, setEditInc] = useState<{ id: string; label: string; icon: React.ElementType } | null>(null)
const [newIncLabel, setNewIncLabel] = useState('')
const [addIncOpen, setAddIncOpen] = useState(false)
const DEFAULT_MEAL_IDS = MEAL_PLANS.map(m => m.id)
const DEFAULT_INC_IDS = ALL_INCLUSIONS.map(i => i.id)
const save = (data: Omit<Tariff, 'id'>) => { const save = (data: Omit<Tariff, 'id'>) => {
if (typeof modal === 'object' && modal !== null) { if (typeof modal === 'object' && modal !== null) {
setTariffs(prev => prev.map(t => t.id === modal.id ? { ...data, id: modal.id } : t)) setTariffs(prev => prev.map(t => t.id === modal.id ? { ...data, id: modal.id } : t))
@@ -433,7 +455,7 @@ export function TariffsPage() {
{/* Tariff list */} {/* Tariff list */}
<div className="space-y-3"> <div className="space-y-3">
{tariffs.map(tariff => { {tariffs.map(tariff => {
const mp = MEAL_PLANS.find(m => m.id === tariff.mealPlan)! const mp = mealPlans.find(m => m.id === tariff.mealPlan) ?? mealPlans[0]
const modLabel = tariff.modifierValue === 0 const modLabel = tariff.modifierValue === 0
? 'Без надбавки' ? 'Без надбавки'
: `${tariff.modifierValue > 0 ? '+' : ''}${tariff.modifierValue}${tariff.modifierType === 'percent' ? '%' : ' ₽'}` : `${tariff.modifierValue > 0 ? '+' : ''}${tariff.modifierValue}${tariff.modifierType === 'percent' ? '%' : ' ₽'}`
@@ -469,7 +491,7 @@ export function TariffsPage() {
{/* Inclusions */} {/* Inclusions */}
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{tariff.inclusions.map(incId => { {tariff.inclusions.map(incId => {
const inc = ALL_INCLUSIONS.find(i => i.id === incId) const inc = allInclusions.find(i => i.id === incId)
if (!inc) return null if (!inc) return null
return ( return (
<span key={incId} className="flex items-center gap-1 px-2 py-0.5 rounded-lg text-xs bg-emerald-50 text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-800"> <span key={incId} className="flex items-center gap-1 px-2 py-0.5 rounded-lg text-xs bg-emerald-50 text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-400 border border-emerald-200 dark:border-emerald-800">
@@ -526,12 +548,133 @@ export function TariffsPage() {
})} })}
</div> </div>
{/* Справочники (meal plans + inclusions) */}
<div className="card overflow-hidden">
<button
onClick={() => setShowDicts(v => !v)}
className="w-full flex items-center justify-between px-5 py-4 text-left hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors"
>
<div>
<p className="font-semibold text-slate-900 dark:text-slate-100">Справочники</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Типы питания и комплектация номера</p>
</div>
{showDicts ? <ChevronUp size={16} className="text-slate-400" /> : <ChevronDown size={16} className="text-slate-400" />}
</button>
{showDicts && (
<div className="border-t border-slate-200 dark:border-slate-700 grid md:grid-cols-2 divide-y md:divide-y-0 md:divide-x divide-slate-200 dark:divide-slate-700">
{/* Meal plans */}
<div className="p-5 space-y-3">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-slate-700 dark:text-slate-300">Типы питания</p>
<button onClick={() => setAddMealOpen(true)} className="btn-secondary text-xs py-1 px-2.5 gap-1">
<Plus size={12} /> Добавить
</button>
</div>
<div className="space-y-1.5">
{mealPlans.map(mp => (
<div key={mp.id} className="flex items-center gap-2 py-1.5 px-2 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700/30 group">
{editMeal?.id === mp.id ? (
<>
<input className="input text-sm flex-1 py-1" value={editMeal.label}
onChange={e => setEditMeal(p => p ? {...p, label: e.target.value} : p)} />
<input className="input text-xs flex-1 py-1 text-slate-500" value={editMeal.description}
onChange={e => setEditMeal(p => p ? {...p, description: e.target.value} : p)} />
<button onClick={() => { setMealPlans(prev => prev.map(m => m.id === mp.id ? editMeal : m)); setEditMeal(null) }}
className="p-1 rounded hover:bg-emerald-100 text-emerald-600"><Check size={13} /></button>
<button onClick={() => setEditMeal(null)} className="p-1 rounded hover:bg-red-100 text-red-500"><X size={13} /></button>
</>
) : (
<>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-800 dark:text-slate-200 truncate">{mp.label}</p>
<p className="text-[11px] text-slate-400 truncate">{mp.description}</p>
</div>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button onClick={() => setEditMeal({...mp})} className="p-1 rounded hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-400 hover:text-slate-700"><Pencil size={12} /></button>
{!DEFAULT_MEAL_IDS.includes(mp.id) && (
<button onClick={() => setMealPlans(prev => prev.filter(m => m.id !== mp.id))} className="p-1 rounded hover:bg-red-100 text-slate-400 hover:text-red-500"><Trash2 size={12} /></button>
)}
</div>
</>
)}
</div>
))}
</div>
{addMealOpen && (
<div className="space-y-2 p-3 rounded-xl border border-brand-200 dark:border-brand-700 bg-brand-50/30 dark:bg-brand-900/10">
<input className="input text-sm" placeholder="Название (напр. Полный завтрак)" value={newMealLabel} onChange={e => setNewMealLabel(e.target.value)} />
<input className="input text-sm" placeholder="Описание (напр. FB — Full Board)" value={newMealDesc} onChange={e => setNewMealDesc(e.target.value)} />
<div className="flex gap-2">
<button onClick={() => setAddMealOpen(false)} className="btn-secondary flex-1 text-xs py-1.5 justify-center">Отмена</button>
<button disabled={!newMealLabel.trim()} onClick={() => {
setMealPlans(prev => [...prev, { id: `meal-${Date.now()}`, label: newMealLabel.trim(), description: newMealDesc.trim(), inclusions: [] }])
setNewMealLabel(''); setNewMealDesc(''); setAddMealOpen(false)
}} className="btn-primary flex-1 text-xs py-1.5 justify-center disabled:opacity-50">Добавить</button>
</div>
</div>
)}
</div>
{/* Inclusions */}
<div className="p-5 space-y-3">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-slate-700 dark:text-slate-300">Что включено в номер</p>
<button onClick={() => setAddIncOpen(true)} className="btn-secondary text-xs py-1 px-2.5 gap-1">
<Plus size={12} /> Добавить
</button>
</div>
<div className="space-y-1.5">
{allInclusions.map(inc => (
<div key={inc.id} className="flex items-center gap-2 py-1.5 px-2 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700/30 group">
{editInc?.id === inc.id ? (
<>
<input className="input text-sm flex-1 py-1" value={editInc.label}
onChange={e => setEditInc(p => p ? {...p, label: e.target.value} : p)} />
<button onClick={() => { setAllInclusions(prev => prev.map(i => i.id === inc.id ? editInc : i)); setEditInc(null) }}
className="p-1 rounded hover:bg-emerald-100 text-emerald-600"><Check size={13} /></button>
<button onClick={() => setEditInc(null)} className="p-1 rounded hover:bg-red-100 text-red-500"><X size={13} /></button>
</>
) : (
<>
<inc.icon size={14} className="text-slate-400 shrink-0" />
<span className="flex-1 text-sm text-slate-800 dark:text-slate-200">{inc.label}</span>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button onClick={() => setEditInc({...inc})} className="p-1 rounded hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-400 hover:text-slate-700"><Pencil size={12} /></button>
{!DEFAULT_INC_IDS.includes(inc.id) && (
<button onClick={() => setAllInclusions(prev => prev.filter(i => i.id !== inc.id))} className="p-1 rounded hover:bg-red-100 text-slate-400 hover:text-red-500"><Trash2 size={12} /></button>
)}
</div>
</>
)}
</div>
))}
</div>
{addIncOpen && (
<div className="space-y-2 p-3 rounded-xl border border-brand-200 dark:border-brand-700 bg-brand-50/30 dark:bg-brand-900/10">
<input className="input text-sm" placeholder="Название (напр. Тренажёрный зал)" value={newIncLabel} onChange={e => setNewIncLabel(e.target.value)} />
<div className="flex gap-2">
<button onClick={() => setAddIncOpen(false)} className="btn-secondary flex-1 text-xs py-1.5 justify-center">Отмена</button>
<button disabled={!newIncLabel.trim()} onClick={() => {
setAllInclusions(prev => [...prev, { id: `inc-${Date.now()}`, label: newIncLabel.trim(), icon: Package }])
setNewIncLabel(''); setAddIncOpen(false)
}} className="btn-primary flex-1 text-xs py-1.5 justify-center disabled:opacity-50">Добавить</button>
</div>
</div>
)}
</div>
</div>
)}
</div>
{/* Tariff Modal */} {/* Tariff Modal */}
{modal && ( {modal && (
<TariffModal <TariffModal
tariff={typeof modal === 'object' ? modal : undefined} tariff={typeof modal === 'object' ? modal : undefined}
onSave={save} onSave={save}
onClose={() => setModal(null)} onClose={() => setModal(null)}
mealPlans={mealPlans}
allInclusions={allInclusions}
/> />
)} )}