Личный кабинет: оформление «Табло», разделы Табло/Реестр/Поручения/Профиль, поручение как вид
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+142
-22
@@ -14,6 +14,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -119,6 +120,7 @@ type position struct {
|
|||||||
ISIN string `json:"isin"`
|
ISIN string `json:"isin"`
|
||||||
SecurityCode string `json:"security_code"`
|
SecurityCode string `json:"security_code"`
|
||||||
SecurityName string `json:"security_name"`
|
SecurityName string `json:"security_name"`
|
||||||
|
AccountID string `json:"account_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
Blocked float64 `json:"blocked"`
|
Blocked float64 `json:"blocked"`
|
||||||
Available float64 `json:"available"`
|
Available float64 `json:"available"`
|
||||||
@@ -137,6 +139,56 @@ type claimRow struct {
|
|||||||
Reason string `json:"reason"`
|
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 — страница; признаки режима (админ / просмотр кабинета клиента) — из кук этого запроса.
|
// render — страница; признаки режима (админ / просмотр кабинета клиента) — из кук этого запроса.
|
||||||
@@ -238,7 +290,7 @@ func (s *Server) Handler() http.Handler {
|
|||||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Портфель
|
// Табло — обзор: реестр и последние поручения на одной странице.
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/" {
|
if r.URL.Path != "/" {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
@@ -249,26 +301,88 @@ func (s *Server) Handler() http.Handler {
|
|||||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var out struct {
|
pf, err := s.portfolio(r, sess)
|
||||||
Contract string `json:"contract"`
|
if err != nil {
|
||||||
FullName string `json:"full_name"`
|
s.render(w, r, "board", map[string]any{"Nav": "b", "Error": err.Error()})
|
||||||
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()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.render(w, r, "portfolio", map[string]any{
|
total, free, blocked, _ := portfolioView(pf.Positions)
|
||||||
"Nav": "p", "Contract": out.Contract, "FullName": out.FullName,
|
var cl struct {
|
||||||
"Positions": out.Positions, "Flash": r.URL.Query().Get("m"),
|
Claims []claimRow `json:"claims"`
|
||||||
"Source": out.Source, "ReadOnly": out.ReadOnly,
|
}
|
||||||
|
_ = 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) {
|
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)
|
sess := sessionW(w, r)
|
||||||
if sess == "" {
|
if sess == "" {
|
||||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
@@ -280,10 +394,11 @@ func (s *Server) Handler() http.Handler {
|
|||||||
FullName string `json:"full_name"`
|
FullName string `json:"full_name"`
|
||||||
}
|
}
|
||||||
if err := bj.call(r.Context(), http.MethodGet, "/api/lk/portfolio", sess, nil, &out); err != nil {
|
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
|
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
|
return
|
||||||
}
|
}
|
||||||
_ = r.ParseForm()
|
_ = r.ParseForm()
|
||||||
@@ -300,7 +415,7 @@ func (s *Server) Handler() http.Handler {
|
|||||||
secs = append(secs, map[string]any{"isin": isin, "quantity": f})
|
secs = append(secs, map[string]any{"isin": isin, "quantity": f})
|
||||||
}
|
}
|
||||||
if len(secs) == 0 {
|
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
|
return
|
||||||
}
|
}
|
||||||
var res struct {
|
var res struct {
|
||||||
@@ -315,14 +430,17 @@ func (s *Server) Handler() http.Handler {
|
|||||||
"securities": secs,
|
"securities": secs,
|
||||||
}, &res)
|
}, &res)
|
||||||
if err != nil {
|
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
|
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) {
|
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)
|
sess := sessionW(w, r)
|
||||||
if sess == "" {
|
if sess == "" {
|
||||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
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()})
|
s.render(w, r, "claims", map[string]any{"Nav": "c", "Error": err.Error()})
|
||||||
return
|
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})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Администратор: список клиентов
|
// Администратор: список клиентов
|
||||||
|
|||||||
+68
-1
@@ -28,7 +28,18 @@ func fakeBJ(t *testing.T) *httptest.Server {
|
|||||||
})
|
})
|
||||||
mux.HandleFunc("/api/lk/portfolio", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/lk/portfolio", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if keyOK(w, r) && r.Header.Get("X-BJ-Session") == "sess-1" {
|
if keyOK(w, r) && r.Header.Get("X-BJ-Session") == "sess-1" {
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{"full_name": "Мостовиков Тест", "contract": "4519", "positions": []any{}})
|
_ = json.NewEncoder(w).Encode(map[string]any{"full_name": "Мостовиков Тест", "contract": "4519", "source": "депозитарный учёт", "positions": []any{
|
||||||
|
map[string]any{"isin": "RU0009029540", "security_name": "Сбербанк, ао", "account_id": "ВТНКЦ00000001", "quantity": 400, "blocked": 100, "available": 300, "transferable": true, "updated_at": "17.09.2026 21:58"},
|
||||||
|
map[string]any{"isin": "US0378331005", "security_name": "Apple Inc.", "account_id": "В000000044", "quantity": 12, "blocked": 0, "available": 0, "transferable": false, "note": "иностранная бумага — перевод через M2M недоступен"},
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/api/lk/claims", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if keyOK(w, r) && r.Header.Get("X-BJ-Session") == "sess-1" {
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"claims": []any{
|
||||||
|
map[string]any{"guid": "41523F7F-1111", "status": "sent", "status_ru": "Отправлено в НРД", "created_at": "16.09.2026", "securities": 1},
|
||||||
|
map[string]any{"guid": "D744A410-2222", "status": "done", "status_ru": "Завершена", "created_at": "09.09.2026", "securities": 2},
|
||||||
|
}})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
mux.HandleFunc("/api/lk/ping", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/lk/ping", func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -123,3 +134,59 @@ func TestNotConfigured(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Страницы кабинета в оформлении «Табло»: обзор, реестр, поручения, профиль и бланк поручения.
|
||||||
|
func TestBoardPages(t *testing.T) {
|
||||||
|
bj := fakeBJ(t)
|
||||||
|
s, err := New(Config{BJURL: bj.URL, BJKey: "lk-key", Title: "Кабинет"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
h := s.Handler()
|
||||||
|
get := func(path string) string {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||||
|
req.AddCookie(&http.Cookie{Name: "bj_lk", Value: "sess-1"})
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("%s: код %d", path, rec.Code)
|
||||||
|
}
|
||||||
|
return rec.Body.String()
|
||||||
|
}
|
||||||
|
board := get("/")
|
||||||
|
for _, want := range []string{"Бумаг в реестре", "Новое поручение", "Сбербанк, ао", "Отправлено в НРД", "Поручений в работе"} {
|
||||||
|
if !strings.Contains(board, want) {
|
||||||
|
t.Errorf("на табло нет %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reg := get("/portfolio")
|
||||||
|
for _, want := range []string{"ВТНКЦ00000001", "иностранный выпуск", "депозитарный учёт", "перевести"} {
|
||||||
|
if !strings.Contains(reg, want) {
|
||||||
|
t.Errorf("в реестре нет %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
orders := get("/orders")
|
||||||
|
if !strings.Contains(orders, "Поручения депозитарию") || !strings.Contains(orders, "Перевод в другой депозитарий") {
|
||||||
|
t.Errorf("журнал поручений: %s", orders)
|
||||||
|
}
|
||||||
|
order := get("/order?isin=RU0009029540")
|
||||||
|
for _, want := range []string{"Поручение депозитарию", "Вид поручения", "ИНН принимающего депозитария", `value="RU0009029540" id="cRU0009029540" checked`} {
|
||||||
|
if !strings.Contains(order, want) {
|
||||||
|
t.Errorf("в бланке поручения нет %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prof := get("/profile")
|
||||||
|
if !strings.Contains(prof, "Счета депо") || !strings.Contains(prof, "ВТНКЦ00000001") {
|
||||||
|
t.Errorf("профиль: %s", prof)
|
||||||
|
}
|
||||||
|
// старые адреса продолжают работать
|
||||||
|
for path, to := range map[string]string{"/transfer": "/order", "/claims": "/orders"} {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||||
|
req.AddCookie(&http.Cookie{Name: "bj_lk", Value: "sess-1"})
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusMovedPermanently || rec.Header().Get("Location") != to {
|
||||||
|
t.Errorf("%s → %s: код %d, Location %q", path, to, rec.Code, rec.Header().Get("Location"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+327
-271
@@ -2,310 +2,366 @@ package lk
|
|||||||
|
|
||||||
// pageTemplates — встроенные страницы Личного кабинета (без внешних зависимостей,
|
// pageTemplates — встроенные страницы Личного кабинета (без внешних зависимостей,
|
||||||
// чтобы модуль был одним файлом и ставился на любой веб-сервер).
|
// чтобы модуль был одним файлом и ставился на любой веб-сервер).
|
||||||
|
//
|
||||||
|
// Визуальный мир — «Табло» (см. DESIGN.md, раздел «Личный кабинет клиента»):
|
||||||
|
// графитовая доска состояния сверху, светлая бумага реестра, янтарь — «в пути»
|
||||||
|
// и первичное действие, все величины моноширинные табличные. Шрифты грузятся с
|
||||||
|
// Google Fonts, при недоступности сети подхватываются системные аналоги.
|
||||||
const pageTemplates = `
|
const pageTemplates = `
|
||||||
{{define "head"}}<!DOCTYPE html><html lang="ru"><head><meta charset="utf-8">
|
{{define "head"}}<!DOCTYPE html><html lang="ru"><head><meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Golos+Text:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap">
|
||||||
<style>
|
<style>
|
||||||
:root{--bg:#f6f7f9;--card:#fff;--bd:#e3e6ec;--tx:#1b2029;--mut:#5b6472;--acc:#2563eb;--ok:#15803d;--err:#c22a2a;
|
:root{--paper:#edebe4;--sheet:#f7f5ef;--ink:#191a1c;--ink-2:#45403a;--mut:#6a6357;
|
||||||
--bd-strong:#c9cfda;--bg-2:#eef0f4;
|
--line:#ded8ca;--line-2:#c9c3b4;--board:#191a1c;--board-2:#1f2124;--board-3:#2b2d31;
|
||||||
--bg-neu:rgba(37,99,235,.12);--tx-neu:#1d4ed8;--bg-ok:rgba(21,128,61,.14);--tx-ok:#15803d;--bg-err:rgba(194,42,42,.12);--tx-err:#b91c1c;--bg-warn:rgba(180,120,0,.16);--tx-warn:#a15c07}
|
--dim:#a49b8a;--amber:#e8a33d;--amber-bg:#f6e2c0;--amber-tx:#7a4e07;
|
||||||
@media(prefers-color-scheme:dark){:root{--bg:#0f1116;--card:#181b22;--bd:#2a2f3a;--tx:#e8eaed;--mut:#a2acbd;--acc:#7ba7ff;--ok:#5bd085;--err:#ff8f8f;
|
--ok-bg:#dcefe2;--ok-tx:#1f6b3e;--err-bg:#f7dedb;--err-tx:#a32d22;--neu-bg:#e3e0d6;
|
||||||
--bd-strong:#3a4150;--bg-2:#20242e;
|
--acc:#2c46c0;--mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
||||||
--bg-neu:rgba(123,167,255,.16);--tx-neu:#93c5fd;--bg-ok:rgba(91,208,133,.18);--tx-ok:#86efac;--bg-err:rgba(255,143,143,.18);--tx-err:#fca5a5;--bg-warn:rgba(245,180,80,.18);--tx-warn:#fcd34d}}
|
*{box-sizing:border-box}
|
||||||
*{box-sizing:border-box}body{margin:0;font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
body{margin:0;font-family:"Golos Text",system-ui,-apple-system,"Segoe UI",sans-serif;background:var(--paper);color:var(--ink);line-height:1.5}
|
||||||
background:var(--bg);color:var(--tx);line-height:1.5}
|
a{color:var(--acc)}a:hover{color:#1e3395}
|
||||||
.top{background:var(--card);border-bottom:1px solid var(--bd);padding:0 20px;height:56px;display:flex;align-items:center;gap:8px;position:sticky;top:0;z-index:5;overflow-x:auto}
|
.lbl{font-family:var(--mono);font-size:10.5px;font-weight:500;letter-spacing:.1em;text-transform:uppercase}
|
||||||
.brand{font-weight:700;font-size:16px;white-space:nowrap;margin-right:8px}.top a{color:var(--mut);text-decoration:none;font-size:14px;padding:8px 10px;border-radius:7px;white-space:nowrap}
|
.n{font-family:var(--mono);font-variant-numeric:tabular-nums}
|
||||||
.top a:hover{background:var(--bg);color:var(--tx)}.top a.on{background:var(--bg-neu);color:var(--tx-neu)}
|
.top{background:var(--board);color:#f3f0e7;padding:0 20px;min-height:58px;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
|
||||||
.top .right{margin-left:auto;display:flex;align-items:center;gap:12px;font-size:13px;color:var(--mut);white-space:nowrap}
|
.brand{display:flex;align-items:center;gap:10px;white-space:nowrap}
|
||||||
main{max-width:980px;margin:0 auto;padding:22px 20px}
|
.top nav{display:flex;gap:2px;flex-grow:1;overflow-x:auto;scrollbar-width:none}
|
||||||
h1{font-size:22px;margin:0 0 14px}h2{font-size:16px;margin:0 0 12px}
|
.top nav::-webkit-scrollbar{display:none}
|
||||||
.card{background:var(--card);border:1px solid var(--bd);border-radius:12px;padding:18px 20px;margin-bottom:16px}
|
.top nav a{color:var(--dim);text-decoration:none;padding:8px 12px;border-radius:3px;white-space:nowrap}
|
||||||
.tw{overflow-x:auto;-webkit-overflow-scrolling:touch;margin:0 -4px}
|
.top nav a:hover{background:var(--board-3);color:#f3f0e7}
|
||||||
table{width:100%;border-collapse:collapse;font-size:14px}th,td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--bd);white-space:nowrap}
|
.top nav a.on{background:var(--board-3);color:#f3f0e7}
|
||||||
th{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--mut);font-weight:600}
|
.top .who{color:var(--dim);font-size:12px;white-space:nowrap}
|
||||||
.num{text-align:right;font-variant-numeric:tabular-nums}
|
.top .exit{color:var(--dim);text-decoration:none}
|
||||||
code{font-size:12px;background:var(--bg-2);padding:1px 5px;border-radius:4px}
|
.cta{display:inline-flex;align-items:center;gap:8px;min-height:38px;padding:0 14px;background:var(--amber);color:var(--ink);text-decoration:none;border:0;border-radius:3px;cursor:pointer;font:inherit}
|
||||||
input,select,textarea{padding:10px 12px;border:1px solid var(--bd-strong);border-radius:8px;background:var(--card);color:var(--tx);font:inherit;font-size:16px;width:100%}
|
.cta:hover{filter:brightness(1.05);color:var(--ink)}
|
||||||
input:focus,select:focus,textarea:focus{outline:2px solid var(--acc);outline-offset:1px;border-color:var(--acc)}
|
.board{background:var(--board-2);color:#f3f0e7;border-bottom:3px solid var(--amber);padding:22px 20px}
|
||||||
label{font-size:13px;font-weight:500;display:block;margin-bottom:4px}
|
.board .big{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:40px;font-weight:700;letter-spacing:-.02em;line-height:1;position:relative;display:inline-block}
|
||||||
.btn{background:var(--acc);color:#fff;border:0;border-radius:8px;padding:11px 18px;font:inherit;font-size:14px;font-weight:600;cursor:pointer;text-decoration:none;display:inline-block;min-height:44px;line-height:22px}
|
.board .big::after{content:"";position:absolute;left:0;right:0;top:50%;height:1px;background:rgba(0,0,0,.28)}
|
||||||
.btn:hover{filter:brightness(1.06)}.btn.sec{background:var(--card);color:var(--tx);border:1px solid var(--bd-strong)}
|
.board-row{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:20px;max-width:1180px;margin:0 auto}
|
||||||
.btn.sm{min-height:0;padding:6px 10px;font-size:12px}
|
.stats{display:flex;gap:28px;flex-wrap:wrap}
|
||||||
.msg{padding:12px 14px;border-radius:9px;margin-bottom:14px;font-size:14px}
|
.stat{display:flex;flex-direction:column;gap:3px}
|
||||||
.msg.err{background:var(--bg-err);color:var(--tx-err)}.msg.ok{background:var(--bg-ok);color:var(--tx-ok)}
|
.stat .v{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:22px}
|
||||||
.mut{color:var(--mut);font-size:13px}
|
.imp{background:var(--board);color:#f3f0e7;padding:10px 20px;font-size:13px;display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
||||||
.badge{display:inline-block;padding:3px 10px;border-radius:20px;font-size:12px;font-weight:700;background:var(--bg-neu);color:var(--tx-neu)}
|
main{max-width:1180px;margin:0 auto;padding:22px 20px 34px}
|
||||||
.badge.ok{background:var(--bg-ok);color:var(--tx-ok)}.badge.err{background:var(--bg-err);color:var(--tx-err)}.badge.warn{background:var(--bg-warn);color:var(--tx-warn)}
|
h1{font-size:23px;margin:0 0 6px;letter-spacing:-.01em}h2{font-size:15px;margin:0 0 12px;letter-spacing:-.005em}
|
||||||
.login-wrap{max-width:380px;margin:8vh auto;padding:0 20px}
|
.sec{margin-bottom:28px}
|
||||||
.ovl{position:fixed;inset:0;background:rgba(10,12,18,.55);display:none;align-items:center;justify-content:center;padding:16px;z-index:20}
|
.sec-h{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:10px}
|
||||||
.ovl.on{display:flex}
|
.tw{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
||||||
.modal{background:var(--card);border:1px solid var(--bd);border-radius:14px;max-width:460px;width:100%;padding:22px;max-height:90vh;overflow:auto}
|
table{width:100%;border-collapse:collapse;font-size:14px}
|
||||||
.modal h2{margin-top:0}
|
th{font-family:var(--mono);font-size:10.5px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--mut);text-align:left;padding:0 12px 9px 0;border-bottom:1px solid var(--line-2);white-space:nowrap}
|
||||||
.sumrow{display:flex;justify-content:space-between;gap:12px;padding:8px 0;border-bottom:1px solid var(--bd);font-size:14px}
|
td{padding:12px 12px 12px 0;border-bottom:1px solid var(--line);vertical-align:middle}
|
||||||
.warnbox{background:var(--bg-warn);color:var(--tx-warn);border-radius:9px;padding:11px 13px;font-size:13px;font-weight:500;margin:14px 0}
|
td.num,th.num{text-align:right;padding-right:0;font-family:var(--mono);font-variant-numeric:tabular-nums}
|
||||||
@media(max-width:600px){main{padding:16px 14px}.card{padding:15px 15px}h1{font-size:20px}
|
tr.grp td{background:#e4e1d8;border-bottom:1px solid var(--line-2);padding:7px 0 7px 8px}
|
||||||
input,select,textarea{font-size:16px}}
|
.sheet{background:var(--sheet);border:1px solid var(--line-2);padding:20px}
|
||||||
|
.chip{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:3px;font-family:var(--mono);font-size:11px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;background:var(--neu-bg);color:var(--ink-2)}
|
||||||
|
.chip.go{background:var(--amber-bg);color:var(--amber-tx)}
|
||||||
|
.chip.ok{background:var(--ok-bg);color:var(--ok-tx)}
|
||||||
|
.chip.err{background:var(--err-bg);color:var(--err-tx)}
|
||||||
|
.chip.dark{background:var(--board);color:#f3f0e7}
|
||||||
|
@keyframes flip{from{transform:rotateX(-88deg)}to{transform:rotateX(0)}}
|
||||||
|
.live{animation:flip 420ms cubic-bezier(.2,.9,.25,1) 1;transform-origin:50% 0}
|
||||||
|
@media(prefers-reduced-motion:reduce){.live{animation:none}}
|
||||||
|
label{display:block;font-family:var(--mono);font-size:10.5px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--mut);margin-bottom:6px}
|
||||||
|
input,select,textarea{width:100%;min-height:44px;padding:10px 12px;border:1px solid var(--line-2);border-radius:2px;background:#fff;color:var(--ink);font:inherit;font-size:16px}
|
||||||
|
input:focus,select:focus,textarea:focus{outline:2px solid var(--acc);outline-offset:1px}
|
||||||
|
input[type=checkbox]{width:20px;min-height:20px;height:20px}
|
||||||
|
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:46px;padding:0 18px;border:1px solid var(--line-2);border-radius:3px;background:#fff;color:var(--ink);font:inherit;font-size:14px;font-weight:600;cursor:pointer;text-decoration:none}
|
||||||
|
.btn:hover{border-color:var(--ink);color:var(--ink)}
|
||||||
|
.btn.main{background:var(--amber);border-color:var(--amber);color:var(--ink)}
|
||||||
|
.btn.dark{background:var(--board);border-color:var(--board);color:#f3f0e7}
|
||||||
|
.btn.sm{min-height:38px;font-size:13px;font-weight:500}
|
||||||
|
.msg{padding:12px 14px;border-radius:3px;margin-bottom:16px;font-size:14px}
|
||||||
|
.msg.err{background:var(--err-bg);color:var(--err-tx)}
|
||||||
|
.msg.ok{background:var(--ok-bg);color:var(--ok-tx)}
|
||||||
|
.mut{color:var(--ink-2);font-size:12.5px;line-height:1.65}
|
||||||
|
.grid2{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:22px}
|
||||||
|
.rows{display:flex;flex-direction:column}
|
||||||
|
.row{display:flex;justify-content:space-between;gap:14px;padding:11px 0;border-bottom:1px solid var(--line);font-size:14px}
|
||||||
|
.empty{padding:26px 0;color:var(--ink-2);font-size:14px}
|
||||||
|
.login{min-height:100vh;display:grid;grid-template-columns:minmax(0,460px) 1fr}
|
||||||
|
.login .side{background:var(--board);color:#d9d3c6;padding:40px 36px;display:flex;flex-direction:column;justify-content:space-between;gap:30px}
|
||||||
|
.login .side h1{color:#f3f0e7;font-size:30px;line-height:1.2;margin:0}
|
||||||
|
.login .form{display:flex;align-items:center;justify-content:center;padding:36px 20px}
|
||||||
|
.login form{width:100%;max-width:380px;display:flex;flex-direction:column;gap:16px}
|
||||||
|
@media(max-width:820px){.login{grid-template-columns:1fr}.login .side{padding:26px 20px}.login .side h1{font-size:24px}}
|
||||||
|
@media(max-width:600px){main{padding:18px 14px 28px}.board{padding:18px 14px}.board .big{font-size:32px}.top{padding:0 14px}.sheet{padding:16px 14px}}
|
||||||
</style></head><body>{{end}}
|
</style></head><body>{{end}}
|
||||||
|
|
||||||
{{define "nav"}}<div class="top"><span class="brand">{{.Title}}</span>
|
{{define "mark"}}<svg viewBox="0 0 28 28" width="21" height="21" fill="none" stroke="#e8a33d" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="6" cy="14" r="3"/><circle cx="22" cy="14" r="3"/><path d="M9 14h10"/><path d="M11 10.5h6M11 17.5h6"/></svg>{{end}}
|
||||||
{{if .IsAdmin}}<a href="/clients" class="{{if eq .Nav "a"}}on{{end}}">Клиенты</a>{{end}}
|
|
||||||
<a href="/" class="{{if eq .Nav "p"}}on{{end}}">Мои бумаги</a>
|
{{define "nav"}}<header class="top">
|
||||||
<a href="/transfer" class="{{if eq .Nav "t"}}on{{end}}">Перевести бумаги</a>
|
<span class="brand">{{template "mark" .}}<span class="lbl" style="font-size:12px;letter-spacing:.14em">{{.Title}}</span></span>
|
||||||
<a href="/claims" class="{{if eq .Nav "c"}}on{{end}}">Мои заявки</a>
|
<nav>
|
||||||
<span class="right">{{with .FullName}}{{.}}{{end}}{{with .Contract}} · договор {{.}}{{end}} <a href="/logout">Выйти</a></span>
|
{{if .IsAdmin}}<a href="/clients" class="lbl {{if eq .Nav "a"}}on{{end}}">Клиенты</a>{{end}}
|
||||||
</div>
|
<a href="/" class="lbl {{if eq .Nav "b"}}on{{end}}">Табло</a>
|
||||||
{{if .AsClient}}<div style="background:#fff4d6;border-bottom:1px solid #e8c877;padding:9px 20px;font-size:13px">
|
<a href="/portfolio" class="lbl {{if eq .Nav "p"}}on{{end}}">Реестр</a>
|
||||||
👤 Вы просматриваете кабинет клиента <b>{{.AsClient}}</b> от имени администратора ·
|
<a href="/orders" class="lbl {{if eq .Nav "c"}}on{{end}}">Поручения</a>
|
||||||
<a href="/back-to-admin">вернуться к списку клиентов</a></div>{{end}}{{end}}
|
<a href="/profile" class="lbl {{if eq .Nav "u"}}on{{end}}">Профиль</a>
|
||||||
|
</nav>
|
||||||
|
<a href="/order" class="cta lbl"><svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M8 3v10M3 8h10"/></svg>Новое поручение</a>
|
||||||
|
<span class="who">{{with .FullName}}{{.}}{{end}}{{with .Contract}} · {{.}}{{end}}</span>
|
||||||
|
<a href="/logout" class="exit lbl">Выход</a>
|
||||||
|
</header>
|
||||||
|
{{if .AsClient}}<div class="imp">
|
||||||
|
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="#e8a33d" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="8" cy="8" r="6.5"/><path d="M8 5v4M8 11h.01"/></svg>
|
||||||
|
<span>Вы в кабинете клиента <b>{{.AsClient}}</b> от имени сотрудника. Поручение можно подготовить, подписывает его клиент.</span>
|
||||||
|
<a href="/back-to-admin" style="color:#e8a33d">вернуться к списку клиентов</a></div>{{end}}{{end}}
|
||||||
|
|
||||||
{{define "login"}}{{template "head" .}}
|
{{define "login"}}{{template "head" .}}
|
||||||
<div class="login-wrap">
|
<div class="login">
|
||||||
<div class="card">
|
<section class="side">
|
||||||
<h1>{{.Title}}</h1>
|
<span class="brand">{{template "mark" .}}<span class="lbl" style="font-size:12px;letter-spacing:.14em;color:#f3f0e7">{{.Title}}</span></span>
|
||||||
<p class="mut">Войдите, чтобы посмотреть свои ценные бумаги и подать заявку на перевод к другому брокеру.</p>
|
<div>
|
||||||
{{with .Flash}}<div class="msg err">{{.}}</div>{{end}}
|
<h1>Ваши бумаги, поручения и выплаты — на одном табло</h1>
|
||||||
<form method="post" action="/login" style="display:grid;gap:12px;margin-top:14px">
|
<div style="display:flex;flex-direction:column;gap:12px;margin-top:22px;font-size:14.5px">
|
||||||
<div><label for="f-login">Логин</label><input id="f-login" name="login" autocomplete="username" required autofocus></div>
|
<span style="display:flex;gap:12px"><span class="n" style="color:#e8a33d">01</span><span>Реестр бумаг по счетам депо и остаток, свободный к переводу</span></span>
|
||||||
<div><label for="f-pass">Пароль</label><input id="f-pass" name="password" type="password" autocomplete="current-password" required></div>
|
<span style="display:flex;gap:12px"><span class="n" style="color:#e8a33d">02</span><span>Поручения депозитарию и их состояние по шагам</span></span>
|
||||||
<button class="btn" type="submit" style="width:100%">Войти</button>
|
<span style="display:flex;gap:12px"><span class="n" style="color:#e8a33d">03</span><span>Перевод бумаг в другой депозитарий через сервис MOEX МОСТ</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mut" style="color:#8e8879;margin:0">Кабинет работает на сервере депозитария и обращается к учётной системе по защищённому каналу. Данные о вас хранятся в учётной системе, а не в кабинете.</p>
|
||||||
|
</section>
|
||||||
|
<section class="form">
|
||||||
|
<form method="post" action="/login">
|
||||||
|
<h1 style="margin:0">Вход</h1>
|
||||||
|
{{if .Error}}<div class="msg err">{{.Error}}</div>{{end}}
|
||||||
|
<div><label for="login">Логин или номер договора</label><input id="login" name="login" autocomplete="username" autofocus></div>
|
||||||
|
<div><label for="password">Пароль</label><input id="password" name="password" type="password" autocomplete="current-password"></div>
|
||||||
|
<button class="btn main" type="submit">Войти</button>
|
||||||
|
<p class="mut" style="margin:0">Логин выдаёт депозитарий. Сотрудник входит под своей учётной записью и не знает пароль клиента.</p>
|
||||||
</form>
|
</form>
|
||||||
<p class="mut" style="margin-top:14px">🔒 Соединение защищено. Мы не запрашиваем пароль по телефону или в письмах.</p>
|
</section>
|
||||||
</div>
|
|
||||||
<p class="mut" style="text-align:center">Логин и пароль выдаёт ваш брокер. Не можете войти — обратитесь в поддержку брокера.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</body></html>{{end}}
|
</body></html>{{end}}
|
||||||
|
|
||||||
{{define "portfolio"}}{{template "head" .}}{{template "nav" .}}
|
{{define "board"}}{{template "head" .}}{{template "nav" .}}
|
||||||
<main>
|
<section class="board"><div class="board-row">
|
||||||
<h1>Мои ценные бумаги</h1>
|
<div>
|
||||||
{{if .Source}}<p class="mut">Источник данных: {{.Source}}{{if .Contract}} · договор {{.Contract}}{{end}}</p>{{end}}
|
<span class="lbl" style="color:var(--dim)">Бумаг в реестре</span>
|
||||||
{{with .Flash}}<div class="msg ok">{{.}}</div>{{end}}
|
<div class="big" style="margin-top:8px">{{.Total}}</div>
|
||||||
{{with .Error}}<div class="msg err">{{.}}</div>{{end}}
|
<div class="n" style="font-size:13px;color:var(--dim);margin-top:6px">{{with .FullName}}{{.}} · {{end}}договор {{.Contract}}</div>
|
||||||
<div class="card">
|
|
||||||
{{if .Positions}}
|
|
||||||
<div class="tw"><table>
|
|
||||||
<thead><tr><th>Бумага</th><th>ISIN</th><th class="num">Всего</th><th class="num">Заблокировано</th><th class="num">Доступно к переводу</th></tr></thead>
|
|
||||||
<tbody>
|
|
||||||
{{range .Positions}}
|
|
||||||
<tr>
|
|
||||||
<td>{{if .SecurityName}}{{.SecurityName}}{{else}}{{.SecurityCode}}{{end}}</td>
|
|
||||||
<td><code>{{.ISIN}}</code></td>
|
|
||||||
<td class="num">{{fmtnum .Quantity}}</td>
|
|
||||||
<td class="num">{{if .Blocked}}{{fmtnum .Blocked}}{{else}}—{{end}}</td>
|
|
||||||
<td class="num">{{if .Transferable}}<b>{{fmtnum .Available}}</b>{{else}}<span class="mut" title="{{.Note}}">не переводится</span>{{end}}</td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
</tbody>
|
|
||||||
</table></div>
|
|
||||||
<p class="mut" style="margin-top:12px">Через сервис MOEX МОСТ переводятся только российские ценные бумаги (ISIN начинается с RU). Иностранные бумаги показаны для полноты портфеля, но заявку по ним подать нельзя.</p>
|
|
||||||
{{if .ReadOnly}}
|
|
||||||
<p class="mut" style="margin-top:16px">📋 Данные загружены из депозитарного учёта. Подача заявок на перевод для этого договора пока недоступна — раздел работает в режиме просмотра.</p>
|
|
||||||
{{else}}
|
|
||||||
<p style="margin-top:16px"><a class="btn" href="/transfer">Перевести бумаги другому брокеру</a></p>
|
|
||||||
{{end}}
|
|
||||||
{{else}}
|
|
||||||
<p class="mut">Бумаг на счёте не найдено. Если вы считаете, что это ошибка — обратитесь к вашему брокеру.</p>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stats">
|
||||||
|
<span class="stat"><span class="lbl" style="color:var(--dim)">Свободно к переводу</span><span class="v">{{.Free}}</span></span>
|
||||||
|
<span class="stat"><span class="lbl" style="color:var(--dim)">С ограничением</span><span class="v" style="color:var(--amber)">{{.Blocked}}</span></span>
|
||||||
|
<span class="stat"><span class="lbl" style="color:var(--dim)">Поручений в работе</span><span class="v">{{.Active}}</span></span>
|
||||||
|
</div>
|
||||||
|
</div></section>
|
||||||
|
<main>
|
||||||
|
{{if .Flash}}<div class="msg ok">{{.Flash}}</div>{{end}}
|
||||||
|
{{if .Error}}<div class="msg err">{{.Error}}</div>{{end}}
|
||||||
|
<section class="sec">
|
||||||
|
<div class="sec-h"><h1 class="lbl" style="font-size:12px;letter-spacing:.14em">Последние поручения</h1><a href="/orders" style="font-size:13px;text-decoration:none">все поручения</a></div>
|
||||||
|
{{if .Claims}}<div class="tw"><table>
|
||||||
|
<thead><tr><th>Номер</th><th>Дата</th><th>Вид поручения</th><th class="num">Бумаг</th><th>Состояние</th></tr></thead>
|
||||||
|
<tbody>{{range .Claims}}<tr>
|
||||||
|
<td class="n">{{shortid .GUID}}</td>
|
||||||
|
<td class="n">{{.CreatedAt}}</td>
|
||||||
|
<td>Перевод в другой депозитарий</td>
|
||||||
|
<td class="num">{{.Securit}}</td>
|
||||||
|
<td><span class="chip {{if eq .Status "done"}}ok{{else if eq .Status "rejected"}}err{{else if eq .Status "declined"}}err{{else if eq .Status "error"}}err{{else}}go{{end}}">{{.StatusRu}}</span>{{with .Reason}}<div class="mut">{{.}}</div>{{end}}</td>
|
||||||
|
</tr>{{end}}</tbody></table></div>
|
||||||
|
{{else}}<p class="empty">Поручений пока нет. Нажмите «Новое поручение», чтобы перевести бумаги в другой депозитарий.</p>{{end}}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="sec">
|
||||||
|
<div class="sec-h"><h2 class="lbl" style="font-size:12px;letter-spacing:.14em">Реестр бумаг</h2><a href="/portfolio" style="font-size:13px;text-decoration:none">весь реестр</a></div>
|
||||||
|
{{if .Positions}}<div class="tw"><table>
|
||||||
|
<thead><tr><th>Выпуск</th><th>Счёт депо</th><th class="num">Количество</th><th class="num">Свободно</th><th>Состояние</th></tr></thead>
|
||||||
|
<tbody>{{range .Positions}}<tr>
|
||||||
|
<td><b>{{.SecurityName}}</b><div class="n" style="font-size:12.5px;color:var(--mut)">{{.ISIN}}</div></td>
|
||||||
|
<td class="n" style="font-size:13px">{{if .AccountID}}{{.AccountID}}{{else}}—{{end}}</td>
|
||||||
|
<td class="num">{{fmtnum .Quantity}}</td>
|
||||||
|
<td class="num">{{fmtnum .Available}}</td>
|
||||||
|
<td>{{if not .Transferable}}<span class="chip">не переводится</span>{{else if gt .Blocked 0.0}}<span class="chip go">{{fmtnum .Blocked}} с ограничением</span>{{else}}<span class="chip ok">свободно</span>{{end}}</td>
|
||||||
|
</tr>{{end}}</tbody></table></div>
|
||||||
|
{{else}}<p class="empty">Бумаг на счетах депо не найдено. Если это ошибка — напишите в депозитарий.</p>{{end}}
|
||||||
|
</section>
|
||||||
|
</main></body></html>{{end}}
|
||||||
|
|
||||||
|
{{define "portfolio"}}{{template "head" .}}{{template "nav" .}}
|
||||||
|
<section class="board"><div class="board-row">
|
||||||
|
<div><span class="lbl" style="color:var(--dim)">Реестр бумаг</span>
|
||||||
|
<div class="n" style="font-size:13px;color:var(--dim);margin-top:6px">{{.Source}}{{with .UpdatedAt}} · {{.}}{{end}}</div></div>
|
||||||
|
<div class="stats">
|
||||||
|
<span class="stat"><span class="lbl" style="color:var(--dim)">Выпусков</span><span class="v">{{.Total}}</span></span>
|
||||||
|
<span class="stat"><span class="lbl" style="color:var(--dim)">Свободно к переводу</span><span class="v">{{.Free}}</span></span>
|
||||||
|
<span class="stat"><span class="lbl" style="color:var(--dim)">С ограничением</span><span class="v" style="color:var(--amber)">{{.Blocked}}</span></span>
|
||||||
|
</div>
|
||||||
|
</div></section>
|
||||||
|
<main>
|
||||||
|
{{if .Flash}}<div class="msg ok">{{.Flash}}</div>{{end}}
|
||||||
|
{{if .Error}}<div class="msg err">{{.Error}}</div>{{end}}
|
||||||
|
{{if .ReadOnly}}<div class="msg">Кабинет открыт в режиме просмотра: подача поручений по этому договору выключена депозитарием.</div>{{end}}
|
||||||
|
{{if .Positions}}
|
||||||
|
<div class="tw"><table>
|
||||||
|
<thead><tr><th>Выпуск</th><th>Счёт депо</th><th class="num">Количество</th><th class="num">С ограничением</th><th class="num">Свободно</th><th>Состояние</th><th></th></tr></thead>
|
||||||
|
<tbody>{{range .Positions}}<tr>
|
||||||
|
<td><b>{{.SecurityName}}</b><div class="n" style="font-size:12.5px;color:var(--mut)">{{.ISIN}}{{with .SecurityCode}} · {{.}}{{end}}</div></td>
|
||||||
|
<td class="n" style="font-size:13px">{{if .AccountID}}{{.AccountID}}{{else}}—{{end}}</td>
|
||||||
|
<td class="num">{{fmtnum .Quantity}}</td>
|
||||||
|
<td class="num">{{if gt .Blocked 0.0}}<span style="color:var(--amber-tx)">{{fmtnum .Blocked}}</span>{{else}}—{{end}}</td>
|
||||||
|
<td class="num">{{fmtnum .Available}}</td>
|
||||||
|
<td>{{if not .Transferable}}<span class="chip">иностранный выпуск</span>{{else if gt .Blocked 0.0}}<span class="chip go">частично ограничено</span>{{else}}<span class="chip ok">свободно</span>{{end}}{{with .Note}}<div class="mut">{{.}}</div>{{end}}</td>
|
||||||
|
<td style="text-align:right">{{if and .Transferable (gt .Available 0.0) (not $.ReadOnly)}}<a href="/order?isin={{.ISIN}}" class="lbl" style="text-decoration:none">перевести</a>{{end}}</td>
|
||||||
|
</tr>{{end}}</tbody></table></div>
|
||||||
|
<p class="mut" style="margin-top:18px"><b>С ограничением</b> — бумаги в расчётах по сделке, под обременением или ограниченные в обращении: перевести их нельзя, пока ограничение не снято.
|
||||||
|
<b>Иностранный выпуск</b> — перевод между депозитариями по MOEX МОСТ работает только с российскими выпусками, код ISIN начинается на RU.</p>
|
||||||
|
{{else}}<p class="empty">Бумаг на счетах депо не найдено.</p>{{end}}
|
||||||
</main></body></html>{{end}}
|
</main></body></html>{{end}}
|
||||||
|
|
||||||
{{define "transfer"}}{{template "head" .}}{{template "nav" .}}
|
{{define "transfer"}}{{template "head" .}}{{template "nav" .}}
|
||||||
<main>
|
<main>
|
||||||
<h1>Перевод бумаг другому брокеру</h1>
|
{{if .Error}}<div class="msg err">{{.Error}}</div>{{end}}
|
||||||
{{with .Error}}<div class="msg err">{{.}}</div>{{end}}
|
<form method="post" action="/order" id="orderForm">
|
||||||
<form method="post" action="/transfer" id="tform">
|
<div class="sheet">
|
||||||
<div class="card">
|
<div style="display:flex;align-items:flex-end;justify-content:space-between;gap:20px;border-bottom:2px solid var(--ink);padding-bottom:14px;flex-wrap:wrap">
|
||||||
<h2>Куда переводим</h2>
|
<div><h1 style="margin:0">Поручение депозитарию</h1>
|
||||||
<p class="mut">Укажите депозитарий брокера, к которому переводите бумаги. Перевод оформляется на вас же — как перевод «самому себе» в другой депозитарий.</p>
|
<span class="lbl" style="color:var(--mut);margin-top:6px;display:block">Заполняется по видам, как и другие поручения кабинета</span></div>
|
||||||
<div style="display:grid;gap:12px;max-width:520px;margin-top:12px">
|
<div style="text-align:right"><span class="lbl" style="color:var(--mut);display:block">Черновик</span>
|
||||||
<div><label for="rinn">ИНН депозитария-получателя *</label>
|
<span class="n" style="font-size:16px;font-weight:700">{{.Today}}</span></div>
|
||||||
<input id="rinn" name="receiving_inn" required inputmode="numeric" pattern="\d{{"{"}}10{{"}"}}|\d{{"{"}}12{{"}"}}" maxlength="12" placeholder="7722061076"
|
|
||||||
title="ИНН организации — 10 цифр">
|
|
||||||
<p class="mut" style="margin-top:4px">ИНН депозитария вашего нового брокера — уточните его у брокера-получателя.</p></div>
|
|
||||||
<div><label for="rname">Наименование брокера/депозитария</label><input id="rname" name="receiving_name" placeholder="например, ООО «Брокер»"></div>
|
|
||||||
<div><label for="racc">Ваш счёт депо у нового брокера (если известен)</label><input id="racc" name="account_at_recv" placeholder="необязательно"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="card">
|
<div style="margin-top:20px">
|
||||||
<h2>Что переводим</h2>
|
<span class="lbl" style="display:block;margin-bottom:10px;color:var(--ink)">1. Вид поручения</span>
|
||||||
{{if .Positions}}
|
<select name="kind" aria-label="Вид поручения">
|
||||||
<div class="tw"><table>
|
<option value="m2m">Перевод бумаг в другой депозитарий</option>
|
||||||
<thead><tr><th>Бумага</th><th>ISIN</th><th class="num">Доступно</th><th style="min-width:190px">Сколько перевести</th></tr></thead>
|
<option value="other" disabled>Другие виды — подаются в депозитарии, скоро появятся здесь</option>
|
||||||
<tbody>
|
</select>
|
||||||
{{range .Positions}}
|
|
||||||
<tr>
|
|
||||||
<td>{{if .SecurityName}}{{.SecurityName}}{{else}}{{.SecurityCode}}{{end}}</td>
|
|
||||||
<td><code>{{.ISIN}}</code></td>
|
|
||||||
<td class="num">{{if .Transferable}}{{fmtnum .Available}}{{else}}—{{end}}</td>
|
|
||||||
<td>{{if .Transferable}}<input type="hidden" name="isin" value="{{.ISIN}}">
|
|
||||||
<div style="display:flex;gap:6px;align-items:center">
|
|
||||||
<input class="qty" name="qty_{{.ISIN}}" type="number" min="0" step="any" max="{{printf "%g" .Available}}"
|
|
||||||
inputmode="decimal" placeholder="0" style="max-width:120px"
|
|
||||||
aria-label="Сколько перевести: {{if .SecurityName}}{{.SecurityName}}{{else}}{{.ISIN}}{{end}}"
|
|
||||||
data-name="{{if .SecurityName}}{{.SecurityName}}{{else}}{{.ISIN}}{{end}}" data-isin="{{.ISIN}}" data-avail="{{printf "%g" .Available}}"
|
|
||||||
{{if not .Available}}disabled{{end}}>
|
|
||||||
<button type="button" class="btn sec sm qall">всё</button>
|
|
||||||
</div>
|
|
||||||
{{else}}<span class="mut">{{.Note}}</span>{{end}}</td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
</tbody>
|
|
||||||
</table></div>
|
|
||||||
<div style="margin-top:14px;max-width:520px"><label for="cmt">Комментарий (необязательно)</label><textarea id="cmt" name="comment" rows="2"></textarea></div>
|
|
||||||
<div style="margin-top:16px;display:flex;gap:10px;flex-wrap:wrap">
|
|
||||||
<button class="btn" type="submit">Проверить и подать</button>
|
|
||||||
<a class="btn sec" href="/">Отмена</a>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="mut" style="margin-top:12px">Перед подачей вы увидите сводку для проверки. После подачи заявка проходит проверку у брокера, затем направляется в НРД. Статус виден в разделе «Мои заявки».</p>
|
|
||||||
{{else}}<p class="mut">Нет доступных бумаг для перевода.</p>{{end}}
|
<div style="margin-top:20px">
|
||||||
|
<span class="lbl" style="display:block;margin-bottom:10px;color:var(--ink)">2. Что переводим</span>
|
||||||
|
{{if .Positions}}<div class="tw"><table>
|
||||||
|
<thead><tr><th style="width:40px"></th><th>Выпуск</th><th class="num">Свободно</th><th style="width:180px">Количество</th></tr></thead>
|
||||||
|
<tbody>{{range .Positions}}<tr>
|
||||||
|
<td>{{if and .Transferable (gt .Available 0.0)}}<input type="checkbox" name="isin" value="{{.ISIN}}" id="c{{.ISIN}}"{{if eq $.PickISIN .ISIN}} checked{{end}}>{{end}}</td>
|
||||||
|
<td><label for="c{{.ISIN}}" style="font-family:inherit;font-size:14px;letter-spacing:0;text-transform:none;color:var(--ink);margin:0;font-weight:600">{{.SecurityName}}</label>
|
||||||
|
<div class="n" style="font-size:12.5px;color:var(--mut)">{{.ISIN}}{{if not .Transferable}} · иностранный выпуск, перевод недоступен{{else if eq .Available 0.0}} · свободного остатка нет{{end}}</div></td>
|
||||||
|
<td class="num">{{fmtnum .Available}}</td>
|
||||||
|
<td>{{if and .Transferable (gt .Available 0.0)}}<input class="n" name="qty_{{.ISIN}}" inputmode="decimal" style="min-height:40px;text-align:right" aria-label="Количество {{.SecurityName}}">{{else}}<span class="lbl" style="color:var(--mut)">недоступно</span>{{end}}</td>
|
||||||
|
</tr>{{end}}</tbody></table></div>
|
||||||
|
{{else}}<p class="empty">Свободных бумаг для перевода нет.</p>{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:20px">
|
||||||
|
<span class="lbl" style="display:block;margin-bottom:10px;color:var(--ink)">3. Куда переводим</span>
|
||||||
|
<div class="grid2">
|
||||||
|
<div><label for="inn">ИНН принимающего депозитария</label>
|
||||||
|
<input id="inn" name="receiving_inn" inputmode="numeric" maxlength="12" required aria-describedby="innh">
|
||||||
|
<span id="innh" class="mut" style="display:block;margin-top:6px">10 цифр для организации. ИНН можно спросить у депозитария, куда переводите бумаги.</span></div>
|
||||||
|
<div><label for="rname">Наименование</label><input id="rname" name="receiving_name" placeholder="как в договоре"></div>
|
||||||
|
<div><label for="racc">Ваш счёт депо у принимающего</label><input id="racc" name="account_at_recv" placeholder="если знаете"></div>
|
||||||
|
<div><label for="cmt">Комментарий депозитарию</label><input id="cmt" name="comment" placeholder="необязательно"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-top:22px">
|
||||||
|
<button class="btn main" type="submit">Подать поручение</button>
|
||||||
|
<a class="btn" href="/">Отмена</a>
|
||||||
|
</div>
|
||||||
|
<p class="mut" style="margin-top:14px">Переводятся только российские выпуски. Бумаги для квалифицированных инвесторов принимающая сторона может не принять. После отправки в НРД отменить поручение нельзя — состояние будет видно в разделе «Поручения».</p>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="ovl" id="cov" role="dialog" aria-modal="true" aria-labelledby="ctitle">
|
|
||||||
<div class="modal">
|
|
||||||
<h2 id="ctitle">Проверьте заявку на перевод</h2>
|
|
||||||
<div class="sumrow"><span class="mut">Депозитарий-получатель</span><span id="c-recv" style="text-align:right"></span></div>
|
|
||||||
<div id="c-secs" style="margin-top:4px"></div>
|
|
||||||
<div class="warnbox">⚠️ После подачи заявку нельзя отменить или изменить. Бумаги переводятся другому брокеру. Проверьте получателя и количество.</div>
|
|
||||||
<div style="display:flex;gap:10px;flex-wrap:wrap">
|
|
||||||
<button class="btn" id="cyes" type="button">Подтверждаю, подать</button>
|
|
||||||
<button class="btn sec" id="cno" type="button">Вернуться</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function(){
|
|
||||||
var f=document.getElementById('tform'); if(!f) return;
|
|
||||||
var ov=document.getElementById('cov');
|
|
||||||
// «всё» — заполнить доступным количеством
|
|
||||||
f.querySelectorAll('.qall').forEach(function(b){
|
|
||||||
b.addEventListener('click',function(){
|
|
||||||
var q=b.parentNode.querySelector('.qty'); if(q){q.value=q.getAttribute('data-avail'); q.focus();}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
function fmt(n){ n=Number(n); return (n%1===0)? String(n): String(n); }
|
|
||||||
f.addEventListener('submit',function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
// собрать выбранные бумаги
|
|
||||||
var rows=[], bad=null;
|
|
||||||
f.querySelectorAll('.qty').forEach(function(q){
|
|
||||||
var v=parseFloat((q.value||'').replace(',','.'));
|
|
||||||
if(!q.value||isNaN(v)||v<=0) return;
|
|
||||||
var av=parseFloat(q.getAttribute('data-avail'));
|
|
||||||
if(v>av){ bad=q.getAttribute('data-name')+': запрошено '+fmt(v)+', доступно '+fmt(av); }
|
|
||||||
rows.push({name:q.getAttribute('data-name'), isin:q.getAttribute('data-isin'), qty:v});
|
|
||||||
});
|
|
||||||
var inn=(f.receiving_inn.value||'').trim();
|
|
||||||
if(!/^(\d{10}|\d{12})$/.test(inn)){ f.receiving_inn.focus(); alert('Проверьте ИНН депозитария-получателя: должно быть 10 цифр.'); return; }
|
|
||||||
if(rows.length===0){ alert('Укажите количество хотя бы по одной бумаге.'); return; }
|
|
||||||
if(bad){ alert('Превышен доступный остаток. '+bad); return; }
|
|
||||||
// заполнить сводку
|
|
||||||
var rn=(f.receiving_name.value||'').trim();
|
|
||||||
document.getElementById('c-recv').innerHTML='<b>ИНН '+inn+'</b>'+(rn?('<br><span class="mut">'+rn+'</span>'):'');
|
|
||||||
var h=''; rows.forEach(function(r){
|
|
||||||
h+='<div class="sumrow"><span>'+r.name+' <span class="mut">('+r.isin+')</span></span><b>'+fmt(r.qty)+' шт</b></div>';
|
|
||||||
});
|
|
||||||
document.getElementById('c-secs').innerHTML=h;
|
|
||||||
ov.classList.add('on');
|
|
||||||
});
|
|
||||||
document.getElementById('cno').addEventListener('click',function(){ ov.classList.remove('on'); });
|
|
||||||
ov.addEventListener('click',function(e){ if(e.target===ov) ov.classList.remove('on'); });
|
|
||||||
document.getElementById('cyes').addEventListener('click',function(){
|
|
||||||
var b=this; b.disabled=true; b.textContent='Отправляем…'; f.submit();
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</main></body></html>{{end}}
|
</main></body></html>{{end}}
|
||||||
|
|
||||||
{{define "claims"}}{{template "head" .}}{{template "nav" .}}
|
{{define "claims"}}{{template "head" .}}{{template "nav" .}}
|
||||||
|
<section class="board"><div class="board-row">
|
||||||
|
<div><span class="lbl" style="color:var(--dim)">Поручения депозитарию</span>
|
||||||
|
<div class="n" style="font-size:13px;color:var(--dim);margin-top:6px">все виды поручений кабинета</div></div>
|
||||||
|
<div class="stats">
|
||||||
|
<span class="stat"><span class="lbl" style="color:var(--dim)">Всего</span><span class="v">{{.Total}}</span></span>
|
||||||
|
<span class="stat"><span class="lbl" style="color:var(--dim)">В работе</span><span class="v" style="color:var(--amber)">{{.Active}}</span></span>
|
||||||
|
<span class="stat"><span class="lbl" style="color:var(--dim)">Исполнено</span><span class="v">{{.Done}}</span></span>
|
||||||
|
</div>
|
||||||
|
</div></section>
|
||||||
<main>
|
<main>
|
||||||
<h1>Мои заявки</h1>
|
{{if .Flash}}<div class="msg ok">{{.Flash}}</div>{{end}}
|
||||||
{{with .Flash}}<div class="msg ok">{{.}}</div>{{end}}
|
{{if .Error}}<div class="msg err">{{.Error}}</div>{{end}}
|
||||||
{{with .Error}}<div class="msg err">{{.}}</div>{{end}}
|
{{if .Claims}}<div class="tw"><table>
|
||||||
<div class="card">
|
<thead><tr><th>Номер</th><th>Дата</th><th>Вид поручения</th><th class="num">Бумаг</th><th>Состояние</th><th>Примечание</th></tr></thead>
|
||||||
{{if .Claims}}
|
<tbody>{{range .Claims}}<tr>
|
||||||
<div class="tw"><table>
|
<td class="n">{{shortid .GUID}}</td>
|
||||||
<thead><tr><th>Дата</th><th>Номер</th><th class="num">Бумаг</th><th>Статус</th><th>Примечание</th></tr></thead>
|
<td class="n">{{.CreatedAt}}</td>
|
||||||
<tbody>
|
<td>Перевод в другой депозитарий</td>
|
||||||
{{range .Claims}}
|
<td class="num">{{.Securit}}</td>
|
||||||
<tr>
|
<td><span class="chip {{if eq .Status "done"}}ok{{else if eq .Status "rejected"}}err{{else if eq .Status "declined"}}err{{else if eq .Status "error"}}err{{else}}go{{end}}">{{.StatusRu}}</span></td>
|
||||||
<td>{{.CreatedAt}}</td>
|
<td class="mut">{{if .Reason}}{{.Reason}}{{else}}—{{end}}</td>
|
||||||
<td><code title="{{.GUID}}">{{shortid .GUID}}</code></td>
|
</tr>{{end}}</tbody></table></div>
|
||||||
<td class="num">{{.Securit}}</td>
|
<p class="mut" style="margin-top:18px">Пока поручение в работе, бумаги остаются на счёте, но перевести их повторно нельзя. Если принимающая сторона откажет, бумаги освободятся в тот же день, а причина появится в примечании.</p>
|
||||||
<td>{{if or (eq .Status "rejected") (eq .Status "declined") (eq .Status "error")}}<span class="badge err">{{.StatusRu}}</span>
|
{{else}}<p class="empty">Поручений пока нет.</p>{{end}}
|
||||||
{{else if eq .Status "done"}}<span class="badge ok">{{.StatusRu}}</span>
|
</main></body></html>{{end}}
|
||||||
{{else}}<span class="badge">{{.StatusRu}}</span>{{end}}</td>
|
|
||||||
<td class="mut" style="white-space:normal;min-width:180px">{{.Reason}}</td>
|
{{define "profile"}}{{template "head" .}}{{template "nav" .}}
|
||||||
</tr>
|
<section class="board"><div class="board-row">
|
||||||
{{end}}
|
<div><span class="lbl" style="color:var(--dim)">Профиль</span>
|
||||||
</tbody>
|
<div style="font-size:23px;font-weight:700;margin-top:8px">{{if .FullName}}{{.FullName}}{{else}}Клиент депозитария{{end}}</div></div>
|
||||||
</table></div>
|
<div class="stats">
|
||||||
<p class="mut" style="margin-top:12px">Наведите на номер, чтобы увидеть полный идентификатор заявки. Он понадобится при обращении в поддержку.</p>
|
<span class="stat"><span class="lbl" style="color:var(--dim)">Договор</span><span class="v">{{.Contract}}</span></span>
|
||||||
{{else}}
|
<span class="stat"><span class="lbl" style="color:var(--dim)">Счетов депо</span><span class="v">{{.Accounts}}</span></span>
|
||||||
<p class="mut">Заявок пока нет. <a href="/transfer">Подать заявку на перевод</a></p>
|
<span class="stat"><span class="lbl" style="color:var(--dim)">Подача поручений</span><span class="v">{{if .ReadOnly}}выключена{{else}}доступна{{end}}</span></span>
|
||||||
{{end}}
|
</div>
|
||||||
|
</div></section>
|
||||||
|
<main>
|
||||||
|
{{if .Error}}<div class="msg err">{{.Error}}</div>{{end}}
|
||||||
|
<div class="grid2">
|
||||||
|
<section class="sec">
|
||||||
|
<h2 class="lbl" style="font-size:12px;letter-spacing:.14em">Счета депо</h2>
|
||||||
|
<div class="rows">{{range .AccountList}}<div class="row"><span class="n">{{.}}</span><span class="chip ok">открыт</span></div>{{else}}<p class="empty">Счета не найдены.</p>{{end}}</div>
|
||||||
|
</section>
|
||||||
|
<section class="sec">
|
||||||
|
<h2 class="lbl" style="font-size:12px;letter-spacing:.14em">Доступ и безопасность</h2>
|
||||||
|
<div class="rows">
|
||||||
|
<div class="row"><span>Вход в кабинет</span><span class="n">{{.Login}}</span></div>
|
||||||
|
<div class="row"><span>Подача поручений</span><span>{{if .ReadOnly}}выключена депозитарием{{else}}доступна{{end}}</span></div>
|
||||||
|
{{if .AsClient}}<div class="row"><span>Сейчас в кабинете</span><span>сотрудник депозитария</span></div>{{end}}
|
||||||
|
<div class="row"><span>Завершить работу</span><a href="/logout">выйти</a></div>
|
||||||
|
</div>
|
||||||
|
<p class="mut" style="margin-top:14px">Контакты, паспортные данные и реквизиты для выплат меняет депозитарий: обратитесь к своему менеджеру, изменения появятся в кабинете после внесения в анкету.</p>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main></body></html>{{end}}
|
</main></body></html>{{end}}
|
||||||
|
|
||||||
{{define "clients"}}{{template "head" .}}{{template "nav" .}}
|
{{define "clients"}}{{template "head" .}}{{template "nav" .}}
|
||||||
|
<section class="board"><div class="board-row">
|
||||||
|
<div><span class="lbl" style="color:var(--dim)">Режим сотрудника</span>
|
||||||
|
<div style="font-size:23px;font-weight:700;margin-top:8px">Клиенты депозитария</div></div>
|
||||||
|
<div class="stats">
|
||||||
|
<span class="stat"><span class="lbl" style="color:var(--dim)">Учётных записей</span><span class="v">{{len .Clients}}</span></span>
|
||||||
|
<span class="stat"><span class="lbl" style="color:var(--dim)">Договоров в учёте</span><span class="v">{{len .Depo}}</span></span>
|
||||||
|
</div>
|
||||||
|
</div></section>
|
||||||
<main>
|
<main>
|
||||||
<h1>Клиенты</h1>
|
{{if .Error}}<div class="msg err">{{.Error}}</div>{{end}}
|
||||||
<p class="mut">Список клиентов личного кабинета. Можно открыть кабинет любого клиента, чтобы посмотреть его бумаги и заявки его глазами.</p>
|
<section class="sec">
|
||||||
{{with .Flash}}<div class="msg err">{{.}}</div>{{end}}
|
<h2 class="lbl" style="font-size:12px;letter-spacing:.14em">Учётные записи кабинета</h2>
|
||||||
{{with .Error}}<div class="msg err">{{.}}</div>{{end}}
|
{{if .Clients}}<div class="tw"><table>
|
||||||
<div class="card">
|
<thead><tr><th>Клиент</th><th>Логин</th><th>Договор</th><th>Состояние</th><th></th></tr></thead>
|
||||||
{{if .Clients}}
|
<tbody>{{range .Clients}}<tr>
|
||||||
<div class="tw"><table>
|
<td><b>{{.FullName}}</b></td><td class="n">{{.Login}}</td><td class="n">{{.Contract}}</td>
|
||||||
<thead><tr><th>Клиент</th><th>Логин</th><th>Договор</th><th>E-mail</th><th>Статус</th><th></th></tr></thead>
|
<td>{{if .Active}}<span class="chip ok">активна</span>{{else}}<span class="chip err">заблокирована</span>{{end}}</td>
|
||||||
<tbody>
|
<td style="text-align:right"><form method="post" action="/impersonate" style="margin:0"><input type="hidden" name="login" value="{{.Login}}"><button class="btn sm" type="submit">Открыть кабинет</button></form></td>
|
||||||
{{range .Clients}}
|
</tr>{{end}}</tbody></table></div>
|
||||||
<tr>
|
{{else}}<p class="empty">Учётных записей кабинета пока нет.</p>{{end}}
|
||||||
<td>{{if .FullName}}{{.FullName}}{{else}}—{{end}}{{if .IsAdmin}} <span class="badge">админ</span>{{end}}</td>
|
</section>
|
||||||
<td><code>{{.Login}}</code></td>
|
|
||||||
<td>{{.Contract}}</td>
|
|
||||||
<td class="mut">{{.Email}}</td>
|
|
||||||
<td>{{if .Active}}<span class="badge ok">активен</span>{{else}}<span class="badge err">заблокирован</span>{{end}}</td>
|
|
||||||
<td>{{if not .IsAdmin}}<a class="btn sec" href="/impersonate?login={{.Login}}">Открыть кабинет</a>{{end}}</td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
</tbody>
|
|
||||||
</table></div>
|
|
||||||
{{else if not .Error}}
|
|
||||||
<p class="mut">Клиентов пока нет.</p>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Клиенты депозитария</h2>
|
<section class="sec">
|
||||||
<p class="mut">Реальные договоры из депозитарного учёта. Учётная запись в кабинете им не нужна —
|
<div class="sec-h"><h2 class="lbl" style="font-size:12px;letter-spacing:.14em">Клиенты депозитария</h2></div>
|
||||||
администратор открывает кабинет по договору в режиме просмотра.</p>
|
<form method="get" action="/clients" style="display:flex;gap:10px;align-items:flex-end;max-width:560px;margin-bottom:14px">
|
||||||
{{with .DepoError}}<div class="msg err">Депозитарий недоступен: {{.}}</div>{{end}}
|
<div style="flex-grow:1"><label for="s">Поиск</label><input id="s" name="search" value="{{.Search}}" placeholder="фамилия или номер договора"></div>
|
||||||
<div class="card">
|
|
||||||
<form method="get" action="/clients" class="row" style="margin-bottom:12px">
|
|
||||||
<input type="text" name="search" value="{{.Search}}" placeholder="номер договора или ФИО, например БР-1001 или Гравс" style="flex:1">
|
|
||||||
<button class="btn" type="submit">Найти</button>
|
<button class="btn" type="submit">Найти</button>
|
||||||
{{if .Search}}<a class="btn sec" href="/clients">Сбросить</a>{{end}}
|
|
||||||
</form>
|
</form>
|
||||||
{{if .Depo}}
|
{{if .DepoError}}<div class="msg err">{{.DepoError}}</div>{{end}}
|
||||||
<div class="tw"><table>
|
{{if .Depo}}<div class="tw"><table>
|
||||||
<thead><tr><th>Клиент</th><th>Договор</th><th>Бумаг</th><th></th></tr></thead>
|
<thead><tr><th>Клиент</th><th>Договор</th><th class="num">Бумаг</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>{{range .Depo}}<tr>
|
||||||
{{range .Depo}}
|
<td><b>{{.Name}}</b></td><td class="n">{{.Contract}}</td><td class="num">{{.Securities}}</td>
|
||||||
<tr>
|
<td style="text-align:right"><form method="post" action="/impersonate" style="margin:0"><input type="hidden" name="contract" value="{{.Contract}}"><button class="btn sm" type="submit">Открыть кабинет</button></form></td>
|
||||||
<td>{{if .Name}}{{.Name}}{{else}}—{{end}}</td>
|
</tr>{{end}}</tbody></table></div>
|
||||||
<td><code>{{.Contract}}</code></td>
|
{{else}}<p class="empty">Ничего не найдено.</p>{{end}}
|
||||||
<td>{{.Securities}}</td>
|
</section>
|
||||||
<td><a class="btn sec" href="/impersonate?contract={{.Contract}}">Открыть кабинет</a></td>
|
|
||||||
</tr>
|
<p class="mut">В кабинете клиента сотрудник готовит поручение, подписывает его клиент. Каждый вход сотрудника записывается в журнал.</p>
|
||||||
{{end}}
|
|
||||||
</tbody>
|
|
||||||
</table></div>
|
|
||||||
<p class="mut">Показаны первые 100 договоров с ненулевыми остатками. Уточните поиск, чтобы найти нужного клиента.</p>
|
|
||||||
{{else if not .DepoError}}
|
|
||||||
<p class="mut">По запросу «{{.Search}}» договоров с бумагами не найдено.</p>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</main></body></html>{{end}}
|
</main></body></html>{{end}}
|
||||||
`
|
`
|
||||||
|
|||||||
Reference in New Issue
Block a user