Личный кабинет: оформление «Табло», разделы Табло/Реестр/Поручения/Профиль, поручение как вид

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-17 22:51:54 +03:00
parent ea869665df
commit f057b89d2e
3 changed files with 537 additions and 294 deletions
+142 -22
View File
@@ -14,6 +14,7 @@ import (
"log"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
@@ -119,6 +120,7 @@ type position struct {
ISIN string `json:"isin"`
SecurityCode string `json:"security_code"`
SecurityName string `json:"security_name"`
AccountID string `json:"account_id"`
Quantity float64 `json:"quantity"`
Blocked float64 `json:"blocked"`
Available float64 `json:"available"`
@@ -137,6 +139,56 @@ type claimRow struct {
Reason string `json:"reason"`
}
// portfolioView — сводка реестра для доски: сколько выпусков всего, свободных и с ограничением.
func portfolioView(pos []position) (total, free, blocked int, accounts []string) {
seen := map[string]bool{}
for _, p := range pos {
total++
switch {
case p.Blocked > 0 || !p.Transferable:
blocked++
default:
free++
}
if p.AccountID != "" && !seen[p.AccountID] {
seen[p.AccountID] = true
accounts = append(accounts, p.AccountID)
}
}
sort.Strings(accounts)
return
}
// claimsView — сколько поручений в работе и сколько завершено.
func claimsView(rows []claimRow) (active, done int) {
for _, c := range rows {
switch c.Status {
case "done":
done++
case "rejected", "declined", "error":
default:
active++
}
}
return
}
// portfolioResp — ответ /api/lk/portfolio.
type portfolioResp struct {
Contract string `json:"contract"`
FullName string `json:"full_name"`
Positions []position `json:"positions"`
Source string `json:"source"`
ReadOnly bool `json:"read_only"`
}
// portfolio — реестр клиента из ядра.
func (s *Server) portfolio(r *http.Request, sess string) (portfolioResp, error) {
var out portfolioResp
err := s.bj.call(r.Context(), http.MethodGet, "/api/lk/portfolio", sess, nil, &out)
return out, err
}
// ---- страницы ----
// render — страница; признаки режима (админ / просмотр кабинета клиента) — из кук этого запроса.
@@ -238,7 +290,7 @@ func (s *Server) Handler() http.Handler {
http.Redirect(w, r, "/login", http.StatusSeeOther)
})
// Портфель
// Табло — обзор: реестр и последние поручения на одной странице.
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
@@ -249,26 +301,88 @@ func (s *Server) Handler() http.Handler {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
var out struct {
Contract string `json:"contract"`
FullName string `json:"full_name"`
Positions []position `json:"positions"`
Source string `json:"source"`
ReadOnly bool `json:"read_only"`
}
if err := bj.call(r.Context(), http.MethodGet, "/api/lk/portfolio", sess, nil, &out); err != nil {
s.render(w, r, "portfolio", map[string]any{"Nav": "p", "Error": err.Error()})
pf, err := s.portfolio(r, sess)
if err != nil {
s.render(w, r, "board", map[string]any{"Nav": "b", "Error": err.Error()})
return
}
s.render(w, r, "portfolio", map[string]any{
"Nav": "p", "Contract": out.Contract, "FullName": out.FullName,
"Positions": out.Positions, "Flash": r.URL.Query().Get("m"),
"Source": out.Source, "ReadOnly": out.ReadOnly,
total, free, blocked, _ := portfolioView(pf.Positions)
var cl struct {
Claims []claimRow `json:"claims"`
}
_ = bj.call(r.Context(), http.MethodGet, "/api/lk/claims", sess, nil, &cl)
active, _ := claimsView(cl.Claims)
pos := pf.Positions
if len(pos) > 5 {
pos = pos[:5]
}
claims := cl.Claims
if len(claims) > 5 {
claims = claims[:5]
}
s.render(w, r, "board", map[string]any{
"Nav": "b", "Contract": pf.Contract, "FullName": pf.FullName,
"Positions": pos, "Claims": claims, "Flash": r.URL.Query().Get("m"),
"Total": total, "Free": free, "Blocked": blocked, "Active": active,
})
})
// Заявка на перевод
// Реестр бумаг
mux.HandleFunc("/portfolio", func(w http.ResponseWriter, r *http.Request) {
sess := sessionW(w, r)
if sess == "" {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
pf, err := s.portfolio(r, sess)
if err != nil {
s.render(w, r, "portfolio", map[string]any{"Nav": "p", "Error": err.Error()})
return
}
total, free, blocked, _ := portfolioView(pf.Positions)
updated := ""
if len(pf.Positions) > 0 {
updated = pf.Positions[0].UpdatedAt
}
s.render(w, r, "portfolio", map[string]any{
"Nav": "p", "Contract": pf.Contract, "FullName": pf.FullName,
"Positions": pf.Positions, "Flash": r.URL.Query().Get("m"),
"Source": pf.Source, "ReadOnly": pf.ReadOnly, "UpdatedAt": updated,
"Total": total, "Free": free, "Blocked": blocked,
})
})
// Профиль: договор, счета депо, доступ
mux.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
sess := sessionW(w, r)
if sess == "" {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
pf, err := s.portfolio(r, sess)
if err != nil {
s.render(w, r, "profile", map[string]any{"Nav": "u", "Error": err.Error()})
return
}
_, _, _, accounts := portfolioView(pf.Positions)
login := ""
if c, err := r.Cookie("bj_lk_as"); err == nil {
login = c.Value
}
if login == "" {
login = pf.Contract
}
s.render(w, r, "profile", map[string]any{
"Nav": "u", "Contract": pf.Contract, "FullName": pf.FullName,
"AccountList": accounts, "Accounts": len(accounts), "ReadOnly": pf.ReadOnly, "Login": login,
})
})
// Поручение депозитарию (сейчас — перевод бумаг в другой депозитарий)
mux.HandleFunc("/transfer", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/order", http.StatusMovedPermanently)
})
mux.HandleFunc("/order", func(w http.ResponseWriter, r *http.Request) {
sess := sessionW(w, r)
if sess == "" {
http.Redirect(w, r, "/login", http.StatusSeeOther)
@@ -280,10 +394,11 @@ func (s *Server) Handler() http.Handler {
FullName string `json:"full_name"`
}
if err := bj.call(r.Context(), http.MethodGet, "/api/lk/portfolio", sess, nil, &out); err != nil {
s.render(w, r, "transfer", map[string]any{"Nav": "t", "Error": err.Error()})
s.render(w, r, "transfer", map[string]any{"Nav": "c", "Error": err.Error(), "Today": time.Now().Format("02.01.2006")})
return
}
s.render(w, r, "transfer", map[string]any{"Nav": "t", "Positions": out.Positions, "FullName": out.FullName})
s.render(w, r, "transfer", map[string]any{"Nav": "c", "Positions": out.Positions, "FullName": out.FullName,
"PickISIN": r.URL.Query().Get("isin"), "Today": time.Now().Format("02.01.2006")})
return
}
_ = r.ParseForm()
@@ -300,7 +415,7 @@ func (s *Server) Handler() http.Handler {
secs = append(secs, map[string]any{"isin": isin, "quantity": f})
}
if len(secs) == 0 {
s.render(w, r, "transfer", map[string]any{"Nav": "t", "Error": "Укажите количество хотя бы по одной бумаге"})
s.render(w, r, "transfer", map[string]any{"Nav": "c", "Error": "Укажите количество хотя бы по одной бумаге", "Today": time.Now().Format("02.01.2006")})
return
}
var res struct {
@@ -315,14 +430,17 @@ func (s *Server) Handler() http.Handler {
"securities": secs,
}, &res)
if err != nil {
s.render(w, r, "transfer", map[string]any{"Nav": "t", "Error": err.Error()})
s.render(w, r, "transfer", map[string]any{"Nav": "c", "Error": err.Error(), "Today": time.Now().Format("02.01.2006")})
return
}
http.Redirect(w, r, "/claims?m="+url.QueryEscape(res.Message), http.StatusSeeOther)
http.Redirect(w, r, "/orders?m="+url.QueryEscape(res.Message), http.StatusSeeOther)
})
// Мои заявки
// Поручения — журнал всех видов
mux.HandleFunc("/claims", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/orders", http.StatusMovedPermanently)
})
mux.HandleFunc("/orders", func(w http.ResponseWriter, r *http.Request) {
sess := sessionW(w, r)
if sess == "" {
http.Redirect(w, r, "/login", http.StatusSeeOther)
@@ -335,7 +453,9 @@ func (s *Server) Handler() http.Handler {
s.render(w, r, "claims", map[string]any{"Nav": "c", "Error": err.Error()})
return
}
s.render(w, r, "claims", map[string]any{"Nav": "c", "Claims": out.Claims, "Flash": r.URL.Query().Get("m")})
active, done := claimsView(out.Claims)
s.render(w, r, "claims", map[string]any{"Nav": "c", "Claims": out.Claims, "Flash": r.URL.Query().Get("m"),
"Total": len(out.Claims), "Active": active, "Done": done})
})
// Администратор: список клиентов