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,9 +1,12 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import {
Plus, Pencil, Trash2, Search, Shield, User as UserIcon,
Sparkles, Mail, Phone, Eye, EyeOff, CheckCircle2, AlertCircle,
Lock, Check, X as XIcon,
} from 'lucide-react'
import { api } from '../lib/api'
import { useAuth } from '../contexts/AuthContext'
import type { User } from '../types'
import { cn } from '../lib/utils'
import { Modal } from '../components/ui/Modal'
@@ -168,45 +171,30 @@ const INITIAL_ROLE_PERMISSIONS: RolePermissions[] = [
},
]
// ── Mock data ──────────────────────────────────────────────────────────────────
// ── Helpers ────────────────────────────────────────────────────────────────────
const MOCK_USERS: StaffUser[] = [
{
id: 'u1', firstName: 'Александр', lastName: 'Петров',
email: 'manager@grand-palace.ru', phone: '+7 (999) 123-45-67',
role: 'hotel_manager', position: 'Управляющий',
isActive: true, createdAt: '2025-01-15', lastLogin: '2026-03-11',
avatarColor: '#4F46E5',
},
{
id: 'u2', firstName: 'Мария', lastName: 'Сидорова',
email: 'reception@grand-palace.ru', phone: '+7 (999) 234-56-78',
role: 'receptionist', position: 'Старший администратор',
isActive: true, createdAt: '2025-03-01', lastLogin: '2026-03-10',
avatarColor: '#059669',
},
{
id: 'u3', firstName: 'Наталья', lastName: 'Козлова',
email: 'cleaner@grand-palace.ru',
role: 'housekeeper', position: 'Старшая горничная',
isActive: true, createdAt: '2025-04-10', lastLogin: '2026-03-11',
avatarColor: '#2563EB',
},
{
id: 'u4', firstName: 'Дмитрий', lastName: 'Волков',
email: 'night@grand-palace.ru', phone: '+7 (999) 345-67-89',
role: 'receptionist', position: 'Ночной администратор',
isActive: false, createdAt: '2025-06-20',
avatarColor: '#7C3AED',
},
{
id: 'u5', firstName: 'Елена', lastName: 'Морозова',
email: 'accounting@grand-palace.ru',
role: 'accountant', position: 'Главный бухгалтер',
isActive: true, createdAt: '2025-02-01', lastLogin: '2026-03-09',
avatarColor: '#D97706',
},
]
function mapRole(r: string): StaffRole {
return r === 'housekeeper' ? 'housekeeper' : 'hotel_manager'
}
function toStaffUser(u: User): StaffUser {
const parts = u.name.trim().split(' ')
const firstName = parts[0] ?? ''
const lastName = parts.slice(1).join(' ')
const role = mapRole(u.role)
const colorIndex = Math.abs(u.id.charCodeAt(0) + u.id.charCodeAt(1)) % AVATAR_COLORS.length
return {
id: u.id,
firstName,
lastName,
email: u.email,
role,
position: DEFAULT_POSITIONS[role]?.[0] ?? '',
isActive: true,
createdAt: u.createdAt?.slice(0, 10) ?? '',
avatarColor: AVATAR_COLORS[colorIndex],
}
}
// ── User Modal ─────────────────────────────────────────────────────────────────
@@ -216,7 +204,7 @@ function UserModal({
open: boolean
user?: StaffUser
onClose: () => void
onSave: (u: StaffUser) => void
onSave: (u: StaffUser, password?: string) => void
}) {
const [form, setForm] = useState({
firstName: user?.firstName ?? '',
@@ -251,10 +239,10 @@ function UserModal({
if (!validate()) return
onSave({
...form,
id: user?.id ?? `u-${Date.now()}`,
id: user?.id ?? '',
createdAt: user?.createdAt ?? new Date().toISOString().slice(0, 10),
lastLogin: user?.lastLogin,
})
}, password || undefined)
}
const suggestions = DEFAULT_POSITIONS[form.role]
@@ -682,14 +670,23 @@ const PAGE_TABS = [
type PageTab = typeof PAGE_TABS[number]['id']
export function UsersPage() {
const { user: currentUser } = useAuth()
const slug = currentUser?.hotelSlug ?? ''
const [tab, setTab] = useState<PageTab>('staff')
const [users, setUsers] = useState<StaffUser[]>(MOCK_USERS)
const [users, setUsers] = useState<StaffUser[]>([])
const [search, setSearch] = useState('')
const [roleFilter, setRoleFilter] = useState<StaffRole | 'all'>('all')
const [modalOpen, setModalOpen] = useState(false)
const [editing, setEditing] = useState<StaffUser | undefined>()
const [deleteId, setDeleteId] = useState<string | null>(null)
useEffect(() => {
if (!slug) return
api.users.list(slug)
.then(list => setUsers(list.map(toStaffUser)))
.catch(console.error)
}, [slug])
const filtered = users.filter(u => {
const matchSearch = !search ||
`${u.firstName} ${u.lastName}`.toLowerCase().includes(search.toLowerCase()) ||
@@ -702,18 +699,35 @@ export function UsersPage() {
const openCreate = () => { setEditing(undefined); setModalOpen(true) }
const openEdit = (u: StaffUser) => { setEditing(u); setModalOpen(true) }
const handleSave = (u: StaffUser) => {
setUsers(prev => {
const idx = prev.findIndex(x => x.id === u.id)
if (idx >= 0) { const next = [...prev]; next[idx] = u; return next }
return [...prev, u]
})
setModalOpen(false)
const handleSave = async (u: StaffUser, password?: string) => {
const fullName = `${u.firstName} ${u.lastName}`.trim()
const backendRole = u.role === 'housekeeper' ? 'housekeeper' : 'manager'
try {
if (!u.id) {
const created = await api.users.create(slug, {
email: u.email, name: fullName, password: password ?? '', role: backendRole,
})
setUsers(prev => [...prev, toStaffUser(created)])
} else {
const upd: Partial<{ name: string; email: string; password: string }> = { name: fullName, email: u.email }
if (password) upd.password = password
const updated = await api.users.update(slug, u.id, upd)
setUsers(prev => prev.map(x => x.id === u.id ? { ...toStaffUser(updated), lastLogin: x.lastLogin } : x))
}
setModalOpen(false)
} catch (err) {
console.error('Failed to save user', err)
}
}
const handleDelete = (id: string) => {
setUsers(prev => prev.filter(u => u.id !== id))
setDeleteId(null)
const handleDelete = async (id: string) => {
try {
await api.users.delete(slug, id)
setUsers(prev => prev.filter(u => u.id !== id))
setDeleteId(null)
} catch (err) {
console.error('Failed to delete user', err)
}
}
const stats = {