sozday-sovremennyy-veb-sayt/script.js

670 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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();