uchet-propuskov/index.html

371 lines
17 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>
* { margin: 0; box-sizing: border-box; }
body { font-family: -apple-system, "Segoe UI", Roboto, sans-serif; background: #f5f7fa; color: #1a1a1a; min-height: 100vh; }
.container { max-width: 1200px; margin: 0 auto; padding: 24px; }
header { background: linear-gradient(135deg, #1e3a5f 0%, #2d5a87 100%); color: white; padding: 32px 0; margin-bottom: 24px; }
header h1 { font-size: 28px; font-weight: 600; }
header p { opacity: 0.9; margin-top: 8px; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 16px; margin-bottom: 24px; }
.stat-card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); text-align: center; }
.stat-card .num { font-size: 32px; font-weight: 700; color: #1e3a5f; }
.stat-card .label { font-size: 13px; color: #666; margin-top: 4px; }
.stat-card.all { border-left: 4px solid #1e3a5f; }
.stat-card.pending { border-left: 4px solid #f59e0b; }
.stat-card.approved { border-left: 4px solid #10b981; }
.stat-card.rejected { border-left: 4px solid #ef4444; }
.stat-card.cancelled { border-left: 4px solid #6b7280; }
.controls { background: white; border-radius: 12px; padding: 20px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.search-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
.search-row input { flex: 1; min-width: 200px; padding: 12px 16px; border: 1px solid #e5e7eb; border-radius: 8px; font-size: 14px; }
.search-row input:focus { outline: none; border-color: #1e3a5f; }
.btn { padding: 12px 20px; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
.btn-primary { background: #1e3a5f; color: white; }
.btn-primary:hover { background: #2d5a87; }
.btn-secondary { background: #f3f4f6; color: #374151; }
.btn-secondary:hover { background: #e5e7eb; }
.btn-danger { background: #ef4444; color: white; }
.btn-danger:hover { background: #dc2626; }
.btn-sm { padding: 6px 12px; font-size: 13px; }
.chips { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; }
.chip { padding: 8px 16px; border-radius: 20px; font-size: 13px; cursor: pointer; border: 2px solid transparent; transition: all 0.2s; }
.chip:hover { background: #f3f4f6; }
.chip.active { border-color: #1e3a5f; background: #eff6ff; }
.chip-all { background: #f3f4f6; }
.chip-pending { background: #fef3c7; color: #92400e; }
.chip-approved { background: #d1fae5; color: #065f46; }
.chip-rejected { background: #fee2e2; color: #991b1b; }
.chip-cancelled { background: #f3f4f6; color: #374151; }
.table-wrap { background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-bottom: 24px; }
table { width: 100%; border-collapse: collapse; }
th { background: #f9fafb; text-align: left; padding: 14px 16px; font-size: 13px; font-weight: 600; color: #6b7280; border-bottom: 1px solid #e5e7eb; }
td { padding: 14px 16px; border-bottom: 1px solid #f3f4f6; font-size: 14px; }
tr:hover { background: #f9fafb; }
.status { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 500; display: inline-block; }
.status-pending { background: #fef3c7; color: #92400e; }
.status-approved { background: #d1fae5; color: #065f46; }
.status-rejected { background: #fee2e2; color: #991b1b; }
.status-cancelled { background: #f3f4f6; color: #374151; }
.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.on { display: flex; }
.modal { background: white; border-radius: 16px; width: 100%; max-width: 500px; max-height: 90vh; overflow-y: auto; }
.modal-header { padding: 20px 24px; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; }
.modal-header h2 { font-size: 18px; }
.modal-close { background: none; border: none; font-size: 24px; cursor: pointer; color: #6b7280; }
.modal-body { padding: 24px; }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; font-size: 13px; font-weight: 500; color: #374151; margin-bottom: 6px; }
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 8px; font-size: 14px; }
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline: none; border-color: #1e3a5f; }
.form-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px; }
.log-wrap { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.log-wrap h3 { font-size: 16px; margin-bottom: 16px; }
.log-table { width: 100%; font-size: 13px; }
.log-table td { padding: 10px 16px; color: #6b7280; }
.empty { text-align: center; padding: 40px; color: #6b7280; }
</style>
</head>
<body>
<header>
<div class="container">
<h1>Учёт заявок на пропуска</h1>
<p>Система регистрации и согласования пропусков для посетителей</p>
</div>
</header>
<div class="container">
<div class="stats">
<div class="stat-card all"><div class="num" id="stat-all">0</div><div class="label">Всего</div></div>
<div class="stat-card pending"><div class="num" id="stat-pending">0</div><div class="label">На согласовании</div></div>
<div class="stat-card approved"><div class="num" id="stat-approved">0</div><div class="label">Согласован</div></div>
<div class="stat-card rejected"><div class="num" id="stat-rejected">0</div><div class="label">Отклонен</div></div>
<div class="stat-card cancelled"><div class="num" id="stat-cancelled">0</div><div class="label">Отменен</div></div>
</div>
<div class="controls">
<div class="search-row">
<input type="text" id="search" placeholder="Поиск по заказчику, получателю, цели или статусу...">
<button class="btn btn-primary" onclick="openModal()">+ Добавить заявку</button>
<button class="btn btn-secondary" onclick="exportCsv()">Скачать CSV</button>
</div>
<div class="chips">
<div class="chip chip-all active" data-status="all">Все</div>
<div class="chip chip-pending" data-status="pending">На согласовании</div>
<div class="chip chip-approved" data-status="approved">Согласован</div>
<div class="chip chip-rejected" data-status="rejected">Отклонен</div>
<div class="chip chip-cancelled" data-status="cancelled">Отменен</div>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Дата визита</th>
<th>Заказчик</th>
<th>Получатель пропуска</th>
<th>Цель визита</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>
<div id="empty" class="empty" style="display:none">Нет заявок</div>
</div>
<div class="log-wrap">
<h3>Журнал изменений</h3>
<table class="log-table">
<thead><tr><th>Дата и время</th><th>Действие</th><th>Автор</th></tr></thead>
<tbody id="log-body"></tbody>
</table>
</div>
</div>
<div class="modal-overlay" id="modal">
<div class="modal">
<div class="modal-header">
<h2 id="modal-title">Добавить заявку</h2>
<button class="modal-close" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body">
<input type="hidden" id="edit-id">
<div class="form-group">
<label>Кто заказал</label>
<input type="text" id="f-requester" placeholder="ФИО заказчика">
</div>
<div class="form-group">
<label>Для кого пропуск</label>
<input type="text" id="f-visitor" placeholder="ФИО посетителя">
</div>
<div class="form-group">
<label>Дата визита</label>
<input type="text" id="f-date" placeholder="ДД.ММ.ГГГГ">
</div>
<div class="form-group">
<label>Цель визита</label>
<textarea id="f-purpose" rows="3" placeholder="Например: деловая встреча"></textarea>
</div>
<div class="form-group">
<label>Статус согласования</label>
<select id="f-status">
<option value="pending">На согласовании</option>
<option value="approved">Согласован</option>
<option value="rejected">Отклонен</option>
<option value="cancelled">Отменен</option>
</select>
</div>
<div class="form-actions">
<button class="btn btn-secondary" onclick="closeModal()">Отмена</button>
<button class="btn btn-primary" onclick="saveItem()">Сохранить</button>
</div>
</div>
</div>
</div>
<script>
const DEMO_DATA = [
{ id: 1, requester: "Айдос Нуртаев", visitor: "Гульмира Садыкова", date: "05.09.2026", purpose: "Деловая встреча по проекту", status: "approved" },
{ id: 2, requester: "Данияр Сулейменов", visitor: "Алия Каримова", date: "07.09.2026", purpose: "Консультация по договору", status: "pending" },
{ id: 3, requester: "Ерлан Бекенов", visitor: "Жанна Омарова", date: "09.09.2026", purpose: "Подписание документов", status: "approved" },
{ id: 4, requester: "Айдос Нуртаев", visitor: "Серикжан Муратов", date: "10.09.2026", purpose: "Технический аудит", status: "pending" },
{ id: 5, requester: "Гульмира Садыкова", visitor: "Бакытжан Ибраев", date: "12.09.2026", purpose: "Встреча с инвесторами", status: "rejected" },
{ id: 6, requester: "Данияр Сулейменов", visitor: "Айгерим Талгатова", date: "14.09.2026", purpose: "Обучение сотрудников", status: "approved" },
{ id: 7, requester: "Ерлан Бекенов", visitor: "Нурлан Сабиров", date: "16.09.2026", purpose: "Презентация продукта", status: "pending" },
{ id: 8, requester: "Жанна Омарова", visitor: "Светлана Петрова", date: "18.09.2026", purpose: "Фотосъёмка офиса", status: "cancelled" }
];
let items = [];
let log = [];
let currentStatus = "all";
let currentEditId = null;
function init() {
const stored = localStorage.getItem("propusk_items");
if (stored) {
items = JSON.parse(stored);
} else {
items = [...DEMO_DATA];
saveToStorage();
}
const logStored = localStorage.getItem("propusk_log");
if (logStored) log = JSON.parse(logStored);
render();
setupChips();
document.getElementById("search").addEventListener("input", render);
}
function saveToStorage() {
localStorage.setItem("propusk_items", JSON.stringify(items));
}
function saveLog() {
localStorage.setItem("propusk_log", JSON.stringify(log));
}
function addLog(action, details) {
log.unshift({
when: new Date().toLocaleString("ru-RU"),
what: action + ": " + details,
who: "Пользователь"
});
saveLog();
}
function render() {
const search = document.getElementById("search").value.toLowerCase();
const filtered = items.filter(item => {
if (currentStatus !== "all" && item.status !== currentStatus) return false;
if (search) {
const hay = (item.requester + " " + item.visitor + " " + item.purpose + " " + item.status).toLowerCase();
if (!hay.includes(search)) return false;
}
return true;
});
document.getElementById("stat-all").textContent = items.length;
document.getElementById("stat-pending").textContent = items.filter(i => i.status === "pending").length;
document.getElementById("stat-approved").textContent = items.filter(i => i.status === "approved").length;
document.getElementById("stat-rejected").textContent = items.filter(i => i.status === "rejected").length;
document.getElementById("stat-cancelled").textContent = items.filter(i => i.status === "cancelled").length;
const tbody = document.getElementById("table-body");
if (filtered.length === 0) {
tbody.innerHTML = "";
document.getElementById("empty").style.display = "block";
} else {
document.getElementById("empty").style.display = "none";
tbody.innerHTML = filtered.map(item => {
const statusLabels = { pending: "На согласовании", approved: "Согласован", rejected: "Отклонен", cancelled: "Отменен" };
return `<tr>
<td>${item.date}</td>
<td>${item.requester}</td>
<td>${item.visitor}</td>
<td>${item.purpose}</td>
<td><span class="status status-${item.status}">${statusLabels[item.status]}</span></td>
<td class="actions">
<button class="btn btn-sm btn-secondary" onclick="editItem(${item.id})">Редактировать</button>
<button class="btn btn-sm btn-danger" onclick="deleteItem(${item.id})">Удалить</button>
</td>
</tr>`;
}).join("");
}
const logBody = document.getElementById("log-body");
logBody.innerHTML = log.slice(0, 10).map(l => `<tr><td>${l.when}</td><td>${l.what}</td><td>${l.who}</td></tr>`).join("");
}
function setupChips() {
document.querySelectorAll(".chip").forEach(chip => {
chip.addEventListener("click", () => {
document.querySelectorAll(".chip").forEach(c => c.classList.remove("active"));
chip.classList.add("active");
currentStatus = chip.dataset.status;
render();
});
});
}
function openModal() {
document.getElementById("modal").classList.add("on");
document.getElementById("modal-title").textContent = "Добавить заявку";
document.getElementById("edit-id").value = "";
document.getElementById("f-requester").value = "";
document.getElementById("f-visitor").value = "";
document.getElementById("f-date").value = "";
document.getElementById("f-purpose").value = "";
document.getElementById("f-status").value = "pending";
}
function closeModal() {
document.getElementById("modal").classList.remove("on");
currentEditId = null;
}
function editItem(id) {
const item = items.find(i => i.id === id);
if (!item) return;
currentEditId = id;
document.getElementById("modal-title").textContent = "Редактировать заявку";
document.getElementById("edit-id").value = item.id;
document.getElementById("f-requester").value = item.requester;
document.getElementById("f-visitor").value = item.visitor;
document.getElementById("f-date").value = item.date;
document.getElementById("f-purpose").value = item.purpose;
document.getElementById("f-status").value = item.status;
document.getElementById("modal").classList.add("on");
}
function saveItem() {
const requester = document.getElementById("f-requester").value.trim();
const visitor = document.getElementById("f-visitor").value.trim();
const date = document.getElementById("f-date").value.trim();
const purpose = document.getElementById("f-purpose").value.trim();
const status = document.getElementById("f-status").value;
if (!requester || !visitor || !date || !purpose) {
alert("Заполните все поля");
return;
}
if (currentEditId) {
const idx = items.findIndex(i => i.id === currentEditId);
if (idx !== -1) {
const old = items[idx];
items[idx] = { ...old, requester, visitor, date, purpose, status };
addLog("Изменение", `Заявка #${currentEditId}: ${visitor}`);
}
} else {
const newItem = {
id: Date.now(),
requester, visitor, date, purpose, status
};
items.unshift(newItem);
addLog("Создание", `Заявка #${newItem.id}: ${visitor}`);
}
saveToStorage();
closeModal();
render();
}
function deleteItem(id) {
const item = items.find(i => i.id === id);
if (!item) return;
if (!confirm(`Удалить заявку на ${item.visitor}?`)) return;
items = items.filter(i => i.id !== id);
addLog("Удаление", `Заявка #${id}: ${item.visitor}`);
saveToStorage();
render();
}
function exportCsv() {
const head = ["Дата визита", "Заказчик", "Получатель пропуска", "Цель визита", "Статус"];
const statusLabels = { pending: "На согласовании", approved: "Согласован", rejected: "Отклонен", cancelled: "Отменен" };
const body = items.map(r => [r.date, r.requester, r.visitor, r.purpose, statusLabels[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 = "propuska.csv";
a.click();
}
init();
</script>
</body>
</html>