sozday-polnostyu-rabochiy-in/index.html

465 lines
20 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru" data-theme="auto">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Аналитическая панель</title>
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
<link rel="stylesheet" href="design-system/kt-ai-components.css">
<link rel="stylesheet" href="design-system/kt-ai-page.css">
<style>
.dashboard { display: grid; gap: var(--kt-ai-space-6); grid-template-columns: 1fr; }
.upload-zone { border: 2px dashed var(--kt-ai-border); border-radius: var(--kt-ai-radius-2xl); padding: var(--kt-ai-space-10); text-align: center; background: var(--kt-ai-bg-soft); cursor: pointer; transition: all 0.3s; }
.upload-zone:hover { border-color: var(--kt-ai-primary); background: var(--kt-ai-card-bg); }
.upload-zone.dragover { border-color: var(--kt-ai-primary); background: var(--kt-ai-primary); color: white; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--kt-ai-space-4); }
.stat-card { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-5); }
.stat-value { font-size: 2rem; font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-primary); }
.stat-label { color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); }
.insights-list { display: flex; flex-direction: column; gap: var(--kt-ai-space-3); }
.insight-item { padding: var(--kt-ai-space-4); border-radius: var(--kt-ai-radius-lg); border-left: 4px solid; }
.insight-item.info { border-left-color: var(--kt-ai-primary); background: rgba(0, 184, 204, 0.1); }
.insight-item.warning { border-left-color: #f59e0b; background: rgba(245, 158, 11, 0.1); }
.insight-item.metric { border-left-color: #10b981; background: rgba(16, 185, 129, 0.1); }
.filters-bar { display: flex; gap: var(--kt-ai-space-3); flex-wrap: wrap; align-items: center; margin-bottom: var(--kt-ai-space-4); }
.filters-bar select, .filters-bar input { min-width: 200px; }
.table-container { overflow-x: auto; background: var(--kt-ai-card-bg); border-radius: var(--kt-ai-radius-xl); border: 1px solid var(--kt-ai-card-border); }
table { width: 100%; border-collapse: collapse; }
th, td { padding: var(--kt-ai-space-3) var(--kt-ai-space-4); text-align: left; border-bottom: 1px solid var(--kt-ai-border); }
th { background: var(--kt-ai-bg-soft); font-weight: var(--kt-ai-weight-semibold); position: sticky; top: 0; }
tr:hover { background: var(--kt-ai-bg-soft); }
.chart-container { background: var(--kt-ai-card-bg); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-5); border: 1px solid var(--kt-ai-card-border); }
.bar-chart { display: flex; align-items: flex-end; gap: 8px; height: 200px; padding-top: var(--kt-ai-space-4); overflow-x: auto; }
.bar { flex: 1; min-width: 40px; background: linear-gradient(to top, var(--kt-ai-primary), rgba(0, 184, 204, 0.6)); border-radius: var(--kt-ai-radius-md) var(--kt-ai-radius-md) 0 0; transition: all 0.3s; position: relative; }
.bar:hover { opacity: 0.8; transform: scaleX(1.05); }
.bar-label { position: absolute; bottom: -25px; left: 50%; transform: translateX(-50%); font-size: 10px; white-space: nowrap; color: var(--kt-ai-fg-muted); }
.bar-value { position: absolute; top: -20px; left: 50%; transform: translateX(-50%); font-size: 11px; font-weight: var(--kt-ai-weight-semibold); }
.hidden { display: none; }
.loading { opacity: 0.5; pointer-events: none; }
.tabs { display: flex; gap: var(--kt-ai-space-2); margin-bottom: var(--kt-ai-space-4); border-bottom: 1px solid var(--kt-ai-border); padding-bottom: var(--kt-ai-space-2); }
.tab { padding: var(--kt-ai-space-2) var(--kt-ai-space-4); border-radius: var(--kt-ai-radius-md); cursor: pointer; transition: all 0.2s; }
.tab:hover { background: var(--kt-ai-bg-soft); }
.tab.active { background: var(--kt-ai-primary); color: white; }
.section-title { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-3); }
@media (min-width: 768px) { .dashboard { grid-template-columns: 2fr 1fr; } }
</style>
</head>
<body>
<header class="kt-ai-pagehead">
<div class="kt-ai-wrap kt-ai-pagehead-in">
<strong>📊 Аналитическая панель</strong>
<span class="kt-ai-spacer"></span>
<button class="kt-ai-btn" data-variant="ghost" onclick="clearData()">Очистить данные</button>
</div>
</header>
<main class="kt-ai-wrap" style="padding: var(--kt-ai-space-8) 0;">
<div id="uploadSection">
<div class="upload-zone" id="dropZone">
<div style="font-size: 3rem; margin-bottom: var(--kt-ai-space-4);">📁</div>
<h2 style="margin-bottom: var(--kt-ai-space-2);">Загрузите файл с данными</h2>
<p style="color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-4);">
Перетащите файл сюда или кликните для выбора<br>
<small>Поддерживаются: Excel (.xlsx, .xls), CSV</small>
</p>
<input type="file" id="fileInput" accept=".xlsx,.xls,.csv" style="display: none;">
<button class="kt-ai-btn" data-variant="primary" data-size="lg" onclick="document.getElementById('fileInput').click()">
Выбрать файл
</button>
</div>
</div>
<div id="dashboardSection" class="hidden">
<div class="tabs">
<div class="tab active" data-tab="overview" onclick="switchTab('overview')">Обзор</div>
<div class="tab" data-tab="table" onclick="switchTab('table')">Таблица</div>
<div class="tab" data-tab="charts" onclick="switchTab('charts')">Графики</div>
</div>
<div id="overviewTab">
<div class="dashboard">
<div>
<div class="section-title">Ключевые показатели</div>
<div class="stats-grid" id="statsGrid" style="margin-bottom: var(--kt-ai-space-6);"></div>
<div class="section-title">Данные</div>
<div class="table-container">
<div class="filters-bar" style="padding: var(--kt-ai-space-4);">
<select id="filterColumn" class="kt-ai-select" onchange="applyFilters()">
<option value="">Все колонки</option>
</select>
<input type="text" id="filterValue" class="kt-ai-input" placeholder="Поиск..." oninput="applyFilters()" style="flex: 1;">
<select id="sortColumn" class="kt-ai-select" onchange="applyFilters()">
<option value="">Сортировка</option>
</select>
<select id="sortOrder" class="kt-ai-select" onchange="applyFilters()">
<option value="asc">По возрастанию</option>
<option value="desc">По убыванию</option>
</select>
</div>
<table id="dataTable">
<thead id="tableHead"></thead>
<tbody id="tableBody"></tbody>
</table>
</div>
<div style="padding: var(--kt-ai-space-3); color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm);">
Показано <span id="shownCount">0</span> из <span id="totalCount">0</span> записей
</div>
</div>
<div>
<div class="section-title">Аналитические выводы</div>
<div class="insights-list" id="insightsList"></div>
</div>
</div>
</div>
<div id="tableTab" class="hidden">
<div class="section-title">Полная таблица данных</div>
<div class="filters-bar">
<select id="fullFilterColumn" class="kt-ai-select" onchange="applyFullFilters()">
<option value="">Все колонки</option>
</select>
<input type="text" id="fullFilterValue" class="kt-ai-input" placeholder="Поиск..." oninput="applyFullFilters()" style="flex: 1;">
<button class="kt-ai-btn" data-variant="ghost" onclick="exportToCSV()">Экспорт в CSV</button>
</div>
<div class="table-container">
<table id="fullTable">
<thead id="fullTableHead"></thead>
<tbody id="fullTableBody"></tbody>
</table>
</div>
</div>
<div id="chartsTab" class="hidden">
<div class="section-title">Визуализация данных</div>
<div style="display: flex; flex-direction: column; gap: var(--kt-ai-space-6);">
<div id="chartsContainer"></div>
</div>
</div>
</div>
</main>
<script>
let currentData = { columns: [], rows: [], stats: {}, insights: [] };
let allData = { columns: [], rows: [], stats: {}, insights: [] };
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); }
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, () => dropZone.classList.add('dragover'), false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, () => dropZone.classList.remove('dragover'), false);
});
dropZone.addEventListener('drop', handleDrop, false);
fileInput.addEventListener('change', handleFileSelect, false);
function handleDrop(e) {
const files = e.dataTransfer.files;
if (files.length) uploadFile(files[0]);
}
function handleFileSelect(e) {
const files = e.target.files;
if (files.length) uploadFile(files[0]);
}
function uploadFile(file) {
dropZone.classList.add('loading');
const formData = new FormData();
formData.append('file', file);
fetch('/api/upload', { method: 'POST', body: formData })
.then(r => r.json())
.then(data => {
if (data.error) { alert('Ошибка: ' + data.error); }
else { loadData(data); }
})
.catch(e => alert('Ошибка загрузки: ' + e.message))
.finally(() => dropZone.classList.remove('loading'));
}
function loadData(data) {
currentData = data;
allData = JSON.parse(JSON.stringify(data));
document.getElementById('uploadSection').classList.add('hidden');
document.getElementById('dashboardSection').classList.remove('hidden');
renderStats(data.stats);
renderInsights(data.insights);
renderTable(data);
renderCharts();
populateFilters();
}
function renderStats(stats) {
const grid = document.getElementById('statsGrid');
const entries = Object.entries(stats);
if (entries.length === 0) {
grid.innerHTML = '<div class="stat-card"><div class="stat-value">0</div><div class="stat-label">Числовых полей не найдено</div></div>';
return;
}
grid.innerHTML = entries.slice(0, 6).map(([col, s]) => `
<div class="stat-card">
<div class="stat-value">${formatNumber(s.sum)}</div>
<div class="stat-label">${col} (сумма)</div>
<div style="font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); margin-top: var(--kt-ai-space-2);">
Среднее: ${s.avg.toFixed(1)} | Мин: ${s.min} | Макс: ${s.max}
</div>
</div>
`).join('');
}
function renderInsights(insights) {
const list = document.getElementById('insightsList');
if (!insights || insights.length === 0) {
list.innerHTML = '<div class="insight-item info">Загрузите данные для получения аналитических выводов</div>';
return;
}
list.innerHTML = insights.map(i => `
<div class="insight-item ${i.type}">
<strong>${i.category}:</strong> ${i.text}
</div>
`).join('');
}
function renderTable(data) {
const thead = document.getElementById('tableHead');
const tbody = document.getElementById('tableBody');
if (data.columns.length === 0) return;
thead.innerHTML = '<tr>' + data.columns.map(col => `<th>${col}</th>`).join('') + '</tr>';
tbody.innerHTML = data.rows.slice(0, 100).map(row =>
'<tr>' + data.columns.map(col => `<td>${row[col] !== undefined ? row[col] : ''}</td>`).join('') + '</tr>'
).join('');
document.getElementById('shownCount').textContent = Math.min(data.rows.length, 100);
document.getElementById('totalCount').textContent = data.rows.length;
}
function populateFilters() {
const filterSelect = document.getElementById('filterColumn');
const sortSelect = document.getElementById('sortColumn');
const fullFilterSelect = document.getElementById('fullFilterColumn');
const options = currentData.columns.map(col => `<option value="${col}">${col}</option>`).join('');
filterSelect.innerHTML = '<option value="">Все колонки</option>' + options;
sortSelect.innerHTML = '<option value="">Сортировка</option>' + options;
fullFilterSelect.innerHTML = '<option value="">Все колонки</option>' + options;
}
function applyFilters() {
const filterColumn = document.getElementById('filterColumn').value;
const filterValue = document.getElementById('filterValue').value;
const sortColumn = document.getElementById('sortColumn').value;
const sortOrder = document.getElementById('sortOrder').value;
let filtered = [...allData.rows];
if (filterColumn && filterValue) {
filtered = filtered.filter(row =>
String(row[filterColumn] || '').toLowerCase().includes(filterValue.toLowerCase())
);
}
if (sortColumn) {
filtered.sort((a, b) => {
const aVal = parseFloat(a[sortColumn]) || a[sortColumn];
const bVal = parseFloat(b[sortColumn]) || b[sortColumn];
if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1;
if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1;
return 0;
});
}
currentData.rows = filtered;
renderTable(currentData);
renderStats(calculateStats(filtered, currentData.columns));
}
function applyFullFilters() {
const filterColumn = document.getElementById('fullFilterColumn').value;
const filterValue = document.getElementById('fullFilterValue').value;
let filtered = [...allData.rows];
if (filterColumn && filterValue) {
filtered = filtered.filter(row =>
String(row[filterColumn] || '').toLowerCase().includes(filterValue.toLowerCase())
);
}
const tbody = document.getElementById('fullTableBody');
const thead = document.getElementById('fullTableHead');
if (allData.columns.length === 0) return;
thead.innerHTML = '<tr>' + allData.columns.map(col => `<th>${col}</th>`).join('') + '</tr>';
tbody.innerHTML = filtered.slice(0, 1000).map(row =>
'<tr>' + allData.columns.map(col => `<td>${row[col] !== undefined ? row[col] : ''}</td>`).join('') + '</tr>'
).join('');
}
function renderCharts() {
const container = document.getElementById('chartsContainer');
const stats = allData.stats;
const numericCols = Object.keys(stats);
if (numericCols.length === 0) {
container.innerHTML = '<div class="insight-item info">Нет числовых данных для построения графиков</div>';
return;
}
container.innerHTML = numericCols.slice(0, 5).map(col => {
const s = stats[col];
const maxValue = s.max;
const bars = [
{ label: 'Мин', value: s.min, color: 'var(--kt-ai-primary)' },
{ label: 'Среднее', value: s.avg, color: 'var(--kt-ai-primary)' },
{ label: 'Медиана', value: s.median, color: '#10b981' },
{ label: 'Макс', value: s.max, color: '#f59e0b' }
];
return `
<div class="chart-container">
<h3 style="margin-bottom: var(--kt-ai-space-4);">${col}</h3>
<div class="bar-chart">
${bars.map(b => `
<div class="bar" style="height: ${(b.value / maxValue * 100)}%; background: ${b.color};">
<div class="bar-value">${formatNumber(b.value)}</div>
<div class="bar-label">${b.label}</div>
</div>
`).join('')}
</div>
</div>
`;
}).join('');
const stringCols = allData.columns.filter(col => !stats[col]);
if (stringCols.length > 0) {
const col = stringCols[0];
const counts = {};
allData.rows.forEach(row => {
const val = row[col] || 'Пусто';
counts[val] = (counts[val] || 0) + 1;
});
const topEntries = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 10);
const maxCount = Math.max(...topEntries.map(([, c]) => c));
container.innerHTML += `
<div class="chart-container">
<h3 style="margin-bottom: var(--kt-ai-space-4);">Топ значений: ${col}</h3>
<div class="bar-chart">
${topEntries.map(([label, count]) => `
<div class="bar" style="height: ${(count / maxCount * 100)}%;">
<div class="bar-value">${count}</div>
<div class="bar-label" style="transform: translateX(-50%) rotate(-45deg);">${label.substring(0, 15)}</div>
</div>
`).join('')}
</div>
</div>
`;
}
}
function switchTab(tabName) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
document.getElementById('overviewTab').classList.add('hidden');
document.getElementById('tableTab').classList.add('hidden');
document.getElementById('chartsTab').classList.add('hidden');
document.getElementById(tabName + 'Tab').classList.remove('hidden');
if (tabName === 'table') applyFullFilters();
if (tabName === 'charts') renderCharts();
}
function clearData() {
if (confirm('Удалить все загруженные данные?')) {
fetch('/api/analysis', { method: 'DELETE' }).catch(() => {});
location.reload();
}
}
function exportToCSV() {
const filterColumn = document.getElementById('fullFilterColumn').value;
const filterValue = document.getElementById('fullFilterValue').value;
let filtered = [...allData.rows];
if (filterColumn && filterValue) {
filtered = filtered.filter(row =>
String(row[filterColumn] || '').toLowerCase().includes(filterValue.toLowerCase())
);
}
const csv = '\uFEFF' + allData.columns.join(';') + '\n' +
filtered.map(row => allData.columns.map(col =>
'"' + String(row[col] || '').replace(/"/g, '""') + '"'
).join(';')).join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'export.csv';
link.click();
}
function calculateStats(rows, columns) {
const stats = {};
const numericCols = columns.filter(col => {
const sample = rows.slice(0, 10).map(r => r[col]);
return sample.some(v => !isNaN(parseFloat(v)) && isFinite(v));
});
numericCols.forEach(col => {
const values = rows.map(r => parseFloat(r[col])).filter(v => !isNaN(v));
if (values.length > 0) {
const sum = values.reduce((a, b) => a + b, 0);
const avg = sum / values.length;
const min = Math.min(...values);
const max = Math.max(...values);
const sorted = [...values].sort((a, b) => a - b);
const median = sorted.length % 2 === 0
? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2
: sorted[Math.floor(sorted.length / 2)];
stats[col] = { sum, avg, min, max, median, count: values.length };
}
});
return stats;
}
function formatNumber(num) {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toFixed(1);
}
fetch('/api/analysis')
.then(r => r.json())
.then(data => {
if (data && data.columns && data.columns.length > 0) {
loadData(data);
}
})
.catch(() => {});
</script>
</body>
</html>