< Summary

Information
Class: AspNetCoreGeneratedDocument.Areas_Studio2_Views_OzonChat_Index
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Views/OzonChat/Index.cshtml
Line coverage
0%
Covered lines: 0
Uncovered lines: 27
Coverable lines: 27
Total lines: 3325
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 44
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ExecuteAsync()0%2352480%
<ExecuteAsync()100%210%
<ExecuteAsync()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Views/OzonChat/Index.cshtml

#LineLine coverage
 1@model OzonOrdersWeb.Areas.Studio2.ViewModels.OzonChatViewModels.OzonChatIndexViewModel
 2@{
 03    ViewData["Title"] = "Чаты Ozon";
 04    Layout = "~/Views/Shared/_Layout.cshtml";
 5}
 6
 7<div class="container-fluid">
 8
 9
 10    <div class="col-12">
 11        <!-- Вкладки -->
 12        <ul class="nav nav-tabs mb-3" id="chatTabs" role="tablist">
 13            <li class="nav-item" role="presentation">
 14                <button class="nav-link active" id="chats-tab" data-bs-toggle="tab" data-bs-target="#chats-content"
 15                        type="button" role="tab" aria-controls="chats-content" aria-selected="true">
 16                    <i class="fas fa-comments me-1"></i>Чаты
 17                </button>
 18            </li>
 19            <li class="nav-item" role="presentation">
 20                <button class="nav-link" id="response-stats-tab" data-bs-toggle="tab"
 21                        data-bs-target="#response-stats-content"
 22                        type="button" role="tab" aria-controls="response-stats-content" aria-selected="false">
 23                    <i class="fas fa-chart-bar me-1"></i>Статистика ответов
 24                </button>
 25            </li>
 26        </ul>
 27
 28        <div class="tab-content">
 29            <!-- Вкладка Чаты -->
 30
 31
 32            <div class="tab-pane fade show active" id="chats-content" role="tabpanel" aria-labelledby="chats-tab">
 33                <div class="row g-3">
 34
 35                    <!-- Заголовок + выбор клиента -->
 36                    <div class="col-12 col-lg-3">
 37                        <div class="card shadow-sm border-0">
 38
 39                            <!-- Заголовок -->
 40                            <div class="card-header card-header-filter border-bottom py-2">
 41                                <h4 class="mb-0">
 42                                    <i class="fas fa-filter opacity-85 text-primary me-3"></i>Фильтры
 43                                </h4>
 44                            </div>
 45
 46                            <div class="card-body bg-filter-card">
 047                                @if (TempData["ErrorMessage"] != null)
 48                                {
 49                                    <div class="alert alert-danger alert-dismissible fade show" role="alert">
 050                                        <i class="fas fa-exclamation-triangle me-2"></i>@TempData["ErrorMessage"]
 51                                        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
 52                                    </div>
 53                                }
 54
 055                                @if (!Model.OzonClients.Any())
 56                                {
 57                                    <div class="text-center py-5">
 58                                        <i class="fas fa-users fa-4x text-muted mb-3"></i>
 59                                        <h5 class="text-muted">Нет настроенных клиентов Ozon</h5>
 60                                        <p class="text-muted">Для работы с чатами необходимо настроить клиентов Ozon с
 61                                            API-ключами.</p>
 62                                    </div>
 63                                }
 64                                else
 65                                {
 66                                    <!-- Клиент -->
 67                                    <div class="mb-3">
 68                                        <label class="form-label small text-muted">Клиент</label>
 69                                        <select id="sel-client" class="form-select form-select-sm custom-select-init">
 70                                            <option value="0" selected>Все клиенты</option>
 071                                            @foreach (var c in Model.OzonClients)
 72                                            {
 073                                                <option value="@c.Id">@c.Name</option>
 74                                            }
 75                                        </select>
 76                                    </div>
 77                                    <!-- Статус -->
 78                                    <div class="mb-3">
 79                                        <label class="form-label small text-muted">Статус</label>
 80                                        <select id="sel-status" class="form-select form-select-sm custom-select-init">
 81                                            <option value="All">Все чаты</option>
 82                                            <option value="Opened" selected>Открытые</option>
 83                                            <option value="Closed">Закрытые</option>
 84                                        </select>
 85                                    </div>
 86                                    <!-- Тип -->
 87                                    <div class="mb-3">
 88                                        <label class="form-label small text-muted">Тип чата</label>
 89                                        <select id="sel-type" class="form-select form-select-sm custom-select-init">
 90                                            <option value="">Все типы</option>
 91                                            <option value="Seller_Support">Чат с поддержкой</option>
 92                                            <option value="Buyer_Seller" selected>Чат с покупателем</option>
 93                                        </select>
 94                                    </div>
 95
 96                                    <!-- Сортировка -->
 97                                    <div class="mb-3">
 98                                        <label class="form-label small text-muted">Сортировка</label>
 99
 100                                        <div class="row g-2">
 101                                            <div class="col-7">
 102                                                <select id="sel-sort-field" class="form-select form-select-sm custom-sel
 103                                                    <option value="createdAt">Дата создания</option>
 104                                                    <option value="lastMessage">Посл. сообщение</option>
 105                                                </select>
 106                                            </div>
 107
 108                                            <div class="col-5">
 109                                                <select id="sel-sort-order" class="form-select form-select-sm custom-sel
 110                                                    <option value="desc">Сначала новые</option>
 111                                                    <option value="asc">Сначала старые</option>
 112                                                </select>
 113                                            </div>
 114                                        </div>
 115                                    </div>
 116
 117                                    <!-- На странице -->
 118                                    <div class="mb-4">
 119                                        <label class="form-label small text-muted">Кол-во на странице</label>
 120                                        <select id="sel-pagesize" class="form-select form-select-sm custom-select-init">
 121                                            <option value="10">10</option>
 122                                            <option value="30" selected>30</option>
 123                                            <option value="50">50</option>
 124                                            <option value="100">100</option>
 125                                        </select>
 126                                    </div>
 127
 128                                    <!-- Непрочитанные -->
 129                                    <div class="form-check mb-3">
 130                                        <input class="form-check-input" type="checkbox" id="chk-unread">
 131                                        <label class="form-check-label" for="chk-unread">
 132                                            Только непрочитанные
 133                                        </label>
 134                                    </div>
 135
 136                                    <hr class="border-top border-secondary opacity-25 my-3">
 137
 138                                    <!-- Кнопки -->
 139                                    <div class="d-flex gap-2">
 140                                        <button id="btn-load" class="btn btn-primary py-2 rounded-3">
 141                                            <i class="fas fa-search me-2"></i>Загрузить
 142                                        </button>
 143                                        <button id="btn-export" class="btn btn-outline-secondary py-1 d-none">
 144                                            <i class="fas fa-file-excel me-2"></i>CSV
 145                                        </button>
 146                                    </div>
 147                                }
 148
 149                                <hr class="border-top border-secondary opacity-25 my-3">
 150
 151                                <div class="d-flex row align-items-center gap-2">
 152                                    @* <span id="badge-unread" class="d-flex align-items-center gap-1 d-none"> *@
 153                                    @*     <i class="fas fa-envelope fa-fw me-2 text-secondary"></i> *@
 154                                    @*     <div> *@
 155                                    @*         <span>Без ответа:</span> *@
 156                                    @*         <span id="unread-text" class="badge badge-danger-soft"></span> *@
 157                                    @*     </div> *@
 158                                    @* </span> *@
 159                                    <span id="badge-total" class="d-none d-flex align-items-center gap-1">
 160                                            <i class="fas fa-comments fa-fw me-2 text-secondary"></i>
 161                                            <div>
 162                                                <span>Всего чатов:</span>
 163                                                <span id="total-text" class="badge badge-buyer"></span>
 164                                            </div>
 165                                        </span>
 166                                </div>
 167                            </div>
 168                        </div>
 169                    </div>
 170
 171                    <div class="col-12 col-lg-9">
 172
 173                        <!-- Статистика -->
 174                        <div id="stats-row"
 175                             class="d-flex card-header-filter align-items-center justify-content-between mb-3 px-3 py-2 
 176                            <h4 class="mb-0">
 177                                <i class="fas fa-comments fa-fw text-primary me-3"></i>
 178                                <span class="text-dark">Чаты Ozon</span>
 179                            </h4>
 180
 181                            <div class="d-flex gap-4 small text-dark">
 182                                <div class="d-flex align-items-center gap-2">
 183                                    <i class="fas fa-comments text-secondary"></i>
 184                                    <div>
 185                                        <span>Всего чатов:</span>
 186                                        <span id="stat-total" class="badge badge-closed text-dark">0</span>
 187                                    </div>
 188                                </div>
 189
 190                                @* <div class="d-flex align-items-center gap-2"> *@
 191                                @*     <i class="fas fa-envelope text-secondary"></i> *@
 192                                @*     <div> *@
 193                                @*         <span>Без ответа:</span> *@
 194                                @*         <span id="stat-unread" class="badge badge-closed text-dark">0</span> *@
 195                                @*     </div> *@
 196                                @* </div> *@
 197
 198                                <div class="d-flex align-items-center gap-2 text-dark">
 199                                    <i class="fas fa-list text-secondary"></i>
 200                                    <div>
 201                                        <span>На странице:</span>
 202                                        <span id="stat-page" class="badge badge-closed text-dark">0</span>
 203                                    </div>
 204                                </div>
 205                            </div>
 206                        </div>
 207
 208                        <!-- Поиск по артикулу / товару / номеру заказа -->
 209                        <div class="mb-3 position-relative">
 210                            <div class="input-group shadow-sm">
 211                            <span class="input-group-text bg-white border-end-0">
 212                                <i class="fas fa-search text-muted"></i>
 213                            </span>
 214                                <input type="text" id="chat-search-input" class="form-control border-start-0 ps-0"
 215                                       placeholder="Поиск по артикулу (3000990266=SHS), товару или номеру заказа (351817
 216                                       autocomplete="off">
 217                                <button id="btn-clear-search" class="btn btn-outline-secondary d-none" type="button"
 218                                        title="Очистить поиск">
 219                                    <i class="fas fa-times"></i>
 220                                </button>
 221                            </div>
 222                            <!-- Выпадающий список подсказок -->
 223                            <div id="search-suggestions-box" class="d-none border rounded shadow-sm bg-white"
 224                                 style="position:absolute;top:100%;left:0;right:0;z-index:1050;max-height:320px;overflow
 225                            </div>
 226                            <div id="search-mode-badge" class="d-none mt-1">
 227                                <span class="badge badge-buyer me-1"><i
 228                                        class="fas fa-search me-1"></i>Режим поиска</span>
 229                                <span id="search-mode-query" class="text-muted small"></span>
 230                                <span id="search-result-count" class="badge badge-closed ms-1"></span>
 231                            </div>
 232                        </div>
 233
 234                        <!-- Таблица чатов -->
 235                        <div id="chats-section" class="card d-none shadow-sm border-0">
 236                            <div class="card-body p-0">
 237                                <!-- Спиннер -->
 238                                <div id="loading-spinner" class="text-center py-5 d-none">
 239                                    <div class="spinner-border text-primary" role="status">
 240                                        <span class="visually-hidden">Загрузка...</span>
 241                                    </div>
 242                                    <div class="text-muted mt-2">Загрузка чатов...</div>
 243                                </div>
 244
 245                                <!-- Ошибка -->
 246                                <div id="error-box" class="alert alert-danger m-3 d-none"></div>
 247
 248                                <!-- Пустой список -->
 249                                <div id="empty-box" class="text-center py-5 d-none">
 250                                    <i class="fas fa-comments fa-4x text-muted mb-3"></i>
 251                                    <h5 class="text-muted">Нет чатов для выбранных фильтров</h5>
 252                                </div>
 253
 254                                <!-- Таблица -->
 255                                <div id="table-wrap" class="table-responsive d-none">
 256                                    <table class="table table-hover align-middle mb-0">
 257                                        <thead class="table-light">
 258                                        <tr>
 259                                            <th>Клиент</th>
 260                                            <th style="min-width: 300px;">Последнее сообщение</th>
 261                                            <th>Статус</th>
 262                                            <th>Тип</th>
 263                                            <th>Дата создания</th>
 264                                            <th>Посл. сообщение</th>
 265                                            <th class="text-center">Без ответа</th>
 266                                            <th style="width:200px">Действия</th>
 267                                        </tr>
 268                                        </thead>
 269                                        <tbody id="chats-tbody"></tbody>
 270                                    </table>
 271                                </div>
 272
 273                                <!-- Пагинация -->
 274                                <div id="pagination-wrap" class="d-none border-top p-3">
 275                                    <div class="d-flex justify-content-between align-items-center">
 276                                        <div class="text-muted small" id="page-info"></div>
 277                                        <ul class="pagination mb-0" id="pagination-list"></ul>
 278                                    </div>
 279                                </div>
 280                            </div>
 281                        </div>
 282                    </div>
 283                </div>
 284            </div>
 285            <!-- Конец вкладки Чаты -->
 286
 287            <!-- Вкладка Статистика ответов -->
 288            <div class="tab-pane fade" id="response-stats-content" role="tabpanel"
 289                 aria-labelledby="response-stats-tab">
 290                <div class="row g-3">
 291
 292                    <!-- Фильтры статистики -->
 293
 294                    <div class="col-12 col-lg-3">
 295                        <div class="card shadow-sm border-0">
 296
 297                            <!-- Заголовок -->
 298                            <div class="card-header card-header-filter border-bottom py-2">
 299                                <h4 class="mb-0">
 300                                    <i class="fas fa-chart-bar opacity-85 text-primary me-3"></i>Фильтры
 301                                </h4>
 302                            </div>
 303
 304                            <div class="card-body bg-filter-card">
 305
 306                                <!-- Клиент -->
 307                                <div class="mb-3">
 308                                    <label class="form-label small text-muted">Клиент</label>
 309                                    <select id="stat-sel-client" class="form-select form-select-sm custom-select-init">
 310                                        <option value="0" selected>Все клиенты</option>
 0311                                        @foreach (var c in Model.OzonClients)
 312                                        {
 0313                                            <option value="@c.Id">@c.Name</option>
 314                                        }
 315                                    </select>
 316                                </div>
 317
 318                                <!-- Дата с -->
 319                                <div class="mb-3">
 320                                    <label class="form-label small text-muted">Дата с</label>
 321                                    <input type="date"
 322                                           id="stat-date-from"
 323                                           class="form-control form-control-sm"
 324                                           value="@DateTime.Now.AddDays(-30).ToString("yyyy-MM-dd")">
 325                                </div>
 326
 327                                <!-- Дата по -->
 328                                <div class="mb-3">
 329                                    <label class="form-label small text-muted">Дата по</label>
 330                                    <input type="date"
 331                                           id="stat-date-to"
 332                                           class="form-control form-control-sm"
 333                                           value="@DateTime.Now.ToString("yyyy-MM-dd")">
 334                                </div>
 335
 336                                <!-- Менеджер -->
 337                                <div class="mb-4">
 338                                    <label class="form-label small text-muted">Менеджер</label>
 339                                    <select id="stat-sel-manager" class="form-select form-select-sm custom-select-init">
 340                                        <option value="" selected>Все менеджеры</option>
 341                                    </select>
 342                                </div>
 343
 344                                <hr class="border-top border-secondary opacity-25 my-3">
 345
 346                                <!-- Кнопка -->
 347                                <div class="d-grid">
 348                                    <button id="btn-load-stats" class="btn btn-primary py-2 rounded-3">
 349                                        <i class="fas fa-chart-bar me-2"></i>Загрузить
 350                                    </button>
 351                                </div>
 352                            </div>
 353                        </div>
 354                    </div>
 355
 356
 357                    <div class="col-12 col-lg-9">
 358                        <!-- Сводка -->
 359                        <div id="stats-summary" class="row mb-3 g-3 d-none">
 360                            <div class="col-12 col-sm-6 col-xl-3">
 361                                <div class="card stats-card stats-card-blue">
 362                                    <div class="card-body d-flex align-items-center gap-3">
 363                                        <div class="icon">
 364                                            <i class="fas fa-reply"></i>
 365                                        </div>
 366                                        <div>
 367                                            <div class="text-muted small">Всего ответов</div>
 368                                            <div class="fw-semibold fs-5" id="summary-total">0</div>
 369                                        </div>
 370                                    </div>
 371                                </div>
 372                            </div>
 373                            <div class="col-12 col-sm-6 col-xl-3">
 374                                <div class="card stats-card stats-card-green">
 375                                    <div class="card-body d-flex align-items-center gap-3">
 376                                        <div class="icon">
 377                                            <i class="fas fa-clock"></i>
 378                                        </div>
 379                                        <div>
 380                                            <div class="text-muted small stat-line-height">Среднее время ответа</div>
 381                                            <div class="fw-semibold fs-5 stat-line-height" id="summary-avg-time">0</div>
 382                                        </div>
 383                                    </div>
 384                                </div>
 385                            </div>
 386                            <div class="col-12 col-sm-6 col-xl-3">
 387                                <div class="card stats-card stats-card-slate">
 388                                    <div class="card-body d-flex align-items-center gap-3">
 389                                        <div class="icon">
 390                                            <i class="fas fa-calendar"></i>
 391                                        </div>
 392                                        <div>
 393                                            <div class="text-muted small mb-1">Период</div>
 394                                            <div class="fw-semibold fs-5 stat-line-height" id="summary-period">0</div>
 395                                        </div>
 396                                    </div>
 397                                </div>
 398                            </div>
 399                            <div class="col-12 col-sm-6 col-xl-3">
 400                                <div class="card stats-card stats-card-amber">
 401                                    <div class="card-body d-flex align-items-center gap-3">
 402                                        <div class="icon">
 403                                            <i class="fas fa-users"></i>
 404                                        </div>
 405                                        <div>
 406                                            <div class="text-muted small">Менеджеров</div>
 407                                            <div class="fw-semibold fs-5" id="summary-managers">0</div>
 408                                        </div>
 409                                    </div>
 410                                </div>
 411                            </div>
 412                        </div>
 413
 414                        <!-- Статистика по менеджерам -->
 415                        <div id="managers-stats" class="card card-soft mb-3 d-none">
 416                            <div class="card-header card-header-filter text-muted">
 417                                <h6 class="mb-0"><i class="fas fa-user-tie me-2"></i>Статистика по менеджерам</h6>
 418                            </div>
 419                            <div class="card-body p-0">
 420                                <div class="table-responsive">
 421                                    <table class="table table-hover mb-0">
 422                                        <thead class="table-light">
 423                                        <tr>
 424                                            <th>Менеджер</th>
 425                                            <th class="text-center">Ответов</th>
 426                                            <th class="text-center">Среднее время</th>
 427                                            <th style="width: 40%">Прогресс</th>
 428                                        </tr>
 429                                        </thead>
 430                                        <tbody id="managers-tbody"></tbody>
 431                                    </table>
 432                                </div>
 433                            </div>
 434                        </div>
 435
 436                        <!-- Таблица ответов -->
 437                        <div id="responses-table-card" class="card card-soft d-none">
 438                            <div class="card-header card-header-filter d-flex justify-content-between align-items-center
 439">
 440                                <h6 class="mb-0"><i class="fas fa-list me-2"></i>История ответов</h6>
 441                                <small class="text-muted" id="responses-count"></small>
 442                            </div>
 443                            <div class="card-body p-0">
 444                                <!-- Спиннер -->
 445                                <div id="stats-loading" class="text-center py-5 d-none">
 446                                    <div class="spinner-border text-primary"></div>
 447                                    <div class="text-muted mt-2">Загрузка статистики...</div>
 448                                </div>
 449                                <!-- Пустой список -->
 450                                <div id="stats-empty" class="d-none"><div class="st2-empty-state"><img class="st2-empty-
 451                                <!-- Таблица -->
 452                                <div id="responses-table-wrap" class="table-responsive d-none">
 453                                    <table class="table table-hover table-sm mb-0">
 454                                        <thead class="table-light">
 455                                        <tr>
 456                                            <th>Дата ответа</th>
 457                                            <th>Менеджер</th>
 458                                            <th style="min-width: 250px;">Текст ответа</th>
 459                                            <th>Время ответа</th>
 460                                            <th>Чат</th>
 461                                        </tr>
 462                                        </thead>
 463                                        <tbody id="responses-tbody"></tbody>
 464                                    </table>
 465                                </div>
 466                                <!-- Пагинация -->
 467                                <div id="stats-pagination-wrap" class="d-none border-top p-3">
 468                                    <div class="d-flex justify-content-between align-items-center">
 469                                        <div class="text-muted small" id="stats-page-info"></div>
 470                                        <ul class="pagination pagination-sm mb-0" id="stats-pagination-list"></ul>
 471                                    </div>
 472                                </div>
 473
 474                                <!-- === Время трекинга === -->
 475                                <hr/>
 476                                <h5 class="mt-3">Трек времени</h5>
 477                                <div id="time-summary" class="row mb-3 d-none">
 478                                    <div class="col-md-3">
 479                                        <div class="card border-0 bg-primary text-white">
 480                                            <div class="card-body py-3 px-4 d-flex align-items-center">
 481                                                <i class="fas fa-list fs-2 me-3"></i>
 482                                                <div>
 483                                                    <div class="small opacity-75">Записей</div>
 484                                                    <div class="fw-bold fs-4" id="time-summary-total">0</div>
 485                                                </div>
 486                                            </div>
 487                                        </div>
 488                                    </div>
 489                                    <div class="col-md-3">
 490                                        <div class="card border-0 bg-success text-white">
 491                                            <div class="card-body py-3 px-4 d-flex align-items-center">
 492                                                <i class="fas fa-clock fs-2 me-3"></i>
 493                                                <div>
 494                                                    <div class="small opacity-75">Общее время</div>
 495                                                    <div class="fw-bold fs-4" id="time-summary-totaltime">0</div>
 496                                                </div>
 497                                            </div>
 498                                        </div>
 499                                    </div>
 500                                </div>
 501                                <div id="time-loading" class="text-center py-2 d-none">
 502                                    <div class="spinner-border text-primary" role="status"><span
 503                                            class="visually-hidden">Загрузка...</span>
 504                                    </div>
 505                                </div>
 506                                <div id="time-empty" class="d-none"><div class="st2-empty-state"><img class="st2-empty-s
 507                                <div id="time-table-wrap" class="table-responsive d-none">
 508                                    <table class="table table-hover table-sm mb-0">
 509                                        <thead class="table-light">
 510                                        <tr>
 511                                            <th>Дата</th>
 512                                            <th>Пользователь</th>
 513                                            <th>Время</th>
 514                                            <th>Чат</th>
 515                                            <th>Клиент</th>
 516                                        </tr>
 517                                        </thead>
 518                                        <tbody id="time-tbody"></tbody>
 519                                    </table>
 520                                </div>
 521                                <div id="time-pagination-wrap" class="d-none border-top p-3">
 522                                    <div class="d-flex justify-content-between align-items-center">
 523                                        <div class="text-muted small" id="time-page-info"></div>
 524                                        <ul class="pagination pagination-sm mb-0" id="time-pagination-list"></ul>
 525                                    </div>
 526                                </div>
 527                            </div>
 528                        </div>
 529                    </div>
 530                </div>
 531            </div>
 532            <!-- Конец вкладки Статистика -->
 533        </div>
 534        <!-- Конец tab-content -->
 535    </div>
 536
 537</div>
 538
 539<!-- Модальное окно истории чата -->
 540<div class="modal fade" id="chatHistoryModal" tabindex="-1">
 541    <div class="modal-dialog modal-xl modal-dialog-scrollable">
 542        <div class="modal-content" style="height:85vh">
 543            <div class="modal-header py-2">
 544                <div>
 545                    <h4 class="modal-title mb-2">
 546                        <i class="fas fa-comments text-primary me-3"></i>
 547                        Чат <code class="chat-code text-sm" id="hist-chat-id"></code>
 548                    </h4>
 549                    <span class="text-muted" id="hist-chat-meta"></span>
 550                </div>
 551                <div class="d-flex align-items-center gap-2">
 552                    <button id="btn-mark-read" class="btn btn-sm btn-success d-none" onclick="markAsRead()">
 553                        <i class="fas fa-check-double me-1"></i>Прочитано
 554                    </button>
 555                    <button type="button" class="btn-close me-2" data-bs-dismiss="modal"></button>
 556                </div>
 557            </div>
 558            <div class="modal-body p-0 d-flex flex-column" style="overflow:hidden; min-height:0;">
 559                <!-- Спиннер загрузки -->
 560                <div id="hist-spinner" class="text-center py-5">
 561                    <div class="spinner-border text-primary"></div>
 562                    <div class="text-muted mt-2">Загрузка сообщений...</div>
 563                </div>
 564                <!-- Ошибка -->
 565                <div id="hist-error" class="alert alert-danger m-3 d-none"></div>
 566                <!-- Кнопка «Загрузить ранее» -->
 567                <div id="hist-load-older" class="text-center py-2 border-bottom d-none">
 568                    <button class="btn btn-sm btn-outline-secondary" onclick="loadOlderMessages()">
 569                        <i class="fas fa-arrow-up me-1"></i>Загрузить ранние сообщения
 570                    </button>
 571                </div>
 572                <!-- Область сообщений -->
 573                <div id="hist-messages" class="flex-grow-1 p-3" style="overflow-y:auto; min-height:0;"></div>
 574
 575                <!-- Шаблонные сообщения -->
 576                <div id="templates-section" class="px-3 py-2 border-top bg-light">
 577                    <div class="d-flex flex-wrap align-items-center gap-1">
 578                        <button class="btn btn-sm template-btn" onclick="insertTemplate('Здравствуйте!')">
 579                            Здравствуйте!
 580                        </button>
 581                        <button class="btn btn-sm template-btn"
 582                                onclick="insertTemplate('Здравствуйте, для проверки необходим VIN номер Вашего авто')">
 583                            Здравствуйте, для проверки необходим VIN номер Вашего авто
 584                        </button>
 585                        <button class="btn btn-sm template-btn"
 586                                onclick="insertTemplate('Заявка на рассмотрении, ожидайте')">
 587                            Заявка на рассмотрении, ожидайте
 588                        </button>
 589                        <button class="btn btn-sm template-btn" onclick="insertTemplate('Оформите возврат')">
 590                            Оформите возврат
 591                        </button>
 592                    </div>
 593                </div>
 594            </div>
 595            <div class="modal-footer border-top">
 596                <div class="w-100">
 597                    <!-- Прикрепленный файл -->
 598                    <div id="attached-file-info" class="alert alert-info py-2 px-3 mb-2" style="display:none;">
 599                        <div class="d-flex justify-content-between align-items-center">
 600                            <div>
 601                                <i class="fas fa-paperclip me-2"></i>
 602                                <span id="attached-file-name"></span>
 603                            </div>
 604                            <button type="button" class="btn btn-sm btn-link text-danger p-0"
 605                                    onclick="clearAttachedFile()">
 606                                <i class="fas fa-times"></i>
 607                            </button>
 608                        </div>
 609                    </div>
 610
 611                    <!-- Трекинг времени ответа (часы:минуты) -->
 612                    <div id="resp-time-section">
 613                        <label class="form-label small mb-1">Затраченное время</label>
 614                        <div class="input-group input-group-sm" style="max-width:240px;">
 615                            <input type="number" id="resp-time-hours" class="form-control" min="0" placeholder="часы"
 616                                   style="max-width:80px"/>
 617                            <span class="input-group-text">:</span>
 618                            <input type="number" id="resp-time-minutes" class="form-control" min="0" max="59"
 619                                   placeholder="мин" style="max-width:80px"/>
 620                            <button class="btn btn-sm template-btn ms-2" type="button" onclick="saveResponseTime()">
 621                                Сохранить
 622                            </button>
 623                        </div>
 624                        <div id="resp-time-list" class="small text-muted">&nbsp;</div>
 625                    </div>
 626
 627                    <!-- Поле ввода с кнопками -->
 628                    <div class="input-group align-items-end gap-2">
 629                        <!-- Кнопка прикрепления файла -->
 630                        <button class="btn-link text-secondary no-outline" type="button"
 631                                onclick="document.getElementById('file-input').click()">
 632                            <i class="fas fa-paperclip fa-lg"></i>
 633                        </button>
 634                        <!-- Кнопка выбора эмодзи -->
 635                        <div class="btn-group dropup" role="group">
 636                            <button class="btn-link text-secondary no-outline  dropdown-toggle" type="button"
 637                                    id="emoji-picker-btn"
 638                                    data-bs-display="static"
 639                                    data-bs-toggle="dropdown" aria-expanded="false" title="Вставить смайлик">
 640                                <i class="far fa-smile fa-lg"></i>
 641                            </button>
 642                            <div class="dropdown-menu emoji-picker-dropdown p-2" data-bs-popper="static"
 643                                 aria-labelledby="emoji-picker-btn">
 644                                <div class="emoji-picker-grid">
 645                                    <button type="button" class="emoji-item" onclick="insertEmoji('😀')">😀</button>
 646                                    <button type="button" class="emoji-item" onclick="insertEmoji('😃')">😃</button>
 647                                    <button type="button" class="emoji-item" onclick="insertEmoji('😄')">😄</button>
 648                                    <button type="button" class="emoji-item" onclick="insertEmoji('😁')">😁</button>
 649                                    <button type="button" class="emoji-item" onclick="insertEmoji('😊')">😊</button>
 650                                    <button type="button" class="emoji-item" onclick="insertEmoji('😍')">😍</button>
 651                                    <button type="button" class="emoji-item" onclick="insertEmoji('🥰')">🥰</button>
 652                                    <button type="button" class="emoji-item" onclick="insertEmoji('😘')">😘</button>
 653                                    <button type="button" class="emoji-item" onclick="insertEmoji('😗')">😗</button>
 654                                    <button type="button" class="emoji-item" onclick="insertEmoji('😙')">😙</button>
 655                                    <button type="button" class="emoji-item" onclick="insertEmoji('😚')">😚</button>
 656                                    <button type="button" class="emoji-item" onclick="insertEmoji('🤗')">🤗</button>
 657                                    <button type="button" class="emoji-item" onclick="insertEmoji('🤩')">🤩</button>
 658                                    <button type="button" class="emoji-item" onclick="insertEmoji('🤔')">🤔</button>
 659                                    <button type="button" class="emoji-item" onclick="insertEmoji('🤨')">🤨</button>
 660                                    <button type="button" class="emoji-item" onclick="insertEmoji('😐')">😐</button>
 661                                    <button type="button" class="emoji-item" onclick="insertEmoji('😑')">😑</button>
 662                                    <button type="button" class="emoji-item" onclick="insertEmoji('😶')">😶</button>
 663                                    <button type="button" class="emoji-item" onclick="insertEmoji('😏')">😏</button>
 664                                    <button type="button" class="emoji-item" onclick="insertEmoji('😒')">😒</button>
 665                                    <button type="button" class="emoji-item" onclick="insertEmoji('🙄')">🙄</button>
 666                                    <button type="button" class="emoji-item" onclick="insertEmoji('😬')">😬</button>
 667                                    <button type="button" class="emoji-item" onclick="insertEmoji('😔')">😔</button>
 668                                    <button type="button" class="emoji-item" onclick="insertEmoji('😪')">😪</button>
 669                                    <button type="button" class="emoji-item" onclick="insertEmoji('😴')">😴</button>
 670                                    <button type="button" class="emoji-item" onclick="insertEmoji('😷')">😷</button>
 671                                    <button type="button" class="emoji-item" onclick="insertEmoji('🤒')">🤒</button>
 672                                    <button type="button" class="emoji-item" onclick="insertEmoji('🤕')">🤕</button>
 673                                    <button type="button" class="emoji-item" onclick="insertEmoji('🤢')">🤢</button>
 674                                    <button type="button" class="emoji-item" onclick="insertEmoji('😢')">😢</button>
 675                                    <button type="button" class="emoji-item" onclick="insertEmoji('😭')">😭</button>
 676                                    <button type="button" class="emoji-item" onclick="insertEmoji('😤')">😤</button>
 677                                    <button type="button" class="emoji-item" onclick="insertEmoji('😠')">😠</button>
 678                                    <button type="button" class="emoji-item" onclick="insertEmoji('😡')">😡</button>
 679                                    <button type="button" class="emoji-item" onclick="insertEmoji('🤬')">🤬</button>
 680                                    <button type="button" class="emoji-item" onclick="insertEmoji('😱')">😱</button>
 681                                    <button type="button" class="emoji-item" onclick="insertEmoji('😨')">😨</button>
 682                                    <button type="button" class="emoji-item" onclick="insertEmoji('😰')">😰</button>
 683                                    <button type="button" class="emoji-item" onclick="insertEmoji('😥')">😥</button>
 684                                    <button type="button" class="emoji-item" onclick="insertEmoji('😓')">😓</button>
 685                                    <button type="button" class="emoji-item" onclick="insertEmoji('🤯')">🤯</button>
 686                                    <button type="button" class="emoji-item" onclick="insertEmoji('😳')">😳</button>
 687                                    <button type="button" class="emoji-item" onclick="insertEmoji('🥺')">🥺</button>
 688                                    <button type="button" class="emoji-item" onclick="insertEmoji('👍')">👍</button>
 689                                    <button type="button" class="emoji-item" onclick="insertEmoji('👎')">👎</button>
 690                                    <button type="button" class="emoji-item" onclick="insertEmoji('👌')">👌</button>
 691                                    <button type="button" class="emoji-item" onclick="insertEmoji('✌️')">✌️</button>
 692                                    <button type="button" class="emoji-item" onclick="insertEmoji('🤞')">🤞</button>
 693                                    <button type="button" class="emoji-item" onclick="insertEmoji('🤝')">🤝</button>
 694                                    <button type="button" class="emoji-item" onclick="insertEmoji('🙏')">🙏</button>
 695                                    <button type="button" class="emoji-item" onclick="insertEmoji('❤️')">❤️</button>
 696                                    <button type="button" class="emoji-item" onclick="insertEmoji('💕')">💕</button>
 697                                    <button type="button" class="emoji-item" onclick="insertEmoji('💯')">💯</button>
 698                                    <button type="button" class="emoji-item" onclick="insertEmoji('✔️')">✔️</button>
 699                                    <button type="button" class="emoji-item" onclick="insertEmoji('❌')">❌</button>
 700                                    <button type="button" class="emoji-item" onclick="insertEmoji('⚠️')">⚠️</button>
 701                                    <button type="button" class="emoji-item" onclick="insertEmoji('🔥')">🔥</button>
 702                                    <button type="button" class="emoji-item" onclick="insertEmoji('⭐')">⭐</button>
 703                                    <button type="button" class="emoji-item" onclick="insertEmoji('🎉')">🎉</button>
 704                                    <button type="button" class="emoji-item" onclick="insertEmoji('🎊')">🎊</button>
 705                                </div>
 706                            </div>
 707                        </div>
 708                        <!-- Текстовое поле -->
 709                        <textarea id="message-text" class="form-control"
 710                                  placeholder="Введите сообщение или прикрепите файл..."
 711                                  rows="1" maxlength="1000" style="resize:none"></textarea>
 712                        <!-- Кнопка отправки -->
 713                        <button id="btn-send" class="btn btn-primary px-3" onclick="sendMessageOrFile()">
 714                            <i class="fas fa-paper-plane me-2"></i>Отправить
 715                        </button>
 716                    </div>
 717                    <input type="file" id="file-input" class="d-none" accept="image/*,.pdf,.doc,.docx,.txt"
 718                           onchange="handleFileSelect()">
 719
 720                    <div class="d-flex justify-content-between mt-1">
 721                        <small class="text-muted">
 722                            <span id="char-count">0</span>/1000 символов
 723                        </small>
 724                        <small class="text-muted">
 725                            Файлы: JPG, PNG, PDF, DOC, DOCX, TXT (макс 10 МБ)
 726                        </small>
 727                    </div>
 728                </div>
 729            </div>
 730        </div>
 731    </div>
 732</div>
 733
 734<!-- Форма экспорта (скрытая) -->
 735<form id="export-form" method="post" action="@Url.Action("ExportToExcel")" class="d-none">
 736    <input type="hidden" name="clientId" id="exp-clientId"/>
 737    <input type="hidden" name="status" id="exp-status"/>
 738    <input type="hidden" name="unreadOnly" id="exp-unread"/>
 739</form>
 740
 741<style>
 742
 743    .loading-dots {
 744        display: inline-flex;
 745        font-size: 1.5rem;
 746        color: #6c757d;
 747    }
 748
 749    .loading-dots .dot {
 750        animation: dotPulse 1.4s infinite ease-in-out;
 751        animation-fill-mode: both;
 752    }
 753
 754    .loading-dots .dot:nth-child(1) {
 755        animation-delay: -0.32s;
 756    }
 757
 758    .loading-dots .dot:nth-child(2) {
 759        animation-delay: -0.16s;
 760    }
 761
 762    .loading-dots .dot:nth-child(3) {
 763        animation-delay: 0s;
 764    }
 765
 766    @@keyframes dotPulse {
 767        0%, 80%, 100% {
 768            opacity: 0.2;
 769            transform: scale(0.8);
 770        }
 771        40% {
 772            opacity: 1;
 773            transform: scale(1.2);
 774        }
 775    }
 776
 777    .last-msg-cell {
 778        max-width: 350px;
 779    }
 780
 781    .last-msg-cell .msg-content {
 782        display: -webkit-box;
 783        -webkit-line-clamp: 2;
 784        -webkit-box-orient: vertical;
 785        overflow: hidden;
 786        text-overflow: ellipsis;
 787        word-break: break-word;
 788    }
 789
 790    .last-msg-cell .msg-sender {
 791        font-weight: 500;
 792        margin-right: 0.25rem;
 793    }
 794
 795    .last-msg-cell .msg-sender.customer {
 796        background-color: #E0F2FE;
 797        color: #075985;
 798    }
 799
 800    .last-msg-cell .msg-sender.seller {
 801        background-color: #DCFCE7;
 802        color: #166534;
 803    }
 804
 805    .last-msg-cell .msg-sender.system {
 806        background-color: #e5e7eb;
 807        color: #4b4b4b;
 808    }
 809
 810    .last-msg-cell .msg-sender.support {
 811        background-color: #FEF3C7;
 812        color: #92400E;
 813    }
 814
 815    .last-msg-cell .msg-time {
 816        font-size: 0.75rem;
 817        color: #6c757d;
 818        margin-left: 0.5rem;
 819    }
 820
 821    /* Поле ввода сообщения */
 822    #message-text {
 823        border-radius: 8px;
 824        border: 1px solid #ced4da;
 825        padding: 10px 12px;
 826        font-size: 0.95rem;
 827        line-height: 1.4;
 828        background-color: #f8f9fa;
 829
 830        min-height: calc(1.4em * 1 + 20px);
 831        max-height: calc(1.4em * 5 + 20px); /* максимум 5 строк + padding */
 832        overflow-y: hidden;
 833        resize: none;
 834
 835        transition: border-color 0.2s, box-shadow 0.2s;
 836    }
 837
 838    #message-text::-webkit-scrollbar-track {
 839        background: transparent;
 840        border-radius: 12px;
 841    }
 842
 843    #message-text::-webkit-scrollbar-thumb {
 844        background-color: #b0b0b0;
 845        border-radius: 12px;
 846    }
 847
 848    #btn-send {
 849        border-radius: 8px;
 850        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
 851        margin-bottom: 2px;
 852    }
 853
 854    /* Кнопки чата Приклепление файла и Выбор эмоджи */
 855    .no-outline {
 856        margin-bottom: 9px;
 857        border: none;
 858        background: none;
 859        outline: none;
 860        box-shadow: none;
 861        cursor: pointer;
 862    }
 863
 864    .no-outline:focus,
 865    .no-outline:active {
 866        outline: none;
 867        box-shadow: none;
 868    }
 869
 870    /* Hover-эффект на иконках */
 871    .btn-link i {
 872        transition: transform 0.2s, color 0.2s;
 873    }
 874
 875    .btn-link:hover i {
 876        transform: scale(1.2);
 877        color: #343a40;
 878    }
 879
 880    /* Emoji Picker Styles */
 881    .emoji-picker-dropdown {
 882        max-width: 320px;
 883        max-height: 280px;
 884        overflow-y: auto;
 885
 886        top: 100%;
 887        left: 0;
 888        width: 320px;
 889        padding: 8px;
 890        border-radius: 8px;
 891        background-color: #fff;
 892        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
 893    }
 894
 895    .emoji-picker-grid {
 896        display: grid;
 897        grid-template-columns: repeat(auto-fill, minmax(36px, 1fr));
 898        gap: 4px;
 899        justify-items: center;
 900    }
 901
 902    .emoji-item {
 903        background: none;
 904        border: none;
 905        outline: none;
 906        font-size: 1.6rem;
 907        padding: 3px;
 908        cursor: pointer;
 909        border-radius: 6px;
 910        transition: transform 0.2s, background-color 0.2s;
 911    }
 912
 913    .emoji-item:hover {
 914        background-color: #f1f3f5;
 915        transform: scale(1.25);
 916    }
 917
 918    .emoji-item:active {
 919        transform: scale(1.1);
 920    }
 921
 922    .dropup .dropdown-menu[data-bs-popper] {
 923        margin-bottom: 15px;
 924    }
 925
 926
 927    .badge-small {
 928        font-size: 0.85em;
 929    }
 930
 931    .badge-open,
 932    .badge-closed,
 933    .badge-other,
 934    .badge-seller,
 935    .badge-buyer,
 936    .badge-danger-soft {
 937        padding: 6px 12px;
 938        font-weight: 500;
 939        font-size: 0.85em;
 940    }
 941
 942    .badge-open {
 943        background: #DCFCE7;
 944        color: #166534;
 945        border: 1px solid #BBF7D0;
 946    }
 947
 948    .badge-closed {
 949        background: #e5e7eb;
 950        color: #4b4b4b;
 951        border: 1px solid #E2E8F0;
 952    }
 953
 954    .badge-other {
 955        background: #FEF3C7;
 956        color: #92400E;
 957        border: 1px solid #FDE68A;
 958    }
 959
 960    .badge-seller {
 961        background: #CFFAFE;
 962        color: #155E75;
 963        border: 1px solid #A5F3FC;
 964    }
 965
 966    .badge-buyer {
 967        background: #E0F2FE;
 968        color: #075985;
 969        border: 1px solid #BAE6FD;
 970    }
 971
 972    .badge-danger-soft {
 973        background: #EF4444;
 974        color: #FFFFFF;
 975    }
 976
 977    .bg-filter-card {
 978        background-color: #FFFFFF;
 979        box-shadow: 0 4px 20px rgba(15, 23, 42, 0.04);
 980    }
 981
 982    .card-header-filter {
 983        background: #F1F5F9;
 984        color: #334155;
 985    }
 986
 987    .table {
 988        background: #FFFFFF;
 989    }
 990
 991    .table thead {
 992        background: #F1F5F9;
 993    }
 994
 995    .table-row-highlight {
 996        background-color: #F5F7FF;
 997    }
 998
 999    .table tbody tr:hover {
 1000        background-color: #E0E7FF;
 1001        transition: background-color 0.15s ease;
 1002    }
 1003
 1004    .suggestion-item:hover {
 1005        background-color: #e8f0fe !important;
 1006    }
 1007
 1008    #search-suggestions-box mark {
 1009        background: #FDE68A;
 1010        padding: 0 1px;
 1011        border-radius: 2px;
 1012    }
 1013
 1014    .btn-table-light {
 1015        background-color: #e5e7eb;
 1016        color: #4b4b4b;
 1017        border: 2px solid #e0e0e0;
 1018        padding: 4px 10px;
 1019        font-size: 0.85em;
 1020        font-weight: 500;
 1021        border-radius: 0.5rem;
 1022        transition: all 0.2s ease-in-out;
 1023    }
 1024
 1025    .btn-table-light:hover {
 1026        background-color: #e0e0e0;
 1027        border: 2px solid #ffffff;
 1028        color: #2c2c2c;
 1029        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
 1030        border-radius: 0.5rem;
 1031    }
 1032
 1033    .btn-table-light:focus {
 1034        outline: none;
 1035        box-shadow: 0 0 0 2px rgba(180, 180, 180, 0.25);
 1036    }
 1037
 1038    .client-card {
 1039        transition: transform .2s, box-shadow .2s;
 1040        cursor: pointer;
 1041    }
 1042
 1043    .client-card:hover {
 1044        transform: translateY(-2px);
 1045        box-shadow: 0 8px 25px rgba(0, 0, 0, .15) !important;
 1046    }
 1047
 1048    .chat-message-content {
 1049        word-break: break-word;
 1050        line-height: 1.55;
 1051    }
 1052
 1053    .chat-message-content strong {
 1054        font-weight: 600;
 1055    }
 1056
 1057    .chat-message-content a {
 1058        color: #0d6efd;
 1059        text-decoration: underline;
 1060        word-break: break-all;
 1061    }
 1062
 1063    .chat-message-content a:hover {
 1064        color: #0a58ca;
 1065    }
 1066
 1067    .chat-message-content ul {
 1068        margin-top: .25rem;
 1069        margin-bottom: .25rem;
 1070    }
 1071
 1072    .chat-message-content li {
 1073        margin-bottom: .15rem;
 1074    }
 1075
 1076    .chat-code {
 1077        background: #e5e7eb;
 1078        color: #4b4b4b;
 1079        border: 1px solid #E2E8F0;
 1080        padding: 2px 8px;
 1081        border-radius: 8px;
 1082    }
 1083
 1084    #btn-mark-read.btn-success {
 1085        background-color: #DCFCE7;
 1086        border-color: #BBF7D0;
 1087        color: #166534;
 1088        font-weight: 500;
 1089    }
 1090
 1091    #btn-mark-read.btn-success:hover {
 1092        background-color: #9EE5B7; /* чуть темнее, но гармонично с #DCFCE7 */
 1093        border-color: #6FD9A0;
 1094        color: #166534;
 1095    }
 1096
 1097    .chat-bubble {
 1098        max-width: 70%;
 1099        min-width: 200px;
 1100        padding: 8px 16px 2px 16px;
 1101        border-radius: 16px;
 1102        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
 1103        transition: all 0.2s ease;
 1104    }
 1105
 1106    .chat-bubble:hover {
 1107        transform: translateY(-1px);
 1108        box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
 1109    }
 1110
 1111    /* Продавец (справа) */
 1112    .chat-seller {
 1113        background-color: #F1F5F9;
 1114        border-radius: 16px 16px 4px 16px;
 1115        position: relative;
 1116        margin-left: auto;
 1117        margin-right: 16px;
 1118    }
 1119
 1120    /* Покупатель (слева) */
 1121    .chat-customer {
 1122        background-color: #F0F9FF;
 1123        border-radius: 16px 16px 16px 4px;
 1124        position: relative;
 1125        margin-right: auto;
 1126        margin-left: 16px;
 1127    }
 1128
 1129    /* Система */
 1130    .chat-system {
 1131        background-color: #fff3cd;
 1132        border: 1px solid #ffe69c;
 1133    }
 1134
 1135    /* Хвостик у сообщений */
 1136    .chat-seller::after,
 1137    .chat-customer::after {
 1138        content: "";
 1139        position: absolute;
 1140        bottom: 0;
 1141        width: 0;
 1142        height: 0;
 1143        border-top: 20px solid transparent;
 1144        border-bottom: 0 solid transparent;
 1145    }
 1146
 1147    .chat-seller::after {
 1148        right: -16px;
 1149        border-left: 20px solid #F1F5F9;
 1150        filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.05));
 1151    }
 1152
 1153    .chat-customer::after {
 1154        left: -16px;
 1155        border-right: 20px solid #F0F9FF;
 1156        filter: drop-shadow(-1px 1px 2px rgba(0, 0, 0, 0.05));
 1157    }
 1158
 1159    .chat-badge-seller,
 1160    .chat-badge-customer,
 1161    .chat-badge-system {
 1162        font-size: 0.85rem;
 1163        font-weight: 600;
 1164        border-radius: 8px;
 1165        margin: 0;
 1166    }
 1167
 1168    .chat-badge-seller,
 1169    .stats-card-green .icon {
 1170        background-color: #DCFCE7;
 1171        color: #166534;
 1172    }
 1173
 1174    .chat-badge-customer,
 1175    .stats-card-blue .icon {
 1176        background-color: #E0F2FE;
 1177        color: #075985;
 1178    }
 1179
 1180    .chat-badge-system,
 1181    .stats-card-amber .icon {
 1182        background-color: #FEF3C7;
 1183        color: #92400E;
 1184    }
 1185
 1186
 1187    .stats-card-slate .icon {
 1188        background: #CFFAFE;
 1189        color: #155E75;
 1190    }
 1191
 1192    /* Разделить для новых сообщений */
 1193    .new-message-divider {
 1194        text-align: center;
 1195        margin-bottom: 16px;
 1196        position: relative;
 1197        font-size: 14px;
 1198    }
 1199
 1200    .new-message-divider::before,
 1201    .new-message-divider::after {
 1202        content: "";
 1203        display: inline-block;
 1204        width: 40%;
 1205        height: 1px;
 1206        background-color: #d1d5db;
 1207        vertical-align: middle;
 1208        margin: 0 8px;
 1209    }
 1210
 1211    /* кнопки шаблонов */
 1212    .template-btn,
 1213    #resp-time-section .input-group .template-btn {
 1214        border-radius: 10px;
 1215        background-color: #FFFFFF;
 1216        color: #1E293B;
 1217        border: 2px solid #E2E8F0;
 1218        padding: 4px 14px;
 1219        transition: all 0.15s ease;
 1220        font-weight: 500;
 1221        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
 1222    }
 1223
 1224    .template-btn:hover,
 1225    #resp-time-section .input-group .template-btn:hover {
 1226        background-color: #FFFFFF;
 1227        border-color: #60A5FA;
 1228        color: #2563EB;
 1229        box-shadow: 0 4px 8px rgba(37, 99, 235, 0.1);
 1230        transform: translateY(-1px);
 1231    }
 1232
 1233    /* Трекинг времени */
 1234    #resp-time-section {
 1235        margin: -10px 0 0 8px;
 1236    }
 1237
 1238    #resp-time-section .form-label {
 1239        color: #334155;
 1240        font-weight: 600;
 1241        font-size: 0.75rem;
 1242        text-transform: uppercase;
 1243        letter-spacing: 0.5px;
 1244        margin-bottom: 4px;
 1245    }
 1246
 1247    #resp-time-section .input-group {
 1248        flex-wrap: nowrap;
 1249    }
 1250
 1251    #resp-time-section .input-group-text {
 1252        border: none;
 1253        background-color: white;
 1254        font-weight: 600;
 1255    }
 1256
 1257    #resp-time-section .form-control {
 1258        background: #F8FAFC;
 1259        border: 1px solid #E2E8F0;
 1260        border-radius: 7px;
 1261        height: 32px;
 1262        padding: 2px 5px;
 1263        font-size: 0.85rem;
 1264    }
 1265
 1266    #resp-time-list {
 1267        background: transparent;
 1268        padding: 4px 0;
 1269        border: none;
 1270        font-size: 0.85rem;
 1271    }
 1272
 1273    /* Статистика ответов */
 1274    .stats-card {
 1275        border: 1px solid #E5E7EB;
 1276        border-radius: 12px;
 1277        background-color: #FFFFFF;
 1278        transition: 0.2s ease;
 1279        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
 1280    }
 1281
 1282    .stats-card:hover {
 1283        box-shadow: 0 6px 18px rgba(0, 0, 0, 0.1);
 1284    }
 1285
 1286    .stats-card .icon {
 1287        width: 42px;
 1288        height: 42px;
 1289        flex: 0 0 42px;
 1290        min-width: 42px;
 1291        border-radius: 10px;
 1292        display: flex;
 1293        align-items: center;
 1294        justify-content: center;
 1295    }
 1296
 1297    #summary-period {
 1298        white-space: normal;
 1299        line-height: 1.2;
 1300    }
 1301
 1302    .progress-green {
 1303        background-color: #DCFCE7; /* светлый зелёный фон */
 1304    }
 1305
 1306    .progress-bar-green {
 1307        background-color: #166534; /* тёмный зелёный */
 1308        color: #fff;
 1309        font-weight: 500;
 1310        display: flex;
 1311        align-items: center;
 1312        justify-content: center;
 1313    }
 1314
 1315    .card-soft {
 1316        border: none;
 1317        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 1318    }
 1319
 1320    .link-chat {
 1321        color: #075985;
 1322        font-weight: 500;
 1323    }
 1324
 1325    .link-chat:hover {
 1326        color: #0c4a6e;
 1327        text-decoration: underline;
 1328    }
 1329
 1330</style>
 1331
 1332<script>
 1333    (function () {
 1334        // === Элементы ===
 1335        const selClient = document.getElementById('sel-client');
 1336        const selStatus = document.getElementById('sel-status');
 1337        const selType = document.getElementById('sel-type');
 1338        const chkUnread = document.getElementById('chk-unread');
 1339        const selPageSize = document.getElementById('sel-pagesize');
 1340        const btnLoad = document.getElementById('btn-load');
 1341        const btnExport = document.getElementById('btn-export');
 1342
 1343        // === Вставка эмодзи в текстовое поле ===
 1344        window.insertEmoji = function (emoji) {
 1345            const messageText = document.getElementById('message-text');
 1346            if (!messageText) return;
 1347
 1348            const start = messageText.selectionStart;
 1349            const end = messageText.selectionEnd;
 1350            const text = messageText.value;
 1351
 1352            const before = text.substring(0, start);
 1353            const after = text.substring(end, text.length);
 1354            messageText.value = before + emoji + after;
 1355
 1356            const newPosition = start + emoji.length;
 1357            messageText.selectionStart = newPosition;
 1358            messageText.selectionEnd = newPosition;
 1359            messageText.focus();
 1360
 1361            const charCount = document.getElementById('char-count');
 1362            if (charCount) charCount.textContent = messageText.value.length;
 1363        };
 1364
 1365        // Функция для растягивания поля воода сообщения
 1366        function autoResizeTextarea(element) {
 1367            element.style.height = 'auto';
 1368
 1369            const maxHeight = parseInt(getComputedStyle(element).maxHeight);
 1370
 1371            if (element.scrollHeight > maxHeight) {
 1372                element.style.height = maxHeight + 'px';
 1373                element.style.overflowY = 'auto';
 1374            } else {
 1375                element.style.height = element.scrollHeight + 'px';
 1376                element.style.overflowY = 'hidden';
 1377            }
 1378        }
 1379
 1380        // Добавляем обработчик события input
 1381        const textarea = document.getElementById('message-text');
 1382        textarea.addEventListener('input', function () {
 1383            autoResizeTextarea(this);
 1384        });
 1385
 1386        // === Вставка шаблонного сообщения ===
 1387        window.insertTemplate = function (templateText) {
 1388            const messageText = document.getElementById('message-text');
 1389            if (!messageText) return;
 1390
 1391            const start = messageText.selectionStart;
 1392            const end = messageText.selectionEnd;
 1393            const text = messageText.value;
 1394
 1395            const before = text.substring(0, start);
 1396            const after = text.substring(end, text.length);
 1397
 1398            // Добавляем пробел перед шаблоном если нужно
 1399            const needsSpaceBefore = before.length > 0 && !before.endsWith(' ');
 1400            const finalText = needsSpaceBefore ? ' ' + templateText + ' ' : templateText + ' ';
 1401
 1402            messageText.value = before + finalText + after;
 1403
 1404            const newPosition = start + finalText.length;
 1405            messageText.selectionStart = newPosition;
 1406            messageText.selectionEnd = newPosition;
 1407            messageText.focus();
 1408
 1409            // Вызываем функцию растягивания напрямую
 1410            autoResizeTextarea(messageText);
 1411        };
 1412
 1413        const statsRow = document.getElementById('stats-row');
 1414        const chatsSection = document.getElementById('chats-section');
 1415        const spinner = document.getElementById('loading-spinner');
 1416        const errorBox = document.getElementById('error-box');
 1417        const emptyBox = document.getElementById('empty-box');
 1418        const tableWrap = document.getElementById('table-wrap');
 1419        const tbody = document.getElementById('chats-tbody');
 1420        const pagWrap = document.getElementById('pagination-wrap');
 1421        const pagList = document.getElementById('pagination-list');
 1422        const pageInfo = document.getElementById('page-info');
 1423
 1424        @* const badgeUnread = document.getElementById('badge-unread'); *@
 1425        const badgeTotal = document.getElementById('badge-total');
 1426        @* const unreadText = document.getElementById('unread-text'); *@
 1427        const totalText = document.getElementById('total-text');
 1428
 1429        const statTotal = document.getElementById('stat-total');
 1430        @* const statUnread = document.getElementById('stat-unread'); *@
 1431        const statPage = document.getElementById('stat-page');
 1432
 1433        let currentPage = 1;
 1434
 1435        // === ПОИСК ПО АРТИКУЛУ / ТОВАРУ / НОМЕРУ ЗАКАЗА ===
 1436        const searchInput = document.getElementById('chat-search-input');
 1437        const suggestionsBox = document.getElementById('search-suggestions-box');
 1438        const btnClearSearch = document.getElementById('btn-clear-search');
 1439        const searchModeBadge = document.getElementById('search-mode-badge');
 1440        const searchModeQuery = document.getElementById('search-mode-query');
 1441        const searchResultCount = document.getElementById('search-result-count');
 1442
 1443        let searchDebounce = null;
 1444        let isSearchMode = false;
 1445        let loadedChats = [];       // чаты текущей страницы для поиска
 1446        let searchGen = 0;          // поколение поиска — для отмены предыдущего
 1447        const chatMetaCache = new Map(); // chatId → { sku, orderNumber } — заполняется во время поиска
 1448        let preScanRunning = false;  // флаг фонового предсканирования
 1449
 1450        // Фоновое сканирование всех чатов для заполнения кэша SKU/orderNumber
 1451        function startPreScan() {
 1452            if (preScanRunning || !loadedChats.length) return;
 1453            preScanRunning = true;
 1454            const CONCURRENCY = 5;
 1455            let idx = 0;
 1456
 1457            async function nextScan() {
 1458                while (idx < loadedChats.length) {
 1459                    const c = loadedChats[idx++];
 1460                    if (chatMetaCache.has(c.chatId)) continue; // уже есть
 1461                    try {
 1462                        const params = new URLSearchParams({
 1463                            clientId: c.clientId,
 1464                            chatId: c.chatId,
 1465                            limit: 20,
 1466                            direction: 'Backward'
 1467                        });
 01468                        const r = await fetch(`@Url.Action("GetHistoryJson")?${params}`);
 1469                        const hdata = await r.json();
 1470                        if (hdata.success && hdata.messages) {
 1471                            const meta = {clientName: c.clientName || ''};
 1472                            hdata.messages.forEach(m => {
 1473                                const txt = (m.data || []).filter(Boolean).join(' ');
 1474                                if (m.sku && !meta.sku) {
 1475                                    meta.sku = m.sku;
 1476                                    meta.skuSnippet = txt.substring(0, 60) || '';
 1477                                }
 1478                                if (m.orderNumber && !meta.orderNumber) {
 1479                                    meta.orderNumber = m.orderNumber;
 1480                                    meta.orderSnippet = txt.substring(0, 60) || '';
 1481                                }
 1482                            });
 1483                            chatMetaCache.set(c.chatId, meta);
 1484                        }
 1485                    } catch (e) { /* не блокируем */
 1486                    }
 1487                }
 1488            }
 1489
 1490            const workers = [];
 1491            for (let i = 0; i < CONCURRENCY; i++) workers.push(nextScan());
 1492            Promise.all(workers).finally(() => {
 1493                preScanRunning = false;
 1494            });
 1495        }
 1496
 1497        if (searchInput) {
 1498            searchInput.addEventListener('focus', () => {
 1499                startPreScan();
 1500                showAllSuggestions();
 1501            });
 1502
 1503            searchInput.addEventListener('input', () => {
 1504                const q = searchInput.value.trim();
 1505                btnClearSearch.classList.toggle('d-none', !q);
 1506                clearTimeout(searchDebounce);
 1507                if (!q) {
 1508                    if (isSearchMode) exitSearchMode(); // сброс результатов при ручной очистке
 1509                    showAllSuggestions();
 1510                    return;
 1511                }
 1512                if (q.length < 2) {
 1513                    hideSuggestions();
 1514                    return;
 1515                }
 1516                searchDebounce = setTimeout(() => fetchSuggestions(q), 300);
 1517            });
 1518
 1519            searchInput.addEventListener('keydown', e => {
 1520                if (e.key === 'Enter') {
 1521                    e.preventDefault();
 1522                    hideSuggestions();
 1523                    const q = searchInput.value.trim();
 1524                    if (!q) {
 1525                        loadChats();
 1526                        return;
 1527                    } // пустое поле — стандартная таблица
 1528                    // Если это похоже на PostingNumber — сразу ищем только по нему (без перебора страниц)
 1529                    if (/^\d{4,12}-\d{1,6}(-\d{1,6})?$/.test(q)) {
 1530                        const clientId = selClient ? selClient.value : 0;
 01531                        fetch(`@Url.Action("GetChatSearchSuggestions")?clientId=${clientId}&query=${encodeURIComponent(q
 1532                            .then(r => r.json())
 1533                            .then(data => {
 1534                                if (data.success && data.found && data.record && data.record.chatId && data.record.clien
 1535                                    // Только рендерим строку, не открываем чат
 1536                                    renderSingleOrderRow(data.record);
 1537                                    // После рендера строки — подгружаем последнее сообщение и время
 1538                                    const chatId = data.record.chatId;
 1539                                    const clientId = data.record.clientId;
 01540                                    fetch(`@Url.Action("GetLastMessageJson")?clientId=${clientId}&chatId=${encodeURIComp
 1541                                        .then(r => r.json())
 1542                                        .then(msgData => {
 1543                                            const cell = document.querySelector(`.last-msg-cell[data-chat-id="${chatId}"
 1544                                            const timeCell = document.querySelector(`.last-msg-time-cell[data-chat-id="$
 1545                                            if (msgData.success && msgData.hasMessage) {
 1546                                                // Заполняем содержимое ячейки сообщения
 1547                                                if (cell) {
 1548                                                    let msgHtml = '';
 1549                                                    if (msgData.isCustomer) {
 1550                                                        msgHtml += `<span class="msg-sender customer">Покупатель:</span>
 1551                                                    } else {
 1552                                                        msgHtml += `<span class="msg-sender seller">Продавец:</span> `;
 1553                                                    }
 1554                                                    msgHtml += esc(msgData.text || '');
 1555                                                    cell.innerHTML = `<div class="msg-content">${msgHtml}</div>`;
 1556                                                }
 1557                                                // Заполняем время с разницей (timeAgo)
 1558                                                if (timeCell) {
 1559                                                    const ago = timeAgo(msgData.createdAt);
 1560                                                    timeCell.innerHTML = `<span title="${esc(msgData.createdAtFormatted)
 1561                                                }
 1562                                            }
 1563                                        });
 1564                                } else {
 1565                                    doSearch(q);
 1566                                }
 1567                            })
 1568                            .catch(() => doSearch(q));
 1569                        return;
 1570                    }
 1571
 1572                    // Рендер одного заказа (поиск по номеру отправления)
 1573                    function renderSingleOrderRow(order) {
 1574                        // Скрыть пагинацию и показать таблицу
 1575                        document.getElementById('pagination-wrap').classList.add('d-none');
 1576                        document.getElementById('table-wrap').classList.remove('d-none');
 1577                        document.getElementById('chats-section').classList.remove('d-none');
 1578                        document.getElementById('loading-spinner').classList.add('d-none');
 1579                        document.getElementById('empty-box').classList.add('d-none');
 1580                        document.getElementById('error-box').classList.add('d-none');
 1581                        // Очищаем тело таблицы
 1582                        const tbody = document.getElementById('chats-tbody');
 1583                        tbody.innerHTML = '';
 1584                        // Получаем имя клиента по clientId, если не пришло в order.clientName
 1585                        let clientName = order.clientName || '';
 1586                        if (!clientName && order.clientId) {
 1587                            const sel = document.getElementById('sel-client');
 1588                            if (sel) {
 1589                                const opt = sel.querySelector(`option[value="${order.clientId}"]`);
 1590                                if (opt) clientName = opt.textContent.trim();
 1591                            }
 1592                        }
 1593                        // Формируем строку
 1594                        const tr = document.createElement('tr');
 1595                        tr.style.cursor = 'pointer';
 1596                        tr.innerHTML = `
 1597                                    <td><span class="badge badge-closed">${esc(clientName)}</span></td>
 1598                                    <td class="last-msg-cell" data-chat-id="${esc(order.chatId)}" data-client-id="${orde
 1599                                        <div class="msg-content">
 1600                                            <span class="msg-sender seller">Номер отправления:</span> ${esc(order.label)
 1601                                        </div></td>
 1602                                    <td><span class="badge badge-open">Открыт</span></td>
 1603                                    <td><span class="badge badge-buyer">Чат с покупателем</span></td>
 1604                                    <td><span>${new Date().toLocaleDateString('ru-RU')}</span></td>
 1605                                    <td class="last-msg-time-cell" data-chat-id="${esc(order.chatId)}"><span class="text
 1606                                    <td class="text-center"><span class="text-muted">—</span></td>
 1607                                    <td>
 1608                                        <button class="btn btn-sm btn-table-light me-1" onclick="viewChat('${esc(order.c
 1609                                        <button class="btn btn-sm btn-table-light" onclick="copyId('${esc(order.chatId)}
 1610                                    </td>`;
 1611                        tr.addEventListener('click', e => {
 1612                            if (e.target.closest('button')) return;
 1613                            viewChat(order.chatId, order.clientId);
 1614                        });
 1615                        tbody.appendChild(tr);
 1616                    }
 1617
 1618                    doSearch(q);
 1619                }
 1620                if (e.key === 'Escape') {
 1621                    hideSuggestions();
 1622                }
 1623            });
 1624
 1625            // Скрываем подсказки при клике вне
 1626            document.addEventListener('click', e => {
 1627                if (!searchInput.contains(e.target) && !suggestionsBox.contains(e.target))
 1628                    hideSuggestions();
 1629            });
 1630        }
 1631
 1632        if (btnClearSearch) {
 1633            btnClearSearch.addEventListener('click', () => {
 1634                searchInput.value = '';
 1635                btnClearSearch.classList.add('d-none');
 1636                hideSuggestions();
 1637                loadChats(); // возвращаем стандартную таблицу
 1638            });
 1639        }
 1640
 1641        // Показать все доступные подсказки из кэша (без фильтра)
 1642        function showAllSuggestions() {
 1643            const clientId = selClient ? selClient.value : 0;
 1644            // Показываем локальные + PostingNumber из БД
 01645            fetch(`@Url.Action("GetChatSearchSuggestions")?clientId=${clientId}&query=`)
 1646                .then(r => r.json())
 1647                .then(data => {
 1648                    let all = getLocalSuggestions('');
 1649                    if (data.success && data.suggestions && data.suggestions.length) {
 1650                        const localLabels = new Set(all.map(s => s.label.toLowerCase()));
 1651                        all = [...all, ...data.suggestions.filter(s => !localLabels.has(s.label.toLowerCase()))];
 1652                    }
 1653                    if (!all.length) return;
 1654                    renderSuggestions(all, '');
 1655                })
 1656                .catch(() => {
 1657                    const all = getLocalSuggestions('');
 1658                    if (all.length) renderSuggestions(all, '');
 1659                });
 1660        }
 1661
 1662        function fetchSuggestions(q) {
 1663            const clientId = selClient ? selClient.value : 0;
 1664            const localSugg = getLocalSuggestions(q);
 1665
 01666            fetch(`@Url.Action("GetChatSearchSuggestions")?clientId=${clientId}&query=${encodeURIComponent(q)}`)
 1667                .then(r => r.json())
 1668                .then(data => {
 1669                    // Если найдено точное совпадение (found/record), сразу открываем чат
 1670                    if (data.success && data.found && data.record && data.record.chatId && data.record.clientId) {
 1671                        hideSuggestions();
 1672                        // viewChat(data.record.chatId, data.record.clientId);
 1673                        return;
 1674                    }
 1675                    let all = [...localSugg];
 1676                    if (data.success && data.suggestions && data.suggestions.length) {
 1677                        const localLabels = new Set(localSugg.map(s => s.label.toLowerCase()));
 1678                        all = [...localSugg, ...data.suggestions.filter(s => !localLabels.has(s.label.toLowerCase()))];
 1679                    }
 1680                    if (!all.length) {
 1681                        hideSuggestions();
 1682                        return;
 1683                    }
 1684                    renderSuggestions(all, q);
 1685                })
 1686                .catch(() => {
 1687                    if (localSugg.length) renderSuggestions(localSugg, q);
 1688                    else hideSuggestions();
 1689                });
 1690        }
 1691
 1692        // Локальные подсказки из кэша (SKU и orderNumber из уже загруженных историй)
 1693        // q === '' → вернуть всё, иначе — фильтровать
 1694        function getLocalSuggestions(q) {
 1695            const qLow = q.toLowerCase();
 1696            const orderNums = [];
 1697            const skus = [];
 1698            const seenOrder = new Set();
 1699            const seenSku = new Set();
 1700            chatMetaCache.forEach(meta => {
 1701                if (meta.orderNumber && !seenOrder.has(meta.orderNumber) &&
 1702                    (!qLow || meta.orderNumber.toLowerCase().includes(qLow))) {
 1703                    seenOrder.add(meta.orderNumber);
 1704                    orderNums.push({
 1705                        type: 'ordernum', label: meta.orderNumber,
 1706                        icon: 'fas fa-receipt', hint: 'Номер заказа',
 1707                        preview: meta.orderSnippet || '',
 1708                        client: meta.clientName || ''
 1709                    });
 1710                }
 1711                if (meta.sku && !seenSku.has(meta.sku) &&
 1712                    (!qLow || meta.sku.toLowerCase().includes(qLow))) {
 1713                    seenSku.add(meta.sku);
 1714                    skus.push({
 1715                        type: 'sku', label: meta.sku,
 1716                        icon: 'fas fa-barcode', hint: 'SKU',
 1717                        preview: meta.skuSnippet || '',
 1718                        client: meta.clientName || ''
 1719                    });
 1720                }
 1721            });
 1722            return [...orderNums.slice(0, 15), ...skus.slice(0, 15)];
 1723        }
 1724
 1725        function renderSuggestions(suggestions, q) {
 1726            suggestionsBox.innerHTML = '';
 1727            // Порядок групп: номера заказов → артикулы/товары → SKU
 1728            const groupOrder = ['ordernum', 'order', 'article', 'product', 'sku'];
 1729            const groups = {};
 1730            groupOrder.forEach(k => groups[k] = []);
 1731            suggestions.forEach(s => {
 1732                if (groups[s.type] !== undefined) groups[s.type].push(s);
 1733            });
 1734
 1735            const groupLabels = {
 1736                ordernum: 'Номера заказов',
 1737                order: 'Номера заказов (БД)',
 1738                article: 'Артикулы',
 1739                product: 'Товары',
 1740                sku: 'SKU'
 1741            };
 1742            let hasAny = false;
 1743
 1744            for (const type of groupOrder) {
 1745                const items = groups[type];
 1746                if (!items.length) continue;
 1747                hasAny = true;
 1748                const header = document.createElement('div');
 1749                header.className = 'px-3 py-1 text-muted small fw-semibold border-bottom';
 1750                header.style.background = '#f8fafc';
 1751                header.textContent = groupLabels[type] || type;
 1752                suggestionsBox.appendChild(header);
 1753
 1754                items.forEach(s => {
 1755                    const item = document.createElement('div');
 1756                    item.className = 'px-3 py-2 suggestion-item d-flex align-items-start gap-2';
 1757                    item.style.cursor = 'pointer';
 1758                    // Подсветка совпадения — только если есть что искать
 1759                    const escapedLabel = esc(s.label);
 1760                    const highlighted = q
 1761                        ? escapedLabel.replace(new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), m => `<mark>$
 1762                        : escapedLabel;
 1763                    // Сниппет сообщения и клиент
 1764                    const previewHtml = s.preview
 1765                        ? `<div class="text-muted" style="font-size:0.72em;line-height:1.3;margin-top:2px;white-space:no
 1766                        : '';
 1767                    const clientHtml = s.client
 1768                        ? `<span class="badge bg-secondary ms-auto" style="font-size:0.65em;flex-shrink:0">${esc(s.clien
 1769                        : `<small class="text-muted ms-auto" style="flex-shrink:0">${esc(s.hint)}</small>`;
 1770                    item.innerHTML = `<i class="${esc(s.icon)} text-secondary fa-fw mt-1"></i><div class="flex-grow-1 ov
 1771                    item.addEventListener('mouseenter', () => item.style.background = '#e8f0fe');
 1772                    item.addEventListener('mouseleave', () => item.style.background = '');
 1773                    item.addEventListener('click', () => {
 1774                        searchInput.value = s.label;
 1775                        btnClearSearch.classList.remove('d-none');
 1776                        hideSuggestions();
 1777                        doSearchBySuggestion(s);
 1778                    });
 1779                    suggestionsBox.appendChild(item);
 1780                });
 1781            }
 1782
 1783            if (!hasAny) {
 1784                hideSuggestions();
 1785                return;
 1786            }
 1787            suggestionsBox.classList.remove('d-none');
 1788        }
 1789
 1790        function hideSuggestions() {
 1791            if (suggestionsBox) suggestionsBox.classList.add('d-none');
 1792        }
 1793
 1794        // Вызывается при клике по подсказке — для sku/ordernum ищет только подходящие чаты
 1795        function doSearchBySuggestion(s) {
 1796            // Если suggestion содержит прямой chatId (точный PostingNumber из БД) — используем его
 1797            if (s.chatId) {
 1798                const subset = loadedChats.filter(c => c.chatId === s.chatId);
 1799                if (subset.length) {
 1800                    doSearch(s.label, subset);
 1801                    return;
 1802                }
 1803                // Если в текущем page нет нужного чата, всё равно передаём label — контроллер найдёт по PostingNumber
 1804                doSearch(s.label);
 1805                return;
 1806            }
 1807
 1808            if (s.type === 'sku' || s.type === 'ordernum') {
 1809                const field = s.type === 'sku' ? 'sku' : 'orderNumber';
 1810                // Находим chatId из кэша по точному значению
 1811                const matchedIds = new Set();
 1812                chatMetaCache.forEach((meta, chatId) => {
 1813                    if (meta[field] && meta[field] === s.label) matchedIds.add(chatId);
 1814                });
 1815                if (matchedIds.size > 0) {
 1816                    const subset = loadedChats.filter(c => matchedIds.has(c.chatId));
 1817                    if (subset.length) {
 1818                        doSearch(s.label, subset);
 1819                        return;
 1820                    }
 1821                }
 1822                // если кэш ещё не заполнен — полный поиск как запасной вариант
 1823                doSearch(s.label);
 1824            } else {
 1825                // Для артикулов, товаров, номеров заказа (БД) — полный поиск
 1826                doSearch(s.label);
 1827            }
 1828        }
 1829
 1830        function doSearch(q, chatsSubset) {
 1831            if (!q || q.length < 2) return;
 1832            const targetChats = chatsSubset || loadedChats;
 1833            if (!targetChats.length) {
 1834                if (!loadedChats.length) {
 1835                    errorBox.textContent = 'Сначала загрузите чаты кнопкой «Загрузить»';
 1836                    show(chatsSection);
 1837                    show(errorBox);
 1838                }
 1839                return;
 1840            }
 1841
 1842            const gen = ++searchGen;  // уникальный ключ — при новом поиске старый прекратит запись
 1843            isSearchMode = true;
 1844
 1845            show(chatsSection);
 1846            hide(spinner);
 1847            hide(errorBox);
 1848            hide(emptyBox);
 1849            hide(pagWrap);
 1850            hide(statsRow);
 1851
 1852            // Бейдж поиска
 1853            searchModeQuery.textContent = `"${q}"`;
 1854            searchResultCount.textContent = '...';
 1855            show(searchModeBadge);
 1856
 1857            // Подготовка таблицы + строка прогресса
 1858            tbody.innerHTML = '';
 1859            show(tableWrap);
 1860            const progressRow = document.createElement('tr');
 1861            progressRow.id = 'search-progress-row';
 1862            progressRow.innerHTML = `<td colspan="8" class="py-2">
 1863                <i class="fas fa-circle-notch fa-spin text-primary me-2"></i>
 1864                <span id="search-progress-text" class="text-muted small">Проверяю 0 из ${targetChats.length} чатов...</s
 1865                <span id="search-found-count" class="ms-3 text-success fw-semibold small"></span>
 1866            </td>`;
 1867            tbody.appendChild(progressRow);
 1868
 1869            const normalizedQ = q.toLowerCase();
 1870            const escapedQ = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
 1871            let checked = 0;
 1872            let found = 0;
 1873            const total = targetChats.length;
 1874
 1875            // Вспомогательная функция: обработать один чат
 1876            async function processChat(c) {
 1877                const params = new URLSearchParams({
 1878                    clientId: c.clientId,
 1879                    chatId: c.chatId,
 1880                    limit: 100,
 1881                    direction: 'Backward'
 1882                });
 1883                try {
 01884                    const r = await fetch(`@Url.Action("GetHistoryJson")?${params}`);
 1885                    const hdata = await r.json();
 1886
 1887                    if (gen !== searchGen) return; // отменён
 1888
 1889                    checked++;
 1890                    const progressText = document.getElementById('search-progress-text');
 1891                    if (progressText) progressText.textContent = `Проверяю ${checked} из ${total} чатов...`;
 1892
 1893                    if (hdata.success && hdata.messages && hdata.messages.length > 0) {
 1894                        // Текст сообщения хранится в массиве data[] — объединяем для поиска
 1895                        const getMsgText = m => (m.data || []).filter(Boolean).join(' ');
 1896
 1897                        // Заполняем кэш meta-данных этого чата (sku + orderNumber)
 1898                        hdata.messages.forEach(m => {
 1899                            const existing = chatMetaCache.get(c.chatId) || {};
 1900                            let changed = false;
 1901                            if (m.sku && !existing.sku) {
 1902                                existing.sku = m.sku;
 1903                                changed = true;
 1904                            }
 1905                            if (m.orderNumber && !existing.orderNumber) {
 1906                                existing.orderNumber = m.orderNumber;
 1907                                changed = true;
 1908                            }
 1909                            if (changed) chatMetaCache.set(c.chatId, existing);
 1910                        });
 1911
 1912                        // Совпадение: текст, SKU или номер заказа
 1913                        const matches = hdata.messages.filter(m =>
 1914                            getMsgText(m).toLowerCase().includes(normalizedQ) ||
 1915                            (m.sku || '').toLowerCase().includes(normalizedQ) ||
 1916                            (m.orderNumber || '').toLowerCase().includes(normalizedQ)
 1917                        );
 1918                        if (!matches.length) return;
 1919
 1920                        found++;
 1921                        const foundEl = document.getElementById('search-found-count');
 1922                        if (foundEl) foundEl.textContent = `Найдено: ${found}`;
 1923                        searchResultCount.textContent = `${found} чат(ов)`;
 1924
 1925                        // Лучшее совпадение — первое найденное (самое раннее)
 1926                        const bestMsg = matches[matches.length - 1];
 1927                        const msgPreview = getMsgText(bestMsg).substring(0, 200);
 1928                        const highlighted = esc(msgPreview).replace(new RegExp(escapedQ, 'gi'), m => `<mark class="bg-wa
 1929                        const isCustomer = (bestMsg.userType || '').toLowerCase().includes('customer');
 1930                        const senderClass = isCustomer ? 'customer' : 'seller';
 1931                        const senderLabel = isCustomer ? 'Покупатель' : 'Продавец';
 1932
 1933                        // orderNumber из контекста первого сообщения
 1934                        const ctxMsg = hdata.messages[hdata.messages.length - 1];
 1935                        const orderNum = (ctxMsg && ctxMsg.orderNumber) || c.postingNumber || '';
 1936                        const sku = (ctxMsg && ctxMsg.sku) || '';
 1937
 1938                        const tr = document.createElement('tr');
 1939                        if (c.unreadCount > 0) tr.className = 'table-row-highlight';
 1940                        tr.style.cursor = 'pointer';
 1941                        tr.addEventListener('click', e => {
 1942                            if (e.target.closest('button')) return;
 1943                            viewChat(c.chatId, c.clientId);
 1944                        });
 1945                        tr.innerHTML = `
 1946                            <td><span class="badge badge-closed">${esc(c.clientName || '')}</span></td>
 1947                            <td class="last-msg-cell">
 1948                                <div class="msg-content">
 1949                                    <span class="msg-sender ${senderClass}">${senderLabel}:</span> ${highlighted}
 1950                                </div>
 1951                                ${orderNum ? `<div class="mt-1"><small class="text-muted"><i class="fas fa-box me-1"></i
 1952                                ${sku ? `<div><small class="text-muted"><i class="fas fa-barcode me-1"></i>SKU: ${esc(sk
 1953                                <div><small class="text-muted"><i class="fas fa-comment me-1"></i>${matches.length} совп
 1954                            </td>
 1955                            <td><span class="${statusBadge(c.chatStatus)}">${statusLabel(c.chatStatus)}</span></td>
 1956                            <td><span class="${typeBadge(c.chatType)}">${typeLabel(c.chatType)}</span></td>
 1957                            <td><span title="${esc(c.createdAt)}">${esc(c.createdAtFormatted)}</span></td>
 1958                            <td>${bestMsg.createdAtFormatted ? `<span>${esc(bestMsg.createdAtFormatted)}</span>` : '<spa
 1959                            <td class="text-center">${c.unreadCount > 0 ? `<span class="badge badge-danger-soft">${c.unr
 1960                            <td>
 1961                                <button class="btn btn-sm btn-table-light me-1" onclick="viewChat('${esc(c.chatId)}', ${
 1962                                <button class="btn btn-sm btn-table-light" onclick="copyId('${esc(c.chatId)}')"><i class
 1963                            </td>`;
 1964
 1965                        // Вставляем перед строкой прогресса
 1966                        const pr = document.getElementById('search-progress-row');
 1967                        if (pr) pr.before(tr); else tbody.appendChild(tr);
 1968                    }
 1969                } catch (e) {
 1970                    if (gen !== searchGen) return;
 1971                    checked++;
 1972                    const progressText = document.getElementById('search-progress-text');
 1973                    if (progressText) progressText.textContent = `Проверяю ${checked} из ${total} чатов...`;
 1974                }
 1975            }
 1976
 1977            // Запускаем пулом по 5 параллельных запросов
 1978            async function runPool() {
 1979                const CONCURRENCY = 5;
 1980                let idx = 0;
 1981
 1982                async function next() {
 1983                    while (idx < targetChats.length) {
 1984                        if (gen !== searchGen) return;
 1985                        const c = targetChats[idx++];
 1986                        await processChat(c);
 1987                    }
 1988                }
 1989
 1990                const workers = [];
 1991                for (let i = 0; i < CONCURRENCY; i++) workers.push(next());
 1992                await Promise.all(workers);
 1993
 1994                if (gen !== searchGen) return;
 1995
 1996                // Завершение
 1997                const pr = document.getElementById('search-progress-row');
 1998                if (pr) pr.remove();
 1999                searchResultCount.textContent = `${found} чат(ов)`;
 2000
 2001                if (found === 0) {
 2002                    emptyBox.innerHTML = St2EmptyState.markup({ kind: 'search', title: `Ничего не найдено по запросу «${
 2003                    hide(tableWrap);
 2004                    show(emptyBox);
 2005                }
 2006            }
 2007
 2008            runPool();
 2009        }
 2010
 2011        function exitSearchMode() {
 2012            isSearchMode = false;
 2013            hide(searchModeBadge);
 2014            // Очищаем таблицу (она заполнится при следующей загрузке)
 2015            if (tbody) tbody.innerHTML = '';
 2016            hide(tableWrap);
 2017            hide(chatsSection);
 2018            hide(statsRow);
 2019        }
 2020
 2021        // === КОНЕЦ ПОИСКА ===
 2022
 2023
 2024        if (selClient) {
 2025            selClient.addEventListener('change', () => {
 2026                currentPage = 1;
 2027                loadChats();
 2028            });
 2029        }
 2030
 2031        // === Загрузка по кнопке ===
 2032        if (btnLoad) {
 2033            btnLoad.addEventListener('click', () => {
 2034                currentPage = 1;
 2035                loadChats();
 2036            });
 2037        }
 2038
 2039        // === Авто-перезагрузка при смене фильтров ===
 2040        [selStatus, selType, selPageSize].forEach(el => {
 2041            if (el) el.addEventListener('change', () => {
 2042                currentPage = 1;
 2043                loadChats();
 2044            });
 2045        });
 2046        if (chkUnread) chkUnread.addEventListener('change', () => {
 2047            currentPage = 1;
 2048            loadChats();
 2049        });
 2050
 2051        // === Экспорт ===
 2052        if (btnExport) {
 2053            btnExport.addEventListener('click', () => {
 2054                document.getElementById('exp-clientId').value = selClient.value;
 2055                document.getElementById('exp-status').value = selStatus.value;
 2056                document.getElementById('exp-unread').value = chkUnread.checked;
 2057                document.getElementById('export-form').submit();
 2058            });
 2059        }
 2060
 2061        // === Загрузка чатов ===
 2062        function loadChats() {
 2063            if (!selClient || !selStatus || !selType || !chkUnread || !selPageSize) return;
 2064
 2065            // Выходим из режима поиска при обычной загрузке
 2066            if (isSearchMode) {
 2067                isSearchMode = false;
 2068                hide(searchModeBadge);
 2069            }
 2070
 2071            const clientId = selClient.value;
 2072
 2073            show(chatsSection);
 2074            if (window.SkeletonLoader) {
 2075                SkeletonLoader.table(tbody, { columns: 8, rows: 7 });
 2076                hide(spinner);
 2077            } else show(spinner);
 2078            hide(errorBox);
 2079            hide(emptyBox);
 2080            hide(tableWrap);
 2081            hide(pagWrap);
 2082            if (window.SkeletonLoader) show(tableWrap);
 2083
 2084
 2085            const sortField = document.getElementById('sel-sort-field')?.value || 'createdAt';
 2086            const sortOrder = document.getElementById('sel-sort-order')?.value || 'desc';
 2087            const params = new URLSearchParams({
 2088                clientId,
 2089                status: selStatus.value,
 2090                chatType: selType.value,
 2091                unreadOnly: chkUnread.checked,
 2092                page: currentPage,
 2093                pageSize: selPageSize.value,
 2094                sortField,
 2095                sortOrder
 2096            });
 2097            // Сортировка — обработчики
 2098            const selSortField = document.getElementById('sel-sort-field');
 2099            const selSortOrder = document.getElementById('sel-sort-order');
 2100            if (selSortField) selSortField.addEventListener('change', () => {
 2101                currentPage = 1;
 2102                loadChats();
 2103            });
 2104            if (selSortOrder) selSortOrder.addEventListener('change', () => {
 2105                currentPage = 1;
 2106                loadChats();
 2107            });
 2108
 02109            fetch(`@Url.Action("GetChatsJson")?${params}`)
 2110                .then(r => r.json())
 2111                .then(data => {
 2112                    hide(spinner);
 2113                    if (window.SkeletonLoader) SkeletonLoader.finish(tbody);
 2114                    if (!data.success) {
 2115                        hide(tableWrap);
 2116                        errorBox.textContent = data.message || 'Неизвестная ошибка';
 2117                        show(errorBox);
 2118                        return;
 2119                    }
 2120
 2121                    // Бейджи в шапке
 2122                    @* if (data.totalUnreadCount > 0) { *@
 2123                    @*     unreadText.textContent = data.totalUnreadCount; *@
 2124                    @*     show(badgeUnread); *@
 2125                    @* } else { *@
 2126                    @*     hide(badgeUnread); *@
 2127                    @* } *@
 2128                    totalText.textContent = data.totalChatsCount;
 2129                    show(badgeTotal);
 2130
 2131                    // Статистика
 2132                    statTotal.textContent = data.totalChatsCount;
 2133                    @* statUnread.textContent = data.totalUnreadCount; *@
 2134                    statPage.textContent = data.chats.length;
 2135                    show(statsRow);
 2136
 2137                    // Обновляем бейдж непрочитанных в навигации
 2138                    const navBadge = document.getElementById('ozon-chat-unread-badge');
 2139                    if (navBadge) {
 2140                        if (data.totalUnreadCount > 0) {
 2141                            navBadge.textContent = data.totalUnreadCount;
 2142                            navBadge.style.display = 'inline-block';
 2143                        } else {
 2144                            navBadge.style.display = 'none';
 2145                        }
 2146                    }
 2147
 2148                    // Кнопка экспорта
 2149                    btnExport.classList.remove('d-none');
 2150
 2151                    if (!data.chats || data.chats.length === 0) {
 2152                        show(emptyBox);
 2153                        hide(pagWrap);
 2154                        return;
 2155                    }
 2156
 2157                    // Рендер таблицы
 2158                    tbody.innerHTML = '';
 2159                    data.chats.forEach(c => {
 2160                        const tr = document.createElement('tr');
 2161                        if (c.unreadCount > 0) tr.className = 'table-row-highlight';
 2162                        tr.style.cursor = 'pointer';
 2163                        tr.addEventListener('click', e => {
 2164                            if (e.target.closest('button')) return;
 2165                            viewChat(c.chatId, c.clientId);
 2166                        });
 2167                        tr.innerHTML = `
 2168                        <td><span class="badge badge-closed">${esc(c.clientName || '')}</span></td>
 2169                        <td class="last-msg-cell" data-chat-id="${esc(c.chatId)}" data-client-id="${c.clientId}">
 2170                            <div class="loading-dots">
 2171                                <span class="dot">.</span><span class="dot">.</span><span class="dot">.</span>
 2172                            </div>
 2173                        </td>
 2174                        <td><span class="${statusBadge(c.chatStatus)}">${statusLabel(c.chatStatus)}</span></td>
 2175                        <td><span class="${typeBadge(c.chatType)}">${typeLabel(c.chatType)}</span></td>
 2176                        <td><span title="${esc(c.createdAt)}">${esc(c.createdAtFormatted)}</span></td>
 2177                        <td class="last-msg-time-cell" data-chat-id="${esc(c.chatId)}"><span class="text-muted">--</span
 2178                        <td class="text-center">${c.unreadCount > 0 ? `<span class="badge badge-danger-soft">${c.unreadC
 2179                        <td>
 2180                            <button class="btn btn-sm btn-table-light me-1" onclick="viewChat('${esc(c.chatId)}', ${c.cl
 2181                            <button class="btn btn-sm btn-table-light" onclick="copyId('${esc(c.chatId)}')"><i class="fa
 2182                        </td>`;
 2183                        tbody.appendChild(tr);
 2184                    });
 2185                    loadedChats = data.chats.slice(); // сохраняем для поиска
 2186                    chatMetaCache.clear();           // сбрасываем кэш при новой загрузке
 2187                    preScanRunning = false;
 2188                    startPreScan();                  // фоновое заполнение кэша SKU/orderNumber
 2189                    show(tableWrap);
 2190
 2191                    // Загружаем последние сообщения для каждого чата
 2192                    loadLastMessages();
 2193
 2194                    // Пагинация
 2195                    renderPagination(data.currentPage, data.totalPages, data.totalChatsCount);
 2196                })
 2197                .catch(err => {
 2198                    hide(spinner);
 2199                    if (window.SkeletonLoader) SkeletonLoader.finish(tbody);
 2200                    hide(tableWrap);
 2201                    errorBox.textContent = `Ошибка сети: ${err.message}`;
 2202                    show(errorBox);
 2203                });
 2204        }
 2205
 2206        // === Рендер пагинации ===
 2207        function renderPagination(page, totalPages, totalChats) {
 2208            if (totalPages <= 1) {
 2209                hide(pagWrap);
 2210                return;
 2211            }
 2212            show(pagWrap);
 2213            pageInfo.textContent = `Страница ${page} из ${totalPages} (всего чатов: ${totalChats})`;
 2214
 2215            pagList.innerHTML = '';
 2216
 2217            addPageItem('«', 1, page === 1);
 2218            addPageItem('‹', page - 1, page === 1);
 2219
 2220            let start = Math.max(1, page - 3);
 2221            let end = Math.min(totalPages, page + 3);
 2222
 2223            if (start > 1) {
 2224                addPageItem('1', 1);
 2225                if (start > 2) addPageItem('…', null, true);
 2226            }
 2227            for (let i = start; i <= end; i++) addPageItem(String(i), i, false, i === page);
 2228            if (end < totalPages) {
 2229                if (end < totalPages - 1) addPageItem('…', null, true);
 2230                addPageItem(String(totalPages), totalPages);
 2231            }
 2232
 2233            addPageItem('›', page + 1, page >= totalPages);
 2234            addPageItem('»', totalPages, page >= totalPages);
 2235        }
 2236
 2237        function addPageItem(label, pageNum, disabled, active) {
 2238            const li = document.createElement('li');
 2239            li.className = `page-item${disabled ? ' disabled' : ''}${active ? ' active' : ''}`;
 2240            const a = document.createElement('a');
 2241            a.className = 'page-link';
 2242            a.href = '#';
 2243            a.textContent = label;
 2244            if (!disabled && pageNum != null && !active) {
 2245                a.addEventListener('click', e => {
 2246                    e.preventDefault();
 2247                    currentPage = pageNum;
 2248                    loadChats();
 2249                });
 2250            }
 2251            li.appendChild(a);
 2252            pagList.appendChild(li);
 2253        }
 2254
 2255        // === Хелперы статусов/типов ===
 2256        function statusLabel(s) {
 2257            switch ((s || '').toUpperCase()) {
 2258                case 'OPENED':
 2259                    return 'Открыт';
 2260                case 'CLOSED':
 2261                    return 'Закрыт';
 2262                case 'UNSPECIFIED':
 2263                    return 'Не определён';
 2264                default:
 2265                    return s;
 2266            }
 2267        }
 2268
 2269        function statusBadge(s) {
 2270            switch ((s || '').toUpperCase()) {
 2271                case 'OPENED':
 2272                    return 'badge badge-open';
 2273                case 'CLOSED':
 2274                    return 'badge badge-closed';
 2275                default:
 2276                    return 'badge badge-other';
 2277            }
 2278        }
 2279
 2280        function typeLabel(t) {
 2281            switch ((t || '').toUpperCase()) {
 2282                case 'SELLER_SUPPORT':
 2283                    return 'Чат с поддержкой';
 2284                case 'BUYER_SELLER':
 2285                    return 'Чат с покупателем';
 2286                case 'UNSPECIFIED':
 2287                    return 'Не определён';
 2288                default:
 2289                    return t;
 2290            }
 2291        }
 2292
 2293        function typeBadge(t) {
 2294            switch ((t || '').toUpperCase()) {
 2295                case 'SELLER_SUPPORT':
 2296                    return 'badge badge-seller';
 2297                case 'BUYER_SELLER':
 2298                    return 'badge badge-buyer';
 2299                default:
 2300                    return 'badge badge-other';
 2301            }
 2302        }
 2303
 2304        // === Утилиты ===
 2305        function esc(s) {
 2306            if (s === null || s === undefined) return '';
 2307            const d = document.createElement('div');
 2308            d.textContent = s;
 2309            return d.innerHTML;
 2310        }
 2311
 2312        function escapeHtml(s) {
 2313            return esc(s);
 2314        }
 2315
 2316        // === Прокси URL для файлов Ozon (требуют авторизации) ===
 2317        function getProxyUrl(originalUrl, clientId) {
 2318            if (!originalUrl) return '';
 2319            if (originalUrl.includes('api-seller.ozon.ru')) {
 02320                return `@Url.Action("GetChatFile")?clientId=${encodeURIComponent(clientId)}&fileUrl=${encodeURIComponent
 2321            }
 2322            return originalUrl;
 2323        }
 2324
 2325        // === Функция для отображения "сколько времени назад" ===
 2326        function timeAgo(dateString) {
 2327            if (!dateString) return '';
 2328            const date = new Date(dateString);
 2329            const now = new Date();
 2330            const diffMs = now - date;
 2331            const diffSec = Math.floor(diffMs / 1000);
 2332            const diffMin = Math.floor(diffSec / 60);
 2333            const diffHour = Math.floor(diffMin / 60);
 2334            const diffDay = Math.floor(diffHour / 24);
 2335
 2336            if (diffSec < 60) return 'только что';
 2337            if (diffMin < 60) {
 2338                return `${diffMin} мин назад`;
 2339            }
 2340            if (diffHour < 24) {
 2341                return `${diffHour} ч назад`;
 2342            }
 2343            if (diffDay < 30) {
 2344                return `${diffDay} дн назад`;
 2345            }
 2346            return 'более месяца назад';
 2347        }
 2348
 2349        function renderMarkdown(text, clientId) {
 2350            let html = esc(text);
 2351            html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
 2352            // Ссылки
 2353            html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</
 2354            // Ozon картинки через прокси с лоадером и увеличением
 2355            html = html.replace(/!\[[^\]]*\]\((https:\/\/api-seller\.ozon\.ru[^)]+)\)/gi, function (match, url) {
 2356                if (clientId) {
 2357                    const proxied = getProxyUrl(url, clientId);
 2358                    const loader = '<div class="img-loader" style="display:inline-block;width:48px;height:48px;vertical-
 2359                    return `<span class="img-wrapper" style="display:inline-block;position:relative;max-width:100%;max-h
 2360                }
 2361                return match;
 2362            });
 2363            // Обычные картинки (не Ozon) с лоадером и увеличением
 2364            html = html.replace(/!\[[^\]]*\]\(([^)]+)\)/g, function (match, url) {
 2365                const loader = '<div class="img-loader" style="display:inline-block;width:48px;height:48px;vertical-alig
 2366                return `<span class="img-wrapper" style="display:inline-block;position:relative;max-width:100%;max-heigh
 2367            });
 2368            const lines = html.split('\n');
 2369            let result = [];
 2370            let inList = false;
 2371            for (let i = 0; i < lines.length; i++) {
 2372                const line = lines[i];
 2373                const listMatch = line.match(/^\s*(?:\*|•)\s+(.+)/);
 2374                if (listMatch) {
 2375                    if (!inList) {
 2376                        result.push('<ul class="mb-1 ps-3">');
 2377                        inList = true;
 2378                    }
 2379                    result.push('<li>' + listMatch[1] + '</li>');
 2380                } else {
 2381                    if (inList) {
 2382                        result.push('</ul>');
 2383                        inList = false;
 2384                    }
 2385                    result.push(line);
 2386                }
 2387            }
 2388            if (inList) result.push('</ul>');
 2389            html = result.join('\n');
 2390            html = html.replace(/\n(?!<\/?[uo]l|<li)/g, '<br>');
 2391            return html;
 2392        }
 2393
 2394        function show(el) {
 2395            if (el) el.classList.remove('d-none');
 2396        }
 2397
 2398        function hide(el) {
 2399            if (el) el.classList.add('d-none');
 2400        }
 2401
 2402        // === Загрузка последних сообщений для ячеек таблицы ===
 2403        function loadLastMessages() {
 2404            const cells = document.querySelectorAll('.last-msg-cell');
 2405            cells.forEach(cell => {
 2406                const chatId = cell.dataset.chatId;
 2407                const clientId = cell.dataset.clientId;
 2408                if (!chatId || !clientId) return;
 2409
 02410                fetch(`@Url.Action("GetLastMessageJson")?clientId=${clientId}&chatId=${encodeURIComponent(chatId)}`)
 2411                    .then(r => r.json())
 2412                    .then(data => {
 2413                        const timeCell = document.querySelector(`.last-msg-time-cell[data-chat-id="${chatId}"]`);
 2414
 2415                        if (data.success && data.hasMessage) {
 2416                            const senderClass = (data.senderType || 'unknown').toLowerCase();
 2417                            cell.innerHTML = `
 2418                            <div class="msg-content">
 2419                                <span class="msg-sender ${esc(senderClass)}">${esc(data.sender)}:</span>
 2420                                ${esc(data.text)}
 2421                            </div>`;
 2422
 2423                            if (timeCell && data.createdAt) {
 2424                                const ago = timeAgo(data.createdAt);
 2425                                timeCell.innerHTML = `<span title="${esc(data.createdAtFormatted)}">${esc(data.createdAt
 2426                            }
 2427                        } else if (data.success && !data.hasMessage) {
 2428                            const ozonUrl = `https://seller.ozon.ru/app/messenger/?group=customers_v2&id=${encodeURIComp
 2429                            cell.innerHTML = `<span class="text-muted fst-italic">Обработать на <a href="${ozonUrl}" tar
 2430                            if (timeCell) timeCell.innerHTML = '<span class="text-muted">—</span>';
 2431                        } else {
 2432                            cell.innerHTML = `<span class="text-danger small">Ошибка загрузки</span>`;
 2433                            if (timeCell) timeCell.innerHTML = '<span class="text-muted">—</span>';
 2434                        }
 2435                    })
 2436                    .catch(() => {
 2437                        cell.innerHTML = '<span class="text-danger small">Ошибка сети</span>';
 2438                        const timeCell = document.querySelector(`.last-msg-time-cell[data-chat-id="${cell.dataset.chatId
 2439                        if (timeCell) timeCell.innerHTML = '<span class="text-muted">—</span>';
 2440                    });
 2441            });
 2442        }
 2443
 2444
 2445        // === Глобальные функции ===
 2446        let histChatId = '';
 2447        let histClientId = null;
 2448        let histOldestMessageId = null;
 2449        let histNewestMessageId = null;
 2450        let histHasNext = false;
 2451        let histHasUnread = false;
 2452
 2453        window.viewChat = function (chatId, clientId) {
 2454            histChatId = chatId;
 2455            histClientId = clientId || selClient.value;
 2456            histOldestMessageId = null;
 2457            histNewestMessageId = null;
 2458            histHasNext = false;
 2459            histHasUnread = false;
 2460
 2461            const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('chatHistoryModal'));
 2462            document.getElementById('hist-chat-id').textContent = chatId;
 2463            document.getElementById('hist-chat-meta').textContent = '';
 2464            document.getElementById('hist-messages').innerHTML = '';
 2465            document.getElementById('hist-spinner').classList.remove('d-none');
 2466            document.getElementById('hist-error').classList.add('d-none');
 2467            document.getElementById('hist-load-older').classList.add('d-none');
 2468            document.getElementById('btn-mark-read').classList.add('d-none');
 2469            document.getElementById('message-text').value = '';
 2470            document.getElementById('char-count').textContent = '0';
 2471            document.getElementById('file-input').value = '';
 2472            clearAttachedFile();
 2473
 2474            // очистка полей трекинга
 2475            const hoursEl = document.getElementById('resp-time-hours');
 2476            const minutesEl = document.getElementById('resp-time-minutes');
 2477            if (hoursEl) hoursEl.value = '';
 2478            if (minutesEl) minutesEl.value = '';
 2479            modal.show();
 2480
 2481            // загрузить предыдущие записи времени
 2482            loadResponseTimes();
 2483
 2484            loadHistory(chatId, null);
 2485        };
 2486
 2487
 2488        let newMsgDividerInserted = false; // флаг, чтобы вставить только один раз разделить новых сообщений
 2489
 2490        function loadHistory(chatId, fromMessageId) {
 2491
 2492            // сброс флага только если это initial load
 2493            if (!fromMessageId) {
 2494                newMsgDividerInserted = false;
 2495            }
 2496
 2497            const clientId = histClientId || selClient.value;
 2498            if (!clientId) return;
 2499
 2500            const historyContainer = document.getElementById('hist-messages');
 2501            if (!fromMessageId && window.SkeletonLoader) {
 2502                SkeletonLoader.lines(historyContainer, { count: 7 });
 2503                document.getElementById('hist-spinner').classList.add('d-none');
 2504            }
 2505
 2506            const params = new URLSearchParams({clientId, chatId, limit: 100, direction: 'Backward'});
 2507            if (fromMessageId) params.set('fromMessageId', fromMessageId);
 2508
 02509            fetch(`@Url.Action("GetHistoryJson")?${params}`)
 2510                .then(r => r.json())
 2511                .then(data => {
 2512                    document.getElementById('hist-spinner').classList.add('d-none');
 2513                    if (!fromMessageId && window.SkeletonLoader) SkeletonLoader.finish(historyContainer);
 2514
 2515                    if (!data.success) {
 2516                        const errEl = document.getElementById('hist-error');
 2517                        // Если сообщение содержит HTML (например, ссылка), используем innerHTML, иначе textContent
 2518                        if (data.premiumRequired && data.message && data.message.includes('<a')) {
 2519                            errEl.innerHTML = data.message;
 2520                        } else {
 2521                            errEl.textContent = data.message || 'Ошибка загрузки';
 2522                        }
 2523                        errEl.classList.remove('d-none');
 2524                        return;
 2525                    }
 2526
 2527                    histHasNext = data.hasNext;
 2528
 2529                    if (data.messages && data.messages.length > 0) {
 2530                        const reversed = [...data.messages].reverse();
 2531                        const container = document.getElementById('hist-messages');
 2532                        const isInitialLoad = !fromMessageId;
 2533                        const frag = document.createDocumentFragment();
 2534
 2535                        if (isInitialLoad) {
 2536                            container.innerHTML = '';  // Очищаем контейнер сообщений при первой загрузке
 2537                        }
 2538
 2539                        reversed.forEach(m => {
 2540                            frag.appendChild(createMessageBubble(m));
 2541                            if (!histOldestMessageId || m.messageId < histOldestMessageId) {
 2542                                histOldestMessageId = m.messageId;
 2543                            }
 2544                            if (!histNewestMessageId || m.messageId > histNewestMessageId) {
 2545                                histNewestMessageId = m.messageId;
 2546                            }
 2547                            if (!m.isRead) histHasUnread = true;
 2548                        });
 2549
 2550                        if (isInitialLoad) {
 2551                            container.appendChild(frag);
 2552                            container.scrollTop = container.scrollHeight;
 2553                        }
 2554
 2555
 2556                        const first = data.messages[data.messages.length - 1];
 2557                        if (first.orderNumber || first.sku) {
 2558                            let meta = [];
 2559                            if (first.orderNumber) meta.push(`Заказ: ${first.orderNumber}`);
 2560                            if (first.sku) meta.push(`SKU: ${first.sku}`);
 2561                            document.getElementById('hist-chat-meta').textContent = meta.join(' | ');
 2562                        }
 2563                    }
 2564
 2565                    if (histHasNext) {
 2566                        document.getElementById('hist-load-older').classList.remove('d-none');
 2567                    } else {
 2568                        document.getElementById('hist-load-older').classList.add('d-none');
 2569                    }
 2570
 2571                    if (histHasUnread) {
 2572                        document.getElementById('btn-mark-read').classList.remove('d-none');
 2573                    }
 2574
 2575                    if (!data.messages || data.messages.length === 0) {
 2576                        if (!fromMessageId) {
 2577                            const histMessages = document.getElementById('hist-messages');
 2578                            const chatId = histChatId || (typeof data.ozonUrl === 'string' && data.ozonUrl.split('id=')[
 2579                            const ozonUrl = `https://seller.ozon.ru/app/messenger/?group=customers_v2&id=${encodeURIComp
 2580                            histMessages.innerHTML = `<div class="text-center text-muted py-5"><i class="fas fa-lock fa-
 2581                        }
 2582                    }
 2583                })
 2584                .catch(err => {
 2585                    document.getElementById('hist-spinner').classList.add('d-none');
 2586                    if (!fromMessageId && window.SkeletonLoader) SkeletonLoader.finish(historyContainer);
 2587                    const errEl = document.getElementById('hist-error');
 2588                    errEl.textContent = `Ошибка сети: ${err.message}`;
 2589                    errEl.classList.remove('d-none');
 2590                });
 2591        }
 2592
 2593        window.loadOlderMessages = function () {
 2594            if (!histHasNext || !histOldestMessageId) return;
 2595            document.getElementById('hist-load-older').classList.add('d-none');
 2596            loadHistory(histChatId, histOldestMessageId);
 2597        };
 2598
 2599        function createMessageBubble(m) {
 2600            const isSeller = (m.userType || '').toLowerCase().includes('seller');
 2601            const isSystem = (m.userType || '').toLowerCase().includes('system');
 2602
 2603            const wrapper = document.createElement('div');
 2604            wrapper.className = `d-flex flex-column mb-3  ${isSeller ? 'justify-content-end' : 'justify-content-start'}`
 2605
 2606            const bubble = document.createElement('div');
 2607
 2608            bubble.className = `chat-bubble ${isSeller ? 'chat-seller' : isSystem ? 'chat-system' : 'chat-customer'}`;
 2609
 2610            const header = document.createElement('div');
 2611            header.className = `d-flex justify-content-between align-items-center mb-2 ${isSeller ? 'flex-row-reverse' :
 2612            const userBadge = document.createElement('span');
 2613            userBadge.className = `badge ${isSeller ? 'chat-badge-seller' : isSystem ? 'chat-badge-system' : 'chat-badge
 2614            userBadge.textContent = userTypeLabel(m.userType);
 2615            header.appendChild(userBadge);
 2616
 2617
 2618            if (m.orderNumber || m.sku) {
 2619                const ctx = document.createElement('div');
 2620                ctx.className = `small text-muted ${isSeller ? 'me-4' : 'ms-4'}`;
 2621                let parts = [];
 2622                if (m.orderNumber) parts.push(`<i class="fas fa-box me-1"></i>Заказ: ${esc(m.orderNumber)}`);
 2623                if (m.sku) parts.push(`<i class="fas fa-barcode me-1"></i>SKU: ${esc(m.sku)}`);
 2624                ctx.innerHTML = parts.join(' &nbsp;|&nbsp; ');
 2625                header.appendChild(ctx);
 2626            }
 2627
 2628            if (!m.isRead && !newMsgDividerInserted) {
 2629                const divider = document.createElement('div');
 2630                divider.className = 'new-message-divider text-secondary';
 2631                divider.textContent = 'Новые сообщения';
 2632                wrapper.appendChild(divider);
 2633                newMsgDividerInserted = true;
 2634            }
 2635
 2636            bubble.appendChild(header);
 2637
 2638            if (m.data && m.data.length > 0) {
 2639                m.data.forEach(text => {
 2640                    const p = document.createElement('div');
 2641                    p.className = 'mb-1 chat-message-content';
 2642                    p.innerHTML = renderMarkdown(text, m.clientId || histClientId);
 2643                    bubble.appendChild(p);
 2644                });
 2645            }
 2646
 2647            const time = document.createElement('div');
 2648            time.className = 'text-muted small text-end';
 2649            time.textContent = m.createdAtFormatted;
 2650
 2651            bubble.appendChild(time);
 2652
 2653            wrapper.appendChild(bubble);
 2654            return wrapper;
 2655        }
 2656
 2657        function userTypeLabel(type) {
 2658            switch ((type || '').toLowerCase()) {
 2659                case 'seller':
 2660                    return 'Продавец';
 2661                case 'customer':
 2662                    return 'Покупатель';
 2663                case 'system':
 2664                    return 'Система';
 2665                case 'support':
 2666                    return 'Поддержка';
 2667                default:
 2668                    return type || 'Неизвестный';
 2669            }
 2670        }
 2671
 2672        window.copyId = function (chatId) {
 2673            navigator.clipboard.writeText(chatId).then(() => {
 2674                showToastMessage('ID скопирован', true);
 2675            });
 2676        };
 2677
 2678        function showToastMessage(message, isSuccess = true) {
 2679            const toast = document.createElement('div');
 2680            toast.className = `toast align-items-center text-white ${isSuccess ? 'bg-success' : 'bg-danger'} border-0 po
 2681            toast.style.zIndex = '99999';
 2682            const icon = isSuccess ? 'fa-check-circle' : 'fa-exclamation-circle';
 2683            toast.innerHTML = `<div class="d-flex"><div class="toast-body"><i class="fas ${icon} me-2"></i>${message}</d
 2684            document.body.appendChild(toast);
 2685            const bsToast = new bootstrap.Toast(toast);
 2686            bsToast.show();
 2687            toast.addEventListener('hidden.bs.toast', () => document.body.removeChild(toast));
 2688        }
 2689
 2690        window.markAsRead = function () {
 2691            const clientId = histClientId || selClient.value;
 2692            if (!clientId || !histChatId) return;
 2693
 2694            const fromMessageId = histNewestMessageId || null;
 2695
 2696            const btn = document.getElementById('btn-mark-read');
 2697            btn.disabled = true;
 2698            btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Отправка...';
 2699
 2700            const body = JSON.stringify({clientId: parseInt(clientId), chatId: histChatId, fromMessageId});
 2701
 02702            fetch(`@Url.Action("MarkAsRead")`, {
 2703                method: 'POST',
 2704                headers: {'Content-Type': 'application/json'},
 2705                body: body
 2706            })
 2707                .then(r => r.json())
 2708                .then(data => {
 2709                    btn.disabled = false;
 2710                    btn.innerHTML = '<i class="fas fa-check-double me-1"></i>Прочитано';
 2711
 2712                    if (data.success) {
 2713                        showToastMessage(`${data.message} (без ответа: ${data.unreadCount})`, true);
 2714
 2715                        // Сбрасываем флаг полоски Новых сообщений для будущих загрузок
 2716                        newMsgDividerInserted = false;
 2717
 2718                        if (data.unreadCount === 0) {
 2719                            btn.classList.add('d-none');
 2720                        }
 2721
 2722                        loadChats();
 2723                    } else {
 2724                        showToastMessage(data.message || 'Ошибка при отметке', false);
 2725                    }
 2726                })
 2727                .catch(err => {
 2728                    btn.disabled = false;
 2729                    btn.innerHTML = '<i class="fas fa-check-double me-1"></i>Прочитано';
 2730                    showToastMessage(`Ошибка сети: ${err.message}`, false);
 2731                });
 2732        };
 2733
 2734        // загрузить ранее сохранённые временные записи для текущего чата
 2735        function loadResponseTimes() {
 2736            if (!histChatId) return;
 02737            fetch(`@Url.Action("GetResponseTimes")?chatId=${encodeURIComponent(histChatId)}`)
 2738                .then(r => r.json())
 2739                .then(data => {
 2740                    const listEl = document.getElementById('resp-time-list');
 2741                    if (data.success && data.times) {
 2742                        if (data.times.length) {
 2743                            listEl.innerHTML = data.times.map(t => {
 2744                                const h = Math.floor(t.minutes / 60);
 2745                                const m = t.minutes % 60;
 2746                                const hm = h ? `${h} ч ${m} мин` : `${m} мин`;
 2747                                return `<div>${t.recordedAt} — <strong>${hm}</strong> <span class="text-muted">[${escape
 2748                            }).join('');
 2749                        } else {
 2750                            listEl.innerHTML = '<em>Нет записей</em>';
 2751                        }
 2752                    } else {
 2753                        listEl.innerHTML = '<span class="text-danger">Ошибка загрузки</span>';
 2754                    }
 2755                })
 2756                .catch(() => {
 2757                    const listEl = document.getElementById('resp-time-list');
 2758                    if (listEl) listEl.innerHTML = '<span class="text-danger">Ошибка сети</span>';
 2759                });
 2760        }
 2761
 2762        // сохранить вводимое время ответа
 2763        window.saveResponseTime = function () {
 2764            const hoursEl = document.getElementById('resp-time-hours');
 2765            const minutesEl = document.getElementById('resp-time-minutes');
 2766            if (!hoursEl || !minutesEl) return;
 2767
 2768            const h = parseInt(hoursEl.value) || 0;
 2769            const m = parseInt(minutesEl.value) || 0;
 2770            const total = h * 60 + m;
 2771            if (total <= 0) {
 2772                alert('Укажите ненулевое время в часах или минутах');
 2773                return;
 2774            }
 2775            if (!histChatId || !histClientId) {
 2776                alert('Не выбран чат');
 2777                return;
 2778            }
 2779
 2780            const payload = {
 2781                ChatId: histChatId,
 2782                ClientId: parseInt(histClientId),
 2783                Minutes: total
 2784            };
 2785
 2786            // выключаем кнопку на время отправки
 2787            const btn = this || document.querySelector('#resp-time-section button');
 2788            if (btn) {
 2789                btn.disabled = true;
 2790                btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Сохранение...';
 2791            }
 2792
 02793            fetch(`@Url.Action("AddResponseTime")`, {
 2794                method: 'POST',
 2795                headers: {'Content-Type': 'application/json'},
 2796                body: JSON.stringify(payload)
 2797            })
 2798                .then(r => r.json())
 2799                .then(data => {
 2800                    if (btn) {
 2801                        btn.disabled = false;
 2802                        btn.innerHTML = 'Сохранить';
 2803                    }
 2804                    if (data.success) {
 2805                        hoursEl.value = '';
 2806                        minutesEl.value = '';
 2807                        loadResponseTimes();
 2808                        showToastMessage('Время успешно сохранено', true);
 2809                    } else {
 2810                        showToastMessage(data.message || 'Ошибка при сохранении', false);
 2811                    }
 2812                })
 2813                .catch(err => {
 2814                    if (btn) {
 2815                        btn.disabled = false;
 2816                        btn.innerHTML = 'Сохранить';
 2817                    }
 2818                    showToastMessage(`Ошибка сети: ${err.message}`, false);
 2819                });
 2820        }
 2821
 2822        window.handleFileSelect = function () {
 2823            const fileInput = document.getElementById('file-input');
 2824            const file = fileInput.files[0];
 2825
 2826            if (file) {
 2827                const maxSize = 10 * 1024 * 1024;
 2828                if (file.size > maxSize) {
 2829                    showToastMessage('Размер файла не должен превышать 10 МБ', false);
 2830                    fileInput.value = '';
 2831                    return;
 2832                }
 2833
 2834                document.getElementById('attached-file-name').textContent = file.name;
 2835                document.getElementById('attached-file-info').style.display = 'block';
 2836            } else {
 2837                clearAttachedFile();
 2838            }
 2839        };
 2840
 2841        window.clearAttachedFile = function () {
 2842            document.getElementById('file-input').value = '';
 2843            document.getElementById('attached-file-info').style.display = 'none';
 2844            document.getElementById('attached-file-name').textContent = '';
 2845        };
 2846
 2847        window.sendMessageOrFile = function () {
 2848            const clientId = histClientId || selClient.value;
 2849            if (!clientId || !histChatId) return;
 2850
 2851            const messageTextarea = document.getElementById('message-text');
 2852            const text = messageTextarea.value.trim();
 2853            const fileInput = document.getElementById('file-input');
 2854            const file = fileInput.files[0];
 2855
 2856            if (!text && !file) {
 2857                showToastMessage('Введите сообщение или прикрепите файл', false);
 2858                return;
 2859            }
 2860
 2861            if (text && text.length > 1000) {
 2862                showToastMessage('Сообщение не должно превышать 1000 символов', false);
 2863                return;
 2864            }
 2865
 2866            const btn = document.getElementById('btn-send');
 2867            btn.disabled = true;
 2868            btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Отправка...';
 2869
 2870            const promises = [];
 2871
 2872            if (text) {
 2873                const messageBody = JSON.stringify({clientId: parseInt(clientId), chatId: histChatId, text});
 2874                promises.push(
 02875                    fetch(`@Url.Action("SendMessage")`, {
 2876                        method: 'POST',
 2877                        headers: {'Content-Type': 'application/json'},
 2878                        body: messageBody
 2879                    })
 2880                        .then(r => r.json())
 2881                        .then(data => {
 2882                            if (!data.success) {
 2883                                throw new Error(data.message || 'Ошибка при отправке сообщения');
 2884                            }
 2885                            return data;
 2886                        })
 2887                );
 2888            }
 2889
 2890            if (file) {
 2891                const filePromise = new Promise((resolve, reject) => {
 2892                    const reader = new FileReader();
 2893                    reader.onload = function (e) {
 2894                        const base64String = e.target.result.split(',')[1];
 2895                        const fileBody = JSON.stringify({
 2896                            clientId: parseInt(clientId),
 2897                            chatId: histChatId,
 2898                            base64Content: base64String,
 2899                            fileName: file.name
 2900                        });
 2901
 02902                        fetch(`@Url.Action("SendFile")`, {
 2903                            method: 'POST',
 2904                            headers: {'Content-Type': 'application/json'},
 2905                            body: fileBody
 2906                        })
 2907                            .then(r => r.json())
 2908                            .then(data => {
 2909                                if (!data.success) {
 2910                                    reject(new Error(data.message || 'Ошибка при отправке файла'));
 2911                                } else {
 2912                                    resolve(data);
 2913                                }
 2914                            })
 2915                            .catch(reject);
 2916                    };
 2917                    reader.onerror = function () {
 2918                        reject(new Error('Ошибка при чтении файла'));
 2919                    };
 2920                    reader.readAsDataURL(file);
 2921                });
 2922                promises.push(filePromise);
 2923            }
 2924
 2925            Promise.all(promises)
 2926                .then(() => {
 2927                    btn.disabled = false;
 2928                    btn.innerHTML = '<i class="fas fa-paper-plane me-1"></i>Отправить';
 2929
 2930                    messageTextarea.value = '';
 2931                    document.getElementById('char-count').textContent = '0';
 2932                    clearAttachedFile();
 2933
 2934                    histOldestMessageId = null;
 2935                    histNewestMessageId = null;
 2936                    document.getElementById('hist-messages').innerHTML = '';
 2937                    document.getElementById('hist-spinner').classList.remove('d-none');
 2938                    loadHistory(histChatId, null);
 2939
 2940                    setTimeout(() => {
 02941                        fetch(`@Url.Action("MarkAsRead")`, {
 2942                            method: 'POST',
 2943                            headers: {'Content-Type': 'application/json'},
 2944                            body: JSON.stringify({
 2945                                clientId: parseInt(clientId),
 2946                                chatId: histChatId,
 2947                                fromMessageId: null
 2948                            })
 2949                        }).then(() => loadChats());
 2950                    }, 500);
 2951                })
 2952                .catch(err => {
 2953                    btn.disabled = false;
 2954                    btn.innerHTML = '<i class="fas fa-paper-plane me-1"></i>Отправить';
 2955                    showToastMessage(err.message || 'Ошибка при отправке', false);
 2956                });
 2957        };
 2958
 2959        const messageTextarea = document.getElementById('message-text');
 2960        if (messageTextarea) {
 2961            messageTextarea.addEventListener('input', () => {
 2962                document.getElementById('char-count').textContent = messageTextarea.value.length;
 2963            });
 2964
 2965            messageTextarea.addEventListener('keydown', (e) => {
 2966                if (e.key === 'Enter' && !e.shiftKey) {
 2967                    e.preventDefault();
 2968                    sendMessageOrFile();
 2969                }
 2970            });
 2971        }
 2972
 2973
 2974        const chatModalEl = document.getElementById('chatHistoryModal');
 2975
 2976        chatModalEl.addEventListener('shown.bs.modal', function () {
 2977            if (histChatId) {
 2978                loadHistory(histChatId, null);
 2979            }
 2980        });
 2981
 2982        // === СТАТИСТИКА ОТВЕТОВ ===
 2983        let statsCurrentPage = 1;
 2984        let timeCurrentPage = 1;
 2985        const statsPageSize = 50;
 2986
 2987        const btnLoadStats = document.getElementById('btn-load-stats');
 2988        const statSelClient = document.getElementById('stat-sel-client');
 2989        const statDateFrom = document.getElementById('stat-date-from');
 2990        const statDateTo = document.getElementById('stat-date-to');
 2991        const statSelManager = document.getElementById('stat-sel-manager');
 2992
 2993        if (btnLoadStats) {
 2994            btnLoadStats.addEventListener('click', () => {
 2995                loadResponseStats(1);
 2996                loadTimeStats(1);
 2997            });
 2998        }
 2999
 3000        const responseStatsTab = document.getElementById('response-stats-tab');
 3001        if (responseStatsTab) {
 3002            responseStatsTab.addEventListener('shown.bs.tab', () => {
 3003                const summaryDiv = document.getElementById('stats-summary');
 3004                if (summaryDiv && summaryDiv.classList.contains('d-none')) {
 3005                    loadResponseStats(1);
 3006                    loadTimeStats(1);
 3007                }
 3008            });
 3009        }
 3010
 3011        function loadResponseStats(page) {
 3012            statsCurrentPage = page;
 3013
 3014            const clientId = statSelClient?.value || 0;
 3015            const dateFrom = statDateFrom?.value || '';
 3016            const dateTo = statDateTo?.value || '';
 3017            const managerName = statSelManager?.value || '';
 3018
 3019            const loading = document.getElementById('stats-loading');
 3020            const empty = document.getElementById('stats-empty');
 3021            const tableWrap = document.getElementById('responses-table-wrap');
 3022            const tableCard = document.getElementById('responses-table-card');
 3023            const summaryDiv = document.getElementById('stats-summary');
 3024            const managersDiv = document.getElementById('managers-stats');
 3025
 3026            if (loading) loading.classList.remove('d-none');
 3027            if (empty) empty.classList.add('d-none');
 3028            if (tableWrap) tableWrap.classList.add('d-none');
 3029            if (tableCard) tableCard.classList.remove('d-none');
 3030
 03031            let url = `@Url.Action("GetResponseStatistics")?clientId=${clientId}&page=${page}&pageSize=${statsPageSize}`
 3032            if (dateFrom) url += `&from=${dateFrom}`;
 3033            if (dateTo) url += `&to=${dateTo}`;
 3034            if (managerName) url += `&responderName=${encodeURIComponent(managerName)}`;
 3035
 3036            fetch(url)
 3037                .then(r => r.json())
 3038                .then(data => {
 3039                    if (loading) loading.classList.add('d-none');
 3040
 3041                    if (!data.success) {
 3042                        alert(data.message || 'Ошибка загрузки статистики');
 3043                        return;
 3044                    }
 3045
 3046                    if (statSelManager && data.allResponders && data.allResponders.length > 0) {
 3047                        const currentVal = statSelManager.value;
 3048                        statSelManager.innerHTML = '<option value="" selected>Все менеджеры</option>' +
 3049                            data.allResponders.map(m => `<option value="${escapeHtml(m)}">${escapeHtml(m)}</option>`).jo
 3050                        if (currentVal) statSelManager.value = currentVal;
 3051                    }
 3052
 3053                    if (summaryDiv) {
 3054                        summaryDiv.classList.remove('d-none');
 3055                        document.getElementById('summary-total').textContent = data.summary.totalResponses;
 3056                        document.getElementById('summary-avg-time').textContent = data.summary.avgResponseTimeFormatted;
 3057                        document.getElementById('summary-period').textContent = `${data.summary.dateFrom} - ${data.summa
 3058                        document.getElementById('summary-managers').textContent = data.summary.responders?.length || 0;
 3059                    }
 3060
 3061                    if (managersDiv && data.summary.responders && data.summary.responders.length > 0) {
 3062                        managersDiv.classList.remove('d-none');
 3063                        const maxCount = Math.max(...data.summary.responders.map(r => r.count));
 3064                        const managersTbody = document.getElementById('managers-tbody');
 3065                        managersTbody.innerHTML = data.summary.responders.map(r => {
 3066                            const pct = Math.round((r.count / data.summary.totalResponses) * 100);
 3067                            const avgTime = formatSecondsJs(Math.round(r.avgResponseTime));
 3068                            return `
 3069                                <tr>
 3070                                    <td>
 3071                                        <i class="fas fa-user-circle text-secondary me-2"></i>
 3072                                        <strong>${escapeHtml(r.name)}</strong>
 3073                                    </td>
 3074                                    <td class="text-center">
 3075                                        <span class="badge badge-buyer">${r.count}</span>
 3076                                    </td>
 3077                                    <td class="text-center">
 3078                                        <span class="text-muted">${avgTime}</span>
 3079                                    </td>
 3080                                    <td>
 3081                                        <div class="progress progress-green" style="height: 20px;">
 3082                                            <div class="progress-bar progress-bar-green" role="progressbar"
 3083                                                 style="width: ${pct}%;" aria-valuenow="${pct}" aria-valuemin="0" aria-v
 3084                                                ${pct}%
 3085                                            </div>
 3086                                        </div>
 3087                                    </td>
 3088                                </tr>`;
 3089                        }).join('');
 3090                    } else if (managersDiv) {
 3091                        managersDiv.classList.add('d-none');
 3092                    }
 3093
 3094                    if (data.statistics && data.statistics.length > 0) {
 3095                        if (tableWrap) tableWrap.classList.remove('d-none');
 3096                        if (empty) empty.classList.add('d-none');
 3097
 3098                        document.getElementById('responses-count').textContent =
 3099                            `Показано ${data.statistics.length} из ${data.pagination.totalItems}`;
 3100
 3101                        const tbody = document.getElementById('responses-tbody');
 3102                        tbody.innerHTML = data.statistics.map(s => `
 3103                            <tr>
 3104                                <td>
 3105                                    <small class="text-muted">${s.sellerResponseDate || '—'}</small>
 3106                                </td>
 3107                                <td>
 3108                                    <span class="badge badge-closed">${escapeHtml(s.responderName)}</span>
 3109                                </td>
 3110                                <td>
 3111                                    <div class="text-truncate" style="max-width: 250px;"
 3112                                         title="${escapeHtml(s.sellerResponseText || '')}">
 3113                                        ${escapeHtml(s.sellerResponseText || '—')}
 3114                                    </div>
 3115                                </td>
 3116                                <td>
 3117                                    <span class="badge ${s.responseTimeSeconds > 3600 ? 'badge-other ' : 'badge-open '}"
 3118                                        ${s.responseTimeFormatted}
 3119                                    </span>
 3120                                </td>
 3121                                <td>
 3122
 3123                                    <a href="#" onclick="viewChat('${s.chatId}', ${s.clientId}); return false;"
 3124                                     class="link-chat d-inline-flex align-items-center gap-1">
 3125                                        <i class="fas fa-comments me-2"></i>${s.chatId?.substring(0, 12) || '—'}...
 3126                                    </a>
 3127                                </td>
 3128                            </tr>
 3129                        `).join('');
 3130
 3131                        renderStatsPagination(data.pagination);
 3132                    } else {
 3133                        if (tableWrap) tableWrap.classList.add('d-none');
 3134                        if (empty) empty.classList.remove('d-none');
 3135                    }
 3136                })
 3137                .catch(err => {
 3138                    if (loading) loading.classList.add('d-none');
 3139                    console.error('Ошибка загрузки статистики:', err);
 3140                    alert('Ошибка загрузки статистики');
 3141                });
 3142        }
 3143
 3144        function renderStatsPagination(pagination) {
 3145            const wrap = document.getElementById('stats-pagination-wrap');
 3146            const list = document.getElementById('stats-pagination-list');
 3147            const info = document.getElementById('stats-page-info');
 3148
 3149            if (!pagination || pagination.totalPages <= 1) {
 3150                if (wrap) wrap.classList.add('d-none');
 3151                return;
 3152            }
 3153
 3154            if (wrap) wrap.classList.remove('d-none');
 3155
 3156            const start = (pagination.currentPage - 1) * pagination.pageSize + 1;
 3157            const end = Math.min(pagination.currentPage * pagination.pageSize, pagination.totalItems);
 3158            if (info) info.textContent = `${start}-${end} из ${pagination.totalItems}`;
 3159
 3160            let html = '';
 3161
 3162            html += `<li class="page-item ${pagination.currentPage === 1 ? 'disabled' : ''}">
 3163                        <a class="page-link" href="#" onclick="loadResponseStats(${pagination.currentPage - 1}); return 
 3164                     </li>`;
 3165
 3166            const maxPages = 5;
 3167            let startPage = Math.max(1, pagination.currentPage - Math.floor(maxPages / 2));
 3168            let endPage = Math.min(pagination.totalPages, startPage + maxPages - 1);
 3169
 3170            if (endPage - startPage < maxPages - 1) {
 3171                startPage = Math.max(1, endPage - maxPages + 1);
 3172            }
 3173
 3174            for (let i = startPage; i <= endPage; i++) {
 3175                html += `<li class="page-item ${i === pagination.currentPage ? 'active' : ''}">
 3176                            <a class="page-link" href="#" onclick="loadResponseStats(${i}); return false;">${i}</a>
 3177                         </li>`;
 3178            }
 3179
 3180            html += `<li class="page-item ${pagination.currentPage === pagination.totalPages ? 'disabled' : ''}">
 3181                        <a class="page-link" href="#" onclick="loadResponseStats(${pagination.currentPage + 1}); return 
 3182                     </li>`;
 3183
 3184            if (list) list.innerHTML = html;
 3185        }
 3186
 3187        // === функции для статистики времени ===
 3188        function loadTimeStats(page) {
 3189            timeCurrentPage = page;
 3190
 3191            const clientId = statSelClient?.value || 0;
 3192            const dateFrom = statDateFrom?.value || '';
 3193            const dateTo = statDateTo?.value || '';
 3194
 3195            const loading = document.getElementById('time-loading');
 3196            const empty = document.getElementById('time-empty');
 3197            const tableWrap = document.getElementById('time-table-wrap');
 3198
 3199            if (loading) loading.classList.remove('d-none');
 3200            if (empty) empty.classList.add('d-none');
 3201            if (tableWrap) tableWrap.classList.add('d-none');
 3202
 03203            let url = `@Url.Action("GetResponseTimeStats")?clientId=${clientId}&page=${page}&pageSize=${statsPageSize}`;
 3204            if (dateFrom) url += `&from=${dateFrom}`;
 3205            if (dateTo) url += `&to=${dateTo}`;
 3206
 3207            fetch(url)
 3208                .then(r => r.json())
 3209                .then(data => {
 3210                    if (loading) loading.classList.add('d-none');
 3211                    if (!data.success) {
 3212                        alert(data.message || 'Ошибка загрузки времени');
 3213                        return;
 3214                    }
 3215                    if (data.items && data.items.length > 0) {
 3216                        if (tableWrap) tableWrap.classList.remove('d-none');
 3217                        if (empty) empty.classList.add('d-none');
 3218
 3219                        document.getElementById('time-page-info').textContent =
 3220                            `Показано ${data.items.length} из ${data.pagination.totalItems}`;
 3221
 3222                        // summary cards
 3223                        if (data.summary) {
 3224                            const sumDiv = document.getElementById('time-summary');
 3225                            if (sumDiv) sumDiv.classList.remove('d-none');
 3226                            const totRec = document.getElementById('time-summary-total');
 3227                            const totTime = document.getElementById('time-summary-totaltime');
 3228                            if (totRec) totRec.textContent = data.summary.totalRecords;
 3229                            if (totTime) {
 3230                                const tmin = data.summary.totalMinutes || 0;
 3231                                const h = Math.floor(tmin / 60);
 3232                                const m = tmin % 60;
 3233                                totTime.textContent = h ? `${h} ч ${m} мин` : `${m} мин`;
 3234                            }
 3235                        }
 3236
 3237                        const tbody = document.getElementById('time-tbody');
 3238                        tbody.innerHTML = data.items.map(t => {
 3239                            const h = Math.floor(t.minutes / 60);
 3240                            const m = t.minutes % 60;
 3241                            const hm = h ? `${h} ч ${m} мин` : `${m} мин`;
 3242                            return `
 3243                            <tr>
 3244                                <td><small class="text-muted">${t.recordedAt}</small></td>
 3245                                <td><span class="badge bg-secondary">${escapeHtml(t.userName)}</span></td>
 3246                                <td>${hm}</td>
 3247                                <td><a href="#" onclick="viewChat('${t.chatId}', ${t.clientId}); return false;" class="t
 3248                                <td>${escapeHtml(t.clientName || '')}</td>
 3249                            </tr>`;
 3250                        }).join('');
 3251
 3252                        renderTimeStatsPagination(data.pagination);
 3253                    } else {
 3254                        if (tableWrap) tableWrap.classList.add('d-none');
 3255                        if (empty) empty.classList.remove('d-none');
 3256                    }
 3257                })
 3258                .catch(err => {
 3259                    if (loading) loading.classList.add('d-none');
 3260                    console.error('Ошибка загрузки времени:', err);
 3261                    alert('Ошибка загрузки времени');
 3262                });
 3263        }
 3264
 3265        function renderTimeStatsPagination(pagination) {
 3266            const wrap = document.getElementById('time-pagination-wrap');
 3267            const list = document.getElementById('time-pagination-list');
 3268            const info = document.getElementById('time-page-info');
 3269
 3270            if (!pagination || pagination.totalPages <= 1) {
 3271                if (wrap) wrap.classList.add('d-none');
 3272                return;
 3273            }
 3274            if (wrap) wrap.classList.remove('d-none');
 3275            const start = (pagination.currentPage - 1) * pagination.pageSize + 1;
 3276            const end = Math.min(pagination.currentPage * pagination.pageSize, pagination.totalItems);
 3277            if (info) info.textContent = `${start}-${end} из ${pagination.totalItems}`;
 3278            let html = '';
 3279            html += `<li class="page-item ${pagination.currentPage === 1 ? 'disabled' : ''}">
 3280                        <a class="page-link" href="#" onclick="loadTimeStats(${pagination.currentPage - 1}); return fals
 3281                     </li>`;
 3282            const maxPages = 5;
 3283            let startPage = Math.max(1, pagination.currentPage - Math.floor(maxPages / 2));
 3284            let endPage = Math.min(pagination.totalPages, startPage + maxPages - 1);
 3285            if (endPage - startPage < maxPages - 1) {
 3286                startPage = Math.max(1, endPage - maxPages + 1);
 3287            }
 3288            for (let i = startPage; i <= endPage; i++) {
 3289                html += `<li class="page-item ${i === pagination.currentPage ? 'active' : ''}">
 3290                            <a class="page-link" href="#" onclick="loadTimeStats(${i}); return false;">${i}</a>
 3291                         </li>`;
 3292            }
 3293            html += `<li class="page-item ${pagination.currentPage === pagination.totalPages ? 'disabled' : ''}">
 3294                        <a class="page-link" href="#" onclick="loadTimeStats(${pagination.currentPage + 1}); return fals
 3295                     </li>`;
 3296            if (list) list.innerHTML = html;
 3297        }
 3298
 3299        function formatSecondsJs(totalSeconds) {
 3300            if (!totalSeconds || totalSeconds <= 0) return '0 сек';
 3301            if (totalSeconds < 60) return totalSeconds + ' сек';
 3302            if (totalSeconds < 3600) return Math.floor(totalSeconds / 60) + ' мин';
 3303            const hours = Math.floor(totalSeconds / 3600);
 3304            const minutes = Math.floor((totalSeconds % 3600) / 60);
 3305            return hours + ' ч ' + minutes + ' мин';
 3306        }
 3307
 3308        window.loadResponseStats = loadResponseStats;
 3309
 3310        // === Авто-загрузка при открытии страницы ===
 3311        const urlParams = new URLSearchParams(window.location.search);
 3312        const urlChatId = urlParams.get('chatId');
 3313        const urlClientId = urlParams.get('clientId');
 3314
 3315        if (urlChatId && urlClientId && selClient) {
 3316            selClient.value = urlClientId;
 3317            loadChats();
 3318            setTimeout(() => {
 3319                viewChat(urlChatId, urlClientId);
 3320            }, 500);
 3321        } else {
 3322            loadChats();
 3323        }
 3324    })();
 3325</script>