Files
hotelsync/src/pages/FloorMapPage.tsx
HotelSync 55a34dc0dc Connect ChannelsPage, FloorMapPage, SettingsPage, UsersPage to real API
- ChannelsPage: load from API, toggle/sync call real endpoints; normalize
  enabled→isEnabled, lastSyncedAt→lastSyncAt, add displayName/mappings defaults
- FloorMapPage: load rooms+bookings from API; create booking via API
- SettingsPage: load hotel via GET /api/hotels/:slug; save general section
  via PATCH (name, address, timezone, currency, check_in/out times)
- UsersPage: load users from API; create/update/delete via API;
  map backend User (name/role) → StaffUser (firstName/lastName/StaffRole)
- api.ts: add hotels.get/update, users.delete, channel normalization,
  HotelPayload/toHotelPayload
- types/index.ts: extend Hotel with phone/checkInTime/checkOutTime/optional fields;
  add User.createdAt/updatedAt
- backend/routes/hotels.ts: extend PATCH to allow address/phone/check_in_time/check_out_time
- backend/migrations/004_hotels_contacts.sql: add address/phone/check_in/out_time to hotels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 14:21:45 +03:00

115 lines
4.5 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, useEffect } from 'react'
import { format, addDays } from 'date-fns'
import { Pencil, Check } from 'lucide-react'
import { api } from '../lib/api'
import { useAuth } from '../contexts/AuthContext'
import { FloorMap } from '../components/floormap/FloorMap'
import { BookingModal } from '../components/bookings/BookingModal'
import { cn } from '../lib/utils'
import type { Room, Booking } from '../types'
import type { BookingPayload } from '../lib/api'
export function FloorMapPage() {
const { user } = useAuth()
const slug = user?.hotelSlug ?? ''
const [rooms, setRooms] = useState<Room[]>([])
const [bookings, setBookings] = useState<Booking[]>([])
const [loading, setLoading] = useState(true)
const [createForRoom, setCreateForRoom] = useState<string | null>(null)
const [editMode, setEditMode] = useState(false)
useEffect(() => {
if (!slug) return
Promise.all([api.rooms.list(slug), api.bookings.list(slug)])
.then(([r, b]) => { setRooms(r); setBookings(b) })
.catch(console.error)
.finally(() => setLoading(false))
}, [slug])
const stats = {
total: rooms.length,
occupied: rooms.filter(r => r.status === 'occupied').length,
available: rooms.filter(r => r.status === 'available').length,
maintenance: rooms.filter(r => r.status === 'maintenance' || r.status === 'blocked').length,
}
if (loading) return (
<div className="p-6 flex items-center justify-center text-slate-400 text-sm">Загрузка...</div>
)
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={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={rooms}
onClose={() => setCreateForRoom(null)}
onSave={async (data) => {
try {
const created = await api.bookings.create(slug, data as BookingPayload)
setBookings(prev => [...prev, created])
} finally {
setCreateForRoom(null)
}
}}
/>
)}
</div>
)
}