Files
mkd/components/admin/UsersSection.tsx
2026-02-04 00:17:04 +05:00

747 lines
34 KiB
TypeScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<PortalUserRow[]>([]);
const [employees, setEmployees] = useState<Employee[]>([]);
const [districts, setDistricts] = useState<District[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showCreateForm, setShowCreateForm] = useState(false);
const [createMode, setCreateMode] = useState<CreateMode>('select');
const [editingId, setEditingId] = useState<number | null>(null);
const [formEmployeeId, setFormEmployeeId] = useState('');
const [formLogin, setFormLogin] = useState('');
const [formPassword, setFormPassword] = useState('');
const [formEmail, setFormEmail] = useState('');
const [formRole, setFormRole] = useState<UserRole>('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<string | null>(null);
const [permissionsModalUserId, setPermissionsModalUserId] = useState<number | null>(null);
const [permissionTemplates, setPermissionTemplates] = useState<PermissionTemplateRow[]>([]);
const [userPermissions, setUserPermissions] = useState<string[]>([]);
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 (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-primary-500" />
</div>
);
}
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h3 className="text-lg font-bold text-slate-800">Пользователи портала</h3>
<button
type="button"
onClick={() => {
setShowCreateForm(true);
setCreateError(null);
setCreateMode(employeesWithoutUser.length === 0 ? 'createNew' : 'select');
setFormEmployeeId(employeesWithoutUser[0]?.id || '');
setFormLogin('');
setFormPassword('');
setFormEmail('');
setFormRole('ENGINEER');
setEmpName('');
setEmpPosition('');
setEmpPhone('');
setEmpSalary('');
setEmpDistrictId('');
}}
className="flex items-center gap-2 px-4 py-2 bg-primary-600 text-white text-sm font-bold rounded-xl hover:bg-primary-700"
>
<UserPlus className="w-4 h-4" /> Создать пользователя
</button>
</div>
<p className="text-sm text-slate-500">
Пользователя можно создать только для сотрудника. Выберите существующего сотрудника или создайте нового тогда сразу создастся и пользователь портала.
</p>
{error && (
<div className="rounded-xl border border-red-200 bg-red-50 p-4 text-red-700 text-sm">{error}</div>
)}
{createError && (
<div className="rounded-xl border border-red-200 bg-red-50 p-4 text-red-700 text-sm">{createError}</div>
)}
{showCreateForm && (
<div className="rounded-xl border border-slate-200 bg-slate-50 p-6 space-y-4">
<h4 className="font-bold text-slate-800">Новый пользователь</h4>
<div className="flex gap-2 p-1 bg-slate-200/50 rounded-xl">
<button
type="button"
onClick={() => setCreateMode('select')}
className={`flex-1 flex items-center justify-center gap-2 py-2 px-4 rounded-lg text-sm font-medium transition-colors ${createMode === 'select' ? 'bg-white text-primary-600 shadow' : 'text-slate-600 hover:text-slate-800'}`}
>
<UsersRound className="w-4 h-4" /> Выбрать сотрудника
</button>
<button
type="button"
onClick={() => setCreateMode('createNew')}
className={`flex-1 flex items-center justify-center gap-2 py-2 px-4 rounded-lg text-sm font-medium transition-colors ${createMode === 'createNew' ? 'bg-white text-primary-600 shadow' : 'text-slate-600 hover:text-slate-800'}`}
>
<UserCog className="w-4 h-4" /> Создать сотрудника и пользователя
</button>
</div>
{createMode === 'select' ? (
employeesWithoutUser.length === 0 ? (
<div className="flex items-center gap-3 p-4 bg-amber-50 border border-amber-200 rounded-lg text-amber-800 text-sm">
<UsersRound className="w-5 h-5 flex-shrink-0" />
<div>
<p className="font-medium">Нет сотрудников без пользователя</p>
<p className="mt-1">Переключитесь на вкладку <strong>«Создать сотрудника и пользователя»</strong> так можно сразу добавить нового сотрудника и учётную запись портала.</p>
</div>
</div>
) : (
<>
<div>
<label className="block text-xs font-bold text-slate-600 mb-1">Сотрудник</label>
<select
value={formEmployeeId}
onChange={(e) => setFormEmployeeId(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value=""> Выберите </option>
{employeesWithoutUser.map((e) => (
<option key={e.id} value={e.id}>{e.name} ({e.position})</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-bold text-slate-600 mb-1">Логин</label>
<input
type="text"
value={formLogin}
onChange={(e) => setFormLogin(e.target.value)}
placeholder="Уникальный логин"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-600 mb-1">Пароль (необязательно при создании)</label>
<input
type="password"
value={formPassword}
onChange={(e) => setFormPassword(e.target.value)}
placeholder="Задайте пароль для входа"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-600 mb-1">Email (необязательно)</label>
<input
type="email"
value={formEmail}
onChange={(e) => setFormEmail(e.target.value)}
placeholder="email@example.com"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-600 mb-1">Роль</label>
<select
value={formRole}
onChange={(e) => setFormRole(e.target.value as UserRole)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
{(Object.keys(ROLE_NAMES) as UserRole[]).map((r) => (
<option key={r} value={r}>{ROLE_NAMES[r]}</option>
))}
</select>
</div>
<div className="flex gap-2">
<button
type="button"
onClick={handleCreate}
disabled={saving || !formEmployeeId || !formLogin.trim()}
className="px-4 py-2 bg-primary-600 text-white text-sm font-bold rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
{saving ? <Loader2 className="w-4 h-4 animate-spin inline" /> : 'Создать'}
</button>
<button
type="button"
onClick={() => { setShowCreateForm(false); setCreateError(null); }}
className="px-4 py-2 border border-slate-200 text-slate-700 text-sm font-medium rounded-lg hover:bg-slate-50"
>
Отмена
</button>
</div>
</>
)
) : (
<>
<p className="text-xs text-slate-600 font-medium">Данные сотрудника</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-bold text-slate-600 mb-1">ФИО</label>
<input
type="text"
value={empName}
onChange={(e) => setEmpName(e.target.value)}
placeholder="Иванов Иван Иванович"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-600 mb-1">Должность</label>
<input
type="text"
value={empPosition}
onChange={(e) => setEmpPosition(e.target.value)}
placeholder="Мастер участка"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-600 mb-1">Телефон</label>
<input
type="text"
value={empPhone}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-600 mb-1">Зарплата</label>
<input
type="text"
value={empSalary}
onChange={(e) => setEmpSalary(e.target.value)}
placeholder="50000"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
<div className="sm:col-span-2">
<label className="block text-xs font-bold text-slate-600 mb-1">Участок (необязательно)</label>
<select
value={empDistrictId}
onChange={(e) => setEmpDistrictId(e.target.value)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value=""> Не выбран </option>
{districts.map((d) => (
<option key={d.id} value={d.id}>{d.name}</option>
))}
</select>
</div>
</div>
<p className="text-xs text-slate-600 font-medium pt-2">Данные пользователя портала</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-bold text-slate-600 mb-1">Логин</label>
<input
type="text"
value={formLogin}
onChange={(e) => setFormLogin(e.target.value)}
placeholder="Уникальный логин"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-600 mb-1">Email (необязательно)</label>
<input
type="email"
value={formEmail}
onChange={(e) => setFormEmail(e.target.value)}
placeholder="email@example.com"
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-600 mb-1">Роль</label>
<select
value={formRole}
onChange={(e) => setFormRole(e.target.value as UserRole)}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
{(Object.keys(ROLE_NAMES) as UserRole[]).map((r) => (
<option key={r} value={r}>{ROLE_NAMES[r]}</option>
))}
</select>
</div>
</div>
<div className="flex gap-2 pt-2">
<button
type="button"
onClick={handleCreateEmployeeAndUser}
disabled={saving || !empName.trim() || !empPosition.trim() || !empPhone.trim() || !empSalary.trim() || !formLogin.trim()}
className="px-4 py-2 bg-primary-600 text-white text-sm font-bold rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
{saving ? <Loader2 className="w-4 h-4 animate-spin inline" /> : 'Создать сотрудника и пользователя'}
</button>
<button
type="button"
onClick={() => { setShowCreateForm(false); setCreateError(null); }}
className="px-4 py-2 border border-slate-200 text-slate-700 text-sm font-medium rounded-lg hover:bg-slate-50"
>
Отмена
</button>
</div>
</>
)}
</div>
)}
<div className="rounded-xl border border-slate-200 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-slate-50 border-b border-slate-200">
<tr>
<th className="text-left py-3 px-4 font-bold text-slate-700">Сотрудник</th>
<th className="text-left py-3 px-4 font-bold text-slate-700">Логин</th>
<th className="text-left py-3 px-4 font-bold text-slate-700">Роль</th>
<th className="text-right py-3 px-4 font-bold text-slate-700">Действия</th>
</tr>
</thead>
<tbody>
{portalUsers.length === 0 ? (
<tr>
<td colSpan={4} className="py-8 px-4 text-center text-slate-500">
Нет пользователей портала. Создайте первого выше.
</td>
</tr>
) : (
portalUsers.map((u) => (
<tr key={u.id} className="border-b border-slate-100 hover:bg-slate-50/50">
<td className="py-3 px-4">
<span className="font-medium text-slate-800">{u.employeeName}</span>
<span className="block text-xs text-slate-500">{u.employeePosition}</span>
</td>
<td className="py-3 px-4">{editingId === u.id ? (
<input
type="text"
defaultValue={u.login}
onBlur={(e) => {
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
)}</td>
<td className="py-3 px-4">
{editingId === u.id ? (
<select
defaultValue={u.role}
onChange={(e) => {
const r = e.target.value as UserRole;
if (r !== u.role) handleUpdate(u.id, { role: r });
setEditingId(null);
}}
className="px-2 py-1 border border-slate-200 rounded text-sm"
>
{(Object.keys(ROLE_NAMES) as UserRole[]).map((r) => (
<option key={r} value={r}>{ROLE_NAMES[r]}</option>
))}
</select>
) : (
ROLE_NAMES[u.role] || u.role
)}
</td>
<td className="py-3 px-4 text-right">
<button
type="button"
onClick={() => openPermissionsModal(u)}
className="p-2 text-slate-500 hover:text-primary-600"
title="Настроить права"
>
<Shield className="w-4 h-4" />
</button>
<button
type="button"
onClick={() => setEditingId(editingId === u.id ? null : u.id)}
className="p-2 text-slate-500 hover:text-primary-600"
title="Изменить"
>
<Pencil className="w-4 h-4" />
</button>
<button
type="button"
onClick={() => handleDelete(u.id)}
disabled={saving}
className="p-2 text-slate-500 hover:text-red-600"
title="Удалить"
>
<Trash2 className="w-4 h-4" />
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
{permissionsModalUserId != null && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-slate-900/80 backdrop-blur-md" onClick={() => setPermissionsModalUserId(null)}>
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg max-h-[90vh] overflow-y-auto p-6" onClick={(e) => e.stopPropagation()}>
<div className="flex justify-between items-center mb-4">
<h4 className="font-bold text-slate-800">Права доступа</h4>
<button type="button" onClick={() => setPermissionsModalUserId(null)} className="p-2 hover:bg-slate-100 rounded-full">
<X className="w-5 h-5 text-slate-500" />
</button>
</div>
<p className="text-sm text-slate-500 mb-4">Права задаются шаблоном или вручную. Роль можно подставить из шаблона. Пустой список права по роли.</p>
{templatesByPosition.length > 0 && currentModalUser && (
<div className="mb-4">
<label className="block text-xs font-bold text-slate-600 mb-2">Применить по должности</label>
<select
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
onChange={(e) => {
const id = Number(e.target.value);
const t = permissionTemplates.find((x) => x.id === id);
if (t && permissionsModalUserId != null) applyTemplateToUser(permissionsModalUserId, t);
}}
>
<option value=""> Выберите шаблон по должности </option>
{templatesByPosition.map((t) => (
<option key={t.id} value={t.id}>{t.forPosition} {t.name}</option>
))}
</select>
{currentModalUser && (
<p className="text-xs text-slate-500 mt-1">Должность пользователя: {currentModalUser.employeePosition}</p>
)}
</div>
)}
{permissionTemplates.length > 0 && (
<div className="mb-4">
<label className="block text-xs font-bold text-slate-600 mb-2">Применить шаблон</label>
<select
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
onChange={(e) => {
const id = Number(e.target.value);
const t = permissionTemplates.find((x) => x.id === id);
if (t && permissionsModalUserId != null) applyTemplateToUser(permissionsModalUserId, t);
}}
>
<option value=""> Выберите шаблон </option>
{permissionTemplates.map((t) => (
<option key={t.id} value={t.id}>{t.name}</option>
))}
</select>
</div>
)}
<div className="mb-4">
<label className="block text-xs font-bold text-slate-600 mb-1">Обзор данных (дашборды)</label>
<select
value={userScope}
onChange={(e) => setUserScope(e.target.value as 'all' | 'own_district')}
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
>
<option value="all">По всем участкам</option>
<option value="own_district">Только свой участок</option>
</select>
</div>
<div className="mb-4">
<label className="block text-xs font-bold text-slate-600 mb-2">Разделы и подразделы (вручную)</label>
<div className="space-y-1 max-h-[280px] overflow-y-auto pr-2">
{ALL_SECTION_KEYS.map((section) => {
const subs = SECTION_SUBS[section];
const sectionChecked = hasUserSection(section);
return (
<div key={section} className="rounded-lg border border-slate-200 bg-white overflow-hidden">
<label className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-slate-50">
<input
type="checkbox"
checked={sectionChecked}
onChange={() => toggleUserSection(section)}
className="sr-only"
/>
{sectionChecked ? (
<CheckSquare className="w-4 h-4 text-primary-600 shrink-0" />
) : (
<Square className="w-4 h-4 text-slate-400 shrink-0" />
)}
<span className="text-sm font-medium text-slate-800">{SECTION_LABELS[section] || section}</span>
{subs && subs.length > 0 && !sectionChecked && (
<span className="text-xs text-slate-500">
({subs.filter((s) => hasUserSub(section, s.id)).length} из {subs.length})
</span>
)}
</label>
{subs && subs.length > 0 && !sectionChecked && (
<div className="pl-6 pr-3 pb-2 pt-0 space-y-2 border-t border-slate-100">
{subs.map((sub) => (
<div key={sub.id} className="flex items-center justify-between gap-2 py-1">
<span className="text-xs text-slate-600">{sub.label}</span>
<select
value={getUserSubLevel(section, sub.id)}
onChange={(e) => setUserSubLevel(section, sub.id, e.target.value as PermissionLevel)}
className="text-xs border border-slate-200 rounded-lg px-2 py-1 bg-white min-w-[140px]"
>
<option value="none">Нет доступа</option>
<option value="read">{PERMISSION_LEVEL_LABELS.read}</option>
<option value="edit">{PERMISSION_LEVEL_LABELS.edit}</option>
<option value="own">{PERMISSION_LEVEL_LABELS.own}</option>
</select>
</div>
))}
</div>
)}
</div>
);
})}
</div>
</div>
<div className="flex gap-2">
<button
type="button"
onClick={saveUserPermissions}
disabled={saving}
className="px-4 py-2 bg-primary-600 text-white text-sm font-bold rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
{saving ? <Loader2 className="w-4 h-4 animate-spin inline" /> : 'Сохранить'}
</button>
<button
type="button"
onClick={() => setPermissionsModalUserId(null)}
className="px-4 py-2 border border-slate-200 text-slate-700 text-sm font-medium rounded-lg hover:bg-slate-50"
>
Отмена
</button>
</div>
</div>
</div>
)}
</div>
);
};