Files

1009 lines
36 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Управление пользователями - CRUD Dialogs Demo</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.main-container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
border-radius: 15px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.header h1 {
color: #333;
margin: 0;
font-weight: 600;
}
.stats-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
transition: transform 0.3s;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-card .stat-number {
font-size: 2rem;
font-weight: bold;
color: #667eea;
}
.stat-card .stat-label {
color: #666;
margin-top: 5px;
}
.users-table-container {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.table-header h2 {
margin: 0;
color: #333;
}
.btn-create {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
padding: 10px 25px;
border-radius: 25px;
font-weight: 500;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-create:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.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: bold;
margin-right: 10px;
}
.user-info {
display: flex;
align-items: center;
}
.btn-action {
padding: 5px 10px;
margin: 0 3px;
border-radius: 8px;
transition: all 0.2s;
}
.btn-action:hover {
transform: scale(1.1);
}
.table {
margin-bottom: 0;
}
.table th {
border-top: none;
color: #666;
font-weight: 600;
text-transform: uppercase;
font-size: 0.85rem;
letter-spacing: 0.5px;
}
.table td {
vertical-align: middle;
}
.badge-role {
padding: 5px 12px;
border-radius: 20px;
font-size: 0.85rem;
}
.badge-admin {
background: #ffeaa7;
color: #d63031;
}
.badge-user {
background: #dfe6e9;
color: #636e72;
}
.badge-moderator {
background: #74b9ff;
color: #0984e3;
}
.search-box {
position: relative;
}
.search-box input {
padding-left: 35px;
border-radius: 25px;
border: 2px solid #e0e0e0;
transition: border-color 0.3s;
}
.search-box input:focus {
border-color: #667eea;
box-shadow: none;
}
.search-box i {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #999;
}
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
}
.showing-text {
color: #666;
font-size: 0.9rem;
}
.bulk-actions {
display: flex;
gap: 10px;
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255,255,255,0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
z-index: 10001;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.empty-state {
text-align: center;
padding: 50px;
color: #999;
}
.empty-state i {
font-size: 4rem;
margin-bottom: 20px;
}
</style>
</head>
<body>
<!-- Loading Overlay -->
<div class="loading-overlay" id="loadingOverlay" style="display: none;">
<div class="text-center">
<div class="spinner-border text-primary" role="status" style="width: 3rem; height: 3rem;">
<span class="visually-hidden">Загрузка...</span>
</div>
<p class="mt-2">Загрузка...</p>
</div>
</div>
<!-- Notification Container -->
<div id="notificationContainer"></div>
<!-- Main Container -->
<div class="main-container">
<!-- Header -->
<div class="header">
<div class="row align-items-center">
<div class="col-md-6">
<h1><i class="bi bi-people-fill"></i> Управление пользователями</h1>
<p class="text-muted mb-0">Демонстрация библиотеки CRUD Dialogs</p>
</div>
<div class="col-md-6 text-end">
<button class="btn btn-light me-2" onclick="refreshUsers()">
<i class="bi bi-arrow-clockwise"></i> Обновить
</button>
<button class="btn btn-create text-white" onclick="openCreateDialog()">
<i class="bi bi-plus-lg"></i> Добавить пользователя
</button>
</div>
</div>
</div>
<!-- Stats Cards -->
<div class="stats-cards">
<div class="stat-card">
<div class="stat-number" id="totalUsers">0</div>
<div class="stat-label">Всего пользователей</div>
</div>
<div class="stat-card">
<div class="stat-number" id="activeUsers">0</div>
<div class="stat-label">Активных</div>
</div>
<div class="stat-card">
<div class="stat-number" id="adminUsers">0</div>
<div class="stat-label">Администраторов</div>
</div>
<div class="stat-card">
<div class="stat-number" id="newUsers">0</div>
<div class="stat-label">Новых за неделю</div>
</div>
</div>
<!-- Users Table -->
<div class="users-table-container">
<div class="table-header">
<h2><i class="bi bi-table"></i> Список пользователей</h2>
<div class="d-flex gap-3">
<div class="search-box">
<i class="bi bi-search"></i>
<input type="text" class="form-control" id="searchInput"
placeholder="Поиск пользователей..." onkeyup="filterUsers()">
</div>
<select class="form-select" style="width: auto;" id="roleFilter" onchange="filterUsers()">
<option value="all">Все роли</option>
<option value="admin">Администратор</option>
<option value="user">Пользователь</option>
<option value="moderator">Модератор</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Пользователь</th>
<th>Email</th>
<th>Роль</th>
<th>Статус</th>
<th>Дата создания</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="usersTableBody">
<!-- Users will be loaded here -->
</tbody>
</table>
</div>
<div class="empty-state" id="emptyState" style="display: none;">
<i class="bi bi-people"></i>
<h4>Пользователи не найдены</h4>
<p>Создайте нового пользователя или измените параметры поиска</p>
</div>
<div class="pagination-container">
<div class="showing-text" id="showingText">
Показано 0 из 0 пользователей
</div>
<nav>
<ul class="pagination mb-0" id="pagination">
<!-- Pagination will be generated here -->
</ul>
</nav>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- DOM библиотека -->
<script type="text/javascript" src="dom.js"></script>
<!-- CRUD Dialogs библиотека -->
<script type="text/javascript" src="crud_d.js"></script>
<script>
// ============ Инициализация приложения ============
// Состояние приложения
let users = [];
let filteredUsers = [];
let currentPage = 1;
const usersPerPage = 10;
// Mock API с задержкой для реалистичности
const MockAPI = {
users: [],
init() {
// Генерируем тестовых пользователей
const firstNames = ['Анна', 'Иван', 'Мария', 'Петр', 'Елена', 'Алексей', 'Ольга', 'Дмитрий',
'Наталья', 'Сергей', 'Екатерина', 'Андрей', 'Татьяна', 'Максим', 'Юлия'];
const lastNames = ['Иванова', 'Петров', 'Сидорова', 'Козлов', 'Смирнова', 'Кузнецов',
'Попова', 'Васильев', 'Морозова', 'Новиков', 'Федорова', 'Зайцев'];
const roles = ['user', 'admin', 'moderator'];
const statuses = ['active', 'inactive', 'blocked'];
for (let i = 1; i <= 50; i++) {
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)];
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)];
const role = roles[Math.floor(Math.random() * roles.length)];
const status = statuses[Math.floor(Math.random() * statuses.length)];
this.users.push({
id: i,
first_name: firstName,
last_name: lastName,
email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}${i}@example.com`,
phone: `+7 (999) ${String(Math.floor(Math.random() * 900) + 100)}-${String(Math.floor(Math.random() * 90) + 10)}-${String(Math.floor(Math.random() * 90) + 10)}`,
role: role,
status: status,
description: `Описание пользователя ${firstName} ${lastName}`,
birth_date: `${String(Math.floor(Math.random() * 30) + 1970)}-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
created_at: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000).toISOString(),
address: `ул. ${['Ленина', 'Пушкина', 'Гагарина', 'Мира'][Math.floor(Math.random() * 4)]}, д. ${Math.floor(Math.random() * 100) + 1}`
});
}
},
async getUsers() {
await this.delay(500 + Math.random() * 1000);
return [...this.users];
},
async getUser(id) {
await this.delay(300 + Math.random() * 500);
const user = this.users.find(u => u.id === id);
if (!user) throw new Error('Пользователь не найден');
return {...user};
},
async createUser(data) {
await this.delay(500 + Math.random() * 1000);
const newUser = {
id: Math.max(...this.users.map(u => u.id)) + 1,
...data,
created_at: new Date().toISOString()
};
this.users.unshift(newUser);
return newUser;
},
async updateUser(id, data) {
await this.delay(500 + Math.random() * 1000);
const index = this.users.findIndex(u => u.id === id);
if (index === -1) throw new Error('Пользователь не найден');
this.users[index] = { ...this.users[index], ...data };
return this.users[index];
},
async deleteUser(id) {
await this.delay(500 + Math.random() * 1000);
const index = this.users.findIndex(u => u.id === id);
if (index === -1) throw new Error('Пользователь не найден');
this.users.splice(index, 1);
return { success: true };
},
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
};
// Переопределяем fetch для использования Mock API
window.fetch = async function(url, options = {}) {
const method = options.method || 'GET';
const body = options.body ? JSON.parse(options.body) : null;
// GET /api/users
if (url === '/api/users' && method === 'GET') {
const users = await MockAPI.getUsers();
return new Response(JSON.stringify(users), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
// GET /api/users/{id}
const getMatch = url.match(/\/api\/users\/(\d+)/);
if (getMatch && method === 'GET') {
try {
const user = await MockAPI.getUser(parseInt(getMatch[1]));
return new Response(JSON.stringify(user), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
return new Response(JSON.stringify({ message: error.message }), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});
}
}
// POST /api/users
if (url === '/api/users' && method === 'POST') {
const user = await MockAPI.createUser(body);
return new Response(JSON.stringify(user), {
status: 201,
headers: { 'Content-Type': 'application/json' }
});
}
// PUT /api/users/{id}
const putMatch = url.match(/\/api\/users\/(\d+)/);
if (putMatch && method === 'PUT') {
try {
const user = await MockAPI.updateUser(parseInt(putMatch[1]), body);
return new Response(JSON.stringify(user), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
return new Response(JSON.stringify({ message: error.message }), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});
}
}
// DELETE /api/users/{id}
const deleteMatch = url.match(/\/api\/users\/(\d+)/);
if (deleteMatch && method === 'DELETE') {
try {
await MockAPI.deleteUser(parseInt(deleteMatch[1]));
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
return new Response(JSON.stringify({ message: error.message }), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});
}
}
return new Response(JSON.stringify({ message: 'Not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});
};
// Инициализация Mock API
MockAPI.init();
// ============ Инициализация диалогов ============
// Создаем диалог для пользователей
const userDialog = crud_d.makeDialog('user_management', {
first_name: ['text', {
label: 'Имя',
required: true,
placeholder: 'Введите имя'
}, ''],
last_name: ['text', {
label: 'Фамилия',
required: true,
placeholder: 'Введите фамилию'
}, ''],
email: ['email', {
label: 'Email',
required: true,
placeholder: 'user@example.com'
}, ''],
phone: ['text', {
label: 'Телефон',
placeholder: '+7 (999) 123-45-67'
}, ''],
role: ['select', {
label: 'Роль',
required: true,
options: [
{ value: 'user', label: '👤 Пользователь' },
{ value: 'moderator', label: '🛡️ Модератор' },
{ value: 'admin', label: '👑 Администратор' }
]
}, 'user'],
status: ['select', {
label: 'Статус',
required: true,
options: [
{ value: 'active', label: '🟢 Активен' },
{ value: 'inactive', label: '🟡 Неактивен' },
{ value: 'blocked', label: '🔴 Заблокирован' }
]
}, 'active'],
birth_date: ['date', {
label: 'Дата рождения'
}, ''],
address: ['text', {
label: 'Адрес',
placeholder: 'ул. Примерная, д. 1'
}, ''],
description: ['textarea', {
label: 'Описание',
rows: 3,
placeholder: 'Дополнительная информация о пользователе'
}, '']
}, {
width: 'lg',
categories: {
'Основная информация': [
['first_name', 'last_name'],
['email', 'phone'],
['role', 'status']
],
'Дополнительно': [
['birth_date'],
['address'],
['description']
]
}
});
// Устанавливаем эндпоинты
userDialog
.setCreateEndpoint('/api/users')
.setGetEndpoint('/api/users/{id}')
.setUpdateEndpoint('/api/users/{id}')
.setDeleteEndpoint('/api/users/{id}');
// ============ Функции для работы с таблицей ============
function loadUsers() {
showLoading();
fetch('/api/users')
.then(response => response.json())
.then(data => {
users = data;
applyFilters();
updateStats();
hideLoading();
})
.catch(error => {
console.error('Ошибка загрузки:', error);
showNotification('Ошибка загрузки пользователей', 'danger');
hideLoading();
});
}
function applyFilters() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const roleFilter = document.getElementById('roleFilter').value;
filteredUsers = users.filter(user => {
const matchesSearch =
user.first_name.toLowerCase().includes(searchTerm) ||
user.last_name.toLowerCase().includes(searchTerm) ||
user.email.toLowerCase().includes(searchTerm) ||
user.phone.toLowerCase().includes(searchTerm);
const matchesRole = roleFilter === 'all' || user.role === roleFilter;
return matchesSearch && matchesRole;
});
currentPage = 1;
renderTable();
}
function filterUsers() {
applyFilters();
}
function renderTable() {
const tbody = document.getElementById('usersTableBody');
const emptyState = document.getElementById('emptyState');
const table = document.querySelector('.table');
if (filteredUsers.length === 0) {
tbody.innerHTML = '';
emptyState.style.display = 'block';
table.style.display = 'none';
updatePagination(0);
return;
}
emptyState.style.display = 'none';
table.style.display = 'table';
const startIndex = (currentPage - 1) * usersPerPage;
const endIndex = startIndex + usersPerPage;
const pageUsers = filteredUsers.slice(startIndex, endIndex);
tbody.innerHTML = pageUsers.map(user => `
<tr>
<td>
<div class="user-info">
<div class="user-avatar">
${user.first_name[0]}${user.last_name[0]}
</div>
<div>
<strong>${user.first_name} ${user.last_name}</strong>
<br>
<small class="text-muted">ID: ${user.id}</small>
</div>
</div>
</td>
<td>
<i class="bi bi-envelope"></i> ${user.email}
${user.phone ? `<br><i class="bi bi-phone"></i> ${user.phone}` : ''}
</td>
<td>
<span class="badge-role badge-${user.role}">
${getRoleLabel(user.role)}
</span>
</td>
<td>
<span class="badge bg-${getStatusColor(user.status)}">
${getStatusLabel(user.status)}
</span>
</td>
<td>
<small>${formatDate(user.created_at)}</small>
</td>
<td>
<button class="btn btn-sm btn-outline-primary btn-action"
onclick="openEditDialog(${user.id})"
title="Редактировать">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-info btn-action"
onclick="viewUser(${user.id})"
title="Просмотр">
<i class="bi bi-eye"></i>
</button>
</td>
</tr>
`).join('');
updatePagination(filteredUsers.length);
updateShowingText(startIndex + 1, Math.min(endIndex, filteredUsers.length), filteredUsers.length);
}
function updatePagination(totalItems) {
const totalPages = Math.ceil(totalItems / usersPerPage);
const pagination = document.getElementById('pagination');
if (totalPages <= 1) {
pagination.innerHTML = '';
return;
}
let html = '';
html += `
<li class="page-item ${currentPage === 1 ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="changePage(${currentPage - 1})">
<i class="bi bi-chevron-left"></i>
</a>
</li>
`;
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= currentPage - 1 && i <= currentPage + 1)) {
html += `
<li class="page-item ${currentPage === i ? 'active' : ''}">
<a class="page-link" href="#" onclick="changePage(${i})">${i}</a>
</li>
`;
} else if (i === currentPage - 2 || i === currentPage + 2) {
html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
}
}
html += `
<li class="page-item ${currentPage === totalPages ? 'disabled' : ''}">
<a class="page-link" href="#" onclick="changePage(${currentPage + 1})">
<i class="bi bi-chevron-right"></i>
</a>
</li>
`;
pagination.innerHTML = html;
}
function updateShowingText(start, end, total) {
document.getElementById('showingText').textContent =
`Показано ${start}-${end} из ${total} пользователей`;
}
function changePage(page) {
const totalPages = Math.ceil(filteredUsers.length / usersPerPage);
if (page < 1 || page > totalPages) return;
currentPage = page;
renderTable();
window.scrollTo({ top: document.querySelector('.users-table-container').offsetTop - 20, behavior: 'smooth' });
}
function updateStats() {
document.getElementById('totalUsers').textContent = users.length;
document.getElementById('activeUsers').textContent = users.filter(u => u.status === 'active').length;
document.getElementById('adminUsers').textContent = users.filter(u => u.role === 'admin').length;
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
document.getElementById('newUsers').textContent =
users.filter(u => new Date(u.created_at) > new Date(weekAgo)).length;
// Анимация чисел
document.querySelectorAll('.stat-number').forEach(el => {
el.style.transform = 'scale(1.1)';
setTimeout(() => el.style.transform = 'scale(1)', 200);
});
}
// ============ Диалоговые операции ============
function openCreateDialog() {
userDialog.openCreate('Новый пользователь')
.onSuccess(function(result) {
showNotification(`Пользователь ${result.first_name} ${result.last_name} успешно создан!`, 'success');
loadUsers();
})
.onError(function(error) {
showNotification('Ошибка создания: ' + error.message, 'danger');
});
}
function openEditDialog(userId) {
userDialog.openUpdate(userId, 'Редактирование пользователя')
.onSuccess(function(result) {
showNotification(`Пользователь ${result.first_name} ${result.last_name} обновлен!`, 'success');
loadUsers();
})
.onDelete(function(id) {
showNotification('Пользователь удален!', 'warning');
loadUsers();
})
.onError(function(error) {
showNotification('Ошибка: ' + error.message, 'danger');
});
}
function viewUser(userId) {
const user = users.find(u => u.id === userId);
if (!user) return;
// Создаем модальное окно для просмотра
const modalHtml = `
<div class="modal fade" id="viewUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-person-circle"></i>
${user.first_name} ${user.last_name}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="text-center mb-4">
<div class="user-avatar mx-auto" style="width: 80px; height: 80px; font-size: 2rem;">
${user.first_name[0]}${user.last_name[0]}
</div>
</div>
<table class="table table-borderless">
<tr><td><strong>ID:</strong></td><td>${user.id}</td></tr>
<tr><td><strong>Email:</strong></td><td>${user.email}</td></tr>
<tr><td><strong>Телефон:</strong></td><td>${user.phone || 'Не указан'}</td></tr>
<tr><td><strong>Роль:</strong></td><td>${getRoleLabel(user.role)}</td></tr>
<tr><td><strong>Статус:</strong></td><td>${getStatusLabel(user.status)}</td></tr>
<tr><td><strong>Дата рождения:</strong></td><td>${user.birth_date || 'Не указана'}</td></tr>
<tr><td><strong>Адрес:</strong></td><td>${user.address || 'Не указан'}</td></tr>
<tr><td><strong>Описание:</strong></td><td>${user.description || 'Нет описания'}</td></tr>
<tr><td><strong>Создан:</strong></td><td>${formatDate(user.created_at)}</td></tr>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Закрыть
</button>
<button type="button" class="btn btn-primary"
onclick="bootstrap.Modal.getInstance(document.getElementById('viewUserModal')).hide(); openEditDialog(${user.id})">
<i class="bi bi-pencil"></i> Редактировать
</button>
</div>
</div>
</div>
</div>
`;
// Удаляем предыдущее окно просмотра если есть
const existingModal = document.getElementById('viewUserModal');
if (existingModal) existingModal.remove();
// Добавляем и показываем
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = new bootstrap.Modal(document.getElementById('viewUserModal'));
modal.show();
// Удаляем после закрытия
document.getElementById('viewUserModal').addEventListener('hidden.bs.modal', function() {
this.remove();
});
}
function refreshUsers() {
loadUsers();
showNotification('Данные обновлены', 'info');
}
// ============ Вспомогательные функции ============
function getRoleLabel(role) {
const labels = {
'admin': '👑 Администратор',
'user': '👤 Пользователь',
'moderator': '🛡️ Модератор'
};
return labels[role] || role;
}
function getStatusLabel(status) {
const labels = {
'active': 'Активен',
'inactive': 'Неактивен',
'blocked': 'Заблокирован'
};
return labels[status] || status;
}
function getStatusColor(status) {
const colors = {
'active': 'success',
'inactive': 'warning',
'blocked': 'danger'
};
return colors[status] || 'secondary';
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
function showLoading() {
document.getElementById('loadingOverlay').style.display = 'flex';
}
function hideLoading() {
document.getElementById('loadingOverlay').style.display = 'none';
}
function showNotification(message, type = 'info') {
const container = document.getElementById('notificationContainer');
const notification = document.createElement('div');
const icons = {
success: 'check-circle',
danger: 'exclamation-circle',
warning: 'exclamation-triangle',
info: 'info-circle'
};
notification.className = `notification alert alert-${type} alert-dismissible fade show`;
notification.innerHTML = `
<i class="bi bi-${icons[type] || 'info-circle'} me-2"></i>
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
container.appendChild(notification);
// Автоматическое удаление через 3 секунды
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// ============ Инициализация приложения ============
document.addEventListener('DOMContentLoaded', function() {
loadUsers();
// Горячие клавиши
document.addEventListener('keydown', function(e) {
// Ctrl+N - новый пользователь
if (e.ctrlKey && e.key === 'n') {
e.preventDefault();
openCreateDialog();
}
// Ctrl+R - обновить
if (e.ctrlKey && e.key === 'r') {
e.preventDefault();
refreshUsers();
}
// Escape - закрыть диалог
if (e.key === 'Escape') {
crud_d.closeAll();
}
});
console.log('🚀 Приложение "Управление пользователями" запущено');
console.log('📝 Горячие клавиши:');
console.log(' Ctrl+N - Новый пользователь');
console.log(' Ctrl+R - Обновить список');
console.log(' Esc - Закрыть все диалоги');
console.log('💡 Кликните на пользователя для просмотра деталей');
});
</script>
</body>
</html>