chto-ty-umeesh/index.html

376 lines
13 KiB
HTML
Raw 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">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Дашборд сертификации сотрудников</title>
<style>
* { margin: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
min-height: 100vh;
padding: 20px;
}
.container { max-width: 1600px; margin: 0 auto; }
h1 { color: #fff; text-align: center; margin-bottom: 25px; font-size: clamp(24px, 4vw, 36px); }
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 15px;
margin-bottom: 25px;
}
.stat-card {
background: rgba(255,255,255,0.95);
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 15px rgba(0,0,0,0.15);
}
.stat-card .num { font-size: 36px; font-weight: bold; color: #1e3c72; }
.stat-card .label { color: #666; font-size: 14px; margin-top: 5px; }
.filters {
background: rgba(255,255,255,0.95);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: center;
}
.filters label { font-weight: 600; color: #333; margin-right: 8px; }
.filters select, .filters input {
padding: 10px 14px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
background: #fff;
}
.filters input { flex: 1; min-width: 200px; }
.chip {
display: inline-block;
padding: 6px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s;
}
.chip.active { border-color: #1e3c72; background: #e8f0fe; }
.chip-level-Специалист { background: #e8f5e9; color: #2e7d32; }
.chip-level-Профессионал { background: #fff3e0; color: #ef6c00; }
.chip-level-Эксперт { background: #f3e5f5; color: #6a1b9a; }
.charts-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.chart-card {
background: rgba(255,255,255,0.95);
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.chart-card h3 { margin-bottom: 15px; color: #333; font-size: 16px; }
.bar-chart { display: flex; flex-direction: column; gap: 8px; }
.bar-row { display: flex; align-items: center; gap: 10px; }
.bar-label { width: 120px; font-size: 13px; color: #555; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-track { flex: 1; height: 24px; background: #e0e0e0; border-radius: 4px; overflow: hidden; }
.bar-fill { height: 100%; border-radius: 4px; transition: width 0.3s; }
.bar-value { width: 40px; text-align: right; font-size: 13px; font-weight: 600; color: #333; }
.table-card {
background: rgba(255,255,255,0.95);
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
overflow-x: auto;
}
table { width: 100%; border-collapse: collapse; min-width: 900px; }
th, td { padding: 12px 10px; text-align: left; border-bottom: 1px solid #e0e0e0; }
th { background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); color: #fff; font-weight: 600; position: sticky; top: 0; }
tr:hover { background: #f5f5f5; }
.level-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.level-Специалист { background: #e8f5e9; color: #2e7d32; }
.level-Профессионал { background: #fff3e0; color: #ef6c00; }
.level-Эксперт { background: #f3e5f5; color: #6a1b9a; }
.city-badge { background: #e3f2fd; color: #1565c0; padding: 4px 10px; border-radius: 12px; font-size: 12px; }
.empty-state { text-align: center; padding: 60px 20px; color: #888; }
.export-btn {
padding: 10px 20px;
background: #2e7d32;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
margin-left: auto;
}
.export-btn:hover { background: #1b5e20; }
</style>
</head>
<body>
<div class="container">
<h1>📊 Дашборд сертификации сотрудников</h1>
<div class="stats">
<div class="stat-card">
<div class="num" id="stat-total">0</div>
<div class="label">Всего сертификатов</div>
</div>
<div class="stat-card">
<div class="num" id="stat-employees">0</div>
<div class="label">Сотрудников</div>
</div>
<div class="stat-card">
<div class="num" id="stat-specialist">0</div>
<div class="label">Специалистов</div>
</div>
<div class="stat-card">
<div class="num" id="stat-professional">0</div>
<div class="label">Профессионалов</div>
</div>
<div class="stat-card">
<div class="num" id="stat-expert">0</div>
<div class="label">Экспертов</div>
</div>
<div class="stat-card">
<div class="num" id="stat-vendors">0</div>
<div class="label">Вендоров</div>
</div>
</div>
<div class="filters">
<label>🔍 Поиск:</label>
<input type="text" id="search" placeholder="ФИО, должность, сертификат...">
<label>Город:</label>
<select id="filter-city"><option value="">Все</option></select>
<label>Уровень:</label>
<div id="filter-levels"></div>
<button class="export-btn" onclick="exportCsv()">📥 Выгрузить в Excel</button>
</div>
<div class="charts-row">
<div class="chart-card">
<h3>📈 По уровням сертификации</h3>
<div class="bar-chart" id="chart-levels"></div>
</div>
<div class="chart-card">
<h3>🏢 По городам</h3>
<div class="bar-chart" id="chart-cities"></div>
</div>
<div class="chart-card">
<h3>🏷️ Топ вендоров</h3>
<div class="bar-chart" id="chart-vendors"></div>
</div>
</div>
<div class="table-card">
<table>
<thead>
<tr>
<th>№</th>
<th>Город</th>
<th>ФИО сотрудника</th>
<th>Должность</th>
<th>Вендор</th>
<th>Сертификат</th>
<th>Уровень</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>
<div class="empty-state" id="empty-state">
<p>Ничего не найдено</p>
</div>
</div>
</div>
<script>
let allData = [];
let filteredData = [];
let selectedLevels = new Set();
async function load() {
const res = await fetch('/data.json').then(r => r.json());
allData = res.employees || [];
filteredData = [...allData];
initFilters();
render();
}
function initFilters() {
const cities = [...new Set(allData.map(d => d.city))].sort();
const citySelect = document.getElementById('filter-city');
cities.forEach(c => {
if (c) {
const opt = document.createElement('option');
opt.value = c;
opt.textContent = c;
citySelect.appendChild(opt);
}
});
const levels = ['Специалист', 'Профессионал', 'Эксперт'];
const levelsContainer = document.getElementById('filter-levels');
levels.forEach(l => {
const chip = document.createElement('span');
chip.className = 'chip chip-level-' + l;
chip.textContent = l;
chip.onclick = () => toggleLevel(l, chip);
levelsContainer.appendChild(chip);
});
}
function toggleLevel(level, chip) {
if (selectedLevels.has(level)) {
selectedLevels.delete(level);
chip.classList.remove('active');
} else {
selectedLevels.add(level);
chip.classList.add('active');
}
applyFilters();
}
function applyFilters() {
const search = document.getElementById('search').value.toLowerCase();
const city = document.getElementById('filter-city').value;
filteredData = allData.filter(d => {
const matchSearch = !search ||
(d.name && d.name.toLowerCase().includes(search)) ||
(d.position && d.position.toLowerCase().includes(search)) ||
(d.certificate && d.certificate.toLowerCase().includes(search)) ||
(d.vendor && d.vendor.toLowerCase().includes(search));
const matchCity = !city || d.city === city;
const matchLevel = selectedLevels.size === 0 || selectedLevels.has(d.level);
return matchSearch && matchCity && matchLevel;
});
render();
}
function render() {
renderStats();
renderCharts();
renderTable();
}
function renderStats() {
const total = allData.length;
const employees = new Set(allData.map(d => d.name)).size;
const specialist = allData.filter(d => d.level === 'Специалист').length;
const professional = allData.filter(d => d.level === 'Профессионал').length;
const expert = allData.filter(d => d.level === 'Эксперт').length;
const vendors = new Set(allData.map(d => d.vendor)).size;
document.getElementById('stat-total').textContent = total;
document.getElementById('stat-employees').textContent = employees;
document.getElementById('stat-specialist').textContent = specialist;
document.getElementById('stat-professional').textContent = professional;
document.getElementById('stat-expert').textContent = expert;
document.getElementById('stat-vendors').textContent = vendors;
}
function renderCharts() {
const levelCounts = {};
const cityCounts = {};
const vendorCounts = {};
allData.forEach(d => {
levelCounts[d.level] = (levelCounts[d.level] || 0) + 1;
cityCounts[d.city] = (cityCounts[d.city] || 0) + 1;
vendorCounts[d.vendor] = (vendorCounts[d.vendor] || 0) + 1;
});
renderBarChart('chart-levels', levelCounts, {
'Специалист': '#4caf50',
'Профессионал': '#ff9800',
'Эксперт': '#9c27b0'
});
const sortedCities = Object.entries(cityCounts).sort((a, b) => b[1] - a[1]);
renderBarChart('chart-cities', Object.fromEntries(sortedCities), '#1e3c72');
const sortedVendors = Object.entries(vendorCounts).sort((a, b) => b[1] - a[1]).slice(0, 8);
renderBarChart('chart-vendors', Object.fromEntries(sortedVendors), '#2a5298');
}
function renderBarChart(containerId, data, colorMap) {
const container = document.getElementById(containerId);
const max = Math.max(...Object.values(data), 1);
const getColor = key => typeof colorMap === 'object' ? (colorMap[key] || '#1e3c72') : colorMap;
container.innerHTML = Object.entries(data).map(([key, val]) => `
<div class="bar-row">
<div class="bar-label">${key || 'Без города'}</div>
<div class="bar-track">
<div class="bar-fill" style="width: ${(val / max) * 100}%; background: ${getColor(key)}"></div>
</div>
<div class="bar-value">${val}</div>
</div>
`).join('');
}
function renderTable() {
const tbody = document.getElementById('table-body');
const empty = document.getElementById('empty-state');
if (filteredData.length === 0) {
tbody.innerHTML = '';
empty.style.display = 'block';
return;
}
empty.style.display = 'none';
tbody.innerHTML = filteredData.map((d, i) => `
<tr>
<td>${i + 1}</td>
<td><span class="city-badge">${d.city}</span></td>
<td>${escapeHtml(d.name || '—')}</td>
<td>${escapeHtml(d.position || '—')}</td>
<td>${escapeHtml(d.vendor || '—')}</td>
<td>${escapeHtml(d.certificate || '—')}</td>
<td><span class="level-badge level-${d.level}">${d.level}</span></td>
</tr>
`).join('');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function exportCsv() {
const head = ['№', 'Город', 'ФИО', 'Должность', 'Вендор', 'Сертификат', 'Уровень'];
const body = filteredData.map((d, i) => [
i + 1, d.city, d.name, d.position, d.vendor, d.certificate, d.level
].map(v => '"' + String(v || '').replace(/"/g, '""') + '"').join(';'));
const csv = '\uFEFF' + [head.join(';'), ...body].join('\n');
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }));
a.download = 'sertifikacii.csv';
a.click();
}
document.getElementById('search').addEventListener('input', applyFilters);
document.getElementById('filter-city').addEventListener('change', applyFilters);
load();
</script>
</body>
</html>