snp-report/index.html

305 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>
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<style>
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7}
*{box-sizing:border-box;margin:0;padding:0}
body{font:17px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:var(--white)}
.container{max-width:1140px;margin:0 auto;padding:80px 24px}
.hero{background:var(--ink);color:var(--white)}
.hero h1{font-size:56px;font-weight:800;line-height:1.05;margin-bottom:16px}
.hero p{font-size:20px;color:#9aa3b2;max-width:700px;margin-bottom:32px}
.btn{display:inline-block;background:var(--cyan);color:var(--ink);padding:14px 28px;border-radius:8px;font-weight:700;font-size:16px;border:none;cursor:pointer;text-decoration:none;transition:background .15s}
.btn:hover{background:#1be5ff}
.btn-outline{background:transparent;color:var(--ink);border:2px solid var(--ink);padding:12px 26px}
.btn-outline:hover{background:var(--gray-100)}
.section h2{font-size:36px;font-weight:700;margin-bottom:8px}
.section .subtitle{color:var(--gray-500);margin-bottom:32px;font-size:18px}
.card{background:var(--gray-100);border-radius:16px;padding:32px;margin-bottom:24px}
.drop-zone{border:2px dashed var(--gray-500);border-radius:16px;padding:60px 24px;text-align:center;cursor:pointer;transition:border-color .2s,background .2s}
.drop-zone:hover,.drop-zone.dragover{border-color:var(--cyan);background:var(--cyan-50)}
.drop-zone.has-file{border-color:var(--cyan);background:var(--cyan-50);padding:24px}
.drop-zone-icon{font-size:48px;margin-bottom:12px;display:block}
.drop-zone-text{font-size:18px}
.drop-zone-hint{color:var(--gray-500);font-size:14px;margin-top:8px}
.drop-zone input{display:none}
.filename{font-weight:700;font-size:18px}
.filename small{font-weight:400;color:var(--gray-500);font-size:14px}
.filters{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-bottom:24px}
.filter-group label{display:block;font-size:14px;font-weight:600;color:var(--gray-500);margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}
.filter-group select{width:100%;padding:12px 16px;border:2px solid var(--gray-100);border-radius:8px;font-size:16px;background:var(--white);appearance:auto;cursor:pointer}
.filter-group select:focus{outline:none;border-color:var(--cyan)}
.summary-bar{display:flex;flex-wrap:wrap;gap:16px;justify-content:space-between;align-items:center;margin-bottom:16px;padding:16px 24px;background:var(--white);border-radius:12px;border:1px solid var(--gray-100)}
.summary-count{font-size:20px;font-weight:700}
.summary-count span{color:var(--cyan)}
.table-wrap{overflow-x:auto;border-radius:12px;border:1px solid var(--gray-100)}
table{width:100%;border-collapse:collapse;font-size:15px}
thead{background:var(--ink);color:var(--white)}
thead th{padding:14px 16px;text-align:left;white-space:nowrap;font-weight:600;font-size:13px;text-transform:uppercase;letter-spacing:.5px}
tbody td{padding:12px 16px;border-bottom:1px solid var(--gray-100)}
tbody tr:last-child td{border-bottom:none}
tbody tr:hover{background:var(--cyan-50)}
.hidden{display:none!important}
.empty-state{text-align:center;padding:60px 24px;color:var(--gray-500)}
.empty-state-icon{font-size:64px;display:block;margin-bottom:16px}
@media (max-width:640px){
.hero h1{font-size:36px}.hero p{font-size:17px}
.section h2{font-size:28px}.container{padding:48px 20px}
.drop-zone{padding:40px 16px}
.filters{grid-template-columns:1fr}
}
</style>
</head>
<body>
<section class="hero">
<div class="container">
<h1>📋 Выборка СНП по КАТО</h1>
<p>Загрузите Excel-файл со справочником населённых пунктов, выберите область → район → округ — и получите отфильтрованную выборку с возможностью скачать в Excel.</p>
</div>
</section>
<section id="app" class="section">
<div class="container">
<div class="card">
<h2>📁 Загрузить файл</h2>
<p class="subtitle">Excel-файл с колонками: КАТО, область, район, округ и др.</p>
<div class="drop-zone" id="dropZone">
<input type="file" id="fileInput" accept=".xlsx,.xls,.csv">
<span class="drop-zone-icon">📂</span>
<div class="drop-zone-text">Перетащите файл сюда или <strong>выберите</strong></div>
<div class="drop-zone-hint">Форматы: .xlsx, .xls, .csv</div>
</div>
</div>
<div id="resultArea" class="hidden">
<div class="card">
<h2>🔍 Фильтр</h2>
<p class="subtitle">Выберите регион, чтобы отфильтровать населённые пункты</p>
<div class="filters" id="filtersContainer"></div>
<div class="summary-bar" id="summaryBar">
<div class="summary-count">Найдено: <span id="countDisplay">0</span> населённых пунктов</div>
<button class="btn" id="exportBtn">⬇ Скачать Excel</button>
</div>
<div id="tableContainer">
<div class="table-wrap"><table><thead id="tableHead"></thead><tbody id="tableBody"></tbody></table></div>
</div>
</div>
</div>
</div>
</section>
<script>
let allData = [];
let filteredData = [];
let dataColumns = [];
let hierarchy = {};
const COL_KEYWORDS = {
kato: ['като', 'код', 'katо', 'kato'],
oblast: ['област', 'регион', 'oblast', 'region'],
rayon: ['район', 'rayon', 'district'],
okrug: ['округ', 'okrug', 'сельск'],
snp: ['снп', 'населен', 'село', 'поселок', 'аул', 'snp', 'settlement', 'name']
};
function detectColumn(name) {
const n = name.toLowerCase().trim();
for (const [key, keywords] of Object.entries(COL_KEYWORDS)) {
if (keywords.some(k => n.includes(k))) return key;
}
return null;
}
function initHierarchy(data) {
const katoIx = dataColumns.indexOf('kato') !== -1 ? dataColumns.indexOf('kato') : 0;
const obIx = dataColumns.indexOf('oblast');
const raIx = dataColumns.indexOf('rayon');
const okIx = dataColumns.indexOf('okrug');
hierarchy = { oblasts: {}, rayons: {}, okrugs: {} };
data.forEach(row => {
const ob = obIx !== -1 && row[obIx] ? String(row[obIx]).trim() : '';
const ra = raIx !== -1 && row[raIx] ? String(row[raIx]).trim() : '';
const ok = okIx !== -1 && row[okIx] ? String(row[okIx]).trim() : '';
if (ob && !hierarchy.oblasts[ob]) hierarchy.oblasts[ob] = new Set();
if (ob && ra) {
if (!hierarchy.rayons[ob]) hierarchy.rayons[ob] = new Set();
hierarchy.rayons[ob].add(ra);
}
if (ob && ra && ok) {
const key = ob + '|' + ra;
if (!hierarchy.okrugs[key]) hierarchy.okrugs[key] = new Set();
hierarchy.okrugs[key].add(ok);
}
});
hierarchy.oblasts = Object.keys(hierarchy.oblasts).sort();
Object.keys(hierarchy.rayons).forEach(k => hierarchy.rayons[k] = [...hierarchy.rayons[k]].sort());
Object.keys(hierarchy.okrugs).forEach(k => hierarchy.okrugs[k] = [...hierarchy.okrugs[k]].sort());
}
function renderFilters() {
const container = document.getElementById('filtersContainer');
container.innerHTML = `
<div class="filter-group">
<label>Область / Регион</label>
<select id="filterOblast"><option value="">— Все —</option>
${hierarchy.oblasts.map(o => `<option value="${o}">${o}</option>`).join('')}
</select>
</div>
<div class="filter-group">
<label>Район</label>
<select id="filterRayon"><option value="">— Все —</option></select>
</div>
<div class="filter-group">
<label>Округ</label>
<select id="filterOkrug"><option value="">— Все —</option></select>
</div>
`;
const selOblast = document.getElementById('filterOblast');
const selRayon = document.getElementById('filterRayon');
const selOkrug = document.getElementById('filterOkrug');
selOblast.addEventListener('change', () => {
updateRayonOptions();
selOkrug.innerHTML = '<option value="">— Все —</option>';
applyFilter();
});
selRayon.addEventListener('change', () => {
updateOkrugOptions();
applyFilter();
});
selOkrug.addEventListener('change', applyFilter);
}
function updateRayonOptions() {
const ob = document.getElementById('filterOblast').value;
const sel = document.getElementById('filterRayon');
const rayons = ob && hierarchy.rayons[ob] ? hierarchy.rayons[ob] : [];
sel.innerHTML = '<option value="">— Все —</option>' + rayons.map(r => `<option value="${r}">${r}</option>`).join('');
}
function updateOkrugOptions() {
const ob = document.getElementById('filterOblast').value;
const ra = document.getElementById('filterRayon').value;
const sel = document.getElementById('filterOkrug');
const key = ob + '|' + ra;
const okrugs = ob && ra && hierarchy.okrugs[key] ? hierarchy.okrugs[key] : [];
sel.innerHTML = '<option value="">— Все —</option>' + okrugs.map(o => `<option value="${o}">${o}</option>`).join('');
}
function applyFilter() {
const ob = document.getElementById('filterOblast').value;
const ra = document.getElementById('filterRayon').value;
const ok = document.getElementById('filterOkrug').value;
const obIx = dataColumns.indexOf('oblast');
const raIx = dataColumns.indexOf('rayon');
const okIx = dataColumns.indexOf('okrug');
filteredData = allData.filter(row => {
const rowOb = obIx !== -1 ? String(row[obIx] || '').trim() : '';
const rowRa = raIx !== -1 ? String(row[raIx] || '').trim() : '';
const rowOk = okIx !== -1 ? String(row[okIx] || '').trim() : '';
if (ob && rowOb !== ob) return false;
if (ra && rowRa !== ra) return false;
if (ok && rowOk !== ok) return false;
return true;
});
renderTable();
}
function renderTable() {
document.getElementById('countDisplay').textContent = filteredData.length;
const head = document.getElementById('tableHead');
const body = document.getElementById('tableBody');
if (filteredData.length === 0) {
head.innerHTML = '';
body.innerHTML = '<tr><td colspan="99"><div class="empty-state"><span class="empty-state-icon">🔍</span>Нет данных по выбранным фильтрам</div></td></tr>';
return;
}
const headers = allData.columns || Object.keys(allData[0] || {});
head.innerHTML = '<tr>' + headers.map(h => `<th>${h}</th>`).join('') + '</tr>';
body.innerHTML = filteredData.map(row => {
const vals = Array.isArray(row) ? row : headers.map(h => row[h] ?? '');
return '<tr>' + vals.map(v => `<td>${v != null ? v : ''}</td>`).join('') + '</tr>';
}).join('');
}
function handleFile(file) {
const reader = new FileReader();
reader.onload = function(e) {
try {
const wb = XLSX.read(e.target.result, { type: 'array' });
const ws = wb.Sheets[wb.SheetNames[0]];
const json = XLSX.utils.sheet_to_json(ws, { header: 1 });
if (json.length < 2) { alert('Файл пуст или содержит только заголовки'); return; }
const headers = json[0].map(h => String(h).trim());
const rows = json.slice(1).filter(r => r.some(c => c != null && c !== ''));
dataColumns = headers.map((h, i) => detectColumn(h) || ('col_' + i));
allData = rows.map(r => {
const obj = {};
headers.forEach((h, i) => { obj[h] = r[i] != null ? r[i] : ''; });
obj.columns = headers;
return obj;
});
allData.columns = headers;
const zone = document.getElementById('dropZone');
zone.classList.add('has-file');
zone.innerHTML = `<span class="drop-zone-icon">✅</span><div class="filename">${file.name} <small>(${allData.length} записей)</small></div>`;
initHierarchy(allData);
renderFilters();
applyFilter();
document.getElementById('resultArea').classList.remove('hidden');
} catch(err) {
alert('Ошибка при чтении файла: ' + err.message);
}
};
reader.readAsArrayBuffer(file);
}
document.getElementById('fileInput').addEventListener('change', e => {
if (e.target.files[0]) handleFile(e.target.files[0]);
});
const dropZone = document.getElementById('dropZone');
dropZone.addEventListener('click', () => document.getElementById('fileInput').click());
dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragover'); });
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
dropZone.addEventListener('drop', e => {
e.preventDefault();
dropZone.classList.remove('dragover');
if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
});
document.getElementById('exportBtn').addEventListener('click', () => {
if (filteredData.length === 0) { alert('Нет данных для экспорта'); return; }
const headers = allData.columns;
const wsData = [headers];
filteredData.forEach(row => {
wsData.push(headers.map(h => row[h] != null ? row[h] : ''));
});
const ws = XLSX.utils.aoa_to_sheet(wsData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'СНП');
XLSX.writeFile(wb, 'snp-vyborka.xlsx');
});
</script>
</body>
</html>