Compare commits

...

1 Commits
main ... pages

Author SHA1 Message Date
2217a20d8a B2C dashboard - top services by region 2026-09-01 11:22:08 +00:00
4 changed files with 286 additions and 220 deletions

View 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

View File

@ -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>

View File

@ -1,6 +0,0 @@
{
"name": "crm-dashboard",
"private": true,
"type": "commonjs",
"scripts": { "start": "node server.js" }
}

110
server.js
View File

@ -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));