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,8 @@
import { useState } from 'react'
import { Save, Building2, Bell, Shield, Globe, CreditCard, BedDouble, Mail, MessageSquare, Users, Plus, X as XIcon, Send, CheckCircle2, Copy } from 'lucide-react'
import { MOCK_HOTELS } from '../data/mockData'
import { useState, useEffect } from 'react'
import { Save, Building2, Bell, Shield, Globe, CreditCard, BedDouble, Mail, MessageSquare, Plus, X as XIcon, Send, CheckCircle2, Copy } from 'lucide-react'
import { api } from '../lib/api'
import { useAuth } from '../contexts/AuthContext'
import type { Hotel } from '../types'
import { cn, PLAN_COLORS, PLAN_LABELS } from '../lib/utils'
import { Badge } from '../components/ui/Badge'
import { useTheme } from '../contexts/ThemeContext'
@@ -26,11 +28,18 @@ function Toggle({ on, onChange }: { on: boolean; onChange: () => void }) {
}
export function SettingsPage() {
const { user } = useAuth()
const slug = user?.hotelSlug ?? ''
const [section, setSection] = useState('general')
const [saved, setSaved] = useState(false)
const hotel = MOCK_HOTELS[0]
const [hotel, setHotel] = useState<Hotel | null>(null)
const { theme, toggle } = useTheme()
useEffect(() => {
if (!slug) return
api.hotels.get(slug).then(setHotel).catch(console.error)
}, [slug])
const [calendarCompact, setCalendarCompact] = useState(
() => localStorage.getItem('calendarCompact') === 'true',
)
@@ -43,14 +52,26 @@ export function SettingsPage() {
}
const [form, setForm] = useState({
name: hotel.name,
address: hotel.address,
timezone: hotel.timezone,
currency: hotel.currency,
name: '',
address: '',
timezone: 'Europe/Moscow',
currency: 'RUB',
checkInTime: '14:00',
checkOutTime: '12:00',
})
useEffect(() => {
if (!hotel) return
setForm({
name: hotel.name ?? '',
address: hotel.address ?? '',
timezone: hotel.timezone ?? 'Europe/Moscow',
currency: hotel.currency ?? 'RUB',
checkInTime: hotel.checkInTime ?? '14:00',
checkOutTime: hotel.checkOutTime ?? '12:00',
})
}, [hotel])
// Booking / assignment settings
const [assignmentStrategy, setAssignmentStrategy] = useState<'spread' | 'together' | 'sequential' | 'manual'>('spread')
const [showBookingSource, setShowBookingSource] = useState(false)
@@ -126,7 +147,22 @@ export function SettingsPage() {
setTimeout(() => setCopiedToken(false), 2000)
}
const handleSave = () => {
const handleSave = async () => {
if (section === 'general') {
try {
const updated = await api.hotels.update(slug, {
name: form.name,
address: form.address,
timezone: form.timezone,
currency: form.currency,
checkInTime: form.checkInTime,
checkOutTime: form.checkOutTime,
})
setHotel(updated)
} catch (err) {
console.error('Failed to save hotel settings', err)
}
}
setSaved(true)
setTimeout(() => setSaved(false), 2000)
}
@@ -135,7 +171,7 @@ export function SettingsPage() {
<div className="p-4 md:p-6 max-w-4xl mx-auto">
<div className="mb-5">
<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">{hotel.name}</p>
<p className="text-sm text-slate-500 dark:text-slate-400">{hotel?.name ?? '...'}</p>
</div>
<div className="flex gap-5">
@@ -617,7 +653,7 @@ export function SettingsPage() {
<div>
<p className="text-sm text-slate-500 dark:text-slate-400">Текущий план</p>
<div className="flex items-center gap-2 mt-1">
<Badge className={PLAN_COLORS[hotel.plan]}>{PLAN_LABELS[hotel.plan]}</Badge>
<Badge className={PLAN_COLORS[hotel?.plan ?? 'starter']}>{PLAN_LABELS[hotel?.plan ?? 'starter']}</Badge>
</div>
</div>
<button className="btn-primary">Улучшить план</button>
@@ -630,11 +666,11 @@ export function SettingsPage() {
] as const).map(p => (
<div key={p.plan} className={cn(
'p-4 rounded-xl border-2 transition-all',
hotel.plan === p.plan ? 'border-brand-500 bg-brand-50/50 dark:bg-brand-900/10' : 'border-slate-200 dark:border-slate-600',
hotel?.plan === p.plan ? 'border-brand-500 bg-brand-50/50 dark:bg-brand-900/10' : 'border-slate-200 dark:border-slate-600',
)}>
<div className="flex items-center justify-between mb-2">
<Badge className={PLAN_COLORS[p.plan]}>{PLAN_LABELS[p.plan]}</Badge>
{hotel.plan === p.plan && <span className="text-xs text-brand-600 dark:text-brand-400 font-medium">Текущий</span>}
{hotel?.plan === p.plan && <span className="text-xs text-brand-600 dark:text-brand-400 font-medium">Текущий</span>}
</div>
<p className="text-lg font-bold text-slate-900 dark:text-slate-100 mb-3">{p.price}</p>
<ul className="space-y-1 text-xs text-slate-600 dark:text-slate-400">