Ручной ввод: без обязательных галок, удаление черновика, запрет количества больше остатка; отказ НРД по поручению — красным

- ручных отметок перед отправкой нет: автор виден по учётной записи, клиент/счёт/остаток/
  бумага проверены сверкой с DEPO при сохранении; блокировка в QUIK — необязательно,
  по умолчанию не требуется (модуль неторговых поручений сейчас не используется)
- «Удалить черновик» в карточке (POST /admin/manual/{id}/delete), только для черновиков
- количество не больше остатка на торговом разделе: проверка в форме (по выбранной бумаге)
  и на сервере при сохранении (по счёту DEPO) — поручение с превышением не сохраняется
- отчёт MS010 с отказом: orderResponse.Rejected, OutgoingDeal.Rejected; шаг «Операция
  завершена отказом» и бейдж — красные в карточках исходящего и входящего; онлайн-статус
  «поручение 16 завершено отказом НРД»

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
zuevav
2026-09-18 14:39:24 +03:00
parent b9da2ca0ff
commit d2024b44ca
11 changed files with 171 additions and 54 deletions
@@ -162,12 +162,17 @@
.ord-step.wait .dot{background:#f59e0b;color:#fff;box-shadow:0 0 0 4px color-mix(in srgb,#f59e0b 28%,transparent)}
.ord-step.wait .s{color:#b45309}
.ord-step.todo{opacity:.55}
.ord-step.bad{border-color:#dc2626;background:color-mix(in srgb,#dc2626 12%,var(--card))}
.ord-step.bad .dot{background:#dc2626;color:#fff}
.ord-step.bad .s{color:#b3261e}
.ord-dl.bad{border-color:#dc2626;color:#b3261e}
.ord-dl{display:inline-block;margin-top:9px;font-size:11.5px;font-weight:700;padding:4px 10px;border-radius:8px;background:var(--card);border:1px solid #16a34a;color:#0f7a37;text-decoration:none;white-space:nowrap}
.ord-dl:hover{background:color-mix(in srgb,#16a34a 12%,var(--card))}
</style>
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<h2 style="margin:0">Депозитарное поручение 16 (списание)</h2>
{{if .Depo.Done}}<span class="badge confirmed">проведено · MS010</span>
{{if and .Depo.Done .Depo.Rejected}}<span class="badge rejected">завершено отказом · MS010</span>
{{else if .Depo.Done}}<span class="badge confirmed">проведено · MS010</span>
{{else if .Depo.Submitted}}<span class="badge awaiting_decision">подано, ждём MS010</span>
{{else}}<span class="badge" style="background:var(--card-2);color:var(--muted)">требует подачи</span>{{end}}
</div>
@@ -186,10 +191,10 @@
<div><div class="t">Поручение 16 подано</div><div class="s">{{if .Depo.Submitted}}{{msk .Depo.OrderAt}}{{else}}нужно подать в НРД{{end}}</div>
{{if .Depo.OrderXML}}<a class="ord-dl" href="/admin/outgoing/{{.Depo.GUID}}/order.xml">⬇ Отправленная форма 16</a>{{end}}</div>
</div>
<div class="ord-step {{if .Depo.Done}}done{{else if .Depo.Submitted}}wait{{else}}todo{{end}}">
<span class="dot">{{if .Depo.Done}}✓{{else}}3{{end}}</span>
<div><div class="t">Списание проведено · MS010</div><div class="s">{{if .Depo.Done}}отчёт получен от НРД{{else if .Depo.Submitted}}ждём встречный 16/1 + MS010{{else}}после подачи{{end}}</div>
{{if .Depo.Responses}}<a class="ord-dl" href="/admin/outgoing/{{.Depo.GUID}}/ms010.zip">⬇ Ответ НРД (MS010)</a>{{end}}</div>
<div class="ord-step {{if and .Depo.Done .Depo.Rejected}}bad{{else if .Depo.Done}}done{{else if .Depo.Submitted}}wait{{else}}todo{{end}}">
<span class="dot">{{if and .Depo.Done .Depo.Rejected}}✕{{else if .Depo.Done}}✓{{else}}3{{end}}</span>
<div><div class="t">{{if and .Depo.Done .Depo.Rejected}}Операция завершена отказом · MS010{{else}}Списание проведено · MS010{{end}}</div><div class="s">{{if and .Depo.Done .Depo.Rejected}}НРД отказал в исполнении — см. отчёт{{else if .Depo.Done}}отчёт получен от НРД{{else if .Depo.Submitted}}ждём встречный 16/1 + MS010{{else}}после подачи{{end}}</div>
{{if .Depo.Responses}}<a class="ord-dl{{if .Depo.Rejected}} bad{{end}}" href="/admin/outgoing/{{.Depo.GUID}}/ms010.zip">⬇ Ответ НРД (MS010)</a>{{end}}</div>
</div>
</div>
@@ -226,6 +226,10 @@
.ord-step.wait .dot{background:#f59e0b;color:#fff;box-shadow:0 0 0 4px color-mix(in srgb,#f59e0b 28%,transparent)}
.ord-step.wait .s{color:#b45309}
.ord-step.todo{opacity:.55}
.ord-step.bad{border-color:#dc2626;background:color-mix(in srgb,#dc2626 12%,var(--card))}
.ord-step.bad .dot{background:#dc2626;color:#fff}
.ord-step.bad .s{color:#b3261e}
.ord-dl.bad{border-color:#dc2626;color:#b3261e}
.ord-dl{display:inline-block;margin-top:9px;font-size:11.5px;font-weight:700;padding:4px 10px;border-radius:8px;background:var(--card);border:1px solid #16a34a;color:#0f7a37;text-decoration:none;white-space:nowrap}
.ord-dl:hover{background:color-mix(in srgb,#16a34a 12%,var(--card))}
</style>
@@ -265,9 +269,9 @@
<div><div class="t">Поручение подано в НРД</div><div class="s">{{if .It.OrderSubmitted}}{{msk .It.OrderAt}}{{else if .AutoMode}}bj подаст автоматически по черновику{{else if .DraftReceived}}нужно подать{{else}}после черновика{{end}}</div>
{{if .It.OrderXML}}<a class="ord-dl" href="/admin/incoming/{{.It.ID}}/order.xml">⬇ Отправленная форма 16/1</a>{{end}}</div>
</div>
<div class="ord-step {{if .OrderDone}}done{{else if .It.OrderSubmitted}}wait{{else}}todo{{end}}">
<span class="dot">{{if .OrderDone}}✓{{else}}3{{end}}</span>
<div><div class="t">Проведено · MS010</div><div class="s">{{if .OrderDone}}{{if .OrderDoneAt}}{{.OrderDoneAt}}{{else}}отчёт получен от НРД{{end}}{{else if .It.OrderSubmitted}}ждём MS010 от НРД{{else}}после подачи{{end}}</div>
<div class="ord-step {{if and .OrderDone .OrderRejected}}bad{{else if .OrderDone}}done{{else if .It.OrderSubmitted}}wait{{else}}todo{{end}}">
<span class="dot">{{if and .OrderDone .OrderRejected}}✕{{else if .OrderDone}}✓{{else}}3{{end}}</span>
<div><div class="t">{{if and .OrderDone .OrderRejected}}Операция завершена отказом · MS010{{else}}Проведено · MS010{{end}}</div><div class="s">{{if and .OrderDone .OrderRejected}}НРД отказал в исполнении{{if .OrderDoneAt}} · {{.OrderDoneAt}}{{end}}{{else if .OrderDone}}{{if .OrderDoneAt}}{{.OrderDoneAt}}{{else}}отчёт получен от НРД{{end}}{{else if .It.OrderSubmitted}}ждём MS010 от НРД{{else}}после подачи{{end}}</div>
{{if .OrderResponses}}<a class="ord-dl" href="/admin/incoming/{{.It.ID}}/ms010.zip">⬇ Ответ НРД (MS010)</a>{{end}}</div>
</div>
</div>
@@ -286,7 +290,7 @@
<table style="margin-top:8px">
<thead><tr><th style="width:150px">Время</th><th>Ответ НРД</th></tr></thead>
<tbody>
{{range .OrderResponses}}<tr><td style="white-space:nowrap">{{.Time}}</td><td>{{.Kind}}</td></tr>{{end}}
{{range .OrderResponses}}<tr><td style="white-space:nowrap">{{.Time}}</td><td{{if .Rejected}} style="color:#b3261e;font-weight:600"{{end}}>{{.Kind}}</td></tr>{{end}}
</tbody>
</table>
{{else}}
@@ -349,7 +353,7 @@
<div class="st st-nrd">
<div class="tm">{{.Time}}</div>
<div class="ti">Ответ НРД</div>
<div class="de">{{.Kind}}</div>
<div class="de"{{if .Rejected}} style="color:#b3261e;font-weight:600"{{end}}>{{.Kind}}</div>
</div>
{{end}}
</aside>
@@ -176,19 +176,13 @@
<div class="card"><h2>Сверка с учётной системой</h2><p class="muted" style="margin:0">Учётная система депозитария не подключена к bj — сверку выполните вручную и подтвердите отметками ниже.</p></div>
{{end}}
{{/* ---- Обязательные отметки ---- */}}
{{if or (eq $o.Status "draft") $o.Checks}}
{{/* ---- Торговая система: блокировка (необязательно) ---- */}}
{{if or (eq $o.Status "draft") $o.QuikMode}}
<div class="card" id="checks">
<h2>Обязательные отметки перед отправкой</h2>
<h2>Блокировка в торговой системе <span class="muted" style="font-size:13px;font-weight:500">· необязательно</span></h2>
<p class="muted" style="margin:0 0 8px">Клиент, счёт, остаток и бумага проверяются сверкой с учётной системой при сохранении — отдельных отметок не нужно. Здесь только отметьте, если бумаги заблокированы в торговой системе; по умолчанию блокировка не требуется.</p>
{{$editable := and (eq $o.Status "draft") (eq .Mode "card") (can "operate")}}
<form method="post" action="/admin/manual/{{$o.ID}}/checks" id="chkForm">
{{$grp := ""}}
{{range .PreChecks}}
{{if ne .Group $grp}}<div class="mf-grp">{{.Group}}</div>{{$grp = .Group}}{{end}}
{{$m := index $o.Checks .Key}}
<label class="mf-chk"><input type="checkbox" name="check_{{.Key}}" value="1" {{if $m.Done}}checked{{end}} {{if not $editable}}disabled{{end}} onchange="this.form.submit()">
<span><div class="t">{{.Title}}</div><div class="h">{{.Hint}}</div>{{if $m.Done}}<div class="who">✓ {{$m.By}} · {{msk $m.At}}</div>{{end}}</span></label>
{{end}}
<div class="mf-grp">Торговая система (QUIK)</div>
<label class="mf-chk"><input type="radio" name="quik_mode" value="auto" {{if eq $o.QuikMode "auto"}}checked{{end}} {{if or (not $editable) (not .QuikAuto)}}disabled{{end}} onchange="this.form.submit()">
<span><div class="t">Заблокировать бумаги автоматически при отправке</div><div class="h">{{if .QuikAuto}}bj зарегистрирует в модуле неторговых поручений вывод бумаг (лимит уменьшится), по исполнению подпишет списание, по отказу снимет блокировку.{{else}}Недоступно: автоматический цикл в торговой системе не включён в настройках.{{end}}</div></span></label>
@@ -196,7 +190,7 @@
<span><div class="t">Бумаги заблокированы вручную (лимит уменьшен)</div><div class="h">Позиция клиента проверена, поручение на вывод зарегистрировано сотрудником. После ответа отметьте списание или снятие блокировки.</div>
{{if eq $o.QuikMode "manual"}}<div style="margin-top:6px;display:flex;gap:8px;align-items:center;flex-wrap:wrap"><span class="mf-note">№ поручения в торговой системе:</span><input name="quik_instr" value="{{$o.QuikInstr}}" {{if not $editable}}disabled{{end}} style="max-width:200px;padding:5px 8px;border-radius:7px;border:1px solid var(--border);background:var(--card);color:var(--text)">{{if $editable}}<button type="submit" class="btn btn-secondary" style="padding:5px 10px">Сохранить</button>{{end}}</div>{{end}}
</span></label>
<label class="mf-chk"><input type="radio" name="quik_mode" value="none" {{if eq $o.QuikMode "none"}}checked{{end}} {{if not $editable}}disabled{{end}} onchange="this.form.submit()">
<label class="mf-chk"><input type="radio" name="quik_mode" value="none" {{if or (eq $o.QuikMode "none") (eq $o.QuikMode "")}}checked{{end}} {{if not $editable}}disabled{{end}} onchange="this.form.submit()">
<span><div class="t">Блокировка в торговой системе не требуется</div><div class="h">Бумаги не учитываются в торговой системе (не на торговом разделе, клиент не пользуется брокерским обслуживанием).</div></span></label>
{{if and $editable (ne $o.QuikMode "manual")}}<input type="hidden" name="quik_instr" value="{{$o.QuikInstr}}">{{end}}
</form>
@@ -211,13 +205,16 @@
<p class="muted" style="margin:0">Отправить пока нельзя:</p>
<ul class="mf-block">{{range .Blockers}}<li>{{.}}</li>{{end}}</ul>
{{else}}
<p style="margin:0"><span class="dc-ok">Все проверки и отметки пройдены.</span> Поручение уйдёт в НРД тем же путём, что и заявка из личного кабинета{{if eq $o.QuikMode "auto"}}, бумаги будут заблокированы в торговой системе перед отправкой{{end}}.</p>
<p style="margin:0"><span class="dc-ok">Сверка пройдена, поручение готово.</span> Поручение уйдёт в НРД тем же путём, что и заявка из личного кабинета{{if eq $o.QuikMode "auto"}}, бумаги будут заблокированы в торговой системе перед отправкой{{end}}.</p>
{{end}}
{{if can "operate"}}
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:14px;align-items:flex-start">
<form method="post" action="/admin/manual/{{$o.ID}}/send" onsubmit="return confirm('Отправить поручение {{$o.Number}} ({{$o.FullName}}) в НРД? Отменить отправку будет нельзя.')">
<button type="submit" class="btn btn-ok" {{if .Blockers}}disabled style="opacity:.5;cursor:not-allowed"{{end}}>Отправить в НРД</button>
</form>
<form method="post" action="/admin/manual/{{$o.ID}}/delete" style="margin:0" onsubmit="return confirm('Удалить черновик {{$o.Number}}? Восстановить его будет нельзя.')">
<button type="submit" class="btn btn-secondary">Удалить черновик</button>
</form>
<details><summary class="btn btn-secondary" style="list-style:none;cursor:pointer">Отклонить поручение…</summary>
<form method="post" action="/admin/manual/{{$o.ID}}/decline" style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap" onsubmit="return confirm('Отклонить поручение без отправки в НРД?')">
<input name="reason" required placeholder="причина отказа клиенту" style="min-width:320px;padding:7px 9px;border-radius:8px;border:1px solid var(--border);background:var(--card);color:var(--text)">
@@ -296,8 +293,8 @@ function mfRerun(){const f=document.getElementById('mf');if(f&&f.closest('detail
{{/* ======================= ФОРМА (новое / черновик) ======================= */}}
{{define "manualForm"}}
<form method="post" action="/admin/manual/save" id="mf">
<input type="hidden" name="id" value="{{.O.ID}}">
<form method="post" action="/admin/manual/save" id="mf" onsubmit="return mfQtyOK()">
<input type="hidden" name="id" value="{{.O.ID}}"><input type="hidden" name="depo_client" id="f_depo_client" value="">
<div class="mf-h" style="margin-top:0">1. Основание</div>
<div class="mf-grid">
<div><label class="mf-req">Дата поручения</label><input type="date" name="paper_date" value="{{.O.PaperDate}}" required></div>
@@ -384,6 +381,7 @@ async function mfPick(i,j){
if(f.doc_type)mfSet('doc_type',f.doc_type);
mfSet('doc_series',f.doc_ser||'');mfSet('doc_number',f.doc_num);mfSet('doc_issue_date',f.doc_date||'');mfSet('doc_issuer',f.doc_issuer||'');
mfSet('account',a.number);mfSet('contract',a.contract||'');if(f.phone)mfSet('phone',f.phone);
const dc=document.getElementById('f_depo_client');if(dc)dc.value=a.client_id||'';
document.getElementById('dres').innerHTML='<span class="dc-ok">Подставлено из DEPO: '+mfEsc(f.name)+', счёт '+mfEsc(a.number)+'. Сверьте с поручением клиента.</span>';
const box=document.getElementById('dpos');box.innerHTML='<span class="mf-note">Загрузка бумаг клиента…</span>';
try{const r=await (await fetch('/admin/manual/depo/positions?client='+a.client_id)).json();
@@ -394,7 +392,11 @@ async function mfPick(i,j){
}catch(e){box.innerHTML='<span class="dc-bad">'+mfEsc(e)+'</span>'}
}
let mfPos=[];
function mfSec(k){const p=mfPos[k];document.getElementById('f_isin').value=p.isin;document.getElementById('f_sname').value=p.name;const q=document.getElementById('f_qty');q.placeholder='доступно '+p.qty;q.focus()}
function mfSec(k){const p=mfPos[k];document.getElementById('f_isin').value=p.isin;document.getElementById('f_sname').value=p.name;const q=document.getElementById('f_qty');q.placeholder='доступно '+p.qty;q.dataset.avail=p.qty;q.dataset.isin=p.isin;q.focus()}
// Больше остатка на торговом разделе ввести нельзя: такому поручению дальше идти некуда.
function mfQtyOK(){const q=document.getElementById('f_qty'),isin=document.getElementById('f_isin');if(!q||!q.dataset.avail||(isin&&isin.value.toUpperCase()!==q.dataset.isin))return true;
const v=parseFloat(String(q.value).replace(',','.'));if(isNaN(v))return true;
if(v>parseFloat(q.dataset.avail)){alert('По бумаге '+q.dataset.isin+' доступно '+q.dataset.avail+' шт., в поручении '+q.value+'. Укажите не больше доступного.');q.focus();return false}return true}
// hbFind — подбор участника справочника НРД по ИНН, названию или коду (без учёта регистра и кавычек).
function hbNorm(s){return String(s||'').toLowerCase().replace(/ё/g,'е').replace(/[«»"'“”.,()]/g,' ').replace(/\s+/g,' ').trim()}
function hbFind(list,q){q=hbNorm(q);