diff --git a/public/js/users.js b/public/js/users.js
new file mode 100644
index 0000000..fa739ce
--- /dev/null
+++ b/public/js/users.js
@@ -0,0 +1,127 @@
+async function init() {
+ if (!(await checkAuth())) return;
+ if (!isAdmin()) { location.href = 'journal.html'; return; }
+ buildSidebar('users');
+ document.getElementById('addUserBtn').addEventListener('click', () => openModal(null));
+ document.getElementById('uCancel').addEventListener('click', closeModal);
+ document.getElementById('uSave').addEventListener('click', saveUser);
+ document.getElementById('uRole').addEventListener('change', () => {
+ document.getElementById('uBranchesField').style.display = document.getElementById('uRole').value === 'admin' ? 'none' : '';
+ });
+ await loadUsers();
+}
+
+let branches = [];
+let editingId = null;
+
+async function loadUsers() {
+ const [uRes, bRes] = await Promise.all([
+ fetch(`${API}/api/users`, { headers: hdr() }),
+ fetch(`${API}/api/branches`, { headers: hdr() }),
+ ]);
+ if (!uRes.ok) { showToast('Ошибка загрузки пользователей'); return; }
+ branches = await bRes.json();
+ renderUsers(await uRes.json());
+}
+
+function renderUsers(users) {
+ const tbody = document.querySelector('#usersTable tbody');
+ const empty = document.getElementById('usersEmpty');
+ tbody.innerHTML = '';
+ empty.style.display = users.length ? 'none' : 'block';
+ const branchById = {};
+ branches.forEach(b => { branchById[b.id] = b.name; });
+ users.forEach(u => {
+ const tr = document.createElement('tr');
+ const branchNames = (u.branch_ids || []).map(id => branchById[id] || `#${id}`).join(', ') || '—';
+ const roleCls = u.role === 'admin' ? 'badge-admin' : 'badge-tutor';
+ const roleTxt = u.role === 'admin' ? 'Администратор' : 'Тутор';
+ const status = u.is_active ? 'Активен' : 'Заблокирован';
+ const delBtn = u.id === currentUser.id
+ ? ''
+ : ``;
+ tr.innerHTML = `
+
${u.id} |
+ ${esc(u.username)} |
+ ${esc(u.name || '')} |
+ ${roleTxt} |
+ ${esc(branchNames)} |
+ ${status} |
+
+
+ ${delBtn}
+ | `;
+ tbody.appendChild(tr);
+ });
+ tbody.querySelectorAll('[data-edit]').forEach(b => {
+ const userId = Number(b.dataset.edit);
+ const user = users.find(u => u.id === userId);
+ b.addEventListener('click', () => openModal(user));
+ });
+ tbody.querySelectorAll('[data-del]').forEach(b => b.addEventListener('click', () => delUser(Number(b.dataset.del))));
+}
+
+function openModal(user) {
+ editingId = user ? user.id : null;
+ document.getElementById('userModalTitle').textContent = user ? `Пользователь: ${user.username}` : 'Новый пользователь';
+ document.getElementById('uUsername').value = user ? user.username : '';
+ document.getElementById('uName').value = user ? (user.name || '') : '';
+ document.getElementById('uRole').value = user ? user.role : 'tutor';
+ document.getElementById('uPassword').value = '';
+ document.getElementById('uActive').checked = user ? user.is_active : true;
+ document.getElementById('pwdHint').textContent = user ? '(оставьте пустым, чтобы не менять)' : '(мин. 6 символов)';
+ document.getElementById('uRole').disabled = !!user && user.id === currentUser.id;
+ const sel = document.getElementById('uBranches');
+ sel.innerHTML = '';
+ branches.forEach(b => {
+ const o = document.createElement('option');
+ o.value = b.id;
+ o.textContent = b.name;
+ if (user && (user.branch_ids || []).includes(b.id)) o.selected = true;
+ sel.appendChild(o);
+ });
+ document.getElementById('uBranchesField').style.display = document.getElementById('uRole').value === 'admin' ? 'none' : '';
+ document.getElementById('userModal').classList.add('open');
+}
+
+function closeModal() {
+ document.getElementById('userModal').classList.remove('open');
+}
+
+async function saveUser() {
+ const username = document.getElementById('uUsername').value.trim();
+ const name = document.getElementById('uName').value.trim() || null;
+ const role = document.getElementById('uRole').value;
+ const password = document.getElementById('uPassword').value;
+ const is_active = document.getElementById('uActive').checked;
+ const branchSel = document.getElementById('uBranches');
+ const branch_ids = role === 'admin' ? [] : [...branchSel.selectedOptions].map(o => Number(o.value));
+ if (!username) { showToast('Укажите логин'); return; }
+ if (!editingId && password.length < 6) { showToast('Пароль должен быть не короче 6 символов'); return; }
+ const payload = { username, name, role, is_active, branch_ids };
+ if (password) payload.password = password;
+ const res = await fetch(editingId ? `${API}/api/users/${editingId}` : `${API}/api/users`, {
+ method: editingId ? 'PUT' : 'POST',
+ headers: hdrJson(),
+ body: JSON.stringify(payload),
+ });
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok) { showToast(data.error || 'Ошибка сохранения'); return; }
+ showToast('Сохранено');
+ closeModal();
+ loadUsers();
+}
+
+async function delUser(id) {
+ if (!confirm('Удалить пользователя?')) return;
+ const res = await fetch(`${API}/api/users/${id}`, { method: 'DELETE', headers: hdr() });
+ if (!res.ok) {
+ const data = await res.json().catch(() => ({}));
+ showToast(data.error || 'Ошибка удаления');
+ return;
+ }
+ showToast('Пользователь удалён');
+ loadUsers();
+}
+
+init();
\ No newline at end of file