Files
2026-07-27 12:58:44 +03:00

341 lines
14 KiB
JavaScript

/**
* JS раздела «Пользователи». Drawer-форма (создание/правка), toggle активности,
* удаление — через общий Adminx.Ajax по контракту success/error. Подключается
* точечно из Users\Controller (ТЗ §4).
*/
(function (window, document) {
'use strict';
var Adminx = window.Adminx || (window.Adminx = {});
Adminx.Users = {
form: null,
init: function () {
this.form = document.getElementById('userForm');
if (!this.form) { return; }
var self = this;
document.addEventListener('click', function (e) {
if (e.target.closest('[data-user-new]')) { self.fillNew(); }
var editBtn = e.target.closest('[data-user-edit]');
if (editBtn) { self.fillEdit(editBtn.closest('tr')); }
var delBtn = e.target.closest('[data-user-delete]');
if (delBtn) { self.remove(delBtn.closest('tr')); }
var pwBtn = e.target.closest('[data-pw-toggle]');
if (pwBtn) { self.togglePw(pwBtn); }
if (e.target.closest('[data-pw-generate]')) { self.generatePassword(); }
if (e.target.closest('[data-pw-copy]')) { self.copyPassword(); }
});
document.addEventListener('change', function (e) {
var t = e.target.closest('[data-user-toggle]');
if (t) { self.toggle(t); }
});
this.form.addEventListener('input', function (e) {
var pw = e.target.closest('[data-pw-input]');
if (pw) { self.strength(pw.value || ''); }
if (e.target.matches('[name="name"], [name="email"]')) { self.updatePreview(); }
});
this.form.addEventListener('change', function (e) {
if (e.target.matches('[name="role"]')) { self.updatePreview(); }
});
this.form.addEventListener('submit', function (e) {
e.preventDefault();
self.submit();
});
},
base: function () { return this.form.getAttribute('data-base') || Adminx.base(); },
clearErrors: function () {
this.form.querySelectorAll('[data-error]').forEach(function (s) { s.textContent = ''; });
this.form.querySelectorAll('.input, .select').forEach(function (i) { i.classList.remove('is-invalid'); });
},
setField: function (name, value) {
var el = this.form.querySelector('[name="' + name + '"]');
if (el) { el.value = value; }
},
fillNew: function () {
this.clearErrors();
this.form.reset();
this.setField('id', '');
this.form.querySelector('[name="is_active"]').checked = true;
this.setSelfLock(false);
this.resetPassword();
document.getElementById('userDrawerTitle').textContent = 'Новый пользователь';
var hint = this.form.querySelector('[data-pass-hint]');
if (hint) { hint.textContent = 'Пароль обязателен для новой учётной записи.'; }
var dates = this.form.querySelector('[data-user-dates]');
if (dates) { dates.hidden = true; }
this.updatePreview();
},
fillEdit: function (row) {
if (!row) { return; }
this.clearErrors();
this.setField('id', row.dataset.id);
this.setField('name', row.dataset.name);
this.setField('email', row.dataset.email);
this.setField('login', row.dataset.login || '');
this.setField('phone', row.dataset.phone || '');
this.setField('role', row.dataset.role);
this.setField('password', '');
this.resetPassword();
this.form.querySelector('[name="is_active"]').checked = row.dataset.active === '1';
this.setSelfLock(row.hasAttribute('data-self'));
document.getElementById('userDrawerTitle').textContent = 'Изменение: ' + row.dataset.name;
var hint = this.form.querySelector('[data-pass-hint]');
if (hint) { hint.textContent = 'Оставьте пароль пустым, если его не нужно менять.'; }
var dates = this.form.querySelector('[data-user-dates]');
if (dates) { dates.hidden = false; }
var created = this.form.querySelector('[data-user-created]');
var updated = this.form.querySelector('[data-user-updated]');
if (created) { created.textContent = this.formatDate(row.dataset.created); }
if (updated) { updated.textContent = this.formatDate(row.dataset.updated); }
this.updatePreview(row.dataset.roleLabel || row.dataset.role);
},
setSelfLock: function (locked) {
var role = this.form.querySelector('[name="role"]');
var active = this.form.querySelector('[name="is_active"]');
var activeCard = active ? active.closest('.users-active-card') : null;
var roleHint = this.form.querySelector('[data-user-self-role-hint]');
var activeHint = this.form.querySelector('[data-user-active-hint]');
if (role) { role.disabled = !!locked; }
if (active) { active.disabled = !!locked; }
if (activeCard) { activeCard.classList.toggle('is-locked', !!locked); }
if (roleHint) { roleHint.hidden = !locked; }
if (activeHint) { activeHint.textContent = locked ? 'Собственную учётную запись отключить нельзя.' : 'Отключённый пользователь не сможет войти.'; }
},
formatDate: function (value) {
if (!value) { return '—'; }
return String(value).replace(/^([0-9]{4})-([0-9]{2})-([0-9]{2})/, '$3.$2.$1').slice(0, 16);
},
updatePreview: function (roleLabel) {
var name = this.form.querySelector('[name="name"]').value.trim() || 'Новый пользователь';
var email = this.form.querySelector('[name="email"]').value.trim() || 'Учётная запись ещё не сохранена';
var role = this.form.querySelector('[name="role"]');
var roleText = roleLabel || (role && role.options[role.selectedIndex] ? role.options[role.selectedIndex].text : 'Пользователь');
var avatar = this.form.querySelector('[data-user-preview-avatar]');
var nameNode = this.form.querySelector('[data-user-preview-name]');
var meta = this.form.querySelector('[data-user-preview-meta]');
var roleNode = this.form.querySelector('[data-user-preview-role]');
if (avatar) { avatar.textContent = name.slice(0, 2).toUpperCase(); }
if (nameNode) { nameNode.textContent = name; }
if (meta) { meta.textContent = email; }
if (roleNode) { roleNode.textContent = roleText; }
},
// --- Пароль: показ/скрытие + индикатор надёжности (компоненты AdminKit) ---
PW_HINT: 'Минимум 6 символов, желательно буквы разных регистров и цифры',
togglePw: function (btn) {
var wrap = btn.closest('.input-wrap');
var input = wrap ? wrap.querySelector('input') : null;
if (!input) { return; }
var show = input.type === 'password';
input.type = show ? 'text' : 'password';
var icon = btn.querySelector('i');
if (icon) { icon.className = show ? 'ti ti-eye-off' : 'ti ti-eye'; }
},
strength: function (value) {
var meter = this.form.querySelector('.pw-strength');
var hint = this.form.querySelector('[data-pw-hint]');
if (!meter) { return; }
if (value === '') {
meter.dataset.level = '';
if (hint) { hint.textContent = this.PW_HINT; }
return;
}
var s = 0;
if (value.length >= 6) { s++; }
if (value.length >= 10) { s++; }
if (/[a-z]/.test(value) && /[A-Z]/.test(value)) { s++; }
if (/\d/.test(value) && /[^A-Za-z0-9]/.test(value)) { s++; }
if (s < 1) { s = 1; }
if (s > 4) { s = 4; }
meter.dataset.level = String(s);
var labels = { 1: 'слабый', 2: 'средний', 3: 'хороший', 4: 'отличный' };
if (hint) { hint.textContent = 'Надёжность: ' + labels[s]; }
},
randomPassword: function (len) {
var lower = 'abcdefghijkmnpqrstuvwxyz';
var upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
var digit = '23456789';
var sym = '!@#$%^&*-_=+';
var all = lower + upper + digit + sym;
var rnd = function (max) {
var a = new Uint32Array(1);
(window.crypto || window.msCrypto).getRandomValues(a);
return a[0] % max;
};
var pick = function (set) { return set.charAt(rnd(set.length)); };
var out = [pick(lower), pick(upper), pick(digit), pick(sym)];
for (var i = out.length; i < len; i++) { out.push(pick(all)); }
for (var j = out.length - 1; j > 0; j--) { // перемешать
var k = rnd(j + 1);
var t = out[j]; out[j] = out[k]; out[k] = t;
}
return out.join('');
},
generatePassword: function () {
var input = this.form.querySelector('[data-pw-input]');
if (!input) { return; }
input.value = this.randomPassword(14);
input.type = 'text'; // показываем, чтобы можно было скопировать
var icon = this.form.querySelector('[data-pw-toggle] i');
if (icon) { icon.className = 'ti ti-eye-off'; }
this.strength(input.value);
Adminx.Toast.show('Пароль сгенерирован', 'success');
},
copyPassword: function () {
var input = this.form.querySelector('[data-pw-input]');
var val = input ? input.value : '';
if (!val) {
Adminx.Toast.show('Сначала введите или сгенерируйте пароль', 'error');
return;
}
var ok = function () { Adminx.Toast.show('Пароль скопирован в буфер', 'success'); };
var fail = function () { Adminx.Toast.show('Не удалось скопировать', 'error'); };
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(val).then(ok).catch(function () {
if (!Adminx.Users.legacyCopy(input)) { fail(); } else { ok(); }
});
} else if (Adminx.Users.legacyCopy(input)) {
ok();
} else {
fail();
}
},
legacyCopy: function (input) {
try {
var wasPassword = input.type === 'password';
if (wasPassword) { input.type = 'text'; }
input.focus();
input.select();
var done = document.execCommand('copy');
if (wasPassword) { input.type = 'password'; }
return done;
} catch (e) {
return false;
}
},
resetPassword: function () {
var meter = this.form.querySelector('.pw-strength');
if (meter) { meter.dataset.level = ''; }
var hint = this.form.querySelector('[data-pw-hint]');
if (hint) { hint.textContent = this.PW_HINT; }
var input = this.form.querySelector('[data-pw-input]');
if (input) { input.type = 'password'; }
var icon = this.form.querySelector('[data-pw-toggle] i');
if (icon) { icon.className = 'ti ti-eye'; }
},
submit: function () {
this.clearErrors();
var id = (this.form.querySelector('[name="id"]').value || '').trim();
var url = this.base() + '/users' + (id ? '/' + id : '');
var fd = new FormData(this.form);
var self = this;
Adminx.Loader.show();
Adminx.Ajax.post(url, fd).then(function (payload) {
Adminx.Loader.hide();
var d = payload.data || {};
if (d.success) {
Adminx.Ajax.handle(payload);
if (!d.redirect) { window.location.reload(); }
return;
}
// ошибки валидации по полям
var errors = d.errors || {};
Object.keys(errors).forEach(function (field) {
var span = self.form.querySelector('[data-error="' + field + '"]');
if (span) { span.textContent = errors[field]; }
var input = self.form.querySelector('[name="' + field + '"]');
if (input) { input.classList.add('is-invalid'); }
});
if (d.message) { Adminx.Toast.show(d.message, 'error'); }
}).catch(function () {
Adminx.Loader.hide();
Adminx.Toast.show('Ошибка сети', 'error');
});
},
toggle: function (checkbox) {
var row = checkbox.closest('tr');
var id = row.dataset.id;
//-- Нельзя отключать собственную учётную запись (дублирует запрет в контроллере).
if (row.hasAttribute('data-self')) {
checkbox.checked = true;
Adminx.Toast.show('Нельзя отключить свою учётную запись', 'error');
return;
}
Adminx.Ajax.post(this.base() + '/users/' + id + '/toggle').then(function (payload) {
var d = payload.data || {};
if (d.success) {
row.dataset.active = String((d.data && d.data.is_active) || 0);
Adminx.Toast.show(d.message, 'success');
} else {
checkbox.checked = !checkbox.checked;
Adminx.Toast.show(d.message || 'Не удалось', 'error');
}
}).catch(function () {
checkbox.checked = !checkbox.checked;
Adminx.Toast.show('Ошибка сети', 'error');
});
},
remove: function (row) {
var id = row.dataset.id;
var base = this.base();
Adminx.Confirm.open({
kind: 'error',
title: 'Удалить пользователя?',
message: '«' + row.dataset.name + '» (' + row.dataset.email + ') будет удалён без возможности восстановления.',
confirmLabel: 'Удалить',
confirmClass: 'btn-danger',
onConfirm: function () {
Adminx.Loader.show();
Adminx.Ajax.post(base + '/users/' + id + '/delete').then(function (payload) {
Adminx.Loader.hide();
var d = payload.data || {};
if (d.success) {
row.remove();
Adminx.Toast.show(d.message, 'success');
} else {
Adminx.Toast.show(d.message || 'Не удалось удалить', 'error');
}
}).catch(function () {
Adminx.Loader.hide();
Adminx.Toast.show('Ошибка сети', 'error');
});
}
});
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { Adminx.Users.init(); });
} else {
Adminx.Users.init();
}
})(window, document);