intrabot/web/templates/dashboard.html
2026-02-24 15:15:23 +03:00

689 lines
30 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.0">
<title>IntradBot Dashboard</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:#0d1117;--surface:#161b22;--surface2:#21262d;--border:#30363d;
--accent:#388bfd;--accent-h:#58a6ff;--success:#3fb950;--warn:#d29922;
--danger:#da3633;--text:#c9d1d9;--muted:#8b949e;--r:6px;
--font:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
--mono:'SFMono-Regular',Consolas,monospace;
}
body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:14px;display:flex;height:100vh;overflow:hidden}
/* ── Sidebar ── */
.sidebar{width:220px;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}
.brand{padding:18px 16px 16px;display:flex;align-items:center;gap:8px;font-size:15px;font-weight:700;border-bottom:1px solid var(--border)}
.nav{padding:8px 0;flex:1}
.nav-item{display:flex;align-items:center;gap:10px;padding:10px 16px;cursor:pointer;color:var(--muted);border-left:3px solid transparent;font-size:13px;transition:.15s}
.nav-item:hover{background:var(--surface2);color:var(--text)}
.nav-item.active{color:var(--accent-h);border-left-color:var(--accent);background:rgba(56,139,253,.1)}
.nav-icon{width:18px;text-align:center}
.sidebar-foot{padding:14px 16px;border-top:1px solid var(--border)}
.status-row{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}
.dot{width:8px;height:8px;border-radius:50%;background:var(--muted)}
.dot.on{background:var(--success);box-shadow:0 0 6px var(--success)}
.dot.off{background:var(--danger)}
.counter{margin-left:auto;background:var(--accent);color:#fff;border-radius:10px;padding:1px 7px;font-size:11px;font-weight:600;display:none}
.counter.show{display:inline}
/* ── Main ── */
.main{flex:1;display:flex;flex-direction:column;overflow:hidden}
.tab{display:none;flex:1;flex-direction:column;overflow:hidden}
.tab.active{display:flex}
.tab-head{padding:14px 24px;border-bottom:1px solid var(--border);background:var(--surface);display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
.tab-head h2{font-size:15px;font-weight:600}
.tab-body{flex:1;overflow-y:auto;padding:16px 24px}
/* ── Buttons ── */
.btn{padding:6px 14px;border:1px solid var(--border);background:var(--surface2);color:var(--text);border-radius:var(--r);cursor:pointer;font-size:13px;font-family:var(--font);transition:.15s;display:inline-flex;align-items:center;gap:6px}
.btn:hover{border-color:var(--accent);color:var(--accent-h)}
.btn:disabled{opacity:.4;cursor:not-allowed}
.btn-primary{background:var(--accent);border-color:var(--accent);color:#fff}
.btn-primary:hover{background:var(--accent-h);border-color:var(--accent-h);color:#fff}
.btn-danger{background:var(--danger);border-color:var(--danger);color:#fff}
.btn-danger:hover{opacity:.85;color:#fff}
.btn-sm{padding:4px 10px;font-size:12px}
.btn-group{display:flex;gap:8px}
/* ── Inputs ── */
input,select,textarea{background:var(--surface2);border:1px solid var(--border);color:var(--text);padding:8px 12px;border-radius:var(--r);font-family:var(--font);font-size:13px;width:100%;transition:.15s}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--accent)}
textarea{resize:vertical;min-height:80px}
select option{background:var(--surface2)}
.form-row{display:flex;gap:8px;margin-bottom:12px}
.form-row input,.form-row select{flex:1}
.form-group{margin-bottom:14px}
.form-label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);margin-bottom:5px}
/* ── Webhook cards ── */
.feed{display:flex;flex-direction:column;gap:10px}
.wcard{border:1px solid var(--border);border-radius:var(--r);overflow:hidden;background:var(--surface)}
.wcard.new{border-left:3px solid var(--success)}
.wcard.updated{border-left:3px solid var(--accent)}
.wcard-head{padding:12px 16px;display:flex;align-items:center;gap:10px;cursor:pointer;user-select:none}
.wcard-head:hover{background:var(--surface2)}
.badge{padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;text-transform:uppercase}
.badge-new{background:rgba(63,185,80,.15);color:var(--success)}
.badge-upd{background:rgba(56,139,253,.15);color:var(--accent-h)}
.wcard-num{font-weight:700;font-size:14px}
.wcard-name{color:var(--muted);font-size:13px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.wcard-time{color:var(--muted);font-size:12px;flex-shrink:0}
.chevron{color:var(--muted);font-size:11px;transition:.2s;flex-shrink:0}
.wcard.open .chevron{transform:rotate(180deg)}
.wcard-body{display:none;border-top:1px solid var(--border)}
.wcard.open .wcard-body{display:block}
.wsec{border-bottom:1px solid var(--border)}
.wsec:last-child{border-bottom:none}
.wsec-head{padding:8px 16px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);cursor:pointer;display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.02)}
.wsec-head:hover{color:var(--text)}
.wsec-body{padding:12px 16px;display:none}
.wsec.open .wsec-body{display:block}
.wsec.open .wsec-chevron{transform:rotate(90deg)}
.wsec-chevron{transition:.15s}
.change-item{padding:4px 0;border-bottom:1px solid rgba(255,255,255,.04);font-size:13px}
.change-item:last-child{border-bottom:none}
.comment-item{background:rgba(56,139,253,.06);border-left:2px solid var(--accent);padding:6px 10px;margin:3px 0;border-radius:0 var(--r) var(--r) 0;font-size:13px}
.formatted-msg{font-family:var(--mono);font-size:12px;background:var(--bg);border:1px solid var(--border);border-radius:var(--r);padding:12px;white-space:pre-wrap;word-break:break-all;max-height:300px;overflow-y:auto}
/* ── Tasks ── */
.task-card{border:1px solid var(--border);border-radius:var(--r);padding:20px;background:var(--surface);margin-bottom:12px;cursor:pointer;transition:.15s}
.task-card:hover{border-color:var(--accent)}
.task-card.expanded{border-color:var(--accent)}
.task-title{font-size:15px;font-weight:600;margin-bottom:10px;display:flex;align-items:flex-start;gap:10px}
.task-num{color:var(--muted);font-size:13px;font-weight:400;flex-shrink:0}
.task-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px;margin-bottom:12px}
.meta-item{background:var(--surface2);border-radius:var(--r);padding:10px 12px}
.meta-label{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);margin-bottom:3px}
.meta-val{font-size:13px;font-weight:500}
.tasks-list{display:flex;flex-direction:column;gap:8px}
.status-badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px}
.s-open{background:rgba(63,185,80,.15);color:var(--success)}
.s-work{background:rgba(56,139,253,.15);color:var(--accent-h)}
.s-done{background:rgba(139,148,158,.15);color:var(--muted)}
.s-wait{background:rgba(210,153,34,.15);color:var(--warn)}
.task-desc{font-size:13px;color:var(--muted);padding:10px;background:var(--bg);border-radius:var(--r);margin-top:8px;white-space:pre-wrap}
/* ── API form ── */
.api-layout{display:grid;grid-template-columns:360px 1fr;gap:20px;align-items:start}
.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:20px}
.panel-title{font-size:13px;font-weight:600;margin-bottom:16px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
.preview-url{font-family:var(--mono);font-size:12px;padding:8px 10px;background:var(--bg);border-radius:var(--r);margin-bottom:10px;color:var(--warn);word-break:break-all}
pre{font-family:var(--mono);font-size:12px;background:var(--bg);border:1px solid var(--border);border-radius:var(--r);padding:12px;overflow-x:auto;white-space:pre-wrap;word-break:break-all}
.result-block{margin-top:12px;padding:10px 14px;border-radius:var(--r);font-size:13px}
.result-ok{background:rgba(63,185,80,.1);border:1px solid var(--success);color:var(--success)}
.result-err{background:rgba(218,54,51,.1);border:1px solid var(--danger);color:var(--danger)}
.result-info{background:rgba(56,139,253,.1);border:1px solid var(--accent);color:var(--accent-h)}
.hidden{display:none!important}
.empty{text-align:center;color:var(--muted);padding:60px 20px;font-size:14px}
.spin{display:inline-block;width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
::-webkit-scrollbar-thumb:hover{background:var(--muted)}
</style>
</head>
<body>
<aside class="sidebar">
<div class="brand">🤖 IntradBot</div>
<nav class="nav">
<div class="nav-item active" data-tab="webhooks" onclick="switchTab('webhooks',this)">
<span class="nav-icon">🔔</span> Вебхуки
<span class="counter" id="wh-counter">0</span>
</div>
<div class="nav-item" data-tab="tasks" onclick="switchTab('tasks',this)">
<span class="nav-icon">📋</span> Заявки
</div>
<div class="nav-item" data-tab="api" onclick="switchTab('api',this)">
<span class="nav-icon">⚙️</span> API
</div>
</nav>
<div class="sidebar-foot">
<div class="status-row">
<div class="dot" id="dot"></div>
<span id="dot-label">Подключение...</span>
</div>
</div>
</aside>
<main class="main">
<!-- ── Вебхуки ── -->
<section class="tab active" id="tab-webhooks">
<div class="tab-head">
<h2>Вебхуки в реальном времени</h2>
<div class="btn-group">
<button class="btn btn-sm" onclick="clearFeed()">Очистить</button>
</div>
</div>
<div class="tab-body">
<div class="feed" id="feed">
<div class="empty" id="feed-empty">Ожидание вебхуков…</div>
</div>
</div>
</section>
<!-- ── Заявки ── -->
<section class="tab" id="tab-tasks">
<div class="tab-head">
<h2>Заявки</h2>
<div class="btn-group">
<button class="btn btn-sm btn-primary" onclick="loadActive()">Загрузить активные</button>
</div>
</div>
<div class="tab-body">
<div class="form-row" style="margin-bottom:16px">
<input type="number" id="search-num" placeholder="Номер заявки" onkeydown="if(event.key==='Enter')searchTask()">
<button class="btn btn-primary" onclick="searchTask()" style="width:auto;flex-shrink:0">Найти</button>
</div>
<div id="tasks-result">
<div class="empty">Введите номер заявки или загрузите активные</div>
</div>
</div>
</section>
<!-- ── API ── -->
<section class="tab" id="tab-api">
<div class="tab-head">
<h2>API — изменение заявки</h2>
</div>
<div class="tab-body">
<div class="api-layout">
<!-- Форма -->
<div class="panel">
<div class="panel-title">Параметры</div>
<div class="form-group">
<label class="form-label">Номер заявки</label>
<input type="number" id="api-num" placeholder="272">
</div>
<div class="form-group">
<label class="form-label">Действие</label>
<select id="api-action" onchange="renderActionFields()">
<option value="status">Сменить статус</option>
<option value="comment">Добавить комментарий</option>
<option value="executor">Назначить исполнителя</option>
<option value="priority">Изменить приоритет</option>
<option value="resolution_date">Плановый срок</option>
<option value="tags">Теги (через запятую)</option>
<option value="custom">Произвольный блок JSON</option>
</select>
</div>
<div id="action-fields"></div>
<button class="btn btn-primary" style="width:100%" onclick="previewRequest()">
Предпросмотр запроса →
</button>
</div>
<!-- Предпросмотр + результат -->
<div>
<div class="panel" id="preview-panel" style="margin-bottom:16px">
<div class="panel-title">Запрос будет отправлен</div>
<div class="preview-url" id="preview-url"></div>
<pre id="preview-json">Нажмите «Предпросмотр запроса»</pre>
<div class="btn-group" style="margin-top:14px">
<button class="btn btn-danger" id="exec-btn" onclick="executeRequest()" disabled>
✓ Выполнить
</button>
<button class="btn" onclick="resetPreview()">Отмена</button>
</div>
</div>
<div id="api-result"></div>
</div>
</div>
</div>
</section>
</main>
<script>
// ═══════════════════════════════════════════════════════════════
// GLOBALS
// ═══════════════════════════════════════════════════════════════
let whCount = 0;
let _pendingBlocks = null;
const STATUSES = {
68046:'Открыта', 68048:'Требует уточнения',
68051:'В работе', 68045:'Выполнена'
};
const PRIORITIES = {
29749:'Наивысший', 29598:'Критический',
29597:'Высокий', 29596:'Низкий'
};
// ═══════════════════════════════════════════════════════════════
// TABS
// ═══════════════════════════════════════════════════════════════
function switchTab(id, el) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.getElementById('tab-' + id).classList.add('active');
el.classList.add('active');
if (id === 'webhooks') {
whCount = 0;
const c = document.getElementById('wh-counter');
c.textContent = '0'; c.classList.remove('show');
}
if (id === 'api') renderActionFields();
}
// ═══════════════════════════════════════════════════════════════
// SSE — вебхуки в реальном времени
// ═══════════════════════════════════════════════════════════════
const es = new EventSource('/stream');
es.onopen = () => setStatus(true);
es.onerror = () => setStatus(false);
es.onmessage = e => {
const d = JSON.parse(e.data);
if (d.type === 'ping') return;
if (d.type === 'webhook') addWebhookCard(d);
};
function setStatus(on) {
document.getElementById('dot').className = 'dot ' + (on ? 'on' : 'off');
document.getElementById('dot-label').textContent = on ? 'Подключено' : 'Нет связи';
}
function addWebhookCard(d) {
const feed = document.getElementById('feed');
document.getElementById('feed-empty')?.remove();
const isNew = d.operation === 'Created';
const cls = isNew ? 'new' : 'updated';
const badge = isNew
? '<span class="badge badge-new">Новая</span>'
: '<span class="badge badge-upd">Обновлена</span>';
const changes = (d.parsed?.changes || []);
const comments = (d.parsed?.comments || []);
// Секция: Изменения
let changesHtml = '';
if (changes.length || comments.length) {
const items = [
...changes.map(c => `<div class="change-item">${esc(c)}</div>`),
...comments.map(c => `<div class="comment-item">${esc(c)}</div>`),
].join('');
changesHtml = makeSec('📝 Изменения / комментарии', items, true);
}
// Секция: Форматированное сообщение
const fmtHtml = makeSec('💬 Сообщение в канале',
`<div class="formatted-msg">${esc(d.formatted || '')}</div>`);
// Секция: Сырые данные
const rawHtml = makeSec('{ } Сырые данные',
`<pre>${esc(JSON.stringify(d.raw, null, 2))}</pre>`);
const card = document.createElement('div');
card.className = `wcard ${cls}`;
card.innerHTML = `
<div class="wcard-head" onclick="toggleCard(this)">
${badge}
<span class="wcard-num">#${esc(d.task_number)}</span>
<span class="wcard-name">${esc(getTaskName(d.raw))}</span>
<span class="wcard-time">${esc(d.timestamp)}</span>
<span class="chevron">▼</span>
</div>
<div class="wcard-body">
${changesHtml}
${fmtHtml}
${rawHtml}
</div>`;
feed.insertBefore(card, feed.firstChild);
// Счётчик на вкладке
if (!document.querySelector('.nav-item[data-tab="webhooks"]').classList.contains('active')) {
whCount++;
const c = document.getElementById('wh-counter');
c.textContent = whCount; c.classList.add('show');
}
}
function makeSec(title, body, openByDefault = false) {
const cls = openByDefault ? 'wsec open' : 'wsec';
return `
<div class="${cls}">
<div class="wsec-head" onclick="toggleSec(this)">
<span class="wsec-chevron">▶</span> ${title}
</div>
<div class="wsec-body">${body}</div>
</div>`;
}
function toggleCard(head) {
head.closest('.wcard').classList.toggle('open');
}
function toggleSec(head) {
head.closest('.wsec').classList.toggle('open');
}
function getTaskName(raw) {
try {
const n = raw.Fields?.name;
if (typeof n === 'object') return n.Value || '';
if (typeof n === 'string') return JSON.parse(n).Value || '';
} catch {}
return '';
}
function clearFeed() {
const feed = document.getElementById('feed');
feed.innerHTML = '<div class="empty" id="feed-empty">Ожидание вебхуков…</div>';
whCount = 0;
const c = document.getElementById('wh-counter');
c.textContent = '0'; c.classList.remove('show');
}
// ═══════════════════════════════════════════════════════════════
// ЗАЯВКИ
// ═══════════════════════════════════════════════════════════════
async function searchTask() {
const num = document.getElementById('search-num').value.trim();
if (!num) return;
const el = document.getElementById('tasks-result');
el.innerHTML = '<div class="empty"><span class="spin"></span> Загрузка…</div>';
try {
const r = await fetch(`/api/tasks/search?number=${num}`);
const data = await r.json();
if (data.error) { el.innerHTML = `<div class="empty">${esc(data.error)}</div>`; return; }
el.innerHTML = renderTaskCard(data);
} catch(e) {
el.innerHTML = `<div class="empty">Ошибка: ${esc(e.message)}</div>`;
}
}
async function loadActive() {
const el = document.getElementById('tasks-result');
el.innerHTML = '<div class="empty"><span class="spin"></span> Загрузка активных заявок…</div>';
try {
const r = await fetch('/api/tasks/active');
const tasks = await r.json();
if (!tasks.length) { el.innerHTML = '<div class="empty">Активных заявок нет</div>'; return; }
el.innerHTML = `<div style="margin-bottom:8px;color:var(--muted);font-size:12px">Найдено: ${tasks.length}</div>` +
'<div class="tasks-list">' + tasks.map(t => renderTaskCard(t)).join('') + '</div>';
} catch(e) {
el.innerHTML = `<div class="empty">Ошибка: ${esc(e.message)}</div>`;
}
}
function renderTaskCard(t) {
const statusId = t.status;
const statusName= resolveId(STATUSES, statusId, 'statuses');
const priId = t.priority;
const priName = resolveId(PRIORITIES, priId, 'priorities');
const sCls = statusCls(statusId);
const meta = [
['Статус', `<span class="status-badge ${sCls}">${esc(statusName)}</span>`],
['Приоритет', esc(priName)],
['Исполнитель',esc(t.executorname || t.executor || '—')],
['Группа', esc(t.executorgroupname || t.executorgroup || '—')],
['Сервис', esc(t.servicename || t.service || '—')],
['Тип', esc(t.tasktypename || t.tasktype || '—')],
['Создана', esc(fmtDate(t.createdat))],
['Обновлена', esc(fmtDate(t.updatedat))],
['Создатель', esc(t.createdby || '—')],
['Плановый срок', esc(fmtDate(t.resolutiondateplan))],
].map(([l,v]) => `<div class="meta-item"><div class="meta-label">${l}</div><div class="meta-val">${v}</div></div>`).join('');
const desc = t.description
? `<div class="task-desc">${esc(t.description)}</div>` : '';
return `
<div class="task-card">
<div class="task-title">
<span class="task-num">#${t.tasknumber}</span>
${esc(t.name || '—')}
</div>
<div class="task-grid">${meta}</div>
${desc}
<div style="margin-top:10px">
<button class="btn btn-sm" onclick="fillApiForm(${t.tasknumber})">⚙️ Изменить через API</button>
<a href="https://cleverparking.intradesk.ru/task/list/${t.tasknumber}" target="_blank">
<button class="btn btn-sm" style="margin-left:6px">🔗 Открыть в системе</button>
</a>
</div>
</div>`;
}
function fillApiForm(num) {
switchTab('api', document.querySelector('.nav-item[data-tab="api"]'));
document.getElementById('api-num').value = num;
}
function statusCls(id) {
if (id === 68046) return 's-open';
if (id === 68051) return 's-work';
if (id === 68045) return 's-done';
if (id === 68048) return 's-wait';
return '';
}
function resolveId(dict, id, entity) {
if (dict[id]) return dict[id];
return id ? String(id) : '—';
}
function fmtDate(s) {
if (!s) return '—';
try {
return new Date(s).toLocaleString('ru-RU', {
day:'2-digit', month:'2-digit', year:'numeric',
hour:'2-digit', minute:'2-digit'
});
} catch { return s; }
}
// ═══════════════════════════════════════════════════════════════
// API — форма изменения заявки
// ═══════════════════════════════════════════════════════════════
async function renderActionFields() {
const action = document.getElementById('api-action').value;
const el = document.getElementById('action-fields');
resetPreview();
if (action === 'status') {
let opts = Object.entries(STATUSES).map(([id,n]) =>
`<option value="${id}">${n}</option>`).join('');
try {
const r = await fetch('/api/ref/statuses');
const items = await r.json();
if (items.length) opts = items.map(i => `<option value="${i.id}">${esc(i.name)}</option>`).join('');
} catch {}
el.innerHTML = `
<div class="form-group">
<label class="form-label">Новый статус</label>
<select id="f-status">${opts}</select>
</div>`;
}
else if (action === 'comment') {
el.innerHTML = `
<div class="form-group">
<label class="form-label">Текст комментария</label>
<textarea id="f-comment" placeholder="Принято в работу…"></textarea>
</div>`;
}
else if (action === 'executor') {
el.innerHTML = `
<div class="form-group">
<label class="form-label">ID группы (groupid)</label>
<input type="number" id="f-groupid" placeholder="243025">
</div>
<div class="form-group">
<label class="form-label">ID сотрудника (userid, необязательно)</label>
<input type="number" id="f-userid" placeholder="109747">
</div>`;
}
else if (action === 'priority') {
let opts = Object.entries(PRIORITIES).map(([id,n]) =>
`<option value="${id}">${n}</option>`).join('');
try {
const r = await fetch('/api/ref/priorities');
const items = await r.json();
if (items.length) opts = items.map(i => `<option value="${i.id}">${esc(i.name)}</option>`).join('');
} catch {}
el.innerHTML = `
<div class="form-group">
<label class="form-label">Приоритет</label>
<select id="f-priority">${opts}</select>
</div>`;
}
else if (action === 'resolution_date') {
const now = new Date();
now.setDate(now.getDate() + 1);
const iso = now.toISOString().slice(0,16);
el.innerHTML = `
<div class="form-group">
<label class="form-label">Дата и время (локальное)</label>
<input type="datetime-local" id="f-date" value="${iso}">
</div>`;
}
else if (action === 'tags') {
el.innerHTML = `
<div class="form-group">
<label class="form-label">ID тегов через запятую</label>
<input id="f-tags" placeholder="32887, 32893">
</div>`;
}
else if (action === 'custom') {
el.innerHTML = `
<div class="form-group">
<label class="form-label">Название блока</label>
<input id="f-block-name" placeholder="status">
</div>
<div class="form-group">
<label class="form-label">Значение (JSON)</label>
<textarea id="f-block-val" placeholder='{"value": 68051}'></textarea>
</div>`;
}
}
function buildBlocks() {
const action = document.getElementById('api-action').value;
const jv = v => JSON.stringify({value: v});
if (action === 'status')
return {status: jv(parseInt(document.getElementById('f-status').value))};
if (action === 'comment')
return {comment: jv(document.getElementById('f-comment').value)};
if (action === 'executor') {
const v = {};
const gid = document.getElementById('f-groupid').value;
const uid = document.getElementById('f-userid').value;
if (gid) v.groupid = parseInt(gid);
if (uid) v.userid = parseInt(uid);
return {executor: jv(v)};
}
if (action === 'priority')
return {priority: jv(parseInt(document.getElementById('f-priority').value))};
if (action === 'resolution_date') {
const dt = new Date(document.getElementById('f-date').value);
return {resolutiondateplan: jv(dt.toISOString())};
}
if (action === 'tags') {
const ids = document.getElementById('f-tags').value
.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n));
return {tags: jv(ids)};
}
if (action === 'custom') {
const name = document.getElementById('f-block-name').value.trim();
const val = document.getElementById('f-block-val').value.trim();
if (!name) return null;
return {[name]: val};
}
return null;
}
async function previewRequest() {
const num = parseInt(document.getElementById('api-num').value);
if (!num) { showResult('error', 'Укажите номер заявки'); return; }
const blocks = buildBlocks();
if (!blocks) { showResult('error', 'Заполните поля действия'); return; }
try {
const r = await fetch('/api/task/preview', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({number: num, blocks}),
});
const d = await r.json();
document.getElementById('preview-url').textContent = `${d.method} ${d.url}`;
document.getElementById('preview-json').textContent = JSON.stringify(d.body, null, 2);
document.getElementById('exec-btn').disabled = false;
document.getElementById('api-result').innerHTML = '';
_pendingBlocks = blocks;
} catch(e) {
showResult('error', e.message);
}
}
async function executeRequest() {
if (!_pendingBlocks) return;
const num = parseInt(document.getElementById('api-num').value);
const btn = document.getElementById('exec-btn');
btn.disabled = true;
btn.innerHTML = '<span class="spin"></span> Выполняется…';
try {
const r = await fetch('/api/task/execute', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({number: num, blocks: _pendingBlocks}),
});
const d = await r.json();
if (d.success) {
showResult('ok', `✓ Заявка #${num} успешно изменена`);
} else {
showResult('error', `Ошибка: ${d.error || 'неизвестная ошибка'}`);
}
} catch(e) {
showResult('error', e.message);
}
btn.innerHTML = '✓ Выполнить';
btn.disabled = true;
_pendingBlocks = null;
}
function resetPreview() {
document.getElementById('preview-url').textContent = '—';
document.getElementById('preview-json').textContent = 'Нажмите «Предпросмотр запроса»';
document.getElementById('exec-btn').disabled = true;
document.getElementById('api-result').innerHTML = '';
_pendingBlocks = null;
}
function showResult(type, msg) {
const cls = type === 'ok' ? 'result-ok' : type === 'error' ? 'result-err' : 'result-info';
document.getElementById('api-result').innerHTML =
`<div class="result-block ${cls}">${esc(msg)}</div>`;
}
// ═══════════════════════════════════════════════════════════════
// UTILS
// ═══════════════════════════════════════════════════════════════
function esc(s) {
return String(s ?? '')
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
.replace(/"/g,'&quot;');
}
// Инициализация формы API при загрузке
renderActionFields();
</script>
</body>
</html>