670 lines
23 KiB
JavaScript
670 lines
23 KiB
JavaScript
// Mock data for initial development
|
||
let equipment = [
|
||
{
|
||
id: 1,
|
||
name: 'Огнетушитель ОП-5',
|
||
category: 'Огнетушитель',
|
||
inventoryNumber: 'ИН-001',
|
||
location: 'Кабинет 101',
|
||
responsible: 'Иванов И.И.',
|
||
lastCheck: '2025-09-15',
|
||
period: 12,
|
||
nextCheck: '2026-09-15',
|
||
note: 'Порошковый огнетушитель',
|
||
status: 'warning'
|
||
},
|
||
{
|
||
id: 2,
|
||
name: 'Аптечка медицинская',
|
||
category: 'Аптечка',
|
||
inventoryNumber: 'ИН-002',
|
||
location: 'Кабинет 205',
|
||
responsible: 'Петрова А.С.',
|
||
lastCheck: '2025-03-10',
|
||
period: 12,
|
||
nextCheck: '2026-03-10',
|
||
note: '',
|
||
status: 'expired'
|
||
},
|
||
{
|
||
id: 3,
|
||
name: 'Дрель аккумуляторная',
|
||
category: 'Электроинструмент',
|
||
inventoryNumber: 'ИН-003',
|
||
location: 'Склад №3',
|
||
responsible: 'Сидоров П.К.',
|
||
lastCheck: '2026-08-01',
|
||
period: 6,
|
||
nextCheck: '2027-02-01',
|
||
note: 'Bosch GSR 12V',
|
||
status: 'ok'
|
||
},
|
||
{
|
||
id: 4,
|
||
name: 'Каска защитная',
|
||
category: 'СИЗ',
|
||
inventoryNumber: 'ИН-004',
|
||
location: 'Участок №2',
|
||
responsible: 'Козлов М.М.',
|
||
lastCheck: '2026-06-15',
|
||
period: 24,
|
||
nextCheck: '2028-06-15',
|
||
note: '',
|
||
status: 'ok'
|
||
},
|
||
{
|
||
id: 5,
|
||
name: 'Мультиметр цифровой',
|
||
category: 'Измерительный прибор',
|
||
inventoryNumber: 'ИН-005',
|
||
location: 'Лаборатория',
|
||
responsible: 'Николаев В.В.',
|
||
lastCheck: '2026-07-20',
|
||
period: 12,
|
||
nextCheck: '2027-07-20',
|
||
note: 'Fluke 117',
|
||
status: 'ok'
|
||
},
|
||
{
|
||
id: 6,
|
||
name: 'Лестница алюминиевая',
|
||
category: 'Лестница',
|
||
inventoryNumber: 'ИН-006',
|
||
location: 'Хозблок',
|
||
responsible: 'Морозов А.А.',
|
||
lastCheck: '2025-12-01',
|
||
period: 12,
|
||
nextCheck: '2026-12-01',
|
||
note: '3 метра',
|
||
status: 'ok'
|
||
},
|
||
{
|
||
id: 7,
|
||
name: 'Огнетушитель ОУ-3',
|
||
category: 'Огнетушитель',
|
||
inventoryNumber: 'ИН-007',
|
||
location: 'Серверная',
|
||
responsible: 'Иванов И.И.',
|
||
lastCheck: '2026-08-25',
|
||
period: 60,
|
||
nextCheck: '2031-08-25',
|
||
note: 'Углекислотный',
|
||
status: 'ok'
|
||
},
|
||
{
|
||
id: 8,
|
||
name: 'Перчатки диэлектрические',
|
||
category: 'СИЗ',
|
||
inventoryNumber: 'ИН-008',
|
||
location: 'Электрощитовая',
|
||
responsible: 'Соколов Д.Д.',
|
||
lastCheck: '2026-03-01',
|
||
period: 6,
|
||
nextCheck: '2026-09-01',
|
||
note: '',
|
||
status: 'expired'
|
||
}
|
||
];
|
||
|
||
// Load from API on production
|
||
async function loadEquipment() {
|
||
try {
|
||
const response = await fetch('api/items');
|
||
if (response.ok) {
|
||
equipment = await response.json();
|
||
}
|
||
} catch (e) {
|
||
// Using mock data
|
||
}
|
||
updateAll();
|
||
}
|
||
|
||
function updateAll() {
|
||
calculateStatuses();
|
||
updateKPIs();
|
||
renderDashboard();
|
||
renderTable();
|
||
renderExpiring();
|
||
renderExpired();
|
||
updateNotifications();
|
||
updateCounts();
|
||
}
|
||
|
||
function calculateStatuses() {
|
||
const today = new Date();
|
||
today.setHours(0, 0, 0, 0);
|
||
|
||
equipment.forEach(item => {
|
||
const nextCheck = new Date(item.nextCheck);
|
||
const diffTime = nextCheck - today;
|
||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||
|
||
if (diffDays < 0) {
|
||
item.status = 'expired';
|
||
} else if (diffDays <= 30) {
|
||
item.status = 'warning';
|
||
} else {
|
||
item.status = 'ok';
|
||
}
|
||
item.daysLeft = diffDays;
|
||
});
|
||
}
|
||
|
||
function updateKPIs() {
|
||
const today = new Date();
|
||
const thisMonth = today.getMonth();
|
||
const thisYear = today.getFullYear();
|
||
|
||
const total = equipment.length;
|
||
const expiring = equipment.filter(e => e.status === 'warning').length;
|
||
const expired = equipment.filter(e => e.status === 'expired').length;
|
||
const active = equipment.filter(e => e.status === 'ok').length;
|
||
|
||
const thisMonthCount = equipment.filter(e => {
|
||
const next = new Date(e.nextCheck);
|
||
return next.getMonth() === thisMonth && next.getFullYear() === thisYear;
|
||
}).length;
|
||
|
||
document.getElementById('kpi-total').textContent = total;
|
||
document.getElementById('kpi-expiring').textContent = expiring;
|
||
document.getElementById('kpi-expired').textContent = expired;
|
||
document.getElementById('kpi-active').textContent = active;
|
||
document.getElementById('kpi-this-month').textContent = thisMonthCount;
|
||
}
|
||
|
||
function renderDashboard() {
|
||
const tbody = document.getElementById('dashboard-table-body');
|
||
const expiring = equipment
|
||
.filter(e => e.status === 'warning' || e.status === 'expired')
|
||
.sort((a, b) => a.daysLeft - b.daysLeft)
|
||
.slice(0, 10);
|
||
|
||
if (expiring.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: var(--kt-ai-space-6); color: var(--kt-ai-fg-muted);">Нет предстоящих проверок</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = expiring.map(item => {
|
||
const statusClass = item.status === 'expired' ? 'status-danger' : 'status-warning';
|
||
const statusText = item.status === 'expired' ? 'ПРОСРОЧЕНО' : 'Скоро проверка';
|
||
const rowClass = item.status === 'expired' ? 'row-expired' : 'row-expiring';
|
||
|
||
return `
|
||
<tr class="${rowClass}">
|
||
<td>${escapeHtml(item.name)}</td>
|
||
<td>${escapeHtml(item.category)}</td>
|
||
<td>${escapeHtml(item.inventoryNumber)}</td>
|
||
<td>${formatDate(item.nextCheck)}</td>
|
||
<td style="color: ${item.daysLeft < 0 ? 'var(--kt-ai-danger)' : 'var(--kt-ai-warning)'}; font-weight: 600;">
|
||
${item.daysLeft < 0 ? Math.abs(item.daysLeft) + ' дней назад' : item.daysLeft + ' дн.'}
|
||
</td>
|
||
<td><span class="kt-ai-chip ${statusClass}">${statusText}</span></td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderTable() {
|
||
const tbody = document.getElementById('all-table-body');
|
||
const search = document.getElementById('global-search').value.toLowerCase();
|
||
const categoryFilter = document.getElementById('filter-category').value;
|
||
const statusFilter = document.getElementById('filter-status').value;
|
||
|
||
let filtered = equipment.filter(item => {
|
||
const matchesSearch = !search ||
|
||
item.name.toLowerCase().includes(search) ||
|
||
item.inventoryNumber.toLowerCase().includes(search) ||
|
||
item.responsible.toLowerCase().includes(search) ||
|
||
item.location.toLowerCase().includes(search);
|
||
|
||
const matchesCategory = !categoryFilter || item.category === categoryFilter;
|
||
const matchesStatus = !statusFilter || item.status === statusFilter;
|
||
|
||
return matchesSearch && matchesCategory && matchesStatus;
|
||
});
|
||
|
||
if (filtered.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="11" style="text-align: center; padding: var(--kt-ai-space-6); color: var(--kt-ai-fg-muted);">Оборудование не найдено</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = filtered.map((item, index) => {
|
||
const statusClass = item.status === 'expired' ? 'status-danger' : item.status === 'warning' ? 'status-warning' : '';
|
||
const statusText = item.status === 'expired' ? 'ПРОСРОЧЕНО' : item.status === 'warning' ? 'Скоро проверка' : 'Действует';
|
||
const rowClass = item.status === 'expired' ? 'row-expired' : item.status === 'warning' ? 'row-expiring' : '';
|
||
|
||
return `
|
||
<tr class="${rowClass}">
|
||
<td>${index + 1}</td>
|
||
<td><strong>${escapeHtml(item.name)}</strong></td>
|
||
<td>${escapeHtml(item.category)}</td>
|
||
<td>${escapeHtml(item.inventoryNumber)}</td>
|
||
<td>${escapeHtml(item.location)}</td>
|
||
<td>${escapeHtml(item.responsible)}</td>
|
||
<td>${formatDate(item.lastCheck)}</td>
|
||
<td>${item.period} мес.</td>
|
||
<td>${formatDate(item.nextCheck)}</td>
|
||
<td><span class="kt-ai-chip ${statusClass}">${statusText}</span></td>
|
||
<td>
|
||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="openDrawer(${item.id})">
|
||
<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#more"></use></svg>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderExpiring() {
|
||
const tbody = document.getElementById('expiring-table-body');
|
||
const expiring = equipment.filter(e => e.status === 'warning').sort((a, b) => a.daysLeft - b.daysLeft);
|
||
|
||
if (expiring.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="9" style="text-align: center; padding: var(--kt-ai-space-6); color: var(--kt-ai-fg-muted);">Нет оборудования на проверке</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = expiring.map((item, index) => `
|
||
<tr class="row-expiring">
|
||
<td>${index + 1}</td>
|
||
<td><strong>${escapeHtml(item.name)}</strong></td>
|
||
<td>${escapeHtml(item.category)}</td>
|
||
<td>${escapeHtml(item.inventoryNumber)}</td>
|
||
<td>${escapeHtml(item.location)}</td>
|
||
<td>${escapeHtml(item.responsible)}</td>
|
||
<td>${formatDate(item.nextCheck)}</td>
|
||
<td style="color: var(--kt-ai-warning); font-weight: 600;">${item.daysLeft} дн.</td>
|
||
<td>
|
||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="openDrawer(${item.id})">
|
||
<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#more"></use></svg>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function renderExpired() {
|
||
const tbody = document.getElementById('expired-table-body');
|
||
const expired = equipment.filter(e => e.status === 'expired').sort((a, b) => a.daysLeft - b.daysLeft);
|
||
|
||
if (expired.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="9" style="text-align: center; padding: var(--kt-ai-space-6); color: var(--kt-ai-fg-muted);">Нет просроченного оборудования</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = expired.map((item, index) => `
|
||
<tr class="row-expired">
|
||
<td>${index + 1}</td>
|
||
<td><strong>${escapeHtml(item.name)}</strong></td>
|
||
<td>${escapeHtml(item.category)}</td>
|
||
<td>${escapeHtml(item.inventoryNumber)}</td>
|
||
<td>${escapeHtml(item.location)}</td>
|
||
<td>${escapeHtml(item.responsible)}</td>
|
||
<td>${formatDate(item.nextCheck)}</td>
|
||
<td style="color: var(--kt-ai-danger); font-weight: 600;">${Math.abs(item.daysLeft)} дн.</td>
|
||
<td>
|
||
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="openDrawer(${item.id})">
|
||
<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#more"></use></svg>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function updateNotifications() {
|
||
const container = document.getElementById('notifications-container');
|
||
const expired = equipment.filter(e => e.status === 'expired');
|
||
const expiring = equipment.filter(e => e.status === 'warning' && e.daysLeft <= 7);
|
||
|
||
let html = '';
|
||
|
||
if (expired.length > 0) {
|
||
html += `<div class="notification notification-danger">
|
||
<strong>Проверка просрочена!</strong> ${expired.length} ед. оборудования с истёкшим сроком проверки.
|
||
</div>`;
|
||
}
|
||
|
||
if (expiring.length > 0) {
|
||
html += `<div class="notification notification-warning">
|
||
<strong>Внимание! До проверки осталось 7 дней или меньше!</strong> ${expiring.length} ед. оборудования.
|
||
</div>`;
|
||
}
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function updateCounts() {
|
||
const expiringCount = equipment.filter(e => e.status === 'warning').length;
|
||
const expiredCount = equipment.filter(e => e.status === 'expired').length;
|
||
|
||
const expiringBadge = document.getElementById('expiring-count');
|
||
const expiredBadge = document.getElementById('expired-count');
|
||
|
||
if (expiringCount > 0) {
|
||
expiringBadge.textContent = expiringCount;
|
||
expiringBadge.style.display = 'inline-block';
|
||
} else {
|
||
expiringBadge.style.display = 'none';
|
||
}
|
||
|
||
if (expiredCount > 0) {
|
||
expiredBadge.textContent = expiredCount;
|
||
expiredBadge.classList.add('status-danger');
|
||
expiredBadge.style.display = 'inline-block';
|
||
} else {
|
||
expiredBadge.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function addEquipment(event) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
|
||
const lastCheck = formData.get('lastCheck');
|
||
const period = parseInt(formData.get('period'));
|
||
const nextCheck = calculateNextDateValue(lastCheck, period);
|
||
|
||
const newItem = {
|
||
id: Date.now(),
|
||
name: formData.get('name'),
|
||
category: formData.get('category'),
|
||
inventoryNumber: formData.get('inventoryNumber'),
|
||
location: formData.get('location'),
|
||
responsible: formData.get('responsible'),
|
||
lastCheck: lastCheck,
|
||
period: period,
|
||
nextCheck: nextCheck,
|
||
note: formData.get('note') || '',
|
||
status: 'ok'
|
||
};
|
||
|
||
equipment.push(newItem);
|
||
saveEquipment();
|
||
resetForm();
|
||
updateAll();
|
||
switchTab('all');
|
||
|
||
alert('Оборудование успешно добавлено!');
|
||
}
|
||
|
||
function calculateNextDateValue(lastCheck, periodMonths) {
|
||
const date = new Date(lastCheck);
|
||
date.setMonth(date.getMonth() + periodMonths);
|
||
return date.toISOString().split('T')[0];
|
||
}
|
||
|
||
function calculateNextDate() {
|
||
const form = document.getElementById('add-form');
|
||
const lastCheck = form.lastCheck.value;
|
||
const period = parseInt(form.period.value);
|
||
|
||
if (lastCheck && period) {
|
||
const nextCheck = calculateNextDateValue(lastCheck, period);
|
||
document.getElementById('next-check').value = nextCheck;
|
||
}
|
||
}
|
||
|
||
function resetForm() {
|
||
document.getElementById('add-form').reset();
|
||
document.getElementById('next-check').value = '';
|
||
}
|
||
|
||
function openDrawer(id) {
|
||
const item = equipment.find(e => e.id === id);
|
||
if (!item) return;
|
||
|
||
const drawer = document.getElementById('equipment-drawer');
|
||
const title = document.getElementById('drawer-title');
|
||
const content = document.getElementById('drawer-content');
|
||
const actions = document.getElementById('drawer-actions');
|
||
|
||
title.textContent = item.name;
|
||
|
||
const statusClass = item.status === 'expired' ? 'status-danger' : item.status === 'warning' ? 'status-warning' : '';
|
||
const statusText = item.status === 'expired' ? 'ПРОСРОЧЕНО' : item.status === 'warning' ? 'Скоро проверка' : 'Действует';
|
||
|
||
content.innerHTML = `
|
||
<div style="display: grid; gap: var(--kt-ai-space-4);">
|
||
<div style="display: flex; gap: var(--kt-ai-space-2); align-items: center;">
|
||
<span class="kt-ai-chip ${statusClass}" style="font-size: var(--kt-ai-text-base); padding: var(--kt-ai-space-2) var(--kt-ai-space-3);">${statusText}</span>
|
||
${item.daysLeft !== undefined ? `<span style="color: var(--kt-ai-fg-muted);">До проверки: ${item.daysLeft < 0 ? Math.abs(item.daysLeft) + ' дней назад' : item.daysLeft + ' дн.'}</span>` : ''}
|
||
</div>
|
||
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--kt-ai-space-4);">
|
||
<div>
|
||
<div style="color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm);">Категория</div>
|
||
<div style="font-weight: 500;">${escapeHtml(item.category)}</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm);">Инвентарный номер</div>
|
||
<div style="font-weight: 500;">${escapeHtml(item.inventoryNumber)}</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm);">Место нахождения</div>
|
||
<div style="font-weight: 500;">${escapeHtml(item.location)}</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm);">Ответственный</div>
|
||
<div style="font-weight: 500;">${escapeHtml(item.responsible)}</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm);">Дата последней проверки</div>
|
||
<div style="font-weight: 500;">${formatDate(item.lastCheck)}</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm);">Периодичность</div>
|
||
<div style="font-weight: 500;">${item.period} мес.</div>
|
||
</div>
|
||
<div>
|
||
<div style="color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm);">Следующая проверка</div>
|
||
<div style="font-weight: 500; color: ${item.status === 'expired' ? 'var(--kt-ai-danger)' : item.status === 'warning' ? 'var(--kt-ai-warning)' : 'var(--kt-ai-fg)'};">
|
||
${formatDate(item.nextCheck)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
${item.note ? `
|
||
<div style="border-top: 1px solid var(--kt-ai-border); padding-top: var(--kt-ai-space-4);">
|
||
<div style="color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); margin-bottom: var(--kt-ai-space-2);">Примечание</div>
|
||
<div>${escapeHtml(item.note)}</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
<div style="border-top: 1px solid var(--kt-ai-border); padding-top: var(--kt-ai-space-4);">
|
||
<h3 style="margin-bottom: var(--kt-ai-space-3);">История проверок</h3>
|
||
<div style="color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm);">
|
||
История проверок будет доступна после обновления системы
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
actions.innerHTML = `
|
||
<button class="kt-ai-btn" data-variant="primary" onclick="editEquipment(${item.id})">
|
||
<svg class="kt-icon kt-icon-sm" aria-hidden="true"><use href="#pencil"></use></svg>
|
||
Редактировать
|
||
</button>
|
||
<button class="kt-ai-btn" data-variant="ghost" onclick="closeDrawer()">Закрыть</button>
|
||
`;
|
||
|
||
drawer.hidden = false;
|
||
}
|
||
|
||
function closeDrawer() {
|
||
document.getElementById('equipment-drawer').hidden = true;
|
||
}
|
||
|
||
function editEquipment(id) {
|
||
const item = equipment.find(e => e.id === id);
|
||
if (!item) return;
|
||
|
||
closeDrawer();
|
||
switchTab('add');
|
||
|
||
const form = document.getElementById('add-form');
|
||
form.name.value = item.name;
|
||
form.category.value = item.category;
|
||
form.inventoryNumber.value = item.inventoryNumber;
|
||
form.location.value = item.location;
|
||
form.responsible.value = item.responsible;
|
||
form.lastCheck.value = item.lastCheck;
|
||
form.period.value = item.period;
|
||
document.getElementById('next-check').value = item.nextCheck;
|
||
form.note.value = item.note || '';
|
||
|
||
// Remove old item
|
||
equipment = equipment.filter(e => e.id !== id);
|
||
}
|
||
|
||
function exportToExcel() {
|
||
const headers = ['№', 'Наименование', 'Категория', 'Инвентарный номер', 'Место нахождения', 'Ответственный', 'Дата последней проверки', 'Периодичность (мес.)', 'Следующая проверка', 'Статус'];
|
||
const rows = equipment.map((item, index) => [
|
||
index + 1,
|
||
item.name,
|
||
item.category,
|
||
item.inventoryNumber,
|
||
item.location,
|
||
item.responsible,
|
||
item.lastCheck,
|
||
item.period,
|
||
item.nextCheck,
|
||
item.status === 'expired' ? 'Просрочено' : item.status === 'warning' ? 'Скоро проверка' : 'Действует'
|
||
]);
|
||
|
||
const csv = '\uFEFF' + [headers, ...rows].map(row =>
|
||
row.map(cell => '"' + String(cell).replace(/"/g, '""') + '"').join(';')
|
||
).join('\n');
|
||
|
||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||
const link = document.createElement('a');
|
||
link.href = URL.createObjectURL(blob);
|
||
link.download = 'oborudovanie_' + new Date().toISOString().split('T')[0] + '.csv';
|
||
link.click();
|
||
}
|
||
|
||
function generateReport(type) {
|
||
let filtered = [];
|
||
let filename = '';
|
||
|
||
switch(type) {
|
||
case 'all':
|
||
filtered = equipment;
|
||
filename = 'otchet_vse_oborudovanie';
|
||
break;
|
||
case 'expiring':
|
||
filtered = equipment.filter(e => e.status === 'warning');
|
||
filename = 'otchet_na_proverku';
|
||
break;
|
||
case 'expired':
|
||
filtered = equipment.filter(e => e.status === 'expired');
|
||
filename = 'otchet_prosrocheno';
|
||
break;
|
||
case 'active':
|
||
filtered = equipment.filter(e => e.status === 'ok');
|
||
filename = 'otchet_proverno';
|
||
break;
|
||
}
|
||
|
||
const headers = ['№', 'Наименование', 'Категория', 'Инвентарный номер', 'Место нахождения', 'Ответственный', 'Дата последней проверки', 'Периодичность (мес.)', 'Следующая проверка', 'Статус', 'Примечание'];
|
||
const rows = filtered.map((item, index) => [
|
||
index + 1,
|
||
item.name,
|
||
item.category,
|
||
item.inventoryNumber,
|
||
item.location,
|
||
item.responsible,
|
||
item.lastCheck,
|
||
item.period,
|
||
item.nextCheck,
|
||
item.status === 'expired' ? 'Просрочено' : item.status === 'warning' ? 'Скоро проверка' : 'Действует',
|
||
item.note || ''
|
||
]);
|
||
|
||
const csv = '\uFEFF' + [headers, ...rows].map(row =>
|
||
row.map(cell => '"' + String(cell).replace(/"/g, '""') + '"').join(';')
|
||
).join('\n');
|
||
|
||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||
const link = document.createElement('a');
|
||
link.href = URL.createObjectURL(blob);
|
||
link.download = filename + '_' + new Date().toISOString().split('T')[0] + '.csv';
|
||
link.click();
|
||
}
|
||
|
||
function switchTab(tabId) {
|
||
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
|
||
document.querySelectorAll('.kt-ai-nav-item').forEach(nav => nav.classList.remove('is-active'));
|
||
|
||
document.getElementById('tab-' + tabId).classList.add('active');
|
||
document.querySelector('[data-tab="' + tabId + '"]').classList.add('is-active');
|
||
|
||
const titles = {
|
||
dashboard: 'Dashboard',
|
||
all: 'Всё оборудование',
|
||
expiring: 'Проверка в ближайшие 30 дней',
|
||
expired: 'Просрочено',
|
||
add: 'Добавить оборудование',
|
||
reports: 'Отчёты'
|
||
};
|
||
|
||
document.getElementById('page-title').textContent = titles[tabId] || '';
|
||
|
||
if (tabId === 'all') renderTable();
|
||
if (tabId === 'expiring') renderExpiring();
|
||
if (tabId === 'expired') renderExpired();
|
||
}
|
||
|
||
function toggleTheme() {
|
||
const html = document.documentElement;
|
||
const current = html.getAttribute('data-theme');
|
||
const next = current === 'dark' ? 'light' : 'dark';
|
||
html.setAttribute('data-theme', next);
|
||
}
|
||
|
||
function formatDate(dateStr) {
|
||
if (!dateStr) return '';
|
||
const date = new Date(dateStr);
|
||
return date.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
if (!text) return '';
|
||
return String(text)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
function saveEquipment() {
|
||
// In production, this will POST to API
|
||
try {
|
||
fetch('api/items', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(equipment)
|
||
});
|
||
} catch (e) {
|
||
// Silent fail for mock
|
||
}
|
||
}
|
||
|
||
// Navigation
|
||
document.querySelectorAll('[data-tab]').forEach(link => {
|
||
link.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
const tabId = link.getAttribute('data-tab');
|
||
switchTab(tabId);
|
||
});
|
||
});
|
||
|
||
// Global search
|
||
document.getElementById('global-search').addEventListener('input', () => {
|
||
const activeTab = document.querySelector('.tab-content.active').id;
|
||
if (activeTab === 'tab-all') renderTable();
|
||
});
|
||
|
||
// Initialize
|
||
loadEquipment();
|