feat(bj): модуль ЛК на PostgreSQL, обмен с DEPO, боевой ЛК с имитатором НРД, QUIK, НПР
- bj_exchange и «кран» Fancy убраны: данные модуля ЛК в PostgreSQL (схема lk) - трансферные таблицы FTV_M2M: создание при установке, события, справочник участников - сверка с DEPO: счёт, ФИО, дата рождения, ИИС по договору, квал-инвестор из опции DEPO - боевой ЛК ЕСИА Финанс при тестовом ИШ: журнал, отказ через DEPO, статусы в ЛК - онлайн-лицензирование в bj, план тестирования M2M для контрагента (интерактивный HTML) - модули QUIK МНП, экспорт QUIK, НПР/УДС; документы и тестовые материалы Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,32 +6,42 @@
|
||||
<title>{{.Title}} · Bridge&Join</title>
|
||||
<style>
|
||||
/* ===================== Дизайн-система ===================== */
|
||||
/* Светлая тема (по умолчанию) */
|
||||
/* «Диспетчерская депозитария»: приборная панель оператора НРД/M2M.
|
||||
Restrained — прохладные нейтрали + один индиго-акцент; MOEX-красный
|
||||
зарезервирован строго под знак бренда и критику. Все величины —
|
||||
моноширинными табличными цифрами (приборная подпись). */
|
||||
/* Светлая тема (дневной свет, рабочее место) */
|
||||
:root {
|
||||
--bg:#f5f6f8; --bg-elev:#ffffff; --card:#ffffff; --card-2:#fafbfc;
|
||||
--border:#e4e7ec; --border-strong:#d0d5dd;
|
||||
--text:#1a1f29; --text-2:#475067; --muted:#7a8499;
|
||||
--accent:#2563eb; --accent-weak:rgba(37,99,235,0.10); --accent-strong:#1d4ed8;
|
||||
--ok:#16a34a; --ok-weak:rgba(22,163,74,0.12);
|
||||
--warn:#d97706; --warn-weak:rgba(217,119,6,0.12);
|
||||
--err:#dc2626; --err-weak:rgba(220,38,38,0.12);
|
||||
--brand:#c5203e; /* MOEX-красный для акцентов бренда */
|
||||
--shadow:0 1px 2px rgba(16,24,40,0.06), 0 1px 3px rgba(16,24,40,0.10);
|
||||
--shadow-lg:0 8px 24px rgba(16,24,40,0.12);
|
||||
--radius:10px; --radius-sm:6px;
|
||||
--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:#0f1115; --bg-elev:#161922; --card:#1a1d24; --card-2:#20242e;
|
||||
--border:#2a2f3a; --border-strong:#3a4150;
|
||||
--text:#e8eaed; --text-2:#b4bcc9; --muted:#8b94a3;
|
||||
--accent:#5b9dff; --accent-weak:rgba(91,157,255,0.14); --accent-strong:#7db0ff;
|
||||
--ok:#3fbf6c; --ok-weak:rgba(63,191,108,0.16);
|
||||
--warn:#e8b13a; --warn-weak:rgba(232,177,58,0.16);
|
||||
--err:#e85a5a; --err-weak:rgba(232,90,90,0.16);
|
||||
--brand:#ff5a78;
|
||||
--shadow:0 1px 2px rgba(0,0,0,0.3); --shadow-lg:0 8px 28px rgba(0,0,0,0.5);
|
||||
--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 {
|
||||
@@ -40,31 +50,56 @@ body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ---- Шапка ---- */
|
||||
/* ---- Шапка (панель управления) ---- */
|
||||
.topbar {
|
||||
display:flex; align-items:center; gap:20px;
|
||||
padding:0 24px; height:58px;
|
||||
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:9px; font-weight:700; font-size:16px; letter-spacing:-0.01em; }
|
||||
.brand .logo { width:26px; height:26px; border-radius:7px; background:linear-gradient(135deg,var(--accent),var(--brand)); display:inline-block; }
|
||||
.nav { display:flex; align-items:center; gap:2px; margin-left:8px; }
|
||||
.nav .group-label { font-size:10px; text-transform:uppercase; letter-spacing:0.06em; color:var(--muted); padding:0 8px 0 14px; border-left:1px solid var(--border); margin-left:6px; }
|
||||
.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:13.5px; font-weight:500;
|
||||
padding:7px 11px; border-radius:7px; white-space:nowrap;
|
||||
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); }
|
||||
.topbar-right { margin-left:auto; display:flex; align-items:center; gap:14px; }
|
||||
.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:1200px){ .sys-status span:not(.pulse){ display:none } .sys-status{ padding:5px 8px } .topbar .clock{ 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:8px; cursor:pointer; font-size:15px;
|
||||
display:flex; align-items:center; justify-content:center; padding:0;
|
||||
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:12.5px; color:var(--muted); font-variant-numeric:tabular-nums; }
|
||||
.topbar .clock { font-size:12px; color:var(--muted); font-family:var(--mono); font-variant-numeric:tabular-nums; letter-spacing:-0.02em; }
|
||||
|
||||
/* ---- Контент ---- */
|
||||
main { padding:24px; max-width:1200px; margin:0 auto; }
|
||||
@@ -83,10 +118,12 @@ a:hover { text-decoration:underline; }
|
||||
.card h2:first-child { margin-top:0; }
|
||||
.grid { display:grid; gap:14px; grid-template-columns:repeat(auto-fit, minmax(220px,1fr)); }
|
||||
|
||||
/* ---- Статы ---- */
|
||||
.stat { padding:16px 18px; background:var(--card); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); }
|
||||
.stat-label { font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; font-weight:600; }
|
||||
.stat-value { font-size:26px; font-weight:700; margin-top:6px; letter-spacing:-0.02em; }
|
||||
/* ---- Статы (приборные читалки) ---- */
|
||||
.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; }
|
||||
@@ -152,13 +189,15 @@ label { font-size:13px; font-weight:500; }
|
||||
text-decoration:none; color:var(--text); transition:transform .14s, box-shadow .14s, border-color .14s;
|
||||
min-height:128px;
|
||||
}
|
||||
.tile:hover { transform:translateY(-3px); box-shadow:var(--shadow-lg); border-color:var(--accent); text-decoration:none; }
|
||||
.tile .ico { width:46px; height:46px; border-radius:12px; display:flex; align-items:center; justify-content:center; font-size:24px; background:var(--accent-weak); }
|
||||
.tile.brand .ico { background:linear-gradient(135deg,var(--accent),var(--brand)); }
|
||||
.tile .t-title { font-size:16px; font-weight:640; letter-spacing:-0.01em; }
|
||||
.tile .t-sub { font-size:12.5px; color:var(--muted); margin-top:-4px; }
|
||||
.tile .t-arrow { margin-top:auto; color:var(--muted); font-size:18px; }
|
||||
.tile:hover .t-arrow { color:var(--accent); }
|
||||
.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; }
|
||||
@@ -176,7 +215,9 @@ label { font-size:13px; font-weight:500; }
|
||||
}
|
||||
.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 { font-size:16px; width:20px; text-align:center; }
|
||||
.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); }
|
||||
@@ -190,8 +231,35 @@ label { font-size:13px; font-weight:500; }
|
||||
.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:72px; right:24px; z-index:9999; max-width:520px; padding:14px 18px; background:var(--card); border-left:4px solid var(--ok); 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, transform .25s; }
|
||||
#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); }
|
||||
@@ -201,26 +269,52 @@ label { font-size:13px; font-weight:500; }
|
||||
<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">
|
||||
<span class="brand"><span class="logo"></span>Bridge&Join</span>
|
||||
<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">
|
||||
<span class="group-label">Оператор</span>
|
||||
<a href="/admin/" class="{{if eq .Active "home"}}active{{end}}">Дашборд</a>
|
||||
<a href="/admin/claims" class="{{if eq .Active "claims"}}active{{end}}">Переводы</a>
|
||||
<a href="/admin/news" class="{{if eq .Active "news"}}active{{end}}">События</a>
|
||||
<span class="group-label">Администратор</span>
|
||||
{{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 "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}}
|
||||
{{if can "settings"}}<span class="group-label">Администратор</span>
|
||||
<a href="/admin/setup" class="{{if eq .Active "setup"}}active{{end}}">Настройка</a>
|
||||
<a href="/admin/status" class="{{if eq .Active "status"}}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>
|
||||
</nav>
|
||||
<div class="topbar-right">
|
||||
{{with curUser}}<span class="user-chip" title="{{.RoleTitle}}{{if eq .Source "ad"}} · вход через AD{{end}}"><span class="user-name">{{if .Name}}{{.Name}}{{else}}{{.Login}}{{end}}</span><span class="user-role">{{.RoleTitle}}</span></span><a class="user-logout" href="/logout" title="Выйти">Выйти</a>{{else}}{{if authOpen}}<a class="user-logout" href="/admin/setup#users" title="Пользователи не созданы — вход открыт">без входа</a>{{end}}{{end}}
|
||||
<span class="sys-status {{if .IsMockMode}}warn{{end}}" title="Состояние обмена с НРД">
|
||||
<span class="pulse"></span>{{if .IsMockMode}}Эмуляция{{else}}НРД на связи{{end}}</span>
|
||||
<span class="clock">{{.Now}}</span>
|
||||
<button class="theme-toggle" id="theme-toggle" title="Светлая/тёмная тема" aria-label="Переключить тему">🌙</button>
|
||||
<button class="theme-toggle" id="theme-toggle" title="Светлая/тёмная тема" aria-label="Переключить тему"></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{if .IsMockMode}}
|
||||
<div class="banner-mock">
|
||||
<span style="font-size:16px">🟡</span>
|
||||
<span class="dot warn" style="width:9px;height:9px;margin:0"></span>
|
||||
<div><strong style="color:var(--warn)">Режим эмуляции</strong> — реального обмена с НРД нет. <span class="muted">{{.MockReason}}</span></div>
|
||||
<a href="/admin/wizard" style="margin-left:auto">Открыть мастер настройки →</a>
|
||||
</div>
|
||||
@@ -235,9 +329,11 @@ label { font-size:13px; font-weight:500; }
|
||||
// --- Тема: 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.textContent = (t === 'dark') ? '☀️' : '🌙';
|
||||
toggle.innerHTML = (t === 'dark') ? icoSun : icoMoon;
|
||||
try { localStorage.setItem('bj-theme', t); } catch(e) {}
|
||||
}
|
||||
var saved = 'light';
|
||||
@@ -258,28 +354,140 @@ label { font-size:13px; font-weight:500; }
|
||||
}
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var flash = params.get('flash');
|
||||
if (flash) {
|
||||
showToast(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) {
|
||||
window.requestAnimationFrame(function() {
|
||||
window.scrollTo(0, parseInt(sc, 10));
|
||||
try { sessionStorage.removeItem('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>
|
||||
|
||||
Reference in New Issue
Block a user