371 lines
17 KiB
HTML
371 lines
17 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>
|
||
* { 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()">×</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>
|