import React, { useEffect, useState } from 'react'; import { backendApi, PortalUserRow } from '../../services/apiClient'; import { Employee } from '../../types'; import { UserRole } from '../../types'; import { District } from '../../types'; import { UserPlus, Loader2, Pencil, Trash2, UsersRound, Shield, CheckSquare, Square, X, UserCog } from 'lucide-react'; import { PermissionTemplateRow } from '../../services/apiClient'; import { SECTION_LABELS, SECTION_SUBS, ALL_SECTION_KEYS, getPermissionLevel, permissionKey, PERMISSION_LEVEL_LABELS, type PermissionLevel, } from '../../constants/permissions'; import { ROLE_NAMES } from '../../constants/roleAccess'; type CreateMode = 'select' | 'createNew'; export const UsersSection: React.FC = () => { const [portalUsers, setPortalUsers] = useState([]); const [employees, setEmployees] = useState([]); const [districts, setDistricts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showCreateForm, setShowCreateForm] = useState(false); const [createMode, setCreateMode] = useState('select'); const [editingId, setEditingId] = useState(null); const [formEmployeeId, setFormEmployeeId] = useState(''); const [formLogin, setFormLogin] = useState(''); const [formPassword, setFormPassword] = useState(''); const [formEmail, setFormEmail] = useState(''); const [formRole, setFormRole] = useState('ENGINEER'); const [empName, setEmpName] = useState(''); const [empPosition, setEmpPosition] = useState(''); const [empPhone, setEmpPhone] = useState(''); const [empSalary, setEmpSalary] = useState(''); const [empDistrictId, setEmpDistrictId] = useState(''); const [saving, setSaving] = useState(false); const [createError, setCreateError] = useState(null); const [permissionsModalUserId, setPermissionsModalUserId] = useState(null); const [permissionTemplates, setPermissionTemplates] = useState([]); const [userPermissions, setUserPermissions] = useState([]); const [userScope, setUserScope] = useState<'all' | 'own_district'>('all'); const load = async () => { setLoading(true); setError(null); try { const [users, emps, dists, templates] = await Promise.all([ backendApi.getPortalUsers(), backendApi.getEmployees(), backendApi.getDistricts(), backendApi.getPermissionTemplates(), ]); setPortalUsers(users); setEmployees(emps); setDistricts(dists); setPermissionTemplates(templates); } catch (e: any) { setError(e?.message || 'Ошибка загрузки'); } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const openPermissionsModal = (u: PortalUserRow) => { setPermissionsModalUserId(u.id); setUserPermissions(Array.isArray(u.permissions) ? u.permissions : []); setUserScope(u.scope === 'own_district' ? 'own_district' : 'all'); }; const applyTemplateToUser = async (userId: number, t: PermissionTemplateRow) => { setSaving(true); try { await backendApi.updatePortalUser(userId, { permissions: (t.permissions?.length ?? 0) > 0 ? (t.permissions ?? []) : null, scope: t.scope || 'all', role: t.suggestedRole || undefined, }); setUserPermissions(t.permissions ?? []); setUserScope(t.scope || 'all'); await load(); } catch (e: any) { alert(e?.message || 'Ошибка применения шаблона'); } finally { setSaving(false); } }; const toggleUserSection = (section: string) => { setUserPermissions((prev) => { const hasSection = prev.includes(section); const subs = SECTION_SUBS[section]; const subKeyPrefixes = subs ? subs.flatMap((s) => { const base = `${section}_${s.id}`; return [base, `${base}:read`, `${base}:edit`, `${base}:own`]; }) : []; if (hasSection) { return prev.filter((p) => p !== section && !subKeyPrefixes.some((prefix) => p === prefix || p.startsWith(prefix + ':'))); } else { return prev.filter((p) => p !== section && !subKeyPrefixes.some((prefix) => p === prefix || p.startsWith(prefix + ':'))).concat([section]); } }); }; const setUserSubLevel = (section: string, subId: string, level: PermissionLevel) => { setUserPermissions((prev) => { const base = `${section}_${subId}`; const toRemove = prev.filter( (p) => p === base || p === `${base}:read` || p === `${base}:edit` || p === `${base}:own` ); const next = prev.filter((p) => !toRemove.includes(p)); if (level !== 'none') { next.push(permissionKey(section, subId, level)); } return next.filter((p) => p !== section); }); }; const hasUserSection = (section: string) => userPermissions.includes(section); const getUserSubLevel = (section: string, subId: string) => getPermissionLevel(userPermissions, section, subId); const hasUserSub = (section: string, subId: string) => getUserSubLevel(section, subId) !== 'none'; const saveUserPermissions = async () => { if (permissionsModalUserId == null) return; setSaving(true); try { await backendApi.updatePortalUser(permissionsModalUserId, { permissions: userPermissions.length > 0 ? userPermissions : null, scope: userScope, }); setPermissionsModalUserId(null); await load(); } catch (e: any) { alert(e?.message || 'Ошибка сохранения'); } finally { setSaving(false); } }; const currentModalUser = permissionsModalUserId != null ? portalUsers.find((u) => u.id === permissionsModalUserId) : null; const templatesByPosition = permissionTemplates.filter((t) => t.forPosition && t.forPosition.trim()); const employeeIdsWithUser = new Set(portalUsers.map((u) => u.employeeId)); const employeesWithoutUser = employees.filter((e) => !employeeIdsWithUser.has(e.id)); const handleCreate = async () => { if (!formEmployeeId || !formLogin.trim()) { setCreateError('Выберите сотрудника и укажите логин'); return; } setSaving(true); setCreateError(null); try { await backendApi.createPortalUser({ employeeId: formEmployeeId, login: formLogin.trim(), password: formPassword.trim() || undefined, email: formEmail.trim() || undefined, role: formRole, }); setShowCreateForm(false); setFormEmployeeId(''); setFormLogin(''); setFormPassword(''); setFormEmail(''); setFormRole('ENGINEER'); await load(); } catch (e: any) { setCreateError(e?.message || 'Ошибка создания'); } finally { setSaving(false); } }; const handleCreateEmployeeAndUser = async () => { if (!empName.trim() || !empPosition.trim() || !empPhone.trim() || !empSalary.trim() || !formLogin.trim()) { setCreateError('Заполните ФИО, должность, телефон, зарплату и логин'); return; } const salaryNum = parseFloat(empSalary.replace(/\s/g, '').replace(',', '.')); if (isNaN(salaryNum) || salaryNum < 0) { setCreateError('Укажите корректную зарплату'); return; } setSaving(true); setCreateError(null); try { await backendApi.createEmployeeWithUser({ name: empName.trim(), position: empPosition.trim(), phone: empPhone.trim(), status: 'active', salary: salaryNum, assignedDistrictId: empDistrictId.trim() || undefined, login: formLogin.trim(), email: formEmail.trim() || undefined, role: formRole, }); setShowCreateForm(false); setCreateMode('select'); setEmpName(''); setEmpPosition(''); setEmpPhone(''); setEmpSalary(''); setEmpDistrictId(''); setFormEmployeeId(''); setFormLogin(''); setFormEmail(''); setFormRole('ENGINEER'); await load(); } catch (e: any) { setCreateError(e?.message || 'Ошибка создания'); } finally { setSaving(false); } }; const handleUpdate = async (id: number, updates: { login?: string; email?: string; role?: string }) => { setSaving(true); setCreateError(null); try { await backendApi.updatePortalUser(id, updates); setEditingId(null); await load(); } catch (e: any) { setCreateError(e?.message || 'Ошибка сохранения'); } finally { setSaving(false); } }; const handleDelete = async (id: number) => { if (!confirm('Удалить пользователя портала? Сотрудник останется в системе.')) return; setSaving(true); setCreateError(null); try { await backendApi.deletePortalUser(id); await load(); } catch (e: any) { setCreateError(e?.message || 'Ошибка удаления'); } finally { setSaving(false); } }; if (loading) { return (
); } return (

Пользователи портала

Пользователя можно создать только для сотрудника. Выберите существующего сотрудника или создайте нового — тогда сразу создастся и пользователь портала.

{error && (
{error}
)} {createError && (
{createError}
)} {showCreateForm && (

Новый пользователь

{createMode === 'select' ? ( employeesWithoutUser.length === 0 ? (

Нет сотрудников без пользователя

Переключитесь на вкладку «Создать сотрудника и пользователя» — так можно сразу добавить нового сотрудника и учётную запись портала.

) : ( <>
setFormLogin(e.target.value)} placeholder="Уникальный логин" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
setFormPassword(e.target.value)} placeholder="Задайте пароль для входа" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
setFormEmail(e.target.value)} placeholder="email@example.com" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
) ) : ( <>

Данные сотрудника

setEmpName(e.target.value)} placeholder="Иванов Иван Иванович" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
setEmpPosition(e.target.value)} placeholder="Мастер участка" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
setEmpPhone(e.target.value)} placeholder="+7 900 123-45-67" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
setEmpSalary(e.target.value)} placeholder="50000" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />

Данные пользователя портала

setFormLogin(e.target.value)} placeholder="Уникальный логин" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
setFormEmail(e.target.value)} placeholder="email@example.com" className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
)}
)}
{portalUsers.length === 0 ? ( ) : ( portalUsers.map((u) => ( )) )}
Сотрудник Логин Роль Действия
Нет пользователей портала. Создайте первого выше.
{u.employeeName} {u.employeePosition} {editingId === u.id ? ( { const v = e.target.value.trim(); if (v && v !== u.login) handleUpdate(u.id, { login: v }); }} onKeyDown={(e) => e.key === 'Enter' && (e.target as HTMLInputElement).blur()} className="w-full max-w-[140px] px-2 py-1 border border-slate-200 rounded text-sm" /> ) : ( u.login )} {editingId === u.id ? ( ) : ( ROLE_NAMES[u.role] || u.role )}
{permissionsModalUserId != null && (
setPermissionsModalUserId(null)}>
e.stopPropagation()}>

Права доступа

Права задаются шаблоном или вручную. Роль можно подставить из шаблона. Пустой список — права по роли.

{templatesByPosition.length > 0 && currentModalUser && (
{currentModalUser && (

Должность пользователя: {currentModalUser.employeePosition}

)}
)} {permissionTemplates.length > 0 && (
)}
{ALL_SECTION_KEYS.map((section) => { const subs = SECTION_SUBS[section]; const sectionChecked = hasUserSection(section); return (
{subs && subs.length > 0 && !sectionChecked && (
{subs.map((sub) => (
{sub.label}
))}
)}
); })}
)}
); };