Кабинет: поиск клиентов с подсказками, чистая страница документов, ширина настроек
Поиск клиентов (режим сотрудника): - поле поиска вынесено наверх страницы, рядом «Найти» и «Сбросить»; - браузер сам подсказывает выпадающим списком по мере ввода — договоры депозитария отдаются страницей в <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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user