zhurnal-ucheta-zayavok-2/index.html

326 lines
14 KiB
HTML
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.

<!DOCTYPE html>
<html lang="ru" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Журнал учёта заявок</title>
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
<link rel="stylesheet" href="design-system/kt-ai-components.css">
<link rel="stylesheet" href="design-system/kt-ai-page.css">
<style>
.kt-ai-btn[data-variant="link"] { background: none; border: none; color: var(--kt-ai-link); cursor: pointer; font: inherit; padding: 0; }
.kt-ai-btn[data-variant="link"]:hover { text-decoration: underline; }
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: none; align-items: center; justify-content: center; z-index: 1000; }
.modal-overlay.on { display: flex; }
.modal { background: var(--kt-ai-bg-elevated); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-3xl); padding: var(--kt-ai-space-8); width: 100%; max-width: 520px; max-height: 90vh; overflow-y: auto; }
.modal-title { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-6); }
.modal-foot { display: flex; gap: var(--kt-ai-space-3); justify-content: flex-end; margin-top: var(--kt-ai-space-7); }
.status-chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: var(--kt-ai-radius-full); font-size: var(--kt-ai-text-xs); font-weight: var(--kt-ai-weight-medium); }
.status-chip[data-tone="info"] { background: var(--kt-ai-status-info-subtle); color: var(--kt-ai-status-info-fg); }
.status-chip[data-tone="warn"] { background: var(--kt-ai-status-warn-subtle); color: var(--kt-ai-status-warn-fg); }
.status-chip[data-tone="ok"] { background: var(--kt-ai-status-ok-subtle); color: var(--kt-ai-status-ok-fg); }
.log-entry { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); padding: 6px 0; border-bottom: 1px solid var(--kt-ai-divider); }
.log-entry:last-child { border-bottom: none; }
.toolbar { display: flex; gap: var(--kt-ai-space-4); align-items: center; flex-wrap: wrap; margin-bottom: var(--kt-ai-space-6); }
.toolbar .spacer { flex: 1; }
.chip-row { display: flex; gap: 8px; flex-wrap: wrap; }
.kt-ai-chip { cursor: pointer; user-select: none; }
.kt-ai-chip[data-active="true"] { background: var(--kt-ai-primary); color: var(--kt-ai-bg); }
.kpi-strip { display: flex; gap: var(--kt-ai-space-6); flex-wrap: wrap; margin-bottom: var(--kt-ai-space-8); }
.kpi-card { background: var(--kt-ai-bg-soft); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-2xl); padding: var(--kt-ai-space-5); min-width: 140px; }
.kpi-value { font-size: var(--kt-ai-text-3xl); font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-fg); }
.kpi-label { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); margin-top: 4px; }
.table-wrap { overflow-x: auto; }
.kt-ai-table th { white-space: nowrap; }
.kt-ai-table td { vertical-align: middle; }
.row-actions { display: flex; gap: 8px; opacity: 0; transition: opacity 0.2s; }
.kt-ai-table tbody tr:hover .row-actions { opacity: 1; }
.log-panel { margin-top: var(--kt-ai-space-10); }
.log-panel h3 { font-size: var(--kt-ai-text-lg); font-weight: var(--kt-ai-weight-semibold); margin-bottom: var(--kt-ai-space-4); }
.log-list { max-height: 200px; overflow-y: auto; background: var(--kt-ai-bg-soft); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-xl); padding: var(--kt-ai-space-4); }
</style>
</head>
<body>
<header class="kt-ai-pagehead">
<div class="kt-ai-wrap kt-ai-pagehead-in">
<strong>Журнал заявок</strong>
<span class="kt-ai-spacer"></span>
<button class="kt-ai-btn" data-variant="ghost" id="themeToggle">🌓 Тема</button>
<button class="kt-ai-btn" data-variant="primary" id="btnNew">+ Заявка</button>
</div>
</header>
<section class="kt-ai-hero" data-align="center">
<div class="kt-ai-wrap">
<h1>Учёт заявок сотрудников</h1>
<p class="kt-ai-hero-sub" style="max-width:560px;margin:0 auto var(--kt-ai-space-6)">
Регистрация, отслеживание статусов и сроков. Все данные сохраняются в вашем браузере.
</p>
</div>
</section>
<section class="kt-ai-section">
<div class="kt-ai-wrap" data-width="wide">
<div class="kpi-strip" id="kpiStrip"></div>
<div class="toolbar">
<input class="kt-ai-input" id="searchInput" placeholder="Поиск по заявке..." style="min-width:240px">
<div class="chip-row" id="filterRow"></div>
<span class="spacer"></span>
<button class="kt-ai-btn" id="btnExport">Выгрузить в Excel</button>
</div>
<div class="table-wrap">
<table class="kt-ai-table">
<thead>
<tr>
<th>Кто подал</th>
<th>Что нужно</th>
<th>Срок</th>
<th>Статус</th>
<th></th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
<div class="log-panel">
<h3>Журнал изменений</h3>
<div class="log-list" id="logList"></div>
</div>
</div>
</section>
<footer class="kt-ai-wrap kt-ai-pagefoot">
<span>© 2026 Журнал заявок</span>
<span class="kt-ai-spacer"></span>
<span>Демо-прототип</span>
</footer>
<!-- Модалка добавления/редактирования -->
<div class="modal-overlay" id="modalOverlay">
<div class="modal">
<h2 class="modal-title" id="modalTitle">Новая заявка</h2>
<form id="appForm">
<input type="hidden" id="formId">
<div class="kt-ai-field">
<label>Кто подал</label>
<input class="kt-ai-input" id="formPerson" required placeholder="И. Фамилия">
</div>
<div class="kt-ai-field">
<label>Что нужно</label>
<input class="kt-ai-input" id="formWhat" required placeholder="Например: Настройка ПК">
</div>
<div class="kt-ai-field">
<label>Срок</label>
<input class="kt-ai-input" id="formDate" required placeholder="ДД.ММ.ГГГГ">
</div>
<div class="kt-ai-field">
<label>Статус</label>
<select class="kt-ai-select" id="formStatus">
<option value="Новая">Новая</option>
<option value="В работе">В работе</option>
<option value="Закрыта">Закрыта</option>
</select>
</div>
<div class="modal-foot">
<button type="button" class="kt-ai-btn" data-variant="ghost" id="btnCancel">Отмена</button>
<button type="submit" class="kt-ai-btn" data-variant="primary">Сохранить</button>
</div>
</form>
</div>
</div>
<script>
// === Демо-данные ===
const DEMO = [
{ id: 1, person: "А. Иванов", what: "Настройка ПК", date: "01.09.2026", status: "Новая" },
{ id: 2, person: "Б. Серикбаев", what: "Доступ в серверную", date: "02.09.2026", status: "В работе" },
{ id: 3, person: "В. Ли", what: "Закупка СИЗ", date: "03.09.2026", status: "Закрыта" },
{ id: 4, person: "А. Иванов", what: "Замена картриджа", date: "03.09.2026", status: "Новая" },
{ id: 5, person: "Б. Серикбаев", what: "Подключение принтера", date: "02.09.2026", status: "В работе" }
];
const STATUSES = { "Новая": "info", "В работе": "warn", "Закрыта": "ok" };
// === Хранилище ===
function load(key, fallback) { try { return JSON.parse(localStorage.getItem(key)) || fallback; } catch { return fallback; } }
function save(key, val) { localStorage.setItem(key, JSON.stringify(val)); }
let items = load("zayavki_items", DEMO);
let log = load("zayavki_log", []);
// === Рендер KPI ===
function renderKPI() {
const total = items.length;
const byStatus = {};
["Новая", "В работе", "Закрыта"].forEach(s => byStatus[s] = items.filter(i => i.status === s).length);
const kpiStrip = document.getElementById("kpiStrip");
kpiStrip.innerHTML = [
{ v: total, l: "Всего заявок" },
{ v: byStatus["Новая"], l: "Новые" },
{ v: byStatus["В работе"], l: "В работе" },
{ v: byStatus["Закрыта"], l: "Закрыты" }
].map(k => `<div class="kpi-card"><div class="kpi-value">${k.v}</div><div class="kpi-label">${k.l}</div></div>`).join("");
}
// === Рендер фильтров ===
let activeFilter = "Все";
function renderFilters() {
const row = document.getElementById("filterRow");
const chips = ["Все", "Новая", "В работе", "Закрыта"];
row.innerHTML = chips.map(c =>
`<span class="kt-ai-chip" data-active="${c === activeFilter ? "true" : "false"}" onclick="setFilter('${c}')">${c}</span>`
).join("");
}
function setFilter(name) { activeFilter = name; render(); }
// === Рендер таблицы ===
function renderTable() {
const search = document.getElementById("searchInput").value.toLowerCase();
const tbody = document.getElementById("tableBody");
let filtered = items;
if (activeFilter !== "Все") filtered = items.filter(i => i.status === activeFilter);
if (search) filtered = filtered.filter(i => i.what.toLowerCase().includes(search) || i.person.toLowerCase().includes(search));
tbody.innerHTML = filtered.map(i => `
<tr>
<td>${i.person}</td>
<td>${i.what}</td>
<td>${i.date}</td>
<td><span class="status-chip" data-tone="${STATUSES[i.status]}">${i.status}</span></td>
<td>
<div class="row-actions">
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" onclick="editItem(${i.id})">Изменить</button>
<button class="kt-ai-btn" data-variant="danger" data-size="sm" onclick="deleteItem(${i.id})">Удалить</button>
</div>
</td>
</tr>
`).join("");
}
// === Журнал логов ===
function renderLog() {
const list = document.getElementById("logList");
if (log.length === 0) {
list.innerHTML = '<span style="color:var(--kt-ai-fg-muted);font-size:var(--kt-ai-text-sm)">Пока нет записей</span>';
return;
}
list.innerHTML = log.slice().reverse().map(e =>
`<div class="log-entry"><strong>${e.when}</strong> — ${e.what} (${e.who})</div>`
).join("");
}
function addLog(what, who = "Пользователь") {
log.push({ when: new Date().toLocaleString("ru-RU"), what, who });
save("zayavki_log", log);
renderLog();
}
// === Модалка ===
const modalOverlay = document.getElementById("modalOverlay");
const appForm = document.getElementById("appForm");
function openModal(editId = null) {
document.getElementById("modalTitle").textContent = editId ? "Редактировать заявку" : "Новая заявка";
document.getElementById("formId").value = editId || "";
if (editId) {
const item = items.find(i => i.id === editId);
document.getElementById("formPerson").value = item.person;
document.getElementById("formWhat").value = item.what;
document.getElementById("formDate").value = item.date;
document.getElementById("formStatus").value = item.status;
} else {
appForm.reset();
}
modalOverlay.classList.add("on");
document.getElementById("formPerson").focus();
}
function closeModal() { modalOverlay.classList.remove("on"); }
appForm.addEventListener("submit", (e) => {
e.preventDefault();
const id = document.getElementById("formId").value;
const data = {
person: document.getElementById("formPerson").value,
what: document.getElementById("formWhat").value,
date: document.getElementById("formDate").value,
status: document.getElementById("formStatus").value
};
if (id) {
const idx = items.findIndex(i => i.id == id);
items[idx] = { ...items[idx], ...data };
addLog(`Изменена заявка #${id}: ${data.what}`);
} else {
const newItem = { id: Date.now(), ...data };
items.push(newItem);
addLog(`Добавлена заявка #${newItem.id}: ${data.what}`);
}
save("zayavki_items", items);
closeModal();
render();
});
// === Глобальные функции ===
window.editItem = (id) => openModal(id);
window.setFilter = setFilter;
window.deleteItem = (id) => {
if (!confirm("Вы уверены, что хотите удалить эту заявку?")) return;
const item = items.find(i => i.id === id);
items = items.filter(i => i.id !== id);
save("zayavki_items", items);
addLog(`Удалена заявка #${id}: ${item.what}`);
render();
};
// === Экспорт в CSV ===
document.getElementById("btnExport").addEventListener("click", () => {
const head = ["Кто подал", "Что нужно", "Срок", "Статус"];
const body = items.map(i => [i.person, i.what, i.date, i.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 = "zayavki_" + new Date().toISOString().slice(0,10) + ".csv";
a.click();
addLog("Выгружен файл Excel (CSV)");
});
// === Тема ===
document.getElementById("themeToggle").addEventListener("click", () => {
const html = document.documentElement;
const cur = html.getAttribute("data-theme");
html.setAttribute("data-theme", cur === "dark" ? "light" : "dark");
});
// === Кнопки ===
document.getElementById("btnNew").addEventListener("click", () => openModal());
document.getElementById("btnCancel").addEventListener("click", closeModal);
modalOverlay.addEventListener("click", (e) => { if (e.target === modalOverlay) closeModal(); });
document.getElementById("searchInput").addEventListener("input", renderTable);
// === Старт ===
function render() {
renderKPI();
renderFilters();
renderTable();
renderLog();
}
render();
</script>
</body>
</html>