// VOLS Registry - Kazakhtelecom Business
// State
let state = {
screen: 'start',
device: null,
mode: null,
user: null,
users: [],
records: [],
audit: [],
filter: { segment: '', oblast: '', status: '', search: '' },
sort: { key: null, dir: 'asc' },
editingId: null,
showAdminPopup: false
};
const TODAY = '2026-08-22';
const SUBDIVISIONS = ['ДПБО', 'ДПККК'];
const SEGMENTS = ['Корпоративный', 'Гос.органы', 'Банки', 'Телеком', 'Нефтегаз', 'Энергетика', 'Транспорт', 'Другой'];
const OBLASTS = ['Алматы', 'Астана', 'Шымкент', 'Актюбинская', 'Атырауская', 'Западно-Казахстанская', 'Карагандинская', 'Костанайская', 'Кызылординская', 'Мангистауская', 'Павлодарская', 'Северо-Казахстанская', 'Туркестанская', 'Восточно-Казахстанская', 'Жамбылская', 'Улытауская', 'Абайская'];
const BUILD_STATUSES = ['Проект', 'СМР', 'СМР завершено', 'Исключено'];
const CONNECT_STATUSES = ['Подключено', 'Заявка', 'Отказ', 'Перспектива'];
// Storage helpers
const storage = {
get(key, fallback) { try { return JSON.parse(localStorage.getItem(key)) ?? fallback; } catch { return fallback; } },
set(key, val) { localStorage.setItem(key, JSON.stringify(val)); }
};
// Seed data
function seedData() {
const defaultUsers = [
{ id: 1, lastName: 'Торегожиев', firstName: 'Саяжан', middleName: 'Сабырович', phone: '+77771112233', role: 'admin', region: 'all' },
{ id: 2, lastName: 'Иванов', firstName: 'Петр', middleName: '', phone: '+77774445566', role: 'editor', region: 'ДПБО' },
{ id: 3, lastName: 'Смирнов', firstName: 'Алексей', middleName: '', phone: '+77777778899', role: 'editor', region: 'ДПККК' },
{ id: 4, lastName: 'Гость', firstName: 'Тестовый', middleName: '', phone: '+77770001122', role: 'view', region: 'none' }
];
const genRecords = () => {
const r = [];
const clients = ['ТОО "KazEnergy"', 'АО "Народный Банк"', 'ТОО "TelecomPlus"', 'ТОО "OilServ"', 'АО "ТрансЛогистик"', 'ТОО "TechSoft"', 'ТОО "BuildMaster"', 'АО "KazMunay"', 'ТОО "DataCenter"', 'ТОО "SmartGrid"'];
const activities = ['Нефтегазовый сектор', 'Финансы и банки', 'Телекоммуникации', 'Транспорт и логистика', 'IT и технологии', 'Строительство', 'Энергетика', 'Государственные услуги'];
const cities = ['Алматы', 'Астана', 'Шымкент', 'Актобе', 'Атырау', 'Уральск', 'Караганда', 'Костанай', 'Кызылорда', 'Актау', 'Павлодар', 'Петропавловск', 'Туркестан', 'Усть-Каменогорск', 'Тараз'];
const addresses = ['ул. Абая 150', 'пр. Достык 234', 'ул. Толе би 89', 'ул. Сатпаева 45', 'пр. Абая 12', 'ул. Желтоксан 67', 'ул. Карасай батыра 101', 'пр.Tauke хана 78'];
for (let i = 0; i < 30; i++) {
const sub = SUBDIVISIONS[i % 2];
const obl = OBLASTS[i % OBLASTS.length];
const seg = SEGMENTS[i % SEGMENTS.length];
const buildStart = new Date(2025, Math.floor(Math.random()*12), Math.floor(Math.random()*28)+1);
const buildEnd = new Date(buildStart);
buildEnd.setMonth(buildEnd.getMonth() + Math.floor(Math.random()*6)+3);
const buildEndStr = buildEnd.toISOString().slice(0,10);
const connected = Math.random() > 0.4;
const connDate = connected ? new Date(buildEnd.getFullYear(), buildEnd.getMonth()+1, Math.floor(Math.random()*28)+1) : null;
r.push({
id: Date.now() + i,
subdivision: sub,
oblast: obl,
district: cities[i % cities.length] + 'ский',
city: cities[i % cities.length],
address: addresses[i % addresses.length] + ', офис ' + (Math.floor(Math.random()*500)+10),
client: clients[i % clients.length],
activity: activities[Math.floor(Math.random()*activities.length)],
hasTv: Math.random() > 0.5,
segment: seg,
buildStatus: BUILD_STATUSES[Math.floor(Math.random()*BUILD_STATUSES.length)],
buildStart: buildStart.toISOString().slice(0,10),
buildEnd: buildEndStr,
note: i % 5 === 0 ? 'Требуется согласование' : '',
deviceNum: 'SW-' + String(1000 + i),
connectStatus: connected ? 'Подключено' : CONNECT_STATUSES[Math.floor(Math.random()*(CONNECT_STATUSES.length-1))+1],
connectDate: connDate ? connDate.toISOString().slice(0,10) : '',
tariff: connected ? ['Бизнес Старт', 'Бизнес Оптима', 'Бизнес Премиум', 'Корпоративный'][Math.floor(Math.random()*4)] : '',
arpu: connected ? Math.floor(Math.random()*150)+30 : 0
});
}
return r;
};
if (!storage.get('volsUsers', null)) storage.set('volsUsers', defaultUsers);
if (!storage.get('volsRecords', null)) storage.set('volsRecords', genRecords());
if (!storage.get('volsAudit', null)) storage.set('volsAudit', []);
state.users = storage.get('volsUsers');
state.records = storage.get('volsRecords');
state.audit = storage.get('volsAudit');
}
// Audit log
function logAudit(action, details) {
if (!state.user) return;
const entry = {
id: Date.now(),
date: new Date().toISOString().slice(0,19).replace('T', ' '),
user: state.user.lastName + ' ' + state.user.firstName.charAt(0) + '.',
action,
details
};
state.audit.unshift(entry);
if (state.audit.length > 300) state.audit.pop();
storage.set('volsAudit', state.audit);
}
// Render
function render() {
const app = document.getElementById('app');
if (state.screen === 'start') { app.innerHTML = renderStart(); initStart(); }
else if (state.screen === 'auth') { app.innerHTML = renderAuth(); initAuth(); }
else if (state.screen === 'main') { app.innerHTML = renderMain(); initMain(); }
}
function renderStart() {
return `
Реестр объектов ВОЛС
Kazakhtelecom Business
`;
}
function initStart() {
let device = null, mode = null;
document.querySelectorAll('.device-btn').forEach(btn => {
btn.onclick = () => { device = btn.dataset.device; document.querySelectorAll('.device-btn').forEach(b=>b.classList.remove('selected')); btn.classList.add('selected'); updateContinue(); };
});
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.onclick = () => { mode = btn.dataset.mode; document.querySelectorAll('.mode-btn').forEach(b=>b.classList.remove('selected')); btn.classList.add('selected'); updateContinue(); };
});
function updateContinue() {
const btn = document.getElementById('continueBtn');
if (device && mode) { btn.classList.add('active'); btn.onclick = () => { state.device = device; state.mode = mode; state.screen = 'auth'; render(); }; }
}
}
function renderAuth() {
const demoUsers = state.users.slice(0, 4);
return `
Вход в систему
Выберите демо-пользователя или зарегистрируйтесь
Демо-пользователи
${demoUsers.map(u => `
`).join('')}
${state.mode === 'view' ? '
← Назад' : ''}
`;
}
function initAuth() {
document.querySelectorAll('.user-btn').forEach(btn => {
btn.onclick = () => {
const u = state.users.find(x => x.id === +btn.dataset.id);
state.user = u;
logAudit('Вход в систему', `Пользователь: ${u.lastName} ${u.firstName}`);
state.screen = 'main';
render();
};
});
const regBtn = document.getElementById('regBtn');
if (regBtn) regBtn.onclick = () => {
const last = document.getElementById('regLast').value.trim();
const first = document.getElementById('regFirst').value.trim();
const middle = document.getElementById('regMiddle').value.trim();
const phone = document.getElementById('regPhone').value.trim();
if (!last || !first || !phone) { alert('Заполните фамилию, имя и телефон'); return; }
const newUser = { id: Date.now(), lastName: last, firstName: first, middleName: middle, phone, role: 'view', region: 'none' };
state.users.push(newUser);
storage.set('volsUsers', state.users);
state.user = newUser;
logAudit('Регистрация', `Новый пользователь: ${last} ${first}`);
state.screen = 'main';
render();
};
const back = document.getElementById('backLink');
if (back) back.onclick = (e) => { e.preventDefault(); state.screen = 'start'; render(); };
}
function renderMain() {
const u = state.user;
const filtered = getFilteredRecords();
const stats = getStats(filtered);
const isAdmin = u.role === 'admin';
return `
${isAdmin ? `` : ''}
${u.role !== 'view' ? `
` : ''}
${isAdmin ? renderAdminPopup() : ''}
`;
}
function renderAdminPopup() {
const u = state.user;
return `
`;
}
function initMain() {
const u = state.user;
let currentView = 'table';
let detailId = null;
// Mobile menu
const menuBtn = document.getElementById('menuBtn');
const sidebar = document.getElementById('sidebar');
if (menuBtn) menuBtn.onclick = () => sidebar.classList.toggle('open');
// Nav
document.querySelectorAll('.nav-item').forEach(item => {
item.onclick = () => {
document.querySelectorAll('.nav-item').forEach(i=>i.classList.remove('active'));
item.classList.add('active');
currentView = item.dataset.view;
if (state.device === 'mobile') sidebar.classList.remove('open');
renderContent();
};
});
// Logout
const logout = document.getElementById('logoutBtn');
if (logout) logout.onclick = () => { logAudit('Выход', ''); state.screen = 'start'; state.user = null; state.device = null; state.mode = null; render(); };
// Admin popup
const adminPopupBtn = document.getElementById('adminPopupBtn');
const adminPopup = document.getElementById('adminPopup');
if (adminPopupBtn && adminPopup) {
adminPopupBtn.onclick = (e) => { e.stopPropagation(); state.showAdminPopup = !state.showAdminPopup; adminPopup.style.display = state.showAdminPopup ? 'block' : 'none'; };
document.addEventListener('click', () => { if (state.showAdminPopup) { state.showAdminPopup = false; adminPopup.style.display = 'none'; } });
}
// Filters
const searchInput = document.getElementById('searchInput');
const segmentFilter = document.getElementById('segmentFilter');
const oblastFilter = document.getElementById('oblastFilter');
const statusFilter = document.getElementById('statusFilter');
const resetBtn = document.getElementById('resetBtn');
[searchInput, segmentFilter, oblastFilter, statusFilter].forEach(el => {
if (el) el.oninput = () => { state.filter = { search: searchInput?.value || '', segment: segmentFilter?.value || '', oblast: oblastFilter?.value || '', status: statusFilter?.value || '' }; renderContent(); };
});
if (resetBtn) resetBtn.onclick = () => { state.filter = { search: '', segment: '', oblast: '', status: '' }; searchInput.value = ''; segmentFilter.value = ''; oblastFilter.value = ''; statusFilter.value = ''; renderContent(); };
// Add
const addBtn = document.getElementById('addBtn');
if (addBtn) addBtn.onclick = () => openModal();
// Export
const exportBtn = document.getElementById('exportBtn');
if (exportBtn) exportBtn.onclick = exportToExcel;
renderContent();
function renderContent() {
const area = document.getElementById('contentArea');
const title = document.getElementById('pageTitle');
const actionBar = document.getElementById('actionBar');
if (currentView === 'table' || currentView === 'dashboard') {
actionBar.style.display = 'flex';
} else {
actionBar.style.display = 'none';
}
if (currentView === 'table') {
title.textContent = 'Реестр объектов';
const filtered = getFilteredRecords();
if (state.device === 'desktop') {
area.innerHTML = renderTable(filtered);
initTable(area);
} else {
area.innerHTML = renderMobileCards(filtered);
initMobileCards(area);
}
} else if (currentView === 'dashboard') {
title.textContent = 'Дашборд';
area.innerHTML = renderDashboard();
initDashboard();
} else if (currentView === 'users') {
title.textContent = 'Пользователи';
area.innerHTML = renderUsers();
initUsers();
} else if (currentView === 'audit') {
title.textContent = 'Журнал аудита';
area.innerHTML = renderAudit();
}
}
function getFilteredRecords() {
const f = state.filter;
return state.records.filter(r => {
if (f.segment && r.segment !== f.segment) return false;
if (f.oblast && r.oblast !== f.oblast) return false;
if (f.status && r.connectStatus !== f.status) return false;
if (f.search) {
const s = f.search.toLowerCase();
const hay = Object.values(r).join(' ').toLowerCase();
if (!hay.includes(s)) return false;
}
// Role filter
if (u.role === 'editor' && u.region !== 'all' && r.subdivision !== u.region) return false;
return true;
});
}
function renderTable(records) {
const cols = [
{ key: 'subdivision', label: 'Подразд.' },
{ key: 'oblast', label: 'Область' },
{ key: 'client', label: 'Клиент' },
{ key: 'segment', label: 'Сегмент' },
{ key: 'buildStatus', label: 'Стат. строит.' },
{ key: 'connectStatus', label: 'Стат. подкл.' },
{ key: 'arpu', label: 'ARPU' },
{ key: 'actions', label: '' }
];
const isOverdue = r => r.buildEnd < TODAY && !['СМР завершено', 'Исключено'].includes(r.buildStatus);
return `
`;
}
function initTable(area) {
// Sort
area.querySelectorAll('th[data-sort]').forEach(th => {
th.onclick = () => {
const key = th.dataset.sort;
if (!key || key === 'actions') return;
if (state.sort.key === key) {
state.sort.dir = state.sort.dir === 'asc' ? 'desc' : 'asc';
} else {
state.sort.key = key;
state.sort.dir = 'asc';
}
renderContent();
};
});
// Edit
area.querySelectorAll('[data-edit]').forEach(btn => {
btn.onclick = () => {
const r = state.records.find(x => x.id === +btn.dataset.edit);
openModal(r);
};
});
// Delete
area.querySelectorAll('[data-del]').forEach(btn => {
btn.onclick = () => {
const r = state.records.find(x => x.id === +btn.dataset.del);
openConfirm(r);
};
});
}
function renderMobileCards(records) {
if (records.length === 0) return 'Нет данных
';
return `${records.map(r => `
${r.oblast}, ${r.city}
${r.segment}
${r.buildStatus}
ARPU: ${r.arpu || 0} ₸
`).join('')}
`;
}
function initMobileCards(area) {
area.querySelectorAll('[data-detail]').forEach(btn => {
btn.onclick = () => {
detailId = +btn.dataset.detail;
openDetail(detailId);
};
});
}
function openDetail(id) {
const r = state.records.find(x => x.id === id);
if (!r) return;
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML = `
${renderDetailRow('Подразделение', r.subdivision)}
${renderDetailRow('Область', r.oblast)}
${renderDetailRow('Район', r.district)}
${renderDetailRow('Город', r.city)}
${renderDetailRow('Адрес', r.address)}
${renderDetailRow('Сегмент', r.segment)}
${renderDetailRow('Вид деятельности', r.activity)}
${renderDetailRow('Наличие ТВ', r.hasTv ? 'Да' : 'Нет')}
${renderDetailRow('Статус строительства', r.buildStatus)}
${renderDetailRow('Начало строительства', r.buildStart)}
${renderDetailRow('Срок строительства', r.buildEnd)}
${renderDetailRow('Примечание', r.note || '-')}
${renderDetailRow('№ устройства', r.deviceNum)}
${renderDetailRow('Статус подключения', r.connectStatus)}
${renderDetailRow('Дата подключения', r.connectDate || '-')}
${renderDetailRow('Тариф', r.tariff || '-')}
${renderDetailRow('ARPU', r.arpu ? r.arpu + ' ₸' : '-')}
${u.role !== 'view' ? `` : ''}
`;
document.body.appendChild(overlay);
document.getElementById('closeDetail').onclick = () => overlay.remove();
const editBtn = document.getElementById('editDetail');
if (editBtn) editBtn.onclick = () => { overlay.remove(); openModal(r); };
}
function renderDetailRow(label, value) {
return `${label}${value}
`;
}
function renderDashboard() {
const all = state.records;
const s = getStats(all);
const byOblast = {};
const byConnect = {};
const byDept = { ДПБО: { total: 0, connected: 0, arpu: 0 }, ДПККК: { total: 0, connected: 0, arpu: 0 } };
all.forEach(r => {
byOblast[r.oblast] = (byOblast[r.oblast] || 0) + 1;
byConnect[r.connectStatus] = (byConnect[r.connectStatus] || 0) + 1;
if (SUBDIVISIONS.includes(r.subdivision)) {
byDept[r.subdivision].total++;
if (r.connectStatus === 'Подключено') { byDept[r.subdivision].connected++; byDept[r.subdivision].arpu += r.arpu || 0; }
}
});
return `
${s.totalArpu.toLocaleString()} ₸
Суммарный ARPU
ДПБО: всего ${byDept.ДПБО.total}, подключено ${byDept.ДПБО.connected}, ARPU ${byDept.ДПБО.arpu.toLocaleString()} ₸
ДПККК: всего ${byDept.ДПККК.total}, подключено ${byDept.ДПККК.connected}, ARPU ${byDept.ДПККК.arpu.toLocaleString()} ₸
`;
}
function initDashboard() {
const all = state.records;
const byOblast = {};
const byConnect = {};
all.forEach(r => {
byOblast[r.oblast] = (byOblast[r.oblast] || 0) + 1;
byConnect[r.connectStatus] = (byConnect[r.connectStatus] || 0) + 1;
});
new Chart(document.getElementById('oblastChart'), {
type: 'bar',
data: {
labels: Object.keys(byOblast),
datasets: [{ label: 'Объекты', data: Object.values(byOblast), backgroundColor: '#4B1D6B', borderRadius: 4 }]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, grid: { color: '#f0f0f0' } }, x: { grid: { display: false } } } }
});
new Chart(document.getElementById('connectChart'), {
type: 'doughnut',
data: {
labels: Object.keys(byConnect),
datasets: [{ data: Object.values(byConnect), backgroundColor: ['#22c55e', '#3b82f6', '#f59e0b', '#ef4444'], borderWidth: 0 }]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right' } } }
});
}
function renderUsers() {
return `
`;
}
function initUsers() {
area = document.getElementById('contentArea');
area.querySelectorAll('[data-edit-user]').forEach(btn => {
btn.onclick = () => {
const user = state.users.find(x => x.id === +btn.dataset.editUser);
openUserModal(user);
};
});
area.querySelectorAll('[data-del-user]').forEach(btn => {
btn.onclick = () => {
const user = state.users.find(x => x.id === +btn.dataset.delUser);
if (confirm(`Удалить пользователя ${user.lastName} ${user.firstName}?`)) {
state.users = state.users.filter(x => x.id !== user.id);
storage.set('volsUsers', state.users);
renderContent();
logAudit('Удаление пользователя', `${user.lastName} ${user.firstName}`);
}
};
});
}
function openUserModal(user) {
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML = `
`;
document.body.appendChild(overlay);
document.getElementById('closeUserModal').onclick = () => overlay.remove();
document.getElementById('cancelUser').onclick = () => overlay.remove();
document.getElementById('saveUser').onclick = () => {
user.lastName = document.getElementById('userLast').value.trim();
user.firstName = document.getElementById('userFirst').value.trim();
user.middleName = document.getElementById('userMiddle').value.trim();
user.phone = document.getElementById('userPhone').value.trim();
user.role = document.getElementById('userRole').value;
user.region = document.getElementById('userRegion').value;
storage.set('volsUsers', state.users);
overlay.remove();
renderContent();
logAudit('Изменение пользователя', `${user.lastName} ${user.firstName}`);
};
}
function renderAudit() {
return `
`;
}
function openModal(record) {
const isEdit = !!record;
const canEditAll = u.role === 'admin' || u.role === 'editor';
const isDeptEditor = u.role === 'editor' && record && u.region !== 'all' && record.subdivision !== u.region;
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML = `
${renderField('Подразделение', 'subdivision', record?.subdivision || 'ДПБО', SUBDIVISIONS, !canEditAll || isDeptEditor)}
${renderField('Область', 'oblast', record?.oblast || '', OBLASTS, !canEditAll)}
${renderField('Район', 'district', record?.district || '', null, !canEditAll)}
${renderField('Город', 'city', record?.city || '', null, !canEditAll)}
${renderField('Адрес', 'address', record?.address || '', null, !canEditAll, true)}
${renderField('Клиент', 'client', record?.client || '', null, !canEditAll)}
${renderField('Сегмент', 'segment', record?.segment || SEGMENTS[0], SEGMENTS, !canEditAll)}
${renderField('Вид деятельности', 'activity', record?.activity || '', null, !canEditAll, true)}
${renderField('Наличие ТВ', 'hasTv', record?.hasTv ? 'true' : 'false', ['true', 'false'], !canEditAll)}
${renderField('Статус строительства', 'buildStatus', record?.buildStatus || 'Проект', BUILD_STATUSES, !canEditAll)}
${renderField('Начало строительства', 'buildStart', record?.buildStart || '', null, !canEditAll)}
${renderField('Срок строительства', 'buildEnd', record?.buildEnd || '', null, !canEditAll)}
${renderField('Примечание', 'note', record?.note || '', null, !canEditAll, true)}
${renderField('№ устройства', 'deviceNum', record?.deviceNum || '', null, !canEditAll)}
${renderField('Статус подключения', 'connectStatus', record?.connectStatus || 'Заявка', CONNECT_STATUSES, !canEditAll)}
${renderField('Дата подключения', 'connectDate', record?.connectDate || '', null, !canEditAll)}
${renderField('Тариф', 'tariff', record?.tariff || '', null, !canEditAll)}
${renderField('ARPU (₸)', 'arpu', record?.arpu || '0', null, !canEditAll)}
`;
document.body.appendChild(overlay);
document.getElementById('closeModal').onclick = () => overlay.remove();
document.getElementById('cancelModal').onclick = () => overlay.remove();
document.getElementById('saveModal').onclick = () => {
const data = {
subdivision: document.getElementById('subdivision').value,
oblast: document.getElementById('oblast').value,
district: document.getElementById('district').value,
city: document.getElementById('city').value,
address: document.getElementById('address').value,
client: document.getElementById('client').value,
segment: document.getElementById('segment').value,
activity: document.getElementById('activity').value,
hasTv: document.getElementById('hasTv').value === 'true',
buildStatus: document.getElementById('buildStatus').value,
buildStart: document.getElementById('buildStart').value,
buildEnd: document.getElementById('buildEnd').value,
note: document.getElementById('note').value,
deviceNum: document.getElementById('deviceNum').value,
connectStatus: document.getElementById('connectStatus').value,
connectDate: document.getElementById('connectDate').value,
tariff: document.getElementById('tariff').value,
arpu: +document.getElementById('arpu').value || 0
};
if (isEdit) {
const idx = state.records.findIndex(x => x.id === record.id);
const old = { ...state.records[idx] };
state.records[idx] = { ...record, ...data };
storage.set('volsRecords', state.records);
logAudit('Редактирование', `ID: ${record.id}, Клиент: ${data.client}`);
} else {
const newRec = { id: Date.now(), ...data };
state.records.push(newRec);
storage.set('volsRecords', state.records);
logAudit('Создание', `Клиент: ${data.client}`);
}
overlay.remove();
renderContent();
};
}
function renderField(label, key, value, options, disabled, textarea) {
const input = textarea
? ``
: options
? ``
: key.includes('Date')
? ``
: key === 'arpu'
? ``
: ``;
return `${input}
`;
}
function openConfirm(record) {
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML = `
Удалить запись?
Клиент: ${record.client}
Эту операцию нельзя отменить
`;
document.body.appendChild(overlay);
document.getElementById('cancelConfirm').onclick = () => overlay.remove();
document.getElementById('confirmDel').onclick = () => {
state.records = state.records.filter(x => x.id !== record.id);
storage.set('volsRecords', state.records);
overlay.remove();
renderContent();
logAudit('Удаление', `Клиент: ${record.client}`);
};
}
function getStats(records) {
const total = records.length;
const connected = records.filter(r => r.connectStatus === 'Подключено').length;
const pending = records.filter(r => ['Заявка', 'Перспектива'].includes(r.connectStatus)).length;
const totalArpu = records.filter(r => r.connectStatus === 'Подключено').reduce((s, r) => s + (r.arpu || 0), 0);
return { total, connected, pending, totalArpu };
}
function exportToExcel() {
const ws = XLSX.utils.json_to_sheet(getFilteredRecords().map(r => ({
'Подразделение': r.subdivision,
'Область': r.oblast,
'Район': r.district,
'Город': r.city,
'Адрес': r.address,
'Клиент': r.client,
'Вид деятельности': r.activity,
'Наличие ТВ': r.hasTv ? 'Да' : 'Нет',
'Сегмент': r.segment,
'Статус строительства': r.buildStatus,
'Начало строительства': r.buildStart,
'Срок строительства': r.buildEnd,
'Примечание': r.note,
'№ устройства': r.deviceNum,
'Статус подключения': r.connectStatus,
'Дата подключения': r.connectDate,
'Тариф': r.tariff,
'ARPU': r.arpu
})));
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Объекты');
XLSX.writeFile(wb, 'reestr_vols_' + TODAY + '.xlsx');
logAudit('Экспорт в Excel', '');
}
}
function getFilteredRecords() {
const f = state.filter;
const u = state.user;
return state.records.filter(r => {
if (f.segment && r.segment !== f.segment) return false;
if (f.oblast && r.oblast !== f.oblast) return false;
if (f.status && r.connectStatus !== f.status) return false;
if (f.search) {
const s = f.search.toLowerCase();
const hay = Object.values(r).join(' ').toLowerCase();
if (!hay.includes(s)) return false;
}
if (u?.role === 'editor' && u.region !== 'all' && r.subdivision !== u.region) return false;
return true;
});
}
function getStats(records) {
const total = records.length;
const connected = records.filter(r => r.connectStatus === 'Подключено').length;
const pending = records.filter(r => r.connectStatus === 'Заявка' || r.connectStatus === 'Перспектива').length;
const totalArpu = records.filter(r => r.connectStatus === 'Подключено').reduce((s, r) => s + (r.arpu || 0), 0);
return { total, connected, pending, totalArpu };
}
function exportToExcel() {
const ws = XLSX.utils.json_to_sheet(getFilteredRecords().map(r => ({
'Подразделение': r.subdivision,
'Область': r.oblast,
'Район': r.district,
'Город': r.city,
'Адрес': r.address,
'Клиент': r.client,
'Вид деятельности': r.activity,
'Наличие ТВ': r.hasTv ? 'Да' : 'Нет',
'Сегмент': r.segment,
'Статус строительства': r.buildStatus,
'Начало строительства': r.buildStart,
'Срок строительства': r.buildEnd,
'Примечание': r.note,
'№ устройства': r.deviceNum,
'Статус подключения': r.connectStatus,
'Дата подключения': r.connectDate,
'Тариф': r.tariff,
'ARPU': r.arpu
})));
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Объекты');
XLSX.writeFile(wb, 'reestr_vols_' + TODAY + '.xlsx');
}
// Init
seedData();
render();