Compare commits

..

5 Commits
main ... pages

3 changed files with 291 additions and 123 deletions

176
data.json
View File

@ -1,79 +1,151 @@
{ {
"total_applications": 387, "total_unique_applications": 129,
"unique_clients": 128, "total_clients_with_repeats": 7,
"total_repeats": 8, "clients": [
"repeats": [
{ {
"company": "АО \"КАЗТЕЛЕПОРТ\"", "company": "АО \"КАЗТЕЛЕПОРТ\"",
"phone": 7002242610, "phone": "7002294367",
"first_id": 561827, "total_requests": 3,
"first_date": "17.12.2025",
"last_date": "03.08.2026",
"days_span": 229,
"requests": [
{
"id": 562274,
"date": "17.12.2025",
"channel": 5360481426
},
{
"id": 563603,
"date": "30.01.2026",
"channel": 5360481426
},
{
"id": 570640,
"date": "03.08.2026",
"channel": 5360481426
}
]
},
{
"company": "АО \"КАЗТЕЛЕПОРТ\"",
"phone": "7002242610",
"total_requests": 2,
"first_date": "03.12.2025", "first_date": "03.12.2025",
"repeat_id": 568131, "last_date": "04.06.2026",
"repeat_date": "04.06.2026", "days_span": 183,
"days_between": 183 "requests": [
{
"id": 561827,
"date": "03.12.2025",
"channel": 5361864230
},
{
"id": 568131,
"date": "04.06.2026",
"channel": 5361864230
}
]
}, },
{ {
"company": "АО \"КАЗТЕЛЕПОРТ\"", "company": "АО \"КАЗТЕЛЕПОРТ\"",
"phone": 7002294367, "phone": "7081484578",
"first_id": 563603, "total_requests": 2,
"first_date": "30.01.2026",
"repeat_id": 570640,
"repeat_date": "03.08.2026",
"days_between": 185
},
{
"company": "АО \"КАЗТЕЛЕПОРТ\"",
"phone": 7081484578,
"first_id": 539438,
"first_date": "02.05.2024", "first_date": "02.05.2024",
"repeat_id": 566477, "last_date": "16.04.2026",
"repeat_date": "16.04.2026", "days_span": 714,
"days_between": 714 "requests": [
{
"id": 539438,
"date": "02.05.2024",
"channel": 5293570099
},
{
"id": 566477,
"date": "16.04.2026",
"channel": 5293570099
}
]
}, },
{ {
"company": "АО \"КАЗТЕЛЕПОРТ\"", "company": "АО \"КАЗТЕЛЕПОРТ\"",
"phone": 7081655470, "phone": "7081655470",
"first_id": 561826, "total_requests": 2,
"first_date": "03.12.2025", "first_date": "03.12.2025",
"repeat_id": 568369, "last_date": "10.06.2026",
"repeat_date": "10.06.2026", "days_span": 189,
"days_between": 189 "requests": [
{
"id": 561826,
"date": "03.12.2025",
"channel": 5361874210
},
{
"id": 568369,
"date": "10.06.2026",
"channel": 5361874210
}
]
}, },
{ {
"company": "АО \"КАЗТЕЛЕПОРТ\"", "company": "АО \"КАЗТЕЛЕПОРТ\"",
"phone": 7081916257, "phone": "7081916257",
"first_id": 563614, "total_requests": 2,
"first_date": "30.01.2026", "first_date": "30.01.2026",
"repeat_id": 568951, "last_date": "24.06.2026",
"repeat_date": "24.06.2026", "days_span": 144,
"days_between": 144 "requests": [
{
"id": 563614,
"date": "30.01.2026",
"channel": 5361394989
},
{
"id": 568951,
"date": "24.06.2026",
"channel": 5361394989
}
]
}, },
{ {
"company": "АО \"КАЗТЕЛЕПОРТ\"", "company": "АО \"КАЗТЕЛЕПОРТ\"",
"phone": 7086768198, "phone": "7086768198",
"first_id": 562359, "total_requests": 2,
"first_date": "19.12.2025", "first_date": "19.12.2025",
"repeat_id": 568953, "last_date": "24.06.2026",
"repeat_date": "24.06.2026", "days_span": 187,
"days_between": 187 "requests": [
}, {
{ "id": 562359,
"company": "АО \"КАЗТЕЛЕПОРТ\"", "date": "19.12.2025",
"phone": 7088025661, "channel": 5290291540
"first_id": 564403, },
"first_date": "20.02.2026", {
"repeat_id": 570643, "id": 568953,
"repeat_date": "03.08.2026", "date": "24.06.2026",
"days_between": 164 "channel": 5290291540
}
]
}, },
{ {
"company": "АО «BEREKE BANK» ДОЧЕРНИЙ БАНК LESHA BANK LLC (PUBLIC))", "company": "АО «BEREKE BANK» ДОЧЕРНИЙ БАНК LESHA BANK LLC (PUBLIC))",
"phone": 7081784711, "phone": "7081784711",
"first_id": 557582, "total_requests": 2,
"first_date": "11.08.2025", "first_date": "11.08.2025",
"repeat_id": 566616, "last_date": "21.04.2026",
"repeat_date": "21.04.2026", "days_span": 252,
"days_between": 252 "requests": [
{
"id": 557582,
"date": "11.08.2025",
"channel": 5116262945
},
{
"id": 566616,
"date": "21.04.2026",
"channel": 5116262945
}
]
} }
] ]
} }

View File

@ -3,82 +3,110 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>Алтел — Повторные обращения клиентов</title> <title>Алтел — Все повторные обращения клиентов</title>
<style> <style>
:root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7;--red:#FF4757;--orange:#FFA502} :root{--ink:#0F1218;--cyan:#00E5FF;--cyan-50:#E8FCFF;--white:#fff;--gray-500:#5B6573;--gray-100:#F2F4F7;--red:#FF4757;--orange:#FFA502;--green:#2ED573}
*{box-sizing:border-box;margin:0;padding:0} *{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)} body{font:17px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;color:var(--ink);background:var(--white)}
.container{max-width:1200px;margin:0 auto;padding:80px 24px} .container{max-width:1400px;margin:0 auto;padding:80px 24px}
.hero{background:var(--ink);color:var(--white);padding:100px 24px} .hero{background:var(--ink);color:var(--white);padding:100px 24px}
.hero h1{font-size:48px;font-weight:800;line-height:1.1;margin-bottom:20px} .hero h1{font-size:42px;font-weight:800;line-height:1.1;margin-bottom:20px}
.hero p{font-size:20px;color:#9aa3b2;max-width:800px} .hero p{font-size:18px;color:#9aa3b2;max-width:900px;margin-bottom:32px}
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:24px;margin-top:40px} .stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:20px;margin-top:32px}
.stat-card{background:rgba(255,255,255,0.1);border-radius:12px;padding:24px;text-align:center} .stat-card{background:rgba(255,255,255,0.1);border-radius:12px;padding:20px;text-align:center}
.stat-card .num{font-size:48px;font-weight:800;color:var(--cyan);display:block} .stat-card .num{font-size:42px;font-weight:800;color:var(--cyan);display:block}
.stat-card .label{color:#9aa3b2;font-size:14px;margin-top:8px;display:block} .stat-card .label{color:#9aa3b2;font-size:13px;margin-top:8px;display:block;text-transform:uppercase;letter-spacing:1px}
.section{padding:80px 24px} .section{padding:80px 24px}
.section h2{font-size:36px;font-weight:700;margin-bottom:16px} .section h2{font-size:32px;font-weight:700;margin-bottom:12px}
.section .subtitle{color:var(--gray-500;font-size:18px;margin-bottom:32px} .section .subtitle{color:var(--gray-500);font-size:17px;margin-bottom:32px}
.controls{display:flex;gap:16px;margin-bottom:24px;flex-wrap:wrap}
.controls input{padding:12px 16px;border:2px solid var(--gray-100);border-radius:8px;font-size:16px;min-width:250px}
.controls input:focus{outline:none;border-color:var(--cyan)}
.controls select{padding:12px 16px;border:2px solid var(--gray-100);border-radius:8px;font-size:16px;background:var(--white)}
.table-container{overflow-x:auto;background:var(--gray-100);border-radius:16px;padding:24px} .table-container{overflow-x:auto;background:var(--gray-100);border-radius:16px;padding:24px}
table{width:100%;border-collapse:collapse;font-size:15px} table{width:100%;border-collapse:collapse;font-size:14px}
th{text-align:left;background:var(--ink);color:var(--white);padding:16px;font-weight:700;font-size:14px;text-transform:uppercase;letter-spacing:0.5px} th{text-align:left;background:var(--ink);color:var(--white);padding:14px 12px;font-weight:700;font-size:13px;text-transform:uppercase;letter-spacing:0.5px;position:sticky;top:0}
td{padding:16px;border-bottom:1px solid #e0e4e8} td{padding:14px 12px;border-bottom:1px solid #e0e4e8;vertical-align:top}
tr:hover{background:#fff} tr:hover{background:#fff}
.badge{display:inline-block;padding:6px 12px;border-radius:20px;font-size:13px;font-weight:700} .company-col{font-weight:700;min-width:200px;font-size:15px}
.badge-warning{background:var(--orange);color:#000} .phone-col{font-family:monospace;color:var(--gray-500);white-space:nowrap}
.badge-danger{background:var(--red);color:#fff} .count-col{font-weight:800;font-size:18px;text-align:center}
.company-col{font-weight:600;min-width:250px} .badge{display:inline-block;padding:6px 12px;border-radius:20px;font-size:12px;font-weight:700}
.phone-col{color:var(--gray-500);font-family:monospace} .badge-critical{background:var(--red);color:#fff}
.id-col{font-family:monospace;color:var(--gray-500)} .badge-high{background:var(--orange);color:#000}
.days-col{font-weight:700;color:var(--ink)} .badge-medium{background:var(--cyan);color:var(--ink)}
.highlight{background:var(--cyan-50)} .history-cell{font-size:13px}
@media (max-width:768px){ .history-item{background:var(--white);border-left:3px solid var(--cyan);padding:8px 10px;margin-bottom:6px;border-radius:0 6px 6px 0}
.history-item:last-child{margin-bottom:0}
.history-date{font-weight:700;font-size:13px;margin-bottom:4px}
.history-details{display:flex;gap:12px;color:var(--gray-500);font-size:12px}
.history-details span{background:var(--gray-100);padding:3px 8px;border-radius:4px}
.days-badge{background:var(--gray-100);padding:4px 10px;border-radius:4px;font-size:12px;font-weight:700;display:inline-block}
.expand-btn{background:var(--cyan);border:none;color:var(--ink);padding:6px 14px;border-radius:6px;cursor:pointer;font-weight:700;font-size:12px}
.expand-btn:hover{background:#1be5ff}
.hidden-row{display:none}
@media (max-width:1024px){
.hero h1{font-size:32px} .hero h1{font-size:32px}
.section h2{font-size:28px} .section h2{font-size:26px}
.stats{grid-template-columns:1fr}
.container{padding:48px 20px} .container{padding:48px 20px}
th,td{padding:12px 8px;font-size:13px} table{font-size:13px}
} }
</style> </style>
</head> </head>
<body> <body>
<section class="hero"> <section class="hero">
<div class="container" style="padding:40px 0"> <div class="container" style="padding:40px 0">
<h1>📊 Анализ повторных обращений клиентов Алтел</h1> <h1>📋 История всех обращений клиентов Алтел</h1>
<p>Мониторинг клиентов, которые обращались повторно после истечения 3-месячного срока устранения проблем. Проблема не была решена в установленный срок.</p> <p>Полная информация по клиентам с повторными обращениями: сколько раз обращался, все номера заявок и каналов, даты каждого обращения</p>
<div class="stats"> <div class="stats">
<div class="stat-card"> <div class="stat-card">
<span class="num" id="totalApps">0</span> <span class="num" id="totalClients">0</span>
<span class="label">Клиентов с повторами</span>
</div>
<div class="stat-card">
<span class="num" id="maxRequests">0</span>
<span class="label">Макс. обращений</span>
</div>
<div class="stat-card">
<span class="num" id="criticalClients">0</span>
<span class="label">Критично (5+ раз)</span>
</div>
<div class="stat-card">
<span class="num" id="totalRequests">0</span>
<span class="label">Всего заявок</span> <span class="label">Всего заявок</span>
</div> </div>
<div class="stat-card">
<span class="num" id="uniqueClients">0</span>
<span class="label">Уникальных клиентов</span>
</div>
<div class="stat-card">
<span class="num" id="repeatCount" style="color:var(--red)">0</span>
<span class="label">Повторных (>3 мес)</span>
</div>
</div> </div>
</div> </div>
</section> </section>
<section class="section"> <section class="section">
<div class="container"> <div class="container">
<h2>🔴 Клиенты с повторными обращениями после 3 месяцев</h2> <h2>🔍 Все клиенты с повторными обращениями</h2>
<p class="subtitle">Период между заявками превышает 90 дней — проблема осталась нерешённой</p> <p class="subtitle">Показаны только уникальные заявки (дубликаты исключены). Отсортировано по количеству обращений.</p>
<div class="controls">
<input type="text" id="searchInput" placeholder="🔍 Поиск по компании или телефону...">
<select id="filterSelect">
<option value="all">Все обращения</option>
<option value="5+">5+ обращений</option>
<option value="3-4">3-4 обращения</option>
<option value="2">2 обращения</option>
</select>
</div>
<div class="table-container"> <div class="table-container">
<table id="repeatsTable"> <table>
<thead> <thead>
<tr> <tr>
<th style="width:60px">#</th>
<th>Компания</th> <th>Компания</th>
<th>Телефон</th> <th>Телефон</th>
<th>Первая заявка</th> <th style="text-align:center">Раз обращался</th>
<th>Повторная заявка</th> <th>Период</th>
<th>Дней между</th> <th>История заявок (№ заявки / № канала / Дата)</th>
<th>Статус</th>
</tr> </tr>
</thead> </thead>
<tbody id="summaryRow"></tbody>
<tbody id="tableBody"></tbody> <tbody id="tableBody"></tbody>
</table> </table>
</div> </div>
@ -86,37 +114,105 @@ tr:hover{background:#fff}
</section> </section>
<script> <script>
const data = {"total_applications":387,"unique_clients":128,"total_repeats":8,"repeats":[{"company":"АО \"КАЗТЕЛЕПОРТ\"","phone":7081121715,"first_id":549607,"first_date":"05.01.2025","repeat_id":568952,"repeat_date":"24.06.2026","days_between":171},{"company":"АО \"КАЗТЕЛЕПОРТ\"","phone":7002242610,"first_id":561827,"first_date":"03.12.2025","repeat_id":568131,"repeat_date":"04.06.2026","days_between":183},{"company":"АО \"КАЗТЕЛЕПОРТ\"","phone":7002294367,"first_id":563603,"first_date":"30.01.2026","repeat_id":570640,"repeat_date":"03.08.2026","days_between":185},{"company":"АО \"КАЗТЕЛЕПОРТ\"","phone":7081484578,"first_id":539438,"first_date":"02.05.2024","repeat_id":566477,"repeat_date":"16.04.2026","days_between":714},{"company":"АО \"КАЗТЕЛЕПОРТ\"","phone":7081655470,"first_id":561826,"first_date":"03.12.2025","repeat_id":568369,"repeat_date":"10.06.2026","days_between":189},{"company":"АО \"КАЗТЕЛЕПОРТ\"","phone":7081916257,"first_id":563614,"first_date":"30.01.2026","repeat_id":568951,"repeat_date":"24.06.2026","days_between":144},{"company":"АО \"КАЗТЕЛЕПОРТ\"","phone":7086768198,"first_id":562359,"first_date":"19.12.2025","repeat_id":568953,"repeat_date":"24.06.2026","days_between":187},{"company":"АО «BEREKE BANK» ДОЧЕРНИЙ БАНК LESHA BANK LLC (PUBLIC))","phone":7081784711,"first_id":557582,"first_date":"11.08.2025","repeat_id":566616,"repeat_date":"21.04.2026","days_between":252}]}; let data = null;
document.getElementById('totalApps').textContent = data.total_applications.toLocaleString('ru-RU'); async function loadData() {
document.getElementById('uniqueClients').textContent = data.unique_clients.toLocaleString('ru-RU'); const response = await fetch('data.json?v=' + Date.now());
document.getElementById('repeatCount').textContent = data.total_repeats.toLocaleString('ru-RU'); data = await response.json();
const tbody = document.getElementById('tableBody'); document.getElementById('totalClients').textContent = data.total_clients_with_repeats.toLocaleString('ru-RU');
data.repeats.forEach((row, index) => { document.getElementById('maxRequests').textContent = Math.max(...data.clients.map(c => c.total_requests)).toLocaleString('ru-RU');
const tr = document.createElement('tr'); document.getElementById('criticalClients').textContent = data.clients.filter(c => c.total_requests >= 5).length.toLocaleString('ru-RU');
if (index % 2 === 0) tr.className = 'highlight'; document.getElementById('totalRequests').textContent = data.clients.reduce((sum, c) => sum + c.total_requests, 0).toLocaleString('ru-RU');
const statusBadge = row.days_between > 180 renderTable(data.clients);
? '<span class="badge badge-danger">Критично (>6 мес)</span>' }
: '<span class="badge badge-warning">>3 месяцев</span>';
tr.innerHTML = ` function getBadge(count) {
<td class="company-col">${row.company}</td> if (count >= 5) return '<span class="badge badge-critical">Критично</span>';
<td class="phone-col">+7 (${row.phone.toString().slice(0,3)}) ${row.phone.toString().slice(3,6)}-${row.phone.toString().slice(6,8)}-${row.phone.toString().slice(8)}</td> if (count >= 3) return '<span class="badge badge-high">Высокий</span>';
<td> return '<span class="badge badge-medium">Средний</span>';
<div style="font-weight:600">${row.first_date}</div> }
<div class="id-col">№ ${row.first_id}</div>
</td> function formatPhone(phone) {
<td> const p = phone.toString();
<div style="font-weight:600">${row.repeat_date}</div> return `+7 (${p.slice(0,3)}) ${p.slice(3,6)}-${p.slice(6,8)}-${p.slice(8)}`;
<div class="id-col">№ ${row.repeat_id}</div> }
</td>
<td class="days-col">${row.days_between} дн.</td> function renderTable(clients) {
<td>${statusBadge}</td> // Добавляем строку с общей статистикой
const summaryRow = document.getElementById('summaryRow');
summaryRow.innerHTML = `
<tr style="background:var(--cyan-50);border-bottom:2px solid var(--ink)">
<td style="text-align:center;font-weight:700">📊</td>
<td colspan="2" style="font-weight:700">ИТОГО ПОВТОРНЫХ ОБРАЩЕНИЙ</td>
<td style="text-align:center;font-size:20px;font-weight:800;color:var(--red)">${clients.reduce((sum, c) => sum + c.total_requests, 0)}</td>
<td>
<div style="font-size:13px">Уникальных заявок: <strong>${data.total_unique_applications}</strong></div>
<div style="font-size:13px">Клиентов с повторами: <strong>${clients.length}</strong></div>
</td>
<td style="font-size:13px;color:var(--gray-500)">Все заявки показаны ниже в таблице</td>
</tr>
`; `;
tbody.appendChild(tr);
const tbody = document.getElementById('tableBody');
tbody.innerHTML = '';
clients.forEach((client, index) => {
const tr = document.createElement('tr');
const historyHtml = client.requests.map((req, i) => `
<div class="history-item">
<div class="history-date">#${i + 1}. Заявка № ${req.id} &bull; ${req.date}</div>
<div class="history-details">
<span>Канал: ${req.channel}</span>
</div>
</div>
`).join('');
tr.innerHTML = `
<td style="text-align:center;font-weight:700;color:var(--gray-500)">${index + 1}</td>
<td class="company-col">${client.company}</td>
<td class="phone-col">${formatPhone(client.phone)}</td>
<td class="count-col">${getBadge(client.total_requests)}<div style="margin-top:6px;font-size:24px">${client.total_requests}</div></td>
<td>
<div style="font-weight:600">${client.first_date}</div>
<div style="color:var(--gray-500)">↓</div>
<div style="font-weight:600">${client.last_date}</div>
<div class="days-badge" style="margin-top:6px">${client.days_span} дн.</div>
</td>
<td class="history-cell">${historyHtml}</td>
`;
tbody.appendChild(tr);
});
}
document.getElementById('searchInput').addEventListener('input', (e) => {
const term = e.target.value.toLowerCase();
const filtered = data.clients.filter(c =>
c.company.toLowerCase().includes(term) ||
c.phone.toString().includes(term)
);
renderTable(filtered);
}); });
document.getElementById('filterSelect').addEventListener('change', (e) => {
const value = e.target.value;
let filtered = data.clients;
if (value === '5+') {
filtered = data.clients.filter(c => c.total_requests >= 5);
} else if (value === '3-4') {
filtered = data.clients.filter(c => c.total_requests >= 3 && c.total_requests < 5);
} else if (value === '2') {
filtered = data.clients.filter(c => c.total_requests === 2);
}
renderTable(filtered);
});
loadData();
</script> </script>
</body> </body>
</html> </html>

Binary file not shown.