442 lines
21 KiB
HTML
442 lines
21 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Журнал выдачи СИЗ</title>
|
||
<style>
|
||
:root {
|
||
--bg: #f5f7fa;
|
||
--card: #ffffff;
|
||
--text: #1a202c;
|
||
--text-secondary: #718096;
|
||
--primary: #3182ce;
|
||
--primary-hover: #2c5282;
|
||
--success: #38a169;
|
||
--warning: #d69e2e;
|
||
--danger: #e53e3e;
|
||
--border: #e2e8f0;
|
||
--shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||
--shadow-lg: 0 10px 15px rgba(0,0,0,0.1);
|
||
}
|
||
* { margin: 0; box-sizing: border-box; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; padding: 24px; }
|
||
.container { max-width: 1400px; margin: 0 auto; }
|
||
h1 { font-size: 28px; font-weight: 700; margin-bottom: 8px; color: var(--text); }
|
||
.subtitle { color: var(--text-secondary); margin-bottom: 24px; }
|
||
|
||
.card { background: var(--card); border-radius: 12px; box-shadow: var(--shadow); padding: 20px; margin-bottom: 20px; }
|
||
.card-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
|
||
|
||
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 16px; margin-bottom: 20px; }
|
||
.stat { background: var(--card); padding: 16px; border-radius: 10px; box-shadow: var(--shadow); text-align: center; }
|
||
.stat-value { font-size: 32px; font-weight: 700; color: var(--primary); }
|
||
.stat-label { font-size: 13px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.stat.success .stat-value { color: var(--success); }
|
||
.stat.warning .stat-value { color: var(--warning); }
|
||
.stat.danger .stat-value { color: var(--danger); }
|
||
|
||
.controls { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; margin-bottom: 20px; }
|
||
.search { flex: 1; min-width: 250px; padding: 12px 16px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; outline: none; transition: border-color 0.2s; }
|
||
.search:focus { border-color: var(--primary); }
|
||
.chip { padding: 8px 16px; border: 1px solid var(--border); border-radius: 20px; background: var(--card); cursor: pointer; font-size: 13px; font-weight: 500; transition: all 0.2s; }
|
||
.chip:hover { background: var(--bg); }
|
||
.chip.active { background: var(--primary); color: white; border-color: var(--primary); }
|
||
.btn { padding: 12px 24px; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; display: inline-flex; align-items: center; gap: 8px; }
|
||
.btn-primary { background: var(--primary); color: white; }
|
||
.btn-primary:hover { background: var(--primary-hover); }
|
||
.btn-secondary { background: var(--bg); color: var(--text); }
|
||
.btn-secondary:hover { background: var(--border); }
|
||
.btn-danger { background: var(--danger); color: white; }
|
||
.btn-danger:hover { background: #c53030; }
|
||
.btn-success { background: var(--success); color: white; }
|
||
.btn-sm { padding: 6px 12px; font-size: 12px; }
|
||
|
||
.table-container { overflow-x: auto; }
|
||
table { width: 100%; border-collapse: collapse; }
|
||
th, td { padding: 14px 12px; text-align: left; border-bottom: 1px solid var(--border); }
|
||
th { background: var(--bg); font-weight: 600; font-size: 13px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); position: sticky; top: 0; }
|
||
tr:hover { background: var(--bg); cursor: pointer; }
|
||
tr:last-child td { border-bottom: none; }
|
||
.status { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; text-transform: uppercase; }
|
||
.status-issued { background: #bee3f8; color: #2c5282; }
|
||
.status-returned { background: #c6f6d5; color: #22543d; }
|
||
.status-overdue { background: #fed7d7; color: #742a2a; }
|
||
|
||
.actions { display: flex; gap: 8px; }
|
||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; }
|
||
.modal-overlay.open { display: flex; }
|
||
.modal { background: var(--card); border-radius: 12px; padding: 24px; width: 100%; max-width: 500px; max-height: 90vh; overflow-y: auto; box-shadow: var(--shadow-lg); }
|
||
.modal-title { font-size: 20px; font-weight: 600; margin-bottom: 20px; }
|
||
.form-group { margin-bottom: 16px; }
|
||
.form-label { display: block; font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 6px; }
|
||
.form-input, .form-select { width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 14px; outline: none; }
|
||
.form-input:focus, .form-select:focus { border-color: var(--primary); }
|
||
.form-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px; }
|
||
|
||
.history-item { padding: 12px; border-left: 3px solid var(--primary); background: var(--bg); border-radius: 0 8px 8px 0; margin-bottom: 8px; }
|
||
.history-when { font-size: 12px; color: var(--text-secondary); }
|
||
.history-what { font-size: 14px; font-weight: 500; margin: 4px 0; }
|
||
.history-who { font-size: 12px; color: var(--text-secondary); }
|
||
|
||
.tabs { display: flex; gap: 8px; margin-bottom: 20px; border-bottom: 2px solid var(--border); padding-bottom: 12px; }
|
||
.tab { padding: 8px 16px; border-radius: 8px 8px 0 0; cursor: pointer; font-weight: 500; color: var(--text-secondary); transition: all 0.2s; }
|
||
.tab:hover { background: var(--bg); }
|
||
.tab.active { background: var(--primary); color: white; }
|
||
|
||
@media (max-width: 768px) {
|
||
body { padding: 12px; }
|
||
.stats { grid-template-columns: repeat(2, 1fr); }
|
||
.controls { flex-direction: column; }
|
||
.search { width: 100%; }
|
||
th, td { padding: 10px 8px; font-size: 13px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>📋 Журнал выдачи СИЗ</h1>
|
||
<p class="subtitle">Учёт выдачи спецодежды и средств индивидуальной защиты</p>
|
||
|
||
<div class="stats">
|
||
<div class="stat"><div class="stat-value" id="stat-total">0</div><div class="stat-label">Всего</div></div>
|
||
<div class="stat success"><div class="stat-value" id="stat-issued">0</div><div class="stat-label">Выдано</div></div>
|
||
<div class="stat warning"><div class="stat-value" id="stat-returned">0</div><div class="stat-label">Возвращено</div></div>
|
||
<div class="stat danger"><div class="stat-value" id="stat-overdue">0</div><div class="stat-label">Просрочено</div></div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="tabs">
|
||
<div class="tab active" data-tab="journal">📖 Журнал</div>
|
||
<div class="tab" data-tab="history">📜 История изменений</div>
|
||
</div>
|
||
|
||
<div id="tab-journal">
|
||
<div class="controls">
|
||
<input type="text" class="search" id="search" placeholder="🔍 Поиск по всем полям...">
|
||
<div class="chip active" data-status="all">Все</div>
|
||
<div class="chip" data-status="issued">Выдано</div>
|
||
<div class="chip" data-status="returned">Возвращено</div>
|
||
<div class="chip" data-status="overdue">Просрочено</div>
|
||
<button class="btn btn-primary" id="btn-add">➕ Добавить запись</button>
|
||
<button class="btn btn-secondary" id="btn-export">📥 Выгрузить в Excel (CSV)</button>
|
||
</div>
|
||
|
||
<div class="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Дата выдачи</th>
|
||
<th>Что выдали</th>
|
||
<th>Кому выдали</th>
|
||
<th>Кол-во</th>
|
||
<th>Вернуть до</th>
|
||
<th>Кто выдал</th>
|
||
<th>Статус</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="table-body"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="tab-history" style="display: none;">
|
||
<div id="history-list"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="modal-form">
|
||
<div class="modal">
|
||
<h2 class="modal-title" id="form-title">Добавить запись</h2>
|
||
<form id="record-form">
|
||
<input type="hidden" id="record-id">
|
||
<div class="form-group">
|
||
<label class="form-label">Дата выдачи *</label>
|
||
<input type="date" class="form-input" id="date-issue" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Что выдали *</label>
|
||
<input type="text" class="form-input" id="item-name" placeholder="Например: каска защитная" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Кому выдали *</label>
|
||
<input type="text" class="form-input" id="person" placeholder="ФИО сотрудника" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Количество *</label>
|
||
<input type="number" class="form-input" id="qty" min="1" value="1" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Когда вернуть *</label>
|
||
<input type="date" class="form-input" id="date-return" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Кто выдал *</label>
|
||
<input type="text" class="form-input" id="issuer" placeholder="ФИО кладовщика" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Статус *</label>
|
||
<select class="form-select" id="status" required>
|
||
<option value="issued">Выдано</option>
|
||
<option value="returned">Возвращено</option>
|
||
<option value="overdue">Просрочено</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-secondary" onclick="closeModal()">Отмена</button>
|
||
<button type="submit" class="btn btn-primary">💾 Сохранить</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="modal-confirm">
|
||
<div class="modal">
|
||
<h2 class="modal-title">⚠️ Подтверждение удаления</h2>
|
||
<p style="margin-bottom: 20px;">Вы уверены, что хотите удалить эту запись? Это действие нельзя отменить.</p>
|
||
<input type="hidden" id="delete-id">
|
||
<div class="form-actions">
|
||
<button type="button" class="btn btn-secondary" onclick="closeConfirm()">Отмена</button>
|
||
<button type="button" class="btn btn-danger" id="btn-confirm-delete">🗑️ Удалить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const DEMO_DATA = [
|
||
{ id: 1, dateIssue: "2026-06-15", item: "Каска защитная", person: "Айдос Нурланов", qty: 1, dateReturn: "2026-07-15", issuer: "Петров В.И.", status: "returned" },
|
||
{ id: 2, dateIssue: "2026-06-20", item: "Костюм летний", person: "Гульмира Сейсембаева", qty: 1, dateReturn: "2026-08-20", issuer: "Петров В.И.", status: "issued" },
|
||
{ id: 3, dateIssue: "2026-07-01", item: "Ботинки защитные", person: "Данияр Ермеков", qty: 1, dateReturn: "2026-08-01", issuer: "Сидорова А.К.", status: "returned" },
|
||
{ id: 4, dateIssue: "2026-07-05", item: "Перчатки диэлектрические", person: "Алина Касенова", qty: 2, dateReturn: "2026-07-20", issuer: "Петров В.И.", status: "returned" },
|
||
{ id: 5, dateIssue: "2026-07-10", item: "Жилет сигнальный", person: "Ерлан Тлеубаев", qty: 1, dateReturn: "2026-09-10", issuer: "Сидорова А.К.", status: "issued" },
|
||
{ id: 6, dateIssue: "2026-07-15", item: "Куртка зимняя", person: "Айдос Нурланов", qty: 1, dateReturn: "2026-10-15", issuer: "Петров В.И.", status: "issued" },
|
||
{ id: 7, dateIssue: "2026-07-20", item: "Каска защитная", person: "Гульмира Сейсембаева", qty: 1, dateReturn: "2026-08-20", issuer: "Сидорова А.К.", status: "issued" },
|
||
{ id: 8, dateIssue: "2026-05-10", item: "Костюм летний", person: "Данияр Ермеков", qty: 1, dateReturn: "2026-06-10", issuer: "Петров В.И.", status: "overdue" },
|
||
{ id: 9, dateIssue: "2026-07-25", item: "Ботинки защитные", person: "Алина Касенова", qty: 1, dateReturn: "2026-09-25", issuer: "Сидорова А.К.", status: "issued" },
|
||
{ id: 10, dateIssue: "2026-06-01", item: "Перчатки диэлектрические", person: "Ерлан Тлеубаев", qty: 3, dateReturn: "2026-07-01", issuer: "Петров В.И.", status: "overdue" },
|
||
{ id: 11, dateIssue: "2026-07-28", item: "Жилет сигнальный", person: "Айдос Нурланов", qty: 1, dateReturn: "2026-09-28", issuer: "Сидорова А.К.", status: "issued" },
|
||
{ id: 12, dateIssue: "2026-06-25", item: "Куртка зимняя", person: "Гульмира Сейсембаева", qty: 1, dateReturn: "2026-08-25", issuer: "Петров В.И.", status: "returned" }
|
||
];
|
||
|
||
let records = [];
|
||
let history = [];
|
||
let currentStatus = 'all';
|
||
let currentSearch = '';
|
||
|
||
function loadData() {
|
||
const saved = localStorage.getItem('siz-records');
|
||
records = saved ? JSON.parse(saved) : [...DEMO_DATA];
|
||
const savedHistory = localStorage.getItem('siz-history');
|
||
history = savedHistory ? JSON.parse(savedHistory) : [];
|
||
}
|
||
|
||
function saveData() {
|
||
localStorage.setItem('siz-records', JSON.stringify(records));
|
||
localStorage.setItem('siz-history', JSON.stringify(history));
|
||
}
|
||
|
||
function formatDate(dateStr) {
|
||
if (!dateStr) return '';
|
||
const [y, m, d] = dateStr.split('-');
|
||
return `${d}.${m}.${y}`;
|
||
}
|
||
|
||
function getStatusLabel(status) {
|
||
const labels = { issued: 'Выдано', returned: 'Возвращено', overdue: 'Просрочено' };
|
||
return labels[status] || status;
|
||
}
|
||
|
||
function getStatusClass(status) {
|
||
return `status-${status}`;
|
||
}
|
||
|
||
function updateStats() {
|
||
const total = records.length;
|
||
const issued = records.filter(r => r.status === 'issued').length;
|
||
const returned = records.filter(r => r.status === 'returned').length;
|
||
const overdue = records.filter(r => r.status === 'overdue').length;
|
||
document.getElementById('stat-total').textContent = total;
|
||
document.getElementById('stat-issued').textContent = issued;
|
||
document.getElementById('stat-returned').textContent = returned;
|
||
document.getElementById('stat-overdue').textContent = overdue;
|
||
}
|
||
|
||
function getFilteredRecords() {
|
||
return records.filter(r => {
|
||
const matchesStatus = currentStatus === 'all' || r.status === currentStatus;
|
||
const searchLower = currentSearch.toLowerCase();
|
||
const matchesSearch = !currentSearch ||
|
||
r.item.toLowerCase().includes(searchLower) ||
|
||
r.person.toLowerCase().includes(searchLower) ||
|
||
r.issuer.toLowerCase().includes(searchLower) ||
|
||
r.dateIssue.includes(searchLower) ||
|
||
r.dateReturn.includes(searchLower);
|
||
return matchesStatus && matchesSearch;
|
||
});
|
||
}
|
||
|
||
function renderTable() {
|
||
const tbody = document.getElementById('table-body');
|
||
const filtered = getFilteredRecords();
|
||
tbody.innerHTML = filtered.map(r => `
|
||
<tr onclick="openEdit(${r.id})">
|
||
<td>${formatDate(r.dateIssue)}</td>
|
||
<td>${r.item}</td>
|
||
<td>${r.person}</td>
|
||
<td>${r.qty}</td>
|
||
<td>${formatDate(r.dateReturn)}</td>
|
||
<td>${r.issuer}</td>
|
||
<td><span class="status ${getStatusClass(r.status)}">${getStatusLabel(r.status)}</span></td>
|
||
<td class="actions" onclick="event.stopPropagation()">
|
||
<button class="btn btn-sm btn-danger" onclick="openConfirm(${r.id})">🗑️</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
updateStats();
|
||
}
|
||
|
||
function renderHistory() {
|
||
const list = document.getElementById('history-list');
|
||
if (history.length === 0) {
|
||
list.innerHTML = '<p style="color: var(--text-secondary); text-align: center; padding: 40px;">История изменений пуста</p>';
|
||
return;
|
||
}
|
||
list.innerHTML = history.slice().reverse().map(h => `
|
||
<div class="history-item">
|
||
<div class="history-when">${formatDate(h.when)} ${h.time}</div>
|
||
<div class="history-what">${h.what}</div>
|
||
<div class="history-who">${h.who}</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function openModal(isEdit = false, record = null) {
|
||
document.getElementById('form-title').textContent = isEdit ? 'Редактировать запись' : 'Добавить запись';
|
||
document.getElementById('record-id').value = record ? record.id : '';
|
||
document.getElementById('date-issue').value = record ? record.dateIssue : '';
|
||
document.getElementById('item-name').value = record ? record.item : '';
|
||
document.getElementById('person').value = record ? record.person : '';
|
||
document.getElementById('qty').value = record ? record.qty : 1;
|
||
document.getElementById('date-return').value = record ? record.dateReturn : '';
|
||
document.getElementById('issuer').value = record ? record.issuer : '';
|
||
document.getElementById('status').value = record ? record.status : 'issued';
|
||
document.getElementById('modal-form').classList.add('open');
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('modal-form').classList.remove('open');
|
||
document.getElementById('record-form').reset();
|
||
}
|
||
|
||
function openConfirm(id) {
|
||
document.getElementById('delete-id').value = id;
|
||
document.getElementById('modal-confirm').classList.add('open');
|
||
}
|
||
|
||
function closeConfirm() {
|
||
document.getElementById('modal-confirm').classList.remove('open');
|
||
}
|
||
|
||
function openEdit(id) {
|
||
const record = records.find(r => r.id === id);
|
||
if (record) openModal(true, record);
|
||
}
|
||
|
||
function addHistory(what, who) {
|
||
history.push({
|
||
when: new Date().toISOString().split('T')[0],
|
||
time: new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }),
|
||
what, who
|
||
});
|
||
saveData();
|
||
renderHistory();
|
||
}
|
||
|
||
document.getElementById('record-form').addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
const id = document.getElementById('record-id').value;
|
||
const record = {
|
||
id: id ? parseInt(id) : Date.now(),
|
||
dateIssue: document.getElementById('date-issue').value,
|
||
item: document.getElementById('item-name').value,
|
||
person: document.getElementById('person').value,
|
||
qty: parseInt(document.getElementById('qty').value),
|
||
dateReturn: document.getElementById('date-return').value,
|
||
issuer: document.getElementById('issuer').value,
|
||
status: document.getElementById('status').value
|
||
};
|
||
if (id) {
|
||
const idx = records.findIndex(r => r.id == id);
|
||
const old = records[idx];
|
||
records[idx] = record;
|
||
addHistory(`Изменена запись: ${old.item} → ${record.item} (${record.person})`, record.issuer);
|
||
} else {
|
||
records.push(record);
|
||
addHistory(`Добавлена запись: ${record.item} (${record.person})`, record.issuer);
|
||
}
|
||
saveData();
|
||
closeModal();
|
||
renderTable();
|
||
});
|
||
|
||
document.getElementById('btn-confirm-delete').addEventListener('click', function() {
|
||
const id = parseInt(document.getElementById('delete-id').value);
|
||
const record = records.find(r => r.id === id);
|
||
if (record) {
|
||
addHistory(`Удалена запись: ${record.item} (${record.person})`, 'Кладовщик');
|
||
records = records.filter(r => r.id !== id);
|
||
saveData();
|
||
renderTable();
|
||
}
|
||
closeConfirm();
|
||
});
|
||
|
||
document.getElementById('btn-add').addEventListener('click', () => openModal(false));
|
||
document.getElementById('btn-export').addEventListener('click', exportCsv);
|
||
|
||
document.getElementById('search').addEventListener('input', function(e) {
|
||
currentSearch = e.target.value;
|
||
renderTable();
|
||
});
|
||
|
||
document.querySelectorAll('.chip').forEach(chip => {
|
||
chip.addEventListener('click', function() {
|
||
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
|
||
this.classList.add('active');
|
||
currentStatus = this.dataset.status;
|
||
renderTable();
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.tab').forEach(tab => {
|
||
tab.addEventListener('click', function() {
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
this.classList.add('active');
|
||
document.getElementById('tab-journal').style.display = this.dataset.tab === 'journal' ? 'block' : 'none';
|
||
document.getElementById('tab-history').style.display = this.dataset.tab === 'history' ? 'block' : 'none';
|
||
if (this.dataset.tab === 'history') renderHistory();
|
||
});
|
||
});
|
||
|
||
function exportCsv() {
|
||
const rows = getFilteredRecords();
|
||
const head = ['Дата выдачи', 'Что выдали', 'Кому выдали', 'Количество', 'Вернуть до', 'Кто выдал', 'Статус'];
|
||
const body = rows.map(r => [
|
||
formatDate(r.dateIssue), r.item, r.person, r.qty, formatDate(r.dateReturn), r.issuer, getStatusLabel(r.status)
|
||
].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 = 'zhurnal-siz.csv';
|
||
a.click();
|
||
addHistory('Выгружена таблица в CSV', 'Кладовщик');
|
||
}
|
||
|
||
loadData();
|
||
renderTable();
|
||
</script>
|
||
</body>
|
||
</html>
|