889 lines
33 KiB
JavaScript
889 lines
33 KiB
JavaScript
// ==================== ДАННЫЕ И ХРАНИЛИЩЕ ====================
|
||
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 `<div class="chart-segment" style="flex:${s.count || 0.1}; background:${s.color}20; border-color:${s.color}; padding:12px; border-radius:8px; margin:4px;">
|
||
<div style="font-weight:600; color:${s.color}">${percent}%</div>
|
||
<div style="font-size:12px; color:var(--text-secondary)">${s.label}</div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
container.innerHTML = `<div style="display:flex; flex-wrap:wrap; gap:8px; justify-content:center;">${segments}</div>`;
|
||
}
|
||
|
||
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 => `
|
||
<tr>
|
||
<td>${d.name}</td>
|
||
<td>${d.count}</td>
|
||
<td style="max-width:400px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${d.equipment}</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function renderRecentOperations() {
|
||
const tbody = document.querySelector('#recent-operations-table tbody');
|
||
const recent = db.movements.slice(-6).reverse();
|
||
|
||
tbody.innerHTML = recent.map(m => `
|
||
<tr>
|
||
<td>${formatDate(m.date)}</td>
|
||
<td>${m.sn}</td>
|
||
<td>${m.model}</td>
|
||
<td>${m.operation}</td>
|
||
<td>${m.to || '—'}</td>
|
||
</tr>
|
||
`).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 => `<option value="${m}">${m}</option>`).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 => `<option value="${e.id}">${e.name}</option>`).join('');
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateStatusFilters() {
|
||
const select = document.getElementById('warehouse-status-filter');
|
||
select.innerHTML = '<option value="">Все статусы</option>' +
|
||
db.statuses.map(s => `<option value="${s.id}">${s.label}</option>`).join('');
|
||
}
|
||
|
||
function updateTypeFilters() {
|
||
const select = document.getElementById('warehouse-type-filter');
|
||
select.innerHTML = '<option value="">Все типы</option>' +
|
||
db.models.map(m => `<option value="${m}">${m}</option>`).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 `
|
||
<tr>
|
||
<td>${formatDate(e.date)}</td>
|
||
<td>${e.sn}</td>
|
||
<td>${e.imei || '—'}</td>
|
||
<td>${e.model}</td>
|
||
<td><span class="badge ${getStatusBadgeClass(e.status)}">${e.status}</span></td>
|
||
<td>${e.account || '—'}</td>
|
||
<td>${e.order || '—'}</td>
|
||
<td>${emp ? emp.name : '—'}</td>
|
||
<td>
|
||
<button class="action-btn" onclick="showHistory('${e.sn}')" title="История">🔍</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).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 = '<option value="">Все сотрудники</option>' +
|
||
db.employees.filter(e => e.active).map(e => `<option value="${e.id}">${e.name}</option>`).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 => `
|
||
<tr>
|
||
<td>${formatDate(m.date)}</td>
|
||
<td>${m.sn}</td>
|
||
<td>${m.model}</td>
|
||
<td>${m.operation}</td>
|
||
<td>${m.from || '—'}</td>
|
||
<td>${m.to || '—'}</td>
|
||
<td>${m.account || '—'}</td>
|
||
<td>${m.order || '—'}</td>
|
||
<td>${m.comment || '—'}</td>
|
||
<td>
|
||
<button class="action-btn ${m.processed ? 'processed' : ''}" onclick="toggleProcessed(${m.id})" title="Отметить">
|
||
${m.processed ? '✓' : '○'}
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`).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 = `
|
||
<strong>${equipment.model}</strong><br>
|
||
SN: ${equipment.sn}<br>
|
||
IMEI: ${equipment.imei || '—'}<br>
|
||
Текущий статус: <span class="badge ${getStatusBadgeClass(equipment.status)}">${equipment.status}</span>
|
||
`;
|
||
|
||
document.getElementById('modal-timeline').innerHTML = movements.map(m => `
|
||
<div class="timeline-item">
|
||
<div class="timeline-date">${formatDate(m.date)}</div>
|
||
<div class="timeline-content">
|
||
<div class="timeline-operation">${m.operation}</div>
|
||
<div class="timeline-details">
|
||
${m.from ? 'Откуда: ' + m.from + '. ' : ''}
|
||
${m.to ? 'Кому: ' + m.to + '. ' : ''}
|
||
${m.comment || ''}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`).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 => `
|
||
<tr>
|
||
<td>${e.name}</td>
|
||
<td>${e.active ? '<span class="badge badge-warehouse">Активен</span>' : '<span class="badge badge-written-off">В архиве</span>'}</td>
|
||
<td>
|
||
<button class="action-btn" onclick="toggleEmployee(${e.id})">${e.active ? '📥 В архив' : '📤 Активен'}</button>
|
||
</td>
|
||
</tr>
|
||
`).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 => `
|
||
<tr>
|
||
<td>${m}</td>
|
||
<td>${modelCounts[m] || 0}</td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
// Статусы
|
||
document.getElementById('status-legend').innerHTML = db.statuses.map(s => `
|
||
<div class="status-item">
|
||
<div class="status-color" style="background:${s.color}"></div>
|
||
<span>${s.label}</span>
|
||
</div>
|
||
`).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();
|