proshu-sozdat-sayt-dlya-otch/index.html

223 lines
10 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>
* { margin: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: #f5f5f7; color: #1d1d1f; padding: 20px; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #1d1d1f; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.stat-card h3 { font-size: 14px; color: #6e6e73; margin-bottom: 8px; font-weight: 500; }
.stat-card .value { font-size: 32px; font-weight: 700; color: #1d1d1f; }
.stat-card.problem .value { color: #dc3545; }
.stat-card.partial .value { color: #ffc107; }
.stat-card.active .value { color: #28a745; }
.stat-card.closed .value { color: #6c757d; }
.controls { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }
.search-box { flex: 1; min-width: 250px; padding: 12px 16px; border: 1px solid #d2d2d7; border-radius: 10px; font-size: 15px; background: white; }
.filter-btn { padding: 12px 20px; border: 1px solid #d2d2d7; border-radius: 10px; background: white; cursor: pointer; font-size: 14px; transition: all 0.2s; }
.filter-btn:hover { background: #f0f0f0; }
.filter-btn.active { background: #007aff; color: white; border-color: #007aff; }
.export-btn { padding: 12px 20px; border: none; border-radius: 10px; background: #28a745; color: white; cursor: pointer; font-size: 14px; font-weight: 600; }
.export-btn:hover { background: #218838; }
.table-container { background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead { background: #f8f9fa; position: sticky; top: 0; }
th { text-align: left; padding: 12px 16px; font-weight: 600; color: #495057; border-bottom: 2px solid #dee2e6; white-space: nowrap; }
td { padding: 12px 16px; border-bottom: 1px solid #dee2e6; vertical-align: top; }
tbody tr:hover { background: #f8f9fa; }
.status-badge { display: inline-block; padding: 4px 10px; border-radius: 20px; font-size: 12px; font-weight: 600; }
.status-problem { background: #f8d7da; color: #721c24; }
.status-partial { background: #fff3cd; color: #856404; }
.status-other { background: #e2e3e5; color: #383d41; }
.status-sd-active { color: #28a745; font-weight: 600; }
.status-sd-closed { color: #6c757d; font-weight: 600; }
.amount { font-family: "Courier New", monospace; text-align: right; }
.region-col { font-weight: 600; color: #007aff; }
.no-results { padding: 40px; text-align: center; color: #6c757d; font-size: 16px; }
</style>
</head>
<body>
<div class="container">
<h1>📊 Отчёт по проблемной дебиторской задолженности (ПДЗ)</h1>
<div class="stats">
<div class="stat-card">
<h3>Всего записей</h3>
<div class="value" id="stat-total">0</div>
</div>
<div class="stat-card problem">
<h3>Проблемные</h3>
<div class="value" id="stat-problem">0</div>
</div>
<div class="stat-card partial">
<h3>Частично проблемные</h3>
<div class="value" id="stat-partial">0</div>
</div>
<div class="stat-card active">
<h3>Действующие</h3>
<div class="value" id="stat-active">0</div>
</div>
<div class="stat-card closed">
<h3>Снятые</h3>
<div class="value" id="stat-closed">0</div>
</div>
</div>
<div class="controls">
<input type="text" class="search-box" id="search" placeholder="Поиск по контрагенту, БИН, адресу...">
<button class="filter-btn active" data-filter="all">Все</button>
<button class="filter-btn" data-filter="problem">Проблемные</button>
<button class="filter-btn" data-filter="partial">Частично проблемные</button>
<button class="filter-btn" data-filter="active">Действующие</button>
<button class="filter-btn" data-filter="closed">Снятые</button>
<button class="export-btn" onclick="exportToExcel()">📥 Выгрузить в Excel</button>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Регион</th>
<th>Район</th>
<th>Город</th>
<th>Абонент</th>
<th>БИН</th>
<th>ДЗ итого</th>
<th>ПДЗ</th>
<th>ИТОГО ПДЗ</th>
<th>Статус С/Д</th>
<th>Статус</th>
<th>Менеджер</th>
<th>Ход работы</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>
<div class="no-results" id="no-results" style="display: none;">Нет записей по заданным фильтрам</div>
</div>
</div>
<script>
let data = [];
let filteredData = [];
let currentFilter = 'all';
fetch('/api/data')
.then(r => r.json())
.then(rows => {
data = rows;
filteredData = rows;
updateStats();
renderTable();
})
.catch(err => {
console.error('Ошибка загрузки данных:', err);
document.getElementById('table-body').innerHTML = '<tr><td colspan="12" style="text-align:center;padding:40px;color:#dc3545;">Ошибка загрузки данных. Убедитесь, что сервер запущен.</td></tr>';
});
function updateStats() {
document.getElementById('stat-total').textContent = data.length;
document.getElementById('stat-problem').textContent = data.filter(r => r['Статус'] === 'Проблемный').length;
document.getElementById('stat-partial').textContent = data.filter(r => r['Статус'] === 'Частично проблемный').length;
document.getElementById('stat-active').textContent = data.filter(r => r['Статус С/Д'] === 'действующий').length;
document.getElementById('stat-closed').textContent = data.filter(r => r['Статус С/Д'] === 'снятый').length;
}
function applyFilters() {
const search = document.getElementById('search').value.toLowerCase();
filteredData = data.filter(row => {
const matchesSearch = search === '' ||
(row['Абонент'] && row['Абонент'].toLowerCase().includes(search)) ||
(row['БИН'] && String(row['БИН']).includes(search)) ||
(row['Адрес'] && row['Адрес'].toLowerCase().includes(search)) ||
(row['Район'] && row['Район'].toLowerCase().includes(search)) ||
(row['Город'] && row['Город'].toLowerCase().includes(search)) ||
(row['Менеджер'] && row['Менеджер'].toLowerCase().includes(search));
let matchesFilter = true;
if (currentFilter === 'problem') matchesFilter = row['Статус'] === 'Проблемный';
if (currentFilter === 'partial') matchesFilter = row['Статус'] === 'Частично проблемный';
if (currentFilter === 'active') matchesFilter = row['Статус С/Д'] === 'действующий';
if (currentFilter === 'closed') matchesFilter = row['Статус С/Д'] === 'снятый';
return matchesSearch && matchesFilter;
});
renderTable();
}
function renderTable() {
const tbody = document.getElementById('table-body');
const noResults = document.getElementById('no-results');
if (filteredData.length === 0) {
tbody.innerHTML = '';
noResults.style.display = 'block';
return;
}
noResults.style.display = 'none';
tbody.innerHTML = filteredData.map(row => {
const statusClass = row['Статус'] === 'Проблемный' ? 'status-problem' :
row['Статус'] === 'Частично проблемный' ? 'status-partial' : 'status-other';
const sdClass = row['Статус С/Д'] === 'действующий' ? 'status-sd-active' : 'status-sd-closed';
return `<tr>
<td class="region-col">${row['Регион'] || ''}</td>
<td>${row['Район'] || ''}</td>
<td>${row['Город'] || ''}</td>
<td title="${(row['Абонент'] || '').replace(/"/g, '&quot;')}">${truncate(row['Абонент'], 60)}</td>
<td>${row['БИН'] || ''}</td>
<td class="amount">${formatAmount(row['ДЗ итого'])}</td>
<td class="amount">${formatAmount(row['ПДЗ'])}</td>
<td class="amount"><strong>${formatAmount(row['ИТОГО ПДЗ'])}</strong></td>
<td class="${sdClass}">${row['Статус С/Д'] || ''}</td>
<td><span class="status-badge ${statusClass}">${row['Статус'] || ''}</span></td>
<td>${row['Менеджер'] || ''}</td>
<td title="${(row['Ход работы'] || '').replace(/"/g, '&quot;')}">${truncate(row['Ход работы'], 80)}</td>
</tr>`;
}).join('');
}
function truncate(str, max) {
if (!str) return '';
return str.length > max ? str.substring(0, max) + '...' : str;
}
function formatAmount(val) {
if (val === null || val === undefined || val === '') return '';
return Number(val).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function exportToExcel() {
const ws = XLSX.utils.json_to_sheet(filteredData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'ПДЗ Отчёт');
XLSX.writeFile(wb, 'Otchet_PDZ.xlsx');
}
document.getElementById('search').addEventListener('input', applyFilters);
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentFilter = btn.dataset.filter;
applyFilters();
});
});
</script>
</body>
</html>