416 lines
21 KiB
HTML
416 lines
21 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru" data-theme="light">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Дэшборд клиентов — Казахтелеком Кокшетау</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>
|
||
.kt-ai-app { background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-family: var(--kt-ai-font-sans); min-height: 100vh; }
|
||
.app-header { display: flex; align-items: center; gap: 16px; padding: 16px 24px; background: var(--kt-ai-bg-soft); border-bottom: 1px solid var(--kt-ai-border); position: sticky; top: 0; z-index: 100; }
|
||
.app-title { font-size: 20px; font-weight: 600; }
|
||
.kpi-strip { display: flex; gap: 16px; padding: 20px 24px; overflow-x: auto; }
|
||
.kpi-card { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-xl); padding: 20px 24px; min-width: 180px; }
|
||
.kpi-value { font-size: 32px; font-weight: 700; color: var(--kt-ai-fg); line-height: 1; }
|
||
.kpi-label { font-size: 14px; color: var(--kt-ai-fg-muted); margin-top: 8px; }
|
||
.kpi-hint { font-size: 12px; color: var(--kt-ai-fg-faint); margin-top: 4px; }
|
||
.controls-bar { display: flex; gap: 12px; padding: 0 24px 16px; align-items: center; flex-wrap: wrap; }
|
||
.search-input { flex: 1; min-width: 200px; max-width: 360px; }
|
||
.table-container { padding: 0 24px 24px; overflow-x: auto; }
|
||
.data-table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
||
.data-table th { text-align: left; padding: 12px 16px; border-bottom: 2px solid var(--kt-ai-border); color: var(--kt-ai-fg-muted); font-weight: 600; white-space: nowrap; }
|
||
.data-table td { padding: 14px 16px; border-bottom: 1px solid var(--kt-ai-border); vertical-align: middle; }
|
||
.data-table tbody tr:hover { background: var(--kt-ai-bg-soft); }
|
||
.text-right { text-align: right; }
|
||
.tabular { font-variant-numeric: tabular-nums; }
|
||
.status-chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 500; }
|
||
.status-ok { background: rgba(16, 185, 129, 0.12); color: var(--kt-ai-status-ok-fg); }
|
||
.status-warn { background: rgba(245, 158, 11, 0.12); color: var(--kt-ai-status-warn-fg); }
|
||
.status-risk { background: rgba(239, 68, 68, 0.12); color: var(--kt-ai-status-risk-fg); }
|
||
.actions-cell { display: flex; gap: 8px; }
|
||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; }
|
||
.modal-overlay.on { display: flex; }
|
||
.modal { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-card-border); border-radius: var(--kt-ai-radius-2xl); padding: 24px; width: 100%; max-width: 480px; max-height: 90vh; overflow-y: auto; }
|
||
.modal-title { font-size: 20px; font-weight: 600; margin-bottom: 20px; }
|
||
.form-group { margin-bottom: 16px; }
|
||
.form-label { display: block; font-size: 14px; font-weight: 500; margin-bottom: 6px; color: var(--kt-ai-fg); }
|
||
.form-input, .form-select, .form-textarea { width: 100%; padding: 10px 12px; border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-lg); background: var(--kt-ai-bg); color: var(--kt-ai-fg); font-size: 14px; font-family: var(--kt-ai-font-sans); }
|
||
.form-input:focus, .form-select:focus, .form-textarea:focus { outline: 2px solid var(--kt-ai-primary); border-color: transparent; }
|
||
.modal-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px; }
|
||
.empty-state { text-align: center; padding: 60px 20px; color: var(--kt-ai-fg-muted); }
|
||
</style>
|
||
</head>
|
||
<body class="kt-ai-app">
|
||
|
||
<header class="app-header">
|
||
<img src="https://code.vibe42.kz/tore/aitu/kt-logo.svg" alt="Казахтелеком" height="28">
|
||
<span class="app-title">Дэшборд клиентов — Кокшетау</span>
|
||
<span style="flex:1"></span>
|
||
<button class="kt-ai-btn" data-variant="ghost" id="themeToggle">🌓 Тема</button>
|
||
</header>
|
||
|
||
<div class="kpi-strip">
|
||
<div class="kpi-card">
|
||
<div class="kpi-value" id="kpiTotal">0</div>
|
||
<div class="kpi-label">Всего клиентов</div>
|
||
<div class="kpi-hint">Физ. и юр. лица</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-value" id="kpiActive">0</div>
|
||
<div class="kpi-label">Активные</div>
|
||
<div class="kpi-hint">Платят вовремя</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-value" id="kpiDebtors">0</div>
|
||
<div class="kpi-label">Должники</div>
|
||
<div class="kpi-hint">Есть задолженность</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-value" id="kpiDisconnected">0</div>
|
||
<div class="kpi-label">Отключены</div>
|
||
<div class="kpi-hint">За неуплату</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-value tabular" id="kpiDebtSum">0 ₸</div>
|
||
<div class="kpi-label">Общий долг</div>
|
||
<div class="kpi-hint">По всем должникам</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="controls-bar">
|
||
<input type="text" class="kt-ai-input search-input" id="searchInput" placeholder="Поиск по имени, телефону, адресу...">
|
||
<select class="kt-ai-select" id="statusFilter">
|
||
<option value="all">Все статусы</option>
|
||
<option value="active">Активные</option>
|
||
<option value="debtor">Должники</option>
|
||
<option value="disconnected">Отключены</option>
|
||
</select>
|
||
<select class="kt-ai-select" id="typeFilter">
|
||
<option value="all">Все типы</option>
|
||
<option value="individual">Физ. лица</option>
|
||
<option value="business">Юр. лица</option>
|
||
</select>
|
||
<button class="kt-ai-btn" data-variant="primary" id="addBtn">+ Добавить</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" id="exportBtn">📥 Экспорт CSV</button>
|
||
</div>
|
||
|
||
<div class="table-container">
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Клиент</th>
|
||
<th>Тип</th>
|
||
<th>Телефон</th>
|
||
<th>Адрес</th>
|
||
<th>Тариф</th>
|
||
<th>Баланс</th>
|
||
<th>Статус</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tableBody"></tbody>
|
||
</table>
|
||
<div class="empty-state" id="emptyState" style="display:none">
|
||
<p>Нет данных для отображения</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="modalOverlay">
|
||
<div class="modal">
|
||
<h2 class="modal-title" id="modalTitle">Добавить клиента</h2>
|
||
<form id="clientForm">
|
||
<input type="hidden" id="clientId">
|
||
<div class="form-group">
|
||
<label class="form-label">ФИО / Название организации *</label>
|
||
<input type="text" class="form-input" id="clientName" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Тип клиента *</label>
|
||
<select class="form-select" id="clientType" required>
|
||
<option value="individual">Физ. лицо</option>
|
||
<option value="business">Юр. лицо</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Телефон *</label>
|
||
<input type="tel" class="form-input" id="clientPhone" placeholder="+7 (7XX) XXX-XX-XX" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Адрес *</label>
|
||
<input type="text" class="form-input" id="clientAddress" placeholder="г. Кокшетау, ул., дом, квартира" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Тариф *</label>
|
||
<select class="form-select" id="clientTariff" required>
|
||
<option value="home_basic">Домашний Базовый</option>
|
||
<option value="home_optima">Домашний Оптима</option>
|
||
<option value="home_max">Домашний Макс</option>
|
||
<option value="business_start">Бизнес Старт</option>
|
||
<option value="business_pro">Бизнес Про</option>
|
||
<option value="business_premium">Бизнес Премиум</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Баланс (₸)</label>
|
||
<input type="number" class="form-input" id="clientBalance" placeholder="0" value="0">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Статус *</label>
|
||
<select class="form-select" id="clientStatus" required>
|
||
<option value="active">Активный</option>
|
||
<option value="debtor">Должник</option>
|
||
<option value="disconnected">Отключён</option>
|
||
</select>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button type="button" class="kt-ai-btn" data-variant="ghost" id="cancelBtn">Отмена</button>
|
||
<button type="submit" class="kt-ai-btn" data-variant="primary">Сохранить</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const STORAGE_KEY = 'kt_clients_data';
|
||
const DEMO_DATA = [
|
||
{ id: 1, name: 'Иванов Сергей Петрович', type: 'individual', phone: '+7 (777) 123-45-67', address: 'г. Кокшетау, ул. Абая, 45-12', tariff: 'home_optima', balance: 0, status: 'active' },
|
||
{ id: 2, name: 'ТОО "Алмаз"', type: 'business', phone: '+7 (701) 234-56-78', address: 'г. Кокшетау, пр. Абылай хана, 120', tariff: 'business_pro', balance: -15000, status: 'debtor' },
|
||
{ id: 3, name: 'Петрова Елена Владимировна', type: 'individual', phone: '+7 (778) 345-67-89', address: 'г. Кокшетау, мкр. Сарыарка, 8-34', tariff: 'home_basic', balance: 0, status: 'active' },
|
||
{ id: 4, name: 'ИП "Нурлан"', type: 'business', phone: '+7 (702) 456-78-90', address: 'г. Кокшетау, ул. Ауезова, 67', tariff: 'business_start', balance: -8500, status: 'debtor' },
|
||
{ id: 5, name: 'Сидоров Александр Михайлович', type: 'individual', phone: '+7 (776) 567-89-01', address: 'г. Кокшетау, ул. Бейбитшилик, 23-56', tariff: 'home_max', balance: 0, status: 'active' },
|
||
{ id: 6, name: 'Кафе "Достык"', type: 'business', phone: '+7 (705) 678-90-12', address: 'г. Кокшетау, ул. Козыбаева, 89', tariff: 'business_premium', balance: 0, status: 'active' },
|
||
{ id: 7, name: 'Козлова Наталья Ивановна', type: 'individual', phone: '+7 (775) 789-01-23', address: 'г. Кокшетау, мкр. Оркен, 15-8', tariff: 'home_optima', balance: -3200, status: 'debtor' },
|
||
{ id: 8, name: 'ТОО "Байтерк"', type: 'business', phone: '+7 (707) 890-12-34', address: 'г. Кокшетау, пр. Конаева, 45', tariff: 'business_pro', balance: 0, status: 'disconnected' },
|
||
{ id: 9, name: 'Ахметов Ержан Куанышевич', type: 'individual', phone: '+7 (774) 901-23-45', address: 'г. Кокшетау, ул. Шокана Уалиханова, 78-23', tariff: 'home_basic', balance: 0, status: 'active' },
|
||
{ id: 10, name: 'Магазин "Сул"', type: 'business', phone: '+7 (708) 012-34-56', address: 'г. Кокшетау, ул. Желтоксан, 34', tariff: 'business_start', balance: 0, status: 'active' },
|
||
{ id: 11, name: 'Смирнова Ольга Петровна', type: 'individual', phone: '+7 (773) 123-45-67', address: 'г. Кокшетау, мкр. Байтерек, 5-67', tariff: 'home_optima', balance: -5600, status: 'debtor' },
|
||
{ id: 12, name: 'ИП "Айгерим"', type: 'business', phone: '+7 (700) 234-56-78', address: 'г. Кокшетау, ул. Манаса, 12', tariff: 'business_pro', balance: 0, status: 'active' },
|
||
{ id: 13, name: 'Уалиев Муратбек', type: 'individual', phone: '+7 (772) 345-67-89', address: 'г. Кокшетау, ул. Пушкина, 56-14', tariff: 'home_max', balance: 0, status: 'active' },
|
||
{ id: 14, name: 'ТОО "Кокше-Транс"', type: 'business', phone: '+7 (706) 456-78-90', address: 'г. Кокшетау, ул. Интернациональная, 234', tariff: 'business_premium', balance: -45000, status: 'disconnected' },
|
||
{ id: 15, name: 'Никитенко Игорь Васильевич', type: 'individual', phone: '+7 (771) 567-89-01', address: 'г. Кокшетау, мкр. Жулдыз, 9-45', tariff: 'home_basic', balance: 0, status: 'active' },
|
||
{ id: 16, name: 'Салон красоты "Ару"', type: 'business', phone: '+7 (709) 678-90-12', address: 'г. Кокшетау, пр. Абылай хана, 78', tariff: 'business_start', balance: -2100, status: 'debtor' },
|
||
{ id: 17, name: 'Досанова Гульнара', type: 'individual', phone: '+7 (770) 789-01-23', address: 'г. Кокшетау, ул. Тәуелсіздік, 101-78', tariff: 'home_optima', balance: 0, status: 'active' },
|
||
{ id: 18, name: 'ИП "Серик"', type: 'business', phone: '+7 (703) 890-12-34', address: 'г. Кокшетау, ул. Мира, 45', tariff: 'business_pro', balance: 0, status: 'active' }
|
||
];
|
||
|
||
function loadData() {
|
||
const stored = localStorage.getItem(STORAGE_KEY);
|
||
if (stored) return JSON.parse(stored);
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(DEMO_DATA));
|
||
return DEMO_DATA;
|
||
}
|
||
|
||
function saveData(data) {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||
}
|
||
|
||
function getStatusLabel(status) {
|
||
const labels = { active: 'Активен', debtor: 'Должник', disconnected: 'Отключён' };
|
||
return labels[status] || status;
|
||
}
|
||
|
||
function getStatusClass(status) {
|
||
const classes = { active: 'status-ok', debtor: 'status-warn', disconnected: 'status-risk' };
|
||
return classes[status] || '';
|
||
}
|
||
|
||
function getTariffName(tariff) {
|
||
const names = {
|
||
home_basic: 'Домашний Базовый',
|
||
home_optima: 'Домашний Оптима',
|
||
home_max: 'Домашний Макс',
|
||
business_start: 'Бизнес Старт',
|
||
business_pro: 'Бизнес Про',
|
||
business_premium: 'Бизнес Премиум'
|
||
};
|
||
return names[tariff] || tariff;
|
||
}
|
||
|
||
function getTypeLabel(type) {
|
||
return type === 'individual' ? 'Физ. лицо' : 'Юр. лицо';
|
||
}
|
||
|
||
function formatBalance(balance) {
|
||
const val = Number(balance) || 0;
|
||
return (val >= 0 ? '' : '-') + Math.abs(val).toLocaleString('ru-RU') + ' ₸';
|
||
}
|
||
|
||
function updateKPIs(data) {
|
||
const total = data.length;
|
||
const active = data.filter(d => d.status === 'active').length;
|
||
const debtors = data.filter(d => d.status === 'debtor').length;
|
||
const disconnected = data.filter(d => d.status === 'disconnected').length;
|
||
const debtSum = data.filter(d => d.status === 'debtor').reduce((s, d) => s + Math.abs(Number(d.balance) || 0), 0);
|
||
|
||
document.getElementById('kpiTotal').textContent = total;
|
||
document.getElementById('kpiActive').textContent = active;
|
||
document.getElementById('kpiDebtors').textContent = debtors;
|
||
document.getElementById('kpiDisconnected').textContent = disconnected;
|
||
document.getElementById('kpiDebtSum').textContent = debtSum.toLocaleString('ru-RU') + ' ₸';
|
||
}
|
||
|
||
function renderTable(data) {
|
||
const tbody = document.getElementById('tableBody');
|
||
const emptyState = document.getElementById('emptyState');
|
||
|
||
if (data.length === 0) {
|
||
tbody.innerHTML = '';
|
||
emptyState.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
emptyState.style.display = 'none';
|
||
tbody.innerHTML = data.map(item => `
|
||
<tr>
|
||
<td><strong>${escapeHtml(item.name)}</strong></td>
|
||
<td>${getTypeLabel(item.type)}</td>
|
||
<td class="tabular">${escapeHtml(item.phone)}</td>
|
||
<td>${escapeHtml(item.address)}</td>
|
||
<td>${getTariffName(item.tariff)}</td>
|
||
<td class="text-right tabular" style="color: ${Number(item.balance) < 0 ? 'var(--kt-ai-status-risk-fg)' : 'inherit'}">${formatBalance(item.balance)}</td>
|
||
<td><span class="status-chip ${getStatusClass(item.status)}">${getStatusLabel(item.status)}</span></td>
|
||
<td class="actions-cell">
|
||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="editClient(${item.id})">✏️</button>
|
||
<button class="kt-ai-btn" data-variant="danger" data-size="sm" onclick="deleteClient(${item.id})">🗑️</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function filterData() {
|
||
const search = document.getElementById('searchInput').value.toLowerCase();
|
||
const status = document.getElementById('statusFilter').value;
|
||
const type = document.getElementById('typeFilter').value;
|
||
|
||
let data = loadData();
|
||
|
||
if (search) {
|
||
data = data.filter(d =>
|
||
d.name.toLowerCase().includes(search) ||
|
||
d.phone.includes(search) ||
|
||
d.address.toLowerCase().includes(search)
|
||
);
|
||
}
|
||
if (status !== 'all') {
|
||
data = data.filter(d => d.status === status);
|
||
}
|
||
if (type !== 'all') {
|
||
data = data.filter(d => d.type === type);
|
||
}
|
||
|
||
renderTable(data);
|
||
updateKPIs(data);
|
||
}
|
||
|
||
function openModal(isEdit = false) {
|
||
document.getElementById('modalTitle').textContent = isEdit ? 'Редактировать клиента' : 'Добавить клиента';
|
||
document.getElementById('modalOverlay').classList.add('on');
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('modalOverlay').classList.remove('on');
|
||
document.getElementById('clientForm').reset();
|
||
document.getElementById('clientId').value = '';
|
||
}
|
||
|
||
function editClient(id) {
|
||
const data = loadData();
|
||
const item = data.find(d => d.id === id);
|
||
if (!item) return;
|
||
|
||
document.getElementById('clientId').value = item.id;
|
||
document.getElementById('clientName').value = item.name;
|
||
document.getElementById('clientType').value = item.type;
|
||
document.getElementById('clientPhone').value = item.phone;
|
||
document.getElementById('clientAddress').value = item.address;
|
||
document.getElementById('clientTariff').value = item.tariff;
|
||
document.getElementById('clientBalance').value = item.balance;
|
||
document.getElementById('clientStatus').value = item.status;
|
||
|
||
openModal(true);
|
||
}
|
||
|
||
function deleteClient(id) {
|
||
if (!confirm('Вы уверены, что хотите удалить этого клиента?')) return;
|
||
let data = loadData();
|
||
data = data.filter(d => d.id !== id);
|
||
saveData(data);
|
||
filterData();
|
||
}
|
||
|
||
document.getElementById('addBtn').addEventListener('click', () => openModal(false));
|
||
document.getElementById('cancelBtn').addEventListener('click', closeModal);
|
||
document.getElementById('modalOverlay').addEventListener('click', (e) => {
|
||
if (e.target === e.currentTarget) closeModal();
|
||
});
|
||
|
||
document.getElementById('clientForm').addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
|
||
const id = document.getElementById('clientId').value;
|
||
const clientData = {
|
||
id: id ? Number(id) : Date.now(),
|
||
name: document.getElementById('clientName').value,
|
||
type: document.getElementById('clientType').value,
|
||
phone: document.getElementById('clientPhone').value,
|
||
address: document.getElementById('clientAddress').value,
|
||
tariff: document.getElementById('clientTariff').value,
|
||
balance: Number(document.getElementById('clientBalance').value) || 0,
|
||
status: document.getElementById('clientStatus').value
|
||
};
|
||
|
||
let data = loadData();
|
||
if (id) {
|
||
const idx = data.findIndex(d => d.id == id);
|
||
if (idx >= 0) data[idx] = clientData;
|
||
} else {
|
||
data.push(clientData);
|
||
}
|
||
|
||
saveData(data);
|
||
closeModal();
|
||
filterData();
|
||
});
|
||
|
||
document.getElementById('searchInput').addEventListener('input', filterData);
|
||
document.getElementById('statusFilter').addEventListener('change', filterData);
|
||
document.getElementById('typeFilter').addEventListener('change', filterData);
|
||
|
||
document.getElementById('exportBtn').addEventListener('click', () => {
|
||
const data = loadData();
|
||
const headers = ['Название', 'Тип', 'Телефон', 'Адрес', 'Тариф', 'Баланс', 'Статус'];
|
||
const rows = data.map(d => [
|
||
d.name, getTypeLabel(d.type), d.phone, d.address, getTariffName(d.tariff), d.balance, getStatusLabel(d.status)
|
||
]);
|
||
const csv = '\uFEFF' + [headers.join(';'), ...rows.map(r => r.map(v => '"' + String(v).replace(/"/g, '""') + '"').join(';'))].join('\n');
|
||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = 'klienty_kokshetau.csv';
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
});
|
||
|
||
document.getElementById('themeToggle').addEventListener('click', () => {
|
||
const html = document.documentElement;
|
||
const current = html.getAttribute('data-theme');
|
||
html.setAttribute('data-theme', current === 'light' ? 'dark' : 'light');
|
||
});
|
||
|
||
// Init
|
||
filterData();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|