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