- 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>
93 lines
3.8 KiB
TypeScript
93 lines
3.8 KiB
TypeScript
import { useState } from 'react'
|
||
import { format, addDays } from 'date-fns'
|
||
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 [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,
|
||
maintenance: MOCK_ROOMS.filter(r => r.status === 'maintenance' || r.status === 'blocked').length,
|
||
}
|
||
|
||
return (
|
||
<div className="p-4 md:p-6 space-y-4">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<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">
|
||
{editMode
|
||
? 'Режим редактирования — перетащите номера, загрузите план этажа'
|
||
: 'Нажмите на свободный номер чтобы создать бронирование'}
|
||
</p>
|
||
</div>
|
||
<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.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' },
|
||
].map(s => (
|
||
<div key={s.label} className="card px-4 py-3">
|
||
<p className="text-xs text-slate-500 dark:text-slate-400 mb-0.5">{s.label}</p>
|
||
<p className={`text-2xl font-bold ${s.color}`}>{s.value}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Floor map */}
|
||
<div className="card overflow-hidden p-0">
|
||
<FloorMap
|
||
rooms={MOCK_ROOMS}
|
||
bookings={bookings}
|
||
editMode={editMode}
|
||
onSelectRoom={editMode ? undefined : (id) => setCreateForRoom(id)}
|
||
/>
|
||
</div>
|
||
|
||
{/* Create booking modal */}
|
||
{createForRoom && (
|
||
<BookingModal
|
||
open
|
||
draft={{
|
||
roomId: createForRoom,
|
||
checkIn: format(new Date(), 'yyyy-MM-dd'),
|
||
checkOut: format(addDays(new Date(), 1), 'yyyy-MM-dd'),
|
||
}}
|
||
rooms={MOCK_ROOMS}
|
||
onClose={() => setCreateForRoom(null)}
|
||
onSave={(data) => {
|
||
setBookings(prev => [...prev, data as Booking])
|
||
setCreateForRoom(null)
|
||
}}
|
||
/>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|