From 46e55e12776a957fb1a1625f3733ca3f7392f427 Mon Sep 17 00:00:00 2001 From: zuevav Date: Fri, 18 Sep 2026 11:55:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=96=D1=83=D1=80=D0=BD=D0=B0=D0=BB:=20=D0=BE?= =?UTF-8?q?=D0=BD=D0=BB=D0=B0=D0=B9=D0=BD-=D1=81=D1=82=D0=B0=D1=82=D1=83?= =?UTF-8?q?=D1=81=20=C2=AB=D1=87=D0=B5=D0=B3=D0=BE=20=D0=B6=D0=B4=D1=91?= =?UTF-8?q?=D1=82=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B0=C2=BB,=20=D1=81?= =?UTF-8?q?=D1=81=D1=8B=D0=BB=D0=BA=D0=B0=20=D0=B8=D0=B7=20=D1=80=D1=83?= =?UTF-8?q?=D1=87=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B2=D0=B2=D0=BE=D0=B4=D0=B0?= =?UTF-8?q?=20=D0=B2=20=D0=B6=D1=83=D1=80=D0=BD=D0=B0=D0=BB,=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D1=84=D0=BE=D1=80=D0=BC=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - livestatus.go: одна фраза состояния для исходящих (сделка M2M + черновик/подача/MS010 поручения 16) и входящих (наше решение → поручение 16/1); признак «в работе» - строки журналов и карточки показывают её и обновляют по /admin/journal/live каждые 8 с, пока заявка в работе; завершённые и отклонённые не опрашиваются - ручной ввод: у отправленного поручения ссылка «в журнале →» и тот же онлайн-статус; вид документа занимает две колонки - выбор участника: по ИНН и коду с кнопки, а не по номеру строки; Enter выбирает только при единственном совпадении Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL --- internal/lkgateway/admin.go | 2 + internal/lkgateway/journal.go | 9 +- internal/lkgateway/livestatus.go | 132 ++++++++++++++++++ .../lkgateway/web/templates/admin_claim.html | 1 + .../lkgateway/web/templates/admin_claims.html | 2 +- .../web/templates/admin_incoming.html | 2 +- .../web/templates/admin_incoming_detail.html | 1 + .../web/templates/admin_journal.html | 2 +- .../lkgateway/web/templates/admin_manual.html | 16 ++- .../lkgateway/web/templates/admin_setup.html | 8 +- internal/lkgateway/web/templates/layout.html | 29 ++++ 11 files changed, 190 insertions(+), 14 deletions(-) create mode 100644 internal/lkgateway/livestatus.go diff --git a/internal/lkgateway/admin.go b/internal/lkgateway/admin.go index ccc2174..65512a1 100644 --- a/internal/lkgateway/admin.go +++ b/internal/lkgateway/admin.go @@ -83,6 +83,8 @@ var templateFuncs = template.FuncMap{ }, // inStatus — статус входящего перевода (класс/текст/исход принято-отказано). "inStatus": incomingStatusOf, + "liveOut": liveOutgoing, + "liveIn": liveIncoming, // healthLabel — человекочитаемое имя фонового компонента для панели здоровья. "healthLabel": HealthLabel, // dur — округление длительности до секунд для вывода возраста отметки. diff --git a/internal/lkgateway/journal.go b/internal/lkgateway/journal.go index befc784..b477b47 100644 --- a/internal/lkgateway/journal.go +++ b/internal/lkgateway/journal.go @@ -24,6 +24,8 @@ type journalRow struct { StatusText string // человекочитаемый текст статуса (русский) Outcome string // для входящих: «Принято» / «Отказано» / «Частично …» Href string + LiveKey string // out: | in: — для онлайн-обновления строки + Live LiveStatus // чего ждёт заявка } // incomingJournalStatus — статус входящего для журнала: класс бейджа (цвет), @@ -63,6 +65,7 @@ func incomingStatusOf(it *IncomingTransfer) incomingStatusVM { // registerJournal вешает /admin/journal — объединённая лента вход+исход. func registerJournal(mux *http.ServeMux, a *admin, svc *Service, store *IncomingStore) { + registerLiveStatus(mux, svc, store) // «чего ждёт заявка» для строк в работе mux.HandleFunc("/admin/journal", func(w http.ResponseWriter, r *http.Request) { var rows []journalRow @@ -79,7 +82,8 @@ func registerJournal(mux *http.ServeMux, a *admin, svc *Service, store *Incoming Time: c.CreatedAt, Dir: "Исходящий", DirKind: "out", Party: party, SecN: len(c.Securities), Status: c.Status, StatusText: russianState(c.Status), - Href: "/admin/claims/" + c.ID, + Href: "/admin/claims/" + c.ID, + LiveKey: "out:" + c.ID, Live: liveOutgoing(c), }) } } @@ -99,7 +103,8 @@ func registerJournal(mux *http.ServeMux, a *admin, svc *Service, store *Incoming Time: it.ReceivedAt, Dir: "Входящий", DirKind: "in", Party: party, SecN: len(it.Securities), Status: cls, StatusText: txt, Outcome: outcome, - Href: "/admin/incoming/" + it.ID, + Href: "/admin/incoming/" + it.ID, + LiveKey: "in:" + it.ID, Live: liveIncoming(it), }) } sort.Slice(rows, func(i, j int) bool { return rows[i].Time.After(rows[j].Time) }) diff --git a/internal/lkgateway/livestatus.go b/internal/lkgateway/livestatus.go new file mode 100644 index 0000000..87e968a --- /dev/null +++ b/internal/lkgateway/livestatus.go @@ -0,0 +1,132 @@ +package lkgateway + +// livestatus.go — «чего ждёт заявка»: одна строка состояния для журналов и карточек. +// +// Статус сделки (черновик, отправлена, ожидает решение…) говорит, где заявка, но не говорит, +// чего мы ждём: черновика поручения от НРД, подачи, ответа MS010, решения оператора. Здесь это +// сводится в одну фразу с признаком «в работе». Пока заявка в работе, страницы обновляют +// строку по /admin/journal/live каждые несколько секунд; завершённые и отклонённые не +// опрашиваются — там ничего не изменится. + +import ( + "context" + "encoding/json" + "net/http" + "strings" + "time" +) + +// LiveStatus — состояние для показа. +type LiveStatus struct { + Text string `json:"text"` // чего ждём / чем закончилось + Kind string `json:"kind"` // wait | warn | ok | bad + Active bool `json:"active"` // в работе: страница продолжает опрашивать +} + +// liveOutgoing — состояние исходящего перевода: сделка M2M + депозитарное поручение 16. +func liveOutgoing(c ClaimView) LiveStatus { + guid := string(c.M2MGUID) + var depo *OutgoingDeal + if guid != "" && globalOutgoingStore != nil { + if d, ok := scanForm16Deal(globalOutgoingStore, guid); ok { + depo = d + } + } + var watch *OrderWatch + if guid != "" && globalOrderWatch != nil { + watch = globalOrderWatch.Get(guid) + } + switch c.Status { + case "draft", "validated": + return LiveStatus{Text: "ожидает отправки в НРД", Kind: "wait", Active: true} + case "submitted_to_nsd": + return LiveStatus{Text: "отправлена в НРД, ожидает подтверждения приёма", Kind: "wait", Active: true} + case "awaiting_decision": + return LiveStatus{Text: "ожидает решения принимающего депозитария", Kind: "wait", Active: true} + case "manual_approval": + return LiveStatus{Text: "ожидает решения оператора", Kind: "warn", Active: true} + case "awaiting_sub16": + return LiveStatus{Text: "принята, ожидает депозитарного исполнения", Kind: "wait", Active: true} + case "rejected": + return LiveStatus{Text: "отклонена", Kind: "bad"} + case "timed_out": + return LiveStatus{Text: "истёк срок ответа контрагента", Kind: "bad"} + case "done": + return LiveStatus{Text: "исполнена", Kind: "ok"} + case "confirmed": + switch { + case depo == nil: + return LiveStatus{Text: "принята контрагентом, ожидает черновика поручения 16 от НРД", Kind: "wait", Active: true} + case !depo.Submitted: + return LiveStatus{Text: "черновик поручения 16 получен, ожидает подачи в НРД", Kind: "warn", Active: true} + case depo.Done: + return LiveStatus{Text: "поручение 16 исполнено: MS010 получен", Kind: "ok"} + case watch != nil: + k := watch.Kind() + if k == "" { + k = "wait" + } + return LiveStatus{Text: "поручение 16 подано: " + watch.StatusRu(), Kind: k, Active: k == "wait" || k == "warn"} + default: + return LiveStatus{Text: "поручение 16 подано, ожидает ответа НРД (MS010)", Kind: "wait", Active: true} + } + } + return LiveStatus{Text: russianState(c.Status), Kind: "wait"} +} + +// liveIncoming — состояние входящего перевода: наше решение, затем поручение 16/1. +func liveIncoming(it *IncomingTransfer) LiveStatus { + if it == nil { + return LiveStatus{} + } + if it.Status != "answered" { + return LiveStatus{Text: "ожидает нашего решения", Kind: "warn", Active: true} + } + _, _, outcome := incomingJournalStatus(it) + if outcome == "Отказано" { + return LiveStatus{Text: "отказано", Kind: "bad"} + } + if globalOrderWatch != nil { + if w := globalOrderWatch.Get(it.GUID); w != nil { + k := w.Kind() + return LiveStatus{Text: "поручение 16/1 подано: " + w.StatusRu(), Kind: k, Active: k == "wait" || k == "warn"} + } + } + if strings.HasPrefix(outcome, "Частично") { + return LiveStatus{Text: strings.ToLower(outcome) + "; ожидает поручения 16/1", Kind: "wait", Active: true} + } + return LiveStatus{Text: "принято, ожидает подачи поручения 16/1", Kind: "wait", Active: true} +} + +// registerLiveStatus — GET /admin/journal/live?out=id,id&in=id,id → состояния для строк в работе. +func registerLiveStatus(mux *http.ServeMux, svc *Service, store *IncomingStore) { + mux.HandleFunc("/admin/journal/live", func(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + out := map[string]LiveStatus{} + for _, id := range splitIDs(r.URL.Query().Get("out")) { + if c, err := svc.GetClaim(ctx, id); err == nil { + out[id] = liveOutgoing(c) + } + } + in := map[string]LiveStatus{} + for _, id := range splitIDs(r.URL.Query().Get("in")) { + if it, ok := store.Get(id); ok { + in[id] = liveIncoming(it) + } + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Cache-Control", "no-store") + _ = json.NewEncoder(w).Encode(map[string]any{"out": out, "in": in}) + }) +} + +func splitIDs(s string) []string { + var out []string + for _, p := range strings.Split(s, ",") { + if p = strings.TrimSpace(p); p != "" && len(p) <= 64 && len(out) < 200 { + out = append(out, p) + } + } + return out +} diff --git a/internal/lkgateway/web/templates/admin_claim.html b/internal/lkgateway/web/templates/admin_claim.html index 7807048..62160c7 100644 --- a/internal/lkgateway/web/templates/admin_claim.html +++ b/internal/lkgateway/web/templates/admin_claim.html @@ -31,6 +31,7 @@ {{if .Flash}}
{{.Flash}}
{{end}}

Заявка {{slice .Claim.ID 0 8}} · {{ruState .Claim.Status}}

+ {{$l := liveOut .Claim}}
{{$l.Text}}
diff --git a/internal/lkgateway/web/templates/admin_claims.html b/internal/lkgateway/web/templates/admin_claims.html index e4655f7..fe671c3 100644 --- a/internal/lkgateway/web/templates/admin_claims.html +++ b/internal/lkgateway/web/templates/admin_claims.html @@ -14,7 +14,7 @@ - + {{end}} diff --git a/internal/lkgateway/web/templates/admin_incoming.html b/internal/lkgateway/web/templates/admin_incoming.html index d138374..9368b91 100644 --- a/internal/lkgateway/web/templates/admin_incoming.html +++ b/internal/lkgateway/web/templates/admin_incoming.html @@ -16,7 +16,7 @@ - + {{end}} diff --git a/internal/lkgateway/web/templates/admin_incoming_detail.html b/internal/lkgateway/web/templates/admin_incoming_detail.html index c52fe10..837239f 100644 --- a/internal/lkgateway/web/templates/admin_incoming_detail.html +++ b/internal/lkgateway/web/templates/admin_incoming_detail.html @@ -21,6 +21,7 @@

Входящий перевод {{short 8 .It.GUID}} {{if $answered}}отвечено{{else}}ждёт решения{{end}}

+ {{$l := liveIn .It}}
{{$l.Text}}
Создана{{.Claim.CreatedAt.Format "02.01.2006 15:04:05"}}
Обновлена{{.Claim.UpdatedAt.Format "02.01.2006 15:04:05"}}
{{len .Securities}} {{$f := m2mParty .TransferringDepositoryINN}}{{if $f}}{{$f}}
{{end}}{{.TransferringDepositoryINN}}
{{$t := m2mParty .ReceivingDepositoryINN}}{{if $t}}{{$t}}
{{end}}{{.ReceivingDepositoryINN}}
{{ruState .Status}}{{$l := liveOut .}}{{ruState .Status}}
{{$l.Text}}
детали →
{{short 8 .GUID}} {{$nm := m2mParty .SenderCode}}{{if not $nm}}{{$nm = m2mParty .SenderINN}}{{end}}{{if $nm}}{{$nm}}
{{end}}{{.SenderCode}}{{if .SenderINN}} · ИНН {{.SenderINN}}{{end}}
{{len .Securities}}{{$s := inStatus .}}{{$s.Text}}{{if $s.Outcome}} {{$s.Outcome}}{{end}}{{if .DepoSummary}}
сверка: {{.DepoSummary}}{{end}}
{{$s := inStatus .}}{{$s.Text}}{{if $s.Outcome}} {{$s.Outcome}}{{end}}{{if .DepoSummary}}
сверка: {{.DepoSummary}}{{end}}{{$l := liveIn .}}
{{$l.Text}}
{{if eq .Status "answered"}}просмотр{{else}}ответить{{end}} →
diff --git a/internal/lkgateway/web/templates/admin_journal.html b/internal/lkgateway/web/templates/admin_journal.html index 2c3628b..c805346 100644 --- a/internal/lkgateway/web/templates/admin_journal.html +++ b/internal/lkgateway/web/templates/admin_journal.html @@ -12,7 +12,7 @@ - + {{end}} diff --git a/internal/lkgateway/web/templates/admin_manual.html b/internal/lkgateway/web/templates/admin_manual.html index 99a402d..da46670 100644 --- a/internal/lkgateway/web/templates/admin_manual.html +++ b/internal/lkgateway/web/templates/admin_manual.html @@ -7,6 +7,8 @@ .mf-grid label{display:block;font-size:12px;color:var(--muted);margin-bottom:3px} .mf-grid input,.mf-grid select,.mf-grid textarea{width:100%;padding:7px 9px;border-radius:8px;border:1px solid var(--border);background:var(--card);color:var(--text);font-size:13.5px;box-sizing:border-box} .mf-grid .wide{grid-column:1/-1} + .mf-grid .w2{grid-column:span 2} + @media (max-width:520px){.mf-grid .w2{grid-column:auto}} .mf-h{font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:18px 0 8px} .mf-sec{display:grid;grid-template-columns:1.2fr .9fr 2fr .7fr auto;gap:8px;align-items:center;margin-bottom:6px} .mf-sec input{width:100%;padding:7px 9px;border-radius:8px;border:1px solid var(--border);background:var(--card);color:var(--text);font-size:13.5px;box-sizing:border-box} @@ -82,8 +84,8 @@ - - + + {{end}} @@ -317,7 +319,7 @@ function mfRerun(){const f=document.getElementById('mf');if(f&&f.closest('detail {{end}}
-
+
@@ -406,14 +408,16 @@ function hbBind(inp,box,list,onPick){ const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); let hits=[],picked=inp.dataset.picked==='1'; const q=()=>picked?'':inp.value; - const draw=()=>{hits=hbFind(list,q());box.innerHTML=(hits.length?'
'+(q().trim()?'Найдено: ':'Все участники: ')+hits.length+'
'+hits.map((p,i)=>'').join(''):'
В справочнике не найдено — введите ИНН и наименование в поля ниже.
');box.classList.add('open')}; + const draw=()=>{hits=hbFind(list,q());box.innerHTML=(hits.length?'
'+(q().trim()?'Найдено: ':'Все участники: ')+hits.length+'
'+hits.map((p,i)=>'').join(''):'
В справочнике не найдено — введите ИНН и наименование в поля ниже.
');box.classList.add('open')}; const close=()=>{box.innerHTML='';box.classList.remove('open')}; inp.addEventListener('focus',()=>{if(picked)inp.select();draw()}); inp.addEventListener('input',()=>{picked=false;draw()}); - inp.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();if(hits[0]){pick(hits[0])}}if(e.key==='Escape')close()}); + // Enter выбирает только при единственном совпадении: иначе молча ушёл бы первый попавшийся. + inp.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();if(hits.length===1)pick(hits[0]);else if(hits.length>1){const m=box.querySelector('.hb-m');if(m)m.textContent='Найдено: '+hits.length+' — уточните запрос или выберите мышью'}}if(e.key==='Escape')close()}); const pick=p=>{onPick(p);picked=true;close()}; box.addEventListener('mousedown',e=>e.preventDefault()); - box.addEventListener('click',e=>{const b=e.target.closest('.hb-hit');if(b)pick(hits[+b.dataset.i])}); + // Участник — по ИНН и коду с самой кнопки, а не по номеру строки: список мог перерисоваться между показом и нажатием. + box.addEventListener('click',e=>{const b=e.target.closest('.hb-hit');if(!b)return;const p=list.find(x=>String(x.inn)===b.dataset.inn&&String(x.code)===b.dataset.code)||hits[+b.dataset.i];if(p)pick(p)}); inp.addEventListener('blur',()=>setTimeout(close,150)); const tg=inp.parentNode.querySelector('.hb-toggle'); if(tg)tg.addEventListener('click',()=>{if(box.classList.contains('open'))close();else{inp.focus();draw()}}); diff --git a/internal/lkgateway/web/templates/admin_setup.html b/internal/lkgateway/web/templates/admin_setup.html index 37e8149..bbf22b7 100644 --- a/internal/lkgateway/web/templates/admin_setup.html +++ b/internal/lkgateway/web/templates/admin_setup.html @@ -520,14 +520,16 @@ function hbBind(inp,box,list,onPick){ const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); let hits=[],picked=inp.dataset.picked==='1'; const q=()=>picked?'':inp.value; - const draw=()=>{hits=hbFind(list,q());box.innerHTML=(hits.length?'
'+(q().trim()?'Найдено: ':'Все участники: ')+hits.length+'
'+hits.map((p,i)=>'').join(''):'
В справочнике не найдено — введите ИНН и наименование в поля ниже.
');box.classList.add('open')}; + const draw=()=>{hits=hbFind(list,q());box.innerHTML=(hits.length?'
'+(q().trim()?'Найдено: ':'Все участники: ')+hits.length+'
'+hits.map((p,i)=>'').join(''):'
В справочнике не найдено — введите ИНН и наименование в поля ниже.
');box.classList.add('open')}; const close=()=>{box.innerHTML='';box.classList.remove('open')}; inp.addEventListener('focus',()=>{if(picked)inp.select();draw()}); inp.addEventListener('input',()=>{picked=false;draw()}); - inp.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();if(hits[0]){pick(hits[0])}}if(e.key==='Escape')close()}); + // Enter выбирает только при единственном совпадении: иначе молча ушёл бы первый попавшийся. + inp.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();if(hits.length===1)pick(hits[0]);else if(hits.length>1){const m=box.querySelector('.hb-m');if(m)m.textContent='Найдено: '+hits.length+' — уточните запрос или выберите мышью'}}if(e.key==='Escape')close()}); const pick=p=>{onPick(p);picked=true;close()}; box.addEventListener('mousedown',e=>e.preventDefault()); - box.addEventListener('click',e=>{const b=e.target.closest('.hb-hit');if(b)pick(hits[+b.dataset.i])}); + // Участник — по ИНН и коду с самой кнопки, а не по номеру строки: список мог перерисоваться между показом и нажатием. + box.addEventListener('click',e=>{const b=e.target.closest('.hb-hit');if(!b)return;const p=list.find(x=>String(x.inn)===b.dataset.inn&&String(x.code)===b.dataset.code)||hits[+b.dataset.i];if(p)pick(p)}); inp.addEventListener('blur',()=>setTimeout(close,150)); const tg=inp.parentNode.querySelector('.hb-toggle'); if(tg)tg.addEventListener('click',()=>{if(box.classList.contains('open'))close();else{inp.focus();draw()}}); diff --git a/internal/lkgateway/web/templates/layout.html b/internal/lkgateway/web/templates/layout.html index ff2144a..de59add 100644 --- a/internal/lkgateway/web/templates/layout.html +++ b/internal/lkgateway/web/templates/layout.html @@ -150,6 +150,14 @@ tbody tr:hover td { background:var(--accent-weak); } /* ---- Бейджи статусов ---- */ .badge { display:inline-block; padding:3px 9px; border-radius:20px; font-size:11px; font-weight:600; } +.live { font-size:11.5px; margin-top:4px; color:var(--muted); display:flex; align-items:center; gap:6px; line-height:1.3 } +.live::before { content:''; width:7px; height:7px; border-radius:50%; background:var(--muted); flex:none } +.live-wait::before { background:var(--accent); animation:pulse 2.4s var(--ease) infinite } +.live-warn { color:var(--warn) } .live-warn::before { background:var(--warn); animation:pulse 2.4s var(--ease) infinite } +.live-ok { color:var(--ok) } .live-ok::before { background:var(--ok) } +.live-bad { color:var(--err) } .live-bad::before { background:var(--err) } +.live-big { font-size:14px; margin:-6px 0 12px; font-weight:500 } +.live[data-active] { color:var(--text-2) } .live-warn[data-active] { color:var(--warn) } .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); } @@ -524,6 +532,27 @@ label { font-size:13px; font-weight:500; } } poll(); setInterval(poll, 12000); // каждые 12 сек + // Онлайн-статус заявок в работе: строки с data-live и data-active опрашиваются, пока ждут. + (function(){ + function tick(){ + var els = document.querySelectorAll('.live[data-live][data-active]'); + if (!els.length) return; + var out = [], inn = []; + els.forEach(function(e){ var k = e.getAttribute('data-live').split(':'); (k[0]==='out'?out:inn).push(k[1]); }); + var q = 'out=' + encodeURIComponent(out.join(',')) + '&in=' + encodeURIComponent(inn.join(',')); + fetch('/admin/journal/live?' + q, {cache:'no-store'}).then(function(r){ return r.json(); }).then(function(d){ + els.forEach(function(e){ + var k = e.getAttribute('data-live').split(':'), st = (d[k[0]]||{})[k[1]]; + if (!st) return; + e.textContent = st.text; + e.className = 'live ' + (e.classList.contains('live-big') ? 'live-big ' : '') + 'live-' + st.kind; + if (!st.active) e.removeAttribute('data-active'); + }); + }).catch(function(){}); + } + setInterval(tick, 8000); + if (document.querySelector('.live[data-live][data-active]')) setTimeout(tick, 1500); + })(); })(); })();
Запрос сформирован (отправитель){{msk .It.RequestCreated}}
Забрали из шлюза (получено нами){{msk .It.ReceivedAt}}{{if not .It.RequestCreated.IsZero}} · прохождение: {{transit .It.RequestCreated .It.ReceivedAt}}{{end}}
{{if eq .DirKind "out"}}↗ Исходящий{{else}}↘ Входящий{{end}} {{.Party}} {{.SecN}}{{.StatusText}}{{if .Outcome}} {{.Outcome}}{{end}}{{.StatusText}}{{if .Outcome}} {{.Outcome}}{{end}}
{{.Live.Text}}
открыть →
{{.O.FullName}} {{len .O.Securities}} {{if .O.RecvName}}{{.O.RecvName}}{{else}}ИНН {{.O.RecvINN}}{{end}}{{.StateRu}}открыть →{{.StateRu}}{{if .O.DealID}}
…
{{end}}
открыть →{{if .O.DealID}} · в журнале →{{end}}