223 lines
10 KiB
HTML
223 lines
10 KiB
HTML
<!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, '"')}">${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, '"')}">${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>
|