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