Floor map: drag & drop room editor with background image support

- Edit mode toggle on FloorMapPage (кнопка Редактировать / Готово)
- Rooms are draggable within the canvas in edit mode; positions persist to localStorage per floor
- Upload a floor plan image as background (per floor), stored as base64 in localStorage
- Reset button to clear custom layout back to auto-grid
- Grid-dot pattern background when no image is uploaded
- Hint bar in edit mode; drag cursor, hover ring on draggable cards
- View mode: click free room → create booking (unchanged)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 19:00:15 +03:00
parent 57c22be60d
commit fbf4b269e3
2 changed files with 385 additions and 170 deletions

View File

@@ -1,24 +1,21 @@
import { useState } from 'react'
import { format, addDays } from 'date-fns'
import { RefreshCw } from 'lucide-react'
import { Pencil, Check } from 'lucide-react'
import { MOCK_ROOMS, MOCK_BOOKINGS } from '../data/mockData'
import { FloorMap } from '../components/floormap/FloorMap'
import { BookingModal } from '../components/bookings/BookingModal'
import { cn } from '../lib/utils'
import type { Booking } from '../types'
export function FloorMapPage() {
const [bookings, setBookings] = useState<Booking[]>(MOCK_BOOKINGS)
const [createForRoom, setCreateForRoom] = useState<string | null>(null)
const [lastUpdate] = useState(new Date())
const handleSelectRoom = (id: string) => {
setCreateForRoom(id)
}
const [editMode, setEditMode] = useState(false)
const stats = {
total: MOCK_ROOMS.length,
occupied: MOCK_ROOMS.filter(r => r.status === 'occupied').length,
available: MOCK_ROOMS.filter(r => r.status === 'available').length,
total: MOCK_ROOMS.length,
occupied: MOCK_ROOMS.filter(r => r.status === 'occupied').length,
available: MOCK_ROOMS.filter(r => r.status === 'available').length,
maintenance: MOCK_ROOMS.filter(r => r.status === 'maintenance' || r.status === 'blocked').length,
}
@@ -29,19 +26,29 @@ export function FloorMapPage() {
<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">
Обновлено: {format(lastUpdate, 'HH:mm')} · Нажмите на свободный номер чтобы создать бронирование
{editMode
? 'Режим редактирования — перетащите номера, загрузите план этажа'
: 'Нажмите на свободный номер чтобы создать бронирование'}
</p>
</div>
<button className="btn-secondary flex items-center gap-1.5">
<RefreshCw size={14} />
Обновить
<button
onClick={() => setEditMode(v => !v)}
className={cn(
'flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium border-2 transition-all',
editMode
? 'bg-brand-600 border-brand-600 text-white shadow-md'
: 'bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-600 text-slate-700 dark:text-slate-300 hover:border-brand-400',
)}
>
{editMode ? <Check size={15} /> : <Pencil size={15} />}
{editMode ? 'Готово' : 'Редактировать'}
</button>
</div>
{/* Quick stats */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{[
{ label: 'Всего номеров', value: stats.total, color: 'text-slate-900 dark:text-slate-100' },
{ label: 'Всего номеров', value: stats.total, color: 'text-slate-900 dark:text-slate-100' },
{ label: 'Занято', value: stats.occupied, color: 'text-red-600 dark:text-red-400' },
{ label: 'Свободно', value: stats.available, color: 'text-emerald-600 dark:text-emerald-400' },
{ label: 'Ремонт/закрыт', value: stats.maintenance, color: 'text-slate-500 dark:text-slate-400' },
@@ -54,11 +61,12 @@ export function FloorMapPage() {
</div>
{/* Floor map */}
<div className="card">
<div className="card overflow-hidden p-0">
<FloorMap
rooms={MOCK_ROOMS}
bookings={bookings}
onSelectRoom={handleSelectRoom}
editMode={editMode}
onSelectRoom={editMode ? undefined : (id) => setCreateForRoom(id)}
/>
</div>
@@ -67,8 +75,8 @@ export function FloorMapPage() {
<BookingModal
open
draft={{
roomId: createForRoom,
checkIn: format(new Date(), 'yyyy-MM-dd'),
roomId: createForRoom,
checkIn: format(new Date(), 'yyyy-MM-dd'),
checkOut: format(addDays(new Date(), 1), 'yyyy-MM-dd'),
}}
rooms={MOCK_ROOMS}