diff --git a/app.js b/app.js
new file mode 100644
index 0000000..24168c3
--- /dev/null
+++ b/app.js
@@ -0,0 +1,917 @@
+// 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();
diff --git a/index.html b/index.html
index c1d5e41..01ac00d 100644
--- a/index.html
+++ b/index.html
@@ -2,740 +2,195 @@
-
-Система учёта клиентов
+
+Реестр объектов ВОЛС | Kazakhtelecom Business
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- | № |
- Сегмент |
- Область |
- Район |
- Нас. пункт |
- Адрес |
- Клиент |
- Деятельность |
- Тех. возможность |
- Статус строительства |
- Начало |
- Завершение |
- Примечание |
- № устр-ва |
- Статус |
- Дата подключ. |
- Тариф |
- ARPU |
- Действия |
-
-
-
-
-
-
-
-
-
-
-
Новая запись
-
-
-
-
-
-
-
-
-
-
-
📈 Статистика
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
🔐 Настройка доступов
-
-
-
-
-
-
-
-
-
-
-
-
-
-
👥 Пользователи
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index dc5a160..0000000
--- a/package-lock.json
+++ /dev/null
@@ -1,343 +0,0 @@
-{
- "name": "client-tracker",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "client-tracker",
- "dependencies": {
- "formidable": "^3.5.1",
- "pptxgenjs": "^3.12.0",
- "xlsx": "^0.18.5"
- }
- },
- "node_modules/@noble/hashes": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
- "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
- "license": "MIT",
- "engines": {
- "node": "^14.21.3 || >=16"
- },
- "funding": {
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/@paralleldrive/cuid2": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
- "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
- "license": "MIT",
- "dependencies": {
- "@noble/hashes": "^1.1.5"
- }
- },
- "node_modules/@types/node": {
- "version": "18.19.130",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
- "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
- "license": "MIT",
- "dependencies": {
- "undici-types": "~5.26.4"
- }
- },
- "node_modules/adler-32": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
- "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=0.8"
- }
- },
- "node_modules/asap": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
- "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
- "license": "MIT"
- },
- "node_modules/cfb": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
- "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
- "license": "Apache-2.0",
- "dependencies": {
- "adler-32": "~1.3.0",
- "crc-32": "~1.2.0"
- },
- "engines": {
- "node": ">=0.8"
- }
- },
- "node_modules/codepage": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
- "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=0.8"
- }
- },
- "node_modules/core-util-is": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
- "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
- "license": "MIT"
- },
- "node_modules/crc-32": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
- "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
- "license": "Apache-2.0",
- "bin": {
- "crc32": "bin/crc32.njs"
- },
- "engines": {
- "node": ">=0.8"
- }
- },
- "node_modules/dezalgo": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
- "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
- "license": "ISC",
- "dependencies": {
- "asap": "^2.0.0",
- "wrappy": "1"
- }
- },
- "node_modules/formidable": {
- "version": "3.5.4",
- "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
- "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
- "license": "MIT",
- "dependencies": {
- "@paralleldrive/cuid2": "^2.2.2",
- "dezalgo": "^1.0.4",
- "once": "^1.4.0"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "url": "https://ko-fi.com/tunnckoCore/commissions"
- }
- },
- "node_modules/frac": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
- "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=0.8"
- }
- },
- "node_modules/https": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
- "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==",
- "license": "ISC"
- },
- "node_modules/image-size": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
- "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
- "license": "MIT",
- "dependencies": {
- "queue": "6.0.2"
- },
- "bin": {
- "image-size": "bin/image-size.js"
- },
- "engines": {
- "node": ">=16.x"
- }
- },
- "node_modules/immediate": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
- "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
- "license": "MIT"
- },
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "license": "ISC"
- },
- "node_modules/isarray": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
- "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
- "license": "MIT"
- },
- "node_modules/jszip": {
- "version": "3.10.1",
- "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
- "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
- "license": "(MIT OR GPL-3.0-or-later)",
- "dependencies": {
- "lie": "~3.3.0",
- "pako": "~1.0.2",
- "readable-stream": "~2.3.6",
- "setimmediate": "^1.0.5"
- }
- },
- "node_modules/lie": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
- "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
- "license": "MIT",
- "dependencies": {
- "immediate": "~3.0.5"
- }
- },
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "license": "ISC",
- "dependencies": {
- "wrappy": "1"
- }
- },
- "node_modules/pako": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
- "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
- "license": "(MIT AND Zlib)"
- },
- "node_modules/pptxgenjs": {
- "version": "3.12.0",
- "resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-3.12.0.tgz",
- "integrity": "sha512-ZozkYKWb1MoPR4ucw3/aFYlHkVIJxo9czikEclcUVnS4Iw/M+r+TEwdlB3fyAWO9JY1USxJDt0Y0/r15IR/RUA==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "^18.7.3",
- "https": "^1.0.0",
- "image-size": "^1.0.0",
- "jszip": "^3.7.1"
- }
- },
- "node_modules/process-nextick-args": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
- "license": "MIT"
- },
- "node_modules/queue": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
- "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
- "license": "MIT",
- "dependencies": {
- "inherits": "~2.0.3"
- }
- },
- "node_modules/readable-stream": {
- "version": "2.3.8",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
- "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
- "license": "MIT",
- "dependencies": {
- "core-util-is": "~1.0.0",
- "inherits": "~2.0.3",
- "isarray": "~1.0.0",
- "process-nextick-args": "~2.0.0",
- "safe-buffer": "~5.1.1",
- "string_decoder": "~1.1.1",
- "util-deprecate": "~1.0.1"
- }
- },
- "node_modules/safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "license": "MIT"
- },
- "node_modules/setimmediate": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
- "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
- "license": "MIT"
- },
- "node_modules/ssf": {
- "version": "0.11.2",
- "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
- "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
- "license": "Apache-2.0",
- "dependencies": {
- "frac": "~1.1.2"
- },
- "engines": {
- "node": ">=0.8"
- }
- },
- "node_modules/string_decoder": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
- "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "license": "MIT",
- "dependencies": {
- "safe-buffer": "~5.1.0"
- }
- },
- "node_modules/undici-types": {
- "version": "5.26.5",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
- "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
- "license": "MIT"
- },
- "node_modules/util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "license": "MIT"
- },
- "node_modules/wmf": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
- "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=0.8"
- }
- },
- "node_modules/word": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
- "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=0.8"
- }
- },
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "license": "ISC"
- },
- "node_modules/xlsx": {
- "version": "0.18.5",
- "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
- "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "adler-32": "~1.3.0",
- "cfb": "~1.2.1",
- "codepage": "~1.15.0",
- "crc-32": "~1.2.1",
- "ssf": "~0.11.2",
- "wmf": "~1.0.1",
- "word": "~0.3.0"
- },
- "bin": {
- "xlsx": "bin/xlsx.njs"
- },
- "engines": {
- "node": ">=0.8"
- }
- }
- }
-}
diff --git a/package.json b/package.json
index 4e8e833..77fe92b 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,11 @@
{
- "name": "client-tracker",
+ "name": "vols-registry",
"private": true,
"type": "commonjs",
"scripts": {
"start": "node --env-file=.env server.js"
},
"dependencies": {
- "xlsx": "^0.18.5",
- "pptxgenjs": "^3.12.0",
- "formidable": "^3.5.1"
+ "xlsx": "^0.18.5"
}
}
diff --git a/server.js b/server.js
deleted file mode 100644
index d416f67..0000000
--- a/server.js
+++ /dev/null
@@ -1,267 +0,0 @@
-const http = require("http");
-const fs = require("fs");
-const path = require("path");
-
-const PORT = process.env.PORT || 3000;
-const DATA = path.join(__dirname, "data.json");
-const USERS = path.join(__dirname, "users.json");
-const PERMISSIONS = path.join(__dirname, "permissions.json");
-
-function readJSON(file, empty) {
- try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch (_) { return empty; }
-}
-function writeJSON(file, data) {
- fs.writeFileSync(file, JSON.stringify(data, null, 2));
-}
-
-function loadRecords() {
- const data = readJSON(DATA, { records: [], sheets: [] });
- if (!data.records) data.records = [];
- if (!data.sheets) data.sheets = [];
- return data;
-}
-function saveRecords(data) {
- writeJSON(DATA, data);
-}
-
-function loadUsers() {
- return readJSON(USERS, { users: [] });
-}
-function saveUsers(data) {
- writeJSON(USERS, data);
-}
-
-function loadPermissions() {
- return readJSON(PERMISSIONS, { permissions: [] });
-}
-function savePermissions(data) {
- writeJSON(PERMISSIONS, data);
-}
-
-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({}); }
- });
- });
-}
-
-const mime = {
- ".html": "text/html; charset=utf-8",
- ".css": "text/css; charset=utf-8",
- ".js": "application/javascript; charset=utf-8",
- ".json": "application/json; charset=utf-8",
- ".png": "image/png",
- ".jpg": "image/jpeg",
- ".svg": "image/svg+xml",
-};
-
-http.createServer(async (req, res) => {
- const url = req.url.split("?")[0];
- const method = req.method;
-
- // API: Records
- if (method === "GET" && url === "/api/records") {
- res.writeHead(200, { "Content-Type": "application/json" });
- return res.end(JSON.stringify(loadRecords()));
- }
- if (method === "POST" && url === "/api/records") {
- const rec = await body(req);
- const data = loadRecords();
- data.records.push({ id: Date.now(), ...rec });
- saveRecords(data);
- res.writeHead(200, { "Content-Type": "application/json" });
- return res.end(JSON.stringify({ ok: true }));
- }
- if (method === "PUT" && url === "/api/records") {
- const rec = await body(req);
- const data = loadRecords();
- const idx = data.records.findIndex(r => r.id === rec.id);
- if (idx >= 0) {
- data.records[idx] = { ...data.records[idx], ...rec };
- saveRecords(data);
- }
- res.writeHead(200, { "Content-Type": "application/json" });
- return res.end(JSON.stringify({ ok: true }));
- }
- if (method === "DELETE" && url === "/api/records") {
- const { id } = await body(req);
- const data = loadRecords();
- data.records = data.records.filter(r => r.id !== id);
- saveRecords(data);
- res.writeHead(200, { "Content-Type": "application/json" });
- return res.end(JSON.stringify({ ok: true }));
- }
-
- // API: Sheets (области, города, ДПБО, ДПКК)
- if (method === "GET" && url === "/api/sheets") {
- res.writeHead(200, { "Content-Type": "application/json" });
- return res.end(JSON.stringify(loadRecords()));
- }
- if (method === "POST" && url === "/api/sheets") {
- const sheet = await body(req);
- const data = loadRecords();
- data.sheets.push({ id: Date.now(), ...sheet });
- saveRecords(data);
- res.writeHead(200, { "Content-Type": "application/json" });
- return res.end(JSON.stringify({ ok: true }));
- }
-
- // API: Users
- if (method === "GET" && url === "/api/users") {
- res.writeHead(200, { "Content-Type": "application/json" });
- return res.end(JSON.stringify(loadUsers()));
- }
- if (method === "POST" && url === "/api/users") {
- const user = await body(req);
- const data = loadUsers();
- data.users.push({ id: Date.now(), ...user });
- saveUsers(data);
- res.writeHead(200, { "Content-Type": "application/json" });
- return res.end(JSON.stringify({ ok: true }));
- }
- if (method === "PUT" && url === "/api/users") {
- const user = await body(req);
- const data = loadUsers();
- const idx = data.users.findIndex(u => u.id === user.id);
- if (idx >= 0) {
- data.users[idx] = { ...data.users[idx], ...user };
- saveUsers(data);
- }
- res.writeHead(200, { "Content-Type": "application/json" });
- return res.end(JSON.stringify({ ok: true }));
- }
- if (method === "DELETE" && url === "/api/users") {
- const { id } = await body(req);
- const data = loadUsers();
- data.users = data.users.filter(u => u.id !== id);
- saveUsers(data);
- res.writeHead(200, { "Content-Type": "application/json" });
- return res.end(JSON.stringify({ ok: true }));
- }
-
- // API: Permissions
- if (method === "GET" && url === "/api/permissions") {
- res.writeHead(200, { "Content-Type": "application/json" });
- return res.end(JSON.stringify(loadPermissions()));
- }
- if (method === "POST" && url === "/api/permissions") {
- const perm = await body(req);
- const data = loadPermissions();
- data.permissions.push({ id: Date.now(), ...perm });
- savePermissions(data);
- res.writeHead(200, { "Content-Type": "application/json" });
- return res.end(JSON.stringify({ ok: true }));
- }
- if (method === "DELETE" && url === "/api/permissions") {
- const { id } = await body(req);
- const data = loadPermissions();
- data.permissions = data.permissions.filter(p => p.id !== id);
- savePermissions(data);
- res.writeHead(200, { "Content-Type": "application/json" });
- return res.end(JSON.stringify({ ok: true }));
- }
-
- // API: Statistics
- if (method === "POST" && url === "/api/stats") {
- const { startDate, endDate, groupBy } = await body(req);
- const data = loadRecords();
- const start = startDate ? new Date(startDate) : null;
- const end = endDate ? new Date(endDate) : null;
-
- let filtered = data.records.filter(r => {
- if (!r.date) return true;
- const d = new Date(r.date);
- if (start && d < start) return false;
- if (end && d > end) return false;
- return true;
- });
-
- const stats = {};
- filtered.forEach(r => {
- const key = r[groupBy] || "Не указано";
- if (!stats[key]) stats[key] = 0;
- stats[key]++;
- });
-
- res.writeHead(200, { "Content-Type": "application/json" });
- return res.end(JSON.stringify({ stats, total: filtered.length }));
- }
-
- // API: Export to Excel
- if (method === "GET" && url.startsWith("/api/export/excel")) {
- const XLSX = require("xlsx");
- const data = loadRecords();
- const wb = XLSX.utils.book_new();
-
- const ws = XLSX.utils.json_to_sheet(data.records.map(r => ({
- "№": r.num || "",
- "Сегмент": r.segment || "",
- "Область": r.region || "",
- "Район": r.district || "",
- "Населенный пункт": r.settlement || "",
- "Адрес": r.address || "",
- "Наименование клиента": r.clientName || "",
- "Вид деятельности": r.activity || "",
- "Тех. возможность": r.techPossibility || "",
- "Статус строительства": r.constructionStatus || "",
- "Начало строительства": r.constructionStart || "",
- "Сроки завершения": r.constructionEnd || "",
- "Примечание": r.note || "",
- "№ устройства": r.deviceNum || "",
- "Статус подключения": r.connectionStatus || "",
- "Дата подключения": r.connectionDate || "",
- "Тарифный план": r.tariff || "",
- "ARPU (тыс. тг)": r.arpu || ""
- })));
-
- XLSX.utils.book_append_sheet(wb, ws, "Все записи");
-
- const regions = [...new Set(data.records.map(r => r.region).filter(Boolean))];
- regions.forEach(region => {
- const regionData = data.records.filter(r => r.region === region);
- const regionWs = XLSX.utils.json_to_sheet(regionData.map(r => ({
- "№": r.num || "",
- "Сегмент": r.segment || "",
- "Район": r.district || "",
- "Населенный пункт": r.settlement || "",
- "Адрес": r.address || "",
- "Наименование клиента": r.clientName || "",
- "Вид деятельности": r.activity || "",
- "Тех. возможность": r.techPossibility || "",
- "Статус строительства": r.constructionStatus || "",
- "Начало строительства": r.constructionStart || "",
- "Сроки завершения": r.constructionEnd || "",
- "Примечание": r.note || "",
- "№ устройства": r.deviceNum || "",
- "Статус подключения": r.connectionStatus || "",
- "Дата подключения": r.connectionDate || "",
- "Тарифный план": r.tariff || "",
- "ARPU (тыс. тг)": r.arpu || ""
- })));
- XLSX.utils.book_append_sheet(wb, regionWs, region.slice(0, 31));
- });
-
- const buffer = XLSX.write(wb, { type: "buffer", bookType: "xlsx" });
- res.writeHead(200, {
- "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
- "Content-Disposition": "attachment; filename=clients.xlsx"
- });
- return res.end(buffer);
- }
-
- // Static files
- let file = url === "/" ? "/index.html" : 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 = mime[ext] || "text/plain";
- res.writeHead(200, { "Content-Type": type });
- return res.end(fs.readFileSync(full));
- }
-
- res.writeHead(404, { "Content-Type": "text/plain" });
- res.end("Not found");
-}).listen(PORT, () => console.log("Сервер запущен на порту " + PORT));