Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2217a20d8a |
91
b2c_top_services_july_2026.md
Normal file
91
b2c_top_services_july_2026.md
Normal file
@ -0,0 +1,91 @@
|
||||
# Топ-50 услуг по регионам — сегмент B2C
|
||||
## Данные за июль 2026 года
|
||||
|
||||
|
||||
| № | Регион | Услуга | Кол-во покупок | Сумма (₸) |
|
||||
|---|--------|--------|----------------|-----------|
|
||||
| 1 | Астана | B2C Абонплата ШПД по FTTx ГТС (New) | 37 372 | 238 463 786,09 |
|
||||
| 2 | г.Алматы | B2C Абонплата ШПД по FTTx ГТС (New) | 39 671 | 237 005 698,16 |
|
||||
| 3 | г.Алматы | Абонплата ШПД по FTTx для B2C | 35 588 | 152 893 561,28 |
|
||||
| 4 | Астана | 2025 B2C Абонплата за Домашний Интернет FTTx ГТС | 35 637 | 136 187 938,99 |
|
||||
| 5 | Астана | B2C TV Plus Абонплата Full ГТС | 47 661 | 132 060 372,93 |
|
||||
| 6 | Астана | 2021 B2C Абонплата за мобильный номер Kcell GSM закр. | 61 700 | 123 403 164,43 |
|
||||
| 7 | г.Алматы | 2022 B2C Абонплата за мобильный номер MT-C GSM закр. | 57 378 | 110 497 209,55 |
|
||||
| 8 | г.Алматы | 2025 B2C Абонплата за Домашний Интернет FTTx ГТС | 29 997 | 106 749 675,59 |
|
||||
| 9 | Астана | 2022 B2C Абонплата за мобильный номер MT-C GSM закр. | 52 504 | 99 235 780,41 |
|
||||
| 10 | Астана | 2022 B2C Абонплата за Домашний Интернет FTTx ГТС | 36 946 | 96 430 127,27 |
|
||||
| 11 | Астана | Абонплата ШПД по FTTx для B2C | 19 565 | 88 834 659,73 |
|
||||
| 12 | Астана | 2024 B2C Абонплата за Домашний Интернет FTTx ГТС | 22 167 | 83 549 325,34 |
|
||||
| 13 | г.Алматы | B2C TV Plus Абонплата Full ГТС | 31 221 | 83 511 013,05 |
|
||||
| 14 | г.Алматы | 2021 B2C Абонплата за мобильный номер Kcell GSM закр. | 40 838 | 82 748 215,46 |
|
||||
| 15 | г.Семей | 2022 B2C Абонплата за мобильный номер MT-C GSM закр. | 41 486 | 76 400 272,02 |
|
||||
| 16 | Астана | TV Plus Абонплата за Премиальный пакет ГТС | 29 210 | 71 278 165,09 |
|
||||
| 17 | г.Алматы | 2022 B2C Абонплата за Домашний Интернет FTTx ГТС | 24 001 | 62 521 274,30 |
|
||||
| 18 | Астана | 2021 B2C Абонплата за мобильный номер Kcell GSM откр. | 27 864 | 59 635 624,39 |
|
||||
| 19 | г.Алматы | 2024 B2C Абонплата за Домашний Интернет FTTx ГТС | 15 678 | 58 970 815,48 |
|
||||
| 20 | г.Актобе | 2022 B2C Абонплата за мобильный номер MT-C GSM закр. | 30 641 | 55 819 936,98 |
|
||||
| 21 | г.Алматы | B2C Абонплата за основной телефон ГТС | 52 732 | 49 636 818,50 |
|
||||
| 22 | г.Актобе | 2021 B2C Абонплата за мобильный номер Kcell GSM закр. | 25 178 | 48 000 483,43 |
|
||||
| 23 | г.Алматы | TV Plus Абонплата за Премиальный пакет ГТС | 19 055 | 45 298 192,89 |
|
||||
| 24 | г.Павлодар | 2022 B2C Абонплата за мобильный номер MT-C GSM закр. | 23 334 | 44 857 518,84 |
|
||||
| 25 | г.Алматы | B2C TV Plus Аренда STB оператора новое | 96 183 | 43 252 519,71 |
|
||||
| 26 | г.Караганда | Абонплата ШПД по FTTx для B2C | 8 916 | 41 624 727,66 |
|
||||
| 27 | Астана | 2026 B2C Абонплата за Домашний Интернет FTTx ГТС | 13 685 | 41 618 770,34 |
|
||||
| 28 | г.Алматы | 2018 B2C Абонплата за Домашний Интернет FTTx ГТС | 11 568 | 40 199 964,35 |
|
||||
| 29 | г.Тараз | 2022 B2C Абонплата за мобильный номер MT-C GSM закр. | 21 880 | 39 564 813,34 |
|
||||
| 30 | г.Алматы | Абонплата ШПД по ADSL для B2C | 10 710 | 36 238 454,72 |
|
||||
| 31 | г.Алматы | 2020 B2C Абонплата за Домашний Интернет FTTx ГТС | 12 148 | 36 010 481,68 |
|
||||
| 32 | г.Усть-Каменогорск | 2022 B2C Абонплата за мобильный номер MT-C GSM закр. | 18 362 | 35 883 527,39 |
|
||||
| 33 | г. Шымкент | 2022 B2C Абонплата за мобильный номер MT-C GSM закр. | 19 180 | 35 338 634,51 |
|
||||
| 34 | г. Шымкент | 2021 B2C Абонплата за мобильный номер Kcell GSM закр. | 18 945 | 35 138 664,91 |
|
||||
| 35 | Астана | B2C TV Plus Аренда STB оператора новое | 76 655 | 34 746 783,92 |
|
||||
| 36 | Астана | 2018 B2C Абонплата за Домашний Интернет FTTx ГТС | 8 705 | 32 905 725,03 |
|
||||
| 37 | г.Актобе | B2C Абонплата ШПД по FTTx ГТС (New) | 6 742 | 32 313 021,91 |
|
||||
| 38 | г.Усть-Каменогорск | Абонплата ШПД по FTTx для B2C | 7 269 | 31 834 355,15 |
|
||||
| 39 | г.Атырау | B2C Абонплата ШПД по FTTx ГТС (New) | 5 495 | 31 688 118,61 |
|
||||
| 40 | г.Усть-Каменогорск | B2C Абонплата ШПД по FTTx ГТС (New) | 5 600 | 31 520 413,53 |
|
||||
| 41 | Астана | B2C Аренда оборудования ONT оператора - новое | 87 071 | 31 220 264,09 |
|
||||
| 42 | г.Павлодар | B2C Абонплата ШПД по FTTx ГТС (New) | 6 104 | 30 831 455,08 |
|
||||
| 43 | г.Кокшетау | 2022 B2C Абонплата за мобильный номер MT-C GSM закр. | 16 818 | 30 654 924,73 |
|
||||
| 44 | г.Актобе | 2025 B2C Абонплата за Домашний Интернет FTTx ГТС | 8 613 | 30 562 705,78 |
|
||||
| 45 | г.Костанай | 2022 B2C Абонплата за мобильный номер MT-C GSM закр. | 16 473 | 30 539 180,35 |
|
||||
| 46 | Енбекшиказахский район | 2022 B2C Абонплата за мобильный номер MT-C GSM закр. | 17 347 | 30 429 552,38 |
|
||||
| 47 | г.Актобе | 2022 B2C Абонплата за Домашний Интернет FTTx ГТС | 11 709 | 30 303 557,00 |
|
||||
| 48 | г.Кызылорда | 2022 B2C Абонплата за мобильный номер MT-C GSM закр. | 16 539 | 29 772 241,67 |
|
||||
| 49 | г.Алматы | 2026 B2C Абонплата за Домашний Интернет FTTx ГТС | 10 206 | 29 200 316,21 |
|
||||
| 50 | г.Караганда | 2022 B2C Абонплата за мобильный номер MT-C GSM закр. | 15 714 | 28 837 871,98 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Ключевые наблюдения
|
||||
|
||||
|
||||
### Лидеры по доходам
|
||||
- **Астана** и **г.Алматы** занимают первые места с услугами широкополосного доступа (ШПД) и мобильной связи.
|
||||
|
||||
|
||||
### Популярные услуги
|
||||
- Абонплата за интернет (FTTx) и мобильные номера (Kcell, MT-C) доминируют в топе.
|
||||
|
||||
|
||||
### Массовые услуги
|
||||
- **Аренда STB**: 96 183 покупки в Алматы при сумме 43,25 млн ₸
|
||||
- **Аренда ONT оборудования**: 87 071 покупка в Астане при сумме 31,22 млн ₸
|
||||
|
||||
|
||||
### География
|
||||
В топ-50 вошли все крупные города Казахстана:
|
||||
- Алматы, Астана, Семей, Актобе, Павлодар, Караганда, Усть-Каменогорск, Шымкент, Тараз, Атырау, Кокшетау, Костанай, Кызылорда, Енбекшиказахский район
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
**Источник данных:** ClickHouse Хранилище АО «Казахтелеком»
|
||||
**Таблица:** sandbox.spec2_main_2spec_detail2_v2_ddt
|
||||
**Период:** Июль 2026 (report_period_id = 202607)
|
||||
**Сегмент:** B2C
|
||||
**Валюта:** Тенге (без НДС)
|
||||
**Дата формирования:** 01.09.2026
|
||||
299
index.html
299
index.html
@ -3,31 +3,42 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Топ-10 заказов B2C | Казахтелеком</title>
|
||||
<title>Топ услуг B2C по регионам | Казахтелеком</title>
|
||||
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-components.css">
|
||||
<link rel="stylesheet" href="design-system/kt-ai-page.css">
|
||||
<style>
|
||||
.dashboard-wrap { padding: var(--kt-ai-space-6) 0; }
|
||||
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-8); }
|
||||
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-8); }
|
||||
.kpi-card { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-5); }
|
||||
.kpi-label { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); margin-bottom: var(--kt-ai-space-2); }
|
||||
.kpi-value { font-size: var(--kt-ai-text-2xl); font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-fg); }
|
||||
.table-container { overflow-x: auto; border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); }
|
||||
.kpi-sub { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-faint); margin-top: var(--kt-ai-space-1); }
|
||||
.charts-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--kt-ai-space-6); margin-bottom: var(--kt-ai-space-8); }
|
||||
@media (max-width: 900px) { .charts-grid { grid-template-columns: 1fr; } }
|
||||
.chart-card { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-5); }
|
||||
.chart-title { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-4); color: var(--kt-ai-fg); }
|
||||
.bar-chart { display: flex; flex-direction: column; gap: var(--kt-ai-space-2); }
|
||||
.bar-row { display: flex; align-items: center; gap: var(--kt-ai-space-3); }
|
||||
.bar-label { width: 140px; font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); text-align: right; flex-shrink: 0; }
|
||||
.bar-track { flex: 1; height: 24px; background: var(--kt-ai-bg-soft); border-radius: var(--kt-ai-radius-full); overflow: hidden; }
|
||||
.bar-fill { height: 100%; background: var(--kt-ai-primary); border-radius: var(--kt-ai-radius-full); transition: width 0.5s ease; }
|
||||
.bar-value { width: 80px; font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-semibold); color: var(--kt-ai-fg); text-align: right; flex-shrink: 0; }
|
||||
.table-container { overflow-x: auto; border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); margin-bottom: var(--kt-ai-space-6); }
|
||||
table { width: 100%; border-collapse: collapse; background: var(--kt-ai-card-bg); }
|
||||
th { text-align: left; padding: var(--kt-ai-space-3) var(--kt-ai-space-4); font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); border-bottom: 1px solid var(--kt-ai-divider); }
|
||||
th { text-align: left; padding: var(--kt-ai-space-3) var(--kt-ai-space-4); font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); border-bottom: 1px solid var(--kt-ai-divider); white-space: nowrap; }
|
||||
td { padding: var(--kt-ai-space-4); border-bottom: 1px solid var(--kt-ai-divider); font-size: var(--kt-ai-text-sm); }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tr:hover td { background: var(--kt-ai-bg-soft); }
|
||||
.rank { width: 32px; height: 32px; display: inline-flex; align-items: center; justify-content: center; border-radius: var(--kt-ai-radius-full); font-weight: var(--kt-ai-weight-bold); font-size: var(--kt-ai-text-xs); }
|
||||
.rank-1 { background: #FFD700; color: #000; }
|
||||
.rank-2 { background: #C0C0C0; color: #000; }
|
||||
.rank-3 { background: #CD7F32; color: #fff; }
|
||||
.rank-other { background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg-muted); }
|
||||
.amount { font-weight: var(--kt-ai-weight-semibold); color: var(--kt-ai-primary); }
|
||||
.last-update { margin-top: var(--kt-ai-space-4); font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); }
|
||||
.refresh-btn { margin-left: var(--kt-ai-space-3); }
|
||||
.status-chip { display: inline-block; padding: 2px 10px; border-radius: var(--kt-ai-radius-full); font-size: var(--kt-ai-text-xs); }
|
||||
.status-new { background: rgba(0, 184, 204, 0.15); color: var(--kt-ai-primary); }
|
||||
.loading { text-align: center; padding: var(--kt-ai-space-12); color: var(--kt-ai-fg-muted); }
|
||||
.error-state { text-align: center; padding: var(--kt-ai-space-12); color: var(--kt-ai-fg-muted); }
|
||||
.error-state h3 { margin-bottom: var(--kt-ai-space-2); color: var(--kt-ai-fg); }
|
||||
.last-update { font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); text-align: center; padding: var(--kt-ai-space-4); }
|
||||
.region-tag { display: inline-block; padding: 2px 10px; border-radius: var(--kt-ai-radius-full); font-size: var(--kt-ai-text-xs); background: var(--kt-ai-bg-soft); color: var(--kt-ai-fg); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -36,55 +47,67 @@
|
||||
<div class="kt-ai-wrap kt-ai-pagehead-in">
|
||||
<strong>Дашборд B2C</strong>
|
||||
<span class="kt-ai-spacer"></span>
|
||||
<button class="kt-ai-btn" data-variant="ghost" onclick="refreshData()">⟳ Обновить</button>
|
||||
<span class="region-tag">Июль 2026</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="kt-ai-wrap dashboard-wrap">
|
||||
<section>
|
||||
<h1 style="margin-bottom: var(--kt-ai-space-2)">Топ-10 заказов B2C за сегодня</h1>
|
||||
<p class="kt-ai-hero-sub" style="margin-bottom: var(--kt-ai-space-6)">Самые крупные заказы Казахтелекома по сумме</p>
|
||||
<h1 style="margin-bottom: var(--kt-ai-space-2)">Топ услуг B2C по регионам</h1>
|
||||
<p class="kt-ai-hero-sub" style="margin-bottom: var(--kt-ai-space-6)">Сводная статистика покупок услуг по всем регионам Казахстана</p>
|
||||
|
||||
<div class="kpi-grid" id="kpiGrid">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">Всего заказов B2C</div>
|
||||
<div class="kpi-value" id="totalOrders">—</div>
|
||||
<div class="kpi-label">Всего покупок</div>
|
||||
<div class="kpi-value" id="totalPurchases">—</div>
|
||||
<div class="kpi-sub" id="totalPurchasesSub">в топ-50</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">Сумма топ-10</div>
|
||||
<div class="kpi-value" id="top10Sum">—</div>
|
||||
<div class="kpi-label">Всего регионов</div>
|
||||
<div class="kpi-value" id="totalRegions">—</div>
|
||||
<div class="kpi-sub">уникальных</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">Средний чек</div>
|
||||
<div class="kpi-value" id="avgCheck">—</div>
|
||||
<div class="kpi-label">Услуга-лидер</div>
|
||||
<div class="kpi-value" style="font-size: var(--kt-ai-text-lg)" id="topService">—</div>
|
||||
<div class="kpi-sub" id="topServicePurchases">—</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">Лидер региона</div>
|
||||
<div class="kpi-label">Регион-лидер</div>
|
||||
<div class="kpi-value" id="topRegion">—</div>
|
||||
<div class="kpi-sub" id="topRegionPurchases">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-grid">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">Топ-10 регионов по покупкам</div>
|
||||
<div class="bar-chart" id="regionChart"></div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">Топ-10 услуг по покупкам</div>
|
||||
<div class="bar-chart" id="serviceChart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 style="font-size: var(--kt-ai-text-xl); margin-bottom: var(--kt-ai-space-4)">Полная таблица топ-50</h2>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Клиент</th>
|
||||
<th style="width: 60px">#</th>
|
||||
<th>Регион</th>
|
||||
<th>Услуга</th>
|
||||
<th>Статус</th>
|
||||
<th>Сумма (₸)</th>
|
||||
<th style="text-align: right">Покупок</th>
|
||||
<th style="text-align: right">Сумма (₸)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ordersTable">
|
||||
<tr><td colspan="6" class="loading">Загрузка данных…</td></tr>
|
||||
</tbody>
|
||||
<tbody id="dataTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="last-update">
|
||||
Последнее обновление: <span id="lastUpdate">—</span>
|
||||
<button class="kt-ai-btn" data-variant="primary" data-size="sm" onclick="refreshData()">Обновить сейчас</button>
|
||||
Данные за июль 2026 года | Источник: ClickHouse Хранилище АО «Казахтелеком»
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
@ -96,92 +119,160 @@
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
let ordersData = [];
|
||||
let lastUpdateTime = null;
|
||||
|
||||
// Мок-данные для демонстрации (заменятся на данные от Alem при подключении)
|
||||
const mockOrders = [
|
||||
{ id: 1, client: "ТОО \"Digital Solutions\"", region: "Алматы", service: "Выделенная линия 100 Мбит", status: "новый", amount: 2850000 },
|
||||
{ id: 2, client: "ИП Nazarbaev", region: "Астана", service: "IP-телефония 50 номеров", status: "новый", amount: 1920000 },
|
||||
{ id: 3, client: "ТОО \"KazStroyGroup\"", region: "Караганда", service: "Видеонаблюдение 24 камеры", status: "новый", amount: 1650000 },
|
||||
{ id: 4, client: "АО \"AlmatyTrade\"", region: "Алматы", service: "Облачное хранилище 5ТБ", status: "новый", amount: 1420000 },
|
||||
{ id: 5, client: "ТОО \"NorthOil\"", region: "Атырау", service: "Выделенная линия 50 Мбит", status: "новый", amount: 1180000 },
|
||||
{ id: 6, client: "ИП Seitov", region: "Шымкент", service: "Охранная сигнализация", status: "новый", amount: 980000 },
|
||||
{ id: 7, client: "ТОО \"EurasiaLogistics\"", region: "Актобе", service: "Видеоконференцсвязь", status: "новый", amount: 875000 },
|
||||
{ id: 8, client: "ТОО \"BaumerKazakhstan\"", region: "Астана", service: "Доступ в интернет 200 Мбит", status: "новый", amount: 720000 },
|
||||
{ id: 9, client: "ИП Omarova", region: "Павлодар", service: "IP-телефония 20 номеров", status: "новый", amount: 650000 },
|
||||
{ id: 10, client: "ТОО \"SemeyStroy\"", region: "Семей", service: "Локальная сеть под ключ", status: "новый", amount: 580000 }
|
||||
// Данные из b2c_top_services_july_2026.md
|
||||
const rawData = [
|
||||
{ rank: 1, region: "Астана", service: "B2C Абонплата ШПД по FTTx ГТС (New)", purchases: 37372, amount: 238463786.09 },
|
||||
{ rank: 2, region: "г.Алматы", service: "B2C Абонплата ШПД по FTTx ГТС (New)", purchases: 39671, amount: 237005698.16 },
|
||||
{ rank: 3, region: "г.Алматы", service: "Абонплата ШПД по FTTx для B2C", purchases: 35588, amount: 152893561.28 },
|
||||
{ rank: 4, region: "Астана", service: "2025 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 35637, amount: 136187938.99 },
|
||||
{ rank: 5, region: "Астана", service: "B2C TV Plus Абонплата Full ГТС", purchases: 47661, amount: 132060372.93 },
|
||||
{ rank: 6, region: "Астана", service: "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", purchases: 61700, amount: 123403164.43 },
|
||||
{ rank: 7, region: "г.Алматы", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 57378, amount: 110497209.55 },
|
||||
{ rank: 8, region: "г.Алматы", service: "2025 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 29997, amount: 106749675.59 },
|
||||
{ rank: 9, region: "Астана", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 52504, amount: 99235780.41 },
|
||||
{ rank: 10, region: "Астана", service: "2022 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 36946, amount: 96430127.27 },
|
||||
{ rank: 11, region: "Астана", service: "Абонплата ШПД по FTTx для B2C", purchases: 19565, amount: 88834659.73 },
|
||||
{ rank: 12, region: "Астана", service: "2024 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 22167, amount: 83549325.34 },
|
||||
{ rank: 13, region: "г.Алматы", service: "B2C TV Plus Абонплата Full ГТС", purchases: 31221, amount: 83511013.05 },
|
||||
{ rank: 14, region: "г.Алматы", service: "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", purchases: 40838, amount: 82748215.46 },
|
||||
{ rank: 15, region: "г.Семей", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 41486, amount: 76400272.02 },
|
||||
{ rank: 16, region: "Астана", service: "TV Plus Абонплата за Премиальный пакет ГТС", purchases: 29210, amount: 71278165.09 },
|
||||
{ rank: 17, region: "г.Алматы", service: "2022 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 24001, amount: 62521274.30 },
|
||||
{ rank: 18, region: "Астана", service: "2021 B2C Абонплата за мобильный номер Kcell GSM откр.", purchases: 27864, amount: 59635624.39 },
|
||||
{ rank: 19, region: "г.Алматы", service: "2024 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 15678, amount: 58970815.48 },
|
||||
{ rank: 20, region: "г.Актобе", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 30641, amount: 55819936.98 },
|
||||
{ rank: 21, region: "г.Алматы", service: "B2C Абонплата за основной телефон ГТС", purchases: 52732, amount: 49636818.50 },
|
||||
{ rank: 22, region: "г.Актобе", service: "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", purchases: 25178, amount: 48000483.43 },
|
||||
{ rank: 23, region: "г.Алматы", service: "TV Plus Абонплата за Премиальный пакет ГТС", purchases: 19055, amount: 45298192.89 },
|
||||
{ rank: 24, region: "г.Павлодар", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 23334, amount: 44857518.84 },
|
||||
{ rank: 25, region: "г.Алматы", service: "B2C TV Plus Аренда STB оператора новое", purchases: 96183, amount: 43252519.71 },
|
||||
{ rank: 26, region: "г.Караганда", service: "Абонплата ШПД по FTTx для B2C", purchases: 8916, amount: 41624727.66 },
|
||||
{ rank: 27, region: "Астана", service: "2026 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 13685, amount: 41618770.34 },
|
||||
{ rank: 28, region: "г.Алматы", service: "2018 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 11568, amount: 40199964.35 },
|
||||
{ rank: 29, region: "г.Тараз", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 21880, amount: 39564813.34 },
|
||||
{ rank: 30, region: "г.Алматы", service: "Абонплата ШПД по ADSL для B2C", purchases: 10710, amount: 36238454.72 },
|
||||
{ rank: 31, region: "г.Алматы", service: "2020 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 12148, amount: 36010481.68 },
|
||||
{ rank: 32, region: "г.Усть-Каменогорск", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 18362, amount: 35883527.39 },
|
||||
{ rank: 33, region: "г. Шымкент", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 19180, amount: 35338634.51 },
|
||||
{ rank: 34, region: "г. Шымкент", service: "2021 B2C Абонплата за мобильный номер Kcell GSM закр.", purchases: 18945, amount: 35138664.91 },
|
||||
{ rank: 35, region: "Астана", service: "B2C TV Plus Аренда STB оператора новое", purchases: 76655, amount: 34746783.92 },
|
||||
{ rank: 36, region: "Астана", service: "2018 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 8705, amount: 32905725.03 },
|
||||
{ rank: 37, region: "г.Актобе", service: "B2C Абонплата ШПД по FTTx ГТС (New)", purchases: 6742, amount: 32313021.91 },
|
||||
{ rank: 38, region: "г.Усть-Каменогорск", service: "Абонплата ШПД по FTTx для B2C", purchases: 7269, amount: 31834355.15 },
|
||||
{ rank: 39, region: "г.Атырау", service: "B2C Абонплата ШПД по FTTx ГТС (New)", purchases: 5495, amount: 31688118.61 },
|
||||
{ rank: 40, region: "г.Усть-Каменогорск", service: "B2C Абонплата ШПД по FTTx ГТС (New)", purchases: 5600, amount: 31520413.53 },
|
||||
{ rank: 41, region: "Астана", service: "B2C Аренда оборудования ONT оператора - новое", purchases: 87071, amount: 31220264.09 },
|
||||
{ rank: 42, region: "г.Павлодар", service: "B2C Абонплата ШПД по FTTx ГТС (New)", purchases: 6104, amount: 30831455.08 },
|
||||
{ rank: 43, region: "г.Кокшетау", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 16818, amount: 30654924.73 },
|
||||
{ rank: 44, region: "г.Актобе", service: "2025 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 8613, amount: 30562705.78 },
|
||||
{ rank: 45, region: "г.Костанай", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 16473, amount: 30539180.35 },
|
||||
{ rank: 46, region: "Енбекшиказахский район", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 17347, amount: 30429552.38 },
|
||||
{ rank: 47, region: "г.Актобе", service: "2022 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 11709, amount: 30303557.00 },
|
||||
{ rank: 48, region: "г.Кызылорда", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 16539, amount: 29772241.67 },
|
||||
{ rank: 49, region: "г.Алматы", service: "2026 B2C Абонплата за Домашний интернет FTTx ГТС", purchases: 10206, amount: 29200316.21 },
|
||||
{ rank: 50, region: "г.Караганда", service: "2022 B2C Абонплата за мобильный номер MT-C GSM закр.", purchases: 15714, amount: 28837871.98 }
|
||||
];
|
||||
|
||||
function formatAmount(amount) {
|
||||
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
||||
function formatNumber(n) {
|
||||
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
||||
}
|
||||
|
||||
function renderTable(orders) {
|
||||
const tbody = document.getElementById('ordersTable');
|
||||
tbody.innerHTML = orders.map((order, index) => `
|
||||
<tr>
|
||||
<td>${index + 1}</td>
|
||||
<td>${order.client}</td>
|
||||
<td>${order.region}</td>
|
||||
<td>${order.service}</td>
|
||||
<td><span class="status-chip status-new">${order.status}</span></td>
|
||||
<td class="amount">${formatAmount(order.amount)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
function formatAmount(a) {
|
||||
return formatNumber(Math.round(a).toString()) + " ₸";
|
||||
}
|
||||
|
||||
function updateKPI(orders) {
|
||||
const totalSum = orders.reduce((sum, o) => sum + o.amount, 0);
|
||||
const avg = Math.round(totalSum / orders.length);
|
||||
function normalizeRegion(r) {
|
||||
return r.replace(/^г\./, '').replace(/^\s+/, '').trim();
|
||||
}
|
||||
|
||||
function calculateKPI(data) {
|
||||
const totalPurchases = data.reduce((sum, d) => sum + d.purchases, 0);
|
||||
|
||||
// Подсчёт по регионам
|
||||
const regionCount = {};
|
||||
orders.forEach(o => {
|
||||
regionCount[o.region] = (regionCount[o.region] || 0) + 1;
|
||||
const regionMap = {};
|
||||
data.forEach(d => {
|
||||
const reg = normalizeRegion(d.region);
|
||||
regionMap[reg] = (regionMap[reg] || 0) + d.purchases;
|
||||
});
|
||||
const topRegion = Object.entries(regionCount).sort((a, b) => b[1] - a[1])[0]?.[0] || '—';
|
||||
|
||||
document.getElementById('totalOrders').textContent = orders.length;
|
||||
document.getElementById('top10Sum').textContent = formatAmount(totalSum) + ' ₸';
|
||||
document.getElementById('avgCheck').textContent = formatAmount(avg) + ' ₸';
|
||||
document.getElementById('topRegion').textContent = topRegion;
|
||||
}
|
||||
|
||||
function updateLastUpdate() {
|
||||
const now = new Date();
|
||||
lastUpdateTime = now;
|
||||
const dateStr = now.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' });
|
||||
const timeStr = now.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
||||
document.getElementById('lastUpdate').textContent = dateStr + ' в ' + timeStr;
|
||||
}
|
||||
|
||||
async function fetchFromAlem() {
|
||||
// Попытка получить данные от Alem
|
||||
try {
|
||||
const response = await fetch('/api/alem-orders');
|
||||
if (!response.ok) throw new Error('Alem API недоступен');
|
||||
const data = await response.json();
|
||||
return data.orders || [];
|
||||
} catch (e) {
|
||||
console.log('Alem недоступен, используем мок-данные');
|
||||
return mockOrders;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshData() {
|
||||
const tbody = document.getElementById('ordersTable');
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="loading">Обновление данных…</td></tr>';
|
||||
const totalRegions = Object.keys(regionMap).length;
|
||||
|
||||
ordersData = await fetchFromAlem();
|
||||
renderTable(ordersData);
|
||||
updateKPI(ordersData);
|
||||
updateLastUpdate();
|
||||
const topService = data.reduce((max, d) => d.purchases > max.purchases ? d : max, data[0]);
|
||||
|
||||
const regionTotals = {};
|
||||
data.forEach(d => {
|
||||
const reg = normalizeRegion(d.region);
|
||||
regionTotals[reg] = (regionTotals[reg] || 0) + d.purchases;
|
||||
});
|
||||
const topRegion = Object.entries(regionTotals).sort((a, b) => b[1] - a[1])[0];
|
||||
|
||||
return { totalPurchases, totalRegions, topService, topRegion: topRegion[0], topRegionPurchases: topRegion[1] };
|
||||
}
|
||||
|
||||
// Инициализация при загрузке
|
||||
function renderKPI(kpi) {
|
||||
document.getElementById('totalPurchases').textContent = formatNumber(kpi.totalPurchases);
|
||||
document.getElementById('totalRegions').textContent = kpi.totalRegions;
|
||||
document.getElementById('topService').textContent = kpi.topService.service.length > 40 ? kpi.topService.service.slice(0, 37) + '...' : kpi.topService.service;
|
||||
document.getElementById('topServicePurchases').textContent = formatNumber(kpi.topService.purchases) + ' покупок';
|
||||
document.getElementById('topRegion').textContent = kpi.topRegion;
|
||||
document.getElementById('topRegionPurchases').textContent = formatNumber(kpi.topRegionPurchases) + ' покупок';
|
||||
}
|
||||
|
||||
function renderRegionChart(data) {
|
||||
const regionMap = {};
|
||||
data.forEach(d => {
|
||||
const reg = normalizeRegion(d.region);
|
||||
regionMap[reg] = (regionMap[reg] || 0) + d.purchases;
|
||||
});
|
||||
const sorted = Object.entries(regionMap).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
||||
const maxVal = sorted[0][1];
|
||||
|
||||
const container = document.getElementById('regionChart');
|
||||
container.innerHTML = sorted.map(function(item) {
|
||||
const pct = (item[1] / maxVal) * 100;
|
||||
return '<div class="bar-row">' +
|
||||
'<div class="bar-label">' + item[0] + '</div>' +
|
||||
'<div class="bar-track"><div class="bar-fill" style="width:' + pct + '%"></div></div>' +
|
||||
'<div class="bar-value">' + formatNumber(item[1]) + '</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderServiceChart(data) {
|
||||
const sorted = data.slice(0, 10);
|
||||
const maxVal = sorted[0].purchases;
|
||||
|
||||
const container = document.getElementById('serviceChart');
|
||||
container.innerHTML = sorted.map(function(item, i) {
|
||||
const pct = (item.purchases / maxVal) * 100;
|
||||
const svcName = item.service.length > 35 ? item.service.slice(0, 32) + '...' : item.service;
|
||||
return '<div class="bar-row">' +
|
||||
'<div class="bar-label" style="width:160px">' + svcName + '</div>' +
|
||||
'<div class="bar-track"><div class="bar-fill" style="width:' + pct + '%"></div></div>' +
|
||||
'<div class="bar-value">' + formatNumber(item.purchases) + '</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderTable(data) {
|
||||
const tbody = document.getElementById('dataTable');
|
||||
tbody.innerHTML = data.map(function(d, i) {
|
||||
const rankClass = d.rank <= 3 ? 'rank-' + d.rank : 'rank-other';
|
||||
return '<tr>' +
|
||||
'<td><span class="rank ' + rankClass + '">' + d.rank + '</span></td>' +
|
||||
'<td>' + d.region + '</td>' +
|
||||
'<td>' + d.service + '</td>' +
|
||||
'<td style="text-align:right">' + formatNumber(d.purchases) + '</td>' +
|
||||
'<td style="text-align:right" class="amount">' + formatAmount(d.amount) + '</td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
refreshData();
|
||||
const kpi = calculateKPI(rawData);
|
||||
renderKPI(kpi);
|
||||
renderRegionChart(rawData);
|
||||
renderServiceChart(rawData);
|
||||
renderTable(rawData);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "crm-dashboard",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"scripts": { "start": "node server.js" }
|
||||
}
|
||||
110
server.js
110
server.js
@ -1,110 +0,0 @@
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
function body(req) {
|
||||
return new Promise((resolve) => {
|
||||
let s = "";
|
||||
req.on("data", (c) => (s += c));
|
||||
req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); } });
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchAlemOrders() {
|
||||
// Интеграция с Alem CRM - данные подтянутся, когда Alem подключится
|
||||
if (!process.env.ALEM_BASE_URL || !process.env.ALEM_API_KEY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Создаём сессию
|
||||
const sessionResp = await fetch(process.env.ALEM_BASE_URL + "/api/v1/open-api/conversation", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + process.env.ALEM_API_KEY
|
||||
},
|
||||
body: JSON.stringify({
|
||||
template_id: process.env.ALEM_TEMPLATE_ID,
|
||||
title: "CRM Dashboard",
|
||||
user_id: "crm_b2c_user"
|
||||
})
|
||||
});
|
||||
|
||||
if (!sessionResp.ok) throw new Error("Alem session " + sessionResp.status);
|
||||
const sessionBody = await sessionResp.json();
|
||||
const sessionId = sessionBody?.data?.session_id;
|
||||
if (!sessionId) throw new Error("No session_id from Alem");
|
||||
|
||||
// Запрашиваем топ-10 заказов B2C
|
||||
const queryResp = await fetch(process.env.ALEM_BASE_URL + "/api/v1/open-api/conversation/" + sessionId + "/send", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + process.env.ALEM_API_KEY
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: "Покажи топ-10 самых крупных заказов B2C за сегодня в формате JSON с полями: id, client, region, service, status, amount. Только JSON массив.",
|
||||
user_id: "crm_b2c_user",
|
||||
model_api_key: process.env.ALEM_API_KEY
|
||||
})
|
||||
});
|
||||
|
||||
if (!queryResp.ok) throw new Error("Alem query " + queryResp.status);
|
||||
|
||||
// SSE-поток
|
||||
const raw = await queryResp.text();
|
||||
let answer = null;
|
||||
for (const line of raw.split("\n")) {
|
||||
const s = line.trim();
|
||||
if (!s.startsWith("data:")) continue;
|
||||
const payload = s.slice(5).trim();
|
||||
if (!payload || payload === "[DONE]") continue;
|
||||
try {
|
||||
const ev = JSON.parse(payload);
|
||||
if (ev && ev.detail && typeof ev.detail.full_content === "string" && ev.detail.full_content) {
|
||||
answer = ev.detail.full_content;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (!answer) return null;
|
||||
|
||||
// Пытаемся извлечь JSON из ответа
|
||||
const jsonMatch = answer.match(/\[[\s\S]*\]/);
|
||||
if (jsonMatch) {
|
||||
return JSON.parse(jsonMatch[0]);
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.error("Alem error:", e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
http.createServer(async (req, res) => {
|
||||
// API: данные для дашборда
|
||||
if (req.method === "GET" && req.url.split("?")[0] === "/api/alem-orders") {
|
||||
const orders = await fetchAlemOrders();
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "application/json",
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
});
|
||||
res.end(JSON.stringify({ orders: orders || [] }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Статика: index.html
|
||||
let file = req.url === "/" ? "/index.html" : req.url.split("?")[0];
|
||||
const full = path.join(__dirname, file);
|
||||
if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) {
|
||||
const ext = path.extname(full);
|
||||
const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "text/html";
|
||||
res.writeHead(200, { "Content-Type": type + "; charset=utf-8" });
|
||||
return res.end(fs.readFileSync(full));
|
||||
}
|
||||
|
||||
res.writeHead(404); res.end("Not found");
|
||||
}).listen(PORT, () => console.log("CRM Dashboard на порту " + PORT));
|
||||
Loading…
Reference in New Issue
Block a user