1058 lines
38 KiB
HTML
1058 lines
38 KiB
HTML
<!-- Добавьте в ваш index.html или создайте отдельную страницу -->
|
||
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Документация БД</title>
|
||
<style>
|
||
* {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background-color: #f5f5f5;
|
||
color: #333;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
.header {
|
||
background-color: #2c3e50;
|
||
color: white;
|
||
padding: 20px 0;
|
||
margin-bottom: 30px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.header h1 {
|
||
text-align: center;
|
||
font-size: 28px;
|
||
font-weight: 300;
|
||
}
|
||
|
||
.header p {
|
||
text-align: center;
|
||
margin-top: 10px;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.controls {
|
||
background: white;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.table-selector {
|
||
display: flex;
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.table-btn {
|
||
padding: 10px 20px;
|
||
background-color: #3498db;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
transition: background-color 0.3s;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.table-btn:hover {
|
||
background-color: #2980b9;
|
||
}
|
||
|
||
.table-btn.active {
|
||
background-color: #2c3e50;
|
||
}
|
||
|
||
.filters {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 15px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.filter-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.filter-group label {
|
||
margin-bottom: 5px;
|
||
font-weight: 500;
|
||
color: #555;
|
||
}
|
||
|
||
.filter-group input,
|
||
.filter-group select {
|
||
padding: 10px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.filter-group input:focus,
|
||
.filter-group select:focus {
|
||
outline: none;
|
||
border-color: #3498db;
|
||
}
|
||
|
||
.stats {
|
||
display: flex;
|
||
gap: 20px;
|
||
margin-top: 15px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.database-info {
|
||
background: white;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
border-left: 4px solid #3498db;
|
||
}
|
||
|
||
.data-section {
|
||
display: none;
|
||
animation: fadeIn 0.5s;
|
||
}
|
||
|
||
.data-section.active {
|
||
display: block;
|
||
}
|
||
|
||
.table-container {
|
||
background: white;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.table-header {
|
||
background-color: #2c3e50;
|
||
color: white;
|
||
padding: 15px 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.table-header h3 {
|
||
font-weight: 400;
|
||
}
|
||
|
||
.row-count {
|
||
background-color: rgba(255,255,255,0.2);
|
||
padding: 5px 10px;
|
||
border-radius: 20px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.table-wrapper {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
min-width: 800px;
|
||
}
|
||
|
||
thead {
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
th {
|
||
padding: 12px 15px;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
color: #495057;
|
||
border-bottom: 2px solid #dee2e6;
|
||
position: sticky;
|
||
top: 0;
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
td {
|
||
padding: 12px 15px;
|
||
border-bottom: 1px solid #dee2e6;
|
||
vertical-align: top;
|
||
}
|
||
|
||
tr:hover {
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.boolean {
|
||
display: inline-block;
|
||
padding: 3px 8px;
|
||
border-radius: 12px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.boolean.true {
|
||
background-color: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.boolean.false {
|
||
background-color: #f8d7da;
|
||
color: #721c24;
|
||
}
|
||
|
||
.null-value {
|
||
color: #6c757d;
|
||
font-style: italic;
|
||
}
|
||
|
||
.json-value {
|
||
background-color: #f8f9fa;
|
||
padding: 5px 10px;
|
||
border-radius: 4px;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 12px;
|
||
max-width: 300px;
|
||
overflow: auto;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.datetime {
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 13px;
|
||
color: #495057;
|
||
}
|
||
|
||
.export-btn {
|
||
background-color: #27ae60;
|
||
color: white;
|
||
border: none;
|
||
padding: 10px 20px;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
transition: background-color 0.3s;
|
||
}
|
||
|
||
.export-btn:hover {
|
||
background-color: #219653;
|
||
}
|
||
|
||
.pagination {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-top: 20px;
|
||
padding: 10px;
|
||
background: white;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.page-btn {
|
||
padding: 5px 10px;
|
||
border: 1px solid #ddd;
|
||
background: white;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.page-btn.active {
|
||
background-color: #3498db;
|
||
color: white;
|
||
border-color: #3498db;
|
||
}
|
||
|
||
.page-btn:hover:not(.active) {
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
.loading {
|
||
text-align: center;
|
||
padding: 40px;
|
||
font-size: 18px;
|
||
color: #666;
|
||
}
|
||
|
||
.error {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: #e74c3c;
|
||
background-color: #fdf2f2;
|
||
border-radius: 8px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.container {
|
||
padding: 10px;
|
||
}
|
||
|
||
.table-selector {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.table-btn {
|
||
width: 100%;
|
||
}
|
||
|
||
.filters {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<div class="container">
|
||
<h1>📊 Документация Базы Данных</h1>
|
||
<p>Просмотр и фильтрация данных таблиц SQLite</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="container">
|
||
<div class="controls">
|
||
<div class="table-selector">
|
||
<button class="table-btn active" onclick="showTable('users')">👥 Пользователи</button>
|
||
<button class="table-btn" onclick="showTable('tasks')">📋 Задачи</button>
|
||
<button class="table-btn" onclick="showTable('files')">📎 Файлы</button>
|
||
<button class="table-btn" onclick="showTable('activity_logs')">📊 Логи активности</button>
|
||
<button class="table-btn" onclick="showTable('assignments')">👨💻 Назначения</button>
|
||
</div>
|
||
|
||
<div class="filters" id="filters-container">
|
||
<!-- Фильтры будут добавляться динамически -->
|
||
</div>
|
||
|
||
<div class="stats">
|
||
<span id="total-records">Записей: 0</span>
|
||
<span id="filtered-records">Показано: 0</span>
|
||
<span id="last-updated">Обновлено: --</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="database-info">
|
||
<p><strong>База данных:</strong> SQLite</p>
|
||
<p><strong>Таблицы:</strong> users, tasks, files, activity_logs, assignments</p>
|
||
</div>
|
||
|
||
<div id="users-section" class="data-section active">
|
||
<div class="table-container">
|
||
<div class="table-header">
|
||
<h3>Таблица: users (Пользователи)</h3>
|
||
<span class="row-count" id="users-count">0 записей</span>
|
||
</div>
|
||
<div class="table-wrapper">
|
||
<table id="users-table">
|
||
<thead>
|
||
<tr id="users-headers">
|
||
<!-- Заголовки будут заполнены динамически -->
|
||
</tr>
|
||
</thead>
|
||
<tbody id="users-body">
|
||
<!-- Данные будут заполнены динамически -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="tasks-section" class="data-section">
|
||
<div class="table-container">
|
||
<div class="table-header">
|
||
<h3>Таблица: tasks (Задачи)</h3>
|
||
<span class="row-count" id="tasks-count">0 записей</span>
|
||
</div>
|
||
<div class="table-wrapper">
|
||
<table id="tasks-table">
|
||
<thead>
|
||
<tr id="tasks-headers">
|
||
<!-- Заголовки будут заполнены динамически -->
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tasks-body">
|
||
<!-- Данные будут заполнены динамически -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="files-section" class="data-section">
|
||
<div class="table-container">
|
||
<div class="table-header">
|
||
<h3>Таблица: files (Файлы)</h3>
|
||
<span class="row-count" id="files-count">0 записей</span>
|
||
</div>
|
||
<div class="table-wrapper">
|
||
<table id="files-table">
|
||
<thead>
|
||
<tr id="files-headers">
|
||
<!-- Заголовки будут заполнены динамически -->
|
||
</tr>
|
||
</thead>
|
||
<tbody id="files-body">
|
||
<!-- Данные будут заполнены динамически -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="activity_logs-section" class="data-section">
|
||
<div class="table-container">
|
||
<div class="table-header">
|
||
<h3>Таблица: activity_logs (Логи активности)</h3>
|
||
<span class="row-count" id="activity_logs-count">0 записей</span>
|
||
</div>
|
||
<div class="table-wrapper">
|
||
<table id="activity_logs-table">
|
||
<thead>
|
||
<tr id="activity_logs-headers">
|
||
<!-- Заголовки будут заполнены динамически -->
|
||
</tr>
|
||
</thead>
|
||
<tbody id="activity_logs-body">
|
||
<!-- Данные будут заполнены динамически -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="assignments-section" class="data-section">
|
||
<div class="table-container">
|
||
<div class="table-header">
|
||
<h3>Таблица: assignments (Назначения)</h3>
|
||
<span class="row-count" id="assignments-count">0 записей</span>
|
||
</div>
|
||
<div class="table-wrapper">
|
||
<table id="assignments-table">
|
||
<thead>
|
||
<tr id="assignments-headers">
|
||
<!-- Заголовки будут заполнены динамически -->
|
||
</tr>
|
||
</thead>
|
||
<tbody id="assignments-body">
|
||
<!-- Данные будут заполнены динамически -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pagination" id="pagination">
|
||
<!-- Пагинация будет добавляться динамически -->
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let currentTable = 'users';
|
||
let currentData = {
|
||
users: [],
|
||
tasks: [],
|
||
files: [],
|
||
activity_logs: [],
|
||
assignments: []
|
||
};
|
||
let filteredData = {
|
||
users: [],
|
||
tasks: [],
|
||
files: [],
|
||
activity_logs: [],
|
||
assignments: []
|
||
};
|
||
let filters = {
|
||
users: {},
|
||
tasks: {},
|
||
files: {},
|
||
activity_logs: {},
|
||
assignments: {}
|
||
};
|
||
let pageSize = 50;
|
||
let currentPage = 1;
|
||
|
||
// Загрузка данных при открытии страницы
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
loadTableData('users');
|
||
updateLastUpdated();
|
||
});
|
||
|
||
// Показать выбранную таблицу
|
||
function showTable(tableName) {
|
||
// Обновить активную кнопку
|
||
document.querySelectorAll('.table-btn').forEach(btn => {
|
||
btn.classList.remove('active');
|
||
});
|
||
event.target.classList.add('active');
|
||
|
||
// Скрыть все секции
|
||
document.querySelectorAll('.data-section').forEach(section => {
|
||
section.classList.remove('active');
|
||
});
|
||
|
||
// Показать выбранную секцию
|
||
document.getElementById(`${tableName}-section`).classList.add('active');
|
||
|
||
currentTable = tableName;
|
||
currentPage = 1;
|
||
|
||
// Загрузить данные, если они еще не загружены
|
||
if (currentData[tableName].length === 0) {
|
||
loadTableData(tableName);
|
||
} else {
|
||
applyFilters();
|
||
}
|
||
|
||
// Обновить фильтры для выбранной таблицы
|
||
updateFiltersForTable(tableName);
|
||
}
|
||
|
||
// Загрузка данных таблицы
|
||
async function loadTableData(tableName) {
|
||
try {
|
||
showLoading(tableName);
|
||
|
||
const response = await fetch(`/api/${tableName}`);
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
currentData[tableName] = data;
|
||
filteredData[tableName] = [...data];
|
||
|
||
renderTable(tableName);
|
||
applyFilters();
|
||
updateFiltersForTable(tableName);
|
||
|
||
} catch (error) {
|
||
console.error(`Ошибка загрузки данных для таблицы ${tableName}:`, error);
|
||
showError(tableName, error.message);
|
||
}
|
||
}
|
||
|
||
// Отображение таблицы
|
||
function renderTable(tableName) {
|
||
const data = filteredData[tableName];
|
||
const containerId = `${tableName}-body`;
|
||
const headersId = `${tableName}-headers`;
|
||
const countId = `${tableName}-count`;
|
||
|
||
// Обновить счетчик
|
||
document.getElementById(countId).textContent = `${data.length} записей`;
|
||
|
||
// Очистить текущие данные
|
||
document.getElementById(containerId).innerHTML = '';
|
||
document.getElementById(headersId).innerHTML = '';
|
||
|
||
if (data.length === 0) {
|
||
document.getElementById(containerId).innerHTML = `
|
||
<tr>
|
||
<td colspan="100" style="text-align: center; padding: 40px; color: #666;">
|
||
Нет данных для отображения
|
||
</td>
|
||
</tr>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// Получить все уникальные ключи из данных
|
||
const allKeys = new Set();
|
||
data.forEach(item => {
|
||
Object.keys(item).forEach(key => allKeys.add(key));
|
||
});
|
||
|
||
const sortedKeys = Array.from(allKeys).sort();
|
||
|
||
// Создать заголовки таблицы
|
||
const headersRow = document.getElementById(headersId);
|
||
sortedKeys.forEach(key => {
|
||
const th = document.createElement('th');
|
||
th.textContent = formatHeader(key);
|
||
th.style.cursor = 'pointer';
|
||
th.title = 'Нажмите для сортировки';
|
||
th.onclick = () => sortTable(tableName, key);
|
||
headersRow.appendChild(th);
|
||
});
|
||
|
||
// Создать строки данных с пагинацией
|
||
const startIndex = (currentPage - 1) * pageSize;
|
||
const endIndex = Math.min(startIndex + pageSize, data.length);
|
||
const pageData = data.slice(startIndex, endIndex);
|
||
|
||
pageData.forEach(item => {
|
||
const row = document.createElement('tr');
|
||
|
||
sortedKeys.forEach(key => {
|
||
const td = document.createElement('td');
|
||
const value = item[key];
|
||
|
||
td.innerHTML = formatValue(value);
|
||
row.appendChild(td);
|
||
});
|
||
|
||
document.getElementById(containerId).appendChild(row);
|
||
});
|
||
|
||
// Обновить пагинацию
|
||
updatePagination(tableName, data.length);
|
||
|
||
// Обновить статистику
|
||
updateStats();
|
||
}
|
||
|
||
// Форматирование заголовка
|
||
function formatHeader(key) {
|
||
const headerMap = {
|
||
'id': 'ID',
|
||
'name': 'Имя',
|
||
'login': 'Логин',
|
||
'email': 'Email',
|
||
'role': 'Роль',
|
||
'created_at': 'Создан',
|
||
'updated_at': 'Обновлен',
|
||
'deleted_at': 'Удален',
|
||
'title': 'Название',
|
||
'description': 'Описание',
|
||
'task_type': 'Тип задачи',
|
||
'due_date': 'Срок выполнения',
|
||
'closed_at': 'Закрыта',
|
||
'rework_comment': 'Комментарий доработки',
|
||
'original_task_id': 'ID оригинальной задачи',
|
||
'original_task_title': 'Название оригинала',
|
||
'original_creator_name': 'Автор оригинала',
|
||
'filename': 'Имя файла',
|
||
'original_filename': 'Оригинальное имя',
|
||
'filepath': 'Путь к файлу',
|
||
'file_size': 'Размер файла',
|
||
'mime_type': 'Тип файла',
|
||
'user_id': 'ID пользователя',
|
||
'task_id': 'ID задачи',
|
||
'action': 'Действие',
|
||
'details': 'Детали',
|
||
'status': 'Статус',
|
||
'start_date': 'Дата начала',
|
||
'user_name': 'Имя пользователя',
|
||
'user_login': 'Логин пользователя',
|
||
'creator_name': 'Имя создателя',
|
||
'created_by': 'Создатель'
|
||
};
|
||
|
||
return headerMap[key] || key.replace(/_/g, ' ');
|
||
}
|
||
|
||
// Форматирование значения
|
||
function formatValue(value) {
|
||
if (value === null || value === undefined) {
|
||
return '<span class="null-value">NULL</span>';
|
||
}
|
||
|
||
if (typeof value === 'boolean') {
|
||
return `<span class="boolean ${value}">${value ? 'Да' : 'Нет'}</span>`;
|
||
}
|
||
|
||
if (typeof value === 'object') {
|
||
try {
|
||
return `<div class="json-value">${JSON.stringify(value, null, 2)}</div>`;
|
||
} catch {
|
||
return String(value);
|
||
}
|
||
}
|
||
|
||
if (typeof value === 'string') {
|
||
// Проверить, является ли это датой
|
||
if (value.match(/^\d{4}-\d{2}-\d{2}/)) {
|
||
try {
|
||
const date = new Date(value);
|
||
return `<span class="datetime" title="${value}">${date.toLocaleString('ru-RU')}</span>`;
|
||
} catch {
|
||
return value;
|
||
}
|
||
}
|
||
|
||
// Проверить, является ли это JSON строкой
|
||
if (value.startsWith('{') || value.startsWith('[')) {
|
||
try {
|
||
const parsed = JSON.parse(value);
|
||
return `<div class="json-value">${JSON.stringify(parsed, null, 2)}</div>`;
|
||
} catch {
|
||
return value;
|
||
}
|
||
}
|
||
|
||
return value;
|
||
}
|
||
|
||
return String(value);
|
||
}
|
||
|
||
// Обновление фильтров для таблицы
|
||
function updateFiltersForTable(tableName) {
|
||
const filtersContainer = document.getElementById('filters-container');
|
||
const data = currentData[tableName];
|
||
|
||
if (data.length === 0) {
|
||
filtersContainer.innerHTML = '<p>Нет данных для фильтрации</p>';
|
||
return;
|
||
}
|
||
|
||
// Получить все уникальные ключи
|
||
const allKeys = new Set();
|
||
data.forEach(item => {
|
||
Object.keys(item).forEach(key => allKeys.add(key));
|
||
});
|
||
|
||
const sortedKeys = Array.from(allKeys).sort();
|
||
|
||
// Создать фильтры для каждого поля
|
||
let filtersHtml = '';
|
||
|
||
sortedKeys.forEach(key => {
|
||
// Получить уникальные значения для этого поля
|
||
const uniqueValues = [...new Set(data.map(item => item[key]))]
|
||
.filter(val => val !== null && val !== undefined)
|
||
.sort();
|
||
|
||
// Определить тип фильтра
|
||
const sampleValue = data[0][key];
|
||
let filterType = 'text';
|
||
|
||
if (typeof sampleValue === 'boolean') {
|
||
filterType = 'boolean';
|
||
} else if (typeof sampleValue === 'number') {
|
||
filterType = 'number';
|
||
} else if (sampleValue && typeof sampleValue === 'string') {
|
||
if (sampleValue.match(/^\d{4}-\d{2}-\d{2}/)) {
|
||
filterType = 'date';
|
||
}
|
||
}
|
||
|
||
filtersHtml += `
|
||
<div class="filter-group">
|
||
<label for="filter-${key}">${formatHeader(key)}</label>
|
||
`;
|
||
|
||
if (filterType === 'boolean') {
|
||
filtersHtml += `
|
||
<select id="filter-${key}" onchange="updateFilter('${tableName}', '${key}', this.value)">
|
||
<option value="">Все</option>
|
||
<option value="true">Да</option>
|
||
<option value="false">Нет</option>
|
||
</select>
|
||
`;
|
||
} else if (filterType === 'date') {
|
||
filtersHtml += `
|
||
<input type="date" id="filter-${key}"
|
||
onchange="updateFilter('${tableName}', '${key}', this.value)"
|
||
placeholder="Фильтр по дате">
|
||
`;
|
||
} else if (uniqueValues.length <= 20) {
|
||
// Для полей с небольшим количеством уникальных значений используем select
|
||
filtersHtml += `
|
||
<select id="filter-${key}" onchange="updateFilter('${tableName}', '${key}', this.value)">
|
||
<option value="">Все</option>
|
||
${uniqueValues.map(val =>
|
||
`<option value="${val}">${formatValue(val)}</option>`
|
||
).join('')}
|
||
</select>
|
||
`;
|
||
} else {
|
||
// Для остальных полей используем текстовый ввод
|
||
filtersHtml += `
|
||
<input type="text" id="filter-${key}"
|
||
oninput="updateFilter('${tableName}', '${key}', this.value)"
|
||
placeholder="Введите значение...">
|
||
`;
|
||
}
|
||
|
||
filtersHtml += `</div>`;
|
||
});
|
||
|
||
filtersContainer.innerHTML = filtersHtml;
|
||
|
||
// Восстановить сохраненные значения фильтров
|
||
Object.keys(filters[tableName]).forEach(key => {
|
||
const filterElement = document.getElementById(`filter-${key}`);
|
||
if (filterElement) {
|
||
filterElement.value = filters[tableName][key];
|
||
}
|
||
});
|
||
}
|
||
|
||
// Обновление фильтра
|
||
function updateFilter(tableName, field, value) {
|
||
filters[tableName][field] = value;
|
||
applyFilters();
|
||
}
|
||
|
||
// Применение фильтров
|
||
function applyFilters() {
|
||
const tableName = currentTable;
|
||
const data = currentData[tableName];
|
||
|
||
if (data.length === 0) {
|
||
filteredData[tableName] = [];
|
||
renderTable(tableName);
|
||
return;
|
||
}
|
||
|
||
let filtered = [...data];
|
||
|
||
// Применить все активные фильтры
|
||
Object.keys(filters[tableName]).forEach(field => {
|
||
const filterValue = filters[tableName][field];
|
||
|
||
if (filterValue && filterValue !== '') {
|
||
filtered = filtered.filter(item => {
|
||
const itemValue = item[field];
|
||
|
||
if (itemValue === null || itemValue === undefined) {
|
||
return false;
|
||
}
|
||
|
||
const itemStr = String(itemValue).toLowerCase();
|
||
const filterStr = filterValue.toLowerCase();
|
||
|
||
if (typeof itemValue === 'boolean') {
|
||
return itemStr === filterStr;
|
||
} else if (filterValue.match(/^\d{4}-\d{2}-\d{2}/)) {
|
||
// Фильтр по дате
|
||
return itemStr.includes(filterStr);
|
||
}
|
||
|
||
return itemStr.includes(filterStr);
|
||
});
|
||
}
|
||
});
|
||
|
||
filteredData[tableName] = filtered;
|
||
currentPage = 1;
|
||
renderTable(tableName);
|
||
}
|
||
|
||
// Сортировка таблицы
|
||
function sortTable(tableName, field) {
|
||
const data = filteredData[tableName];
|
||
|
||
data.sort((a, b) => {
|
||
const valA = a[field];
|
||
const valB = b[field];
|
||
|
||
if (valA === null && valB === null) return 0;
|
||
if (valA === null) return 1;
|
||
if (valB === null) return -1;
|
||
|
||
if (typeof valA === 'string' && typeof valB === 'string') {
|
||
return valA.localeCompare(valB);
|
||
}
|
||
|
||
return (valA < valB) ? -1 : (valA > valB) ? 1 : 0;
|
||
});
|
||
|
||
filteredData[tableName] = data;
|
||
renderTable(tableName);
|
||
}
|
||
|
||
// Обновление пагинации
|
||
function updatePagination(tableName, totalItems) {
|
||
const pagination = document.getElementById('pagination');
|
||
const totalPages = Math.ceil(totalItems / pageSize);
|
||
|
||
if (totalPages <= 1) {
|
||
pagination.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
let paginationHtml = '';
|
||
|
||
// Кнопка "Назад"
|
||
paginationHtml += `
|
||
<button class="page-btn" onclick="changePage(${currentPage - 1})"
|
||
${currentPage === 1 ? 'disabled style="opacity: 0.5;"' : ''}>
|
||
←
|
||
</button>
|
||
`;
|
||
|
||
// Номера страниц
|
||
const maxVisiblePages = 5;
|
||
let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
|
||
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
|
||
|
||
if (endPage - startPage + 1 < maxVisiblePages) {
|
||
startPage = Math.max(1, endPage - maxVisiblePages + 1);
|
||
}
|
||
|
||
for (let i = startPage; i <= endPage; i++) {
|
||
paginationHtml += `
|
||
<button class="page-btn ${i === currentPage ? 'active' : ''}"
|
||
onclick="changePage(${i})">
|
||
${i}
|
||
</button>
|
||
`;
|
||
}
|
||
|
||
// Кнопка "Вперед"
|
||
paginationHtml += `
|
||
<button class="page-btn" onclick="changePage(${currentPage + 1})"
|
||
${currentPage === totalPages ? 'disabled style="opacity: 0.5;"' : ''}>
|
||
→
|
||
</button>
|
||
`;
|
||
|
||
// Информация о странице
|
||
paginationHtml += `
|
||
<span style="margin-left: 20px; color: #666; font-size: 14px;">
|
||
Страница ${currentPage} из ${totalPages} (${totalItems} записей)
|
||
</span>
|
||
`;
|
||
|
||
pagination.innerHTML = paginationHtml;
|
||
}
|
||
|
||
// Изменение страницы
|
||
function changePage(page) {
|
||
const tableName = currentTable;
|
||
const totalItems = filteredData[tableName].length;
|
||
const totalPages = Math.ceil(totalItems / pageSize);
|
||
|
||
if (page < 1 || page > totalPages) return;
|
||
|
||
currentPage = page;
|
||
renderTable(tableName);
|
||
}
|
||
|
||
// Обновление статистики
|
||
function updateStats() {
|
||
const tableName = currentTable;
|
||
const total = currentData[tableName].length;
|
||
const filtered = filteredData[tableName].length;
|
||
|
||
document.getElementById('total-records').textContent = `Всего записей: ${total}`;
|
||
document.getElementById('filtered-records').textContent = `Показано: ${filtered}`;
|
||
}
|
||
|
||
// Обновление времени последнего обновления
|
||
function updateLastUpdated() {
|
||
const now = new Date();
|
||
const timeString = now.toLocaleString('ru-RU');
|
||
document.getElementById('last-updated').textContent = `Обновлено: ${timeString}`;
|
||
}
|
||
|
||
// Показать индикатор загрузки
|
||
function showLoading(tableName) {
|
||
const containerId = `${tableName}-body`;
|
||
document.getElementById(containerId).innerHTML = `
|
||
<tr>
|
||
<td colspan="100" style="text-align: center; padding: 40px;">
|
||
<div class="loading">Загрузка данных...</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}
|
||
|
||
// Показать ошибку
|
||
function showError(tableName, errorMessage) {
|
||
const containerId = `${tableName}-body`;
|
||
document.getElementById(containerId).innerHTML = `
|
||
<tr>
|
||
<td colspan="100">
|
||
<div class="error">
|
||
<strong>Ошибка загрузки данных:</strong><br>
|
||
${errorMessage}
|
||
<br><br>
|
||
<button onclick="loadTableData('${tableName}')"
|
||
style="padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||
Попробовать снова
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}
|
||
|
||
// Экспорт данных в CSV
|
||
function exportToCSV(tableName) {
|
||
const data = filteredData[tableName];
|
||
if (data.length === 0) {
|
||
alert('Нет данных для экспорта');
|
||
return;
|
||
}
|
||
|
||
// Получить все ключи
|
||
const allKeys = new Set();
|
||
data.forEach(item => {
|
||
Object.keys(item).forEach(key => allKeys.add(key));
|
||
});
|
||
|
||
const sortedKeys = Array.from(allKeys).sort();
|
||
|
||
// Создать CSV заголовок
|
||
let csv = sortedKeys.map(key => `"${formatHeader(key)}"`).join(',') + '\n';
|
||
|
||
// Добавить данные
|
||
data.forEach(item => {
|
||
const row = sortedKeys.map(key => {
|
||
let value = item[key];
|
||
|
||
if (value === null || value === undefined) {
|
||
return '';
|
||
}
|
||
|
||
if (typeof value === 'object') {
|
||
value = JSON.stringify(value);
|
||
}
|
||
|
||
// Экранировать кавычки и специальные символы
|
||
value = String(value).replace(/"/g, '""');
|
||
return `"${value}"`;
|
||
}).join(',');
|
||
|
||
csv += row + '\n';
|
||
});
|
||
|
||
// Создать и скачать файл
|
||
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
|
||
const link = document.createElement('a');
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
link.setAttribute('href', url);
|
||
link.setAttribute('download', `${tableName}_export_${new Date().toISOString().slice(0, 10)}.csv`);
|
||
link.style.visibility = 'hidden';
|
||
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
}
|
||
|
||
// Автоматическое обновление данных каждые 5 минут
|
||
setInterval(() => {
|
||
if (currentTable) {
|
||
loadTableData(currentTable);
|
||
updateLastUpdated();
|
||
}
|
||
}, 5 * 60 * 1000);
|
||
</script>
|
||
</body>
|
||
</html> |