Files
mkd/components/admin/UsersSection.tsx

747 lines
34 KiB
TypeScript
Raw Permalink Normal View History

2026-02-04 00:17:04 +05:00
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>
);
};