mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-08-01 08:45:44 +00:00
341 lines
14 KiB
JavaScript
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);
|