// Equipment data
let equipment = [];
// Load from API
async function loadEquipment() {
try {
const response = await fetch('api/items');
if (response.ok) {
equipment = await response.json();
if (equipment.length === 0) {
// Initialize with demo data if empty
equipment = getDemoData();
saveEquipment();
}
} else {
equipment = getDemoData();
}
} catch (e) {
equipment = getDemoData();
}
updateAll();
}
function getDemoData() {
return [
{
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'
}
];
}
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 = '
| Нет предстоящих проверок |
';
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 `
| ${escapeHtml(item.name)} |
${escapeHtml(item.category)} |
${escapeHtml(item.inventoryNumber)} |
${formatDate(item.nextCheck)} |
${item.daysLeft < 0 ? Math.abs(item.daysLeft) + ' дней назад' : item.daysLeft + ' дн.'}
|
${statusText} |
`;
}).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 = '| Оборудование не найдено |
';
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 `
| ${index + 1} |
${escapeHtml(item.name)} |
${escapeHtml(item.category)} |
${escapeHtml(item.inventoryNumber)} |
${escapeHtml(item.location)} |
${escapeHtml(item.responsible)} |
${formatDate(item.lastCheck)} |
${item.period} мес. |
${formatDate(item.nextCheck)} |
${statusText} |
|
`;
}).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 = '| Нет оборудования на проверке |
';
return;
}
tbody.innerHTML = expiring.map((item, index) => `
| ${index + 1} |
${escapeHtml(item.name)} |
${escapeHtml(item.category)} |
${escapeHtml(item.inventoryNumber)} |
${escapeHtml(item.location)} |
${escapeHtml(item.responsible)} |
${formatDate(item.nextCheck)} |
${item.daysLeft} дн. |
|
`).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 = '| Нет просроченного оборудования |
';
return;
}
tbody.innerHTML = expired.map((item, index) => `
| ${index + 1} |
${escapeHtml(item.name)} |
${escapeHtml(item.category)} |
${escapeHtml(item.inventoryNumber)} |
${escapeHtml(item.location)} |
${escapeHtml(item.responsible)} |
${formatDate(item.nextCheck)} |
${Math.abs(item.daysLeft)} дн. |
|
`).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 += `
Проверка просрочена! ${expired.length} ед. оборудования с истёкшим сроком проверки.
`;
}
if (expiring.length > 0) {
html += `
Внимание! До проверки осталось 7 дней или меньше! ${expiring.length} ед. оборудования.
`;
}
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);
if (editingId) {
// Edit existing item
const index = equipment.findIndex(e => e.id === editingId);
if (index !== -1) {
equipment[index] = {
...equipment[index],
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') || ''
};
saveEquipment();
updateAll();
resetForm();
switchTab('all');
}
editingId = null;
} else {
// Add new item
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');
}
}
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 = '';
editingId = null;
// Reset button text
const submitBtn = document.getElementById('add-form').querySelector('button[type="submit"]');
submitBtn.innerHTML = 'Сохранить';
}
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 = `
${statusText}
${item.daysLeft !== undefined ? `До проверки: ${item.daysLeft < 0 ? Math.abs(item.daysLeft) + ' дней назад' : item.daysLeft + ' дн.'}` : ''}
Категория
${escapeHtml(item.category)}
Инвентарный номер
${escapeHtml(item.inventoryNumber)}
Место нахождения
${escapeHtml(item.location)}
Ответственный
${escapeHtml(item.responsible)}
Дата последней проверки
${formatDate(item.lastCheck)}
Периодичность
${item.period} мес.
Следующая проверка
${formatDate(item.nextCheck)}
${item.note ? `
Примечание
${escapeHtml(item.note)}
` : ''}
История проверок
История проверок будет доступна после обновления системы
`;
actions.innerHTML = `
`;
drawer.hidden = false;
}
function closeDrawer() {
document.getElementById('equipment-drawer').hidden = true;
}
let editingId = null;
function editEquipment(id) {
const item = equipment.find(e => e.id === id);
if (!item) return;
editingId = id;
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 || '';
// Change button text
const submitBtn = form.querySelector('button[type="submit"]');
submitBtn.innerHTML = ' Сохранить изменения';
}
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);
// Update logo based on theme
const logos = document.querySelectorAll('img[src*="kazakhtelecom-logo"]');
logos.forEach(img => {
if (next === 'dark') {
img.src = 'design-system/brand/kazakhtelecom-logo-white.png';
} else {
img.src = 'design-system/brand/kazakhtelecom-logo.png';
}
});
}
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, '"');
}
async async function saveEquipment() {
try {
await fetch('api/items', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(equipment)
});
} catch (e) {
console.error('Failed to save equipment:', e);
}
}
function deleteEquipment(id) {
if (!confirm('Вы уверены, что хотите удалить это оборудование?')) return;
equipment = equipment.filter(e => e.id !== id);
saveEquipment();
closeDrawer();
updateAll();
}
// 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();