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>
This commit is contained in:
2026-03-17 14:21:45 +03:00
parent 57162c55c6
commit 55a34dc0dc
9 changed files with 277 additions and 109 deletions

View File

@@ -1,6 +1,7 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { RefreshCw, CheckCircle2, XCircle, Clock, Globe, AlertTriangle } from 'lucide-react'
import { MOCK_CHANNELS } from '../data/mockData'
import { api } from '../lib/api'
import { useAuth } from '../contexts/AuthContext'
import type { Channel, SyncStatus } from '../types'
import { cn } from '../lib/utils'
import { Badge } from '../components/ui/Badge'
@@ -35,25 +36,40 @@ function SyncStatusBadge({ status }: { status: SyncStatus }) {
}
export function ChannelsPage() {
const [channels, setChannels] = useState<Channel[]>(MOCK_CHANNELS)
const { user } = useAuth()
const slug = user?.hotelSlug ?? ''
const [channels, setChannels] = useState<Channel[]>([])
const [loading, setLoading] = useState(true)
const triggerSync = (id: string) => {
setChannels(prev => prev.map(c =>
c.id === id ? { ...c, lastSyncStatus: 'syncing' } : c,
))
setTimeout(() => {
setChannels(prev => prev.map(c =>
c.id === id
? { ...c, lastSyncStatus: 'success', lastSyncAt: new Date().toISOString(), bookingsImported: c.bookingsImported + Math.floor(Math.random() * 3) }
: c,
))
}, 2000)
useEffect(() => {
if (!slug) return
api.channels.list(slug)
.then(setChannels)
.catch(console.error)
.finally(() => setLoading(false))
}, [slug])
const triggerSync = async (id: string) => {
setChannels(prev => prev.map(c => c.id === id ? { ...c, lastSyncStatus: 'syncing' } : c))
try {
const { channel } = await api.channels.sync(slug, id)
setChannels(prev => prev.map(c => c.id === id ? { ...c, ...channel, lastSyncStatus: 'success' } : c))
} catch {
setChannels(prev => prev.map(c => c.id === id ? { ...c, lastSyncStatus: 'error' } : c))
}
}
const toggleChannel = (id: string) => {
setChannels(prev => prev.map(c =>
c.id === id ? { ...c, isEnabled: !c.isEnabled } : c,
))
const toggleChannel = async (id: string) => {
const ch = channels.find(c => c.id === id)
if (!ch) return
const next = !ch.isEnabled
setChannels(prev => prev.map(c => c.id === id ? { ...c, isEnabled: next } : c))
try {
const updated = await api.channels.update(slug, id, { enabled: next })
setChannels(prev => prev.map(c => c.id === id ? updated : c))
} catch {
setChannels(prev => prev.map(c => c.id === id ? { ...c, isEnabled: !next } : c))
}
}
const totalImported = channels.reduce((s, c) => s + c.bookingsImported, 0)
@@ -62,6 +78,10 @@ export function ChannelsPage() {
const internationalChannels = channels.filter(c => !RUSSIAN_CHANNELS.has(c.name))
const russianChannels = channels.filter(c => RUSSIAN_CHANNELS.has(c.name))
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-5">
{/* Header */}