Дашборд: переводы обоих направлений, переключатель «Сегодня / Всего»

На 9 при одном входящем переводе (Витвицкий) дашборд показывал «Переводов
ещё нет»: сводка и «Последние переводы» считали только исходящие, и только
последние 200.

- Сводка по направлениям: исходящие и входящие — всего, исполнено, в работе,
  отказ / истёк срок; исход — по состоянию «чего ждёт заявка» (livestatus.go),
  то же, что в журнале: принятый, но не исполненный перевод — «в работе».
- Период «Сегодня / Всего» (?period=, выбор помнится в куке; по умолчанию —
  сегодня, с 00:00 МСК). Плитка «Переводы» ведёт в журнал, счётчики за всё время.
- «Последние переводы» — общая лента журнала (без анкет участника): направление,
  инвестор, контрагент, статус и строка состояния.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
This commit is contained in:
fontvielle
2026-10-23 15:19:13 +03:00
parent a0ffbfa721
commit ffd69b0698
4 changed files with 202 additions and 59 deletions
+70 -27
View File
@@ -503,13 +503,11 @@ var globalRC *RuntimeConfig
type homeData struct {
page
Status SystemStatus
Counts struct {
Total int
Confirmed int
InProgress int
Failed int
}
Recent []ClaimView
// Сводка по переводам обоих направлений (23.10.2026: считались только исходящие, последние 200).
Period string // today | all — выбранный период сводки
Sum homePeriod // за выбранный период
All homePeriod // за всё время (плитка «Журнал»)
Recent []journalRow
News []NewsItem // top-3 активных или свежих новостей
// Сводка готовности системы для hero-блока дашборда.
@@ -731,21 +729,80 @@ func RegisterAdmin(mux *http.ServeMux, svc *Service, rc *RuntimeConfig, getOpts
return a, nil
}
// homeDir — сводка дашборда по одному направлению: исход — по состоянию «чего ждёт заявка»
// (livestatus.go): в работе, исполнено (в т. ч. частично), отказ / истёк срок.
type homeDir struct{ Total, Done, InProgress, Failed int }
func (d *homeDir) add(st LiveStatus) {
d.Total++
switch {
case st.Active:
d.InProgress++
case st.Kind == "bad":
d.Failed++
case st.Kind == "ok" || st.Kind == "warn":
d.Done++
default:
d.InProgress++
}
}
type homePeriod struct{ Out, In homeDir }
// homePeriodOf — период сводки: ?period=today|all, выбор помнится в куке; по умолчанию — сегодня.
func homePeriodOf(w http.ResponseWriter, r *http.Request) string {
p := r.URL.Query().Get("period")
if p == "today" || p == "all" {
http.SetCookie(w, &http.Cookie{Name: "bj_home_period", Value: p, Path: "/admin", MaxAge: 365 * 24 * 3600, HttpOnly: true, SameSite: http.SameSiteLaxMode})
return p
}
if c, err := r.Cookie("bj_home_period"); err == nil && c.Value == "all" {
return "all"
}
return "today"
}
// homeSummary — сводка и последние переводы (исходящие + входящие, без анкет участника) за период.
func homeSummary(ctx context.Context, svc *Service, period string) (sum, all homePeriod, recent []journalRow) {
store := globalIncomingStore
if store == nil {
store = &IncomingStore{}
}
n := time.Now().In(mskZone)
today := time.Date(n.Year(), n.Month(), n.Day(), 0, 0, 0, 0, mskZone)
for _, row := range buildJournalRows(ctx, svc, store, time.Time{}) {
if row.DirKind != "out" && row.DirKind != "in" || strings.HasPrefix(row.LiveKey, "pf:") {
continue
}
isToday := !row.Time.Before(today)
for _, p := range []*homePeriod{&all, &sum} {
if p == &sum && period == "today" && !isToday {
continue
}
if row.DirKind == "out" {
p.Out.add(row.Live)
} else {
p.In.add(row.Live)
}
}
if len(recent) < 10 && (period == "all" || isToday) {
recent = append(recent, row)
}
}
return sum, all, recent
}
func (a *admin) renderHome(w http.ResponseWriter, r *http.Request, svc *Service, rc *RuntimeConfig, opts CheckOptions) {
ctx := r.Context()
status := CheckAll(ctx, opts)
recent, err := svc.ListClaims(ctx, m2mcore.Filter{Limit: 10})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := homeData{
page: nowPage("Дашборд", "home"),
Status: status,
Recent: recent.Items,
Period: homePeriodOf(w, r),
News: topNews(visibleNews(rc, rc.Snapshot().News.Items), 3),
LKUrl: rc.Snapshot().LK.PublicURL,
}
data.Sum, data.All, data.Recent = homeSummary(ctx, svc, data.Period)
data.NSDEmuOn, _ = lkEmulateNSD(rc)
data.QuikEmuOn = rc.Snapshot().Quik.Emulate && devMode(rc)
if q := rc.Snapshot().Quik; q.RealWrite && devMode(rc) {
@@ -760,20 +817,6 @@ func (a *admin) renderHome(w http.ResponseWriter, r *http.Request, svc *Service,
}
}
data.AllReady = data.TotalCount > 0 && data.NotReadyCount == 0
full, err := svc.ListClaims(ctx, m2mcore.Filter{Limit: 200})
if err == nil {
for _, c := range full.Items {
data.Counts.Total++
switch c.Status {
case string(m2mcore.StateConfirmed), string(m2mcore.StateAwaitingSUB16), string(m2mcore.StateDone):
data.Counts.Confirmed++
case string(m2mcore.StateRejected), string(m2mcore.StateTimedOut):
data.Counts.Failed++
default:
data.Counts.InProgress++
}
}
}
render(w, a.home, data)
}