Списки: период, сортировка, подгрузка; анкета участника — строкой исходящих

«Исходящие», «Входящие», «Журнал обмена»: быстрые периоды (сегодня, 7, 30
дней, всё время), даты с–по, сортировка (время, инвестор/контрагент,
направление, статус), первые 50 строк и подгрузка при прокрутке
(part=rows&offset=N). Раньше показывались только последние 200/100.

Анкета участника — исходящий пакет в НРД: строка в «Исходящих» и
«Журнале» с ответом НРД (принята / отклонена с кодом / ждёт), вместо
плашки над списком. Отправки копятся в participant-regs.json (прежний
participant-reg.json подхватывается). userTemplate — функции текущего
пользователя и для порций строк.

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-22 10:50:43 +03:00
parent 890e575d3d
commit 60b4d46277
13 changed files with 522 additions and 92 deletions
+185
View File
@@ -0,0 +1,185 @@
package lkgateway
// listquery.go — списки «Исходящие», «Входящие», «Журнал обмена»: период (с–по, быстрые «сегодня /
// 7 / 30 дней»), сортировка и подгрузка частями при прокрутке. Страница отдаёт первые listPageSize
// строк; следующие браузер берёт тем же адресом с part=rows&offset=N (ответ — JSON с готовыми строками).
import (
"bytes"
"encoding/json"
"html/template"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
)
const listPageSize = 50
var listMSK = time.FixedZone("МСК", 3*3600)
// listQ — что выбрал пользователь: период, сортировка, с какой строки.
type listQ struct {
From, To time.Time // To — начало дня после «по» (исключительно); нулевые — без границы
FromS, ToS string // как в полях (ГГГГ-ММ-ДД)
Sort string
Asc bool
Offset int
Query string // параметры без offset/part — для подгрузки и ссылок
}
// parseListQ — параметры списка из адреса. sorts — допустимые ключи сортировки, первый — по умолчанию.
func parseListQ(r *http.Request, sorts []string) listQ {
v := r.URL.Query()
q := listQ{Sort: sorts[0]}
if t, err := time.ParseInLocation("2006-01-02", v.Get("from"), listMSK); err == nil {
q.From, q.FromS = t, v.Get("from")
}
if t, err := time.ParseInLocation("2006-01-02", v.Get("to"), listMSK); err == nil {
q.To, q.ToS = t.AddDate(0, 0, 1), v.Get("to")
}
for _, s := range sorts {
if v.Get("sort") == s {
q.Sort = s
}
}
q.Asc = v.Get("dir") == "asc"
q.Offset, _ = strconv.Atoi(v.Get("offset"))
if q.Offset < 0 {
q.Offset = 0
}
keep := url.Values{}
for _, k := range []string{"from", "to", "sort", "dir"} {
if s := v.Get(k); s != "" {
keep.Set(k, s)
}
}
q.Query = keep.Encode()
return q
}
// in — время попадает в выбранный период.
func (q listQ) in(t time.Time) bool {
return (q.From.IsZero() || !t.Before(q.From)) && (q.To.IsZero() || t.Before(q.To))
}
// listItem — строка списка: время, ключи сортировки и данные для шаблона строки.
type listItem struct {
T time.Time
Keys map[string]string
V any
}
// listApply — отбор по периоду, сортировка и одна порция строк.
func listApply(items []listItem, q listQ) (page []listItem, total int, more bool) {
out := items[:0:0]
for _, it := range items {
if q.in(it.T) {
out = append(out, it)
}
}
sort.SliceStable(out, func(i, j int) bool {
a, b := out[i], out[j]
if q.Sort != "time" {
ka, kb := strings.ToLower(a.Keys[q.Sort]), strings.ToLower(b.Keys[q.Sort])
if ka != kb {
if q.Asc {
return ka < kb
}
return ka > kb
}
return a.T.After(b.T) // при равенстве — новые выше
}
if q.Asc {
return a.T.Before(b.T)
}
return a.T.After(b.T)
})
total = len(out)
if q.Offset >= total {
return nil, total, false
}
end := q.Offset + listPageSize
if end > total {
end = total
}
return out[q.Offset:end], total, end < total
}
// listView — панель списка (период, сортировка) и состояние подгрузки — для шаблона "listTools".
type listView struct {
Q listQ
Path string
Body string // id <tbody>, куда дописываются строки
Total int
More bool
Next int
Sorts []listOpt
Presets []listOpt
}
type listOpt struct {
Key, Label, Href string
On bool
}
// newListView — панель: быстрые периоды и варианты сортировки (sorts — пары ключ, подпись).
func newListView(path, body string, q listQ, total int, more bool, sorts ...string) listView {
lv := listView{Q: q, Path: path, Body: body, Total: total, More: more, Next: q.Offset + listPageSize}
for i := 0; i+1 < len(sorts); i += 2 {
lv.Sorts = append(lv.Sorts, listOpt{Key: sorts[i], Label: sorts[i+1], On: q.Sort == sorts[i]})
}
today := time.Now().In(listMSK).Format("2006-01-02")
day := func(n int) string { return time.Now().In(listMSK).AddDate(0, 0, -n).Format("2006-01-02") }
href := func(from, to string) string {
v := url.Values{}
if from != "" {
v.Set("from", from)
v.Set("to", to)
}
if q.Sort != lv.Sorts[0].Key {
v.Set("sort", q.Sort)
}
if q.Asc {
v.Set("dir", "asc")
}
if s := v.Encode(); s != "" {
return path + "?" + s
}
return path
}
for _, p := range []struct{ label, from string }{{"Сегодня", today}, {"7 дней", day(6)}, {"30 дней", day(29)}, {"Всё время", ""}} {
to := today
if p.from == "" {
to = ""
}
lv.Presets = append(lv.Presets, listOpt{Label: p.label, Href: href(p.from, to), On: q.FromS == p.from && q.ToS == to})
}
return lv
}
// listRowsPart — ответ на part=rows: следующая порция строк готовым HTML (шаблон rowTpl на каждую строку).
func listRowsPart(w http.ResponseWriter, t *template.Template, rowTpl string, page []listItem, q listQ, more bool) {
tu := userTemplate(w, t)
var buf bytes.Buffer
for _, it := range page {
if err := tu.ExecuteTemplate(&buf, rowTpl, it.V); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(map[string]any{"html": buf.String(), "more": more, "next": q.Offset + len(page)})
}
// listVals — данные строк порции для шаблона страницы.
func listVals(page []listItem) []any {
out := make([]any, 0, len(page))
for _, it := range page {
out = append(out, it.V)
}
return out
}