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:
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user