Files
hotelsync/src/pages/FloorMapPage.tsx
HotelSync fbf4b269e3 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>
2026-03-11 19:00:15 +03:00

93 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}