Add floor map, rental module wiring, VIP tags and payment tracking in BookingModal

- New: Поэтажный план (/floor-map) — room status visualization per floor with color coding
  (occupied/arriving/departing/available/dirty/maintenance), click free room to create booking
- New: FloorMapModal — embeddable in BookingModal via "Поэтажный план" button near room selector
- New: RentalBookingModal — full-day toggle, hour start/end selects, conflict detection, price summary
- CalendarPage: rental objects/bookings shown in шахматка when rental module is active
- BookingsPage: "Аренда" tab with rental bookings table when rental module is active
- BookingModal: guest tags (VIP, Постоянный гость, etc.), payment method (cash/terminal),
  paidAmount input, debt/задолженность display, floor map quick-access button
- Sidebar: "План этажей" nav link added under Управление

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 17:08:10 +03:00
parent 74e0223c3c
commit 57c22be60d
11 changed files with 1432 additions and 297 deletions

View File

@@ -0,0 +1,84 @@
import { useState } from 'react'
import { format, addDays } from 'date-fns'
import { RefreshCw } from 'lucide-react'
import { MOCK_ROOMS, MOCK_BOOKINGS } from '../data/mockData'
import { FloorMap } from '../components/floormap/FloorMap'
import { BookingModal } from '../components/bookings/BookingModal'
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 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">
Обновлено: {format(lastUpdate, 'HH:mm')} · Нажмите на свободный номер чтобы создать бронирование
</p>
</div>
<button className="btn-secondary flex items-center gap-1.5">
<RefreshCw size={14} />
Обновить
</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">
<FloorMap
rooms={MOCK_ROOMS}
bookings={bookings}
onSelectRoom={handleSelectRoom}
/>
</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>
)
}