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,5 +1,5 @@
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 type { RoomCategory } from '../types'
@@ -71,9 +71,21 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) {
const [photoIdx, setPhotoIdx] = useState(0)
const fileRef = useRef<HTMLInputElement>(null)
const [customInput, setCustomInput] = useState('')
const toggleAmenity = (a: string) =>
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>) => {
Array.from(e.target.files ?? []).forEach(file => {
const reader = new FileReader()
@@ -178,7 +190,7 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) {
</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">
{AMENITY_LIST.map(a => {
const sel = amenities.includes(a)
@@ -198,6 +210,37 @@ function CategoryForm({ category, onClose, onSave }: CategoryFormProps) {
</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>
</>