zhurnal-ucheta-zayavok-3/index.html

663 lines
32 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="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Журнал учёта заявок</title>
<script data-kt-ai-base-resolver>
(function () {
var корень = document.documentElement;
function норм(значение) {
значение = (значение || "").trim();
if (!значение || значение === "/") return "";
if (значение.charAt(0) !== "/") значение = "/" + значение;
return значение.replace(/\/+$/, "");
}
var база = норм(корень.getAttribute("data-kt-ai-base"));
var папка = норм(корень.getAttribute("data-kt-ai-static")) || "/design-system";
window.ktAiBase = база;
window.ktAiAsset = function (имя) {
return /^([a-z][a-z0-9+.-]*:|\/\/)/i.test(имя) ? имя : база + папка + "/" + имя;
};
})();
</script>
<script data-kt-ai-boot="css">
["kt-ai-tokens.css", "kt-ai-components.css"].forEach(function (файл) {
var ссылка = document.createElement("link");
ссылка.rel = "stylesheet";
ссылка.href = window.ktAiAsset(файл);
document.head.appendChild(ссылка);
});
</script>
<style>
@layer product {
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; }
.shell-topbar-burger { display: none; }
@media (max-width: 831.98px) { .shell-topbar-burger { display: inline-flex; } }
.sb-top { display: flex; align-items: center; gap: var(--kt-ai-space-3); padding: 2px 4px 12px; }
.sb-top img { width: 28px; height: 28px; border-radius: var(--kt-ai-radius-xl); flex-shrink: 0; }
.sb-top .nm { font-weight: var(--kt-ai-weight-semibold); font-size: var(--kt-ai-text-sm); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
.sb-bottom { margin-top: auto; display: flex; flex-direction: column; gap: 2px; padding-top: var(--kt-ai-space-4); }
.sb-bottom button { background: none; border: none; font: inherit; color: inherit; text-align: left; cursor: pointer; width: 100%; }
.sb-bottom .badge { margin-left: auto; font-family: var(--kt-ai-font-mono); font-size: var(--kt-ai-text-2xs); background: var(--kt-ai-primary-subtle); color: var(--kt-ai-primary); border-radius: var(--kt-ai-radius-full); padding: 0 6px; }
.sb-new { margin: 0 4px var(--kt-ai-space-5); }
.sb-new .kt-ai-btn { width: 100%; justify-content: center; }
.kt-ai-shell[data-rail="true"] .sb-new .kt-ai-btn { padding: 0; }
#view { width: 100%; max-width: var(--kt-ai-content-table); margin-inline: auto; padding: var(--kt-ai-stack-region) var(--kt-ai-stack-region) var(--kt-ai-space-11); }
@media (max-width: 639.98px) { #view { padding: var(--kt-ai-stack-group) var(--kt-ai-space-6) var(--kt-ai-space-10); } }
.page-hero { display: flex; align-items: flex-start; gap: var(--kt-ai-space-6); margin-bottom: var(--kt-ai-stack-group); }
.page-hero .ph-text { flex: 1; min-width: 0; }
.page-hero h1 { margin: 0; font-size: var(--kt-ai-text-2xl); font-weight: var(--kt-ai-weight-semibold); letter-spacing: -.01em; line-height: 1.15; }
.page-hero .sub { margin-top: var(--kt-ai-space-3); font-size: var(--kt-ai-text-md); color: var(--kt-ai-fg-muted); }
.page-hero .ph-action { flex-shrink: 0; }
@media (max-width: 639.98px) { .page-hero { flex-direction: column; gap: var(--kt-ai-space-5); } .page-hero h1 { font-size: var(--kt-ai-text-2xl); } }
#view > .kt-ai-kpi-strip { margin-bottom: var(--kt-ai-stack-block); }
.toolbar-row { display: flex; align-items: center; gap: var(--kt-ai-space-4); flex-wrap: wrap; padding: 0 0 var(--kt-ai-stack-block); }
.toolbar-row .spacer { flex: 1; }
.drawer-head { display: flex; align-items: flex-start; gap: var(--kt-ai-space-4); margin-bottom: var(--kt-ai-space-6); }
.drawer-head .t { flex: 1; }
.drawer-flags { display: flex; flex-direction: column; gap: var(--kt-ai-space-3); margin: var(--kt-ai-space-6) 0; }
.drawer-actions { display: flex; flex-direction: column; gap: var(--kt-ai-space-4); margin-top: var(--kt-ai-space-8); }
.kt-ai-card .drawer-actions { flex-direction: row-reverse; justify-content: flex-start; }
.kt-ai-card .drawer-actions .kt-ai-btn { width: auto; }
@media (max-width: 639.98px) { .kt-ai-card .drawer-actions { flex-direction: column; } }
.drawer-sec { padding: var(--kt-ai-space-6) 0; border-top: 1px solid var(--kt-ai-divider); }
.drawer-sec:first-of-type { border-top: none; padding-top: 0; }
.log-entry { display: flex; gap: var(--kt-ai-space-3); padding: var(--kt-ai-space-3) 0; font-size: var(--kt-ai-text-sm); border-bottom: 1px solid var(--kt-ai-divider); }
.log-entry:last-child { border-bottom: none; }
.log-time { font-family: var(--kt-ai-font-mono); font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-faint); min-width: 80px; }
.log-what { flex: 1; }
.log-who { font-size: var(--kt-ai-text-xs); color: var(--kt-ai-fg-muted); }
.empty-state { text-align: center; padding: var(--kt-ai-space-12) var(--kt-ai-space-6); color: var(--kt-ai-fg-muted); }
.empty-state .kt-icon { width: 48px; height: 48px; margin-bottom: var(--kt-ai-space-4); color: var(--kt-ai-fg-faint); }
.empty-state h3 { margin: 0 0 var(--kt-ai-space-2); font-size: var(--kt-ai-text-lg); color: var(--kt-ai-fg); }
.empty-state p { margin: 0; font-size: var(--kt-ai-text-sm); }
.conflict-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 1000; }
.conflict-overlay.on { display: flex; }
.conflict-card { background: var(--kt-ai-bg-elevated); border-radius: var(--kt-ai-radius-3xl); padding: var(--kt-ai-space-8); max-width: 480px; width: 90%; }
.conflict-card h3 { margin: 0 0 var(--kt-ai-space-4); font-size: var(--kt-ai-text-xl); }
.conflict-card p { margin: 0 0 var(--kt-ai-space-6); color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-sm); }
.conflict-actions { display: flex; gap: var(--kt-ai-space-3); justify-content: flex-end; }
}
</style>
</head>
<body class="kt-ai-app">
<script id="kt-app-config" type="application/json">
{
"archetype": "queue",
"product": { "name": "Журнал учёта заявок", "purpose": "Учёт внутренних заявок сотрудников", "entity": "заявка", "view": "operational" },
"user": { "name": "Сотрудник подразделения", "role": "Ответственный за обработку заявок" },
"kpis": [
{ "value": "0", "label": "Всего заявок", "hint": "активных записей" },
{ "value": "0", "label": "В работе", "hint": "текущие задачи" },
{ "value": "0", "label": "Выполнено", "hint": "закрытые заявки" }
],
"statuses": {
"new": { "label": "Новая", "tone": "info" },
"in_progress": { "label": "В работе", "tone": "warn" },
"done": { "label": "Выполнена", "tone": "ok" }
},
"scenario": {
"headline": "Управление заявкой",
"control": "Сохранить",
"reject": "Удалить",
"confirmText": "Заявка будет сохранена в журнале."
},
"table": {
"columns": [
{ "key": "person", "label": "ФИО", "type": "text" },
{ "key": "title", "label": "Описание заявки", "type": "text" },
{ "key": "deadline", "label": "Срок", "type": "date" },
{ "key": "status", "label": "Статус", "type": "status" }
],
"rows": [
{ "person": "Алиев А.", "title": "Настройка рабочего места", "deadline": "15.09.2026", "status": "new" },
{ "person": "Смирнова Е.", "title": "Закуп канцтоваров", "deadline": "12.09.2026", "status": "in_progress" },
{ "person": "Иванов П.", "title": "Ремонт принтера", "deadline": "10.09.2026", "status": "done" },
{ "person": "Ким Д.", "title": "Доступ к CRM", "deadline": "18.09.2026", "status": "new" },
{ "person": "Петрова О.", "title": "Командировка в Астану", "deadline": "20.09.2026", "status": "in_progress" }
]
},
"drawer": {
"titleKey": "title",
"fields": [
{ "key": "person", "label": "ФИО заявителя" },
{ "key": "deadline", "label": "Срок исполнения" },
{ "key": "status", "label": "Текущий статус" }
],
"flagsLabel": "Детали заявки"
},
"filterKeys": ["status"],
"search": { "placeholder": "Поиск по заявкам…", "shortcut": "Ctrl+K" }
}
</script>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" style="position:absolute;width:0;height:0;overflow:hidden">
<symbol id="kt-ai-logo" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="4"/></symbol>
<symbol id="home" viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></symbol>
<symbol id="table" viewBox="0 0 24 24"><path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"/></symbol>
<symbol id="file-text" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></symbol>
<symbol id="settings" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></symbol>
<symbol id="download" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></symbol>
<symbol id="plus" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></symbol>
<symbol id="trash" viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></symbol>
<symbol id="edit" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></symbol>
<symbol id="x" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></symbol>
<symbol id="check" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></symbol>
<symbol id="alert-circle" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></symbol>
<symbol id="calendar" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></symbol>
<symbol id="user" viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol>
<symbol id="sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></symbol>
<symbol id="moon" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></symbol>
</svg>
<div class="kt-ai-shell" data-rail="false">
<aside class="kt-ai-shell-sidebar">
<div class="sb-top">
<svg class="kt-icon"><use href="#kt-ai-logo"/></svg>
<span class="nm">Журнал заявок</span>
<button class="kt-ai-btn" data-variant="ghost" data-size="sm" id="themeToggle" title="Сменить тему">
<svg class="kt-icon"><use href="#sun"/></svg>
</button>
</div>
<div class="sb-new">
<button class="kt-ai-btn" data-variant="primary" id="createBtn">
<svg class="kt-icon"><use href="#plus"/></svg>
<span>Добавить заявку</span>
</button>
</div>
<nav class="kt-ai-nav">
<button class="kt-ai-nav-item on">
<svg class="kt-icon"><use href="#table"/></svg>
<span class="nav-label">Все заявки</span>
</button>
<button class="kt-ai-nav-item" id="logBtn">
<svg class="kt-icon"><use href="#file-text"/></svg>
<span class="nav-label">Журнал изменений</span>
</button>
</nav>
<div class="sb-bottom">
<div class="kt-ai-nav-item">
<svg class="kt-icon"><use href="#user"/></svg>
<span class="nav-label">Сотрудник</span>
</div>
</div>
</aside>
<main class="kt-ai-shell-main">
<div class="kt-ai-topbar">
<div class="topbar-breadcrumbs">
<div class="topbar-crumb">
<svg class="kt-icon"><use href="#table"/></svg>
<span>Журнал учёта заявок</span>
</div>
</div>
<div class="kt-ai-topbar-right">
<button class="kt-ai-btn" data-variant="ghost" id="exportBtn" title="Скачать в Excel">
<svg class="kt-icon"><use href="#download"/></svg>
<span>Экспорт</span>
</button>
</div>
</div>
<div id="view">
<div class="page-hero">
<div class="ph-text">
<h1>Журнал учёта заявок</h1>
<p class="sub">Управление внутренними заявками сотрудников</p>
</div>
</div>
<div class="kt-ai-kpi-strip" id="kpiStrip">
<div class="kt-ai-kpi-card"><div class="v" id="kpiTotal">0</div><div class="l">Всего</div></div>
<div class="kt-ai-kpi-card"><div class="v" id="kpiNew">0</div><div class="l">Новые</div></div>
<div class="kt-ai-kpi-card"><div class="v" id="kpiProgress">0</div><div class="l">В работе</div></div>
<div class="kt-ai-kpi-card"><div class="v" id="kpiDone">0</div><div class="l">Выполнено</div></div>
</div>
<div class="toolbar-row" id="toolbar">
<div class="kt-ai-chip-group" id="statusFilter"></div>
<div class="spacer"></div>
</div>
<div id="tableContainer"></div>
<div id="logContainer" style="display:none"></div>
</div>
</main>
</div>
<div class="kt-ai-drawer" id="drawer" data-open="false">
<div class="kt-ai-drawer-overlay"></div>
<aside class="kt-ai-drawer-panel">
<div class="drawer-head">
<div class="t">
<h2 id="drawerTitle" style="margin:0;font-size:var(--kt-ai-text-xl)"></h2>
</div>
<button class="kt-ai-btn" data-variant="ghost" id="closeDrawer">
<svg class="kt-icon"><use href="#x"/></svg>
</button>
</div>
<div id="drawerContent"></div>
<div class="drawer-actions" id="drawerActions"></div>
</aside>
</div>
<div class="kt-ai-modal" id="modal" data-open="false">
<div class="kt-ai-modal-overlay"></div>
<div class="kt-ai-modal-panel" style="max-width:480px">
<div class="drawer-head">
<h2 id="modalTitle" style="margin:0;font-size:var(--kt-ai-text-xl)">Заявка</h2>
<button class="kt-ai-btn" data-variant="ghost" id="closeModal">
<svg class="kt-icon"><use href="#x"/></svg>
</button>
</div>
<div style="display:flex;flex-direction:column;gap:var(--kt-ai-space-4)">
<div class="kt-ai-field">
<label>ФИО</label>
<input class="kt-ai-input" id="formPerson" required>
</div>
<div class="kt-ai-field">
<label>Описание заявки</label>
<input class="kt-ai-input" id="formTitle" required>
</div>
<div class="kt-ai-field">
<label>Срок</label>
<input class="kt-ai-input" type="date" id="formDeadline" required>
</div>
<div class="kt-ai-field">
<label>Статус</label>
<select class="kt-ai-select" id="formStatus">
<option value="new">Новая</option>
<option value="in_progress">В работе</option>
<option value="done">Выполнена</option>
</select>
</div>
</div>
<div class="drawer-actions" style="margin-top:var(--kt-ai-space-6)">
<button class="kt-ai-btn" data-variant="ghost" id="cancelModal">Отмена</button>
<button class="kt-ai-btn" data-variant="primary" id="saveBtn">Сохранить</button>
</div>
</div>
</div>
<div class="conflict-overlay" id="confirmOverlay">
<div class="conflict-card">
<h3>Подтверждение</h3>
<p id="confirmText">Вы уверены?</p>
<div class="conflict-actions">
<button class="kt-ai-btn" data-variant="ghost" id="confirmCancel">Отмена</button>
<button class="kt-ai-btn" data-variant="danger" id="confirmOk">Подтвердить</button>
</div>
</div>
</div>
<script>
(function() {
var CFG = JSON.parse(document.getElementById('kt-app-config').textContent);
var LS_KEY = 'journal_requests_v1';
var LOG_KEY = 'journal_log_v1';
var data = JSON.parse(localStorage.getItem(LS_KEY) || 'null');
var log = JSON.parse(localStorage.getItem(LOG_KEY) || 'null');
if (!data) {
data = CFG.table.rows.map(function(r, i) {
return { id: i + 1, person: r.person, title: r.title, deadline: r.deadline, status: r.status };
});
localStorage.setItem(LS_KEY, JSON.stringify(data));
}
if (!log) {
log = [];
localStorage.setItem(LOG_KEY, JSON.stringify(log));
}
var curFilter = 'all';
var searchQ = '';
var curRow = null;
var editingId = null;
var confirmCb = null;
function save() {
localStorage.setItem(LS_KEY, JSON.stringify(data));
localStorage.setItem(LOG_KEY, JSON.stringify(log));
render();
}
function addLog(action, who) {
log.unshift({
when: new Date().toISOString(),
what: action,
who: who || 'Сотрудник'
});
if (log.length > 100) log.pop();
}
function fmtDate(d) {
var dt = new Date(d);
var dd = String(dt.getDate()).padStart(2, '0');
var mm = String(dt.getMonth() + 1).padStart(2, '0');
var yyyy = dt.getFullYear();
return dd + '.' + mm + '.' + yyyy;
}
function fmtDateTime(iso) {
var dt = new Date(iso);
return fmtDate(iso) + ' ' + String(dt.getHours()).padStart(2,'0') + ':' + String(dt.getMinutes()).padStart(2,'0');
}
function getStatusCfg(key) {
return CFG.statuses[key] || { label: key, tone: 'info' };
}
function renderKpis() {
var total = data.length;
var newC = data.filter(function(r){ return r.status === 'new'; }).length;
var prog = data.filter(function(r){ return r.status === 'in_progress'; }).length;
var done = data.filter(function(r){ return r.status === 'done'; }).length;
document.getElementById('kpiTotal').textContent = total;
document.getElementById('kpiNew').textContent = newC;
document.getElementById('kpiProgress').textContent = prog;
document.getElementById('kpiDone').textContent = done;
}
function renderFilters() {
var cont = document.getElementById('statusFilter');
var opts = [
{ key: 'all', label: 'Все' }
].concat(Object.keys(CFG.statuses).map(function(k){
return { key: k, label: CFG.statuses[k].label };
}));
cont.innerHTML = opts.map(function(o){
var on = curFilter === o.key;
return '<button class="kt-ai-chip' + (on ? ' on' : '') + '" data-filter="' + o.key + '">' + o.label + '</button>';
}).join('');
}
function getFiltered() {
var rows = data.slice();
if (curFilter !== 'all') {
rows = rows.filter(function(r){ return r.status === curFilter; });
}
if (searchQ.trim()) {
var q = searchQ.toLowerCase();
rows = rows.filter(function(r){
return r.person.toLowerCase().indexOf(q) >= 0 ||
r.title.toLowerCase().indexOf(q) >= 0;
});
}
return rows;
}
function renderTable() {
var cont = document.getElementById('tableContainer');
var rows = getFiltered();
if (!rows.length) {
cont.innerHTML = '<div class="empty-state"><svg class="kt-icon"><use href="#table"/></svg><h3>Нет заявок</h3><p>Нажмите «Добавить заявку», чтобы создать первую запись</p></div>';
return;
}
var html = '<div class="kt-ai-card"><table class="kt-ai-table"><thead><tr>';
CFG.table.columns.forEach(function(col){
html += '<th>' + col.label + '</th>';
});
html += '<th style="width:80px"></th></tr></thead><tbody>';
rows.forEach(function(r){
var scfg = getStatusCfg(r.status);
html += '<tr data-id="' + r.id + '">';
CFG.table.columns.forEach(function(col){
var v = r[col.key];
if (col.type === 'status') {
html += '<td><span class="kt-ai-chip" data-tone="' + scfg.tone + '">' + scfg.label + '</span></td>';
} else if (col.type === 'date') {
html += '<td>' + v + '</td>';
} else {
html += '<td>' + v + '</td>';
}
});
html += '<td class="row-act"><div class="row-kebab"><button class="kt-ai-btn" data-variant="ghost" data-size="sm" title="Редактировать" onclick="event.stopPropagation();editRow(' + r.id + ')"><svg class="kt-icon"><use href="#edit"/></svg></button><button class="kt-ai-btn" data-variant="ghost" data-size="sm" title="Удалить" onclick="event.stopPropagation();askDelete(' + r.id + ')"><svg class="kt-icon"><use href="#trash"/></svg></button></div></td>';
html += '</tr>';
});
html += '</tbody></table></div>';
cont.innerHTML = html;
}
function renderLog() {
var cont = document.getElementById('logContainer');
if (!log.length) {
cont.innerHTML = '<div class="empty-state"><svg class="kt-icon"><use href="#file-text"/></svg><h3>Журнал пуст</h3><p>Здесь будет история всех изменений</p></div>';
return;
}
var html = '<div class="kt-ai-card"><div style="padding:var(--kt-ai-space-4)">';
log.forEach(function(e){
html += '<div class="log-entry"><div class="log-time">' + fmtDateTime(e.when) + '</div><div class="log-what">' + e.what + '</div><div class="log-who">' + e.who + '</div></div>';
});
html += '</div></div>';
cont.innerHTML = html;
}
function openDrawer(row) {
curRow = row;
var scfg = getStatusCfg(row.status);
document.getElementById('drawerTitle').textContent = row.title;
var html = '<div class="drawer-sec">';
CFG.drawer.fields.forEach(function(f){
var v = row[f.key];
if (f.key === 'status') {
html += '<div class="kt-ai-field"><label>' + f.label + '</label><div><span class="kt-ai-chip" data-tone="' + scfg.tone + '">' + v + '</span></div></div>';
} else {
html += '<div class="kt-ai-field"><label>' + f.label + '</label><div>' + v + '</div></div>';
}
});
html += '</div><div class="drawer-sec"><div class="kt-ai-field"><label>' + CFG.drawer.flagsLabel + '</label><div class="drawer-flags">';
html += '<div class="agent-signal"><svg class="kt-icon" style="color:var(--kt-ai-primary)"><use href="#check"/></svg><div>Заявка зарегистрирована</div></div>';
html += '<div class="agent-signal"><svg class="kt-icon" style="color:var(--kt-ai-primary)"><use href="#calendar"/></svg><div>Срок: ' + row.deadline + '</div></div>';
html += '</div></div></div>';
document.getElementById('drawerContent').innerHTML = html;
var actions = '<button class="kt-ai-btn" data-variant="ghost" onclick="closeDrawer()">Закрыть</button>';
actions += '<button class="kt-ai-btn" data-variant="primary" onclick="editRow(' + row.id + ');closeDrawer()">Редактировать</button>';
document.getElementById('drawerActions').innerHTML = actions;
document.getElementById('drawer').setAttribute('data-open', 'true');
}
function closeDrawer() {
document.getElementById('drawer').setAttribute('data-open', 'false');
curRow = null;
}
function openModal(id) {
editingId = id;
var row = data.find(function(r){ return r.id === id; });
document.getElementById('modalTitle').textContent = row ? 'Редактирование заявки' : 'Новая заявка';
if (row) {
document.getElementById('formPerson').value = row.person;
document.getElementById('formTitle').value = row.title;
document.getElementById('formDeadline').value = isoDate(row.deadline);
document.getElementById('formStatus').value = row.status;
} else {
document.getElementById('formPerson').value = '';
document.getElementById('formTitle').value = '';
document.getElementById('formDeadline').value = '';
document.getElementById('formStatus').value = 'new';
}
document.getElementById('modal').setAttribute('data-open', 'true');
}
function closeModal() {
document.getElementById('modal').setAttribute('data-open', 'false');
editingId = null;
}
function isoDate(ddmmyyyy) {
if (!ddmmyyyy || ddmmyyyy.length !== 10) return '';
var p = ddmmyyyy.split('.');
return p[2] + '-' + p[1] + '-' + p[0];
}
function saveForm() {
var person = document.getElementById('formPerson').value.trim();
var title = document.getElementById('formTitle').value.trim();
var deadline = document.getElementById('formDeadline').value;
var status = document.getElementById('formStatus').value;
if (!person || !title || !deadline) {
alert('Заполните все поля');
return;
}
var dObj = new Date(deadline);
var dStr = String(dObj.getDate()).padStart(2,'0') + '.' + String(dObj.getMonth()+1).padStart(2,'0') + '.' + dObj.getFullYear();
if (editingId) {
var idx = data.findIndex(function(r){ return r.id === editingId; });
if (idx >= 0) {
var old = data[idx];
data[idx] = { id: editingId, person: person, title: title, deadline: dStr, status: status };
addLog('Изменена заявка #' + editingId + ': ' + old.title + ' → ' + title);
}
} else {
var newId = data.length ? Math.max.apply(null, data.map(function(r){ return r.id; })) + 1 : 1;
data.push({ id: newId, person: person, title: title, deadline: dStr, status: status });
addLog('Добавлена заявка #' + newId + ': ' + title);
}
closeModal();
save();
}
function editRow(id) {
openModal(id);
}
function askDelete(id) {
var row = data.find(function(r){ return r.id === id; });
if (!row) return;
document.getElementById('confirmText').textContent = 'Удалить заявку "' + row.title + '"? Это действие будет записано в журнал.';
confirmCb = function() {
var idx = data.findIndex(function(r){ return r.id === id; });
if (idx >= 0) {
var del = data.splice(idx, 1)[0];
addLog('Удалена заявка #' + id + ': ' + del.title);
save();
}
document.getElementById('confirmOverlay').classList.remove('on');
confirmCb = null;
};
document.getElementById('confirmOverlay').classList.add('on');
}
function exportCsv() {
var rows = getFiltered();
var head = ['ФИО', 'Описание заявки', 'Срок', 'Статус'];
var body = rows.map(function(r){
return [r.person, r.title, r.deadline, getStatusCfg(r.status).label]
.map(function(v){ return '"' + String(v).replace(/"/g, '""') + '"'; })
.join(';');
});
var csv = '\uFEFF' + [head.join(';')].concat(body).join('\n');
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'zayavki_' + new Date().toISOString().slice(0,10) + '.csv';
a.click();
URL.revokeObjectURL(url);
}
function render() {
renderKpis();
renderFilters();
renderTable();
if (document.getElementById('logContainer').style.display !== 'none') {
renderLog();
}
}
document.getElementById('statusFilter').addEventListener('click', function(e){
if (e.target.classList.contains('kt-ai-chip')) {
curFilter = e.target.getAttribute('data-filter');
render();
}
});
document.getElementById('createBtn').addEventListener('click', function(){
openModal(null);
});
document.getElementById('closeDrawer').addEventListener('click', closeDrawer);
document.getElementById('drawer').querySelector('.kt-ai-drawer-overlay').addEventListener('click', closeDrawer);
document.getElementById('closeModal').addEventListener('click', closeModal);
document.getElementById('cancelModal').addEventListener('click', closeModal);
document.getElementById('modal').querySelector('.kt-ai-modal-overlay').addEventListener('click', closeModal);
document.getElementById('saveBtn').addEventListener('click', saveForm);
document.getElementById('confirmCancel').addEventListener('click', function(){
document.getElementById('confirmOverlay').classList.remove('on');
confirmCb = null;
});
document.getElementById('confirmOk').addEventListener('click', function(){
if (confirmCb) confirmCb();
});
document.getElementById('logBtn').addEventListener('click', function(){
var tbl = document.getElementById('tableContainer');
var logC = document.getElementById('logContainer');
if (logC.style.display === 'none') {
tbl.style.display = 'none';
logC.style.display = 'block';
renderLog();
this.classList.add('on');
} else {
tbl.style.display = 'block';
logC.style.display = 'none';
this.classList.remove('on');
}
});
document.getElementById('exportBtn').addEventListener('click', exportCsv);
document.getElementById('themeToggle').addEventListener('click', function(){
var html = document.documentElement;
var cur = html.getAttribute('data-theme');
html.setAttribute('data-theme', cur === 'light' ? 'dark' : 'light');
this.innerHTML = '<svg class="kt-icon"><use href="' + (cur === 'light' ? '#moon' : '#sun') + '"/></svg>';
});
document.addEventListener('keydown', function(e){
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
var inp = document.querySelector('.kt-ai-search-input');
if (inp) inp.focus();
}
});
var tableCont = document.getElementById('tableContainer');
tableCont.addEventListener('click', function(e){
var tr = e.target.closest('tr[data-id]');
if (tr && !e.target.closest('button')) {
var id = parseInt(tr.getAttribute('data-id'));
var row = data.find(function(r){ return r.id === id; });
if (row) openDrawer(row);
}
});
var searchField = document.querySelector('.kt-ai-search-field');
if (searchField) {
var inp = searchField.querySelector('input');
if (inp) {
inp.placeholder = CFG.search.placeholder;
inp.addEventListener('input', function(){
searchQ = this.value;
renderTable();
});
}
}
render();
})();
</script>
<link rel="stylesheet" href="./design-system/kt-ai-fonts.css">
<link rel="stylesheet" href="./design-system/kt-ai-page.css">
<script src="./design-system/kt-ai-components.js"></script>
</body>
</html>