1242 lines
44 KiB
HTML
1242 lines
44 KiB
HTML
<!-- public/admin-profiles.html -->
|
||
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Профили пользователей - School CRM</title>
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
color: #333;
|
||
}
|
||
|
||
.admin-container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.admin-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
background: white;
|
||
padding: 20px;
|
||
border-radius: 15px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.admin-header h1 {
|
||
color: #333;
|
||
font-size: 24px;
|
||
}
|
||
|
||
.user-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
}
|
||
|
||
.user-name {
|
||
font-weight: 500;
|
||
color: #555;
|
||
}
|
||
|
||
.logout-btn {
|
||
background: #f44336;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.logout-btn:hover {
|
||
background: #d32f2f;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.admin-nav {
|
||
background: white;
|
||
border-radius: 15px;
|
||
margin-bottom: 20px;
|
||
padding: 15px;
|
||
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.nav-list {
|
||
display: flex;
|
||
gap: 10px;
|
||
list-style: none;
|
||
}
|
||
|
||
.nav-item a {
|
||
text-decoration: none;
|
||
color: #555;
|
||
padding: 10px 20px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.nav-item a:hover {
|
||
background: #f0f2f5;
|
||
color: #667eea;
|
||
}
|
||
|
||
.nav-item a.active {
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
|
||
.admin-content {
|
||
display: grid;
|
||
grid-template-columns: 1fr 350px;
|
||
gap: 20px;
|
||
}
|
||
|
||
.profiles-section {
|
||
background: white;
|
||
border-radius: 15px;
|
||
padding: 20px;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 15px;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.section-header h2 {
|
||
color: #333;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.search-box {
|
||
position: relative;
|
||
}
|
||
|
||
.search-box input {
|
||
padding: 10px 15px 10px 40px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
width: 300px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.search-box i {
|
||
position: absolute;
|
||
left: 15px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
color: #999;
|
||
}
|
||
|
||
.controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: #5a6fd8;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #f0f2f5;
|
||
color: #555;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #e4e6e9;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.btn-success {
|
||
background: #4CAF50;
|
||
color: white;
|
||
}
|
||
|
||
.btn-success:hover {
|
||
background: #45a049;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.profiles-table {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
th {
|
||
background: #f8f9fa;
|
||
padding: 15px;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
color: #555;
|
||
border-bottom: 2px solid #eee;
|
||
}
|
||
|
||
td {
|
||
padding: 15px;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
tr:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.user-cell {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.user-name {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.user-login {
|
||
color: #888;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.badge {
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.badge-admin {
|
||
background: #ffebee;
|
||
color: #f44336;
|
||
}
|
||
|
||
.badge-teacher {
|
||
background: #e8f5e9;
|
||
color: #4CAF50;
|
||
}
|
||
|
||
.badge-ldap {
|
||
background: #e3f2fd;
|
||
color: #2196F3;
|
||
}
|
||
|
||
.badge-local {
|
||
background: #f3e5f5;
|
||
color: #9C27B0;
|
||
}
|
||
|
||
.email-cell {
|
||
position: relative;
|
||
}
|
||
|
||
.email-address {
|
||
color: #2196F3;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.email-notification {
|
||
font-size: 12px;
|
||
color: #888;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.email-disabled {
|
||
color: #f44336;
|
||
}
|
||
|
||
.email-enabled {
|
||
color: #4CAF50;
|
||
}
|
||
|
||
.actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.action-btn {
|
||
padding: 6px;
|
||
border: none;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
color: white;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.action-btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 3px 10px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
.action-edit {
|
||
background: #2196F3;
|
||
}
|
||
|
||
.action-test {
|
||
background: #FF9800;
|
||
}
|
||
|
||
.action-email {
|
||
background: #4CAF50;
|
||
}
|
||
|
||
.sidebar {
|
||
background: white;
|
||
border-radius: 15px;
|
||
padding: 20px;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.sidebar h3 {
|
||
margin-bottom: 20px;
|
||
color: #333;
|
||
}
|
||
|
||
.profile-form {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
}
|
||
|
||
.form-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.form-group label {
|
||
font-weight: 500;
|
||
color: #555;
|
||
}
|
||
|
||
.form-group input[type="text"],
|
||
.form-group input[type="email"],
|
||
.form-group select {
|
||
padding: 10px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.form-group input[type="checkbox"] {
|
||
width: 18px;
|
||
height: 18px;
|
||
}
|
||
|
||
.checkbox-group {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.form-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.stats {
|
||
margin-top: 30px;
|
||
padding: 20px;
|
||
background: #f8f9fa;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.stat-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 10px 0;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.stat-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.stat-label {
|
||
color: #666;
|
||
}
|
||
|
||
.stat-value {
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0,0,0,0.5);
|
||
z-index: 1000;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal.show {
|
||
display: flex;
|
||
}
|
||
|
||
.modal-content {
|
||
background: white;
|
||
border-radius: 15px;
|
||
padding: 30px;
|
||
max-width: 500px;
|
||
width: 90%;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
color: #333;
|
||
}
|
||
|
||
.modal-close {
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: #999;
|
||
}
|
||
|
||
.bulk-form .form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.users-list {
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
padding: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.user-checkbox {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px;
|
||
border-radius: 5px;
|
||
}
|
||
|
||
.user-checkbox:hover {
|
||
background: #f0f2f5;
|
||
}
|
||
|
||
.loading {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: #666;
|
||
}
|
||
|
||
.toast {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
padding: 15px 20px;
|
||
border-radius: 8px;
|
||
color: white;
|
||
font-weight: 500;
|
||
z-index: 1001;
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.toast.show {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.toast-success {
|
||
background: #4CAF50;
|
||
}
|
||
|
||
.toast-error {
|
||
background: #f44336;
|
||
}
|
||
|
||
.toast-warning {
|
||
background: #FF9800;
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.admin-content {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.sidebar {
|
||
order: -1;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.admin-header {
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
text-align: center;
|
||
}
|
||
|
||
.search-box input {
|
||
width: 100%;
|
||
}
|
||
|
||
.controls {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.section-header {
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
}
|
||
|
||
.nav-list {
|
||
flex-wrap: wrap;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="admin-container">
|
||
<header class="admin-header">
|
||
<h1><i class="fas fa-users-cog"></i> Профили пользователей</h1>
|
||
<div class="user-info">
|
||
<span class="user-name" id="currentUser">Загрузка...</span>
|
||
<button class="logout-btn" onclick="logout()">
|
||
<i class="fas fa-sign-out-alt"></i> Выйти
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<nav class="admin-nav">
|
||
<ul class="nav-list">
|
||
<li class="nav-item">
|
||
<a href="/admin">
|
||
<i class="fas fa-chart-bar"></i> Статистика
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a href="/admin/users">
|
||
<i class="fas fa-users"></i> Пользователи
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a href="/admin/tasks">
|
||
<i class="fas fa-tasks"></i> Задачи
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a href="/admin/activity-logs">
|
||
<i class="fas fa-history"></i> Логи
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a href="/admin/notification-logs">
|
||
<i class="fas fa-bell"></i> Уведомления
|
||
</a>
|
||
</li>
|
||
<li class="nav-item">
|
||
<a href="/admin/profiles" class="active">
|
||
<i class="fas fa-id-card"></i> Профили
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
|
||
<main class="admin-content">
|
||
<section class="profiles-section">
|
||
<div class="section-header">
|
||
<h2>Список профилей пользователей</h2>
|
||
<div class="search-box">
|
||
<i class="fas fa-search"></i>
|
||
<input type="text" id="searchInput" placeholder="Поиск по имени или email..." onkeyup="filterProfiles()">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<button class="btn btn-primary" onclick="loadProfiles()">
|
||
<i class="fas fa-sync-alt"></i> Обновить
|
||
</button>
|
||
<button class="btn btn-secondary" onclick="showBulkModal()">
|
||
<i class="fas fa-users"></i> Массовое редактирование
|
||
</button>
|
||
<button class="btn btn-success" onclick="exportProfiles()">
|
||
<i class="fas fa-download"></i> Экспорт
|
||
</button>
|
||
</div>
|
||
|
||
<div class="profiles-table">
|
||
<table id="profilesTable">
|
||
<thead>
|
||
<tr>
|
||
<th><input type="checkbox" id="selectAll" onchange="toggleSelectAll(this)"></th>
|
||
<th>Пользователь</th>
|
||
<th>Роль</th>
|
||
<th>Email уведомлений</th>
|
||
<th>Статус</th>
|
||
<th>Последний вход</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="profilesBody">
|
||
<tr>
|
||
<td colspan="7" class="loading">
|
||
<i class="fas fa-spinner fa-spin"></i> Загрузка профилей...
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<aside class="sidebar">
|
||
<h3>Редактирование профиля</h3>
|
||
<div class="profile-form" id="profileForm">
|
||
<div class="loading">
|
||
<i class="fas fa-spinner fa-spin"></i> Загрузка формы...
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stats" id="statsSection">
|
||
<h3>Статистика</h3>
|
||
<div class="stat-item">
|
||
<span class="stat-label">Всего пользователей:</span>
|
||
<span class="stat-value" id="totalUsers">0</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-label">Email уведомления:</span>
|
||
<span class="stat-value" id="withEmail">0</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-label">Без email:</span>
|
||
<span class="stat-value" id="withoutEmail">0</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-label">Отключены:</span>
|
||
<span class="stat-value" id="disabled">0</span>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Модальное окно для массового редактирования -->
|
||
<div class="modal" id="bulkModal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3>Массовое редактирование</h3>
|
||
<button class="modal-close" onclick="closeBulkModal()">×</button>
|
||
</div>
|
||
<form class="bulk-form" id="bulkForm" onsubmit="bulkUpdate(event)">
|
||
<div class="form-group">
|
||
<label>Выбранные пользователи:</label>
|
||
<div class="users-list" id="selectedUsersList">
|
||
Пользователи не выбраны
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Включить email уведомления:</label>
|
||
<select id="bulkEmailNotifications" onchange="toggleBulkEmail()">
|
||
<option value="">Не изменять</option>
|
||
<option value="1">Включить</option>
|
||
<option value="0">Отключить</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" id="bulkEmailGroup" style="display: none;">
|
||
<label>Email для уведомлений:</label>
|
||
<input type="email" id="bulkNotificationEmail" placeholder="example@domain.com">
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="submit" class="btn btn-primary">Применить</button>
|
||
<button type="button" class="btn btn-secondary" onclick="closeBulkModal()">Отмена</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Уведомления -->
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<script>
|
||
let currentUser = null;
|
||
let profiles = [];
|
||
let selectedProfiles = new Set();
|
||
let editingProfileId = null;
|
||
|
||
// Проверка авторизации и загрузка данных
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
await checkAuth();
|
||
await loadCurrentUser();
|
||
await loadProfiles();
|
||
});
|
||
|
||
// Проверка авторизации
|
||
async function checkAuth() {
|
||
try {
|
||
const response = await fetch('/api/user');
|
||
if (!response.ok) {
|
||
window.location.href = '/';
|
||
return false;
|
||
}
|
||
const data = await response.json();
|
||
if (data.user.role !== 'admin') {
|
||
alert('Доступ запрещен! Требуются права администратора.');
|
||
window.location.href = '/';
|
||
return false;
|
||
}
|
||
return true;
|
||
} catch (error) {
|
||
console.error('Ошибка проверки авторизации:', error);
|
||
window.location.href = '/';
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Загрузка текущего пользователя
|
||
async function loadCurrentUser() {
|
||
try {
|
||
const response = await fetch('/api/user');
|
||
const data = await response.json();
|
||
currentUser = data.user;
|
||
document.getElementById('currentUser').textContent = currentUser.name;
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки пользователя:', error);
|
||
}
|
||
}
|
||
|
||
// Выход из системы
|
||
async function logout() {
|
||
try {
|
||
await fetch('/api/logout', { method: 'POST' });
|
||
window.location.href = '/';
|
||
} catch (error) {
|
||
console.error('Ошибка выхода:', error);
|
||
}
|
||
}
|
||
|
||
// Загрузка профилей
|
||
async function loadProfiles() {
|
||
try {
|
||
const response = await fetch('/admin/user-profiles');
|
||
if (!response.ok) throw new Error('Ошибка загрузки профилей');
|
||
|
||
profiles = await response.json();
|
||
renderProfiles();
|
||
updateStats();
|
||
loadProfileForm(null);
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки профилей:', error);
|
||
showToast('Ошибка загрузки профилей', 'error');
|
||
}
|
||
}
|
||
|
||
// Отображение профилей в таблице
|
||
function renderProfiles() {
|
||
const tbody = document.getElementById('profilesBody');
|
||
if (profiles.length === 0) {
|
||
tbody.innerHTML = `
|
||
<tr>
|
||
<td colspan="7" class="loading">
|
||
<i class="fas fa-user-slash"></i> Нет данных о профилях
|
||
</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = profiles.map(profile => `
|
||
<tr data-id="${profile.id}">
|
||
<td>
|
||
<input type="checkbox" class="profile-checkbox"
|
||
onchange="toggleProfileSelection(${profile.id}, this.checked)"
|
||
${selectedProfiles.has(profile.id) ? 'checked' : ''}>
|
||
</td>
|
||
<td>
|
||
<div class="user-cell">
|
||
<div class="user-avatar">${getInitials(profile.name)}</div>
|
||
<div>
|
||
<div class="user-name">${escapeHtml(profile.name)}</div>
|
||
<div class="user-login">${escapeHtml(profile.login)}</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<span class="badge badge-${profile.role}">${profile.role === 'admin' ? 'Админ' : 'Учитель'}</span>
|
||
<span class="badge badge-${profile.auth_type}">${profile.auth_type}</span>
|
||
</td>
|
||
<td class="email-cell">
|
||
<div class="email-address">
|
||
${profile.notification_email || profile.user_email || 'Не указан'}
|
||
</div>
|
||
<div class="email-notification ${profile.email_notifications ? 'email-enabled' : 'email-disabled'}">
|
||
<i class="fas fa-${profile.email_notifications ? 'check-circle' : 'times-circle'}"></i>
|
||
${profile.email_notifications ? 'Включены' : 'Отключены'}
|
||
</div>
|
||
</td>
|
||
<td>${profile.last_login ? formatDate(profile.last_login) : 'Никогда'}</td>
|
||
<td>
|
||
<div class="actions">
|
||
<button class="action-btn action-edit" onclick="editProfile(${profile.id})" title="Редактировать">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
<button class="action-btn action-test" onclick="testNotification(${profile.id})"
|
||
${!profile.email_notifications ? 'disabled style="opacity:0.5;cursor:not-allowed"' : ''}
|
||
title="Тестовое уведомление">
|
||
<i class="fas fa-paper-plane"></i>
|
||
</button>
|
||
<button class="action-btn action-email" onclick="toggleEmailNotifications(${profile.id}, ${!profile.email_notifications})"
|
||
title="${profile.email_notifications ? 'Отключить' : 'Включить'} email уведомления">
|
||
<i class="fas fa-${profile.email_notifications ? 'bell-slash' : 'bell'}"></i>
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
// Получение инициалов для аватара
|
||
function getInitials(name) {
|
||
return name.split(' ').map(word => word[0]).join('').toUpperCase().slice(0, 2);
|
||
}
|
||
|
||
// Экранирование HTML
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// Форматирование даты
|
||
function formatDate(dateString) {
|
||
const date = new Date(dateString);
|
||
return date.toLocaleDateString('ru-RU') + ' ' + date.toLocaleTimeString('ru-RU');
|
||
}
|
||
|
||
// Фильтрация профилей
|
||
function filterProfiles() {
|
||
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
||
const rows = document.querySelectorAll('#profilesTable tbody tr');
|
||
|
||
rows.forEach(row => {
|
||
if (row.classList.contains('loading')) return;
|
||
|
||
const text = row.textContent.toLowerCase();
|
||
row.style.display = text.includes(searchTerm) ? '' : 'none';
|
||
});
|
||
}
|
||
|
||
// Переключение выбора всех профилей
|
||
function toggleSelectAll(checkbox) {
|
||
const checkboxes = document.querySelectorAll('.profile-checkbox');
|
||
checkboxes.forEach(cb => {
|
||
cb.checked = checkbox.checked;
|
||
const profileId = parseInt(cb.closest('tr').dataset.id);
|
||
if (checkbox.checked) {
|
||
selectedProfiles.add(profileId);
|
||
} else {
|
||
selectedProfiles.delete(profileId);
|
||
}
|
||
});
|
||
updateSelectedUsersList();
|
||
}
|
||
|
||
// Переключение выбора отдельного профиля
|
||
function toggleProfileSelection(profileId, isSelected) {
|
||
if (isSelected) {
|
||
selectedProfiles.add(profileId);
|
||
} else {
|
||
selectedProfiles.delete(profileId);
|
||
}
|
||
updateSelectAllCheckbox();
|
||
updateSelectedUsersList();
|
||
}
|
||
|
||
// Обновление чекбокса "Выбрать все"
|
||
function updateSelectAllCheckbox() {
|
||
const selectAllCheckbox = document.getElementById('selectAll');
|
||
const allCheckboxes = document.querySelectorAll('.profile-checkbox');
|
||
const checkedCount = document.querySelectorAll('.profile-checkbox:checked').length;
|
||
|
||
selectAllCheckbox.checked = checkedCount === allCheckboxes.length;
|
||
selectAllCheckbox.indeterminate = checkedCount > 0 && checkedCount < allCheckboxes.length;
|
||
}
|
||
|
||
// Редактирование профиля
|
||
async function editProfile(profileId) {
|
||
editingProfileId = profileId;
|
||
await loadProfileForm(profileId);
|
||
}
|
||
|
||
// Загрузка формы редактирования
|
||
async function loadProfileForm(profileId) {
|
||
const formContainer = document.getElementById('profileForm');
|
||
|
||
if (!profileId) {
|
||
formContainer.innerHTML = `
|
||
<div class="loading">
|
||
<i class="fas fa-user"></i> Выберите профиль для редактирования
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/admin/user-profiles/${profileId}`);
|
||
if (!response.ok) throw new Error('Ошибка загрузки профиля');
|
||
|
||
const profile = await response.json();
|
||
|
||
formContainer.innerHTML = `
|
||
<div class="form-group">
|
||
<label>Имя пользователя:</label>
|
||
<input type="text" value="${escapeHtml(profile.name)}" disabled>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Логин:</label>
|
||
<input type="text" value="${escapeHtml(profile.login)}" disabled>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Email профиля:</label>
|
||
<input type="email" value="${escapeHtml(profile.user_email || '')}" disabled>
|
||
</div>
|
||
<div class="form-group checkbox-group">
|
||
<input type="checkbox" id="emailNotifications"
|
||
${profile.email_notifications ? 'checked' : ''}>
|
||
<label for="emailNotifications">Включить email уведомления</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Email для уведомлений:</label>
|
||
<input type="email" id="notificationEmail"
|
||
value="${escapeHtml(profile.notification_email || '')}"
|
||
placeholder="Оставьте пустым для использования email профиля">
|
||
</div>
|
||
<div class="form-group checkbox-group">
|
||
<input type="checkbox" id="telegramNotifications" disabled>
|
||
<label for="telegramNotifications">Telegram уведомления (в разработке)</label>
|
||
</div>
|
||
<div class="form-group checkbox-group">
|
||
<input type="checkbox" id="vkNotifications" disabled>
|
||
<label for="vkNotifications">VK уведомления (в разработке)</label>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-primary" onclick="saveProfileSettings()">
|
||
<i class="fas fa-save"></i> Сохранить
|
||
</button>
|
||
<button type="button" class="btn btn-secondary" onclick="testNotification(${profileId})"
|
||
${!profile.email_notifications ? 'disabled' : ''}>
|
||
<i class="fas fa-paper-plane"></i> Тест
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
// Отключаем email поле если уведомления выключены
|
||
const emailCheckbox = document.getElementById('emailNotifications');
|
||
const emailInput = document.getElementById('notificationEmail');
|
||
|
||
emailCheckbox.addEventListener('change', function() {
|
||
emailInput.disabled = !this.checked;
|
||
});
|
||
|
||
emailInput.disabled = !emailCheckbox.checked;
|
||
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки формы:', error);
|
||
formContainer.innerHTML = `
|
||
<div class="loading">
|
||
<i class="fas fa-exclamation-triangle"></i> Ошибка загрузки профиля
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// Сохранение настроек профиля
|
||
async function saveProfileSettings() {
|
||
if (!editingProfileId) return;
|
||
|
||
const emailNotifications = document.getElementById('emailNotifications').checked;
|
||
const notificationEmail = document.getElementById('notificationEmail').value;
|
||
|
||
// Валидация
|
||
if (emailNotifications && notificationEmail) {
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
if (!emailRegex.test(notificationEmail)) {
|
||
showToast('Неверный формат email', 'error');
|
||
return;
|
||
}
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/admin/user-profiles/${editingProfileId}/notification-settings`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
email_notifications: emailNotifications,
|
||
notification_email: notificationEmail,
|
||
telegram_notifications: false,
|
||
telegram_chat_id: '',
|
||
vk_notifications: false,
|
||
vk_user_id: ''
|
||
})
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Ошибка сохранения');
|
||
|
||
const data = await response.json();
|
||
showToast(data.message || 'Настройки сохранены', 'success');
|
||
loadProfiles(); // Обновляем список
|
||
} catch (error) {
|
||
console.error('Ошибка сохранения:', error);
|
||
showToast('Ошибка сохранения настроек', 'error');
|
||
}
|
||
}
|
||
|
||
// Тестовое уведомление
|
||
async function testNotification(profileId) {
|
||
if (!confirm('Отправить тестовое уведомление пользователю?')) return;
|
||
|
||
try {
|
||
const response = await fetch(`/admin/user-profiles/${profileId}/test-notification`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ notification_type: 'test' })
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Ошибка отправки');
|
||
|
||
const data = await response.json();
|
||
showToast(data.message || 'Уведомление отправлено', 'success');
|
||
} catch (error) {
|
||
console.error('Ошибка отправки:', error);
|
||
showToast('Ошибка отправки уведомления', 'error');
|
||
}
|
||
}
|
||
|
||
// Переключение email уведомлений
|
||
async function toggleEmailNotifications(profileId, enable) {
|
||
const profile = profiles.find(p => p.id === profileId);
|
||
if (!profile) return;
|
||
|
||
const action = enable ? 'включить' : 'отключить';
|
||
if (!confirm(`Вы уверены, что хотите ${action} email уведомления для пользователя ${profile.name}?`)) return;
|
||
|
||
try {
|
||
const response = await fetch(`/admin/user-profiles/${profileId}/notification-settings`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
email_notifications: enable,
|
||
notification_email: profile.notification_email || profile.user_email || '',
|
||
telegram_notifications: profile.telegram_notifications || false,
|
||
telegram_chat_id: profile.telegram_chat_id || '',
|
||
vk_notifications: profile.vk_notifications || false,
|
||
vk_user_id: profile.vk_user_id || ''
|
||
})
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Ошибка обновления');
|
||
|
||
const data = await response.json();
|
||
showToast(data.message || 'Настройки обновлены', 'success');
|
||
loadProfiles(); // Обновляем список
|
||
} catch (error) {
|
||
console.error('Ошибка обновления:', error);
|
||
showToast('Ошибка обновления настроек', 'error');
|
||
}
|
||
}
|
||
|
||
// Обновление статистики
|
||
function updateStats() {
|
||
const total = profiles.length;
|
||
const withEmail = profiles.filter(p => p.email_notifications && (p.notification_email || p.user_email)).length;
|
||
const withoutEmail = profiles.filter(p => !p.email_notifications || (!p.notification_email && !p.user_email)).length;
|
||
const disabled = profiles.filter(p => !p.email_notifications).length;
|
||
|
||
document.getElementById('totalUsers').textContent = total;
|
||
document.getElementById('withEmail').textContent = withEmail;
|
||
document.getElementById('withoutEmail').textContent = withoutEmail;
|
||
document.getElementById('disabled').textContent = disabled;
|
||
}
|
||
|
||
// Массовое редактирование
|
||
function showBulkModal() {
|
||
if (selectedProfiles.size === 0) {
|
||
showToast('Выберите пользователей для массового редактирования', 'warning');
|
||
return;
|
||
}
|
||
|
||
updateSelectedUsersList();
|
||
document.getElementById('bulkModal').classList.add('show');
|
||
}
|
||
|
||
function closeBulkModal() {
|
||
document.getElementById('bulkModal').classList.remove('show');
|
||
document.getElementById('bulkForm').reset();
|
||
document.getElementById('bulkEmailGroup').style.display = 'none';
|
||
}
|
||
|
||
function toggleBulkEmail() {
|
||
const select = document.getElementById('bulkEmailNotifications');
|
||
const emailGroup = document.getElementById('bulkEmailGroup');
|
||
emailGroup.style.display = select.value === '1' ? 'block' : 'none';
|
||
}
|
||
|
||
function updateSelectedUsersList() {
|
||
const listContainer = document.getElementById('selectedUsersList');
|
||
const selectedProfilesArray = profiles.filter(p => selectedProfiles.has(p.id));
|
||
|
||
if (selectedProfilesArray.length === 0) {
|
||
listContainer.innerHTML = '<div class="loading">Пользователи не выбраны</div>';
|
||
return;
|
||
}
|
||
|
||
listContainer.innerHTML = selectedProfilesArray.map(profile => `
|
||
<div class="user-checkbox">
|
||
<i class="fas fa-user"></i>
|
||
<span>${escapeHtml(profile.name)} (${escapeHtml(profile.login)})</span>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
async function bulkUpdate(event) {
|
||
event.preventDefault();
|
||
|
||
const emailNotifications = document.getElementById('bulkEmailNotifications').value;
|
||
const notificationEmail = document.getElementById('bulkNotificationEmail').value;
|
||
|
||
if (emailNotifications === '1' && !notificationEmail) {
|
||
showToast('Укажите email для уведомлений', 'error');
|
||
return;
|
||
}
|
||
|
||
if (emailNotifications === '1' && notificationEmail) {
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
if (!emailRegex.test(notificationEmail)) {
|
||
showToast('Неверный формат email', 'error');
|
||
return;
|
||
}
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/admin/bulk-email-settings', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
users: Array.from(selectedProfiles),
|
||
email_notifications: emailNotifications === '1',
|
||
notification_email: notificationEmail
|
||
})
|
||
});
|
||
|
||
if (!response.ok) throw new Error('Ошибка массового обновления');
|
||
|
||
const data = await response.json();
|
||
closeBulkModal();
|
||
showToast(`Успешно обновлено ${data.results.success} профилей`, 'success');
|
||
selectedProfiles.clear();
|
||
loadProfiles();
|
||
} catch (error) {
|
||
console.error('Ошибка массового обновления:', error);
|
||
showToast('Ошибка массового обновления', 'error');
|
||
}
|
||
}
|
||
|
||
// Экспорт профилей
|
||
async function exportProfiles() {
|
||
try {
|
||
const response = await fetch('/admin/export?dataType=users&format=csv');
|
||
if (!response.ok) throw new Error('Ошибка экспорта');
|
||
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `profiles_export_${new Date().toISOString().split('T')[0]}.csv`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
window.URL.revokeObjectURL(url);
|
||
} catch (error) {
|
||
console.error('Ошибка экспорта:', error);
|
||
showToast('Ошибка экспорта', 'error');
|
||
}
|
||
}
|
||
|
||
// Показать уведомление
|
||
function showToast(message, type = 'info') {
|
||
const toast = document.getElementById('toast');
|
||
toast.textContent = message;
|
||
toast.className = `toast toast-${type}`;
|
||
toast.classList.add('show');
|
||
|
||
setTimeout(() => {
|
||
toast.classList.remove('show');
|
||
}, 3000);
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |