60b4d46277
«Исходящие», «Входящие», «Журнал обмена»: быстрые периоды (сегодня, 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
186 lines
6.0 KiB
Go
186 lines
6.0 KiB
Go
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
|
||
}
|