282 lines
11 KiB
JavaScript
282 lines
11 KiB
JavaScript
let items = [];
|
|
let filteredItems = [];
|
|
let currentFilter = { status: '', search: '', requireCheck: false };
|
|
let sortField = 'name';
|
|
let sortAsc = true;
|
|
let currentUser = null;
|
|
let token = null;
|
|
|
|
const $ = (id) => document.getElementById(id);
|
|
|
|
// Токен и пользователь из localStorage
|
|
token = localStorage.getItem('token');
|
|
currentUser = JSON.parse(localStorage.getItem('user') || 'null');
|
|
|
|
async function checkAuth() {
|
|
if (!token || !currentUser) {
|
|
window.location.href = 'login.html';
|
|
return;
|
|
}
|
|
$('userInfo').textContent = currentUser.login + (currentUser.role === 'admin' ? ' (Админ)' : '');
|
|
$('usersBtn').style.display = currentUser.role === 'admin' ? 'inline-block' : 'none';
|
|
}
|
|
|
|
async function apiRequest(url, method = 'GET') {
|
|
const res = await fetch(url);
|
|
return res.json();
|
|
}
|
|
|
|
async function loadItems() {
|
|
const data = await apiRequest('/api/items');
|
|
if (data) {
|
|
items = data;
|
|
applyFilters();
|
|
}
|
|
}
|
|
|
|
async function loadStats() {
|
|
const stats = await apiRequest('/api/stats');
|
|
if (!stats) return;
|
|
$('statTotal').textContent = stats.total;
|
|
$('statExploitation').textContent = stats.byStatus['в эксплуатации'] || 0;
|
|
$('statWarehouse').textContent = stats.byStatus['на складе'] || 0;
|
|
$('statRepair').textContent = stats.byStatus['в ремонте'] || 0;
|
|
$('statWrittenOff').textContent = stats.byStatus['списано'] || 0;
|
|
$('statRequireCheck').textContent = stats.requireCheck || 0;
|
|
$('statOverdue').textContent = stats.overdue || 0;
|
|
}
|
|
|
|
function applyFilters() {
|
|
filteredItems = items.filter(item => {
|
|
if (currentFilter.status === 'require') {
|
|
const in30Days = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
return item.nextCheckDate && item.nextCheckDate <= in30Days;
|
|
}
|
|
if (currentFilter.status && item.status !== currentFilter.status) return false;
|
|
if (currentFilter.search) {
|
|
const s = currentFilter.search.toLowerCase();
|
|
const text = (item.name + ' ' + (item.serialNumber||'') + ' ' + item.inventoryNumber + ' ' + item.responsible + ' ' + item.location).toLowerCase();
|
|
if (!text.includes(s)) return false;
|
|
}
|
|
return true;
|
|
});
|
|
sortItems();
|
|
renderTable();
|
|
}
|
|
|
|
function sortItems() {
|
|
filteredItems.sort((a, b) => {
|
|
const aVal = a[sortField] || '';
|
|
const bVal = b[sortField] || '';
|
|
const cmp = aVal.localeCompare(bVal, 'ru');
|
|
return sortAsc ? cmp : -cmp;
|
|
});
|
|
}
|
|
|
|
function renderTable() {
|
|
const tbody = $('tableBody');
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
const in30Days = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
const canEdit = currentUser && currentUser.role === 'admin';
|
|
|
|
tbody.innerHTML = filteredItems.map(item => {
|
|
let dateClass = 'check-date-ok';
|
|
if (item.nextCheckDate && item.nextCheckDate < today) dateClass = 'check-date-overdue';
|
|
else if (item.nextCheckDate && item.nextCheckDate <= in30Days) dateClass = 'check-date-warning';
|
|
|
|
return `<tr>
|
|
<td>${escapeHtml(item.name)}</td>
|
|
<td>${escapeHtml(item.type)}</td>
|
|
<td>${escapeHtml(item.inventoryNumber)}</td>
|
|
<td>${escapeHtml(item.responsible)}</td>
|
|
<td>${escapeHtml(item.location)}</td>
|
|
<td><span class="status-badge status-${item.status}">${escapeHtml(item.status)}</span></td>
|
|
<td class="${dateClass}">${formatDate(item.nextCheckDate)}</td>
|
|
<td>
|
|
${canEdit ? `<button class="action-btn edit" onclick="editItem(${item.id})">✏️</button>` : ''}
|
|
${canEdit ? `<button class="action-btn delete" onclick="deleteItem(${item.id})">🗑️</button>` : ''}
|
|
${!canEdit ? '<span style="font-size:12px;color:var(--text-secondary);">Только просмотр</span>' : ''}
|
|
</td>
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text || '';
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '';
|
|
const d = new Date(dateStr + 'T00:00:00');
|
|
return d.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
|
}
|
|
|
|
function openModal(item = null) {
|
|
$('itemModal').classList.add('on');
|
|
$('modalTitle').textContent = item ? 'Редактировать запись' : 'Добавить запись';
|
|
if (item) {
|
|
$('itemId').value = item.id;
|
|
$('name').value = item.name;
|
|
$('type').value = item.type;
|
|
$('serialNumber').value = item.serialNumber || '';
|
|
$('inventoryNumber').value = item.inventoryNumber;
|
|
$('location').value = item.location;
|
|
$('responsible').value = item.responsible;
|
|
$('status').value = item.status;
|
|
$('checkIntervalMonths').value = item.checkIntervalMonths || 12;
|
|
$('lastCheckDate').value = item.lastCheckDate || '';
|
|
$('nextCheckDate').value = item.nextCheckDate;
|
|
$('note').value = item.note || '';
|
|
} else {
|
|
$('itemForm').reset();
|
|
$('itemId').value = '';
|
|
$('checkIntervalMonths').value = 12;
|
|
const next = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
$('nextCheckDate').value = next;
|
|
}
|
|
}
|
|
|
|
function closeModal() { $('itemModal').classList.remove('on'); }
|
|
function openLogModal() { $('logModal').classList.add('on'); loadLog(); }
|
|
function closeLogModal() { $('logModal').classList.remove('on'); }
|
|
function openUsersModal() { $('usersModal').classList.add('on'); loadUsers(); }
|
|
function closeUsersModal() { $('usersModal').classList.remove('on'); }
|
|
|
|
async function loadLog() {
|
|
const log = await apiRequest('/api/log');
|
|
if (!log) return;
|
|
$('logBody').innerHTML = log.map(entry => `
|
|
<tr>
|
|
<td>${new Date(entry.timestamp).toLocaleString('ru-RU')}</td>
|
|
<td>${entry.action}</td>
|
|
<td>${escapeHtml(entry.itemName)}</td>
|
|
<td>${escapeHtml(entry.user)}</td>
|
|
<td>${Object.entries(entry.changes||{}).map(([k,v])=>`${k}: ${v}`).join('; ')||'—'}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
async function loadUsers() {
|
|
const users = await apiRequest('/api/users');
|
|
if (!users) return;
|
|
$('usersBody').innerHTML = users.map(u => `
|
|
<tr>
|
|
<td>${escapeHtml(u.login)}</td>
|
|
<td>${u.role==='admin'?'Админ':'Пользователь'}</td>
|
|
<td>${new Date(u.createdAt).toLocaleDateString('ru-RU')}</td>
|
|
<td>${u.login!==currentUser.login?`<button class="action-btn delete" onclick="deleteUser('${u.login}')">🗑️</button>`:''}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
async function saveItem(e) {
|
|
e.preventDefault();
|
|
const id = $('itemId').value;
|
|
const item = {
|
|
name: $('name').value, type: $('type').value,
|
|
serialNumber: $('serialNumber').value, inventoryNumber: $('inventoryNumber').value,
|
|
location: $('location').value, responsible: $('responsible').value,
|
|
status: $('status').value, checkIntervalMonths: parseInt($('checkIntervalMonths').value) || 12,
|
|
lastCheckDate: $('lastCheckDate').value, nextCheckDate: $('nextCheckDate').value,
|
|
note: $('note').value
|
|
};
|
|
const res = await fetch('/api/items', {
|
|
method: id ? 'PUT' : 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id: id ? parseInt(id) : null, ...item })
|
|
});
|
|
const data = await res.json();
|
|
if (data.ok) { closeModal(); await loadItems(); await loadStats(); }
|
|
else alert('Ошибка: ' + (data.error || 'сохранения'));
|
|
}
|
|
|
|
async function deleteItem(id) {
|
|
if (!confirm('Удалить запись?')) return;
|
|
const res = await fetch('/api/items/' + id, { method: 'DELETE' });
|
|
const data = await res.json();
|
|
if (data.ok) { await loadItems(); await loadStats(); }
|
|
else alert('Ошибка: ' + (data.error || 'удаления'));
|
|
}
|
|
|
|
async function createUser(e) {
|
|
e.preventDefault();
|
|
const login = $('newLogin').value, password = $('newPassword').value, role = $('newRole').value;
|
|
const res = await fetch('/api/users', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ login, password, role })
|
|
});
|
|
const data = await res.json();
|
|
if (data.ok) { $('userForm').reset(); await loadUsers(); }
|
|
else alert('Ошибка: ' + data.error);
|
|
}
|
|
|
|
async function deleteUser(login) {
|
|
if (!confirm(`Удалить ${login}?`)) return;
|
|
const res = await fetch('/api/users/' + login, { method: 'DELETE' });
|
|
const data = await res.json();
|
|
if (data.ok) await loadUsers();
|
|
else alert('Ошибка: ' + data.error);
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.removeItem('token');
|
|
localStorage.removeItem('user');
|
|
window.location.href = 'login.html';
|
|
}
|
|
|
|
function exportCsv() {
|
|
const head = ['Наименование','Тип','Серийный номер','Инвентарный номер','Местоположение','Ответственный','Статус','Дата последней проверки','Дата следующей проверки','Интервал (мес)','Комментарий'];
|
|
const body = filteredItems.map(i => [i.name,i.type,i.serialNumber||'',i.inventoryNumber,i.location,i.responsible,i.status,i.lastCheckDate||'',i.nextCheckDate,i.checkIntervalMonths||'',i.note||'']
|
|
.map(v=>'"'+String(v??'').replace(/"/g,'""')+'"').join(';'));
|
|
const csv = '\uFEFF'+[head.join(';'),...body].join('\n');
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(new Blob([csv],{type:'text/csv'}));
|
|
a.download = 'vygruzka.csv'; a.click();
|
|
}
|
|
|
|
$('addBtn').addEventListener('click', ()=>openModal());
|
|
$('exportBtn').addEventListener('click', exportCsv);
|
|
$('logBtn').addEventListener('click', openLogModal);
|
|
$('usersBtn').addEventListener('click', openUsersModal);
|
|
$('logoutBtn').addEventListener('click', logout);
|
|
$('closeModal').addEventListener('click', closeModal);
|
|
$('cancelBtn').addEventListener('click', closeModal);
|
|
$('closeLogModal').addEventListener('click', closeLogModal);
|
|
$('closeUsersModal').addEventListener('click', closeUsersModal);
|
|
$('itemForm').addEventListener('submit', saveItem);
|
|
$('userForm').addEventListener('submit', createUser);
|
|
$('searchInput').addEventListener('input', e=>{currentFilter.search=e.target.value;applyFilters();});
|
|
document.querySelectorAll('.chip').forEach(chip=>{
|
|
chip.addEventListener('click', ()=>{
|
|
document.querySelectorAll('.chip').forEach(c=>c.classList.remove('active'));
|
|
chip.classList.add('active');
|
|
currentFilter.status=chip.dataset.status;
|
|
currentFilter.requireCheck=chip.dataset.status==='require';
|
|
applyFilters();
|
|
});
|
|
});
|
|
document.querySelectorAll('th[data-sort]').forEach(th=>{
|
|
th.addEventListener('click', ()=>{
|
|
const f=th.dataset.sort;
|
|
sortAsc=sortField===f?!sortAsc:true; sortField=f; sortItems(); renderTable();
|
|
});
|
|
});
|
|
$('lastCheckDate').addEventListener('change', ()=>{
|
|
const d=$('lastCheckDate').value,m=parseInt($('checkIntervalMonths').value)||12;
|
|
if(d){const dt=new Date(d+'T00:00:00');dt.setMonth(dt.getMonth()+m);$('nextCheckDate').value=dt.toISOString().slice(0,10);}
|
|
});
|
|
$('checkIntervalMonths').addEventListener('change', ()=>{
|
|
const d=$('lastCheckDate').value,m=parseInt($('checkIntervalMonths').value)||12;
|
|
if(d){const dt=new Date(d+'T00:00:00');dt.setMonth(dt.getMonth()+m);$('nextCheckDate').value=dt.toISOString().slice(0,10);}
|
|
});
|
|
window.editItem=id=>{const item=items.find(i=>i.id===id);if(item)openModal(item);};
|
|
window.deleteItem=deleteItem;
|
|
window.deleteUser=deleteUser;
|
|
|
|
checkAuth();
|
|
loadItems();
|
|
loadStats();
|