zhurnal-vydachi-siz/index.html

442 lines
21 KiB
HTML
Raw Permalink 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.

<!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>