feat: configurable bed types via BedTypesContext

- Add BedTypesContext with default types + add/remove/update operations
- Replace hardcoded BED_ITEM_TYPES array in RoomModal with useBedTypes()
- Add "Справочник спальных мест" section to RoomCategoriesPage
  (expandable, same UX as amenities: add icon+label, rename, delete)
- BedItem.type changed from union to string for dynamic support

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-23 14:20:09 +03:00
parent 6bf1391890
commit 8c6cdfb1c7
5 changed files with 206 additions and 15 deletions

View File

@@ -3,6 +3,7 @@ import { Plus, Pencil, Trash2, ImagePlus, X as XIcon, Tag, ChevronRight, Chevron
import { cn } from '../lib/utils'
import type { RoomCategory } from '../types'
import { useAmenities } from '../contexts/AmenitiesContext'
import { useBedTypes } from '../contexts/BedTypesContext'
import { useAuth } from '../contexts/AuthContext'
import { api } from '../lib/api'
import type { CategoryApi } from '../lib/api'
@@ -362,6 +363,15 @@ export function RoomCategoriesPage() {
const [editingAmenity, setEditingAmenity] = useState<string | null>(null)
const [editAmenityValue, setEditAmenityValue] = useState('')
// Bed types directory state
const { bedTypes, addBedType, removeBedType, updateBedType } = useBedTypes()
const [showBedTypes, setShowBedTypes] = useState(false)
const [newBedLabel, setNewBedLabel] = useState('')
const [newBedIcon, setNewBedIcon] = useState('🛏')
const [editingBed, setEditingBed] = useState<string | null>(null)
const [editBedLabel, setEditBedLabel] = useState('')
const [editBedIcon, setEditBedIcon] = useState('')
useEffect(() => {
if (!slug) return
api.categories.list(slug)
@@ -599,6 +609,139 @@ export function RoomCategoriesPage() {
)}
</div>
{/* Справочник спальных мест */}
<div className="card overflow-hidden">
<button
onClick={() => setShowBedTypes(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">
{bedTypes.length} типов · используется при настройке номеров
</p>
</div>
{showBedTypes
? <ChevronUp size={16} className="text-slate-400" />
: <ChevronDown size={16} className="text-slate-400" />}
</button>
{showBedTypes && (
<div className="border-t border-slate-200 dark:border-slate-700 p-5 space-y-4">
{/* Add new */}
<div className="flex gap-2">
<select
className="input w-16 text-lg px-2 text-center"
value={newBedIcon}
onChange={e => setNewBedIcon(e.target.value)}
>
{['🛏', '🛋', '🪑', '🛌', '🚪'].map(ic => (
<option key={ic} value={ic}>{ic}</option>
))}
</select>
<input
type="text"
className="input flex-1 text-sm"
placeholder="Название (напр. Водяная кровать, Гамак...)"
value={newBedLabel}
onChange={e => setNewBedLabel(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') {
e.preventDefault()
const label = newBedLabel.trim()
if (!label) return
const value = label.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_а-яё]/gi, '')
+ '_' + Date.now().toString(36)
addBedType({ value, label, icon: newBedIcon })
setNewBedLabel('')
setNewBedIcon('🛏')
}
}}
/>
<button
onClick={() => {
const label = newBedLabel.trim()
if (!label) return
const value = label.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_а-яё]/gi, '')
+ '_' + Date.now().toString(36)
addBedType({ value, label, icon: newBedIcon })
setNewBedLabel('')
setNewBedIcon('🛏')
}}
disabled={!newBedLabel.trim()}
className="btn-primary px-4 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Plus size={15} />
</button>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">
{bedTypes.map(b => (
<div
key={b.value}
className="flex items-center gap-1.5 px-2.5 py-2 rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 group"
>
{editingBed === b.value ? (
<>
<select
className="text-base w-8 shrink-0 bg-transparent"
value={editBedIcon}
onChange={e => setEditBedIcon(e.target.value)}
>
{['🛏', '🛋', '🪑', '🛌', '🚪'].map(ic => (
<option key={ic} value={ic}>{ic}</option>
))}
</select>
<input
autoFocus
className="input text-xs flex-1 py-0.5 h-6"
value={editBedLabel}
onChange={e => setEditBedLabel(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') {
updateBedType(b.value, { label: editBedLabel, icon: editBedIcon })
setEditingBed(null)
}
if (e.key === 'Escape') setEditingBed(null)
}}
/>
<button
onClick={() => { updateBedType(b.value, { label: editBedLabel, icon: editBedIcon }); setEditingBed(null) }}
className="text-emerald-500 hover:text-emerald-600 shrink-0"
>
<Check size={13} />
</button>
<button onClick={() => setEditingBed(null)} className="text-slate-400 hover:text-slate-600 shrink-0">
<XIcon size={13} />
</button>
</>
) : (
<>
<span className="text-base shrink-0">{b.icon}</span>
<span className="text-xs text-slate-700 dark:text-slate-300 flex-1 truncate">{b.label}</span>
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
<button
onClick={() => { setEditingBed(b.value); setEditBedLabel(b.label); setEditBedIcon(b.icon) }}
className="p-0.5 rounded hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-400 hover:text-slate-700"
>
<Pencil size={11} />
</button>
<button
onClick={() => removeBedType(b.value)}
className="p-0.5 rounded hover:bg-red-100 dark:hover:bg-red-900/30 text-slate-400 hover:text-red-500"
>
<Trash2 size={11} />
</button>
</div>
</>
)}
</div>
))}
</div>
</div>
)}
</div>
{formOpen && (
<CategoryForm
category={editingCat}