5bafd60df6
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
533 lines
35 KiB
HTML
533 lines
35 KiB
HTML
{{define "layout"}}<!DOCTYPE html>
|
||
<html lang="ru" data-theme="light">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>{{.Title}} · Bridge&Join</title>
|
||
<style>
|
||
/* ===================== Дизайн-система ===================== */
|
||
/* «Диспетчерская депозитария»: приборная панель оператора НРД/M2M.
|
||
Restrained — прохладные нейтрали + один индиго-акцент; MOEX-красный
|
||
зарезервирован строго под знак бренда и критику. Все величины —
|
||
моноширинными табличными цифрами (приборная подпись). */
|
||
/* Светлая тема (дневной свет, рабочее место) */
|
||
:root {
|
||
--bg:#eef1f6; --bg-elev:#ffffff; --card:#ffffff; --card-2:#f5f7fb;
|
||
--border:#e1e6ef; --border-strong:#cbd3e1;
|
||
--text:#111725; --text-2:#3d465a; --muted:#69748a;
|
||
--accent:#3452d6; --accent-weak:rgba(52,82,214,0.09); --accent-strong:#2740b0;
|
||
--ok:#12813f; --ok-weak:rgba(18,129,63,0.11);
|
||
--warn:#9a5b06; --warn-weak:rgba(154,91,6,0.12);
|
||
--err:#c22a2a; --err-weak:rgba(194,42,42,0.10);
|
||
--brand:#c5203e; /* MOEX-красный — только знак бренда и критика */
|
||
--grid-line:rgba(17,23,37,0.045);
|
||
--shadow:0 1px 2px rgba(16,24,40,0.05), 0 2px 6px rgba(16,24,40,0.06);
|
||
--shadow-lg:0 12px 34px rgba(16,24,40,0.14);
|
||
--radius:11px; --radius-sm:7px; --radius-lg:15px;
|
||
--mono:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;
|
||
--ease:cubic-bezier(.2,.7,.2,1);
|
||
}
|
||
/* Тёмная тема (терминал диспетчерской) */
|
||
[data-theme="dark"] {
|
||
--bg:#0a0d13; --bg-elev:#0f141d; --card:#131926; --card-2:#19212f;
|
||
--border:#232c3d; --border-strong:#354057;
|
||
--text:#e8edf5; --text-2:#aab6c9; --muted:#828fa6;
|
||
--accent:#7089ff; --accent-weak:rgba(112,137,255,0.15); --accent-strong:#94a7ff;
|
||
--ok:#3fce74; --ok-weak:rgba(63,206,116,0.15);
|
||
--warn:#e2ab41; --warn-weak:rgba(226,171,65,0.16);
|
||
--err:#ff7d7d; --err-weak:rgba(255,125,125,0.15);
|
||
--brand:#ff5670;
|
||
--grid-line:rgba(255,255,255,0.035);
|
||
--shadow:0 1px 2px rgba(0,0,0,0.4); --shadow-lg:0 14px 38px rgba(0,0,0,0.55);
|
||
}
|
||
/* Приборные цифры: величины (ID, счётчики, время, коды) — моно, табличные. */
|
||
.num, .stat-value, .clock, td .id, .mono { font-variant-numeric:tabular-nums; }
|
||
* { box-sizing: border-box; }
|
||
html, body { margin:0; padding:0; }
|
||
body {
|
||
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||
background: var(--bg); color: var(--text); line-height: 1.5;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
|
||
/* ---- Шапка (панель управления) ---- */
|
||
.topbar {
|
||
display:flex; align-items:center; gap:18px;
|
||
padding:0 22px; height:56px;
|
||
background: var(--bg-elev); border-bottom:1px solid var(--border);
|
||
position:sticky; top:0; z-index:100;
|
||
}
|
||
.brand { display:flex; align-items:center; gap:10px; font-weight:680; font-size:15.5px; letter-spacing:-0.015em; color:var(--text); flex:none; }
|
||
.brand svg { width:26px; height:26px; display:block; flex:none; }
|
||
.brand b { font-weight:730; } .brand .brand-j { color:var(--muted); font-weight:600; }
|
||
.nav { display:flex; align-items:center; gap:1px; margin-left:6px; flex:1 1 auto; min-width:0; flex-wrap:wrap; }
|
||
.nav::-webkit-scrollbar { display:none; }
|
||
.user-chip { display:inline-flex; flex-direction:column; line-height:1.15; margin-right:6px; max-width:180px; }
|
||
.user-chip .user-name { font-size:12.5px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||
.user-chip .user-role { font-size:10px; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; }
|
||
.user-logout { font-size:12px; color:var(--muted); text-decoration:none; padding:4px 8px; border:1px solid var(--border); border-radius:8px; margin-right:6px; }
|
||
.user-logout:hover { color:var(--text); border-color:var(--border-strong); }
|
||
.nav .group-label { font-size:9.5px; text-transform:uppercase; letter-spacing:0.09em; color:var(--muted); padding:0 9px 0 15px; border-left:1px solid var(--border); margin-left:7px; font-weight:600; }
|
||
.nav a {
|
||
color:var(--text-2); text-decoration:none; font-size:13px; font-weight:500;
|
||
padding:7px 11px; border-radius:8px; white-space:nowrap; transition:background .12s var(--ease), color .12s var(--ease);
|
||
}
|
||
.nav a:hover { background:var(--card-2); color:var(--text); }
|
||
.nav a.active { background:var(--accent-weak); color:var(--accent-strong); font-weight:600; }
|
||
.nav .dd { position:relative; }
|
||
.nav .dd .ddbtn { display:inline-block; cursor:pointer; color:var(--text-2); font-size:13px; font-weight:500;
|
||
padding:7px 11px; border-radius:8px; white-space:nowrap; transition:background .12s var(--ease); }
|
||
.nav .dd:hover .ddbtn, .nav .dd.active .ddbtn { background:var(--accent-weak); color:var(--accent-strong); font-weight:600; }
|
||
.nav .dd:hover .ddmenu, .nav .dd:focus-within .ddmenu { display:block; }
|
||
/* невидимый «мост» под кнопкой — чтобы при спуске мыши к меню hover не срывался */
|
||
.nav .dd::after { content:''; position:absolute; top:100%; left:0; right:0; height:10px; }
|
||
.nav .ddmenu { display:none; position:absolute; top:100%; left:0; margin-top:8px; background:var(--card); border:1px solid var(--border);
|
||
border-radius:10px; box-shadow:0 8px 30px rgba(0,0,0,.14); min-width:210px; z-index:60; padding:6px; }
|
||
.nav .ddmenu a { display:block; padding:8px 12px; border-radius:7px; }
|
||
.topbar-right { display:flex; align-items:center; gap:12px; flex:none; padding-left:10px; }
|
||
@media (max-width:1100px){ .sys-status span:not(.pulse){ display:none } .sys-status{ padding:5px 8px } .topbar .clock{ display:none } .user-dd .user-role{ display:none } }
|
||
/* Строка системного статуса — состояние читается всегда */
|
||
.sys-status { display:inline-flex; align-items:center; gap:7px; font-size:12px; font-weight:600; color:var(--text-2); padding:5px 11px 5px 9px; border:1px solid var(--border); border-radius:20px; background:var(--card-2); white-space:nowrap; }
|
||
.sys-status .pulse { width:8px; height:8px; border-radius:50%; background:var(--ok); box-shadow:0 0 0 0 var(--ok-weak); animation:pulse 2.4s var(--ease) infinite; }
|
||
.sys-status.warn .pulse { background:var(--warn); } .sys-status.err .pulse { background:var(--err); }
|
||
@keyframes pulse { 0%{box-shadow:0 0 0 0 rgba(18,129,63,.35)} 70%{box-shadow:0 0 0 6px rgba(18,129,63,0)} 100%{box-shadow:0 0 0 0 rgba(18,129,63,0)} }
|
||
@media (prefers-reduced-motion: reduce){ .sys-status .pulse{ animation:none } }
|
||
.theme-toggle {
|
||
background:var(--card-2); border:1px solid var(--border); color:var(--text-2);
|
||
width:34px; height:34px; border-radius:9px; cursor:pointer;
|
||
display:flex; align-items:center; justify-content:center; padding:0; transition:background .12s var(--ease), color .12s;
|
||
}
|
||
.theme-toggle svg { width:16px; height:16px; }
|
||
.theme-toggle:hover { background:var(--border); color:var(--text); }
|
||
.topbar .clock { font-size:11px; color:var(--muted); font-family:var(--mono); font-variant-numeric:tabular-nums; letter-spacing:-0.02em; }
|
||
.sys-block { display:inline-flex; flex-direction:column; align-items:center; gap:2px; line-height:1.1; }
|
||
.sys-block .sys-status { padding:3px 9px 3px 7px; font-size:11.5px; }
|
||
.user-dd .ddbtn.user-chip { display:inline-flex; flex-direction:column; align-items:flex-start; padding:4px 10px; border-radius:8px; margin:0; max-width:220px; }
|
||
.user-dd .user-name { font-size:12.5px; }
|
||
.nav .ddmenu.ddmenu-right, .user-dd .ddmenu { left:auto; right:0; min-width:190px; }
|
||
.user-dd { position:relative; }
|
||
.user-dd .ddmenu { display:none; position:absolute; top:100%; right:0; margin-top:8px; background:var(--card); border:1px solid var(--border); border-radius:10px; padding:6px; box-shadow:0 10px 30px rgba(0,0,0,.12); z-index:50; }
|
||
.user-dd:hover .ddmenu, .user-dd:focus-within .ddmenu { display:block; }
|
||
.user-dd::after { content:''; position:absolute; top:100%; left:0; right:0; height:10px; }
|
||
.user-dd .ddmenu a { display:block; padding:8px 12px; border-radius:7px; font-size:13px; color:var(--text-2); text-decoration:none; white-space:nowrap; }
|
||
.user-dd .ddmenu a:hover, .user-dd .ddmenu a.active { background:var(--accent-weak); color:var(--accent-strong); }
|
||
.user-dd:hover .ddbtn, .user-dd.active .ddbtn { background:var(--accent-weak); }
|
||
.ddsep { display:block; height:1px; background:var(--border); margin:6px 4px; }
|
||
|
||
/* ---- Контент ---- */
|
||
main { padding:24px; max-width:1200px; margin:0 auto; }
|
||
h1 { font-size:22px; font-weight:680; margin:0 0 4px; letter-spacing:-0.01em; }
|
||
h2 { font-size:15px; font-weight:650; margin:0 0 12px; }
|
||
h3 { font-size:13.5px; font-weight:600; margin:14px 0 6px; }
|
||
p { margin:0 0 10px; }
|
||
a { color:var(--accent); text-decoration:none; }
|
||
a:hover { text-decoration:underline; }
|
||
|
||
/* ---- Карточки ---- */
|
||
.card {
|
||
background:var(--card); border:1px solid var(--border); border-radius:var(--radius);
|
||
padding:18px 20px; margin-bottom:16px; box-shadow:var(--shadow);
|
||
}
|
||
.card h2:first-child { margin-top:0; }
|
||
.grid { display:grid; gap:14px; grid-template-columns:repeat(auto-fit, minmax(220px,1fr)); }
|
||
|
||
/* ---- Статы (приборные читалки) ---- */
|
||
.stat { padding:15px 17px; background:var(--card); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); }
|
||
.stat-label { display:flex; align-items:center; gap:7px; font-size:10.5px; color:var(--muted); text-transform:uppercase; letter-spacing:.06em; font-weight:600; }
|
||
.stat-label::before { content:""; width:7px; height:7px; border-radius:50%; background:var(--border-strong); flex:none; }
|
||
.stat.ok .stat-label::before { background:var(--ok); } .stat.warn .stat-label::before { background:var(--warn); } .stat.err .stat-label::before { background:var(--err); } .stat.accent .stat-label::before { background:var(--accent); }
|
||
.stat-value { font-size:27px; font-weight:700; margin-top:6px; letter-spacing:-0.03em; font-family:var(--mono); font-variant-numeric:tabular-nums; }
|
||
|
||
/* ---- Индикаторы ---- */
|
||
.dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:7px; vertical-align:middle; }
|
||
.dot.ok { background:var(--ok); } .dot.warn { background:var(--warn); } .dot.err { background:var(--err); }
|
||
|
||
/* ---- Таблицы ---- */
|
||
table { width:100%; border-collapse:collapse; font-size:13.5px; }
|
||
th, td { text-align:left; padding:9px 12px; border-bottom:1px solid var(--border); }
|
||
th { color:var(--muted); font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:.04em; }
|
||
tbody tr:hover td { background:var(--accent-weak); }
|
||
|
||
/* ---- Бейджи статусов ---- */
|
||
.badge { display:inline-block; padding:3px 9px; border-radius:20px; font-size:11px; font-weight:600; }
|
||
.badge.draft, .badge.validated, .badge.submitted_to_nsd { background:var(--accent-weak); color:var(--accent); }
|
||
.badge.awaiting_decision, .badge.manual_approval { background:var(--warn-weak); color:var(--warn); }
|
||
.badge.confirmed, .badge.awaiting_sub16, .badge.done { background:var(--ok-weak); color:var(--ok); }
|
||
.badge.rejected, .badge.timed_out, .badge.err { background:var(--err-weak); color:var(--err); }
|
||
.badge.ok { background:var(--ok-weak); color:var(--ok); }
|
||
|
||
/* ---- Код / preformatted ---- */
|
||
code { background:var(--card-2); border:1px solid var(--border); padding:1.5px 6px; border-radius:5px; font-size:12px; font-family:ui-monospace,"SF Mono",Menlo,monospace; }
|
||
pre { background:var(--card-2); border:1px solid var(--border); border-radius:var(--radius-sm); padding:14px; font-size:12px; overflow:auto; max-height:420px; font-family:ui-monospace,"SF Mono",Menlo,monospace; }
|
||
.muted { color:var(--muted); font-size:13px; }
|
||
|
||
/* ---- Кнопки ---- */
|
||
button, .btn {
|
||
background:var(--accent); color:#fff; border:1px solid var(--accent);
|
||
padding:9px 16px; border-radius:8px; cursor:pointer; font-size:13.5px; font-weight:550;
|
||
font-family:inherit; transition:filter .15s;
|
||
}
|
||
button:hover, .btn:hover { filter:brightness(1.07); text-decoration:none; }
|
||
.btn-secondary { background:var(--card-2); color:var(--text); border-color:var(--border-strong); }
|
||
.btn-ok { background:var(--ok); border-color:var(--ok); }
|
||
.btn-warn { background:var(--warn); border-color:var(--warn); color:#fff; }
|
||
.btn-danger { background:var(--err); border-color:var(--err); }
|
||
.btn-ghost { background:transparent; color:var(--accent); border-color:transparent; }
|
||
|
||
/* ---- Формы ---- */
|
||
input, select, textarea {
|
||
padding:9px 11px; background:var(--bg-elev); border:1px solid var(--border-strong);
|
||
color:var(--text); border-radius:8px; font:inherit; font-size:13.5px;
|
||
}
|
||
input:focus, select:focus, textarea:focus { outline:2px solid var(--accent-weak); border-color:var(--accent); }
|
||
label { font-size:13px; font-weight:500; }
|
||
|
||
/* ---- Баннер режима эмуляции ---- */
|
||
.banner-mock { background:var(--warn-weak); border-bottom:1px solid var(--warn); padding:10px 24px; display:flex; align-items:center; gap:12px; font-size:13px; }
|
||
|
||
/* ---- Hero (приветствие + статус) ---- */
|
||
.hero { padding:8px 0 22px; }
|
||
.hero-greeting { font-size:28px; font-weight:720; letter-spacing:-0.02em; margin:0 0 6px; }
|
||
.hero-status { display:inline-flex; align-items:center; gap:9px; font-size:15px; font-weight:550; padding:7px 16px; border-radius:24px; }
|
||
.hero-status.ok { background:var(--ok-weak); color:var(--ok); }
|
||
.hero-status.warn { background:var(--warn-weak); color:var(--warn); }
|
||
.hero-status.err { background:var(--err-weak); color:var(--err); }
|
||
|
||
/* ---- Плитки задач (task tiles) ---- */
|
||
.tiles { display:grid; gap:16px; grid-template-columns:repeat(auto-fit, minmax(210px,1fr)); margin:8px 0 24px; }
|
||
.tile {
|
||
display:flex; flex-direction:column; gap:10px;
|
||
padding:22px; background:var(--card); border:1px solid var(--border);
|
||
border-radius:16px; box-shadow:var(--shadow); cursor:pointer;
|
||
text-decoration:none; color:var(--text); transition:transform .14s, box-shadow .14s, border-color .14s;
|
||
min-height:128px;
|
||
}
|
||
.tile:hover { transform:translateY(-2px); box-shadow:var(--shadow-lg); border-color:var(--border-strong); text-decoration:none; }
|
||
.tile .ico { width:42px; height:42px; border-radius:11px; display:flex; align-items:center; justify-content:center; background:var(--accent-weak); color:var(--accent); }
|
||
.tile .ico svg { width:21px; height:21px; }
|
||
.tile.brand .ico { background:var(--accent); color:#fff; }
|
||
.tile .t-title { font-size:15.5px; font-weight:650; letter-spacing:-0.01em; }
|
||
.tile .t-sub { font-size:12.5px; color:var(--muted); margin-top:-4px; line-height:1.4; }
|
||
.tile .t-arrow { margin-top:auto; color:var(--border-strong); display:flex; }
|
||
.tile .t-arrow svg { width:18px; height:18px; }
|
||
.tile:hover .t-arrow { color:var(--accent); transform:translateX(2px); transition:transform .15s var(--ease), color .15s; }
|
||
|
||
/* ---- Секция (заголовок + контент) ---- */
|
||
.section-head { display:flex; align-items:baseline; justify-content:space-between; margin:24px 0 12px; }
|
||
.section-head h2 { margin:0; font-size:17px; }
|
||
.section-head a { font-size:13px; }
|
||
|
||
/* ---- Админ-центр: боковые разделы + контент (macOS System Settings) ---- */
|
||
.settings { display:grid; grid-template-columns:236px 1fr; gap:26px; align-items:start; }
|
||
.settings-nav { position:sticky; top:78px; display:flex; flex-direction:column; gap:2px; }
|
||
.settings-nav button {
|
||
display:flex; align-items:center; gap:11px; justify-content:flex-start;
|
||
background:transparent; border:1px solid transparent; color:var(--text-2);
|
||
padding:10px 13px; border-radius:9px; cursor:pointer; font-size:14px; font-weight:520;
|
||
width:100%; text-align:left; transition:background .12s;
|
||
}
|
||
.settings-nav button:hover { background:var(--card-2); color:var(--text); }
|
||
.settings-nav button.active { background:var(--accent-weak); color:var(--accent); }
|
||
.settings-nav button .nico { width:18px; height:18px; flex:none; display:flex; align-items:center; justify-content:center; color:var(--muted); }
|
||
.settings-nav button .nico svg { width:18px; height:18px; }
|
||
.settings-nav button.active .nico { color:var(--accent); }
|
||
.settings-nav button .ind { margin-left:auto; width:8px; height:8px; border-radius:50%; flex:none; }
|
||
.settings-nav button .ind.ok { background:var(--ok); }
|
||
.settings-nav button .ind.warn { background:var(--warn); }
|
||
.settings-nav button .ind.err { background:var(--err); }
|
||
.settings-section { display:none; }
|
||
.settings-section.active { display:block; }
|
||
.settings-section > h1 { margin-bottom:18px; }
|
||
@media (max-width:820px) {
|
||
.settings { grid-template-columns:1fr; }
|
||
.settings-nav { flex-direction:row; overflow-x:auto; position:static; padding-bottom:8px; }
|
||
.settings-nav button { white-space:nowrap; width:auto; }
|
||
}
|
||
|
||
/* ---- Документация / справка (Read) ---- */
|
||
.doc-header { margin:6px 0 22px; }
|
||
.doc-header h1 { font-size:24px; letter-spacing:-0.02em; }
|
||
.doc-header .lede { color:var(--text-2); font-size:15px; max-width:70ch; margin-top:6px; }
|
||
.doc { display:grid; grid-template-columns:236px minmax(0,1fr); gap:40px; align-items:start; }
|
||
.doc-toc { position:sticky; top:74px; max-height:calc(100vh - 96px); overflow:auto; font-size:13px; padding-right:6px; }
|
||
.doc-toc .toc-group { font-size:10px; text-transform:uppercase; letter-spacing:.07em; color:var(--muted); font-weight:700; margin:16px 0 6px; }
|
||
.doc-toc .toc-group:first-child { margin-top:0; }
|
||
.doc-toc a { display:block; color:var(--text-2); text-decoration:none; padding:5px 11px; border-radius:7px; border-left:2px solid transparent; line-height:1.35; }
|
||
.doc-toc a:hover { background:var(--card-2); color:var(--text); }
|
||
.doc-toc a.active { color:var(--accent-strong); border-left-color:var(--accent); background:var(--accent-weak); font-weight:600; }
|
||
.doc-body { max-width:820px; min-width:0; }
|
||
.doc-body > h2 { font-size:19px; letter-spacing:-0.015em; margin:34px 0 12px; padding-top:8px; scroll-margin-top:74px; }
|
||
.doc-body > h2:first-child { margin-top:0; }
|
||
.doc-body h3 { font-size:15px; margin:22px 0 8px !important; }
|
||
.doc-body p, .doc-body li { max-width:74ch; }
|
||
.doc-body p { color:var(--text-2); }
|
||
.doc-body .card { scroll-margin-top:74px; }
|
||
.doc-sep { height:1px; background:var(--border); margin:30px 0; border:0; }
|
||
.callout { background:var(--card-2); border:1px solid var(--border); border-radius:var(--radius-sm); padding:13px 15px; margin:14px 0; font-size:14px; line-height:1.55; }
|
||
.callout.err { background:var(--err-weak); } .callout.accent { background:var(--accent-weak); } .callout.warn { background:var(--warn-weak); } .callout.ok { background:var(--ok-weak); }
|
||
.callout strong:first-child { color:var(--text); }
|
||
/* Код-блоки справки — тёмная тема учтена */
|
||
.doc-body pre, pre.copyable, pre.code { background:var(--card-2) !important; border:1px solid var(--border); border-radius:var(--radius-sm); color:var(--text); }
|
||
@media (max-width:900px){ .doc { grid-template-columns:1fr; gap:0; } .doc-toc { position:static; max-height:none; margin-bottom:18px; border-bottom:1px solid var(--border); padding-bottom:12px; } }
|
||
|
||
/* ---- Toast ---- */
|
||
#bj-toast { position:fixed; top:70px; right:22px; z-index:9999; max-width:520px; padding:14px 18px 14px 40px; background:var(--card); border:1px solid var(--border-strong); border-radius:var(--radius-sm); color:var(--text); box-shadow:var(--shadow-lg); font-size:13px; line-height:1.45; opacity:0; transform:translateY(-12px); transition:opacity .25s var(--ease), transform .25s var(--ease); }
|
||
#bj-toast::before { content:""; position:absolute; left:16px; top:17px; width:9px; height:9px; border-radius:50%; background:var(--ok); }
|
||
#bj-toast.visible { opacity:1; transform:translateY(0); }
|
||
#bj-toast .close { position:absolute; top:6px; right:10px; cursor:pointer; color:var(--muted); font-size:15px; }
|
||
#bj-toast .close:hover { color:var(--text); }
|
||
|
||
.banner-dev{margin:0;border-bottom:1px solid var(--border)}
|
||
.banner-dev button{width:100%;display:flex;gap:10px;align-items:center;padding:7px 18px;background:transparent;border:0;font:inherit;font-size:13px;color:var(--text);cursor:pointer;text-align:left}
|
||
.banner-dev.on{background:color-mix(in srgb,var(--warn) 12%,var(--card))}
|
||
.banner-dev.off{background:color-mix(in srgb,var(--ok) 10%,var(--card))}
|
||
.banner-dev button:hover{filter:brightness(.97)}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="bj-toast"><span class="close" onclick="document.getElementById('bj-toast').classList.remove('visible')">×</span><div id="bj-toast-text"></div></div>
|
||
|
||
<header class="topbar">
|
||
<a class="brand" href="/admin/" title="На дашборд" style="text-decoration:none;color:inherit">
|
||
<svg viewBox="0 0 26 26" fill="none" aria-hidden="true">
|
||
<path d="M5.5 16 C 9 8.5, 17 8.5, 20.5 16" stroke="var(--accent)" stroke-width="1.9" stroke-linecap="round"/>
|
||
<circle cx="5.5" cy="16" r="3.1" fill="var(--accent)"/>
|
||
<circle cx="20.5" cy="16" r="3.1" fill="var(--brand)"/>
|
||
</svg>
|
||
<span><b>Bridge</b><span class="brand-j">&Join</span></span>
|
||
</a>
|
||
<nav class="nav">
|
||
{{if can "view"}}<a href="/admin/" class="{{if eq .Active "home"}}active{{end}}">Дашборд</a>
|
||
<span class="dd{{if or (eq .Active "journal") (eq .Active "claims") (eq .Active "incoming")}} active{{end}}">
|
||
<span class="ddbtn">Журнал ▾<span id="nav-journal-badge" title="Входящих ждут решения" style="display:none;margin-left:5px;background:var(--err);color:#fff;border-radius:10px;padding:1px 7px;font-size:11px;font-weight:700"></span></span>
|
||
<span class="ddmenu">
|
||
<a href="/admin/journal">Все (вход + исход)</a>
|
||
<a href="/admin/claims">Исходящие переводы</a>
|
||
<a href="/admin/incoming">Входящие<span id="nav-incoming-badge" style="display:none;margin-left:5px;background:var(--err);color:#fff;border-radius:10px;padding:1px 7px;font-size:11px;font-weight:700"></span></a>
|
||
</span>
|
||
</span>
|
||
{{end}}{{if can "view"}}<a href="/admin/manual" class="{{if eq .Active "manual"}}active{{end}}">Ручной ввод M2M</a>{{end}}{{if and devOn (can "operate")}}<a href="/admin/m2m-testing" class="{{if eq .Active "m2mtest"}}active{{end}}">Тестирование M2M</a>{{end}}
|
||
{{if and .NPR (can "npr")}}<a href="/admin/npr" class="{{if eq .Active "npr"}}active{{end}}">НПР / УДС</a>{{end}}
|
||
{{if or (can "view") (can "quik")}}<span class="dd{{if or (eq .Active "fancy") (eq .Active "quik") (eq .Active "news") (eq .Active "handbook")}} active{{end}}">
|
||
<span class="ddbtn">Обмен ▾</span>
|
||
<span class="ddmenu">
|
||
{{if can "view"}}<a href="/admin/fancy-queue">Обмен с DEPO (Fancy)</a>{{end}}
|
||
{{if can "quik"}}<a href="/admin/quik">Поручения QUIK</a>{{end}}
|
||
{{if can "view"}}<a href="/admin/news">События</a>
|
||
<a href="/admin/handbook">Справочник НРД</a>{{end}}
|
||
</span>
|
||
</span>{{end}}
|
||
</nav>
|
||
<div class="topbar-right">
|
||
<span class="sys-block">
|
||
<span class="sys-status {{if .IsMockMode}}warn{{end}}" title="Состояние обмена с НРД">
|
||
<span class="pulse"></span>{{if .IsMockMode}}{{if devOn}}Эмуляция{{else}}Нет связи с НРД{{end}}{{else}}НРД на связи{{end}}</span>
|
||
<span class="clock">{{.Now}}</span>
|
||
</span>
|
||
<span class="dd user-dd{{if or (eq .Active "setup") (eq .Active "status") (eq .Active "help")}} active{{end}}">
|
||
<span class="ddbtn user-chip" title="{{with curUser}}{{.RoleTitle}}{{if eq .Source "ad"}} · вход через AD{{end}}{{else}}Пользователи не созданы — вход открыт{{end}}">
|
||
<span class="user-name">{{with curUser}}{{if .Name}}{{.Name}}{{else}}{{.Login}}{{end}}{{else}}без входа{{end}} ▾</span>
|
||
<span class="user-role">{{with curUser}}{{.RoleTitle}}{{else}}вход открыт{{end}}</span>
|
||
</span>
|
||
<span class="ddmenu ddmenu-right">
|
||
{{if can "settings"}}<a href="/admin/setup" class="{{if eq .Active "setup"}}active{{end}}">Настройка</a>
|
||
<a href="/admin/status" class="{{if eq .Active "status"}}active{{end}}">Статус системы</a>{{end}}
|
||
<a href="/admin/help" class="{{if eq .Active "help"}}active{{end}}">Справка</a>
|
||
{{with curUser}}<span class="ddsep"></span><a href="/logout">Выйти</a>{{else}}{{if authOpen}}<span class="ddsep"></span><a href="/admin/setup#users">Создать пользователей</a>{{end}}{{end}}
|
||
</span>
|
||
</span>
|
||
<button class="theme-toggle" id="theme-toggle" title="Светлая/тёмная тема" aria-label="Переключить тему"></button>
|
||
</div>
|
||
</header>
|
||
|
||
{{if devLicensed}}
|
||
<form method="post" action="/admin/dev-toggle" class="banner-dev {{if devOn}}on{{else}}off{{end}}" title="{{if devOn}}Нажмите, чтобы скрыть у себя служебные разделы и имитации{{else}}Нажмите, чтобы снова показать служебные разделы{{end}}">
|
||
<input type="hidden" name="return_to" value="">
|
||
<button type="submit"><span class="dot {{if devOn}}warn{{else}}ok{{end}}" style="width:9px;height:9px;margin:0"></span>
|
||
{{if devOn}}<strong>Режим разработки включён</strong><span class="muted"> — видны служебные разделы, тестовые сценарии и имитации. Нажмите, чтобы выключить у себя.</span>{{else}}<strong>Режим разработки выключен у вас</strong><span class="muted"> — лицензия его разрешает; нажмите, чтобы включить.</span>{{end}}
|
||
</button>
|
||
</form>
|
||
<script>(function(){var f=document.querySelector('.banner-dev input[name=return_to]');if(f)f.value=location.pathname+location.search;})();</script>
|
||
{{end}}
|
||
{{if .IsMockMode}}
|
||
<div class="banner-mock">
|
||
<span class="dot warn" style="width:9px;height:9px;margin:0"></span>
|
||
<div><strong style="color:var(--warn)">{{if devOn}}Режим эмуляции{{else}}Обмен с НРД не настроен{{end}}</strong> — {{if devOn}}реального обмена с НРД нет{{else}}поручения в НРД не отправляются{{end}}. <span class="muted">{{.MockReason}}</span></div>
|
||
<a href="/admin/wizard" style="margin-left:auto">Открыть мастер настройки →</a>
|
||
</div>
|
||
{{end}}
|
||
|
||
<main>
|
||
{{template "content" .}}
|
||
</main>
|
||
|
||
<script>
|
||
(function() {
|
||
// --- Тема: light/dark, сохранение в localStorage ---
|
||
var root = document.documentElement;
|
||
var toggle = document.getElementById('theme-toggle');
|
||
var icoMoon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z"/></svg>';
|
||
var icoSun = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>';
|
||
function applyTheme(t) {
|
||
root.setAttribute('data-theme', t);
|
||
toggle.innerHTML = (t === 'dark') ? icoSun : icoMoon;
|
||
try { localStorage.setItem('bj-theme', t); } catch(e) {}
|
||
}
|
||
var saved = 'light';
|
||
try { saved = localStorage.getItem('bj-theme') || (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); } catch(e) {}
|
||
applyTheme(saved);
|
||
toggle.addEventListener('click', function() {
|
||
applyTheme(root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
|
||
});
|
||
|
||
// --- Toast из ?flash= ---
|
||
var toast = document.getElementById('bj-toast');
|
||
var toastText = document.getElementById('bj-toast-text');
|
||
function showToast(msg) {
|
||
toastText.textContent = msg;
|
||
toast.classList.add('visible');
|
||
var ms = Math.max(5000, Math.min(20000, msg.length * 60));
|
||
setTimeout(function() { toast.classList.remove('visible'); }, ms);
|
||
}
|
||
var params = new URLSearchParams(window.location.search);
|
||
var flash = params.get('flash');
|
||
if (flash) { showToast(flash); }
|
||
// Прокрутка к секции из ?scrollto= (надёжнее фрагмента через редирект).
|
||
var anchored = false;
|
||
var scrollTarget = params.get('scrollto');
|
||
if (scrollTarget) {
|
||
var stEl = document.getElementById(scrollTarget);
|
||
if (stEl) {
|
||
anchored = true;
|
||
var doScroll2 = function() { stEl.scrollIntoView({ block: 'start' }); };
|
||
doScroll2();
|
||
setTimeout(doScroll2, 50);
|
||
setTimeout(doScroll2, 200);
|
||
window.addEventListener('load', doScroll2);
|
||
}
|
||
}
|
||
if (flash || scrollTarget) {
|
||
params.delete('flash');
|
||
params.delete('scrollto');
|
||
var qs = params.toString();
|
||
window.history.replaceState({}, '', window.location.pathname + (qs ? '?' + qs : '') + window.location.hash);
|
||
}
|
||
|
||
// --- Сохранение позиции прокрутки через POST-редиректы ---
|
||
// Отключаем авто-восстановление браузера — иначе оно перебивает наше
|
||
// (скроллит наверх уже ПОСЛЕ нашего scrollTo).
|
||
if ('scrollRestoration' in history) { try { history.scrollRestoration = 'manual'; } catch(e) {} }
|
||
// Присваиваем карточкам стабильные id (по порядку рендера) — чтобы вернуться
|
||
// к нужной секции по якорю после сохранения.
|
||
var cards = document.querySelectorAll('.card');
|
||
for (var ci = 0; ci < cards.length; ci++) { if (!cards[ci].id) cards[ci].id = 'sec-' + ci; }
|
||
document.addEventListener('submit', function(ev) {
|
||
var f = ev.target;
|
||
if (f && f.method && f.method.toLowerCase() === 'post') {
|
||
try { sessionStorage.setItem('bj-scroll', String(window.scrollY)); } catch(e) {}
|
||
// Возврат на ТЕКУЩУЮ страницу (а не на /admin/setup): сервер не всегда
|
||
// получает Referer, поэтому говорим путь явно.
|
||
if (!f.querySelector('input[name="return_to"]')) {
|
||
var rt = document.createElement('input');
|
||
rt.type = 'hidden'; rt.name = 'return_to'; rt.value = location.pathname;
|
||
f.appendChild(rt);
|
||
}
|
||
// Подкладываем секцию (ближайшую карточку) — сервер вернёт ?scrollto=.
|
||
var card = f.closest ? f.closest('.card') : null;
|
||
if (card && card.id && !f.querySelector('input[name="section"]')) {
|
||
var inp = document.createElement('input');
|
||
inp.type = 'hidden'; inp.name = 'section'; inp.value = card.id;
|
||
f.appendChild(inp);
|
||
}
|
||
}
|
||
}, true);
|
||
var sc = null;
|
||
try { sc = sessionStorage.getItem('bj-scroll'); } catch(e) {}
|
||
// При наличии якоря восстановление позиции пропускаем, чтобы не конфликтовать.
|
||
if (sc !== null && !anchored) {
|
||
var y = parseInt(sc, 10) || 0;
|
||
try { sessionStorage.removeItem('bj-scroll'); } catch(e) {}
|
||
// Несколько попыток: страница могла ещё не разложиться (шрифты/контент),
|
||
// поэтому повторяем сразу, на след. кадре, по таймеру и после полной загрузки.
|
||
var restore = function() { window.scrollTo(0, y); };
|
||
restore();
|
||
window.requestAnimationFrame(restore);
|
||
setTimeout(restore, 60);
|
||
setTimeout(restore, 200);
|
||
window.addEventListener('load', restore);
|
||
}
|
||
|
||
// --- Браузерные уведомления о входящих переводах (сценарий 3333) ---
|
||
// Опрашиваем /admin/incoming/feed; на НОВЫЙ входящий показываем Notification,
|
||
// клик по которому ведёт в карточку для подтверждения/отказа. Плюс бейдж в меню.
|
||
(function() {
|
||
var badge = document.getElementById('nav-incoming-badge');
|
||
var jbadge = document.getElementById('nav-journal-badge');
|
||
function setBadges(n) {
|
||
[badge, jbadge].forEach(function(b){
|
||
if (!b) return;
|
||
if (n > 0) { b.textContent = n; b.style.display = 'inline-block'; }
|
||
else { b.style.display = 'none'; }
|
||
});
|
||
}
|
||
function seenSet() {
|
||
try { return new Set(JSON.parse(localStorage.getItem('bj-seen-incoming') || '[]')); }
|
||
catch(e) { return new Set(); }
|
||
}
|
||
function saveSeen(s) {
|
||
try { localStorage.setItem('bj-seen-incoming', JSON.stringify(Array.from(s))); } catch(e) {}
|
||
}
|
||
function notify(it) {
|
||
// 1) Системное уведомление браузера (только в защищённом контексте: HTTPS
|
||
// или localhost; по обычному HTTP браузеры Notification блокируют).
|
||
if (('Notification' in window) && Notification.permission === 'granted') {
|
||
var n = new Notification('Новый входящий перевод НРД', {
|
||
body: 'От ' + it.sender + ' · ' + it.count + ' бумаг · ' + it.received + '\nНажмите, чтобы подтвердить или отказать.',
|
||
tag: 'bj-incoming-' + it.id, requireInteraction: true
|
||
});
|
||
n.onclick = function() { window.focus(); location.href = '/admin/incoming/' + it.id; };
|
||
}
|
||
// 2) Всегда — заметный баннер на странице (работает и по HTTP).
|
||
inPageBanner(it);
|
||
}
|
||
function bannerStack() {
|
||
var c = document.getElementById('bj-incoming-stack');
|
||
if (!c) {
|
||
c = document.createElement('div');
|
||
c.id = 'bj-incoming-stack';
|
||
c.style.cssText = 'position:fixed;top:16px;right:16px;z-index:2000;display:flex;flex-direction:column;gap:10px;max-width:360px';
|
||
document.body.appendChild(c);
|
||
}
|
||
return c;
|
||
}
|
||
function inPageBanner(it) {
|
||
var bar = document.createElement('div');
|
||
bar.style.cssText = 'background:var(--accent,#2563eb);color:#fff;padding:14px 16px;border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.35);cursor:pointer;font-size:14px;line-height:1.4';
|
||
bar.innerHTML = '<b>📥 Новый входящий перевод НРД</b><br>От ' + it.sender + ' · ' + it.count + ' бумаг · ' + it.received + '<br><span style="text-decoration:underline">Открыть для подтверждения/отказа →</span>';
|
||
bar.onclick = function() { location.href = '/admin/incoming/' + it.id; };
|
||
bannerStack().appendChild(bar);
|
||
setTimeout(function(){ bar.style.transition='opacity .5s'; bar.style.opacity='0'; setTimeout(function(){bar.remove();}, 600); }, 20000);
|
||
}
|
||
function poll() {
|
||
fetch('/admin/incoming/feed', { cache: 'no-store' }).then(function(r){ return r.json(); }).then(function(d){
|
||
setBadges(d.count);
|
||
var seen = seenSet(), changed = false;
|
||
(d.pending || []).forEach(function(it){
|
||
if (!seen.has(it.id)) { notify(it); seen.add(it.id); changed = true; }
|
||
});
|
||
if (changed) saveSeen(seen);
|
||
}).catch(function(){});
|
||
}
|
||
// Запрашиваем разрешение на уведомления (один раз).
|
||
if ('Notification' in window && Notification.permission === 'default') {
|
||
Notification.requestPermission().then(function(){ poll(); });
|
||
}
|
||
poll();
|
||
setInterval(poll, 12000); // каждые 12 сек
|
||
})();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
{{end}}
|