Files
shd-modules-mcp-plugin/plugins/shd-mcp-plugin/widgets/finance/v1/index.html
T
markkats cc63037e1f
Validate SHD MCP plugin / validate (push) Has been cancelled
feat: add SHD widget suite
2026-08-24 21:46:52 +03:00

283 lines
15 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">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Финансы — SHD MCP</title>
<style>
:root{color-scheme:light dark;--bg:#f7f8fa;--panel:#fff;--line:#d9dee7;--ink:#172033;--muted:#687386;--accent:#2563eb;--accent-soft:#e8efff;--danger:#b42318}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
.shell{max-width:980px;margin:0 auto;padding:24px}.eyebrow{color:var(--accent);font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}
h1{margin:4px 0 6px;font-size:25px;line-height:1.15}.lede{margin:0;color:var(--muted)}.summary{display:flex;gap:10px;flex-wrap:wrap;margin:18px 0 14px}
.summary span{padding:5px 9px;border:1px solid var(--line);border-radius:999px;background:var(--panel);color:var(--muted)}.summary strong{color:var(--ink)}
.toolbar{display:flex;gap:8px;margin-bottom:12px}.search{min-width:0;flex:1;padding:9px 11px;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:inherit}
button{cursor:pointer}.refresh{padding:9px 14px;border:1px solid var(--accent);border-radius:8px;background:var(--accent);color:#fff;font-weight:600}.refresh:disabled{opacity:.6;cursor:wait}
.status-message{min-height:22px;color:var(--muted)}.error{color:var(--danger)}.table-wrap{overflow:auto;border:1px solid var(--line);border-radius:10px;background:var(--panel)}
table{width:100%;border-collapse:collapse;min-width:560px}th,td{padding:11px 13px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}th{color:var(--muted);font-size:12px;font-weight:600}
tbody tr:last-child td{border-bottom:0}.record{width:100%;border:0;background:transparent;color:inherit;text-align:left}.record:hover{background:var(--accent-soft)}
.title{font-weight:650}.subtitle{display:block;color:var(--muted);font-size:12px;margin-top:2px}.pill{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--accent-soft);color:var(--accent);font-size:12px;white-space:nowrap}
.empty{padding:24px;text-align:center;color:var(--muted)}.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:10px;background:var(--panel)}.details[hidden]{display:none}
.details-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}
.detail{padding:9px;border-radius:8px;background:var(--bg)}.detail-label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.detail-value{overflow-wrap:anywhere}
@media(prefers-color-scheme:dark){:root{--bg:#11151d;--panel:#1b2230;--line:#303a4b;--ink:#edf2fb;--muted:#aab4c5;--accent:#82aaff;--accent-soft:#263858;--danger:#ff9a93}}
</style>
</head>
<body>
<main class="shell" data-widget-module="finance">
<header>
<div class="eyebrow">SHD MCP</div>
<h1>Финансы</h1>
<p class="lede">Реестр финансовых документов с поиском и просмотром деталей.</p>
<div class="summary" id="summary"></div>
</header>
<div class="toolbar">
<input class="search" id="search" type="search" placeholder="Поиск по названию, статусу или ID" autocomplete="off">
<button class="refresh" id="refresh" type="button">Обновить</button>
</div>
<div class="status-message" id="status" role="status" aria-live="polite"></div>
<div class="table-wrap">
<table>
<thead><tr><th>Документ</th><th>Статус</th><th>Срок</th><th>Сумма</th></tr></thead>
<tbody id="rows"></tbody>
</table>
<div class="empty" id="empty" hidden>Нет данных для отображения.</div>
</div>
<section class="details" id="details" hidden aria-live="polite">
<div class="details-head"><div><h2 id="details-title"></h2><div class="subtitle" id="details-subtitle"></div></div><button class="close" id="close" type="button" aria-label="Закрыть">×</button></div>
<div class="detail-grid" id="detail-grid"></div>
</section>
</main>
<script>
(function () {
var CONFIG = {"module":"finance","title":"Финансы","description":"Реестр финансовых документов с поиском и просмотром деталей.","nameLabel":"Документ","statusLabel":"Статус","dateLabel":"Срок","valueLabel":"Сумма","listTool":"shd_list_finance_documents","detailTool":"shd_get_finance_document","detailKey":"document_id","listArgs":{"archived":false,"limit":100}};
var pending = new Map();
var nextRequestId = 1;
var records = [];
var busy = false;
var elements = {
search: document.getElementById('search'),
refresh: document.getElementById('refresh'),
status: document.getElementById('status'),
summary: document.getElementById('summary'),
rows: document.getElementById('rows'),
empty: document.getElementById('empty'),
details: document.getElementById('details'),
detailsTitle: document.getElementById('details-title'),
detailsSubtitle: document.getElementById('details-subtitle'),
detailGrid: document.getElementById('detail-grid'),
close: document.getElementById('close')
};
function request(method, params) {
var openai = typeof window !== 'undefined' ? window.openai : undefined;
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
return openai.callTool(params.name, params.arguments || {});
}
var id = nextRequestId++;
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
return new Promise(function (resolve, reject) {
var timeout = window.setTimeout(function () {
pending.delete(id);
reject(new Error('Host request timed out'));
}, 10000);
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
});
}
function notify(method, params) {
window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params || {} }, '*');
}
function normalizePayload(value) {
if (!value) return null;
if (value.structuredContent) return value.structuredContent;
if (value.toolOutput) return value.toolOutput;
return value;
}
function unwrap(value) {
var payload = normalizePayload(value);
if (value && value.isError) throw new Error('MCP tool returned an error');
if (payload && payload.error) throw new Error(String(payload.error));
return payload || value;
}
function toArray(value) {
var payload = normalizePayload(value);
if (!payload) return [];
var data = payload.data;
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
if (Array.isArray(payload.items)) data = payload.items;
return Array.isArray(data) ? data.filter(function (item) { return item && typeof item === 'object'; }) : [];
}
function idOf(item) { return String(item.id || item.code || item.key || item.number || ''); }
function titleOf(item) { return String(item.title || item.name || item.display_name || item.number || idOf(item) || '—'); }
function subtitleOf(item) { return String(item.subtitle || item.project_code || item.code || item.key || ''); }
function statusOf(item) {
var value = item.status;
if (value && typeof value === 'object') value = value.name || value.id;
return String(value || item.phase || '—');
}
function dateOf(item) { return String(item.date || item.due_date || item.updated_at || item.issued_at || item.created_at || ''); }
function valueOf(item) { return item.value === null || item.value === undefined ? '' : String(item.value); }
function dateText(value) {
if (!value) return '—';
var timestamp = Date.parse(value);
if (Number.isNaN(timestamp)) return value;
try { return new Intl.DateTimeFormat(undefined, { day: '2-digit', month: '2-digit', year: 'numeric' }).format(new Date(timestamp)); }
catch (error) { return value; }
}
function make(tag, value, className) {
var node = document.createElement(tag);
if (className) node.className = className;
if (value !== undefined) node.textContent = value;
return node;
}
function renderSummary() {
elements.summary.replaceChildren();
var item = make('span');
item.append(make('strong', String(records.length)), ' записей');
elements.summary.append(item);
}
function renderRows() {
var query = elements.search.value.trim().toLowerCase();
var filtered = records.filter(function (item) {
if (!query) return true;
return (titleOf(item) + ' ' + subtitleOf(item) + ' ' + statusOf(item) + ' ' + idOf(item)).toLowerCase().indexOf(query) !== -1;
});
elements.rows.replaceChildren();
elements.empty.hidden = filtered.length !== 0;
filtered.forEach(function (item) {
var row = document.createElement('tr');
var name = make('td');
name.append(make('span', titleOf(item), 'title'));
if (subtitleOf(item)) name.append(make('span', subtitleOf(item), 'subtitle'));
var status = make('td');
status.append(make('span', statusOf(item), 'pill'));
row.append(name, status, make('td', dateText(dateOf(item))), make('td', valueOf(item) || '—'));
row.className = 'record';
row.tabIndex = 0;
row.setAttribute('role', 'button');
row.setAttribute('aria-label', titleOf(item));
row.addEventListener('click', function () { showDetails(item); });
row.addEventListener('keydown', function (event) { if (event.key === 'Enter' || event.key === ' ') showDetails(item); });
elements.rows.append(row);
});
renderSummary();
}
function render(value) {
records = toArray(value).slice().sort(function (left, right) {
var a = Date.parse(dateOf(left)); var b = Date.parse(dateOf(right));
if (Number.isNaN(a)) a = Number.POSITIVE_INFINITY;
if (Number.isNaN(b)) b = Number.POSITIVE_INFINITY;
return a - b || titleOf(left).localeCompare(titleOf(right));
});
renderRows();
}
function setStatus(value, isError) {
elements.status.textContent = value || '';
elements.status.className = isError ? 'status-message error' : 'status-message';
}
function detailValue(value) {
if (value === null || value === undefined || value === '') return '—';
if (typeof value === 'object') {
try { return JSON.stringify(value); } catch (error) { return '—'; }
}
return String(value);
}
function addDetail(label, value) {
var item = make('div', undefined, 'detail');
item.append(make('span', label, 'detail-label'), make('span', detailValue(value), 'detail-value'));
elements.detailGrid.append(item);
}
function showDetails(item) {
elements.detailsTitle.textContent = titleOf(item);
elements.detailsSubtitle.textContent = subtitleOf(item) || idOf(item);
elements.detailGrid.replaceChildren();
addDetail('ID', idOf(item));
addDetail(CONFIG.statusLabel, statusOf(item));
addDetail(CONFIG.dateLabel, dateText(dateOf(item)));
if (valueOf(item)) addDetail(CONFIG.valueLabel, valueOf(item));
if (item.meta) addDetail('Meta', item.meta);
elements.details.hidden = false;
var id = idOf(item);
if (!id || !CONFIG.detailTool) return;
var args = {};
args[CONFIG.detailKey] = Number(id) || id;
if (CONFIG.detailSection) args.section = CONFIG.detailSection;
request('tools/call', { name: CONFIG.detailTool, arguments: args }).then(function (result) {
var payload = unwrap(result);
var data = payload && payload.data;
if (Array.isArray(data)) data = data[0];
if (!data || typeof data !== 'object') return;
elements.detailGrid.replaceChildren();
Object.keys(data).slice(0, 12).forEach(function (key) { addDetail(key, data[key]); });
}).catch(function (error) { setStatus(error && error.message ? error.message : 'Не удалось загрузить детали.', true); });
}
function refresh() {
if (busy) return;
busy = true;
elements.refresh.disabled = true;
setStatus('Загрузка…', false);
request('tools/call', { name: CONFIG.listTool, arguments: CONFIG.listArgs }).then(function (result) {
render(unwrap(result));
setStatus('', false);
}).catch(function (error) {
setStatus(error && error.message ? error.message : 'Не удалось загрузить данные.', true);
}).finally(function () {
busy = false;
elements.refresh.disabled = false;
});
}
function initializeHost() {
return request('ui/initialize', {
protocolVersion: '2025-11-25',
appInfo: { name: 'SHD ' + CONFIG.title, title: 'SHD ' + CONFIG.title, version: '1.0.0', websiteUrl: 'https://shd.xyz.su' },
appCapabilities: {}
}).then(function () { notify('ui/notifications/initialized', {}); }).catch(function () {});
}
window.addEventListener('message', function (event) {
if (event.source !== window.parent) return;
var message = event.data;
if (!message || message.jsonrpc !== '2.0') return;
if (message.id !== undefined && pending.has(message.id)) {
var entry = pending.get(message.id);
pending.delete(message.id);
window.clearTimeout(entry.timeout);
if (message.error) entry.reject(new Error(message.error.message || 'Host request failed'));
else entry.resolve(message.result);
return;
}
if (message.method === 'ui/notifications/tool-input') {
if (!records.length) render(message.params || {});
}
if (message.method === 'ui/notifications/tool-result') render(message.params && message.params.structuredContent);
}, { passive: true });
elements.search.addEventListener('input', renderRows);
elements.refresh.addEventListener('click', refresh);
elements.close.addEventListener('click', function () { elements.details.hidden = true; });
void initializeHost();
var initialOutput = typeof window !== 'undefined' && window.openai ? window.openai.toolOutput : null;
if (initialOutput) render(initialOutput);
else {
var initialInput = typeof window !== 'undefined' && window.openai ? window.openai.toolInput : null;
if (initialInput) render(initialInput);
else refresh();
}
}());
</script>
</body>
</html>