Кабинет: поиск клиентов с подсказками, чистая страница документов, ширина настроек

Поиск клиентов (режим сотрудника):
- поле поиска вынесено наверх страницы, рядом «Найти» и «Сбросить»;
- браузер сам подсказывает выпадающим списком по мере ввода — договоры
  депозитария отдаются страницей в <datalist> (скриптов в кабинете нет,
  CSP остаётся строгой), список кэшируется в ядре на 10 минут;
- по запросу фильтруются и учётки кабинета, и договоры депозитария,
  под формой показывается, сколько найдено;
- шаблон LIKE экранируется, чтобы % и _ в номере договора означали себя.

Документы: убраны пустое табло, «Мои заявки» и форма заказа документа —
остался список готовых документов депозитария и пояснение, куда обращаться.

Настройка: колонка сетки больше не растягивается широкой таблицей
(min-width:0), у страницы пропал горизонтальный ползунок.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
This commit is contained in:
zuevav
2026-09-18 18:45:54 +03:00
parent 4b80d030ab
commit 683f516d07
7 changed files with 184 additions and 50 deletions
+57 -1
View File
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strings"
"sync"
"time"
)
@@ -170,7 +171,7 @@ func depoListContracts(ctx context.Context, s FancyMSSQLSettings, search string,
JOIN OD_VALUES v ON v.ID = rst.VALUE_ID AND v.V_TYPE = 2
WHERE CAST(rst.K AS float) <> 0 AND (@p1 = '' OR p.PORT_NUM LIKE @p2)
GROUP BY p.PORT_NUM ORDER BY p.PORT_NUM`, limit)
rows, err := conn.QueryContext(cctx, q, search, "%"+search+"%")
rows, err := conn.QueryContext(cctx, q, search, "%"+likeEscape(search)+"%")
if err != nil {
return nil, err
}
@@ -226,3 +227,58 @@ func likeEscape(v string) string {
r := strings.NewReplacer("[", "[[]", "%", "[%]", "_", "[_]")
return r.Replace(v)
}
// ---- подсказки в поиске клиентов (админка ЛК) ----
// Список договоров нужен на каждой загрузке страницы «Клиенты»: браузер сам
// показывает выпадающий список по мере ввода (<datalist>), без скриптов на
// странице. Запрос дешёвый, но их 1300+, поэтому держим ответ в памяти.
var depoSuggestCache = struct {
mu sync.Mutex
list []string
at time.Time
}{}
// depoSuggestContracts — все номера договоров депозитария для подсказок поиска.
func depoSuggestContracts(ctx context.Context, s FancyMSSQLSettings) []string {
if strings.TrimSpace(s.Server) == "" {
return nil
}
depoSuggestCache.mu.Lock()
if time.Since(depoSuggestCache.at) < 10*time.Minute && depoSuggestCache.list != nil {
list := depoSuggestCache.list
depoSuggestCache.mu.Unlock()
return list
}
depoSuggestCache.mu.Unlock()
conn, err := fancyOpen(s, s.DepoDB())
if err != nil {
return nil
}
defer conn.Close()
cctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
rows, err := conn.QueryContext(cctx,
"SELECT DISTINCT TOP (5000) PORT_NUM FROM OD_CLIENTS_PORTS WHERE PORT_NUM <> '' ORDER BY PORT_NUM")
if err != nil {
return nil
}
defer rows.Close()
var out []string
for rows.Next() {
var v string
if rows.Scan(&v) != nil {
break
}
if v = strings.TrimSpace(v); v != "" {
out = append(out, v)
}
}
if rows.Err() != nil && len(out) == 0 {
return nil
}
depoSuggestCache.mu.Lock()
depoSuggestCache.list, depoSuggestCache.at = out, time.Now()
depoSuggestCache.mu.Unlock()
return out
}
+7 -1
View File
@@ -483,7 +483,13 @@ func registerLKAPI(mux *http.ServeMux, rc *RuntimeConfig, svc *Service) {
// админ ищет по номеру договора/ФИО и открывает кабинет на просмотр.
search := strings.TrimSpace(r.URL.Query().Get("search"))
depo, derr := depoListContracts(r.Context(), fancy, search, 100)
resp := map[string]any{"clients": list, "depo": depo, "search": search}
// Учётки кабинета ищем тем же запросом: сотрудник вводит фамилию или
// номер договора, а где заведена учётка — ему помнить не нужно.
if search != "" {
list = lkFilterUsers(list, search)
}
resp := map[string]any{"clients": list, "depo": depo, "search": search,
"suggest": depoSuggestContracts(r.Context(), fancy)}
if derr != nil {
resp["depo_error"] = derr.Error()
}
+17
View File
@@ -197,3 +197,20 @@ func lkUserByID(ctx context.Context, rc *RuntimeConfig, id int64) (*LKUser, erro
}
return &u, nil
}
// lkFilterUsers — учётки кабинета, подходящие под строку поиска сотрудника
// (часть логина, номера договора, ФИО или почты; регистр не важен).
func lkFilterUsers(list []LKUser, search string) []LKUser {
q := strings.ToLower(strings.TrimSpace(search))
if q == "" {
return list
}
out := make([]LKUser, 0, len(list))
for _, u := range list {
hay := strings.ToLower(u.Login + " " + u.Contract + " " + u.FullName + " " + u.Email)
if strings.Contains(hay, q) {
out = append(out, u)
}
}
return out
}
+11 -2
View File
@@ -191,10 +191,13 @@ input:focus, select:focus, textarea:focus { outline:2px solid var(--accent-weak)
label { font-size:13px; font-weight:500; }
/* Поле формы: подпись, ввод и пояснение — каждое со своей строки. Без этого label,
input и подсказка идут в строку и «разъезжаются» на широком экране. */
.fld { display:grid; gap:4px; align-content:start; }
/* min-width:0 обязателен: иначе длинная строка внутри (ключ, код, ссылка) не даёт
блоку сжаться и растягивает всю страницу — появляется горизонтальная прокрутка. */
.fld { display:grid; gap:4px; align-content:start; min-width:0; max-width:100%; }
.fld > * { min-width:0; }
.fld > label { font-weight:600; }
.fld input[type=text], .fld input[type=password], .fld input[type=email], .fld select { width:100%; max-width:460px; }
.fld .muted { max-width:620px; line-height:1.45; }
.fld .muted { max-width:620px; line-height:1.45; overflow-wrap:anywhere; }
/* ---- Баннер режима эмуляции ---- */
.banner-mock { background:var(--warn-weak); border-bottom:1px solid var(--warn); padding:10px 24px; display:flex; align-items:center; gap:12px; font-size:13px; }
@@ -233,6 +236,12 @@ label { font-size:13px; font-weight:500; }
/* ---- Админ-центр: боковые разделы + контент (macOS System Settings) ---- */
.settings { display:grid; grid-template-columns:236px 1fr; gap:26px; align-items:start; }
/* Колонка сетки по умолчанию не уже своего содержимого (min-width:auto), поэтому
широкая таблица растягивала всю страницу и внизу появлялся ползунок. */
.settings > * { min-width:0; }
.settings-section, .settings-section .card { min-width:0; }
.settings-section table { max-width:100%; }
.tw { overflow-x:auto; max-width:100%; }
.settings-nav { position:sticky; top:78px; display:flex; flex-direction:column; gap:2px; }
.settings-nav button {
display:flex; align-items:center; gap:11px; justify-content:flex-start;