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, Wrench, } 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' // ── Types ────────────────────────────────────────────────────────────────────── type StaffRole = 'hotel_admin' | 'hotel_manager' | 'housekeeper' | 'receptionist' | 'accountant' | 'security' | 'technician' interface StaffUser { id: string firstName: string lastName: string email: string phone?: string role: StaffRole position: string isActive: boolean createdAt: string lastLogin?: string avatarColor: string } interface ModulePermission { key: string label: string description?: string } interface RolePermissions { id: string name: string color: string isSystem: boolean permissions: Record } // ── Constants ────────────────────────────────────────────────────────────────── const ROLE_META: Record = { hotel_admin: { label: 'Сис. администратор', color: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300', icon: Shield }, hotel_manager: { label: 'Менеджер', color: 'bg-brand-100 text-brand-700 dark:bg-brand-900/30 dark:text-brand-300', icon: Shield }, receptionist: { label: 'Ресепшн', color: 'bg-sky-100 text-sky-700 dark:bg-sky-900/30 dark:text-sky-300', icon: UserIcon }, housekeeper: { label: 'Горничная', color: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300', icon: Sparkles }, accountant: { label: 'Бухгалтер', color: 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-300', icon: UserIcon }, security: { label: 'Охрана', color: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300', icon: Shield }, technician: { label: 'Тех. специалист', color: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300', icon: Wrench }, } const AVATAR_COLORS = [ '#4F46E5', '#059669', '#2563EB', '#7C3AED', '#DC2626', '#D97706', '#DB2777', '#0891B2', ] const DEFAULT_POSITIONS: Record = { hotel_admin: ['Генеральный директор', 'Владелец отеля', 'Управляющий директор'], hotel_manager: ['Управляющий', 'Заместитель управляющего', 'Менеджер смены'], receptionist: ['Старший администратор', 'Администратор', 'Ночной администратор'], housekeeper: ['Старшая горничная', 'Горничная', 'Уборщик'], accountant: ['Главный бухгалтер', 'Бухгалтер', 'Финансовый менеджер'], security: ['Начальник охраны', 'Охранник', 'Контролёр доступа'], technician: ['Технический специалист', 'Электрик', 'Сантехник', 'Инженер по оборудованию'], } const MODULE_GROUPS: { group: string; modules: ModulePermission[] }[] = [ { group: 'Фронт-офис', modules: [ { key: 'calendar', label: 'Календарь', description: 'Просмотр и управление сеткой бронирований' }, { key: 'bookings', label: 'Бронирования', description: 'Создание, редактирование, отмена броней' }, { key: 'guests', label: 'Гости', description: 'Карточки гостей, история, лояльность' }, { key: 'rooms', label: 'Номерной фонд', description: 'Номера, категории, тарифы' }, { key: 'availability',label: 'Доступность', description: 'Ограничения и стоп-продажи' }, ], }, { group: 'Операции', modules: [ { key: 'housekeeping', label: 'Горничные', description: 'Задания на уборку, статусы номеров' }, { key: 'maintenance', label: 'Техобслуживание', description: 'Заявки на ремонт и обслуживание' }, { key: 'room_service', label: 'Сервис в номер', description: 'Заказы еды и услуг в номер' }, { key: 'rental', label: 'Аренда объектов', description: 'Корт, баня, конференц-залы' }, ], }, { group: 'Продажи и финансы', modules: [ { key: 'pos', label: 'Кассовый терминал', description: 'POS, смены, оплаты' }, { key: 'tariffs', label: 'Тарифы', description: 'Тарифы и ценовые планы' }, { key: 'pricing', label: 'Динамические цены', description: 'Правила динамического ценообразования' }, { key: 'discounts', label: 'Скидки', description: 'Промокоды и скидочные кампании' }, { key: 'loyalty', label: 'Лояльность', description: 'Баллы, уровни, программа лояльности' }, { key: 'reports', label: 'Отчёты', description: 'Финансовые и операционные отчёты' }, ], }, { group: 'Маркетинг и каналы', modules: [ { key: 'channels', label: 'Каналы продаж', description: 'OTA, менеджер каналов' }, { key: 'reviews', label: 'Отзывы', description: 'Управление отзывами гостей' }, { key: 'website', label: 'Сайт и виджет', description: 'Сайт отеля и виджет бронирования' }, ], }, { group: 'Администрирование', modules: [ { key: 'users', label: 'Сотрудники', description: 'Управление пользователями и ролями' }, { key: 'documents', label: 'Документы', description: 'Шаблоны договоров и документов' }, { key: 'settings', label: 'Настройки', description: 'Настройки отеля и системы' }, ], }, ] const ALL_MODULE_KEYS = MODULE_GROUPS.flatMap(g => g.modules.map(m => m.key)) const allPerms = (v: boolean) => Object.fromEntries(ALL_MODULE_KEYS.map(k => [k, v])) const INITIAL_ROLE_PERMISSIONS: RolePermissions[] = [ { id: 'rp_hotel_admin', name: 'Сис. администратор', color: '#7C3AED', isSystem: true, permissions: allPerms(true), }, { id: 'rp_manager', name: 'Менеджер', color: '#4F46E5', isSystem: true, permissions: allPerms(true), }, { id: 'rp_receptionist', name: 'Ресепшн', color: '#0EA5E9', isSystem: true, permissions: { ...allPerms(false), calendar: true, bookings: true, guests: true, rooms: true, housekeeping: true, room_service: true, rental: true, pos: true, reviews: true, }, }, { id: 'rp_housekeeper', name: 'Горничная', color: '#10B981', isSystem: true, permissions: { ...allPerms(false), housekeeping: true, rooms: true, maintenance: true, }, }, { id: 'rp_accountant', name: 'Бухгалтер', color: '#7C3AED', isSystem: true, permissions: { ...allPerms(false), reports: true, pos: true, discounts: true, tariffs: true, pricing: true, loyalty: true, }, }, { id: 'rp_security', name: 'Охрана', color: '#F97316', isSystem: true, permissions: { ...allPerms(false), calendar: true, bookings: true, }, }, { id: 'rp_technician', name: 'Тех. специалист', color: '#D97706', isSystem: true, permissions: { ...allPerms(false), housekeeping: true, rooms: true, maintenance: true, }, }, ] // ── Helpers ──────────────────────────────────────────────────────────────────── function mapRole(r: string): StaffRole { if (r === 'housekeeper') return 'housekeeper' if (r === 'receptionist') return 'receptionist' if (r === 'accountant') return 'accountant' if (r === 'security') return 'security' if (r === 'technician') return 'technician' if (r === 'hotel_admin') return 'hotel_admin' return '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, phone: u.phone ?? '', role, position: u.position ?? DEFAULT_POSITIONS[role]?.[0] ?? '', isActive: u.active ?? true, createdAt: u.createdAt?.slice(0, 10) ?? '', avatarColor: AVATAR_COLORS[colorIndex], } } // ── User Modal ───────────────────────────────────────────────────────────────── function UserModal({ open, user, onClose, onSave, }: { open: boolean user?: StaffUser onClose: () => void onSave: (u: StaffUser, password?: string) => void }) { const [form, setForm] = useState({ firstName: user?.firstName ?? '', lastName: user?.lastName ?? '', email: user?.email ?? '', phone: user?.phone ?? '', role: (user?.role ?? 'receptionist') as StaffRole, position: user?.position ?? '', isActive: user?.isActive ?? true, avatarColor: user?.avatarColor ?? AVATAR_COLORS[Math.floor(Math.random() * AVATAR_COLORS.length)], }) const [password, setPassword] = useState('') const [showPass, setShowPass] = useState(false) const [errors, setErrors] = useState>({}) const isNew = !user const set = (k: K, v: typeof form[K]) => setForm(prev => ({ ...prev, [k]: v })) const validate = () => { const e: Record = {} if (!form.firstName.trim()) e.firstName = 'Введите имя' if (!form.lastName.trim()) e.lastName = 'Введите фамилию' if (!form.email.trim()) e.email = 'Введите email' if (!form.position.trim()) e.position = 'Введите должность' if (isNew && password.length < 6) e.password = 'Минимум 6 символов' setErrors(e) return Object.keys(e).length === 0 } const handleSave = () => { if (!validate()) return onSave({ ...form, id: user?.id ?? '', createdAt: user?.createdAt ?? new Date().toISOString().slice(0, 10), lastLogin: user?.lastLogin, }, password || undefined) } const suggestions = DEFAULT_POSITIONS[form.role] return ( } >
{/* Avatar color + name */}
{(form.firstName[0] ?? '?').toUpperCase()}
{AVATAR_COLORS.map(c => (
set('firstName', e.target.value)} /> {errors.firstName &&

{errors.firstName}

}
set('lastName', e.target.value)} /> {errors.lastName &&

{errors.lastName}

}
set('email', e.target.value)} /> {errors.email &&

{errors.email}

}
set('phone', e.target.value)} />
{/* Role */}
{form.role === 'hotel_admin' ? (
Сис. администратор — роль нельзя изменить
) : (
{(Object.entries(ROLE_META) as [StaffRole, typeof ROLE_META[StaffRole]][]) .filter(([id]) => id !== 'hotel_admin') .map(([id, meta]) => ( ))}
)}
{/* Position */}
set('position', e.target.value)} /> {errors.position &&

{errors.position}

}
{suggestions.map(s => ( ))}
{/* Password */}
setPassword(e.target.value)} />
{errors.password &&

{errors.password}

}
{/* Active toggle */}

Активный сотрудник

Неактивные не могут войти в систему

) } // ── Roles & Permissions Tab ──────────────────────────────────────────────────── function RolesTab() { // hotel_admin always has all permissions — excluded from the editable list const editableRoles = INITIAL_ROLE_PERMISSIONS.filter(r => r.id !== 'rp_hotel_admin') const [roles, setRoles] = useState(editableRoles) const [selectedRoleId, setSelectedRoleId] = useState(editableRoles[0].id) const [newRoleName, setNewRoleName] = useState('') const [addingRole, setAddingRole] = useState(false) const [deleteId, setDeleteId] = useState(null) const selectedRole = roles.find(r => r.id === selectedRoleId) ?? roles[0] const togglePerm = (key: string) => { setRoles(prev => prev.map(r => r.id === selectedRoleId ? { ...r, permissions: { ...r.permissions, [key]: !r.permissions[key] } } : r )) } const setAllPerms = (v: boolean) => { setRoles(prev => prev.map(r => r.id === selectedRoleId ? { ...r, permissions: allPerms(v) } : r )) } const createRole = () => { const name = newRoleName.trim() if (!name) return const colors = ['#EC4899', '#14B8A6', '#F59E0B', '#6366F1', '#84CC16'] const newRole: RolePermissions = { id: `rp_${Date.now()}`, name, color: colors[roles.length % colors.length], isSystem: false, permissions: allPerms(false), } setRoles(prev => [...prev, newRole]) setSelectedRoleId(newRole.id) setNewRoleName('') setAddingRole(false) } const deleteRole = (id: string) => { setRoles(prev => prev.filter(r => r.id !== id)) setDeleteId(null) if (selectedRoleId === id) { setSelectedRoleId(roles.find(r => r.id !== id)?.id ?? '') } } const activeCount = Object.values(selectedRole?.permissions ?? {}).filter(Boolean).length const totalCount = ALL_MODULE_KEYS.length return (
{/* Role list */}

Роли

{roles.map(role => (
setSelectedRoleId(role.id)} className={cn( 'flex items-center justify-between px-3 py-2.5 cursor-pointer transition-colors', selectedRoleId === role.id ? 'bg-brand-50 dark:bg-brand-900/20' : 'hover:bg-slate-50 dark:hover:bg-slate-700/30', )} >
{role.name}
{role.isSystem && ( )} {!role.isSystem && ( )}
))}
{/* Add role */}
{addingRole ? (
setNewRoleName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') createRole(); if (e.key === 'Escape') setAddingRole(false) }} />
) : ( )}
{/* Permissions editor */} {selectedRole && (
{/* Header */}

{selectedRole.name}

{activeCount} из {totalCount} разделов
{/* Module groups */}
{MODULE_GROUPS.map(group => (

{group.group}

{group.modules.map(mod => { const enabled = selectedRole.permissions[mod.key] ?? false return ( ) })}
))}
{selectedRole.isSystem && (

Системная роль — изменения применяются немедленно для всех сотрудников с этой ролью

)}
)} {/* Delete role confirm */} {deleteId && (

Удалить роль?

Сотрудники с этой ролью потеряют связанные права доступа. Действие нельзя отменить.

)}
) } // ── Page ─────────────────────────────────────────────────────────────────────── const PAGE_TABS = [ { id: 'staff', label: 'Сотрудники' }, { id: 'roles', label: 'Роли и права' }, ] as const type PageTab = typeof PAGE_TABS[number]['id'] export function UsersPage() { const { user: currentUser } = useAuth() const slug = currentUser?.hotelSlug ?? '' const [tab, setTab] = useState('staff') const [users, setUsers] = useState([]) const [search, setSearch] = useState('') const [roleFilter, setRoleFilter] = useState('all') const [modalOpen, setModalOpen] = useState(false) const [editing, setEditing] = useState() const [deleteId, setDeleteId] = useState(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()) || u.email.toLowerCase().includes(search.toLowerCase()) || u.position.toLowerCase().includes(search.toLowerCase()) const matchRole = roleFilter === 'all' || u.role === roleFilter return matchSearch && matchRole }) const openCreate = () => { setEditing(undefined); setModalOpen(true) } const openEdit = (u: StaffUser) => { setEditing(u); setModalOpen(true) } const handleSave = async (u: StaffUser, password?: string) => { const fullName = `${u.firstName} ${u.lastName}`.trim() const backendRoleMap: Record = { hotel_admin: 'hotel_admin', hotel_manager: 'manager', receptionist: 'receptionist', housekeeper: 'housekeeper', accountant: 'accountant', security: 'security', technician: 'technician', } const backendRole = backendRoleMap[u.role] ?? 'manager' try { if (!u.id) { const created = await api.users.create(slug, { email: u.email, name: fullName, password: password ?? '', role: backendRole, phone: u.phone || undefined, position: u.position || undefined, }) setUsers(prev => [...prev, toStaffUser(created)]) } else { const upd: Partial<{ name: string; email: string; password: string; role: string; phone: string; position: string; active: boolean }> = { name: fullName, email: u.email, role: backendRole, phone: u.phone || undefined, position: u.position || undefined, active: u.isActive, } 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 = 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 = { total: users.length, active: users.filter(u => u.isActive).length, } return (
{/* Header */}

Сотрудники

{stats.total} пользователей · {stats.active} активных

{tab === 'staff' && ( )}
{/* Tabs */}
{PAGE_TABS.map(t => ( ))}
{tab === 'staff' && ( <> {/* Stats */}
{(Object.entries(ROLE_META) as [StaffRole, typeof ROLE_META[StaffRole]][]).map(([role, meta]) => { const count = users.filter(u => u.role === role).length return (
setRoleFilter(roleFilter === role ? 'all' : role)}>
{meta.label}

{count}

) })}
{/* Filters */}
setSearch(e.target.value)} />
{(Object.entries(ROLE_META) as [StaffRole, typeof ROLE_META[StaffRole]][]).map(([role, meta]) => ( ))}
{/* Table */}
{filtered.map(u => { const meta = ROLE_META[u.role] return ( ) })}
Сотрудник Роль / Должность Контакты Последний вход Статус
{u.firstName[0]?.toUpperCase()}

{u.lastName} {u.firstName}

{u.email}

{meta.label}

{u.position}

{u.email}
{u.phone && (
{u.phone}
)}

{u.lastLogin ? new Date(u.lastLogin).toLocaleDateString('ru-RU') : '—'}

{u.isActive ? <> Активен : <> Неактивен}
{u.role !== 'hotel_admin' && ( )}
{filtered.length === 0 && (

Сотрудники не найдены

)}
{/* User modal */} setModalOpen(false)} onSave={handleSave} /> {/* Delete confirm */} {deleteId && (

Удалить сотрудника?

Сотрудник потеряет доступ к системе. Историю его действий можно будет просмотреть в журнале.

)} )} {tab === 'roles' && }
) }