deshbord-klientam-abonentam/index.html

416 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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