// ==================== ДАННЫЕ И ХРАНИЛИЩЕ ==================== const DB_KEY = 'inventory_db_v1'; const DEMO_DATA = { equipment: [ { id: 1, date: '2024-01-15', sn: 'SN001', imei: '123456789012345', model: 'TP-Link Archer C6', status: 'у абонента', account: '100001', order: 'Z001', employee_id: null, initiator: 'Отдел продаж' }, { id: 2, date: '2024-01-16', sn: 'SN002', imei: '123456789012346', model: 'Huawei HG8245H', status: 'у сотрудника', account: null, order: null, employee_id: 1, initiator: null }, { id: 3, date: '2024-01-17', sn: 'SN003', imei: '', model: 'Eltex NTU-2', status: 'на складе', account: null, order: null, employee_id: null, initiator: null }, { id: 4, date: '2024-01-18', sn: 'SN004', imei: '123456789012347', model: 'TP-Link Archer C6', status: 'поиск / дз', account: null, order: null, employee_id: null, initiator: null }, { id: 5, date: '2024-01-19', sn: 'SN005', imei: '123456789012348', model: 'D-Link DIR-825', status: 'списано', account: null, order: null, employee_id: null, initiator: null }, { id: 6, date: '2024-01-20', sn: 'SN006', imei: '', model: 'Huawei HG8245H', status: 'на складе', account: null, order: null, employee_id: null, initiator: null }, { id: 7, date: '2024-01-21', sn: 'SN007', imei: '123456789012349', model: 'Eltex NTU-2', status: 'у абонента', account: '100002', order: 'Z002', employee_id: null, initiator: 'Техподдержка' }, { id: 8, date: '2024-01-22', sn: 'SN008', imei: '', model: 'TP-Link Archer C6', status: 'у сотрудника', account: null, order: null, employee_id: 2, initiator: null } ], movements: [ { id: 1, date: '2024-01-15', sn: 'SN001', model: 'TP-Link Archer C6', operation: 'оприходование', from: null, to: null, account: null, order: null, comment: 'Первичное оприходование', status: 'на складе', processed: true }, { id: 2, date: '2024-01-15', sn: 'SN001', model: 'TP-Link Archer C6', operation: 'установка', from: 'на складе', to: 'абонент', account: '100001', order: 'Z001', comment: 'Монтаж', status: 'у абонента', processed: true }, { id: 3, date: '2024-01-16', sn: 'SN002', model: 'Huawei HG8245H', operation: 'оприходование', from: null, to: null, account: null, order: null, comment: 'Первичное оприходование', status: 'на складе', processed: true }, { id: 4, date: '2024-01-16', sn: 'SN002', model: 'Huawei HG8245H', operation: 'выдача', from: 'на складе', to: 'Иванов И.И.', account: null, order: null, comment: 'Выдан сотруднику', status: 'у сотрудника', processed: true }, { id: 5, date: '2024-01-17', sn: 'SN003', model: 'Eltex NTU-2', operation: 'оприходование', from: null, to: null, account: null, order: null, comment: 'Первичное оприходование', status: 'на складе', processed: true }, { id: 6, date: '2024-01-18', sn: 'SN004', model: 'TP-Link Archer C6', operation: 'оприходование', from: null, to: null, account: null, order: null, comment: 'Первичное оприходование', status: 'поиск / дз', processed: true }, { id: 7, date: '2024-01-19', sn: 'SN005', model: 'D-Link DIR-825', operation: 'оприходование', from: null, to: null, account: null, order: null, comment: 'Первичное оприходование', status: 'на складе', processed: true }, { id: 8, date: '2024-01-19', sn: 'SN005', model: 'D-Link DIR-825', operation: 'списание', from: 'на складе', to: null, account: null, order: null, comment: 'Неисправно', status: 'списано', processed: true }, { id: 9, date: '2024-01-20', sn: 'SN006', model: 'Huawei HG8245H', operation: 'оприходование', from: null, to: null, account: null, order: null, comment: 'Первичное оприходование', status: 'на складе', processed: true }, { id: 10, date: '2024-01-21', sn: 'SN007', model: 'Eltex NTU-2', operation: 'оприходование', from: null, to: null, account: null, order: null, comment: 'Первичное оприходование', status: 'на складе', processed: true }, { id: 11, date: '2024-01-21', sn: 'SN007', model: 'Eltex NTU-2', operation: 'установка', from: 'на складе', to: 'абонент', account: '100002', order: 'Z002', comment: 'Монтаж', status: 'у абонента', processed: true }, { id: 12, date: '2024-01-22', sn: 'SN008', model: 'TP-Link Archer C6', operation: 'оприходование', from: null, to: null, account: null, order: null, comment: 'Первичное оприходование', status: 'на складе', processed: true }, { id: 13, date: '2024-01-22', sn: 'SN008', model: 'TP-Link Archer C6', operation: 'выдача', from: 'на складе', to: 'Петров П.П.', account: null, order: null, comment: 'Выдан сотруднику', status: 'у сотрудника', processed: true } ], employees: [ { id: 1, name: 'Иванов И.И.', active: true }, { id: 2, name: 'Петров П.П.', active: true }, { id: 3, name: 'Сидоров С.С.', active: false } ], models: [ 'TP-Link Archer C6', 'Huawei HG8245H', 'Eltex NTU-2', 'D-Link DIR-825', 'Mikrotik hAP ac2', 'ZTE F660' ], statuses: [ { id: 'на складе', color: '#28a745', label: 'На складе' }, { id: 'у сотрудника', color: '#ffc107', label: 'У сотрудника' }, { id: 'у абонента', color: '#00b8cc', label: 'У абонента' }, { id: 'поиск / дз', color: '#dc3545', label: 'Поиск / ДЗ' }, { id: 'списано', color: '#6c757d', label: 'Списано' } ] }; // Загрузка данных function loadDB() { const data = localStorage.getItem(DB_KEY); return data ? JSON.parse(data) : DEMO_DATA; } // Сохранение данных function saveDB(data) { localStorage.setItem(DB_KEY, JSON.stringify(data)); } // Инициализация БД let db = loadDB(); // ==================== УТИЛИТЫ ==================== function generateId() { return Date.now(); } function formatDate(dateStr) { if (!dateStr) return ''; const d = new Date(dateStr); return d.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: '2-digit' }); } function getStatusBadgeClass(status) { const map = { 'на складе': 'badge-warehouse', 'у сотрудника': 'badge-employee', 'у абонента': 'badge-subscriber', 'поиск / дз': 'badge-search', 'списано': 'badge-written-off' }; return map[status] || ''; } // ==================== НАВИГАЦИЯ ==================== document.querySelectorAll('.nav-item').forEach(btn => { btn.addEventListener('click', () => { const screen = btn.dataset.screen; // Переключение активной навигации document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active')); btn.classList.add('active'); // Переключение экранов document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); document.getElementById(screen).classList.add('active'); // Обновление данных при переключении if (screen === 'dashboard') updateDashboard(); if (screen === 'warehouse') renderWarehouse(); if (screen === 'movements') renderMovements(); if (screen === 'settings') renderSettings(); }); }); // Переключение табов операций document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { const tab = btn.dataset.tab; document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); document.getElementById('tab-' + tab).classList.add('active'); }); }); // ==================== ДАШБОРД ==================== function updateDashboard() { // KPI const total = db.equipment.length; const warehouse = db.equipment.filter(e => e.status === 'на складе').length; const subscribers = db.equipment.filter(e => e.status === 'у абонента').length; const employees = db.equipment.filter(e => e.status === 'у сотрудника').length; const search = db.equipment.filter(e => e.status === 'поиск / дз').length; const writtenOff = db.equipment.filter(e => e.status === 'списано').length; document.getElementById('kpi-total').textContent = total; document.getElementById('kpi-warehouse').textContent = warehouse; document.getElementById('kpi-subscribers').textContent = subscribers; document.getElementById('kpi-employees').textContent = employees; document.getElementById('kpi-search').textContent = search; document.getElementById('kpi-written-off').textContent = writtenOff; // Диаграмма (простая визуализация) renderStatusChart(); // Оборудование у сотрудников renderEmployeeEquipment(); // Последние операции renderRecentOperations(); } function renderStatusChart() { const container = document.getElementById('statusChart'); const statuses = db.statuses.map(s => ({ label: s.label, count: db.equipment.filter(e => e.status === s.id).length, color: s.color })); const total = statuses.reduce((sum, s) => sum + s.count, 0) || 1; const segments = statuses.map(s => { const percent = ((s.count / total) * 100).toFixed(1); return `
${percent}%
${s.label}
`; }).join(''); container.innerHTML = `
${segments}
`; } function renderEmployeeEquipment() { const tbody = document.querySelector('#employee-equipment-table tbody'); const activeEmployees = db.employees.filter(e => e.active); const data = activeEmployees.map(emp => { const items = db.equipment.filter(e => e.employee_id === emp.id); return { name: emp.name, count: items.length, equipment: items.map(i => `${i.model} (${i.sn})`).join(', ') || '—' }; }).filter(d => d.count > 0); tbody.innerHTML = data.map(d => ` ${d.name} ${d.count} ${d.equipment} `).join(''); } function renderRecentOperations() { const tbody = document.querySelector('#recent-operations-table tbody'); const recent = db.movements.slice(-6).reverse(); tbody.innerHTML = recent.map(m => ` ${formatDate(m.date)} ${m.sn} ${m.model} ${m.operation} ${m.to || '—'} `).join(''); } // ==================== ОПЕРАЦИИ ==================== // Инициализация форм function initOperations() { // Установка сегодняшней даты document.getElementById('income-date').value = new Date().toISOString().split('T')[0]; // Заполнение справочников updateModelSelects(); updateEmployeeSelects(); updateStatusFilters(); updateTypeFilters(); // Обработчики document.getElementById('income-form').addEventListener('submit', handleIncome); document.getElementById('issue-form').addEventListener('submit', handleIssue); document.getElementById('install-form').addEventListener('submit', handleInstall); document.getElementById('return-form').addEventListener('submit', handleReturn); document.getElementById('add-model-btn').addEventListener('click', () => { document.getElementById('model-modal').classList.add('active'); }); document.getElementById('new-model-form').addEventListener('submit', (e) => { e.preventDefault(); const name = document.getElementById('quick-model-name').value.trim(); if (name && !db.models.includes(name)) { db.models.push(name); saveDB(db); updateModelSelects(); document.getElementById('model-modal').classList.remove('active'); document.getElementById('quick-model-name').value = ''; document.getElementById('income-model').value = name; } }); // Поиск оборудования в формах document.getElementById('issue-sn-search').addEventListener('input', (e) => { showEquipmentPreview(e.target.value, 'issue'); }); document.getElementById('install-sn-search').addEventListener('input', (e) => { showEquipmentPreview(e.target.value, 'install'); }); document.getElementById('return-sn-search').addEventListener('input', (e) => { showEquipmentPreview(e.target.value, 'return'); }); } function updateModelSelects() { const selects = ['income-model']; selects.forEach(id => { const select = document.getElementById(id); if (select) { select.innerHTML = db.models.map(m => ``).join(''); } }); } function updateEmployeeSelects() { const activeEmployees = db.employees.filter(e => e.active); const selects = ['issue-employee']; selects.forEach(id => { const select = document.getElementById(id); if (select) { select.innerHTML = activeEmployees.map(e => ``).join(''); } }); } function updateStatusFilters() { const select = document.getElementById('warehouse-status-filter'); select.innerHTML = '' + db.statuses.map(s => ``).join(''); } function updateTypeFilters() { const select = document.getElementById('warehouse-type-filter'); select.innerHTML = '' + db.models.map(m => ``).join(''); } function showEquipmentPreview(sn, prefix) { const equipment = db.equipment.find(e => e.sn.toLowerCase() === sn.toLowerCase()); const preview = document.getElementById(prefix + '-preview'); if (equipment) { document.getElementById(prefix + '-preview-sn').textContent = equipment.sn; document.getElementById(prefix + '-preview-model').textContent = equipment.model; const statusBadge = document.getElementById(prefix + '-preview-status'); statusBadge.textContent = equipment.status; statusBadge.className = 'badge ' + getStatusBadgeClass(equipment.status); let holder = '—'; if (equipment.employee_id) { const emp = db.employees.find(e => e.id === equipment.employee_id); holder = emp ? emp.name : 'Неизвестный сотрудник'; } else if (equipment.account) { holder = 'Л/С: ' + equipment.account; } document.getElementById(prefix + '-preview-holder').textContent = holder; preview.style.display = 'block'; } else { preview.style.display = 'none'; } } // Оприходование function handleIncome(e) { e.preventDefault(); const date = document.getElementById('income-date').value; const sn = document.getElementById('income-sn').value.trim().toUpperCase(); const imei = document.getElementById('income-imei').value.trim(); const model = document.getElementById('income-model').value; const status = document.getElementById('income-status').value; // Проверка на дубликат SN if (db.equipment.some(eq => eq.sn === sn)) { alert('Оборудование с таким SN уже существует!'); return; } const equipment = { id: generateId(), date, sn, imei, model, status, account: null, order: null, employee_id: null, initiator: null }; db.equipment.push(equipment); // Запись в архив db.movements.push({ id: generateId(), date, sn, model, operation: 'оприходование', from: null, to: null, account: null, order: null, comment: 'Первичное оприходование', status, processed: true }); saveDB(db); alert('Оборудование успешно оприходовано!'); e.target.reset(); document.getElementById('income-date').value = date; document.getElementById('issue-preview').style.display = 'none'; } // Выдача сотруднику function handleIssue(e) { e.preventDefault(); const sn = document.getElementById('issue-sn-search').value.trim().toUpperCase(); const employeeId = parseInt(document.getElementById('issue-employee').value); const equipment = db.equipment.find(eq => eq.sn === sn); if (!equipment) { alert('Оборудование не найдено!'); return; } const oldStatus = equipment.status; const employee = db.employees.find(emp => emp.id === employeeId); // Обновление оборудования equipment.status = 'у сотрудника'; equipment.employee_id = employeeId; equipment.account = null; equipment.order = null; // Запись в архив db.movements.push({ id: generateId(), date: new Date().toISOString().split('T')[0], sn: equipment.sn, model: equipment.model, operation: 'выдача', from: oldStatus, to: employee.name, account: null, order: null, comment: 'Выдано сотруднику', status: 'у сотрудника', processed: true }); saveDB(db); alert('Оборудование выдано сотруднику!'); e.target.reset(); document.getElementById('issue-preview').style.display = 'none'; } // Установка абоненту function handleInstall(e) { e.preventDefault(); const sn = document.getElementById('install-sn-search').value.trim().toUpperCase(); const account = document.getElementById('install-account').value.trim(); const order = document.getElementById('install-order').value.trim(); const initiator = document.getElementById('install-initiator').value.trim(); const hasModel = document.getElementById('install-has-model').checked; const hasImei = document.getElementById('install-has-imei').checked; const equipment = db.equipment.find(eq => eq.sn === sn); if (!equipment) { alert('Оборудование не найдено!'); return; } const oldStatus = equipment.status; // Обновление оборудования equipment.status = 'у абонента'; equipment.account = account; equipment.order = order; equipment.employee_id = null; equipment.initiator = initiator; // Авто-дозапись реквизитов if (hasModel && !equipment.model) { equipment.model = prompt('Введите наименование модели:'); } if (hasImei && !equipment.imei) { equipment.imei = prompt('Введите IMEI:'); } // Запись в архив db.movements.push({ id: generateId(), date: new Date().toISOString().split('T')[0], sn: equipment.sn, model: equipment.model, operation: 'установка', from: oldStatus, to: 'абонент', account, order, comment: 'Установка абоненту' + (initiator ? `. Инициатор: ${initiator}` : ''), status: 'у абонента', processed: true }); saveDB(db); alert('Оборудование установлено абоненту!'); e.target.reset(); document.getElementById('install-preview').style.display = 'none'; document.getElementById('install-has-model').checked = false; document.getElementById('install-has-imei').checked = false; } // Возврат / Списание / ДЗ function handleReturn(e) { e.preventDefault(); const type = document.getElementById('return-type').value; const sn = document.getElementById('return-sn-search').value.trim().toUpperCase(); const comment = document.getElementById('return-comment').value.trim(); const equipment = db.equipment.find(eq => eq.sn === sn); if (!equipment) { alert('Оборудование не найдено!'); return; } const oldStatus = equipment.status; let newStatus, operation, to; switch (type) { case 'снятие': newStatus = 'на складе'; operation = 'снятие'; to = 'склад'; break; case 'списание': newStatus = 'списано'; operation = 'списание'; to = null; break; case 'дз': newStatus = 'поиск / дз'; operation = 'дз'; to = null; break; case 'вывести из склада': newStatus = 'поиск / дз'; operation = 'перемещение'; to = 'поиск / дз'; break; case 'перемещение с др складов': newStatus = 'на складе'; operation = 'перемещение'; to = 'склад'; break; } // Обновление оборудования equipment.status = newStatus; if (newStatus === 'на складе') { equipment.employee_id = null; equipment.account = null; equipment.order = null; } // Запись в архив db.movements.push({ id: generateId(), date: new Date().toISOString().split('T')[0], sn: equipment.sn, model: equipment.model, operation, from: oldStatus, to, account: null, order: null, comment: comment || type, status: newStatus, processed: true }); saveDB(db); alert('Операция выполнена!'); e.target.reset(); document.getElementById('return-preview').style.display = 'none'; } // ==================== СКЛАД ==================== function renderWarehouse() { const tbody = document.querySelector('#warehouse-table tbody'); const search = document.getElementById('warehouse-search').value.toLowerCase(); const statusFilter = document.getElementById('warehouse-status-filter').value; const typeFilter = document.getElementById('warehouse-type-filter').value; const employeeFilter = document.getElementById('warehouse-employee-filter').value; let filtered = db.equipment; if (search) { filtered = filtered.filter(e => e.sn.toLowerCase().includes(search) || (e.imei && e.imei.toLowerCase().includes(search)) || (e.account && e.account.includes(search)) || (e.order && e.order.toLowerCase().includes(search)) || (e.employee_id && db.employees.find(emp => emp.id === e.employee_id && emp.name.toLowerCase().includes(search))) ); } if (statusFilter) { filtered = filtered.filter(e => e.status === statusFilter); } if (typeFilter) { filtered = filtered.filter(e => e.model === typeFilter); } if (employeeFilter) { const empId = parseInt(employeeFilter); filtered = filtered.filter(e => e.employee_id === empId); } tbody.innerHTML = filtered.map(e => { const emp = e.employee_id ? db.employees.find(emp => emp.id === e.employee_id) : null; return ` ${formatDate(e.date)} ${e.sn} ${e.imei || '—'} ${e.model} ${e.status} ${e.account || '—'} ${e.order || '—'} ${emp ? emp.name : '—'} `; }).join(''); } // Фильтры склада document.getElementById('warehouse-search').addEventListener('input', renderWarehouse); document.getElementById('warehouse-status-filter').addEventListener('change', renderWarehouse); document.getElementById('warehouse-type-filter').addEventListener('change', renderWarehouse); // Заполнение фильтра сотрудников function updateEmployeeFilter() { const select = document.getElementById('warehouse-employee-filter'); select.innerHTML = '' + db.employees.filter(e => e.active).map(e => ``).join(''); } document.getElementById('warehouse-employee-filter').addEventListener('change', renderWarehouse); // ==================== ПЕРЕДВИЖЕНИЯ ==================== function renderMovements() { const tbody = document.querySelector('#movements-table tbody'); const search = document.getElementById('movements-search').value.toLowerCase(); const typeFilter = document.getElementById('movements-type-filter').value; let filtered = db.movements; if (search) { filtered = filtered.filter(m => m.sn.toLowerCase().includes(search) || m.model.toLowerCase().includes(search) || (m.to && m.to.toLowerCase().includes(search)) ); } if (typeFilter) { filtered = filtered.filter(m => m.operation === typeFilter); } tbody.innerHTML = filtered.map(m => ` ${formatDate(m.date)} ${m.sn} ${m.model} ${m.operation} ${m.from || '—'} ${m.to || '—'} ${m.account || '—'} ${m.order || '—'} ${m.comment || '—'} `).join(''); } function toggleProcessed(id) { const movement = db.movements.find(m => m.id === id); if (movement) { movement.processed = !movement.processed; saveDB(db); renderMovements(); } } document.getElementById('movements-search').addEventListener('input', renderMovements); document.getElementById('movements-type-filter').addEventListener('change', renderMovements); // ==================== ИСТОРИЯ (МОДАЛЬНОЕ ОКНО) ==================== window.showHistory = function(sn) { const equipment = db.equipment.find(e => e.sn === sn); if (!equipment) return; const movements = db.movements.filter(m => m.sn === sn).sort((a, b) => new Date(b.date) - new Date(a.date)); document.getElementById('modal-equipment-info').innerHTML = ` ${equipment.model}
SN: ${equipment.sn}
IMEI: ${equipment.imei || '—'}
Текущий статус: ${equipment.status} `; document.getElementById('modal-timeline').innerHTML = movements.map(m => `
${formatDate(m.date)}
${m.operation}
${m.from ? 'Откуда: ' + m.from + '. ' : ''} ${m.to ? 'Кому: ' + m.to + '. ' : ''} ${m.comment || ''}
`).join(''); document.getElementById('history-modal').classList.add('active'); }; // Закрытие модальных окон document.querySelectorAll('.modal-close').forEach(btn => { btn.addEventListener('click', () => { document.getElementById(btn.dataset.modal).classList.remove('active'); }); }); document.querySelectorAll('.modal').forEach(modal => { modal.addEventListener('click', (e) => { if (e.target === modal) { modal.classList.remove('active'); } }); }); // ==================== НАСТРОЙКИ ==================== function renderSettings() { // Сотрудники const empTbody = document.querySelector('#employees-table tbody'); empTbody.innerHTML = db.employees.map(e => ` ${e.name} ${e.active ? 'Активен' : 'В архиве'} `).join(''); // Модели const modelTbody = document.querySelector('#models-table tbody'); const modelCounts = {}; db.equipment.forEach(e => { modelCounts[e.model] = (modelCounts[e.model] || 0) + 1; }); modelTbody.innerHTML = db.models.map(m => ` ${m} ${modelCounts[m] || 0} `).join(''); // Статусы document.getElementById('status-legend').innerHTML = db.statuses.map(s => `
${s.label}
`).join(''); updateEmployeeFilter(); } window.toggleEmployee = function(id) { const employee = db.employees.find(e => e.id === id); if (employee) { employee.active = !employee.active; saveDB(db); renderSettings(); } }; // Добавление сотрудника document.getElementById('add-employee-form').addEventListener('submit', (e) => { e.preventDefault(); const name = document.getElementById('new-employee-name').value.trim(); if (name) { db.employees.push({ id: generateId(), name, active: true }); saveDB(db); document.getElementById('new-employee-name').value = ''; renderSettings(); updateEmployeeSelects(); } }); // Добавление модели document.getElementById('add-model-form').addEventListener('submit', (e) => { e.preventDefault(); const name = document.getElementById('new-model-name').value.trim(); if (name && !db.models.includes(name)) { db.models.push(name); saveDB(db); document.getElementById('new-model-name').value = ''; renderSettings(); updateModelSelects(); updateTypeFilters(); } }); // ==================== ЭКСПОРТ ==================== document.getElementById('export-warehouse-btn').addEventListener('click', () => { const headers = ['Дата', 'SN', 'IMEI', 'Модель', 'Статус', 'Л/С', 'Заказ', 'Сотрудник']; const rows = db.equipment.map(e => { const emp = e.employee_id ? db.employees.find(emp => emp.id === e.employee_id) : null; return [ e.date, e.sn, e.imei, e.model, e.status, e.account || '', e.order || '', emp ? emp.name : '' ]; }); const csv = '\uFEFF' + [headers, ...rows].map(row => row.map(cell => '"' + String(cell).replace(/"/g, '""') + '"').join(';') ).join('\n'); downloadFile(csv, 'warehouse.csv', 'text/csv'); }); document.getElementById('export-movements-btn').addEventListener('click', () => { const headers = ['Дата', 'SN', 'Модель', 'Операция', 'Откуда', 'Кому', 'Л/С', 'Заказ', 'Комментарий', 'Статус']; const rows = db.movements.map(m => [ m.date, m.sn, m.model, m.operation, m.from || '', m.to || '', m.account || '', m.order || '', m.comment || '', m.status ]); const csv = '\uFEFF' + [headers, ...rows].map(row => row.map(cell => '"' + String(cell).replace(/"/g, '""') + '"').join(';') ).join('\n'); downloadFile(csv, 'movements.csv', 'text/csv'); }); function downloadFile(content, filename, type) { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } // ==================== РЕЗЕРВНЫЕ КОПИИ ==================== document.getElementById('download-backup-btn').addEventListener('click', () => { const json = JSON.stringify(db, null, 2); downloadFile(json, 'inventory_backup_' + new Date().toISOString().split('T')[0] + '.json', 'application/json'); }); document.getElementById('restore-backup-input').addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { try { const data = JSON.parse(event.target.result); if (data.equipment && data.movements && data.employees && data.models) { db = data; saveDB(db); alert('База успешно восстановлена!'); location.reload(); } else { alert('Неверный формат файла backup'); } } catch (err) { alert('Ошибка чтения файла: ' + err.message); } }; reader.readAsText(file); }); document.getElementById('reset-demo-btn').addEventListener('click', () => { if (confirm('Вы уверены? Все данные будут сброшены к демо-версии.')) { db = JSON.parse(JSON.stringify(DEMO_DATA)); saveDB(db); location.reload(); } }); // ==================== ИНИЦИАЛИЗАЦИЯ ==================== initOperations(); updateDashboard(); renderWarehouse(); renderMovements(); renderSettings();