diff --git a/modules/lk/lk.go b/modules/lk/lk.go index 28cab86..2d54e8c 100644 --- a/modules/lk/lk.go +++ b/modules/lk/lk.go @@ -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}) }) // Администратор: список клиентов diff --git a/modules/lk/lk_test.go b/modules/lk/lk_test.go index daa96c6..c2463cf 100644 --- a/modules/lk/lk_test.go +++ b/modules/lk/lk_test.go @@ -28,7 +28,18 @@ func fakeBJ(t *testing.T) *httptest.Server { }) mux.HandleFunc("/api/lk/portfolio", 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{"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) { @@ -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")) + } + } +} diff --git a/modules/lk/templates.go b/modules/lk/templates.go index 1a598cf..f7937a4 100644 --- a/modules/lk/templates.go +++ b/modules/lk/templates.go @@ -2,310 +2,366 @@ package lk // pageTemplates — встроенные страницы Личного кабинета (без внешних зависимостей, // чтобы модуль был одним файлом и ставился на любой веб-сервер). +// +// Визуальный мир — «Табло» (см. DESIGN.md, раздел «Личный кабинет клиента»): +// графитовая доска состояния сверху, светлая бумага реестра, янтарь — «в пути» +// и первичное действие, все величины моноширинные табличные. Шрифты грузятся с +// Google Fonts, при недоступности сети подхватываются системные аналоги. const pageTemplates = ` {{define "head"}} {{.Title}} + + {{end}} -{{define "nav"}}
{{.Title}} -{{if .IsAdmin}}Клиенты{{end}} -Мои бумаги -Перевести бумаги -Мои заявки -{{with .FullName}}{{.}}{{end}}{{with .Contract}} · договор {{.}}{{end}} Выйти -
-{{if .AsClient}}
- 👤 Вы просматриваете кабинет клиента {{.AsClient}} от имени администратора · - вернуться к списку клиентов
{{end}}{{end}} +{{define "mark"}}{{end}} + +{{define "nav"}}
+ {{template "mark" .}}{{.Title}} + + Новое поручение + {{with .FullName}}{{.}}{{end}}{{with .Contract}} · {{.}}{{end}} + Выход +
+{{if .AsClient}}
+ + Вы в кабинете клиента {{.AsClient}} от имени сотрудника. Поручение можно подготовить, подписывает его клиент. + вернуться к списку клиентов
{{end}}{{end}} {{define "login"}}{{template "head" .}} -
-
-

{{.Title}}

-

Войдите, чтобы посмотреть свои ценные бумаги и подать заявку на перевод к другому брокеру.

- {{with .Flash}}
{{.}}
{{end}} -
-
-
- +
-

Логин и пароль выдаёт ваш брокер. Не можете войти — обратитесь в поддержку брокера.

+
{{end}} -{{define "portfolio"}}{{template "head" .}}{{template "nav" .}} -
-

Мои ценные бумаги

- {{if .Source}}

Источник данных: {{.Source}}{{if .Contract}} · договор {{.Contract}}{{end}}

{{end}} - {{with .Flash}}
{{.}}
{{end}} - {{with .Error}}
{{.}}
{{end}} -
- {{if .Positions}} -
- - - {{range .Positions}} - - - - - - - - {{end}} - -
БумагаISINВсегоЗаблокированоДоступно к переводу
{{if .SecurityName}}{{.SecurityName}}{{else}}{{.SecurityCode}}{{end}}{{.ISIN}}{{fmtnum .Quantity}}{{if .Blocked}}{{fmtnum .Blocked}}{{else}}—{{end}}{{if .Transferable}}{{fmtnum .Available}}{{else}}не переводится{{end}}
-

Через сервис MOEX МОСТ переводятся только российские ценные бумаги (ISIN начинается с RU). Иностранные бумаги показаны для полноты портфеля, но заявку по ним подать нельзя.

- {{if .ReadOnly}} -

📋 Данные загружены из депозитарного учёта. Подача заявок на перевод для этого договора пока недоступна — раздел работает в режиме просмотра.

- {{else}} -

Перевести бумаги другому брокеру

- {{end}} - {{else}} -

Бумаг на счёте не найдено. Если вы считаете, что это ошибка — обратитесь к вашему брокеру.

- {{end}} +{{define "board"}}{{template "head" .}}{{template "nav" .}} +
+
+ Бумаг в реестре +
{{.Total}}
+
{{with .FullName}}{{.}} · {{end}}договор {{.Contract}}
+
+ Свободно к переводу{{.Free}} + С ограничением{{.Blocked}} + Поручений в работе{{.Active}} +
+
+
+ {{if .Flash}}
{{.Flash}}
{{end}} + {{if .Error}}
{{.Error}}
{{end}} +
+

Последние поручения

все поручения
+ {{if .Claims}}
+ + {{range .Claims}} + + + + + + {{end}}
НомерДатаВид порученияБумагСостояние
{{shortid .GUID}}{{.CreatedAt}}Перевод в другой депозитарий{{.Securit}}{{.StatusRu}}{{with .Reason}}
{{.}}
{{end}}
+ {{else}}

Поручений пока нет. Нажмите «Новое поручение», чтобы перевести бумаги в другой депозитарий.

{{end}} +
+ +
+

Реестр бумаг

весь реестр
+ {{if .Positions}}
+ + {{range .Positions}} + + + + + + {{end}}
ВыпускСчёт депоКоличествоСвободноСостояние
{{.SecurityName}}
{{.ISIN}}
{{if .AccountID}}{{.AccountID}}{{else}}—{{end}}{{fmtnum .Quantity}}{{fmtnum .Available}}{{if not .Transferable}}не переводится{{else if gt .Blocked 0.0}}{{fmtnum .Blocked}} с ограничением{{else}}свободно{{end}}
+ {{else}}

Бумаг на счетах депо не найдено. Если это ошибка — напишите в депозитарий.

{{end}} +
+
{{end}} + +{{define "portfolio"}}{{template "head" .}}{{template "nav" .}} +
+
Реестр бумаг +
{{.Source}}{{with .UpdatedAt}} · {{.}}{{end}}
+
+ Выпусков{{.Total}} + Свободно к переводу{{.Free}} + С ограничением{{.Blocked}} +
+
+
+ {{if .Flash}}
{{.Flash}}
{{end}} + {{if .Error}}
{{.Error}}
{{end}} + {{if .ReadOnly}}
Кабинет открыт в режиме просмотра: подача поручений по этому договору выключена депозитарием.
{{end}} + {{if .Positions}} +
+ + {{range .Positions}} + + + + + + + + {{end}}
ВыпускСчёт депоКоличествоС ограничениемСвободноСостояние
{{.SecurityName}}
{{.ISIN}}{{with .SecurityCode}} · {{.}}{{end}}
{{if .AccountID}}{{.AccountID}}{{else}}—{{end}}{{fmtnum .Quantity}}{{if gt .Blocked 0.0}}{{fmtnum .Blocked}}{{else}}—{{end}}{{fmtnum .Available}}{{if not .Transferable}}иностранный выпуск{{else if gt .Blocked 0.0}}частично ограничено{{else}}свободно{{end}}{{with .Note}}
{{.}}
{{end}}
{{if and .Transferable (gt .Available 0.0) (not $.ReadOnly)}}перевести{{end}}
+

С ограничением — бумаги в расчётах по сделке, под обременением или ограниченные в обращении: перевести их нельзя, пока ограничение не снято. + Иностранный выпуск — перевод между депозитариями по MOEX МОСТ работает только с российскими выпусками, код ISIN начинается на RU.

+ {{else}}

Бумаг на счетах депо не найдено.

{{end}}
{{end}} {{define "transfer"}}{{template "head" .}}{{template "nav" .}}
-

Перевод бумаг другому брокеру

- {{with .Error}}
{{.}}
{{end}} -
-
-

Куда переводим

-

Укажите депозитарий брокера, к которому переводите бумаги. Перевод оформляется на вас же — как перевод «самому себе» в другой депозитарий.

-
-
- -

ИНН депозитария вашего нового брокера — уточните его у брокера-получателя.

-
-
+ {{if .Error}}
{{.Error}}
{{end}} + +
+
+

Поручение депозитарию

+ Заполняется по видам, как и другие поручения кабинета
+
Черновик + {{.Today}}
-
-
-

Что переводим

- {{if .Positions}} -
- - - {{range .Positions}} - - - - - - - {{end}} - -
БумагаISINДоступноСколько перевести
{{if .SecurityName}}{{.SecurityName}}{{else}}{{.SecurityCode}}{{end}}{{.ISIN}}{{if .Transferable}}{{fmtnum .Available}}{{else}}—{{end}}{{if .Transferable}} -
- - -
- {{else}}{{.Note}}{{end}}
-
-
- - Отмена + +
+ 1. Вид поручения +
-

Перед подачей вы увидите сводку для проверки. После подачи заявка проходит проверку у брокера, затем направляется в НРД. Статус виден в разделе «Мои заявки».

- {{else}}

Нет доступных бумаг для перевода.

{{end}} + +
+ 2. Что переводим + {{if .Positions}}
+ + {{range .Positions}} + + + + + {{end}}
ВыпускСвободноКоличество
{{if and .Transferable (gt .Available 0.0)}}{{end}} +
{{.ISIN}}{{if not .Transferable}} · иностранный выпуск, перевод недоступен{{else if eq .Available 0.0}} · свободного остатка нет{{end}}
{{fmtnum .Available}}{{if and .Transferable (gt .Available 0.0)}}{{else}}недоступно{{end}}
+ {{else}}

Свободных бумаг для перевода нет.

{{end}} +
+ +
+ 3. Куда переводим +
+
+ + 10 цифр для организации. ИНН можно спросить у депозитария, куда переводите бумаги.
+
+
+
+
+
+ +
+ + Отмена +
+

Переводятся только российские выпуски. Бумаги для квалифицированных инвесторов принимающая сторона может не принять. После отправки в НРД отменить поручение нельзя — состояние будет видно в разделе «Поручения».

- - - -
{{end}} {{define "claims"}}{{template "head" .}}{{template "nav" .}} +
+
Поручения депозитарию +
все виды поручений кабинета
+
+ Всего{{.Total}} + В работе{{.Active}} + Исполнено{{.Done}} +
+
-

Мои заявки

- {{with .Flash}}
{{.}}
{{end}} - {{with .Error}}
{{.}}
{{end}} -
- {{if .Claims}} -
- - - {{range .Claims}} - - - - - - - - {{end}} - -
ДатаНомерБумагСтатусПримечание
{{.CreatedAt}}{{shortid .GUID}}{{.Securit}}{{if or (eq .Status "rejected") (eq .Status "declined") (eq .Status "error")}}{{.StatusRu}} - {{else if eq .Status "done"}}{{.StatusRu}} - {{else}}{{.StatusRu}}{{end}}{{.Reason}}
-

Наведите на номер, чтобы увидеть полный идентификатор заявки. Он понадобится при обращении в поддержку.

- {{else}} -

Заявок пока нет. Подать заявку на перевод

- {{end}} + {{if .Flash}}
{{.Flash}}
{{end}} + {{if .Error}}
{{.Error}}
{{end}} + {{if .Claims}}
+ + {{range .Claims}} + + + + + + + {{end}}
НомерДатаВид порученияБумагСостояниеПримечание
{{shortid .GUID}}{{.CreatedAt}}Перевод в другой депозитарий{{.Securit}}{{.StatusRu}}{{if .Reason}}{{.Reason}}{{else}}—{{end}}
+

Пока поручение в работе, бумаги остаются на счёте, но перевести их повторно нельзя. Если принимающая сторона откажет, бумаги освободятся в тот же день, а причина появится в примечании.

+ {{else}}

Поручений пока нет.

{{end}} +
{{end}} + +{{define "profile"}}{{template "head" .}}{{template "nav" .}} +
+
Профиль +
{{if .FullName}}{{.FullName}}{{else}}Клиент депозитария{{end}}
+
+ Договор{{.Contract}} + Счетов депо{{.Accounts}} + Подача поручений{{if .ReadOnly}}выключена{{else}}доступна{{end}} +
+
+
+ {{if .Error}}
{{.Error}}
{{end}} +
+
+

Счета депо

+
{{range .AccountList}}
{{.}}открыт
{{else}}

Счета не найдены.

{{end}}
+
+
+

Доступ и безопасность

+
+
Вход в кабинет{{.Login}}
+
Подача поручений{{if .ReadOnly}}выключена депозитарием{{else}}доступна{{end}}
+ {{if .AsClient}}
Сейчас в кабинетесотрудник депозитария
{{end}} +
Завершить работувыйти
+
+

Контакты, паспортные данные и реквизиты для выплат меняет депозитарий: обратитесь к своему менеджеру, изменения появятся в кабинете после внесения в анкету.

+
{{end}} {{define "clients"}}{{template "head" .}}{{template "nav" .}} +
+
Режим сотрудника +
Клиенты депозитария
+
+ Учётных записей{{len .Clients}} + Договоров в учёте{{len .Depo}} +
+
-

Клиенты

-

Список клиентов личного кабинета. Можно открыть кабинет любого клиента, чтобы посмотреть его бумаги и заявки его глазами.

- {{with .Flash}}
{{.}}
{{end}} - {{with .Error}}
{{.}}
{{end}} -
- {{if .Clients}} -
- - - {{range .Clients}} - - - - - - - - - {{end}} - -
КлиентЛогинДоговорE-mailСтатус
{{if .FullName}}{{.FullName}}{{else}}—{{end}}{{if .IsAdmin}} админ{{end}}{{.Login}}{{.Contract}}{{.Email}}{{if .Active}}активен{{else}}заблокирован{{end}}{{if not .IsAdmin}}Открыть кабинет{{end}}
- {{else if not .Error}} -

Клиентов пока нет.

- {{end}} -
+ {{if .Error}}
{{.Error}}
{{end}} +
+

Учётные записи кабинета

+ {{if .Clients}}
+ + {{range .Clients}} + + + + {{end}}
КлиентЛогинДоговорСостояние
{{.FullName}}{{.Login}}{{.Contract}}{{if .Active}}активна{{else}}заблокирована{{end}}
+ {{else}}

Учётных записей кабинета пока нет.

{{end}} +
-

Клиенты депозитария

-

Реальные договоры из депозитарного учёта. Учётная запись в кабинете им не нужна — - администратор открывает кабинет по договору в режиме просмотра.

- {{with .DepoError}}
Депозитарий недоступен: {{.}}
{{end}} -
-
- +
+

Клиенты депозитария

+ +
- {{if .Search}}Сбросить{{end}} - {{if .Depo}} -
- - - {{range .Depo}} - - - - - - - {{end}} - -
КлиентДоговорБумаг
{{if .Name}}{{.Name}}{{else}}—{{end}}{{.Contract}}{{.Securities}}Открыть кабинет
-

Показаны первые 100 договоров с ненулевыми остатками. Уточните поиск, чтобы найти нужного клиента.

- {{else if not .DepoError}} -

По запросу «{{.Search}}» договоров с бумагами не найдено.

- {{end}} -
+ {{if .DepoError}}
{{.DepoError}}
{{end}} + {{if .Depo}}
+ + {{range .Depo}} + + + {{end}}
КлиентДоговорБумаг
{{.Name}}{{.Contract}}{{.Securities}}
+ {{else}}

Ничего не найдено.

{{end}} + + +

В кабинете клиента сотрудник готовит поручение, подписывает его клиент. Каждый вход сотрудника записывается в журнал.

{{end}} `