Журнал: онлайн-статус «чего ждёт заявка», ссылка из ручного ввода в журнал, правки формы

- livestatus.go: одна фраза состояния для исходящих (сделка M2M + черновик/подача/MS010
  поручения 16) и входящих (наше решение → поручение 16/1); признак «в работе»
- строки журналов и карточки показывают её и обновляют по /admin/journal/live каждые 8 с,
  пока заявка в работе; завершённые и отклонённые не опрашиваются
- ручной ввод: у отправленного поручения ссылка «в журнале →» и тот же онлайн-статус;
  вид документа занимает две колонки
- выбор участника: по ИНН и коду с кнопки, а не по номеру строки; Enter выбирает только
  при единственном совпадении

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 11:55:39 +03:00
parent 7ac41c0335
commit 46e55e1277
11 changed files with 190 additions and 14 deletions
+132
View File
@@ -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
}