| | | 1 | | @using OzonDomains.Models.BitrixModels |
| | | 2 | | @model OzonOrdersWeb.Areas.Studio2.ViewModels.Bitrix.RemainingStockViewModel |
| | | 3 | | <link rel="stylesheet" href="~/css/Studio2/BitrixStock/bitrix-stock.css" asp-append-version="true"/> |
| | | 4 | | <link rel="stylesheet" href="~/css/Studio2/shared/quick-links-chip.css" asp-append-version="true"/> |
| | 0 | 5 | | @await Html.PartialAsync("_BsxGridShell") |
| | | 6 | | <link rel="stylesheet" href="~/css/Studio2/BitrixStock/bitrix-stock-grid.css" asp-append-version="true"/> |
| | | 7 | | @{ |
| | 0 | 8 | | var isSyncTab = string.Equals(Model.ActiveTab, "sync", StringComparison.OrdinalIgnoreCase); |
| | 0 | 9 | | var isExceptionsTab = string.Equals(Model.ActiveTab, "exceptions", StringComparison.OrdinalIgnoreCase); |
| | 0 | 10 | | var isClearHistoryTab = string.Equals(Model.ActiveTab, "clearHistory", StringComparison.OrdinalIgnoreCase); |
| | 0 | 11 | | var isPushHistoryTab = string.Equals(Model.ActiveTab, "pushHistory", StringComparison.OrdinalIgnoreCase); |
| | 0 | 12 | | var isStocksTab = !isSyncTab && !isExceptionsTab && !isClearHistoryTab && !isPushHistoryTab; |
| | 0 | 13 | | ViewData["Title"] = isSyncTab |
| | 0 | 14 | | ? "Синхронизация остатков 1С → Ozon (отчёт)" |
| | 0 | 15 | | : isExceptionsTab |
| | 0 | 16 | | ? "Справочник исключений из синхронизации" |
| | 0 | 17 | | : isClearHistoryTab |
| | 0 | 18 | | ? "Запуски обнуления остатков Ozon" |
| | 0 | 19 | | : isPushHistoryTab |
| | 0 | 20 | | ? "Запуски наката остатков Ozon" |
| | 0 | 21 | | : "Остатки 1С → Ozon"; |
| | | 22 | | |
| | | 23 | | } |
| | | 24 | | |
| | | 25 | | @functions |
| | | 26 | | { |
| | | 27 | | |
| | | 28 | | |
| | | 29 | | |
| | | 30 | | string BuildStocksTabUrl() => |
| | 0 | 31 | | Url.Action("Index", "BitrixStock", new { area = "Studio2" })!; |
| | | 32 | | |
| | | 33 | | string BuildClearHistoryTabUrl() => |
| | 0 | 34 | | Url.Action("Index", "BitrixStock", new { area = "Studio2", tab = "clearHistory" })!; |
| | | 35 | | |
| | | 36 | | string BuildPushHistoryTabUrl() => |
| | 0 | 37 | | Url.Action("Index", "BitrixStock", new { area = "Studio2", tab = "pushHistory" })!; |
| | | 38 | | |
| | | 39 | | string BuildJobLogsUrl() => |
| | 0 | 40 | | Url.Action("Index", "JobLogs", new { area = "Studio2", tab = "transactions" })!; |
| | | 41 | | |
| | | 42 | | string BuildSyncTabUrl() => |
| | 0 | 43 | | Url.Action("Index", "BitrixStock", new { area = "Studio2", tab = "sync" })!; |
| | | 44 | | |
| | | 45 | | string BuildExceptionsTabUrl() => |
| | 0 | 46 | | Url.Action("Index", "BitrixStock", new { area = "Studio2", tab = "exceptions" })!; |
| | | 47 | | |
| | | 48 | | } |
| | | 49 | | |
| | | 50 | | <script src="~/js/Studio2/Orders/CreateOrderToSupplierTransactionV2/generateInfoRow.js"></script> |
| | | 51 | | |
| | | 52 | | <div id="loadingSpinner" class="spinner-overlay" style="display: none;"> |
| | | 53 | | <div class="spinner-content"> |
| | | 54 | | <div class="spinner-border text-white" role="status"> |
| | | 55 | | <span class="sr-only">Загрузка...</span> |
| | | 56 | | </div> |
| | | 57 | | <div id="loadingSpinnerText" class="spinner-loading-text">Загрузка...</div> |
| | | 58 | | </div> |
| | | 59 | | </div> |
| | | 60 | | |
| | | 61 | | <!-- Модальное окно для сохранения фильтра --> |
| | | 62 | | <div class="modal fade" id="saveFilterModal" tabindex="-1" aria-labelledby="saveFilterModalLabel" aria-hidden="true"> |
| | | 63 | | <div class="modal-dialog"> |
| | | 64 | | <div class="modal-content"> |
| | | 65 | | <div class="modal-header"> |
| | | 66 | | <h5 class="modal-title" id="saveFilterModalLabel"> |
| | | 67 | | <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" |
| | | 68 | | class="bi bi-bookmark-plus me-2" viewBox="0 0 16 16"> |
| | | 69 | | <path |
| | | 70 | | d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v13.5a.5.5 0 0 1-.777.416L8 13.101l-5.223 2.815A.5.5 0 |
| | | 71 | | <path |
| | | 72 | | d="M8 4a.5.5 0 0 1 .5.5V6H10a.5.5 0 0 1 0 1H8.5v1.5a.5.5 0 0 1-1 0V7H6a.5.5 0 0 1 0-1h1.5V4. |
| | | 73 | | </svg> |
| | | 74 | | Сохранить фильтр |
| | | 75 | | </h5> |
| | | 76 | | <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
| | | 77 | | </div> |
| | | 78 | | <div class="modal-body"> |
| | | 79 | | <div class="mb-3"> |
| | | 80 | | <label for="filterNameInput" class="form-label">Название фильтра</label> |
| | | 81 | | <input type="text" class="form-control" id="filterNameInput" |
| | | 82 | | placeholder="Например: Товары со склада Москва" autofocus> |
| | | 83 | | <div class="form-text small">Укажите понятное название</div> |
| | | 84 | | <div id="filterNameError" class="invalid-feedback" style="display: none;"> |
| | | 85 | | Введите название фильтра |
| | | 86 | | </div> |
| | | 87 | | </div> |
| | | 88 | | </div> |
| | | 89 | | <div class="modal-footer"> |
| | | 90 | | <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button> |
| | | 91 | | <button type="button" class="btn btn-primary" id="confirmSaveFilterBtn"> |
| | | 92 | | <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" |
| | | 93 | | class="bi bi-check-lg me-1" viewBox="0 0 16 16"> |
| | | 94 | | <path |
| | | 95 | | d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.0 |
| | | 96 | | </svg> |
| | | 97 | | Сохранить |
| | | 98 | | </button> |
| | | 99 | | </div> |
| | | 100 | | </div> |
| | | 101 | | </div> |
| | | 102 | | </div> |
| | | 103 | | |
| | | 104 | | <div class="modal fade" id="bitrixModal" tabindex="-1" aria-labelledby="bitrixModalLabel" aria-hidden="true"> |
| | | 105 | | <div class="modal-dialog modal-fullscreen modal-dialog-centered modal-dialog-scrollable"> |
| | | 106 | | <div class="modal-content"> |
| | | 107 | | <div class="modal-header"> |
| | | 108 | | <h5 class="modal-title" id="bitrixModalLabel">Данные Bitrix</h5> |
| | | 109 | | <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button> |
| | | 110 | | </div> |
| | | 111 | | <div class="modal-body"> |
| | | 112 | | <div id="bitrixModalContent"> |
| | | 113 | | <div class="d-flex justify-content-center align-items-center" style="height:200px;"> |
| | | 114 | | <div class="spinner-border text-primary" role="status"> |
| | | 115 | | <span class="visually-hidden">Загрузка...</span> |
| | | 116 | | </div> |
| | | 117 | | </div> |
| | | 118 | | </div> |
| | | 119 | | |
| | | 120 | | <!-- Блок для данных Bitrix --> |
| | | 121 | | <div id="bitrixDataSection" class="mb-4" style="display: none;"> |
| | | 122 | | <div id="bitrixTableContainer"></div> |
| | | 123 | | </div> |
| | | 124 | | |
| | | 125 | | <div class="mt-4"> |
| | | 126 | | <strong class="bs-modal-section-title">История цен</strong> |
| | | 127 | | <form id="priceHistoryForm" class="bs-price-form"> |
| | | 128 | | <div class="bs-price-field"> |
| | | 129 | | <label class="form-label" for="modalBitrixId">Bitrix ID</label> |
| | | 130 | | <input name="BitrixId" class="form-control form-control-sm" id="modalBitrixId"/> |
| | | 131 | | <span class="text-danger small" id="bitrixIdError"></span> |
| | | 132 | | </div> |
| | | 133 | | <div class="bs-price-field"> |
| | | 134 | | <label class="form-label" for="modalArticle">Артикул</label> |
| | | 135 | | <input name="Article" class="form-control form-control-sm" id="modalArticle" readonly/> |
| | | 136 | | <span class="text-danger small" id="articleError"></span> |
| | | 137 | | </div> |
| | | 138 | | <div class="bs-price-field"> |
| | | 139 | | <label class="form-label">Цена</label> |
| | | 140 | | <input name="PriceValue" class="form-control form-control-sm text-end" value="" |
| | | 141 | | type="number" step="0.01" placeholder="0.00"/> |
| | | 142 | | <span class="text-danger small" id="priceValueError"></span> |
| | | 143 | | <input type="hidden" name="Currency" value="RUB"> |
| | | 144 | | <input type="hidden" name="RegistrationPrice" value=""/> |
| | | 145 | | </div> |
| | | 146 | | <div class="bs-price-field bs-price-field--wide"> |
| | | 147 | | <label class="form-label">Источник</label> |
| | | 148 | | <input list="suppliersList" name="ValueSource" class="form-control form-control-sm" placehol |
| | | 149 | | <datalist id="suppliersList"> |
| | 0 | 150 | | @foreach (var supplier in Model.AllSuppliers) |
| | | 151 | | { |
| | 0 | 152 | | <option value="@supplier.Name" data-url="@supplier.Site"></option> |
| | | 153 | | } |
| | | 154 | | </datalist> |
| | | 155 | | <span class="text-danger small" id="valueSourceError"></span> |
| | | 156 | | </div> |
| | | 157 | | <div class="bs-price-field bs-price-field--wide"> |
| | | 158 | | <label class="form-label">Автор</label> |
| | | 159 | | <input name="Author" class="form-control form-control-sm" placeholder="Автор" value="@User.I |
| | | 160 | | <span class="text-danger small" id="authorError"></span> |
| | | 161 | | </div> |
| | | 162 | | <div class="bs-price-field"> |
| | | 163 | | <label class="form-label" for="modalCreateDateTime">Дата создания</label> |
| | | 164 | | <input name="CreateDateTime" class="form-control form-control-sm" type="datetime-local" id=" |
| | | 165 | | <span class="text-danger small" id="createDateTimeError"></span> |
| | | 166 | | </div> |
| | | 167 | | <div class="bs-price-actions"> |
| | | 168 | | <button type="submit" class="btn btn-success btn-sm"><i class="fas fa-plus-circle me-1"></i> |
| | | 169 | | <button type="reset" class="btn btn-outline-secondary btn-sm"><i class="fas fa-times-circle |
| | | 170 | | </div> |
| | | 171 | | </form> |
| | | 172 | | </div> |
| | | 173 | | |
| | | 174 | | <!-- Блок для истории цен --> |
| | | 175 | | <div id="priceHistorySection" style="display: none;"> |
| | | 176 | | <div class="d-flex justify-content-center"> |
| | | 177 | | <div class="spinner-border spinner-border-sm text-primary" id="historySpinner" role="status"> |
| | | 178 | | <span class="visually-hidden">Загрузка истории...</span> |
| | | 179 | | </div> |
| | | 180 | | </div> |
| | | 181 | | <div id="priceHistoryTableContainer" class="mt-2"></div> |
| | | 182 | | </div> |
| | | 183 | | |
| | | 184 | | <!-- Блок для данных Ozon --> |
| | | 185 | | <div id="ozonDataSection" class="mb-4" style="display: none;"> |
| | | 186 | | <div id="ozonTableContainer"></div> |
| | | 187 | | </div> |
| | | 188 | | </div> |
| | | 189 | | </div> |
| | | 190 | | </div> |
| | | 191 | | </div> |
| | | 192 | | |
| | | 193 | | |
| | | 194 | | <div class="container-fluid py-3 bsx-page bsx-surface @(isSyncTab ? "bsx-page--sync" : "")"> |
| | | 195 | | <div class="bsx-hero d-flex flex-wrap justify-content-between align-items-start gap-3"> |
| | | 196 | | <div> |
| | | 197 | | <h1> |
| | 0 | 198 | | @if (isSyncTab) |
| | | 199 | | { |
| | | 200 | | <i class="fas fa-exchange-alt me-2 text-primary"></i>@:Синхронизация 1С → Ozon |
| | | 201 | | } |
| | 0 | 202 | | else if (isExceptionsTab) |
| | | 203 | | { |
| | | 204 | | <i class="fas fa-ban me-2 text-warning"></i>@:Исключения из синхронизации |
| | | 205 | | } |
| | 0 | 206 | | else if (isClearHistoryTab) |
| | | 207 | | { |
| | | 208 | | <i class="fas fa-eraser me-2 text-danger"></i>@:Обнуление остатков Ozon |
| | | 209 | | } |
| | 0 | 210 | | else if (isPushHistoryTab) |
| | | 211 | | { |
| | | 212 | | <i class="fas fa-cloud-upload-alt me-2 text-success"></i>@:Накат остатков в Ozon |
| | | 213 | | } |
| | | 214 | | else |
| | | 215 | | { |
| | | 216 | | <i class="fas fa-warehouse me-2 text-primary"></i>@:Остатки 1С |
| | | 217 | | } |
| | | 218 | | </h1> |
| | | 219 | | <div class="subtitle"> |
| | 0 | 220 | | @if (isSyncTab) |
| | | 221 | | { |
| | | 222 | | @:Отчёты, накат и очистка остатков на маркетплейсе |
| | | 223 | | } |
| | 0 | 224 | | else if (isExceptionsTab) |
| | | 225 | | { |
| | | 226 | | @:Артикулы, правила цен и остатков — управление исключениями |
| | | 227 | | } |
| | 0 | 228 | | else if (isClearHistoryTab) |
| | | 229 | | { |
| | | 230 | | @:Запуски обнуления остатков Ozon — ручные и по расписанию |
| | | 231 | | } |
| | 0 | 232 | | else if (isPushHistoryTab) |
| | | 233 | | { |
| | | 234 | | @:Запуски наката остатков в Ozon — ручные и по расписанию |
| | | 235 | | } |
| | | 236 | | else |
| | | 237 | | { |
| | | 238 | | @:Сравнение snapshot WarehouseStock с остатками Ozon |
| | | 239 | | } |
| | | 240 | | </div> |
| | | 241 | | <div class="bsx-quick-links"> |
| | | 242 | | <a asp-area="Studio2" asp-controller="WarehouseMapping" asp-action="Index"> |
| | | 243 | | <i class="fas fa-link"></i>Связи складов Ozon ↔ 1С |
| | | 244 | | </a> |
| | | 245 | | <a asp-area="Studio2" asp-controller="Warehouse" asp-action="Index"> |
| | | 246 | | <i class="fas fa-warehouse"></i>Склады |
| | | 247 | | </a> |
| | | 248 | | <a asp-area="Studio2" asp-controller="OzonClients" asp-action="Index"> |
| | | 249 | | <i class="fas fa-store"></i>Клиенты Ozon |
| | | 250 | | </a> |
| | | 251 | | <a asp-area="Studio2" asp-controller="Job" asp-action="Index"> |
| | | 252 | | <i class="fas fa-tasks"></i>Фоновые задания |
| | | 253 | | </a> |
| | | 254 | | </div> |
| | | 255 | | </div> |
| | 0 | 256 | | @if (isStocksTab) |
| | | 257 | | { |
| | | 258 | | <div class="bsx-stat"> |
| | | 259 | | <div class="label">Найдено</div> |
| | | 260 | | <div class="value" id="bsxStocksTotal">…</div> |
| | | 261 | | </div> |
| | | 262 | | } |
| | | 263 | | </div> |
| | | 264 | | |
| | | 265 | | <ul class="nav bsx-tabs" id="bitrixStockPageTabs" role="tablist"> |
| | | 266 | | <li class="nav-item" role="presentation"> |
| | | 267 | | <a class="nav-link @(isSyncTab ? "active" : "")" href="@BuildSyncTabUrl()"> |
| | | 268 | | <i class="fas fa-exchange-alt me-1"></i>Синхронизация |
| | | 269 | | </a> |
| | | 270 | | </li> |
| | | 271 | | <li class="nav-item" role="presentation"> |
| | | 272 | | <a class="nav-link @(isStocksTab ? "active" : "")" href="@BuildStocksTabUrl()"> |
| | | 273 | | <i class="fas fa-warehouse me-1"></i>Остатки (WarehouseStock) |
| | | 274 | | </a> |
| | | 275 | | </li> |
| | | 276 | | <li class="nav-item" role="presentation"> |
| | | 277 | | <a class="nav-link @(isExceptionsTab ? "active" : "")" href="@BuildExceptionsTabUrl()"> |
| | | 278 | | <i class="fas fa-ban me-1"></i>Исключения |
| | | 279 | | </a> |
| | | 280 | | </li> |
| | | 281 | | <li class="nav-item" role="presentation"> |
| | | 282 | | <a class="nav-link @(isClearHistoryTab ? "active" : "")" href="@BuildClearHistoryTabUrl()" |
| | | 283 | | title="История запусков обнуления остатков Ozon"> |
| | | 284 | | <i class="fas fa-eraser me-1"></i>Обнуление |
| | | 285 | | <span class="bsx-tab-badge badge rounded-pill" data-job-badge="clear"></span> |
| | | 286 | | </a> |
| | | 287 | | </li> |
| | | 288 | | <li class="nav-item" role="presentation"> |
| | | 289 | | <a class="nav-link @(isPushHistoryTab ? "active" : "")" href="@BuildPushHistoryTabUrl()" |
| | | 290 | | title="История запусков наката остатков в Ozon"> |
| | | 291 | | <i class="fas fa-cloud-upload-alt me-1"></i>Накат |
| | | 292 | | <span class="bsx-tab-badge badge rounded-pill" data-job-badge="push"></span> |
| | | 293 | | </a> |
| | | 294 | | </li> |
| | | 295 | | <li class="nav-item" role="presentation"> |
| | | 296 | | <a class="nav-link" href="@BuildJobLogsUrl()" title="Журнал фоновых транзакций, интеграций и диагностики"> |
| | | 297 | | <i class="fas fa-clipboard-list me-1"></i>Журнал задач |
| | | 298 | | </a> |
| | | 299 | | </li> |
| | | 300 | | </ul> |
| | | 301 | | |
| | 0 | 302 | | @await Html.PartialAsync("_BitrixStockJobProgressModals") |
| | 0 | 303 | | @await Html.PartialAsync("_BitrixStockFilterCapture") |
| | | 304 | | |
| | 0 | 305 | | @if (isSyncTab) |
| | | 306 | | { |
| | 0 | 307 | | @await Html.PartialAsync("_SyncReportTab", Model) |
| | | 308 | | } |
| | | 309 | | |
| | 0 | 310 | | @if (isExceptionsTab) |
| | | 311 | | { |
| | 0 | 312 | | @await Html.PartialAsync("_ExceptionsTab", Model) |
| | | 313 | | } |
| | | 314 | | |
| | 0 | 315 | | @if (isClearHistoryTab || isPushHistoryTab) |
| | | 316 | | { |
| | 0 | 317 | | @if (isClearHistoryTab) |
| | | 318 | | { |
| | 0 | 319 | | @await Html.PartialAsync("_BitrixClearHistoryTab") |
| | | 320 | | } |
| | | 321 | | else |
| | | 322 | | { |
| | 0 | 323 | | @await Html.PartialAsync("_BitrixPushHistoryTab") |
| | | 324 | | } |
| | | 325 | | <script> |
| | | 326 | | window.bitrixJobHistoryConfig = { |
| | 0 | 327 | | clearJobsListUrl: '@Url.Action("GetClearJobs", "BitrixStock", new { area = "Studio2" })', |
| | 0 | 328 | | pushJobsListUrl: '@Url.Action("GetPushJobs", "BitrixStock", new { area = "Studio2" })' |
| | | 329 | | }; |
| | 0 | 330 | | window.jobLogsBadgeChannel = '@(isClearHistoryTab ? "clear" : "push")'; |
| | | 331 | | </script> |
| | | 332 | | <script src="~/js/Studio2/BitrixStock/bitrix-job-history.js" asp-append-version="true"></script> |
| | | 333 | | } |
| | | 334 | | |
| | 0 | 335 | | @if (isStocksTab) |
| | | 336 | | { |
| | | 337 | | <div class="alert alert-info py-2 mb-3"> |
| | | 338 | | Источник остатков на этой вкладке: <strong>Studio2/WarehouseStock</strong> (синхронизированный snapshot), а не п |
| | | 339 | | </div> |
| | | 340 | | @* Отчёт авто-синка связок Ozon больше не рисуется здесь: строки грузит грид (stocks-grid), авто-push отключён. *@ |
| | 0 | 341 | | @if (ViewData["PerformanceNotice"] is string performanceNotice && !string.IsNullOrWhiteSpace(performanceNotice)) |
| | | 342 | | { |
| | | 343 | | <div class="alert alert-warning py-2 mb-3"> |
| | 0 | 344 | | @performanceNotice |
| | | 345 | | </div> |
| | | 346 | | } |
| | | 347 | | <div class="alert alert-warning py-2 mb-3" id="bsxStocksNotice" hidden></div> |
| | | 348 | | <script> |
| | | 349 | | Object.assign(window.bsxGridConfig, { |
| | 0 | 350 | | stocksUrl: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Url.Action("GetStocksGrid", "BitrixStock", ne |
| | 0 | 351 | | stocksActiveStores: @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.ActiveStores)) |
| | | 352 | | }); |
| | | 353 | | </script> |
| | | 354 | | <script src="~/js/Studio2/BitrixStock/SyncGrid/bsx-stocks.js" asp-append-version="true"></script> |
| | | 355 | | <div class="bsx-card" id="bitrixStocksCard"> |
| | | 356 | | <div class="bsx-card-header bsx-grid-toolbar"> |
| | | 357 | | <div class="fw-semibold"><i class="fas fa-table me-1 text-primary"></i>Остатки WarehouseStock</div> |
| | | 358 | | <div class="bsx-grid-search bsx-grid-search--wide"> |
| | | 359 | | <i class="fas fa-search"></i> |
| | | 360 | | <input type="search" id="bsxStocksSearch" placeholder="Артикул (Enter — по всему snapshot), Ctrl+F" auto |
| | | 361 | | </div> |
| | | 362 | | <div class="bsx-grid-toolbar-end"> |
| | | 363 | | <span class="bsx-grid-count" id="bsxStocksCount"></span> |
| | | 364 | | <button type="button" class="btn btn-sm btn-outline-secondary" id="bsxStocksRefreshBtn" title="Перечитат |
| | | 365 | | <button type="button" class="btn btn-sm btn-outline-secondary" id="bsxStocksColumnsBtn" data-bsx-popup-a |
| | | 366 | | </div> |
| | | 367 | | </div> |
| | | 368 | | <div class="card-body p-2"> |
| | | 369 | | @* Форма — носитель параметров RemainingStockFilter (как у старой страницы: ссылки, сохранённые фильтры). |
| | | 370 | | Фильтры колонок и сортировка грида пишутся в query, панель ниже — прежние флаги и массовые действия. *@ |
| | | 371 | | <form method="get" id="filterForm" class="bsx-stocks-toolbar"> |
| | | 372 | | <div class="row mt-2 g-2 mb-3"> |
| | | 373 | | <!-- Чекбоксы Ozon --> |
| | | 374 | | <div class="col-auto"> |
| | | 375 | | <div class="d-flex flex-column border rounded p-2 bg-light"> |
| | | 376 | | <div class="form-check"> |
| | | 377 | | <input type="checkbox" class="form-check-input" id="LoadOzonWarehouses" |
| | | 378 | | name="LoadOzonWarehouses" |
| | | 379 | | value="true" |
| | 0 | 380 | | @(Model.Filter.LoadOzonWarehouses ? "checked" : "")/> |
| | | 381 | | <label class="form-check-label small" for="LoadOzonWarehouses"> |
| | | 382 | | Склады Ozon |
| | | 383 | | </label> |
| | | 384 | | </div> |
| | | 385 | | <div class="form-check mt-1"> |
| | | 386 | | <input type="checkbox" class="form-check-input" id="DiffOzonWarehouses" |
| | | 387 | | name="DiffOzonWarehouses" |
| | | 388 | | value="true" |
| | 0 | 389 | | @(Model.Filter.DiffOzonWarehouses ? "checked" : "")/> |
| | | 390 | | <label class="form-check-label small" for="DiffOzonWarehouses"> |
| | | 391 | | Разница остатков |
| | | 392 | | </label> |
| | | 393 | | </div> |
| | | 394 | | </div> |
| | | 395 | | </div> |
| | | 396 | | |
| | | 397 | | <div class="col-auto"> |
| | | 398 | | <div class="d-flex flex-column border rounded p-2 bg-light"> |
| | | 399 | | <div class="form-check"> |
| | | 400 | | <input type="checkbox" class="form-check-input" id="LoadOzonPriceIndexes" |
| | | 401 | | name="LoadOzonPriceIndexes" |
| | | 402 | | value="true" |
| | 0 | 403 | | @(Model.Filter.LoadOzonPriceIndexes ? "checked" : "")/> |
| | | 404 | | <label class="form-check-label small" for="LoadOzonPriceIndexes"> |
| | | 405 | | Индексы цен Ozon |
| | | 406 | | </label> |
| | | 407 | | </div> |
| | | 408 | | <div class="form-check mt-1"> |
| | | 409 | | <input type="checkbox" class="form-check-input" id="RedOzonPriceIndexes" |
| | | 410 | | name="RedOzonPriceIndexes" |
| | | 411 | | value="true" |
| | 0 | 412 | | @(Model.Filter.RedOzonPriceIndexes ? "checked" : "")/> |
| | | 413 | | <label class="form-check-label small" for="RedOzonPriceIndexes"> |
| | | 414 | | Только невыгодные |
| | | 415 | | </label> |
| | | 416 | | </div> |
| | | 417 | | </div> |
| | | 418 | | </div> |
| | | 419 | | |
| | | 420 | | <!-- Клиент Ozon --> |
| | | 421 | | <div class="col-auto"> |
| | | 422 | | <div class="border rounded p-2 bg-light"> |
| | | 423 | | <label for="SelectedClientId" class="form-label small mb-1">Клиент Ozon</label> |
| | | 424 | | <select name="SelectedClientId" class="form-select form-select-sm" |
| | | 425 | | style="width: 180px;" data-filter-col="client"> |
| | | 426 | | <option value="">-- все клиенты --</option> |
| | 0 | 427 | | @foreach (var client in Model.OzonClients) |
| | | 428 | | { |
| | 0 | 429 | | <option value="@client.Id" |
| | | 430 | | selected="@(Model.SelectedClient?.Id == client.Id ? "selected" : null)"> |
| | 0 | 431 | | @client.Name |
| | | 432 | | </option> |
| | | 433 | | } |
| | | 434 | | </select> |
| | | 435 | | </div> |
| | | 436 | | </div> |
| | | 437 | | |
| | | 438 | | <!-- Массовые действия --> |
| | | 439 | | <div class="col-auto" id="refreshSelectedContainer" style="display: none;"> |
| | | 440 | | <div class="border rounded p-2 bg-light"> |
| | | 441 | | <button type="button" class="btn btn-sm btn-primary" id="refreshSelectedBtn"> |
| | | 442 | | <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" |
| | | 443 | | class="bi bi-arrow-clockwise me-1" viewBox="0 0 16 16"> |
| | | 444 | | <path fill-rule="evenodd" |
| | | 445 | | d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2z"/> |
| | | 446 | | <path |
| | | 447 | | d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 |
| | | 448 | | </svg> |
| | | 449 | | Обновить (<span id="refreshSelectedCount">0</span>) |
| | | 450 | | </button> |
| | | 451 | | </div> |
| | | 452 | | </div> |
| | | 453 | | |
| | | 454 | | <div class="col-auto" id="clearSelectedContainer" style="display: none;"> |
| | | 455 | | <div class="border rounded p-2 bg-light"> |
| | | 456 | | <button type="button" class="btn btn-sm btn-danger" id="clearSelectedBtn"> |
| | | 457 | | <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" |
| | | 458 | | class="bi bi-dash-circle me-1" viewBox="0 0 16 16"> |
| | | 459 | | <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/> |
| | | 460 | | <path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8"/> |
| | | 461 | | </svg> |
| | | 462 | | Обнулить (<span id="clearSelectedCount">0</span>) |
| | | 463 | | </button> |
| | | 464 | | </div> |
| | | 465 | | </div> |
| | | 466 | | |
| | | 467 | | <div class="col-auto" id="archiveSelectedContainer" style="display: none;"> |
| | | 468 | | <div class="border rounded p-2 bg-light"> |
| | | 469 | | <button type="button" class="btn btn-sm btn-secondary" id="archiveSelectedBtn" |
| | | 470 | | title="Отправить выбранные товары в архив Ozon (не заархивированные)"> |
| | | 471 | | <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" |
| | | 472 | | class="bi bi-archive me-1" viewBox="0 0 16 16"> |
| | | 473 | | <path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2. |
| | | 474 | | <path d="M3 7.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5M3.854 1.146a.5.5 0 0 1 |
| | | 475 | | </svg> |
| | | 476 | | В архив (<span id="archiveSelectedCount">0</span>) |
| | | 477 | | </button> |
| | | 478 | | </div> |
| | | 479 | | </div> |
| | | 480 | | |
| | | 481 | | <div class="col-auto" id="unarchiveSelectedContainer" style="display: none;"> |
| | | 482 | | <div class="border rounded p-2 bg-light"> |
| | | 483 | | <button type="button" class="btn btn-sm btn-success" id="unarchiveSelectedBtn" |
| | | 484 | | title="Восстановить выбранные товары из архива Ozon"> |
| | | 485 | | <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" |
| | | 486 | | class="bi bi-box-arrow-up me-1" viewBox="0 0 16 16"> |
| | | 487 | | <path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.772 0L3.732 7.5H6.5a.5.5 0 0 0 0 1H2a.5. |
| | | 488 | | <path fill-rule="evenodd" d="M1.5 8.5a.5.5 0 0 1 .5-.5h12a.5.5 0 0 1 0 1H14v5.5a.5.5 0 0 1-. |
| | | 489 | | </svg> |
| | | 490 | | Из архива (<span id="unarchiveSelectedCount">0</span>) |
| | | 491 | | </button> |
| | | 492 | | </div> |
| | | 493 | | </div> |
| | | 494 | | |
| | | 495 | | <!-- Кнопка сброса --> |
| | | 496 | | <div class="col-auto"> |
| | | 497 | | <div class="border rounded p-2 bg-light"> |
| | | 498 | | <button type="button" class="btn btn-sm btn-outline-secondary" id="resetFiltersBtn" |
| | | 499 | | title="Сбросить все фильтры" |
| | | 500 | | onclick="window.location.href='/BitrixStock'"> |
| | | 501 | | <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" |
| | | 502 | | class="bi bi-arrow-counterclockwise me-1" viewBox="0 0 16 16"> |
| | | 503 | | <path fill-rule="evenodd" |
| | | 504 | | d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2z"/> |
| | | 505 | | <path |
| | | 506 | | d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 |
| | | 507 | | </svg> |
| | | 508 | | Сбросить |
| | | 509 | | </button> |
| | | 510 | | </div> |
| | | 511 | | </div> |
| | | 512 | | |
| | | 513 | | <!-- Функционал сохранения фильтров --> |
| | | 514 | | <div class="col-auto"> |
| | | 515 | | <div class="d-flex flex-wrap align-items-center gap-1 border rounded p-2 bg-light"> |
| | | 516 | | <button type="submit" class="btn btn-sm btn-primary" id="applyMainFiltersBtn" |
| | | 517 | | title="Применить фильтры"> |
| | | 518 | | Применить |
| | | 519 | | </button> |
| | | 520 | | |
| | | 521 | | <button type="button" class="btn btn-sm btn-outline-primary" id="saveFilterBtn" |
| | | 522 | | title="Сохранить фильтр"> |
| | | 523 | | <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" |
| | | 524 | | class="bi bi-bookmark-plus me-1" viewBox="0 0 16 16"> |
| | | 525 | | <path |
| | | 526 | | d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v13.5a.5.5 0 0 1-.777.416L8 13.101l-5.223 2.815A.5. |
| | | 527 | | <path |
| | | 528 | | d="M8 4a.5.5 0 0 1 .5.5V6H10a.5.5 0 0 1 0 1H8.5v1.5a.5.5 0 0 1-1 0V7H6a.5.5 0 0 1 0-1h1. |
| | | 529 | | </svg> |
| | | 530 | | Сохранить |
| | | 531 | | </button> |
| | | 532 | | |
| | | 533 | | <select class="form-select form-select-sm" id="savedFiltersSelect" |
| | | 534 | | style="width: 150px;"> |
| | | 535 | | <option value="">Фильтры</option> |
| | 0 | 536 | | @foreach (var savedFilter in Model.SavedFilters) |
| | | 537 | | { |
| | 0 | 538 | | <option value="@savedFilter.Id" data-filter-data="@savedFilter.FilterData"> |
| | 0 | 539 | | @savedFilter.Name |
| | | 540 | | </option> |
| | | 541 | | } |
| | | 542 | | </select> |
| | | 543 | | |
| | | 544 | | <button type="button" class="btn btn-sm btn-outline-success" id="applyFilterBtn" |
| | | 545 | | title="Применить" disabled> |
| | | 546 | | <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" |
| | | 547 | | class="bi bi-play-fill" viewBox="0 0 16 16"> |
| | | 548 | | <path |
| | | 549 | | d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-. |
| | | 550 | | </svg> |
| | | 551 | | </button> |
| | | 552 | | |
| | | 553 | | <button type="button" class="btn btn-sm btn-outline-danger" id="deleteFilterBtn" |
| | | 554 | | title="Удалить" disabled> |
| | | 555 | | <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" |
| | | 556 | | class="bi bi-trash" viewBox="0 0 16 16"> |
| | | 557 | | <path |
| | | 558 | | d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a. |
| | | 559 | | <path |
| | | 560 | | d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 |
| | | 561 | | </svg> |
| | | 562 | | </button> |
| | | 563 | | </div> |
| | | 564 | | </div> |
| | | 565 | | </div> |
| | | 566 | | </form> |
| | | 567 | | <div class="bsx-chips" id="bsxStocksChips" hidden></div> |
| | | 568 | | <div id="productsTable" class="bsx-grid bsx-grid--stocks"></div> |
| | | 569 | | <div class="bsx-grid-pager" id="bsxStocksPager"> |
| | | 570 | | <button type="button" class="btn btn-sm btn-outline-secondary" data-page="first" title="Первая"><i class |
| | | 571 | | <button type="button" class="btn btn-sm btn-outline-secondary" data-page="prev" title="Назад"><i class=" |
| | | 572 | | <span class="bsx-grid-count" id="bsxStocksPageInfo"></span> |
| | | 573 | | <button type="button" class="btn btn-sm btn-outline-secondary" data-page="next" title="Вперёд"><i class= |
| | | 574 | | <button type="button" class="btn btn-sm btn-outline-secondary" data-page="last" title="Последняя"><i cla |
| | | 575 | | <select class="form-select form-select-sm bsx-grid-select" id="bsxStocksPageSize" title="Строк на страни |
| | | 576 | | <option value="100">100</option> |
| | | 577 | | <option value="200">200</option> |
| | | 578 | | <option value="500">500</option> |
| | | 579 | | </select> |
| | | 580 | | </div> |
| | | 581 | | <div class="bsx-grid-hint">Фильтры и сортировка — по всему snapshot на сервере; страница до 500 строк (по не |
| | | 582 | | </div> |
| | | 583 | | </div> |
| | | 584 | | } |
| | | 585 | | |
| | | 586 | | </div> |
| | | 587 | | |
| | | 588 | | <div class="modal fade" id="bitrixClearConfirmModal" tabindex="-1"> |
| | | 589 | | <div class="modal-dialog modal-xl modal-dialog-scrollable"> |
| | | 590 | | <div class="modal-content"> |
| | | 591 | | <div class="modal-header"> |
| | | 592 | | <h5 class="modal-title">Обнуление остатков Ozon</h5> |
| | | 593 | | <button type="button" class="btn-close" data-bs-dismiss="modal"></button> |
| | | 594 | | </div> |
| | | 595 | | <div class="modal-body"> |
| | | 596 | | <p class="text-muted mb-2">Проверьте список позиций. После подтверждения задание выполнится в фоне (Hang |
| | | 597 | | <div id="bitrixClearConfirmTable" class="bsx-grid bsx-grid--modal-tall"></div> |
| | | 598 | | </div> |
| | | 599 | | <div class="modal-footer"> |
| | | 600 | | <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button> |
| | | 601 | | <button type="button" class="btn btn-danger" id="bitrixClearConfirmStartBtn">Продолжить</button> |
| | | 602 | | </div> |
| | | 603 | | </div> |
| | | 604 | | </div> |
| | | 605 | | </div> |
| | | 606 | | |
| | | 607 | | <div class="modal fade" id="bitrixPushConfirmModal" tabindex="-1"> |
| | | 608 | | <div class="modal-dialog modal-xl modal-dialog-scrollable"> |
| | | 609 | | <div class="modal-content"> |
| | | 610 | | <div class="modal-header"> |
| | | 611 | | <h5 class="modal-title">Накат остатков Ozon</h5> |
| | | 612 | | <button type="button" class="btn-close" data-bs-dismiss="modal"></button> |
| | | 613 | | </div> |
| | | 614 | | <div class="modal-body"> |
| | | 615 | | <p class="text-muted mb-2">Проверьте список позиций. После подтверждения остатки на Ozon будут приведены |
| | | 616 | | <div id="bitrixPushConfirmTable" class="bsx-grid bsx-grid--modal-tall"></div> |
| | | 617 | | </div> |
| | | 618 | | <div class="modal-footer"> |
| | | 619 | | <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button> |
| | | 620 | | <button type="button" class="btn btn-primary" id="bitrixPushConfirmStartBtn">Продолжить</button> |
| | | 621 | | </div> |
| | | 622 | | </div> |
| | | 623 | | </div> |
| | | 624 | | </div> |
| | | 625 | | |
| | | 626 | | <div class="modal fade" id="bitrixPushReportModal" tabindex="-1"> |
| | | 627 | | <div class="modal-dialog modal-xl modal-dialog-scrollable"> |
| | | 628 | | <div class="modal-content"> |
| | | 629 | | <div class="modal-header"> |
| | | 630 | | <h5 class="modal-title">Отчёт наката #<span id="bitrixPushReportModalId"></span></h5> |
| | | 631 | | <button type="button" class="btn-close" data-bs-dismiss="modal"></button> |
| | | 632 | | </div> |
| | | 633 | | <div class="modal-body"> |
| | | 634 | | <div id="bitrixPushReportModalSummary" class="mb-2 small text-muted"></div> |
| | | 635 | | <div id="bitrixPushReportTable" class="bsx-grid bsx-grid--modal-tall"></div> |
| | | 636 | | </div> |
| | | 637 | | </div> |
| | | 638 | | </div> |
| | | 639 | | </div> |
| | | 640 | | |
| | | 641 | | <div class="modal fade" id="bitrixUnarchiveConfirmModal" tabindex="-1"> |
| | | 642 | | <div class="modal-dialog modal-xl modal-dialog-scrollable"> |
| | | 643 | | <div class="modal-content"> |
| | | 644 | | <div class="modal-header"> |
| | | 645 | | <h5 class="modal-title">Восстановление товаров из архива Ozon</h5> |
| | | 646 | | <button type="button" class="btn-close" data-bs-dismiss="modal"></button> |
| | | 647 | | </div> |
| | | 648 | | <div class="modal-body"> |
| | | 649 | | <p class="text-muted mb-2">Только позиции в архиве. Авто-архив: не более 100 восстановлений в сутки (сбр |
| | | 650 | | <div id="bitrixUnarchiveConfirmTable" class="bsx-grid bsx-grid--modal-tall"></div> |
| | | 651 | | </div> |
| | | 652 | | <div class="modal-footer"> |
| | | 653 | | <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button> |
| | | 654 | | <button type="button" class="btn btn-success" id="bitrixUnarchiveConfirmStartBtn">Продолжить</button> |
| | | 655 | | </div> |
| | | 656 | | </div> |
| | | 657 | | </div> |
| | | 658 | | </div> |
| | | 659 | | |
| | | 660 | | <div class="modal fade" id="bitrixArchiveConfirmModal" tabindex="-1"> |
| | | 661 | | <div class="modal-dialog modal-xl modal-dialog-scrollable"> |
| | | 662 | | <div class="modal-content"> |
| | | 663 | | <div class="modal-header"> |
| | | 664 | | <h5 class="modal-title">Архивация товаров в Ozon</h5> |
| | | 665 | | <button type="button" class="btn-close" data-bs-dismiss="modal"></button> |
| | | 666 | | </div> |
| | | 667 | | <div class="modal-body"> |
| | | 668 | | <p class="text-muted mb-2">Только позиции не в архиве. После подтверждения задание выполнится в фоне (Ha |
| | | 669 | | <div id="bitrixArchiveConfirmTable" class="bsx-grid bsx-grid--modal-tall"></div> |
| | | 670 | | </div> |
| | | 671 | | <div class="modal-footer"> |
| | | 672 | | <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button> |
| | | 673 | | <button type="button" class="btn btn-secondary" id="bitrixArchiveConfirmStartBtn">Продолжить</button> |
| | | 674 | | </div> |
| | | 675 | | </div> |
| | | 676 | | </div> |
| | | 677 | | </div> |
| | | 678 | | |
| | | 679 | | <div class="modal fade" id="bitrixClearReportModal" tabindex="-1"> |
| | | 680 | | <div class="modal-dialog modal-xl modal-dialog-scrollable"> |
| | | 681 | | <div class="modal-content"> |
| | | 682 | | <div class="modal-header"> |
| | | 683 | | <h5 class="modal-title">Отчёт обнуления #<span id="bitrixClearReportModalId"></span></h5> |
| | | 684 | | <button type="button" class="btn-close" data-bs-dismiss="modal"></button> |
| | | 685 | | </div> |
| | | 686 | | <div class="modal-body"> |
| | | 687 | | <div id="bitrixClearReportModalSummary" class="mb-2 small text-muted"></div> |
| | | 688 | | <div id="bitrixClearReportTable" class="bsx-grid bsx-grid--modal-tall"></div> |
| | | 689 | | </div> |
| | | 690 | | </div> |
| | | 691 | | </div> |
| | | 692 | | </div> |
| | | 693 | | |
| | 0 | 694 | | @if (isStocksTab) |
| | | 695 | | { |
| | | 696 | | <script> |
| | 0 | 697 | | window.suppliersData = @Html.Raw(Json.Serialize(Model.Suppliers)); |
| | 0 | 698 | | window.warehouseMappings = @Html.Raw(Json.Serialize(Model.WarehousesmMappings.Select(mapping => new |
| | 0 | 699 | | { |
| | 0 | 700 | | mapping.Id, |
| | 0 | 701 | | mapping.BitrixWarehouseName, |
| | 0 | 702 | | mapping.OzonWarehouseName, |
| | 0 | 703 | | OzonClientName = mapping.OzonClient != null ? mapping.OzonClient.Name : string.Empty, |
| | 0 | 704 | | mapping.OzonWarehouseId |
| | 0 | 705 | | }))); |
| | | 706 | | window.bitrixStockRows = []; // заполняет грид (BsxStocksLegacy.onRowsRendered) |
| | | 707 | | const canCreateMissingOzonStocks = false; |
| | | 708 | | const oneCStocksBatchSize = 25; |
| | | 709 | | const oneCStockColor = '#7c2d12'; |
| | | 710 | | const ozonArchiveBatchSize = 50; |
| | 0 | 711 | | const selectedOzonClientId = @(Model.Filter.SelectedClientId.HasValue ? Model.Filter.SelectedClientId.Value : 0); |
| | | 712 | | |
| | | 713 | | function findOneCStockRows(article) { |
| | | 714 | | const normalized = (article || '').trim().toLowerCase(); |
| | | 715 | | return Array.from(document.querySelectorAll('.bitrix-store-row[data-article]')) |
| | | 716 | | .filter(row => (row.getAttribute('data-article') || '').trim().toLowerCase() === normalized); |
| | | 717 | | } |
| | | 718 | | |
| | | 719 | | function setOneCStockCellHtml(cell, html) { |
| | | 720 | | if (cell) { |
| | | 721 | | cell.innerHTML = html; |
| | | 722 | | } |
| | | 723 | | } |
| | | 724 | | |
| | | 725 | | function renderOneCStockError(article, message) { |
| | | 726 | | const safeMessage = (message || 'Ошибка 1С').toString(); |
| | | 727 | | findOneCStockRows(article).forEach(row => { |
| | | 728 | | setOneCStockCellHtml( |
| | | 729 | | row.querySelector('.onec-stock-qty'), |
| | | 730 | | `<span class="text-danger small">${safeMessage}</span>`); |
| | | 731 | | }); |
| | | 732 | | } |
| | | 733 | | |
| | | 734 | | function renderOneCStockItem(article, item) { |
| | | 735 | | const rows = findOneCStockRows(article); |
| | | 736 | | if (!rows.length) { |
| | | 737 | | return; |
| | | 738 | | } |
| | | 739 | | |
| | | 740 | | if (!item) { |
| | | 741 | | rows.forEach(row => { |
| | | 742 | | setOneCStockCellHtml(row.querySelector('.onec-stock-qty'), '<span class="text-muted">—</span>'); |
| | | 743 | | }); |
| | | 744 | | return; |
| | | 745 | | } |
| | | 746 | | |
| | | 747 | | const itemSuccess = item.success ?? item.Success; |
| | | 748 | | if (!itemSuccess) { |
| | | 749 | | const msg = item.message || item.Message || 'Номенклатура не найдена'; |
| | | 750 | | rows.forEach(row => { |
| | | 751 | | setOneCStockCellHtml( |
| | | 752 | | row.querySelector('.onec-stock-qty'), |
| | | 753 | | `<span class="text-muted small" title="${msg}">—</span>`); |
| | | 754 | | }); |
| | | 755 | | return; |
| | | 756 | | } |
| | | 757 | | |
| | | 758 | | const stocks = Array.isArray(item.stocks) |
| | | 759 | | ? item.stocks |
| | | 760 | | : (Array.isArray(item.Stocks) ? item.Stocks : []); |
| | | 761 | | |
| | | 762 | | const stockByWarehouse = new Map(); |
| | | 763 | | stocks.forEach(stock => { |
| | | 764 | | const wh = normalizeWarehouseName(stock.warehouse || stock.Warehouse); |
| | | 765 | | if (wh) { |
| | | 766 | | stockByWarehouse.set(wh, stock.quantity ?? stock.Quantity); |
| | | 767 | | } |
| | | 768 | | }); |
| | | 769 | | |
| | | 770 | | rows.forEach(row => { |
| | | 771 | | const wh = normalizeWarehouseName(row.getAttribute('data-bitrix-store-name')); |
| | | 772 | | const cell = row.querySelector('.onec-stock-qty'); |
| | | 773 | | if (!wh || !stockByWarehouse.has(wh)) { |
| | | 774 | | setOneCStockCellHtml(cell, '<span class="text-muted">—</span>'); |
| | | 775 | | return; |
| | | 776 | | } |
| | | 777 | | |
| | | 778 | | const qty = stockByWarehouse.get(wh); |
| | | 779 | | setOneCStockCellHtml( |
| | | 780 | | cell, |
| | | 781 | | `<span style="color:${oneCStockColor}; font-weight:600;">${qty}</span>`); |
| | | 782 | | }); |
| | | 783 | | } |
| | | 784 | | |
| | | 785 | | async function loadOneCStocksForGrid() { |
| | | 786 | | const articles = Array.from(document.querySelectorAll('.bitrix-stock-row[data-article]')) |
| | | 787 | | .map(row => (row.getAttribute('data-article') || '').trim()) |
| | | 788 | | .filter(Boolean); |
| | | 789 | | |
| | | 790 | | if (!articles.length) { |
| | | 791 | | return; |
| | | 792 | | } |
| | | 793 | | |
| | | 794 | | for (let offset = 0; offset < articles.length; offset += oneCStocksBatchSize) { |
| | | 795 | | const batch = articles.slice(offset, offset + oneCStocksBatchSize); |
| | | 796 | | try { |
| | 0 | 797 | | const response = await fetch('@Url.Action("GetOneCStocksByArticles", "BitrixStock")', { |
| | | 798 | | method: 'POST', |
| | | 799 | | headers: { 'Content-Type': 'application/json' }, |
| | | 800 | | body: JSON.stringify({ Articles: batch }) |
| | | 801 | | }); |
| | | 802 | | |
| | | 803 | | if (!response.ok) { |
| | | 804 | | batch.forEach(article => renderOneCStockError(article, 'Ошибка запроса к 1С')); |
| | | 805 | | continue; |
| | | 806 | | } |
| | | 807 | | |
| | | 808 | | const data = await response.json(); |
| | | 809 | | if (data.success === false && data.error) { |
| | | 810 | | batch.forEach(article => renderOneCStockError(article, data.error)); |
| | | 811 | | continue; |
| | | 812 | | } |
| | | 813 | | |
| | | 814 | | const items = Array.isArray(data.items) ? data.items : []; |
| | | 815 | | const byArticle = new Map(); |
| | | 816 | | items.forEach(item => { |
| | | 817 | | const key = (item?.article || item?.Article || '').trim().toLowerCase(); |
| | | 818 | | if (key) { |
| | | 819 | | byArticle.set(key, item); |
| | | 820 | | } |
| | | 821 | | }); |
| | | 822 | | |
| | | 823 | | batch.forEach(article => { |
| | | 824 | | const item = byArticle.get(article.trim().toLowerCase()); |
| | | 825 | | renderOneCStockItem(article, item); |
| | | 826 | | }); |
| | | 827 | | } catch (error) { |
| | | 828 | | batch.forEach(article => renderOneCStockError(article, error?.message || 'Сбой загрузки 1С')); |
| | | 829 | | } |
| | | 830 | | } |
| | | 831 | | } |
| | | 832 | | |
| | | 833 | | function ozonArchivedYesLabel(manual, auto) { |
| | | 834 | | const suffix = manual ? ' (Вручную)' : auto ? ' (Системой)' : ''; |
| | | 835 | | return `Да${suffix}`; |
| | | 836 | | } |
| | | 837 | | |
| | | 838 | | function formatOzonArchiveLine(status) { |
| | | 839 | | if (!status) { |
| | | 840 | | return ''; |
| | | 841 | | } |
| | | 842 | | const manual = status.isArchived === true || status.IsArchived === true; |
| | | 843 | | const auto = status.isAutoArchived === true || status.IsAutoArchived === true; |
| | | 844 | | const archived = manual || auto; |
| | | 845 | | const client = escapeHtml((status.clientName || status.ClientName || '').toString()); |
| | | 846 | | const offerId = escapeHtml((status.offerId || status.OfferId || '').toString()); |
| | | 847 | | const productId = Number(status.ozonProductId || status.OzonProductId) || 0; |
| | | 848 | | const yesLabel = escapeHtml(ozonArchivedYesLabel(manual, auto)); |
| | | 849 | | |
| | | 850 | | if (archived) { |
| | | 851 | | if (productId <= 0) { |
| | | 852 | | return `<div class="ozon-archive-line mb-1"> |
| | | 853 | | <span class="ozon-archive-status ozon-archive-yes-label text-danger small">${yesLabel}</span> |
| | | 854 | | <small class="ozon-archive-client text-muted">${client}</small> |
| | | 855 | | </div>`; |
| | | 856 | | } |
| | | 857 | | return `<div class="ozon-archive-line mb-1 ozon-unarchive-pick" |
| | | 858 | | data-offer-id="${offerId}" |
| | | 859 | | data-ozon-product-id="${productId}" |
| | | 860 | | data-ozon-client="${client}" |
| | | 861 | | data-is-auto-archived="${auto ? '1' : '0'}" |
| | | 862 | | data-is-manual-archived="${manual ? '1' : '0'}"> |
| | | 863 | | <span class="ozon-archive-status ozon-unarchive-yes" |
| | | 864 | | style="cursor:pointer;" |
| | | 865 | | title="Клик — восстановить этот товар из архива Ozon">${yesLabel}</span> |
| | | 866 | | <small class="ozon-archive-client text-muted ozon-unarchive-toggle-col" |
| | | 867 | | style="cursor:pointer;" |
| | | 868 | | title="Клик — выделить / снять выделение для массового восстановления">${client}</small> |
| | | 869 | | </div>`; |
| | | 870 | | } |
| | | 871 | | |
| | | 872 | | if (productId <= 0) { |
| | | 873 | | return `<div class="ozon-archive-line mb-1"> |
| | | 874 | | <span class="ozon-archive-status ozon-archive-no-label text-success small">Нет</span> |
| | | 875 | | <small class="ozon-archive-client text-muted">${client}</small> |
| | | 876 | | </div>`; |
| | | 877 | | } |
| | | 878 | | |
| | | 879 | | return `<div class="ozon-archive-line mb-1 ozon-archive-pick" |
| | | 880 | | data-offer-id="${offerId}" |
| | | 881 | | data-ozon-product-id="${productId}" |
| | | 882 | | data-ozon-client="${client}"> |
| | | 883 | | <span class="ozon-archive-status ozon-archive-no" |
| | | 884 | | style="cursor:pointer;" |
| | | 885 | | title="Клик — отправить этот товар в архив Ozon">Нет</span> |
| | | 886 | | <small class="ozon-archive-client text-muted ozon-archive-toggle-col" |
| | | 887 | | style="cursor:pointer;" |
| | | 888 | | title="Клик — выделить / снять выделение для массовой архивации">${client}</small> |
| | | 889 | | </div>`; |
| | | 890 | | } |
| | | 891 | | |
| | | 892 | | function archiveSelectionKey(client, productId) { |
| | | 893 | | return `${(client || '').trim().toLowerCase()}_${productId}`; |
| | | 894 | | } |
| | | 895 | | |
| | | 896 | | function archiveItemFromPick(pick) { |
| | | 897 | | const row = pick.closest('.bitrix-stock-row'); |
| | | 898 | | return { |
| | | 899 | | article: row?.getAttribute('data-article') || '', |
| | | 900 | | offerId: pick.getAttribute('data-offer-id') || '', |
| | | 901 | | ozonProductId: Number(pick.getAttribute('data-ozon-product-id')), |
| | | 902 | | ozonClient: pick.getAttribute('data-ozon-client') || '', |
| | | 903 | | wasArchived: false |
| | | 904 | | }; |
| | | 905 | | } |
| | | 906 | | |
| | | 907 | | function toggleArchivePickSelection(pick) { |
| | | 908 | | if (!pick) { |
| | | 909 | | return; |
| | | 910 | | } |
| | | 911 | | const productId = Number(pick.getAttribute('data-ozon-product-id')); |
| | | 912 | | const client = pick.getAttribute('data-ozon-client') || ''; |
| | | 913 | | const key = archiveSelectionKey(client, productId); |
| | | 914 | | if (selectedArchiveItems.has(key)) { |
| | | 915 | | selectedArchiveItems.delete(key); |
| | | 916 | | pick.classList.remove('selected-archive-item'); |
| | | 917 | | } else { |
| | | 918 | | selectedArchiveItems.add(key); |
| | | 919 | | pick.classList.add('selected-archive-item'); |
| | | 920 | | } |
| | | 921 | | updateSelectedUI(); |
| | | 922 | | } |
| | | 923 | | |
| | | 924 | | async function archiveSingleFromNoClick(pick) { |
| | | 925 | | const item = archiveItemFromPick(pick); |
| | | 926 | | if (!item.ozonProductId || !item.ozonClient) { |
| | | 927 | | showNotification('Нет product_id Ozon для архивации', 'warning'); |
| | | 928 | | return; |
| | | 929 | | } |
| | | 930 | | const msg = item.offerId |
| | | 931 | | ? `Отправить в архив Ozon?\nOffer: ${item.offerId}\nКлиент: ${item.ozonClient}` |
| | | 932 | | : `Отправить в архив Ozon?\nКлиент: ${item.ozonClient}`; |
| | | 933 | | if (!(await St2Dialog.confirm(msg, { variant: 'warning', confirmText: 'В архив' }))) { |
| | | 934 | | return; |
| | | 935 | | } |
| | | 936 | | pendingArchiveItems = [item]; |
| | | 937 | | await startBitrixArchiveJob({ silent: true }); |
| | | 938 | | } |
| | | 939 | | |
| | | 940 | | function renderOzonArchiveCell(offerId, statuses) { |
| | | 941 | | const cell = document.querySelector(`.ozon-archive-cell[data-offer-id="${CSS.escape(offerId)}"]`); |
| | | 942 | | if (!cell) { |
| | | 943 | | return; |
| | | 944 | | } |
| | | 945 | | if (!statuses || !statuses.length) { |
| | | 946 | | cell.innerHTML = '<span class="text-muted small">—</span>'; |
| | | 947 | | return; |
| | | 948 | | } |
| | | 949 | | cell.innerHTML = statuses.map(formatOzonArchiveLine).join(''); |
| | | 950 | | cell.querySelectorAll('.ozon-archive-pick').forEach(pick => { |
| | | 951 | | const key = archiveSelectionKey( |
| | | 952 | | pick.getAttribute('data-ozon-client'), |
| | | 953 | | pick.getAttribute('data-ozon-product-id')); |
| | | 954 | | if (selectedArchiveItems.has(key)) { |
| | | 955 | | pick.classList.add('selected-archive-item'); |
| | | 956 | | } |
| | | 957 | | }); |
| | | 958 | | cell.querySelectorAll('.ozon-unarchive-pick').forEach(pick => { |
| | | 959 | | const key = unarchiveSelectionKey( |
| | | 960 | | pick.getAttribute('data-ozon-client'), |
| | | 961 | | pick.getAttribute('data-ozon-product-id')); |
| | | 962 | | if (selectedUnarchiveItems.has(key)) { |
| | | 963 | | pick.classList.add('selected-unarchive-item'); |
| | | 964 | | } |
| | | 965 | | }); |
| | | 966 | | updateSelectedUI(); |
| | | 967 | | } |
| | | 968 | | |
| | | 969 | | function renderOzonArchiveError(offerId, message) { |
| | | 970 | | const cell = document.querySelector(`.ozon-archive-cell[data-offer-id="${CSS.escape(offerId)}"]`); |
| | | 971 | | if (cell) { |
| | | 972 | | cell.innerHTML = `<span class="text-danger small" title="${(message || 'Ошибка').toString()}">!</span>`; |
| | | 973 | | } |
| | | 974 | | } |
| | | 975 | | |
| | | 976 | | async function loadOzonArchiveForGrid() { |
| | | 977 | | const offerIds = Array.from(document.querySelectorAll('.bitrix-stock-row[data-offer-id]')) |
| | | 978 | | .map(row => (row.getAttribute('data-offer-id') || '').trim()) |
| | | 979 | | .filter(Boolean); |
| | | 980 | | |
| | | 981 | | const uniqueOfferIds = [...new Set(offerIds)]; |
| | | 982 | | if (!uniqueOfferIds.length) { |
| | | 983 | | return; |
| | | 984 | | } |
| | | 985 | | |
| | | 986 | | for (let offset = 0; offset < uniqueOfferIds.length; offset += ozonArchiveBatchSize) { |
| | | 987 | | const batch = uniqueOfferIds.slice(offset, offset + ozonArchiveBatchSize); |
| | | 988 | | try { |
| | | 989 | | const payload = { OfferIds: batch }; |
| | | 990 | | if (selectedOzonClientId > 0) { |
| | | 991 | | payload.ClientId = selectedOzonClientId; |
| | | 992 | | } |
| | | 993 | | |
| | 0 | 994 | | const response = await fetch('@Url.Action("GetOzonProductArchiveByOfferIds", "BitrixStock")', { |
| | | 995 | | method: 'POST', |
| | | 996 | | headers: { 'Content-Type': 'application/json' }, |
| | | 997 | | body: JSON.stringify(payload) |
| | | 998 | | }); |
| | | 999 | | |
| | | 1000 | | if (!response.ok) { |
| | | 1001 | | batch.forEach(id => renderOzonArchiveError(id, 'Ошибка запроса к Ozon')); |
| | | 1002 | | continue; |
| | | 1003 | | } |
| | | 1004 | | |
| | | 1005 | | const data = await response.json(); |
| | | 1006 | | if (data.success === false && data.error) { |
| | | 1007 | | batch.forEach(id => renderOzonArchiveError(id, data.error)); |
| | | 1008 | | continue; |
| | | 1009 | | } |
| | | 1010 | | |
| | | 1011 | | const items = Array.isArray(data.items) ? data.items : []; |
| | | 1012 | | const byOffer = new Map(); |
| | | 1013 | | items.forEach(item => { |
| | | 1014 | | const key = (item?.offerId || item?.OfferId || '').trim(); |
| | | 1015 | | if (!key) { |
| | | 1016 | | return; |
| | | 1017 | | } |
| | | 1018 | | if (!byOffer.has(key)) { |
| | | 1019 | | byOffer.set(key, []); |
| | | 1020 | | } |
| | | 1021 | | byOffer.get(key).push(item); |
| | | 1022 | | }); |
| | | 1023 | | |
| | | 1024 | | batch.forEach(offerId => { |
| | | 1025 | | renderOzonArchiveCell(offerId, byOffer.get(offerId) || []); |
| | | 1026 | | }); |
| | | 1027 | | } catch (error) { |
| | | 1028 | | batch.forEach(id => renderOzonArchiveError(id, error?.message || 'Сбой загрузки архива')); |
| | | 1029 | | } |
| | | 1030 | | } |
| | | 1031 | | } |
| | | 1032 | | |
| | | 1033 | | // Грид «Остатки» (bsx-stocks.js) вызывает хук после каждой отрисовки страницы строк: ленивые остатки 1С, |
| | | 1034 | | // архив Ozon, кнопки массовых действий работают по DOM отрисованных строк, как раньше по <tr>. |
| | | 1035 | | window.BsxStocksLegacy = { |
| | | 1036 | | onRowsRendered(rows) { |
| | | 1037 | | window.bitrixStockRows = (rows || []).map(r => ({ |
| | | 1038 | | productId: r.productId, |
| | | 1039 | | offerId: r.articleWithKey, |
| | | 1040 | | active: r.active, |
| | | 1041 | | bitrixStores: (r.bitrixStores || []).map(s => ({ title: s.title, amount: s.amount })), |
| | | 1042 | | ozonStores: (r.ozonStores || []).map(s => ({ title: s.title, storeId: s.storeId, clientName: s.clientNam |
| | | 1043 | | })); |
| | | 1044 | | loadOneCStocksForGrid().catch(error => console.warn('Ошибка загрузки остатков 1С', error)); |
| | | 1045 | | loadOzonArchiveForGrid() |
| | | 1046 | | .then(() => updateSelectedUI()) |
| | | 1047 | | .catch(error => console.warn('Ошибка загрузки архива Ozon', error)); |
| | | 1048 | | if (typeof updateMassActionButtons === 'function') updateMassActionButtons(); |
| | | 1049 | | }, |
| | | 1050 | | showNotification: (message, type) => showNotification(message, type) |
| | | 1051 | | }; |
| | | 1052 | | |
| | | 1053 | | const rates = { |
| | | 1054 | | 'RUB': 1, |
| | 0 | 1055 | | 'USD': parseFloat('@Model.RateUSD'.replace(',', '.')), |
| | 0 | 1056 | | 'EUR': parseFloat('@Model.RateEUR'.replace(',', '.')), |
| | 0 | 1057 | | 'BYN': parseFloat('@Model.RateBYN'.replace(',', '.')) |
| | | 1058 | | }; |
| | | 1059 | | const ozonClientHistory = {}; |
| | | 1060 | | |
| | | 1061 | | function normalizeWarehouseName(value) { |
| | | 1062 | | return (value || '').toString().trim().toLowerCase(); |
| | | 1063 | | } |
| | | 1064 | | |
| | | 1065 | | function parseWarehouseId(value) { |
| | | 1066 | | if (value === null || value === undefined) { |
| | | 1067 | | return NaN; |
| | | 1068 | | } |
| | | 1069 | | |
| | | 1070 | | const parsed = Number(value); |
| | | 1071 | | return Number.isFinite(parsed) ? parsed : NaN; |
| | | 1072 | | } |
| | | 1073 | | |
| | | 1074 | | function escapeForAttrSelector(value) { |
| | | 1075 | | if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') { |
| | | 1076 | | return CSS.escape(value); |
| | | 1077 | | } |
| | | 1078 | | |
| | | 1079 | | return (value || '').replace(/["\\]/g, '\\$&'); |
| | | 1080 | | } |
| | | 1081 | | |
| | | 1082 | | function findMissingOzonStocksForRow(rowData) { |
| | | 1083 | | if (!rowData || !Array.isArray(rowData.bitrixStores)) { |
| | | 1084 | | return []; |
| | | 1085 | | } |
| | | 1086 | | |
| | | 1087 | | const allMappings = (window.warehouseMappings || []).filter(mapping => mapping); |
| | | 1088 | | if (!allMappings.length) { |
| | | 1089 | | return []; |
| | | 1090 | | } |
| | | 1091 | | |
| | | 1092 | | const ozonStoresByClient = (rowData.ozonStores || []).reduce((acc, store) => { |
| | | 1093 | | const key = normalizeWarehouseName(store.clientName); |
| | | 1094 | | if (!key) { |
| | | 1095 | | return acc; |
| | | 1096 | | } |
| | | 1097 | | |
| | | 1098 | | if (!acc[key]) { |
| | | 1099 | | acc[key] = []; |
| | | 1100 | | } |
| | | 1101 | | acc[key].push(store); |
| | | 1102 | | return acc; |
| | | 1103 | | }, {}); |
| | | 1104 | | const missingCandidates = []; |
| | | 1105 | | const dedupeSet = new Set(); |
| | | 1106 | | |
| | | 1107 | | for (const bitrixStore of rowData.bitrixStores) { |
| | | 1108 | | const bitrixName = normalizeWarehouseName(bitrixStore.title); |
| | | 1109 | | if (!bitrixName) { |
| | | 1110 | | continue; |
| | | 1111 | | } |
| | | 1112 | | |
| | | 1113 | | const bitrixMappings = allMappings.filter(m => |
| | | 1114 | | normalizeWarehouseName(m.bitrixWarehouseName) === bitrixName); |
| | | 1115 | | if (!bitrixMappings.length) { |
| | | 1116 | | continue; |
| | | 1117 | | } |
| | | 1118 | | |
| | | 1119 | | for (const mapping of bitrixMappings) { |
| | | 1120 | | const mappedWarehouseId = parseWarehouseId(mapping.ozonWarehouseId); |
| | | 1121 | | if (!Number.isFinite(mappedWarehouseId) || mappedWarehouseId <= 0) { |
| | | 1122 | | continue; |
| | | 1123 | | } |
| | | 1124 | | |
| | | 1125 | | const clientKey = normalizeWarehouseName(mapping.ozonClientName); |
| | | 1126 | | const clientOzonStores = ozonStoresByClient[clientKey] || []; |
| | | 1127 | | if (!clientOzonStores.length) { |
| | | 1128 | | continue; |
| | | 1129 | | } |
| | | 1130 | | |
| | | 1131 | | const hasOzonWarehouse = clientOzonStores.some(store => |
| | | 1132 | | parseWarehouseId(store.storeId) === mappedWarehouseId || |
| | | 1133 | | normalizeWarehouseName(store.title) === normalizeWarehouseName(mapping.ozonWarehouseName)); |
| | | 1134 | | if (hasOzonWarehouse) { |
| | | 1135 | | continue; |
| | | 1136 | | } |
| | | 1137 | | |
| | | 1138 | | const clientProductId = Number(clientOzonStores.find(s => Number(s.productId) > 0)?.productId); |
| | | 1139 | | if (!Number.isFinite(clientProductId) || clientProductId <= 0) { |
| | | 1140 | | continue; |
| | | 1141 | | } |
| | | 1142 | | |
| | | 1143 | | const dedupeKey = `${clientKey}:${mappedWarehouseId}`; |
| | | 1144 | | if (dedupeSet.has(dedupeKey)) { |
| | | 1145 | | continue; |
| | | 1146 | | } |
| | | 1147 | | dedupeSet.add(dedupeKey); |
| | | 1148 | | |
| | | 1149 | | missingCandidates.push({ |
| | | 1150 | | bitrixWarehouseName: bitrixStore.title, |
| | | 1151 | | ozonWarehouseName: mapping.ozonWarehouseName, |
| | | 1152 | | ozonClientName: mapping.ozonClientName, |
| | | 1153 | | warehouseId: mappedWarehouseId, |
| | | 1154 | | stock: Math.max(0, Math.round(Number(bitrixStore.amount) || 0)), |
| | | 1155 | | productId: clientProductId |
| | | 1156 | | }); |
| | | 1157 | | } |
| | | 1158 | | } |
| | | 1159 | | |
| | | 1160 | | return missingCandidates; |
| | | 1161 | | } |
| | | 1162 | | |
| | | 1163 | | function findMissingWarehouseMappingsForRow(rowData) { |
| | | 1164 | | if (!rowData || !Array.isArray(rowData.bitrixStores)) { |
| | | 1165 | | return []; |
| | | 1166 | | } |
| | | 1167 | | |
| | | 1168 | | const allMappings = (window.warehouseMappings || []).filter(mapping => mapping); |
| | | 1169 | | if (!allMappings.length) { |
| | | 1170 | | return rowData.bitrixStores |
| | | 1171 | | .filter(store => Number(store.amount) > 0) |
| | | 1172 | | .map(store => store.title); |
| | | 1173 | | } |
| | | 1174 | | |
| | | 1175 | | const missingStores = []; |
| | | 1176 | | const seenStores = new Set(); |
| | | 1177 | | |
| | | 1178 | | for (const bitrixStore of rowData.bitrixStores) { |
| | | 1179 | | const amount = Number(bitrixStore.amount) || 0; |
| | | 1180 | | if (amount <= 0) { |
| | | 1181 | | continue; |
| | | 1182 | | } |
| | | 1183 | | |
| | | 1184 | | const bitrixName = normalizeWarehouseName(bitrixStore.title); |
| | | 1185 | | if (!bitrixName || seenStores.has(bitrixName)) { |
| | | 1186 | | continue; |
| | | 1187 | | } |
| | | 1188 | | |
| | | 1189 | | const hasMapping = allMappings.some(m => |
| | | 1190 | | normalizeWarehouseName(m.bitrixWarehouseName) === bitrixName); |
| | | 1191 | | if (!hasMapping) { |
| | | 1192 | | seenStores.add(bitrixName); |
| | | 1193 | | missingStores.push(bitrixStore.title); |
| | | 1194 | | } |
| | | 1195 | | } |
| | | 1196 | | |
| | | 1197 | | return missingStores; |
| | | 1198 | | } |
| | | 1199 | | |
| | | 1200 | | function renderMissingOzonCreateButtons() { |
| | | 1201 | | if (!canCreateMissingOzonStocks) { |
| | | 1202 | | return; |
| | | 1203 | | } |
| | | 1204 | | |
| | | 1205 | | const rowByProductId = new Map((window.bitrixStockRows || []).map(row => [Number(row.productId), row])); |
| | | 1206 | | const rowElements = document.querySelectorAll('.bitrix-stock-row[data-product-id]'); |
| | | 1207 | | |
| | | 1208 | | rowElements.forEach(rowElement => { |
| | | 1209 | | const productId = Number(rowElement.getAttribute('data-product-id')); |
| | | 1210 | | const targetContainer = rowElement.querySelector('.missing-ozon-stock-actions'); |
| | | 1211 | | if (!targetContainer || !Number.isFinite(productId)) { |
| | | 1212 | | return; |
| | | 1213 | | } |
| | | 1214 | | |
| | | 1215 | | const rowData = rowByProductId.get(productId); |
| | | 1216 | | const missingCandidates = findMissingOzonStocksForRow(rowData); |
| | | 1217 | | const missingMappings = findMissingWarehouseMappingsForRow(rowData); |
| | | 1218 | | rowElement.querySelectorAll('.bitrix-store-sync-actions').forEach(actionContainer => { |
| | | 1219 | | actionContainer.innerHTML = ''; |
| | | 1220 | | }); |
| | | 1221 | | |
| | | 1222 | | const mappingHintsHtml = missingMappings.map(storeName => |
| | | 1223 | | `<span class="missing-warehouse-mapping-hint" title="Добавьте связь в справочнике WarehouseMapping"> |
| | | 1224 | | Для склада ${storeName} нет WarehouseMapping |
| | | 1225 | | </span>` |
| | | 1226 | | ).join(''); |
| | | 1227 | | |
| | | 1228 | | if (!missingCandidates.length || !rowData?.offerId) { |
| | | 1229 | | targetContainer.innerHTML = mappingHintsHtml; |
| | | 1230 | | return; |
| | | 1231 | | } |
| | | 1232 | | |
| | | 1233 | | missingCandidates.forEach(candidate => { |
| | | 1234 | | const selector = `.bitrix-store-row[data-bitrix-store-name="${escapeForAttrSelector(candidate.bitrixWare |
| | | 1235 | | const actionContainer = rowElement.querySelector(selector); |
| | | 1236 | | if (!actionContainer) { |
| | | 1237 | | return; |
| | | 1238 | | } |
| | | 1239 | | |
| | | 1240 | | const candidatePayload = encodeURIComponent(JSON.stringify(candidate)); |
| | | 1241 | | actionContainer.insertAdjacentHTML('beforeend', ` |
| | | 1242 | | <span class="sync-action-chip" |
| | | 1243 | | title="Синхронизировать для клиента ${candidate.ozonClientName}: ${candidate.bitrixWarehouseNa |
| | | 1244 | | <span class="sync-client-label">${candidate.ozonClientName}</span> |
| | | 1245 | | <button type="button" |
| | | 1246 | | class="btn btn-sm btn-link sync-missing-ozon-stock" |
| | | 1247 | | data-product-id="${productId}" |
| | | 1248 | | data-offer-id="${rowData.offerId}" |
| | | 1249 | | data-ozon-product-id="${candidate.productId}" |
| | | 1250 | | data-client="${candidate.ozonClientName}" |
| | | 1251 | | data-stock="${candidate.stock}" |
| | | 1252 | | data-warehouse-id="${candidate.warehouseId}" |
| | | 1253 | | data-ozon-warehouse-name="${candidate.ozonWarehouseName ?? ''}" |
| | | 1254 | | data-bitrix-store-name="${candidate.bitrixWarehouseName}" |
| | | 1255 | | data-candidate="${candidatePayload}" |
| | | 1256 | | title="Синхронизировать клиента ${candidate.ozonClientName}"> |
| | | 1257 | | <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi |
| | | 1258 | | <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 |
| | | 1259 | | <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A. |
| | | 1260 | | </svg> |
| | | 1261 | | </button> |
| | | 1262 | | </span> |
| | | 1263 | | `); |
| | | 1264 | | }); |
| | | 1265 | | |
| | | 1266 | | const groupedByBitrixStore = missingCandidates.reduce((acc, candidate) => { |
| | | 1267 | | const key = candidate.bitrixWarehouseName; |
| | | 1268 | | if (!acc[key]) { |
| | | 1269 | | acc[key] = []; |
| | | 1270 | | } |
| | | 1271 | | acc[key].push(candidate); |
| | | 1272 | | return acc; |
| | | 1273 | | }, {}); |
| | | 1274 | | |
| | | 1275 | | Object.entries(groupedByBitrixStore).forEach(([bitrixStoreName, candidates]) => { |
| | | 1276 | | if (candidates.length < 2) { |
| | | 1277 | | return; |
| | | 1278 | | } |
| | | 1279 | | |
| | | 1280 | | const selector = `.bitrix-store-row[data-bitrix-store-name="${escapeForAttrSelector(bitrixStoreName)}"] |
| | | 1281 | | const actionContainer = rowElement.querySelector(selector); |
| | | 1282 | | if (!actionContainer) { |
| | | 1283 | | return; |
| | | 1284 | | } |
| | | 1285 | | |
| | | 1286 | | const allCandidatesPayload = encodeURIComponent(JSON.stringify(candidates)); |
| | | 1287 | | actionContainer.insertAdjacentHTML('beforeend', ` |
| | | 1288 | | <span class="sync-action-chip sync-action-chip-all" |
| | | 1289 | | title="Синхронизировать сразу все клиенты для склада ${bitrixStoreName}"> |
| | | 1290 | | Все (${candidates.length}) |
| | | 1291 | | <button type="button" |
| | | 1292 | | class="btn btn-sm btn-link sync-all-missing-ozon-stock" |
| | | 1293 | | data-product-id="${productId}" |
| | | 1294 | | data-offer-id="${rowData.offerId}" |
| | | 1295 | | data-bitrix-store-name="${bitrixStoreName}" |
| | | 1296 | | data-all-candidates="${allCandidatesPayload}" |
| | | 1297 | | title="Синхронизировать все маппинги для ${bitrixStoreName} (${candidates.length})"> |
| | | 1298 | | <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi |
| | | 1299 | | <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 |
| | | 1300 | | <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A. |
| | | 1301 | | </svg> |
| | | 1302 | | </button> |
| | | 1303 | | </span> |
| | | 1304 | | `); |
| | | 1305 | | }); |
| | | 1306 | | |
| | | 1307 | | targetContainer.innerHTML = mappingHintsHtml; |
| | | 1308 | | }); |
| | | 1309 | | } |
| | | 1310 | | |
| | | 1311 | | function appendCreatedOzonStoreRow(productId, payload) { |
| | | 1312 | | const rowElement = document.querySelector(`.bitrix-stock-row[data-product-id="${productId}"]`); |
| | | 1313 | | if (!rowElement) { |
| | | 1314 | | return; |
| | | 1315 | | } |
| | | 1316 | | |
| | | 1317 | | const ozonCell = rowElement.children[3]; |
| | | 1318 | | if (!ozonCell) { |
| | | 1319 | | return; |
| | | 1320 | | } |
| | | 1321 | | |
| | | 1322 | | let container = ozonCell.querySelector('.ozon-stores-container'); |
| | | 1323 | | if (!container) { |
| | | 1324 | | ozonCell.innerHTML = `<div class="mb-0 ozon-stores-container" data-product-id="${productId}"></div>`; |
| | | 1325 | | container = ozonCell.querySelector('.ozon-stores-container'); |
| | | 1326 | | } |
| | | 1327 | | |
| | | 1328 | | const existingRow = container.querySelector(`.created-ozon-store-row[data-client="${payload.clientName}"][data-w |
| | | 1329 | | if (existingRow) { |
| | | 1330 | | const amountCell = existingRow.querySelector('.created-ozon-amount'); |
| | | 1331 | | if (amountCell) { |
| | | 1332 | | amountCell.textContent = payload.stock; |
| | | 1333 | | } |
| | | 1334 | | return; |
| | | 1335 | | } |
| | | 1336 | | |
| | | 1337 | | const newRow = document.createElement('div'); |
| | | 1338 | | newRow.className = 'row align-items-center created-ozon-store-row'; |
| | | 1339 | | newRow.setAttribute('data-client', payload.clientName); |
| | | 1340 | | newRow.setAttribute('data-warehouse-id', payload.warehouseId); |
| | | 1341 | | newRow.innerHTML = ` |
| | | 1342 | | <div class="col-4 d-flex align-items-center"> |
| | | 1343 | | <span style="color:#2563eb; font-weight: 600;">${payload.ozonWarehouseName || payload.bitrixStoreName}</ |
| | | 1344 | | </div> |
| | | 1345 | | <div class="col-4 text-end created-ozon-amount" |
| | | 1346 | | style="color:#2563eb; font-weight: 600;" |
| | | 1347 | | data-client="${payload.clientName}" |
| | | 1348 | | data-offer-id="${payload.offerId}" |
| | | 1349 | | data-product-id="${payload.ozonProductId}" |
| | | 1350 | | data-warehouse-id="${payload.warehouseId}"> |
| | | 1351 | | ${payload.stock} |
| | | 1352 | | </div> |
| | | 1353 | | <div class="col-4"> |
| | | 1354 | | <small class="text-muted">${payload.clientName}</small> |
| | | 1355 | | </div> |
| | | 1356 | | `; |
| | | 1357 | | container.prepend(newRow); |
| | | 1358 | | } |
| | | 1359 | | |
| | | 1360 | | |
| | | 1361 | | function handleEnter(event) { |
| | | 1362 | | if (event.key === 'Enter') { |
| | | 1363 | | event.preventDefault(); |
| | | 1364 | | showLoadingSpinner() |
| | | 1365 | | document.getElementById('filterForm').submit(); |
| | | 1366 | | } |
| | | 1367 | | } |
| | | 1368 | | |
| | | 1369 | | document.addEventListener('click', async function (event) { |
| | | 1370 | | const clickedSingleChip = event.target.closest('.sync-action-chip:not(.sync-action-chip-all)'); |
| | | 1371 | | const clickedAllChip = event.target.closest('.sync-action-chip-all'); |
| | | 1372 | | const createButton = event.target.closest('.sync-missing-ozon-stock') || clickedSingleChip?.querySelector('.sync |
| | | 1373 | | const syncAllButton = event.target.closest('.sync-all-missing-ozon-stock') || clickedAllChip?.querySelector('.sy |
| | | 1374 | | |
| | | 1375 | | if (!createButton && !syncAllButton) { |
| | | 1376 | | return; |
| | | 1377 | | } |
| | | 1378 | | |
| | | 1379 | | event.preventDefault(); |
| | | 1380 | | |
| | | 1381 | | if (syncAllButton) { |
| | | 1382 | | if (syncAllButton.disabled) { |
| | | 1383 | | return; |
| | | 1384 | | } |
| | | 1385 | | |
| | | 1386 | | const productId = Number(syncAllButton.getAttribute('data-product-id')); |
| | | 1387 | | const bitrixStoreName = syncAllButton.getAttribute('data-bitrix-store-name'); |
| | | 1388 | | const rawCandidates = syncAllButton.getAttribute('data-all-candidates'); |
| | | 1389 | | let candidates = []; |
| | | 1390 | | try { |
| | | 1391 | | candidates = JSON.parse(decodeURIComponent(rawCandidates || '[]')); |
| | | 1392 | | } catch { |
| | | 1393 | | showNotification('Не удалось прочитать список складов для массовой синхронизации', 'danger'); |
| | | 1394 | | return; |
| | | 1395 | | } |
| | | 1396 | | |
| | | 1397 | | if (!candidates.length) { |
| | | 1398 | | showNotification('Нет складов для массовой синхронизации', 'warning'); |
| | | 1399 | | return; |
| | | 1400 | | } |
| | | 1401 | | |
| | | 1402 | | syncAllButton.disabled = true; |
| | | 1403 | | const previousHtml = syncAllButton.innerHTML; |
| | | 1404 | | syncAllButton.innerHTML = '...'; |
| | | 1405 | | let successCount = 0; |
| | | 1406 | | const failedClients = []; |
| | | 1407 | | |
| | | 1408 | | for (const candidate of candidates) { |
| | | 1409 | | const requestPayload = { |
| | | 1410 | | OzonClient: candidate.ozonClientName, |
| | | 1411 | | OfferId: syncAllButton.getAttribute('data-offer-id'), |
| | | 1412 | | ProductId: Number(candidate.productId), |
| | | 1413 | | Stock: Number(candidate.stock), |
| | | 1414 | | WarehouseId: Number(candidate.warehouseId) |
| | | 1415 | | }; |
| | | 1416 | | |
| | | 1417 | | const ok = await updateOzonProductStock(requestPayload); |
| | | 1418 | | if (ok) { |
| | | 1419 | | successCount += 1; |
| | | 1420 | | appendCreatedOzonStoreRow(productId, { |
| | | 1421 | | clientName: candidate.ozonClientName, |
| | | 1422 | | offerId: requestPayload.OfferId, |
| | | 1423 | | ozonProductId: requestPayload.ProductId, |
| | | 1424 | | warehouseId: requestPayload.WarehouseId, |
| | | 1425 | | stock: requestPayload.Stock, |
| | | 1426 | | bitrixStoreName: candidate.bitrixWarehouseName, |
| | | 1427 | | ozonWarehouseName: candidate.ozonWarehouseName |
| | | 1428 | | }); |
| | | 1429 | | } else { |
| | | 1430 | | failedClients.push(candidate.ozonClientName); |
| | | 1431 | | } |
| | | 1432 | | } |
| | | 1433 | | |
| | | 1434 | | if (successCount > 0 && failedClients.length === 0) { |
| | | 1435 | | showNotification(`Синхронизированы все маппинги (${successCount}) для склада ${bitrixStoreName}`, 'succe |
| | | 1436 | | const actionsContainer = syncAllButton.closest('.bitrix-store-sync-actions'); |
| | | 1437 | | actionsContainer?.querySelectorAll('.sync-action-chip').forEach(chip => chip.remove()); |
| | | 1438 | | } else if (successCount > 0) { |
| | | 1439 | | showNotification(`Частично синхронизировано: ${successCount}/${candidates.length}. Ошибки: ${failedClien |
| | | 1440 | | syncAllButton.disabled = false; |
| | | 1441 | | syncAllButton.innerHTML = previousHtml; |
| | | 1442 | | } else { |
| | | 1443 | | showNotification(`Не удалось синхронизировать склад ${bitrixStoreName} ни для одного клиента`, 'danger') |
| | | 1444 | | syncAllButton.disabled = false; |
| | | 1445 | | syncAllButton.innerHTML = previousHtml; |
| | | 1446 | | } |
| | | 1447 | | return; |
| | | 1448 | | } |
| | | 1449 | | |
| | | 1450 | | if (createButton.disabled) { |
| | | 1451 | | return; |
| | | 1452 | | } |
| | | 1453 | | |
| | | 1454 | | const productId = Number(createButton.getAttribute('data-product-id')); |
| | | 1455 | | const ozonProductId = Number(createButton.getAttribute('data-ozon-product-id')); |
| | | 1456 | | const warehouseId = Number(createButton.getAttribute('data-warehouse-id')); |
| | | 1457 | | const stock = Number(createButton.getAttribute('data-stock')); |
| | | 1458 | | const offerId = createButton.getAttribute('data-offer-id'); |
| | | 1459 | | const clientName = createButton.getAttribute('data-client'); |
| | | 1460 | | const bitrixStoreName = createButton.getAttribute('data-bitrix-store-name'); |
| | | 1461 | | const ozonWarehouseName = createButton.getAttribute('data-ozon-warehouse-name'); |
| | | 1462 | | |
| | | 1463 | | if (!offerId || !Number.isFinite(productId) || productId <= 0) { |
| | | 1464 | | showNotification('Нельзя создать остаток: некорректный товар (offer_id/bitrix product_id)', 'danger'); |
| | | 1465 | | return; |
| | | 1466 | | } |
| | | 1467 | | |
| | | 1468 | | if (!clientName) { |
| | | 1469 | | showNotification('Нельзя создать остаток: не определен клиент Ozon', 'danger'); |
| | | 1470 | | return; |
| | | 1471 | | } |
| | | 1472 | | |
| | | 1473 | | if (!Number.isFinite(ozonProductId) || ozonProductId <= 0) { |
| | | 1474 | | showNotification('Нельзя создать остаток: не определен product_id в Ozon для клиента', 'danger'); |
| | | 1475 | | return; |
| | | 1476 | | } |
| | | 1477 | | |
| | | 1478 | | if (!Number.isFinite(warehouseId) || warehouseId <= 0) { |
| | | 1479 | | showNotification('Нельзя создать остаток: некорректный warehouse_id в связи складов', 'danger'); |
| | | 1480 | | return; |
| | | 1481 | | } |
| | | 1482 | | |
| | | 1483 | | if (!Number.isFinite(stock) || stock < 0) { |
| | | 1484 | | showNotification('Нельзя создать остаток: некорректное количество', 'danger'); |
| | | 1485 | | return; |
| | | 1486 | | } |
| | | 1487 | | |
| | | 1488 | | const originalLabel = createButton.innerHTML; |
| | | 1489 | | createButton.disabled = true; |
| | | 1490 | | createButton.innerHTML = 'Создание...'; |
| | | 1491 | | |
| | | 1492 | | const isSuccess = await updateOzonProductStock({ |
| | | 1493 | | OzonClient: clientName, |
| | | 1494 | | OfferId: offerId, |
| | | 1495 | | ProductId: ozonProductId, |
| | | 1496 | | Stock: stock, |
| | | 1497 | | WarehouseId: warehouseId |
| | | 1498 | | }); |
| | | 1499 | | |
| | | 1500 | | if (isSuccess) { |
| | | 1501 | | appendCreatedOzonStoreRow(productId, { |
| | | 1502 | | clientName, |
| | | 1503 | | offerId, |
| | | 1504 | | ozonProductId, |
| | | 1505 | | warehouseId, |
| | | 1506 | | stock, |
| | | 1507 | | bitrixStoreName, |
| | | 1508 | | ozonWarehouseName |
| | | 1509 | | }); |
| | | 1510 | | createButton.closest('.sync-action-chip')?.remove(); |
| | | 1511 | | showNotification(`Остаток синхронизирован: ${bitrixStoreName} -> ${ozonWarehouseName || 'Ozon склад'}`, 'suc |
| | | 1512 | | return; |
| | | 1513 | | } |
| | | 1514 | | |
| | | 1515 | | createButton.disabled = false; |
| | | 1516 | | createButton.innerHTML = originalLabel; |
| | | 1517 | | }); |
| | | 1518 | | |
| | | 1519 | | |
| | | 1520 | | function normalizeOfferId(...values) { |
| | | 1521 | | for (const value of values) { |
| | | 1522 | | if (value === null || value === undefined) continue; |
| | | 1523 | | const normalized = String(value).trim(); |
| | | 1524 | | if (normalized.length > 0) return normalized; |
| | | 1525 | | } |
| | | 1526 | | return ""; |
| | | 1527 | | } |
| | | 1528 | | |
| | | 1529 | | function getBitrixStoreNamesForOzonFetch(explicitStoreNames) { |
| | | 1530 | | if (Array.isArray(explicitStoreNames) && explicitStoreNames.length > 0) { |
| | | 1531 | | return explicitStoreNames.map(s => String(s).trim()).filter(Boolean); |
| | | 1532 | | } |
| | | 1533 | | |
| | | 1534 | | const fromChecks = Array.from(document.querySelectorAll('input[name="SelectedStoreTitles"]:checked')) |
| | | 1535 | | .map(cb => cb.value.trim()) |
| | | 1536 | | .filter(Boolean); |
| | | 1537 | | if (fromChecks.length > 0) { |
| | | 1538 | | return fromChecks; |
| | | 1539 | | } |
| | | 1540 | | |
| | | 1541 | | return [...new Set((window.warehouseMappings || []) |
| | | 1542 | | .map(m => m.bitrixWarehouseName) |
| | | 1543 | | .filter(Boolean) |
| | | 1544 | | .map(s => String(s).trim()))]; |
| | | 1545 | | } |
| | | 1546 | | |
| | | 1547 | | async function ensureWarehouseMappingsForClient(clientName, bitrixStoreNames) { |
| | | 1548 | | const names = getBitrixStoreNamesForOzonFetch(bitrixStoreNames); |
| | | 1549 | | if (!clientName || names.length === 0) { |
| | | 1550 | | return []; |
| | | 1551 | | } |
| | | 1552 | | |
| | | 1553 | | try { |
| | | 1554 | | const response = await fetch('/BitrixStock/EnsureWarehouseMappings', { |
| | | 1555 | | method: 'POST', |
| | | 1556 | | headers: {'Content-Type': 'application/json'}, |
| | | 1557 | | body: JSON.stringify({clientName, bitrixWarehouseNames: names}) |
| | | 1558 | | }); |
| | | 1559 | | if (!response.ok) { |
| | | 1560 | | return []; |
| | | 1561 | | } |
| | | 1562 | | |
| | | 1563 | | const data = await response.json(); |
| | | 1564 | | const created = data.created || []; |
| | | 1565 | | window.warehouseMappings = window.warehouseMappings || []; |
| | | 1566 | | created.forEach(item => { |
| | | 1567 | | const exists = window.warehouseMappings.some(m => |
| | | 1568 | | normalizeWarehouseName(m.bitrixWarehouseName) === normalizeWarehouseName(item.bitrixWarehouseName) & |
| | | 1569 | | normalizeWarehouseName(m.ozonClientName) === normalizeWarehouseName(item.ozonClientName)); |
| | | 1570 | | if (!exists) { |
| | | 1571 | | window.warehouseMappings.push({ |
| | | 1572 | | Id: 0, |
| | | 1573 | | bitrixWarehouseName: item.bitrixWarehouseName, |
| | | 1574 | | ozonWarehouseName: item.ozonWarehouseName, |
| | | 1575 | | ozonClientName: item.ozonClientName, |
| | | 1576 | | ozonWarehouseId: item.ozonWarehouseId |
| | | 1577 | | }); |
| | | 1578 | | } |
| | | 1579 | | }); |
| | | 1580 | | if (created.length > 0) { |
| | | 1581 | | renderMissingOzonCreateButtons(); |
| | | 1582 | | } |
| | | 1583 | | return created; |
| | | 1584 | | } catch (error) { |
| | | 1585 | | console.warn('EnsureWarehouseMappings failed', error); |
| | | 1586 | | return []; |
| | | 1587 | | } |
| | | 1588 | | } |
| | | 1589 | | |
| | | 1590 | | function enrichOzonDataWithWarehouseConnections(ozonData) { |
| | | 1591 | | if (!Array.isArray(ozonData)) { |
| | | 1592 | | return ozonData; |
| | | 1593 | | } |
| | | 1594 | | |
| | | 1595 | | ozonData.forEach(clientData => { |
| | | 1596 | | if (!clientData?.WarehouseInfo?.result) { |
| | | 1597 | | return; |
| | | 1598 | | } |
| | | 1599 | | |
| | | 1600 | | clientData.WarehouseInfo.result = clientData.WarehouseInfo.result.filter(warehouse => |
| | | 1601 | | checkWarehouseConnection(warehouse.warehouse_name, null, clientData.Client) && |
| | | 1602 | | Number(warehouse.present) > 0); |
| | | 1603 | | |
| | | 1604 | | clientData.WarehouseInfo.result.forEach(warehouse => { |
| | | 1605 | | warehouse.hasBitrixConnection = true; |
| | | 1606 | | }); |
| | | 1607 | | }); |
| | | 1608 | | |
| | | 1609 | | return ozonData; |
| | | 1610 | | } |
| | | 1611 | | |
| | | 1612 | | function applyOzonTableResponse(ozonResponse) { |
| | | 1613 | | if (!ozonResponse || ozonResponse.length === 0) { |
| | | 1614 | | console.warn('Данные от Ozon не получены'); |
| | | 1615 | | $("#ozonTableContainer").empty(); |
| | | 1616 | | return; |
| | | 1617 | | } |
| | | 1618 | | |
| | | 1619 | | enrichOzonDataWithWarehouseConnections(ozonResponse); |
| | | 1620 | | const ozonHtml = renderOzonTable(ozonResponse); |
| | | 1621 | | if (ozonHtml) { |
| | | 1622 | | $("#ozonDataSection").show(); |
| | | 1623 | | $("#ozonTableContainer").html(ozonHtml); |
| | | 1624 | | mountOzonDataGrid(); |
| | | 1625 | | const inputVal = parseFloat($('input[name="PriceValue"]').val()) || 0; |
| | | 1626 | | recalcRanges(inputVal); |
| | | 1627 | | } |
| | | 1628 | | } |
| | | 1629 | | |
| | | 1630 | | async function reloadOzonPanel(offerId, storeNames, clientName) { |
| | | 1631 | | const normalizedOfferId = normalizeOfferId(offerId); |
| | | 1632 | | if (!normalizedOfferId) { |
| | | 1633 | | return null; |
| | | 1634 | | } |
| | | 1635 | | |
| | | 1636 | | const names = getBitrixStoreNamesForOzonFetch(storeNames); |
| | | 1637 | | if (clientName) { |
| | | 1638 | | await ensureWarehouseMappingsForClient(clientName, names); |
| | | 1639 | | } else { |
| | | 1640 | | const clients = [...new Set((window.warehouseMappings || []) |
| | | 1641 | | .map(m => m.ozonClientName) |
| | | 1642 | | .filter(Boolean))]; |
| | | 1643 | | for (const c of clients) { |
| | | 1644 | | await ensureWarehouseMappingsForClient(c, names); |
| | | 1645 | | } |
| | | 1646 | | } |
| | | 1647 | | |
| | | 1648 | | const ozonResponse = await fetchOzonData(normalizedOfferId, names); |
| | | 1649 | | applyOzonTableResponse(ozonResponse); |
| | | 1650 | | return ozonResponse; |
| | | 1651 | | } |
| | | 1652 | | |
| | | 1653 | | function fetchOzonData(article, storeNames) { |
| | | 1654 | | const offerId = normalizeOfferId(article); |
| | | 1655 | | if (!offerId) { |
| | | 1656 | | console.warn('Пустой offer_id для запроса Ozon'); |
| | | 1657 | | return Promise.resolve([]); |
| | | 1658 | | } |
| | | 1659 | | |
| | | 1660 | | return $.ajax({ |
| | 0 | 1661 | | url: '@Url.Action("GetOzonPrices", "BitrixStock")', |
| | | 1662 | | type: "GET", |
| | | 1663 | | data: { |
| | | 1664 | | article: offerId, |
| | | 1665 | | storeNames: getBitrixStoreNamesForOzonFetch(storeNames) |
| | | 1666 | | }, |
| | | 1667 | | traditional: true // важно для корректной передачи List<string> в ASP.NET |
| | | 1668 | | }).catch(error => { |
| | | 1669 | | if (error.status === 500) { |
| | | 1670 | | console.error('Ошибка 500 при получении данных из Ozon:', error.responseText); |
| | | 1671 | | } |
| | | 1672 | | return null; |
| | | 1673 | | }); |
| | | 1674 | | } |
| | | 1675 | | |
| | | 1676 | | function GetTradesoftDataForBitrix(article, producer) { |
| | | 1677 | | return $.ajax({ |
| | 0 | 1678 | | url: '@Url.Action("GetTradesoftDataForBitrix", "Tradesoft")', |
| | | 1679 | | type: "GET", |
| | | 1680 | | data: {article: article, producer: producer} |
| | | 1681 | | }).catch(error => { |
| | | 1682 | | if (error.status === 500 || error.status === 404) { |
| | | 1683 | | console.error('Ошибка 500 при получении данных от Tradesoft:', error); |
| | | 1684 | | } |
| | | 1685 | | return []; |
| | | 1686 | | }); |
| | | 1687 | | } |
| | | 1688 | | |
| | | 1689 | | function GetAbcpDataForBitrix(article, producer) { |
| | | 1690 | | return $.ajax({ |
| | 0 | 1691 | | url: '@Url.Action("GetAbcpDataForBitrix", "Tradesoft")', |
| | | 1692 | | type: "GET", |
| | | 1693 | | data: {article: article, producer: producer} |
| | | 1694 | | }).catch(error => { |
| | | 1695 | | if (error.status === 500) { |
| | | 1696 | | console.error('Ошибка 500 при получении данных от Abcp:', error); |
| | | 1697 | | } |
| | | 1698 | | return []; |
| | | 1699 | | }); |
| | | 1700 | | } |
| | | 1701 | | |
| | | 1702 | | function GetAvdDataForBitrix(orderId) { |
| | | 1703 | | return $.ajax({ |
| | 0 | 1704 | | url: '@Url.Action("GetAvdDataForBitrix", "Tradesoft")', |
| | | 1705 | | type: "GET", |
| | | 1706 | | data: {orderId: orderId} |
| | | 1707 | | }).catch(error => { |
| | | 1708 | | if (error.status === 500 || error.status === 404) { |
| | | 1709 | | console.error('Ошибка 500 при получении данных от Avd:', error); |
| | | 1710 | | } |
| | | 1711 | | return []; |
| | | 1712 | | }); |
| | | 1713 | | } |
| | | 1714 | | |
| | | 1715 | | function GetCombinedDataForBitrix(article, producer, regionCode = 1, orderId = null) { |
| | | 1716 | | let deferred = $.Deferred(); |
| | | 1717 | | |
| | | 1718 | | $.when( |
| | | 1719 | | GetTradesoftDataForBitrix(article, producer), |
| | | 1720 | | GetAbcpDataForBitrix(article, producer), |
| | | 1721 | | orderId ? GetAvdDataForBitrix(orderId) : $.Deferred().resolve([]) |
| | | 1722 | | ).done(function (tradesoftItems, abcpItems, avdItems) { |
| | | 1723 | | try { |
| | | 1724 | | let tItems = Array.isArray(tradesoftItems?.[0]) ? tradesoftItems[0] : tradesoftItems || []; |
| | | 1725 | | let aItems = Array.isArray(abcpItems?.[0]) ? abcpItems[0] : abcpItems || []; |
| | | 1726 | | let vItems = Array.isArray(avdItems?.[0]) ? avdItems[0] : avdItems || []; |
| | | 1727 | | |
| | | 1728 | | deferred.resolve([...tItems, ...aItems, ...vItems]); |
| | | 1729 | | } catch (e) { |
| | | 1730 | | console.error("Ошибка при объединении данных:", e); |
| | | 1731 | | deferred.resolve([]); |
| | | 1732 | | } |
| | | 1733 | | }).fail(function (error1, error2, error3, error4) { |
| | | 1734 | | console.error("Не удалось загрузить данные от одного или нескольких источников"); |
| | | 1735 | | |
| | | 1736 | | let tItems = (error1 && (error1.status === 500 || error1.status === 404)) ? [] : (error1 || []); |
| | | 1737 | | let zItems = (error2 && (error2.status === 500 || error2.status === 404)) ? [] : (error2 || []); |
| | | 1738 | | let aItems = (error3 && (error3.status === 500 || error3.status === 404)) ? [] : (error3 || []); |
| | | 1739 | | let vItems = (error4 && (error4.status === 500 || error4.status === 404)) ? [] : (error4 || []); |
| | | 1740 | | |
| | | 1741 | | if (tItems.length > 0 || zItems.length > 0 || aItems.length > 0 || vItems.length > 0) { |
| | | 1742 | | deferred.resolve([...tItems, ...zItems, ...aItems, ...vItems]); |
| | | 1743 | | } else { |
| | | 1744 | | deferred.reject(error1 || error2 || error3 || error4); |
| | | 1745 | | } |
| | | 1746 | | }); |
| | | 1747 | | |
| | | 1748 | | return deferred.promise(); |
| | | 1749 | | } |
| | | 1750 | | |
| | | 1751 | | |
| | | 1752 | | function fetchBitrixData(orderArticle) { |
| | | 1753 | | return $.ajax({ |
| | 0 | 1754 | | url: '@Url.Action("GetBitrixData", "Bitrix")', |
| | | 1755 | | type: "GET", |
| | | 1756 | | data: {article: orderArticle} |
| | | 1757 | | }).catch(error => { |
| | | 1758 | | if (error.status === 500) { |
| | | 1759 | | console.error('Ошибка 500 при получении данных из Bitrix:', error.responseText); |
| | | 1760 | | } |
| | | 1761 | | return null; |
| | | 1762 | | }); |
| | | 1763 | | } |
| | | 1764 | | |
| | | 1765 | | // «Данные Bitrix» в модалке товара — грид AG Grid: цена/склад/остаток; клик по складу (.bitrix-stock) — |
| | | 1766 | | // как раньше, выбор остатка для наката на выбранный склад Ozon. Разметка — контейнер, грид монтирует mountBitrixDat |
| | | 1767 | | let _bitrixDataRows = []; |
| | | 1768 | | let _bitrixDataGridApi = null; |
| | | 1769 | | |
| | | 1770 | | function renderBitrixTable(data) { |
| | | 1771 | | _bitrixDataRows = []; |
| | | 1772 | | if (!data || data.length === 0) { |
| | | 1773 | | return "<p class='text-muted'>Нет данных</p>"; |
| | | 1774 | | } |
| | | 1775 | | data.forEach((item, gi) => { |
| | | 1776 | | if (item.group === "Учетная цена") { |
| | | 1777 | | document.querySelector('input[name="RegistrationPrice"]').value = item.price; |
| | | 1778 | | } |
| | | 1779 | | const stores = item.stores && Object.keys(item.stores).length ? Object.entries(item.stores) : [[null, null]] |
| | | 1780 | | stores.forEach(([name, amount], index) => _bitrixDataRows.push({ |
| | | 1781 | | id: `${gi}#${index}`, first: index === 0, span: stores.length, |
| | | 1782 | | group: item.group, price: item.price, currency: item.currency, store: name, amount |
| | | 1783 | | })); |
| | | 1784 | | }); |
| | | 1785 | | return '<strong class="bs-modal-section-title">Данные Bitrix</strong><div id="bitrixDataGrid" class="bsx-grid bs |
| | | 1786 | | } |
| | | 1787 | | |
| | | 1788 | | function mountBitrixDataGrid() { |
| | | 1789 | | const el = document.getElementById('bitrixDataGrid'); |
| | | 1790 | | if (_bitrixDataGridApi) { try { _bitrixDataGridApi.destroy(); } catch (_) { } _bitrixDataGridApi = null; } |
| | | 1791 | | if (!el || !window.BsxGrid) return; |
| | | 1792 | | _bitrixDataGridApi = window.BsxGrid.create(el, { |
| | | 1793 | | key: 'bitrixData', |
| | | 1794 | | rowData: _bitrixDataRows, |
| | | 1795 | | getRowId: p => p.data.id, |
| | | 1796 | | defaultColDef: { sortable: false, filter: false, floatingFilter: false }, |
| | | 1797 | | gridOptions: { domLayout: 'autoHeight', suppressRowTransform: true, rowHeight: 30 }, |
| | | 1798 | | columnDefs: [ |
| | | 1799 | | { colId: 'group', headerName: 'Цена', width: 170, rowSpan: p => p.data?.first ? p.data.span : 1, |
| | | 1800 | | cellClassRules: { 'bsx-span-cell': p => !!p.data?.first }, cellRenderer: p => p.data?.first ? escapeHt |
| | | 1801 | | { colId: 'price', headerName: 'Значение', width: 130, type: 'numericColumn', rowSpan: p => p.data?.first |
| | | 1802 | | cellClassRules: { 'bsx-span-cell': p => !!p.data?.first }, |
| | | 1803 | | cellRenderer: p => p.data?.first |
| | | 1804 | | ? `<span data-price-name="${escapeHtml(p.data.group)}" data-price-value="${p.data.price ?? ''}">${ |
| | | 1805 | | { colId: 'store', headerName: 'Склад', flex: 1, minWidth: 120, |
| | | 1806 | | cellRenderer: p => p.data?.store == null ? '<span class="text-muted">Нет данных</span>' |
| | | 1807 | | : `<a href="#" class="bitrix-stock" data-amount="${p.data.amount}" data-name="${escapeHtml(p.data. |
| | | 1808 | | { colId: 'amount', field: 'amount', headerName: 'Всего', width: 100, type: 'numericColumn' }, |
| | | 1809 | | { colId: 'currency', field: 'currency', headerName: 'Валюта', width: 90 } |
| | | 1810 | | ] |
| | | 1811 | | }); |
| | | 1812 | | } |
| | | 1813 | | |
| | | 1814 | | function getOzonHistory() { |
| | | 1815 | | try { |
| | | 1816 | | return JSON.parse(localStorage.getItem("ozonClientHistory")) || {}; |
| | | 1817 | | } catch { |
| | | 1818 | | return {}; |
| | | 1819 | | } |
| | | 1820 | | } |
| | | 1821 | | |
| | | 1822 | | function saveOzonHistory(data) { |
| | | 1823 | | localStorage.setItem("ozonClientHistory", JSON.stringify(data)); |
| | | 1824 | | } |
| | | 1825 | | |
| | | 1826 | | // «Данные Ozon» в модалке товара — грид AG Grid: по клиенту три независимых списка (цены, комиссии и индексы, |
| | | 1827 | | // склады) рядом, клиент и валюта — объединённые ячейки. Расчёты (подсказка «Обновить цены», диапазон прибыли) |
| | | 1828 | | // читают модель _ozonClients, а не DOM. Разметка — контейнер, грид монтирует mountOzonDataGrid(). |
| | | 1829 | | let _ozonRows = []; |
| | | 1830 | | let _ozonClients = {}; |
| | | 1831 | | let _ozonGridApi = null; |
| | | 1832 | | const OZON_INDEX_TONE = { green: 'text-success', orange: 'text-warning', red: 'text-danger', gray: 'text-muted' }; |
| | | 1833 | | |
| | | 1834 | | function renderOzonTable(data) { |
| | | 1835 | | if (!data || data.length === 0) { |
| | | 1836 | | return "<p class='text-muted'>Нет данных Ozon</p>"; |
| | | 1837 | | } |
| | | 1838 | | |
| | | 1839 | | let history = getOzonHistory(); |
| | | 1840 | | const refreshOfferId = normalizeOfferId(data[0]?.Article, data[0]?.Item?.offer_id); |
| | | 1841 | | |
| | | 1842 | | _ozonRows = []; |
| | | 1843 | | _ozonClients = {}; |
| | | 1844 | | data.forEach((entry, clientIndex) => { |
| | | 1845 | | const item = entry.Item || {}; |
| | | 1846 | | const clientName = entry.Client ?? "-"; |
| | | 1847 | | const currency = item.price?.currency_code ?? "-"; |
| | | 1848 | | const clientId = `client-${clientIndex}`; |
| | | 1849 | | const article = data[0].Article; |
| | | 1850 | | const storageKey = `${article}_${clientId}`; |
| | | 1851 | | const priceValue = item.price?.price; |
| | | 1852 | | |
| | | 1853 | | const currentPrice = Number(item.price?.true_price ?? 0); |
| | | 1854 | | const currentIndex = Number(item.price_indexes?.ozon_index_data?.price_index_value ?? 0); |
| | | 1855 | | |
| | | 1856 | | let oldIndexNote = ""; |
| | | 1857 | | if (history[storageKey]) { |
| | | 1858 | | const prev = history[storageKey]; |
| | | 1859 | | if (prev.price !== currentPrice && prev.index === currentIndex) { |
| | | 1860 | | oldIndexNote = `<span style="color: gray; font-size: 0.85em;">(${prev.index})</span>`; |
| | | 1861 | | } |
| | | 1862 | | } |
| | | 1863 | | |
| | | 1864 | | history[storageKey] = {price: currentPrice, index: currentIndex}; |
| | | 1865 | | |
| | | 1866 | | // Цены с классами для раскраски |
| | | 1867 | | const priceTypes = [ |
| | | 1868 | | {key: "true_price", name: "Цена с Ozon Картой", value: item.price?.customer_price ?? item.price?.true_pr |
| | | 1869 | | { |
| | | 1870 | | key: "green_target_customer_price", |
| | | 1871 | | name: "Реком. цена с картой", |
| | | 1872 | | value: item.price?.green_target_customer_price ?? "-", |
| | | 1873 | | nameCssClass: "text-warning", |
| | | 1874 | | valueCssClass: "text-warning" |
| | | 1875 | | }, |
| | | 1876 | | { |
| | | 1877 | | key: "green_target_seller_price", |
| | | 1878 | | name: "Реком. цена продавца", |
| | | 1879 | | value: item.price?.green_target_seller_price ?? "-", |
| | | 1880 | | nameCssClass: "text-warning", |
| | | 1881 | | valueCssClass: "text-warning" |
| | | 1882 | | }, |
| | | 1883 | | { |
| | | 1884 | | key: "green_target_formula", |
| | | 1885 | | name: "Как рассчитана реком. цена", |
| | | 1886 | | value: item.price?.green_target_formula ?? "-", |
| | | 1887 | | nameCssClass: "text-muted", |
| | | 1888 | | valueCssClass: "text-muted" |
| | | 1889 | | }, |
| | | 1890 | | { |
| | | 1891 | | key: "green_target_candidates", |
| | | 1892 | | name: "Кандидаты расчета", |
| | | 1893 | | value: item.price?.green_target_candidates ?? "-", |
| | | 1894 | | nameCssClass: "text-muted", |
| | | 1895 | | valueCssClass: "text-muted" |
| | | 1896 | | }, |
| | | 1897 | | { |
| | | 1898 | | key: "customer_price_formula", |
| | | 1899 | | name: "Как рассчитана цена с картой", |
| | | 1900 | | value: item.price?.customer_price_formula ?? "-", |
| | | 1901 | | nameCssClass: "text-muted", |
| | | 1902 | | valueCssClass: "text-muted" |
| | | 1903 | | }, |
| | | 1904 | | { |
| | | 1905 | | key: "green_target_error", |
| | | 1906 | | name: "Причина отсутствия рекомендации", |
| | | 1907 | | value: item.price?.green_target_error ?? "-", |
| | | 1908 | | nameCssClass: "text-danger", |
| | | 1909 | | valueCssClass: "text-danger" |
| | | 1910 | | }, |
| | | 1911 | | { |
| | | 1912 | | key: "max_commission", |
| | | 1913 | | name: "Комиссия макс.", |
| | | 1914 | | value: item.price?.max_commission ?? "-", |
| | | 1915 | | nameCssClass: "text-success", |
| | | 1916 | | valueCssClass: "text-success" |
| | | 1917 | | }, |
| | | 1918 | | { |
| | | 1919 | | key: "min_commission", |
| | | 1920 | | name: "Комиссия мин.", |
| | | 1921 | | value: item.price?.min_commission ?? "-", |
| | | 1922 | | nameCssClass: "text-danger", |
| | | 1923 | | valueCssClass: "text-danger" |
| | | 1924 | | }, |
| | | 1925 | | { |
| | | 1926 | | key: "price_range_max", |
| | | 1927 | | name: "Прибыль макс.", |
| | | 1928 | | value: "-", |
| | | 1929 | | nameCssClass: "text-success", |
| | | 1930 | | valueCssClass: "text-success" |
| | | 1931 | | }, |
| | | 1932 | | { |
| | | 1933 | | key: "price_range_min", |
| | | 1934 | | name: "Прибыль мин.", |
| | | 1935 | | value: "-", |
| | | 1936 | | nameCssClass: "text-danger", |
| | | 1937 | | valueCssClass: "text-danger" |
| | | 1938 | | }, |
| | | 1939 | | |
| | | 1940 | | {key: "ozon_reward", name: "Вознаграждение Ozon", value: item.price?.ozon_reward ?? "-"}, |
| | | 1941 | | {key: "price", name: "Цена (со скидками)", value: item.price?.price ?? "-"}, |
| | | 1942 | | {key: "old_price", name: "Старая цена", value: item.price?.old_price ?? "-"}, |
| | | 1943 | | {key: "marketing_price", name: "Цена с акциями Ozon", value: item.price?.marketing_price ?? "-"}, |
| | | 1944 | | { |
| | | 1945 | | key: "marketing_seller_price", |
| | | 1946 | | name: "Цена с акциями продавца", |
| | | 1947 | | value: item.price?.marketing_seller_price ?? "-" |
| | | 1948 | | }, |
| | | 1949 | | {key: "min_price", name: "Мин. цена после скидок", value: item.price?.min_price ?? "-"}, |
| | | 1950 | | {key: "net_price", name: "Себестоимость", value: item.price?.net_price ?? "-"}, |
| | | 1951 | | {key: "retail_price", name: "Цена поставщика", value: item.price?.retail_price ?? "-"}, |
| | | 1952 | | {key: "customer_discount_percent", name: "Скидка до цены с картой, %", value: item.price?.customer_disco |
| | | 1953 | | {key: "vat", name: "Ставка НДС", value: item.price?.vat ?? "-"}, |
| | | 1954 | | {key: "currency_code", name: "Валюта", value: item.price?.currency_code ?? "-"}, |
| | | 1955 | | { |
| | | 1956 | | key: "auto_action_enabled", |
| | | 1957 | | name: "Автоприменение акций", |
| | | 1958 | | value: item.price?.auto_action_enabled ? "Да" : "Нет" |
| | | 1959 | | }, |
| | | 1960 | | { |
| | | 1961 | | key: "auto_add_to_ozon_actions_list_enabled", |
| | | 1962 | | name: "Автодобавление в акции", |
| | | 1963 | | value: item.price?.auto_add_to_ozon_actions_list_enabled ? "Да" : "Нет" |
| | | 1964 | | } |
| | | 1965 | | ]; |
| | | 1966 | | |
| | | 1967 | | |
| | | 1968 | | const c = item.commissions || {}; |
| | | 1969 | | const indx = item.price_indexes || {}; |
| | | 1970 | | const commissionTypes = [ |
| | | 1971 | | { |
| | | 1972 | | key: "price_index", |
| | | 1973 | | name: "Индекс цены", |
| | | 1974 | | value: indx.color_index ?? "WITHOUT_INDEX", |
| | | 1975 | | description: getColorIndexDescription(indx.color_index), |
| | | 1976 | | color: getColorIndexColor(indx.color_index) |
| | | 1977 | | }, |
| | | 1978 | | { |
| | | 1979 | | key: "green_target_index", |
| | | 1980 | | name: "Целевой индекс", |
| | | 1981 | | value: item.price?.green_target_index ?? "-" |
| | | 1982 | | }, |
| | | 1983 | | { |
| | | 1984 | | key: "green_target_source", |
| | | 1985 | | name: "Ограничивающий рынок", |
| | | 1986 | | value: item.price?.green_target_source ?? "-" |
| | | 1987 | | }, |
| | | 1988 | | { |
| | | 1989 | | key: "green_target_source_price", |
| | | 1990 | | name: "Цена ориентира", |
| | | 1991 | | value: item.price?.green_target_source_price ?? "-" |
| | | 1992 | | }, |
| | | 1993 | | { |
| | | 1994 | | key: "green_target_source_index", |
| | | 1995 | | name: "Индекс ориентира", |
| | | 1996 | | value: item.price?.green_target_source_index ?? "-" |
| | | 1997 | | }, |
| | | 1998 | | { |
| | | 1999 | | key: "ozon_min_price", |
| | | 2000 | | name: "Мин. цена у конкурентов на Ozon", |
| | | 2001 | | value: indx.ozon_index_data?.min_price ?? "-" |
| | | 2002 | | }, |
| | | 2003 | | { |
| | | 2004 | | key: "ozon_index_value", |
| | | 2005 | | name: "Индекс цены на Ozon", |
| | | 2006 | | value: indx.ozon_index_data?.price_index_value ?? "-" |
| | | 2007 | | }, |
| | | 2008 | | |
| | | 2009 | | { |
| | | 2010 | | key: "external_min_price", |
| | | 2011 | | name: "Мин. цена у конкурентов на другой площадке", |
| | | 2012 | | value: indx.external_index_data?.min_price ?? "-" |
| | | 2013 | | }, |
| | | 2014 | | { |
| | | 2015 | | key: "external_index_value", |
| | | 2016 | | name: "Индекс цены на другой площадке", |
| | | 2017 | | value: indx.external_index_data?.price_index_value ?? "-" |
| | | 2018 | | }, |
| | | 2019 | | |
| | | 2020 | | { |
| | | 2021 | | key: "self_marketplaces_min_price", |
| | | 2022 | | name: "Мин. цена вашего товара на других площадках", |
| | | 2023 | | value: indx.self_marketplaces_index_data?.min_price ?? "-" |
| | | 2024 | | }, |
| | | 2025 | | { |
| | | 2026 | | key: "self_marketplaces_index_value", |
| | | 2027 | | name: "Индекс цены на ваших площадках", |
| | | 2028 | | value: indx.self_marketplaces_index_data?.price_index_value ?? "-" |
| | | 2029 | | }, |
| | | 2030 | | {key: "ozon_reward", name: "Комиссия за продажу (FBS)", value: item.price?.ozon_reward ?? "-"}, |
| | | 2031 | | { |
| | | 2032 | | key: "sales_percent_fbs", |
| | | 2033 | | name: "Процент комиссии за продажу (FBS)", |
| | | 2034 | | value: c.sales_percent_fbs ?? "-" |
| | | 2035 | | }, |
| | | 2036 | | {key: "acquiring", name: "Эквайринг", value: item.acquiring ? Number(item.acquiring).toFixed(2) : "-"}, |
| | | 2037 | | { |
| | | 2038 | | key: "fbs_first_mile_max", |
| | | 2039 | | name: "Максимальная комиссия за обработку отправления (FBS)", |
| | | 2040 | | value: c.fbs_first_mile_max_amount ?? "-" |
| | | 2041 | | }, |
| | | 2042 | | { |
| | | 2043 | | key: "fbs_first_mile_min", |
| | | 2044 | | name: "Минимальная комиссия за обработку отправления (FBS)", |
| | | 2045 | | value: c.fbs_first_mile_min_amount ?? "-" |
| | | 2046 | | }, |
| | | 2047 | | { |
| | | 2048 | | key: "fbs_direct_flow_trans_max", |
| | | 2049 | | name: "Магистраль до (FBS)", |
| | | 2050 | | value: c.fbs_direct_flow_trans_max_amount ?? "-" |
| | | 2051 | | }, |
| | | 2052 | | { |
| | | 2053 | | key: "fbs_deliv_to_customer", |
| | | 2054 | | name: "Последняя миля (FBS)", |
| | | 2055 | | value: c.fbs_deliv_to_customer_amount ?? "-" |
| | | 2056 | | }, |
| | | 2057 | | { |
| | | 2058 | | key: "fbs_return_flow", |
| | | 2059 | | name: "Комиссия за возврат и отмену, обработка отправления (FBS)", |
| | | 2060 | | value: c.fbs_return_flow_amount ?? "-" |
| | | 2061 | | }, |
| | | 2062 | | { |
| | | 2063 | | key: "fbs_direct_flow_trans_min", |
| | | 2064 | | name: "Магистраль от (FBS)", |
| | | 2065 | | value: c.fbs_direct_flow_trans_min_amount ?? "-" |
| | | 2066 | | }, |
| | | 2067 | | ]; |
| | | 2068 | | |
| | | 2069 | | let warehouseTypes = []; |
| | | 2070 | | if (entry.WarehouseInfo?.result && entry.WarehouseInfo.result.length > 0) { |
| | | 2071 | | warehouseTypes = entry.WarehouseInfo.result |
| | | 2072 | | .filter(w => w.hasBitrixConnection !== false) |
| | | 2073 | | .map(w => { |
| | | 2074 | | const present = w.present ?? 0; |
| | | 2075 | | const reserved = w.reserved ?? 0; |
| | | 2076 | | const hasConnection = w.hasBitrixConnection ?? false; |
| | | 2077 | | return { |
| | | 2078 | | key: w.warehouse_id ?? "", |
| | | 2079 | | name: w.warehouse_name ?? "-", |
| | | 2080 | | total: present, |
| | | 2081 | | reserved: reserved, |
| | | 2082 | | offer_id: item.offer_id, |
| | | 2083 | | product_id: item.product_id, |
| | | 2084 | | client: entry.Client, |
| | | 2085 | | hasBitrixConnection: hasConnection |
| | | 2086 | | }; |
| | | 2087 | | }) |
| | | 2088 | | .sort((a, b) => { |
| | | 2089 | | const aHasStock = a.total > 0; |
| | | 2090 | | const bHasStock = b.total > 0; |
| | | 2091 | | const aHasConnection = a.hasBitrixConnection; |
| | | 2092 | | const bHasConnection = b.hasBitrixConnection; |
| | | 2093 | | |
| | | 2094 | | // Сначала склады с наличием и связью с Bitrix |
| | | 2095 | | if (aHasStock && aHasConnection && (!bHasStock || !bHasConnection)) return -1; |
| | | 2096 | | if (bHasStock && bHasConnection && (!aHasStock || !aHasConnection)) return 1; |
| | | 2097 | | |
| | | 2098 | | // Затем склады с наличием |
| | | 2099 | | if (aHasStock && !bHasStock) return -1; |
| | | 2100 | | if (!aHasStock && bHasStock) return 1; |
| | | 2101 | | |
| | | 2102 | | // Затем склады со связью с Bitrix |
| | | 2103 | | if (aHasConnection && !bHasConnection) return -1; |
| | | 2104 | | if (!aHasConnection && bHasConnection) return 1; |
| | | 2105 | | |
| | | 2106 | | return a.name.localeCompare(b.name); |
| | | 2107 | | }); |
| | | 2108 | | } |
| | | 2109 | | const effectiveOfferId = normalizeOfferId(warehouseTypes[0]?.offer_id, item.offer_id, entry.Article, article |
| | | 2110 | | |
| | | 2111 | | const maxRows = Math.max(priceTypes.length, commissionTypes.length, warehouseTypes.length); |
| | | 2112 | | const byKey = list => Object.fromEntries(list.filter(x => x.key).map(x => [x.key, x.value])); |
| | | 2113 | | _ozonClients[clientId] = { prices: byKey(priceTypes), commissions: byKey(commissionTypes), rangeMin: '-', ra |
| | | 2114 | | |
| | | 2115 | | for (let i = 0; i < maxRows; i++) { |
| | | 2116 | | _ozonRows.push({ |
| | | 2117 | | id: `${clientId}#${i}`, clientId, first: i === 0, span: maxRows, |
| | | 2118 | | clientName, effectiveOfferId, productId: warehouseTypes[i]?.product_id ?? warehouseTypes[0]?.product |
| | | 2119 | | priceValue, currency, oldIndexNote, |
| | | 2120 | | price: priceTypes[i], commission: commissionTypes[i], warehouse: warehouseTypes[i] |
| | | 2121 | | }); |
| | | 2122 | | } |
| | | 2123 | | }); |
| | | 2124 | | saveOzonHistory(history); |
| | | 2125 | | return ` |
| | | 2126 | | <strong class="bs-modal-section-title d-inline-flex align-items-center gap-2"> |
| | | 2127 | | Данные Ozon |
| | | 2128 | | <button type="button" class="btn btn-sm btn-link p-0 border-0 refreshOzon" data-article="${refreshOfferId}" |
| | | 2129 | | <i class="fas fa-rotate-right text-primary"></i> |
| | | 2130 | | </button> |
| | | 2131 | | </strong> |
| | | 2132 | | <div id="ozonDataGrid" class="bsx-grid bsx-grid--auto"></div>`; |
| | | 2133 | | } |
| | | 2134 | | |
| | | 2135 | | function mountOzonDataGrid() { |
| | | 2136 | | const el = document.getElementById('ozonDataGrid'); |
| | | 2137 | | if (_ozonGridApi) { try { _ozonGridApi.destroy(); } catch (_) { } _ozonGridApi = null; } |
| | | 2138 | | if (!el || !window.BsxGrid) return; |
| | | 2139 | | const span = p => p.data?.first ? p.data.span : 1; |
| | | 2140 | | const spanCls = { 'bsx-span-cell': p => !!p.data?.first }; |
| | | 2141 | | const attr = v => escapeHtml(v ?? ''); |
| | | 2142 | | const calcKeys = ['green_target_formula', 'green_target_candidates', 'customer_price_formula']; |
| | | 2143 | | _ozonGridApi = window.BsxGrid.create(el, { |
| | | 2144 | | key: 'ozonData', |
| | | 2145 | | rowData: _ozonRows, |
| | | 2146 | | getRowId: p => p.data.id, |
| | | 2147 | | defaultColDef: { sortable: false, filter: false, floatingFilter: false, tooltipValueGetter: () => null }, |
| | | 2148 | | gridOptions: { domLayout: 'autoHeight', suppressRowTransform: true, rowHeight: 30, suppressCellFocus: true } |
| | | 2149 | | columnDefs: [ |
| | | 2150 | | { colId: 'client', headerName: 'Клиент', width: 190, rowSpan: span, cellClassRules: spanCls, |
| | | 2151 | | cellClass: 'ozon-client-cell', |
| | | 2152 | | // contenteditable индекс/процент внутри ячейки — клавиши не перехватывает грид. |
| | | 2153 | | suppressKeyboardEvent: () => true, |
| | | 2154 | | cellRenderer: p => !p.data?.first ? '' : `<div class="client-cell" data-client-id="${p.data.clientId}" |
| | | 2155 | | <div class="fw-semibold">${escapeHtml(p.data.clientName)}</div> |
| | | 2156 | | <a href="#" class="update_price text-decoration-none" data-client-id="${p.data.clientId}" |
| | | 2157 | | data-offer-id="${attr(p.data.effectiveOfferId)}" data-product-id="${attr(p.data.productId)}" |
| | | 2158 | | data-client="${attr(p.data.clientName)}" data-bs-toggle="popover" data-bs-trigger="hover focu |
| | | 2159 | | data-bs-placement="right" data-bs-html="true" data-constant-index="0.90" data-percent="2"> |
| | | 2160 | | Обновить цены |
| | | 2161 | | <small class="text-muted">(<span class="constant-index" contenteditable="true">0.90</span> / |
| | | 2162 | | <span class="percent" contenteditable="true">2</span>%)</small> |
| | | 2163 | | </a><br> |
| | | 2164 | | <a href="#" class="set_price text-decoration-none" data-offer-id="${attr(p.data.effectiveOfferId |
| | | 2165 | | data-product-id="${attr(p.data.productId)}" data-client="${attr(p.data.clientName)}" |
| | | 2166 | | data-price-value="${attr(p.data.priceValue)}">Установить цену</a> |
| | | 2167 | | </div>` }, |
| | | 2168 | | { colId: 'priceName', headerName: 'Цена', width: 210, tooltipValueGetter: p => p.data?.price?.name, |
| | | 2169 | | cellClass: p => ['price-name', p.data?.price?.key === 'true_price' ? 'text-warning fw-semibold' : (p.d |
| | | 2170 | | valueGetter: p => p.data?.price?.name ?? '' }, |
| | | 2171 | | { colId: 'priceValue', headerName: 'Значение', width: 150, type: 'numericColumn', |
| | | 2172 | | tooltipValueGetter: p => p.data?.price?.value, |
| | | 2173 | | cellClass: p => ['price-cell', p.data?.price?.key === 'true_price' ? 'text-warning fw-semibold' : (p.d |
| | | 2174 | | calcKeys.includes(p.data?.price?.key) ? 'ozon-calc-cell' : ''].join(' '), |
| | | 2175 | | cellRenderer: p => { |
| | | 2176 | | const pr = p.data?.price; |
| | | 2177 | | if (!pr) return ''; |
| | | 2178 | | const client = _ozonClients[p.data.clientId] || {}; |
| | | 2179 | | if (pr.key === 'price_range_min') return `<span class="price-range-min">${escapeHtml(client.rangeM |
| | | 2180 | | if (pr.key === 'price_range_max') return `<span class="price-range-max">${escapeHtml(client.rangeM |
| | | 2181 | | if (calcKeys.includes(pr.key)) return buildCalculationPreview(pr.value); |
| | | 2182 | | return pr.value ? escapeHtml(formatNumber(pr.value)) : ''; |
| | | 2183 | | } }, |
| | | 2184 | | { colId: 'commissionName', headerName: 'Комиссия', flex: 1, minWidth: 220, tooltipValueGetter: p => p.da |
| | | 2185 | | cellClass: 'commission-name', valueGetter: p => p.data?.commission?.name ?? '' }, |
| | | 2186 | | { colId: 'commissionValue', headerName: 'Значение', width: 150, type: 'numericColumn', |
| | | 2187 | | tooltipValueGetter: p => p.data?.commission?.key === 'price_index' ? p.data.commission.description : p |
| | | 2188 | | cellClass: p => { |
| | | 2189 | | const c = p.data?.commission; |
| | | 2190 | | if (c?.key === 'ozon_index_value' && !isNaN(+c.value)) return 'commission-cell fw-semibold ' + (+c |
| | | 2191 | | return 'commission-cell'; |
| | | 2192 | | }, |
| | | 2193 | | cellRenderer: p => { |
| | | 2194 | | const c = p.data?.commission; |
| | | 2195 | | if (!c) return ''; |
| | | 2196 | | if (c.key === 'price_index' && c.color) |
| | | 2197 | | return `<span class="fw-bold ${OZON_INDEX_TONE[c.color] || ''}">${escapeHtml(c.description)}</ |
| | | 2198 | | return `${escapeHtml(c.value ?? '')} ${c.key === 'ozon_index_value' ? (p.data.oldIndexNote || '') |
| | | 2199 | | } }, |
| | | 2200 | | { colId: 'warehouse', headerName: 'Склад', width: 220, tooltipValueGetter: p => p.data?.warehouse?.name, |
| | | 2201 | | cellRenderer: p => { |
| | | 2202 | | const w = p.data?.warehouse; |
| | | 2203 | | if (!w) return ''; |
| | | 2204 | | const ok = !!w.hasBitrixConnection; |
| | | 2205 | | return `<a href="#" class="warehouse-link ${ok ? 'text-success fw-bold' : ''}" data-key="${attr(w. |
| | | 2206 | | data-offer-id="${attr(w.offer_id)}" data-product-id="${attr(w.product_id)}" data-client="${att |
| | | 2207 | | data-warehouse-id="${attr(w.key)}" data-warehouse-name="${attr(w.name)}" data-has-connection=" |
| | | 2208 | | title="${attr(w.name)}">${escapeHtml(w.name)}${ok ? ' <i class="fas fa-link ms-1 text-success" |
| | | 2209 | | } }, |
| | | 2210 | | { colId: 'total', headerName: 'Всего', width: 90, type: 'numericColumn', |
| | | 2211 | | cellRenderer: p => { |
| | | 2212 | | const w = p.data?.warehouse; |
| | | 2213 | | if (!w) return ''; |
| | | 2214 | | const ok = !!w.hasBitrixConnection; |
| | | 2215 | | return `<a href="#" class="warehouse-set-link ${ok ? 'text-success fw-bold' : ''}" data-key="${att |
| | | 2216 | | data-offer-id="${attr(w.offer_id)}" data-product-id="${attr(w.product_id)}" data-client="${att |
| | | 2217 | | data-warehouse-id="${attr(w.key)}" data-warehouse-name="${attr(w.name)}" data-total="${w.total |
| | | 2218 | | data-has-connection="${ok}" title="Установить остаток">${w.total ?? ''}</a>`; |
| | | 2219 | | } }, |
| | | 2220 | | { colId: 'reserved', headerName: 'Резерв', width: 90, type: 'numericColumn', |
| | | 2221 | | cellClass: p => p.data?.warehouse?.hasBitrixConnection ? 'text-success fw-bold' : '', |
| | | 2222 | | valueGetter: p => p.data?.warehouse?.reserved ?? '' }, |
| | | 2223 | | { colId: 'currency', headerName: 'Валюта', width: 80, rowSpan: span, cellClassRules: spanCls, |
| | | 2224 | | valueGetter: p => p.data?.first ? p.data.currency : '' } |
| | | 2225 | | ] |
| | | 2226 | | }); |
| | | 2227 | | } |
| | | 2228 | | |
| | | 2229 | | function renderOrderDetailedInformationForBitrix(response, pricesTradesoft) { |
| | | 2230 | | // Берем изображения из imageUrls (новое поле в конце ответа) |
| | | 2231 | | let allImages = response.imageUrls || []; |
| | | 2232 | | |
| | | 2233 | | // Если imageUrls пустой, используем fallback изображение |
| | | 2234 | | if (allImages.length === 0) { |
| | | 2235 | | allImages = ["https://s3.timeweb.cloud/25f554fc-6f66254e-9650-4d17-8e13-77b5b7d3242e/AppData/Studio2/IMG/300 |
| | | 2236 | | } |
| | | 2237 | | |
| | | 2238 | | // Определяем главное изображение (первое в списке) |
| | | 2239 | | const mainImage = allImages[0]; |
| | | 2240 | | |
| | | 2241 | | // Миниатюры (все изображения кроме первого, ограничиваем до 4 штук) |
| | | 2242 | | const limitedMiniatures = allImages.slice(1, 5); |
| | | 2243 | | |
| | | 2244 | | const orderId = 1; |
| | | 2245 | | |
| | | 2246 | | // Цветовая карта для поставщиков |
| | | 2247 | | const colorMap = { |
| | | 2248 | | 'https://www.maxi.parts/': 'text-success', |
| | | 2249 | | 'https://www.abcp.ru/': 'text-primary', |
| | | 2250 | | 'https://www.avdmotors.ru/': 'text-info' |
| | | 2251 | | }; |
| | | 2252 | | |
| | | 2253 | | return ` |
| | | 2254 | | <div class="d-flex" style="align-items: stretch;"> |
| | | 2255 | | <div class="main-content me-4" style="flex: 1;"> |
| | | 2256 | | ${response.detailInfo?.description || response.productInfo?.description ? ` |
| | | 2257 | | <p> |
| | | 2258 | | ${(response.detailInfo?.description || response.productInfo?.description).length > 100 |
| | | 2259 | | ? (response.detailInfo?.description || response.productInfo?.description).slice(0, 100) + '...' |
| | | 2260 | | : (response.detailInfo?.description || response.productInfo?.description)} |
| | | 2261 | | </p> |
| | | 2262 | | ` : ''} |
| | | 2263 | | |
| | | 2264 | | <div class="d-flex justify-content-start align-items-start"> |
| | | 2265 | | <div class="gallery float-start me-3"> |
| | | 2266 | | <div class="main-image mb-2"> |
| | | 2267 | | <img src="${mainImage}" alt="Main Image" class="img-fluid rounded border" style="width: 265px; h |
| | | 2268 | | </div> |
| | | 2269 | | ${limitedMiniatures.length > 0 ? ` |
| | | 2270 | | <div class="miniatures d-flex justify-content-start"> |
| | | 2271 | | ${limitedMiniatures.map((img, index) => ` |
| | | 2272 | | <div class="miniature mx-1"> |
| | | 2273 | | <img src="${img}" alt="Thumbnail" class="img-thumbnail" style="width: 60px; height: |
| | | 2274 | | </div> |
| | | 2275 | | `).join('')} |
| | | 2276 | | </div> |
| | | 2277 | | ` : ''} |
| | | 2278 | | </div> |
| | | 2279 | | |
| | | 2280 | | <div class="mb-3"> |
| | | 2281 | | ${response.detailInfo?.articleSchema?.dataSupplierArticleNumber || response.productInfo?.articlE_NUM |
| | | 2282 | | ? ` |
| | | 2283 | | <strong>Артикул:</strong> |
| | | 2284 | | ${response.detailInfo?.articleSchema?.dataSupplierArticleNumber || response.productInfo? |
| | | 2285 | | ${response.detailInfo?.normalizedArticle && response.detailInfo.normalizedArticle !== (r |
| | | 2286 | | ? `(${response.detailInfo.normalizedArticle})` |
| | | 2287 | | : ''} |
| | | 2288 | | |
| | | 2289 | | ${response.detailInfo?.supplier?.name || response.productInfo?.manufactu |
| | | 2290 | | ? `: ${response.detailInfo?.supplier?.name || response.productInfo?.manufacturer}` |
| | | 2291 | | : ''} |
| | | 2292 | | |
| | | 2293 | | ${response.detailInfo?.supplier?.code || response.productInfo?.manufactu |
| | | 2294 | | ? ` ${response.detailInfo?.supplier?.code || response.productInfo?.manufacturer}` |
| | | 2295 | | : ''} |
| | | 2296 | | |
| | | 2297 | | ${response.detailInfo?.supplier?.rating !== undefined |
| | | 2298 | | ? ` ${response.detailInfo.supplier.rating}` |
| | | 2299 | | : ''} |
| | | 2300 | | |
| | | 2301 | | ${response.detailInfo?.supplier?.tecdocSupplierId || response.productInf |
| | | 2302 | | ? `(${response.detailInfo?.supplier?.tecdocSupplierId || response.productInfo?.teC_DOC_PROD})` |
| | | 2303 | | : ''} |
| | | 2304 | | |
| | | 2305 | | ${response.detailInfo?.description || response.productInfo?.description |
| | | 2306 | | ? ` ${(response.detailInfo?.description || response.productInfo?.description).length > 30 |
| | | 2307 | | ? (response.detailInfo?.description || response.productInfo?.description).slice(0, 30) + '...' |
| | | 2308 | | : (response.detailInfo?.description || response.productInfo?.description)}` |
| | | 2309 | | : ''} |
| | | 2310 | | <br/> |
| | | 2311 | | ` |
| | | 2312 | | : ''} |
| | | 2313 | | <br/> |
| | | 2314 | | ${response.productInfo ? ` |
| | | 2315 | | <strong>Доп. информация:</strong><br> |
| | | 2316 | | <ul style="padding-left: 20px;"> |
| | | 2317 | | ${response.productInfo.shorT_DESCRIPTION ? `<li><strong>Категория:</strong> ${response.produ |
| | | 2318 | | ${response.productInfo.barcodes ? `<li><strong>Штрих-коды:</strong> ${response.productInfo.b |
| | | 2319 | | ${response.productInfo.packagE_WEIGHT ? ` |
| | | 2320 | | <li> |
| | | 2321 | | <strong>Вес:</strong> |
| | | 2322 | | <span class="weight-value" |
| | | 2323 | | style="cursor: pointer; text-decoration: underline; color: blue;" |
| | | 2324 | | data-weight="${response.productInfo.packagE_WEIGHT}" |
| | | 2325 | | data-order-id="${response.orderId || ''}"> |
| | | 2326 | | ${response.productInfo.packagE_WEIGHT} кг |
| | | 2327 | | </span> |
| | | 2328 | | </li> |
| | | 2329 | | ` : ''} |
| | | 2330 | | ${response.productInfo.packagE_LENGTH && response.productInfo.packagE_WIDTH && response.prod |
| | | 2331 | | `<li><strong>Габариты упаковки:</strong> ${response.productInfo.packagE_LENGTH}×${response.productInfo.packa |
| | | 2332 | | </ul> |
| | | 2333 | | ` : ''} |
| | | 2334 | | |
| | | 2335 | | ${response.detailInfo?.detailAttributes?.length > 0 ? ` |
| | | 2336 | | <strong>Характеристики:</strong><br> |
| | | 2337 | | <ul class="attributes-list" style="font-size: 18px; max-height: 200px; overflow-y: auto; padding-lef |
| | | 2338 | | ${response.detailInfo.detailAttributes |
| | | 2339 | | .filter(attr => attr.displayTitle || attr.description || attr.displayValue) |
| | | 2340 | | .map(attr => { |
| | | 2341 | | const title = attr.displayTitle || attr.description || ''; |
| | | 2342 | | const value = attr.displayValue || ''; |
| | | 2343 | | const isWeight = /Вес \[кг\]|Масса/i.test(title); |
| | | 2344 | | |
| | | 2345 | | const displayValue = isWeight && value |
| | | 2346 | | ? `<span class="weight-value" |
| | | 2347 | | style="cursor: pointer; text-decoration: underline; color: blue;" |
| | | 2348 | | data-weight="${value}" |
| | | 2349 | | data-order-id="${orderId}"> |
| | | 2350 | | ${value} кг |
| | | 2351 | | </span>` |
| | | 2352 | | : value ? `- ${value.length > 15 ? value.slice(0, 15) + '...' : value}` : ''; |
| | | 2353 | | |
| | | 2354 | | return `<li title="${attr.description || ''} - ${value}"> |
| | | 2355 | | ${title.length > 15 ? title.slice(0, 15) + '...' : title} ${displayValue} |
| | | 2356 | | </li>`; |
| | | 2357 | | }).join('')} |
| | | 2358 | | </ul> |
| | | 2359 | | ` : ''} |
| | | 2360 | | |
| | | 2361 | | <div class="form-group d-flex align-items-center gap-2"> |
| | | 2362 | | <label for="ProductWeight_${orderId}" class="form-label mb-0">Вес:</label> |
| | | 2363 | | <input class="form-control form-control-sm product-weight-field" id="ProductWeight_${orderId}" s |
| | | 2364 | | </div> |
| | | 2365 | | </div> |
| | | 2366 | | </div> |
| | | 2367 | | </div> |
| | | 2368 | | <div class="table-content" |
| | | 2369 | | style="width: 60%; height: 400px; overflow-y: auto;"> |
| | | 2370 | | ${generateStockTradeTable(orderId, pricesTradesoft, colorMap)} |
| | | 2371 | | ${applyColumnState('.stock-trade-table')} |
| | | 2372 | | </div> |
| | | 2373 | | </div> |
| | | 2374 | | `; |
| | | 2375 | | } |
| | | 2376 | | |
| | | 2377 | | function deletePriceHistory(id, bitrixId) { |
| | | 2378 | | St2Dialog.confirm('Вы уверены, что хотите удалить эту запись?', { variant: 'danger', confirmText: 'Удалить' }) |
| | | 2379 | | .then(ok => { |
| | | 2380 | | if (!ok) return; |
| | | 2381 | | deletePriceHistoryConfirmed(id, bitrixId); |
| | | 2382 | | }); |
| | | 2383 | | return false; |
| | | 2384 | | } |
| | | 2385 | | |
| | | 2386 | | function deletePriceHistoryConfirmed(id, bitrixId) { |
| | | 2387 | | const token = $('input[name="__RequestVerificationToken"]').val(); |
| | | 2388 | | |
| | | 2389 | | $.ajax({ |
| | | 2390 | | url: `/PriceHistory/Delete/${id}`, |
| | | 2391 | | type: 'POST', |
| | | 2392 | | headers: { |
| | | 2393 | | 'RequestVerificationToken': token, |
| | | 2394 | | 'X-Requested-With': 'XMLHttpRequest' |
| | | 2395 | | }, |
| | | 2396 | | success: function (response) { |
| | | 2397 | | if (response === 'ok') { |
| | | 2398 | | showNotification('Запись успешно удалена!', 'success'); |
| | | 2399 | | loadPriceHistory(bitrixId); |
| | | 2400 | | } else { |
| | | 2401 | | showNotification('Ошибка при удалении записи', 'error'); |
| | | 2402 | | } |
| | | 2403 | | }, |
| | | 2404 | | error: function (xhr, status, error) { |
| | | 2405 | | showNotification('Ошибка сервера: ' + error, 'error'); |
| | | 2406 | | } |
| | | 2407 | | }); |
| | | 2408 | | |
| | | 2409 | | return false; // Предотвращаем стандартную отправку формы |
| | | 2410 | | } |
| | | 2411 | | |
| | | 2412 | | // Функция для отображения таблицы истории цен |
| | | 2413 | | // История цен в модалке товара — грид AG Grid (сортировка, фильтр по источнику/автору), колонки выровнены |
| | | 2414 | | // под форму новой цены над ним. Разметка — контейнер, грид монтирует mountPriceHistoryGrid(). |
| | | 2415 | | let _priceHistoryRows = []; |
| | | 2416 | | let _priceHistoryGridApi = null; |
| | | 2417 | | |
| | | 2418 | | function renderPriceHistoryTable(data) { |
| | | 2419 | | _priceHistoryRows = data || []; |
| | | 2420 | | if (!data || data.length === 0) { |
| | | 2421 | | return ` |
| | | 2422 | | <div class="alert alert-info"> |
| | | 2423 | | <i class="bi bi-info-circle"></i> История изменений цен не найдена |
| | | 2424 | | </div> |
| | | 2425 | | `; |
| | | 2426 | | } |
| | | 2427 | | return '<div id="priceHistoryGrid" class="bsx-grid bsx-grid--modal"></div>'; |
| | | 2428 | | } |
| | | 2429 | | |
| | | 2430 | | function mountPriceHistoryGrid() { |
| | | 2431 | | const el = document.getElementById('priceHistoryGrid'); |
| | | 2432 | | if (_priceHistoryGridApi) { try { _priceHistoryGridApi.destroy(); } catch (_) { } _priceHistoryGridApi = null; } |
| | | 2433 | | if (!el || !window.BsxGrid) return; |
| | | 2434 | | const set = { filter: window.BsxGrid.SetFilter, floatingFilterComponent: window.BsxGrid.SetFloatingFilter }; |
| | | 2435 | | _priceHistoryGridApi = window.BsxGrid.create(el, { |
| | | 2436 | | key: 'priceHistory', |
| | | 2437 | | rowData: _priceHistoryRows, |
| | | 2438 | | getRowId: p => String(p.data.id), |
| | | 2439 | | columnDefs: [ |
| | | 2440 | | { field: 'bitrixId', headerName: 'Bitrix ID', width: 110, valueFormatter: p => p.value || 'Не указан' }, |
| | | 2441 | | { field: 'article', headerName: 'Артикул', width: 140, valueFormatter: p => p.value || 'Не указан' }, |
| | | 2442 | | { field: 'priceValue', headerName: 'Цена', width: 110, type: 'numericColumn', filter: 'agNumberColumnFil |
| | | 2443 | | valueGetter: p => p.data?.priceValue ? Math.round(parseFloat(p.data.priceValue)) : 0, valueFormatter: |
| | | 2444 | | { field: 'valueSource', headerName: 'Источник', flex: 1, minWidth: 150, valueFormatter: p => p.value || |
| | | 2445 | | { field: 'author', headerName: 'Автор', width: 150, valueFormatter: p => p.value || 'Не указан', ...set |
| | | 2446 | | { field: 'createDateTime', headerName: 'Дата', width: 160, sort: 'desc', filter: false, |
| | | 2447 | | valueFormatter: p => p.value ? new Date(p.value).toLocaleDateString() + ' ' + new Date(p.value).toLoca |
| | | 2448 | | { colId: 'actions', headerName: '', width: 90, sortable: false, filter: false, lockVisible: true, cellCl |
| | | 2449 | | cellRenderer: p => p.data ? `<div class="bsx-cell-actions-row"> |
| | | 2450 | | <a class="btn btn-xs btn-outline-primary" href="/PriceHistory/Details/${p.data.id}" target="_blank |
| | | 2451 | | <button type="button" class="btn btn-xs btn-outline-danger" title="Удалить запись" onclick="delete |
| | | 2452 | | ] |
| | | 2453 | | }); |
| | | 2454 | | } |
| | | 2455 | | |
| | | 2456 | | |
| | | 2457 | | function openImage(imageUrl) { |
| | | 2458 | | window.open(imageUrl, '_blank'); |
| | | 2459 | | } |
| | | 2460 | | |
| | | 2461 | | $(document).on("click", ".show-bitrix-data", function () { |
| | | 2462 | | let article = $(this).data("article"); |
| | | 2463 | | let article_with_key = $(this).data("article-with-key"); |
| | | 2464 | | let productId = $(this).data("productid"); |
| | | 2465 | | let supplier = $(this).data("supplier"); |
| | | 2466 | | let previewText = $(this).data("previewtext"); |
| | | 2467 | | let articleBlock = $(this).data("articleblock"); |
| | | 2468 | | |
| | | 2469 | | var priceInput = $('input[name="PriceValue"]'); |
| | | 2470 | | priceInput.val(''); |
| | | 2471 | | |
| | | 2472 | | let storesData = $(this).data("stores"); |
| | | 2473 | | let storeNames = []; |
| | | 2474 | | if (storesData) { |
| | | 2475 | | // data-stores приходит строкой: "Краснодар,СПБ_Гатчина,Ростов-на-Дону" |
| | | 2476 | | storeNames = storesData.split(",").map(s => s.trim()); |
| | | 2477 | | } |
| | | 2478 | | |
| | | 2479 | | let headerHtml = ` |
| | | 2480 | | <div class="d-flex flex-wrap gap-4 mb-2"> |
| | | 2481 | | <div>${articleBlock}</div> |
| | | 2482 | | <div>${productId}</div> |
| | | 2483 | | <div>${supplier}</div> |
| | | 2484 | | </div> |
| | | 2485 | | <div class="mb-2"> |
| | | 2486 | | ${previewText} |
| | | 2487 | | </div> |
| | | 2488 | | <hr> |
| | | 2489 | | `; |
| | | 2490 | | |
| | | 2491 | | $("#bitrixModalContent").html(` |
| | | 2492 | | <div class="d-flex justify-content-center align-items-center" style="height:200px;"> |
| | | 2493 | | <div class="spinner-border text-primary" role="status"> |
| | | 2494 | | <span class="visually-hidden">Загрузка...</span> |
| | | 2495 | | </div> |
| | | 2496 | | </div> |
| | | 2497 | | <div class="text-center mt-3"> |
| | | 2498 | | <a href="#" class="btn-load-tradesoft" |
| | | 2499 | | data-article="${article}" |
| | | 2500 | | data-supplier="${supplier}" |
| | | 2501 | | style="display:none;"> |
| | | 2502 | | Загрузить доп. данные |
| | | 2503 | | </a> |
| | | 2504 | | </div> |
| | | 2505 | | `); |
| | | 2506 | | |
| | | 2507 | | $("#priceHistorySection").show(); |
| | | 2508 | | $("#historySpinner").show(); |
| | | 2509 | | $("#priceHistoryTableContainer").empty(); |
| | | 2510 | | |
| | | 2511 | | $("#bitrixModal").modal("show"); |
| | | 2512 | | |
| | | 2513 | | loadPriceHistory(productId); |
| | | 2514 | | |
| | | 2515 | | $.when( |
| | | 2516 | | fetchBitrixData(article), |
| | | 2517 | | fetchOzonData(normalizeOfferId(article_with_key, article), storeNames) |
| | | 2518 | | ).done(async function (bitrixResponse, ozonResponse) { |
| | | 2519 | | const unwrapWhenResult = (result) => { |
| | | 2520 | | // jQuery $.when with jqXHR passes [data, textStatus, jqXHR] |
| | | 2521 | | if (Array.isArray(result) && result.length === 3 && typeof result[1] === 'string') { |
| | | 2522 | | return result[0]; |
| | | 2523 | | } |
| | | 2524 | | // Native Promise or already-unwrapped value |
| | | 2525 | | return result; |
| | | 2526 | | }; |
| | | 2527 | | |
| | | 2528 | | let bitrixData = unwrapWhenResult(bitrixResponse); |
| | | 2529 | | let ozonData = unwrapWhenResult(ozonResponse); |
| | | 2530 | | if (!Array.isArray(bitrixData)) { |
| | | 2531 | | bitrixData = []; |
| | | 2532 | | } |
| | | 2533 | | if (!Array.isArray(ozonData)) { |
| | | 2534 | | ozonData = []; |
| | | 2535 | | } |
| | | 2536 | | |
| | | 2537 | | const linkedStoreNames = getBitrixStoreNamesForOzonFetch(storeNames); |
| | | 2538 | | if (ozonData && Array.isArray(ozonData)) { |
| | | 2539 | | await Promise.all(ozonData |
| | | 2540 | | .filter(cd => cd?.Client) |
| | | 2541 | | .map(cd => ensureWarehouseMappingsForClient(cd.Client, linkedStoreNames))); |
| | | 2542 | | ozonData = await fetchOzonData(normalizeOfferId(article_with_key, article), linkedStoreNames); |
| | | 2543 | | if (!Array.isArray(ozonData)) { |
| | | 2544 | | ozonData = []; |
| | | 2545 | | } |
| | | 2546 | | enrichOzonDataWithWarehouseConnections(ozonData); |
| | | 2547 | | } |
| | | 2548 | | |
| | | 2549 | | let bitrixHtml = renderBitrixTable(bitrixData); |
| | | 2550 | | let ozonHtml = renderOzonTable(ozonData); |
| | | 2551 | | |
| | | 2552 | | $("#bitrixModalContent").html(` |
| | | 2553 | | ${headerHtml} |
| | | 2554 | | <div class="tradesoft-section"> |
| | | 2555 | | <div class="text-center mb-3"> |
| | | 2556 | | <a href="#" class="btn-load-tradesoft" |
| | | 2557 | | data-article="${article}" |
| | | 2558 | | data-supplier="${supplier}" |
| | | 2559 | | style="display:none;"> |
| | | 2560 | | Загрузить доп. данные |
| | | 2561 | | </a> |
| | | 2562 | | </div> |
| | | 2563 | | <div id="tradesoft-content"></div> |
| | | 2564 | | </div> |
| | | 2565 | | <hr> |
| | | 2566 | | `); |
| | | 2567 | | |
| | | 2568 | | $("#ozonTableContainer").html(` |
| | | 2569 | | <div class="d-flex justify-content-center align-items-center" style="height: |
| | | 2570 | | <div class="spinner-border text-primary" role="status"> |
| | | 2571 | | <span class="visually-hidden">Загрузка Ozon...</span> |
| | | 2572 | | </div> |
| | | 2573 | | </div> |
| | | 2574 | | `); |
| | | 2575 | | |
| | | 2576 | | $("#bitrixDataSection").show(); |
| | | 2577 | | $("#bitrixTableContainer").html(bitrixHtml || "<p class='text-muted'>Нет данных</p>"); |
| | | 2578 | | mountBitrixDataGrid(); |
| | | 2579 | | var priceCell = $('[data-price-name="Учетная цена"]'); |
| | | 2580 | | if (priceCell.length > 0) { |
| | | 2581 | | // Берем значение из атрибута data-price-value или текста ячейки |
| | | 2582 | | var priceValue = priceCell.attr('data-price-value') || priceCell.text(); |
| | | 2583 | | |
| | | 2584 | | // Заполняем только если поле пустое |
| | | 2585 | | var priceInput = $('input[name="PriceValue"]'); |
| | | 2586 | | if (!priceInput.val().trim()) { |
| | | 2587 | | priceInput.val(priceValue); |
| | | 2588 | | } |
| | | 2589 | | } |
| | | 2590 | | |
| | | 2591 | | $("#ozonDataSection").show(); |
| | | 2592 | | $("#ozonTableContainer").html(ozonHtml || "<p class='text-muted'>Нет данных Ozon</p>"); |
| | | 2593 | | mountOzonDataGrid(); |
| | | 2594 | | const inputVal = parseFloat($('input[name="PriceValue"]').val()) || 0; |
| | | 2595 | | recalcRanges(inputVal); |
| | | 2596 | | |
| | | 2597 | | // Показываем форму |
| | | 2598 | | $("#priceHistoryFormSection").show(); |
| | | 2599 | | |
| | | 2600 | | // Обновляем поля формы |
| | | 2601 | | updatePriceHistoryForm(article, productId); |
| | | 2602 | | clearValidationErrors(); |
| | | 2603 | | |
| | | 2604 | | $(".btn-load-tradesoft").fadeIn(); |
| | | 2605 | | |
| | | 2606 | | }).fail(function () { |
| | | 2607 | | console.error("Ошибка при получении данных Bitrix или Ozon"); |
| | | 2608 | | $("#bitrixModalContent").html(` |
| | | 2609 | | ${headerHtml} |
| | | 2610 | | <div class="tradesoft-section"> |
| | | 2611 | | <div class="text-center mb-3"> |
| | | 2612 | | <a href="#" class="btn-load-tradesoft" |
| | | 2613 | | data-article="${article}" |
| | | 2614 | | data-supplier="${supplier}" |
| | | 2615 | | style="display:none;"> |
| | | 2616 | | Загрузить доп. данные |
| | | 2617 | | </a> |
| | | 2618 | | </div> |
| | | 2619 | | <div id="tradesoft-content"></div> |
| | | 2620 | | </div> |
| | | 2621 | | <hr> |
| | | 2622 | | <strong class='text-danger'>Не удалось загрузить данные Bitrix/Ozon</strong> |
| | | 2623 | | `); |
| | | 2624 | | |
| | | 2625 | | // Все равно показываем форму и историю |
| | | 2626 | | $("#priceHistoryFormSection").show(); |
| | | 2627 | | $("#bitrixDataSection").show(); |
| | | 2628 | | $("#bitrixTableContainer").html("<p class='text-danger'>Не удалось загрузить данные Bitrix</p>"); |
| | | 2629 | | $("#ozonDataSection").show(); |
| | | 2630 | | $("#ozonTableContainer").html("<p class='text-danger'>Не удалось загрузить данные Ozon</p>"); |
| | | 2631 | | updatePriceHistoryForm(article, productId); |
| | | 2632 | | clearValidationErrors(); |
| | | 2633 | | |
| | | 2634 | | $(".btn-load-tradesoft").fadeIn(); |
| | | 2635 | | }); |
| | | 2636 | | }); |
| | | 2637 | | |
| | | 2638 | | |
| | | 2639 | | $(document).on("click", ".btn-load-tradesoft", function () { |
| | | 2640 | | let article = $(this).data("article"); |
| | | 2641 | | let supplier = $(this).data("supplier"); |
| | | 2642 | | let button = $(this); |
| | | 2643 | | |
| | | 2644 | | button.hide(); |
| | | 2645 | | $("#tradesoft-content").html(` |
| | | 2646 | | <div class="d-flex justify-content-center align-items-center" style="height:100px;"> |
| | | 2647 | | <div class="spinner-border text-primary" role="status"> |
| | | 2648 | | <span class="visually-hidden">Загрузка Tradesoft...</span> |
| | | 2649 | | </div> |
| | | 2650 | | </div> |
| | | 2651 | | `); |
| | | 2652 | | |
| | | 2653 | | fetchOrderDetailedInformationForBitrix(article, supplier) |
| | | 2654 | | .done(function (tradesoftData) { |
| | | 2655 | | let tradesoftHtml = renderOrderDetailedInformationForBitrix(tradesoftData, []); |
| | | 2656 | | $("#tradesoft-content").html(tradesoftHtml); |
| | | 2657 | | |
| | | 2658 | | $(".table-content").html(` |
| | | 2659 | | <div class="d-flex justify-content-center align-items-center" style="height:150px;"> |
| | | 2660 | | <div class="spinner-border text-primary" role="status"> |
| | | 2661 | | <span class="visually-hidden">Загрузка цен...</span> |
| | | 2662 | | </div> |
| | | 2663 | | </div> |
| | | 2664 | | `); |
| | | 2665 | | |
| | | 2666 | | |
| | | 2667 | | GetCombinedDataForBitrix(article, supplier) |
| | | 2668 | | .done(function (pricesTradesoft) { |
| | | 2669 | | let orderId = tradesoftData.orderId || 1; |
| | | 2670 | | const colorMap = { |
| | | 2671 | | 'https://www.maxi.parts/': 'text-success', |
| | | 2672 | | 'https://www.abcp.ru/': 'text-primary', |
| | | 2673 | | 'https://www.avdmotors.ru/': 'text-info' |
| | | 2674 | | }; |
| | | 2675 | | |
| | | 2676 | | $(".table-content").html( |
| | | 2677 | | generateStockTradeTable(orderId, pricesTradesoft, colorMap) |
| | | 2678 | | ); |
| | | 2679 | | }) |
| | | 2680 | | .fail(function (error) { |
| | | 2681 | | $(".table-content").html(` |
| | | 2682 | | <div class="alert alert-danger"> |
| | | 2683 | | Не удалось загрузить цены: ${error.statusText || 'Ошибка сервера'} |
| | | 2684 | | </div> |
| | | 2685 | | `); |
| | | 2686 | | }); |
| | | 2687 | | |
| | | 2688 | | }) |
| | | 2689 | | .fail(function (error) { |
| | | 2690 | | console.error("Ошибка при получении основных данных Tradesoft:", error); |
| | | 2691 | | $("#tradesoft-content").html(` |
| | | 2692 | | <div class="alert alert-danger"> |
| | | 2693 | | Не удалось загрузить данные: ${error.statusText || 'Ошибка сервера'} |
| | | 2694 | | </div> |
| | | 2695 | | `); |
| | | 2696 | | }); |
| | | 2697 | | }); |
| | | 2698 | | |
| | | 2699 | | function checkWarehouseConnection(ozonWarehouseName, bitrixData, clientName) { |
| | | 2700 | | |
| | | 2701 | | if (!window.warehouseMappings || !Array.isArray(window.warehouseMappings)) { |
| | | 2702 | | return false; |
| | | 2703 | | } |
| | | 2704 | | console.log(ozonWarehouseName, bitrixData, clientName); |
| | | 2705 | | |
| | | 2706 | | const mapping = window.warehouseMappings.find(mapping => |
| | | 2707 | | mapping.ozonWarehouseName === ozonWarehouseName && |
| | | 2708 | | mapping.ozonClientName === clientName |
| | | 2709 | | ); |
| | | 2710 | | |
| | | 2711 | | if (!mapping) { |
| | | 2712 | | return false; |
| | | 2713 | | } |
| | | 2714 | | return true; |
| | | 2715 | | } |
| | | 2716 | | |
| | | 2717 | | function updatePriceDisplay(orderId, indexRow) { |
| | | 2718 | | const $priceLink = $(`.price-link[data-order-id="${orderId}"][data-index-row="${indexRow}"]`); |
| | | 2719 | | const $priceBlock = $(`.cost-price-row[data-order-id="${orderId}"][data-index-row="${indexRow}"]`); |
| | | 2720 | | const $weightInput = $(`#ProductWeight_${orderId}`); |
| | | 2721 | | const $supplierSelect = $(`#SupplierId_${orderId}`); |
| | | 2722 | | |
| | | 2723 | | const weight = parseFloat(($weightInput.val() || '0').replace(',', '.')) || 0; |
| | | 2724 | | const priceSite = $priceLink.data('site'); |
| | | 2725 | | |
| | | 2726 | | // Вывод в консоль исходных данных |
| | | 2727 | | console.log('====== РАСЧЕТ СТОИМОСТИ ======'); |
| | | 2728 | | console.log(`Заказ: ${orderId}, Строка: ${indexRow}`); |
| | | 2729 | | console.log(`Сайт: ${priceSite}`); |
| | | 2730 | | console.log(`Вес товара: ${weight} кг`); |
| | | 2731 | | |
| | | 2732 | | // Получаем данные из модели (предполагается, что suppliers доступен глобально) |
| | | 2733 | | const supplierFromModel = window.suppliersData.find(s => s.site === priceSite); |
| | | 2734 | | console.log(supplierFromModel); |
| | | 2735 | | let weightFactor = 0; |
| | | 2736 | | let costFactor = 1; |
| | | 2737 | | let weightCurrency = 'RUB'; |
| | | 2738 | | |
| | | 2739 | | if (supplierFromModel) { |
| | | 2740 | | weightFactor = parseFloat((supplierFromModel.weightFactor || '0').toString().replace(',', '.')) || 0; |
| | | 2741 | | costFactor = parseFloat((supplierFromModel.costFactor || '1').toString().replace(',', '.')) || 1; |
| | | 2742 | | weightCurrency = supplierFromModel.weightFactorCurrencyDisplayName || 'RUB'; |
| | | 2743 | | console.log('Данные поставщика из модели:'); |
| | | 2744 | | console.log(`• weightFactor: ${weightFactor}`); |
| | | 2745 | | console.log(`• costFactor: ${costFactor}`); |
| | | 2746 | | console.log(`• weightCurrency: ${weightCurrency}`); |
| | | 2747 | | } else { |
| | | 2748 | | const supplierOptionBySite = $supplierSelect.find(`option[data-site="${priceSite}"]`); |
| | | 2749 | | weightFactor = parseFloat((supplierOptionBySite.data('weight-factor') || '0').toString().replace(',', '.')) |
| | | 2750 | | costFactor = parseFloat((supplierOptionBySite.data('cost-factor') || '1').toString().replace(',', '.')) || 1 |
| | | 2751 | | weightCurrency = supplierOptionBySite.data('weight-currency-code') || 'RUB'; |
| | | 2752 | | console.log('Данные поставщика из выпадающего списка:'); |
| | | 2753 | | console.log(`• weightFactor: ${weightFactor}`); |
| | | 2754 | | console.log(`• costFactor: ${costFactor}`); |
| | | 2755 | | console.log(`• weightCurrency: ${weightCurrency}`); |
| | | 2756 | | } |
| | | 2757 | | |
| | | 2758 | | const rawPrice = parseFloat(($priceLink.data('price') || '0').toString().replace(',', '.')) || 0; |
| | | 2759 | | const priceCurrency = $priceLink.data('currency-code') || 'RUB'; |
| | | 2760 | | |
| | | 2761 | | console.log('--- Данные о цене ---'); |
| | | 2762 | | console.log(`• Базовая цена: ${rawPrice} ${priceCurrency}`); |
| | | 2763 | | |
| | | 2764 | | const ratePrice = rates[priceCurrency] || 1; |
| | | 2765 | | const rateWeight = rates[weightCurrency] || 1; |
| | | 2766 | | |
| | | 2767 | | console.log('--- Курсы валют ---'); |
| | | 2768 | | console.log(`• Курс ${priceCurrency}/RUB: ${ratePrice}`); |
| | | 2769 | | console.log(`• Курс ${weightCurrency}/RUB: ${rateWeight}`); |
| | | 2770 | | |
| | | 2771 | | // Промежуточные расчеты (если с бэкенда передан data-base-cost-rub — закупка по «входу», не по цене в ссылке) |
| | | 2772 | | const pricePerKgRUB = weightFactor * rateWeight; |
| | | 2773 | | const totalWeightCost = weight * pricePerKgRUB; |
| | | 2774 | | |
| | | 2775 | | const baseCostAttr = $priceBlock.attr('data-base-cost-rub'); |
| | | 2776 | | let baseCostRub = NaN; |
| | | 2777 | | if (baseCostAttr !== undefined && baseCostAttr !== null && String(baseCostAttr).trim() !== '') { |
| | | 2778 | | baseCostRub = parseFloat(String(baseCostAttr).replace(/\s/g, '').replace(',', '.')); |
| | | 2779 | | } |
| | | 2780 | | |
| | | 2781 | | let purchasePriceRUB; |
| | | 2782 | | if (!isNaN(baseCostRub) && baseCostRub >= 0) { |
| | | 2783 | | purchasePriceRUB = baseCostRub * costFactor; |
| | | 2784 | | } else { |
| | | 2785 | | purchasePriceRUB = rawPrice * costFactor * ratePrice; |
| | | 2786 | | } |
| | | 2787 | | |
| | | 2788 | | const totalRUB = Math.round(totalWeightCost + purchasePriceRUB); |
| | | 2789 | | |
| | | 2790 | | console.log('--- Промежуточные расчеты ---'); |
| | | 2791 | | console.log(`• Стоимость кг (weightFactor): ${weightFactor} × ${rateWeight} = ${pricePerKgRUB} RUB/кг`); |
| | | 2792 | | console.log(`• Стоимость веса: ${weight} кг × ${pricePerKgRUB} RUB/кг = ${totalWeightCost} RUB`); |
| | | 2793 | | if (!isNaN(baseCostRub) && baseCostRub >= 0) { |
| | | 2794 | | console.log(`• Цена закупки (вход API): ${baseCostRub} × ${costFactor} = ${purchasePriceRUB} RUB`); |
| | | 2795 | | } else { |
| | | 2796 | | console.log(`• Цена закупки: ${rawPrice} × ${costFactor} × ${ratePrice} = ${purchasePriceRUB} RUB`); |
| | | 2797 | | } |
| | | 2798 | | console.log(`• Итоговая стоимость: ${totalWeightCost} + ${purchasePriceRUB} = ${totalRUB} RUB`); |
| | | 2799 | | |
| | | 2800 | | const formattedTotalRUB = totalRUB.toLocaleString('ru-RU'); |
| | | 2801 | | const displayText = `${formattedTotalRUB} ₽`; |
| | | 2802 | | |
| | | 2803 | | console.log('--- Результат ---'); |
| | | 2804 | | console.log(`• Отображаемая цена: ${displayText}`); |
| | | 2805 | | console.log('=============================='); |
| | | 2806 | | |
| | | 2807 | | $priceBlock.text(displayText); |
| | | 2808 | | $priceBlock.attr('data-value', totalRUB); |
| | | 2809 | | |
| | | 2810 | | if (window.tradesofDataCache && window.tradesofDataCache[orderId] && window.tradesofDataCache[orderId][indexRow] |
| | | 2811 | | window.tradesofDataCache[orderId][indexRow].costPrice = totalRUB; |
| | | 2812 | | window.tradesofDataCache[orderId][indexRow].costPriceFormatted = displayText; |
| | | 2813 | | } |
| | | 2814 | | } |
| | | 2815 | | |
| | | 2816 | | function updateAllPricesForOrder(orderId) { |
| | | 2817 | | $(`.price-link[data-order-id="${orderId}"]`).each(function () { |
| | | 2818 | | const indexRow = $(this).data('index-row'); |
| | | 2819 | | updatePriceDisplay(orderId, indexRow); |
| | | 2820 | | }); |
| | | 2821 | | } |
| | | 2822 | | |
| | | 2823 | | $(document).on('input', '.product-weight-field', function () { |
| | | 2824 | | const orderId = $(this).attr('id').split('_')[1]; |
| | | 2825 | | updateAllPricesForOrder(orderId); |
| | | 2826 | | sortAndRerenderTable(orderId, 'costPrice', 'number', 'asc'); |
| | | 2827 | | }); |
| | | 2828 | | |
| | | 2829 | | function fetchOrderDetailedInformationForBitrix(article, supplier) { |
| | | 2830 | | return $.ajax({ |
| | 0 | 2831 | | url: '@Url.Action("OrderDetailedInformationForBitrix", "Orders")', |
| | | 2832 | | type: "GET", |
| | | 2833 | | data: { |
| | | 2834 | | article: article, |
| | | 2835 | | producer: supplier |
| | | 2836 | | } |
| | | 2837 | | }).catch(error => { |
| | | 2838 | | if (error.status === 500) { |
| | | 2839 | | console.error('Ошибка 500 при получении данных от Tradesoft:', error); |
| | | 2840 | | } |
| | | 2841 | | return null; |
| | | 2842 | | }); |
| | | 2843 | | } |
| | | 2844 | | |
| | | 2845 | | // Функция для получения описания color_index |
| | | 2846 | | function getColorIndexDescription(colorIndex) { |
| | | 2847 | | const descriptions = { |
| | | 2848 | | "WITHOUT_INDEX": "Нет индекса", |
| | | 2849 | | "GREEN": "Выгодный", |
| | | 2850 | | "YELLOW": "Умеренный", |
| | | 2851 | | "RED": "Невыгодный" |
| | | 2852 | | }; |
| | | 2853 | | return descriptions[colorIndex] || "Неизвестный индекс"; |
| | | 2854 | | } |
| | | 2855 | | |
| | | 2856 | | // Функция для получения цвета для подсветки |
| | | 2857 | | function getColorIndexColor(colorIndex) { |
| | | 2858 | | const colors = { |
| | | 2859 | | "WITHOUT_INDEX": "gray", |
| | | 2860 | | "GREEN": "green", |
| | | 2861 | | "YELLOW": "orange", |
| | | 2862 | | "RED": "red" |
| | | 2863 | | }; |
| | | 2864 | | return colors[colorIndex] || "gray"; |
| | | 2865 | | } |
| | | 2866 | | |
| | | 2867 | | // Функция для получения AntiForgeryToken |
| | | 2868 | | function getAntiXsrfToken() { |
| | | 2869 | | return document.querySelector('input[name="__RequestVerificationToken"]').value; |
| | | 2870 | | } |
| | | 2871 | | |
| | | 2872 | | // Функция для обновления полей формы при открытии модального окна |
| | | 2873 | | function updatePriceHistoryForm(article, productId) { |
| | | 2874 | | $('#modalArticle').val(article || ''); |
| | | 2875 | | $('#modalBitrixId').val(productId || ''); |
| | | 2876 | | |
| | | 2877 | | // Устанавливаем текущую дату и время |
| | | 2878 | | const now = new Date().toISOString().slice(0, 16); |
| | | 2879 | | $('#modalCreateDateTime').val(now); |
| | | 2880 | | } |
| | | 2881 | | |
| | | 2882 | | // Очистка ошибок валидации |
| | | 2883 | | function clearValidationErrors() { |
| | | 2884 | | $('.text-danger small').text(''); |
| | | 2885 | | $('.form-control').removeClass('is-invalid'); |
| | | 2886 | | } |
| | | 2887 | | |
| | | 2888 | | // Показать ошибки валидации |
| | | 2889 | | function showValidationErrors(errors) { |
| | | 2890 | | clearValidationErrors(); |
| | | 2891 | | |
| | | 2892 | | for (const field in errors) { |
| | | 2893 | | if (errors.hasOwnProperty(field)) { |
| | | 2894 | | const errorElement = $(`#${field.toLowerCase()}Error`); |
| | | 2895 | | const inputElement = $(`[name="${field}"]`); |
| | | 2896 | | |
| | | 2897 | | if (errorElement.length && inputElement.length) { |
| | | 2898 | | errorElement.text(errors[field].join(', ')); |
| | | 2899 | | inputElement.addClass('is-invalid'); |
| | | 2900 | | } |
| | | 2901 | | } |
| | | 2902 | | } |
| | | 2903 | | } |
| | | 2904 | | |
| | | 2905 | | $(document).on('submit', '#priceHistoryForm', function (e) { |
| | | 2906 | | e.preventDefault(); |
| | | 2907 | | |
| | | 2908 | | const form = $(this); |
| | | 2909 | | // Исправьте URL - добавьте слеш в начале и / перед Area |
| | | 2910 | | const url = '/Studio2/PriceHistory/Create'; |
| | | 2911 | | const formData = form.serialize(); |
| | | 2912 | | |
| | | 2913 | | // Получаем productId из формы |
| | | 2914 | | const productId = $('#modalBitrixId').val(); // Или другого поля, где хранится productId |
| | | 2915 | | console.log('Product ID:', productId); |
| | | 2916 | | console.log('Отправка данных:', formData); |
| | | 2917 | | console.log('URL:', url); |
| | | 2918 | | |
| | | 2919 | | // Показываем индикатор загрузки |
| | | 2920 | | const submitBtn = form.find('button[type="submit"]'); |
| | | 2921 | | const originalText = submitBtn.html(); |
| | | 2922 | | submitBtn.html(` |
| | | 2923 | | <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> |
| | | 2924 | | Добавление... |
| | | 2925 | | `).prop('disabled', true); |
| | | 2926 | | |
| | | 2927 | | $.ajax({ |
| | | 2928 | | url: url, |
| | | 2929 | | type: 'POST', |
| | | 2930 | | data: formData, |
| | | 2931 | | headers: { |
| | | 2932 | | 'RequestVerificationToken': getAntiXsrfToken(), |
| | | 2933 | | 'X-Requested-With': 'XMLHttpRequest' |
| | | 2934 | | }, |
| | | 2935 | | success: function (result) { |
| | | 2936 | | console.log('Успешный ответ:', result); |
| | | 2937 | | if (result.success) { |
| | | 2938 | | showNotification('Запись успешно создана!', 'success'); |
| | | 2939 | | const now = new Date().toISOString().slice(0, 16); |
| | | 2940 | | $('#modalCreateDateTime').val(now); |
| | | 2941 | | clearValidationErrors(); |
| | | 2942 | | |
| | | 2943 | | // Вызываем обновление истории цен после успешного добавления |
| | | 2944 | | if (productId) { |
| | | 2945 | | loadPriceHistory(productId); |
| | | 2946 | | } else { |
| | | 2947 | | console.warn('Product ID не найден для обновления истории'); |
| | | 2948 | | } |
| | | 2949 | | } |
| | | 2950 | | }, |
| | | 2951 | | error: function (xhr, status, error) { |
| | | 2952 | | console.log('Ошибка AJAX:', status, error, xhr.responseText); |
| | | 2953 | | |
| | | 2954 | | try { |
| | | 2955 | | const errorData = JSON.parse(xhr.responseText); |
| | | 2956 | | if (errorData.errors) { |
| | | 2957 | | showValidationErrors(errorData.errors); |
| | | 2958 | | showNotification('Пожалуйста, исправьте ошибки в форме', 'error'); |
| | | 2959 | | } else { |
| | | 2960 | | showNotification(errorData.error || 'Ошибка при создании записи', 'error'); |
| | | 2961 | | } |
| | | 2962 | | } catch (e) { |
| | | 2963 | | showNotification('Ошибка сервера: ' + xhr.status, 'error'); |
| | | 2964 | | } |
| | | 2965 | | }, |
| | | 2966 | | complete: function () { |
| | | 2967 | | submitBtn.html(originalText).prop('disabled', false); |
| | | 2968 | | } |
| | | 2969 | | }); |
| | | 2970 | | }); |
| | | 2971 | | |
| | | 2972 | | // Функция для показа уведомлений поверх всех окон |
| | | 2973 | | function showNotification(message, type = 'info') { |
| | | 2974 | | // Создаем контейнер для уведомлений если его нет |
| | | 2975 | | let notificationContainer = document.getElementById('notification-container'); |
| | | 2976 | | if (!notificationContainer) { |
| | | 2977 | | notificationContainer = document.createElement('div'); |
| | | 2978 | | notificationContainer.id = 'notification-container'; |
| | | 2979 | | notificationContainer.style.cssText = 'position: fixed; top: 20px; right: 20px; z-index: 99999; display: fle |
| | | 2980 | | document.body.appendChild(notificationContainer); |
| | | 2981 | | } |
| | | 2982 | | |
| | | 2983 | | // Создаем уведомление |
| | | 2984 | | const toast = document.createElement('div'); |
| | | 2985 | | toast.className = `alert alert-${type} alert-dismissible fade show`; |
| | | 2986 | | toast.style.cssText = 'min-width: 300px; max-width: 400px; box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);'; |
| | | 2987 | | toast.innerHTML = ` |
| | | 2988 | | ${message} |
| | | 2989 | | <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> |
| | | 2990 | | `; |
| | | 2991 | | |
| | | 2992 | | // Добавляем уведомление в контейнер |
| | | 2993 | | notificationContainer.appendChild(toast); |
| | | 2994 | | |
| | | 2995 | | // Инициализируем Bootstrap alert |
| | | 2996 | | const bsAlert = new bootstrap.Alert(toast); |
| | | 2997 | | |
| | | 2998 | | // Автоматическое скрытие через 5 секунд |
| | | 2999 | | setTimeout(() => { |
| | | 3000 | | if (toast.parentNode) { |
| | | 3001 | | bsAlert.close(); |
| | | 3002 | | } |
| | | 3003 | | }, 5000); |
| | | 3004 | | |
| | | 3005 | | // Удаляем элемент из DOM после скрытия |
| | | 3006 | | toast.addEventListener('closed.bs.alert', function () { |
| | | 3007 | | if (toast.parentNode) { |
| | | 3008 | | toast.parentNode.removeChild(toast); |
| | | 3009 | | } |
| | | 3010 | | }); |
| | | 3011 | | } |
| | | 3012 | | |
| | | 3013 | | // Инициализация при загрузке страницы |
| | | 3014 | | $(document).ready(function () { |
| | | 3015 | | // Устанавливаем текущую дату при загрузке |
| | | 3016 | | const now = new Date().toISOString().slice(0, 16); |
| | | 3017 | | $('#modalCreateDateTime').val(now); |
| | | 3018 | | }); |
| | | 3019 | | |
| | | 3020 | | |
| | | 3021 | | document.addEventListener("click", function (e) { |
| | | 3022 | | const button = e.target.closest(".stok-order"); |
| | | 3023 | | if (!button) return; |
| | | 3024 | | |
| | | 3025 | | const orderId = button.dataset.orderId; |
| | | 3026 | | const indexRow = button.dataset.indexRow; |
| | | 3027 | | const supplierUrl = button.dataset.supplierUrl || ""; |
| | | 3028 | | |
| | | 3029 | | const priceDiv = document.querySelector( |
| | | 3030 | | `.cost-price-row[data-order-id="${orderId}"][data-index-row="${indexRow}"]` |
| | | 3031 | | ); |
| | | 3032 | | |
| | | 3033 | | let costPrice = ""; |
| | | 3034 | | if (priceDiv) { |
| | | 3035 | | costPrice = priceDiv.innerText.trim(); |
| | | 3036 | | costPrice = costPrice.replace(/[^\d.,-]/g, "").replace(",", "."); |
| | | 3037 | | } |
| | | 3038 | | |
| | | 3039 | | const modal = button.closest(".modal"); |
| | | 3040 | | const priceInput = modal?.querySelector('input[name="PriceValue"]'); |
| | | 3041 | | const sourceInput = modal?.querySelector('input[name="ValueSource"]'); |
| | | 3042 | | |
| | | 3043 | | if (priceInput) { |
| | | 3044 | | priceInput.value = costPrice; |
| | | 3045 | | const inputVal = parseFloat(costPrice) || 0; |
| | | 3046 | | recalcRanges(inputVal); |
| | | 3047 | | } |
| | | 3048 | | |
| | | 3049 | | if (sourceInput) { |
| | | 3050 | | // ищем option в datalist с совпадающим url |
| | | 3051 | | const option = document.querySelector( |
| | | 3052 | | `#suppliersList option[data-url="${supplierUrl}"]` |
| | | 3053 | | ); |
| | | 3054 | | |
| | | 3055 | | if (option) { |
| | | 3056 | | sourceInput.value = option.value; // ставим название из списка |
| | | 3057 | | } else { |
| | | 3058 | | sourceInput.value = supplierUrl; // если не нашли, вставляем url как есть |
| | | 3059 | | } |
| | | 3060 | | } |
| | | 3061 | | }); |
| | | 3062 | | |
| | | 3063 | | |
| | | 3064 | | // Функция загрузки истории цен |
| | | 3065 | | function loadPriceHistory(bitrixId) { |
| | | 3066 | | if (!bitrixId) { |
| | | 3067 | | $("#historySpinner").hide(); |
| | | 3068 | | $("#priceHistoryTableContainer").html(` |
| | | 3069 | | <div class="alert alert-warning"> |
| | | 3070 | | Bitrix ID не указан для загрузки истории |
| | | 3071 | | </div> |
| | | 3072 | | `); |
| | | 3073 | | return; |
| | | 3074 | | } |
| | | 3075 | | |
| | | 3076 | | $.ajax({ |
| | | 3077 | | url: `/Studio2/PriceHistory/SearchByBitrixId?bitrixId=${bitrixId}`, |
| | | 3078 | | type: 'GET', |
| | | 3079 | | beforeSend: function () { |
| | | 3080 | | $("#historySpinner").show(); |
| | | 3081 | | $("#priceHistoryTableContainer").empty(); |
| | | 3082 | | }, |
| | | 3083 | | success: function (data) { |
| | | 3084 | | $("#historySpinner").hide(); |
| | | 3085 | | const historyHtml = renderPriceHistoryTable(data); |
| | | 3086 | | $("#priceHistoryTableContainer").html(historyHtml); |
| | | 3087 | | mountPriceHistoryGrid(); |
| | | 3088 | | if (data && data.length > 0 && data[0].priceValue !== null) { |
| | | 3089 | | const priceValue = parseFloat(data[0].priceValue); |
| | | 3090 | | $('input[name="PriceValue"]').val(priceValue.toFixed(2)); |
| | | 3091 | | } |
| | | 3092 | | }, |
| | | 3093 | | error: function (xhr, status, error) { |
| | | 3094 | | $("#historySpinner").hide(); |
| | | 3095 | | console.error("Ошибка при загрузке истории цен:", error); |
| | | 3096 | | $("#priceHistoryTableContainer").html(` |
| | | 3097 | | <div class="alert alert-danger"> |
| | | 3098 | | <i class="bi bi-exclamation-triangle"></i> |
| | | 3099 | | Ошибка при загрузке истории цен: ${xhr.status} ${xhr.statusText} |
| | | 3100 | | </div> |
| | | 3101 | | `); |
| | | 3102 | | } |
| | | 3103 | | }); |
| | | 3104 | | } |
| | | 3105 | | |
| | | 3106 | | function formatNumber(value) { |
| | | 3107 | | const number = parseFloat(value); |
| | | 3108 | | if (isNaN(number)) return value; |
| | | 3109 | | return Math.round(number).toLocaleString('ru-RU'); |
| | | 3110 | | } |
| | | 3111 | | |
| | | 3112 | | function buildCalculationPreview(value) { |
| | | 3113 | | const text = String(value ?? '').replace(/\s+/g, ' ').trim(); |
| | | 3114 | | if (!text) return ''; |
| | | 3115 | | return text.length > 110 ? `${text.slice(0, 110).trim()}...` : text; |
| | | 3116 | | } |
| | | 3117 | | |
| | | 3118 | | function parseNumeric(value) { |
| | | 3119 | | const number = parseFloat(String(value ?? '').replace(',', '.')); |
| | | 3120 | | return Number.isFinite(number) ? number : 0; |
| | | 3121 | | } |
| | | 3122 | | |
| | | 3123 | | function getRequestedUpdateTargetIndex(constantIndex, percent) { |
| | | 3124 | | const normalizedTargetIndex = Math.min(0.9, Math.max(0.8, constantIndex || 0.9)); |
| | | 3125 | | const safetyAdjustment = Math.max(percent || 0, 0) / 1000; |
| | | 3126 | | return Math.min(0.9, Math.max(0.8, normalizedTargetIndex - safetyAdjustment)); |
| | | 3127 | | } |
| | | 3128 | | |
| | | 3129 | | function buildUpdatePriceHint(link) { |
| | | 3130 | | const client = _ozonClients[link.getAttribute('data-client-id')]; |
| | | 3131 | | if (!client) return 'Нет данных для предварительного расчета'; |
| | | 3132 | | |
| | | 3133 | | const readPriceValue = (key) => parseNumeric(client.prices[key]); |
| | | 3134 | | const readCommissionValue = (key) => parseNumeric(client.commissions[key]); |
| | | 3135 | | |
| | | 3136 | | const sellerPrice = readPriceValue('marketing_seller_price') || readPriceValue('price'); |
| | | 3137 | | const customerPrice = readPriceValue('true_price') || readPriceValue('customer_price'); |
| | | 3138 | | const ozonIndex = readCommissionValue('ozon_index_value'); |
| | | 3139 | | const ozonMinPrice = readCommissionValue('ozon_min_price'); |
| | | 3140 | | const externalIndex = readCommissionValue('external_index_value'); |
| | | 3141 | | const externalMinPrice = readCommissionValue('external_min_price'); |
| | | 3142 | | const constantIndex = parseNumeric(link.getAttribute('data-constant-index')); |
| | | 3143 | | const percent = parseNumeric(link.getAttribute('data-percent')); |
| | | 3144 | | const targetIndex = getRequestedUpdateTargetIndex(constantIndex, percent); |
| | | 3145 | | |
| | | 3146 | | if (!sellerPrice || !customerPrice) { |
| | | 3147 | | return 'Недостаточно данных по цене для предварительного расчета'; |
| | | 3148 | | } |
| | | 3149 | | |
| | | 3150 | | const customerFactor = customerPrice / sellerPrice; |
| | | 3151 | | const candidates = []; |
| | | 3152 | | |
| | | 3153 | | if (ozonIndex > 0) { |
| | | 3154 | | const targetCustomerPrice = customerPrice * targetIndex / ozonIndex; |
| | | 3155 | | candidates.push({ |
| | | 3156 | | source: 'Ozon', |
| | | 3157 | | indexValue: ozonIndex, |
| | | 3158 | | sourcePrice: ozonMinPrice, |
| | | 3159 | | targetCustomerPrice, |
| | | 3160 | | targetSellerPrice: targetCustomerPrice / customerFactor |
| | | 3161 | | }); |
| | | 3162 | | } |
| | | 3163 | | |
| | | 3164 | | if (externalIndex > 0) { |
| | | 3165 | | const targetCustomerPrice = customerPrice * targetIndex / externalIndex; |
| | | 3166 | | candidates.push({ |
| | | 3167 | | source: 'Другие площадки', |
| | | 3168 | | indexValue: externalIndex, |
| | | 3169 | | sourcePrice: externalMinPrice, |
| | | 3170 | | targetCustomerPrice, |
| | | 3171 | | targetSellerPrice: targetCustomerPrice / customerFactor |
| | | 3172 | | }); |
| | | 3173 | | } |
| | | 3174 | | |
| | | 3175 | | if (candidates.length === 0) { |
| | | 3176 | | return `Текущая цена с картой: ${Math.round(customerPrice)}. Нет индексов для предварительного расчета.`; |
| | | 3177 | | } |
| | | 3178 | | |
| | | 3179 | | const bestCandidate = candidates.sort((left, right) => right.targetCustomerPrice - left.targetCustomerPrice)[0]; |
| | | 3180 | | |
| | | 3181 | | return ` |
| | | 3182 | | <div style="max-width: 360px; font-size: 12px; line-height: 1.35;"> |
| | | 3183 | | <div style="font-weight: 600; margin-bottom: 6px;">Предварительный пересчет цены</div> |
| | | 3184 | | <div><b>Текущая цена продавца:</b> ${Math.round(sellerPrice)}</div> |
| | | 3185 | | <div><b>Текущая цена с картой:</b> ${Math.round(customerPrice)}</div> |
| | | 3186 | | <div><b>Индекс Ozon:</b> ${ozonIndex ? ozonIndex.toFixed(2) : '-'}</div> |
| | | 3187 | | <div><b>Индекс внешнего рынка:</b> ${externalIndex ? externalIndex.toFixed(2) : '-'}</div> |
| | | 3188 | | <div style="margin-top: 6px;"><b>Целевой индекс:</b> clamp(${constantIndex.toFixed(2)}, 0.80, 0.90) - ${ |
| | | 3189 | | <div><b>Коэффициент цены с картой:</b> ${customerPrice.toFixed(2)} / ${sellerPrice.toFixed(2)} = ${custo |
| | | 3190 | | <div style="margin-top: 6px;"><b>Выбран рынок:</b> ${bestCandidate.source}</div> |
| | | 3191 | | <div><b>Ориентир:</b> ${Math.round(bestCandidate.sourcePrice)} | индекс ${bestCandidate.indexValue.toFix |
| | | 3192 | | <div><b>Реком. цена с картой:</b> ${Math.round(bestCandidate.targetCustomerPrice)}</div> |
| | | 3193 | | <div><b>Реком. цена продавца:</b> ${Math.round(bestCandidate.targetSellerPrice)}</div> |
| | | 3194 | | <div style="margin-top: 6px; color: #6c757d;">Берется наименее агрессивный кандидат, чтобы не уводить ин |
| | | 3195 | | </div> |
| | | 3196 | | `.trim(); |
| | | 3197 | | } |
| | | 3198 | | |
| | | 3199 | | function updateUpdatePriceHint(link) { |
| | | 3200 | | const hint = buildUpdatePriceHint(link); |
| | | 3201 | | link.setAttribute('data-bs-content', hint); |
| | | 3202 | | const constantIndexSpan = link.querySelector('.constant-index'); |
| | | 3203 | | const percentSpan = link.querySelector('.percent'); |
| | | 3204 | | if (constantIndexSpan) constantIndexSpan.setAttribute('title', 'Изменяет целевой индекс пересчета'); |
| | | 3205 | | if (percentSpan) percentSpan.setAttribute('title', 'Добавляет небольшой safety-сдвиг к целевому индексу'); |
| | | 3206 | | |
| | | 3207 | | if (window.bootstrap?.Popover) { |
| | | 3208 | | const popover = bootstrap.Popover.getOrCreateInstance(link, { |
| | | 3209 | | trigger: 'hover focus', |
| | | 3210 | | html: true, |
| | | 3211 | | placement: 'right', |
| | | 3212 | | sanitize: false, |
| | | 3213 | | container: 'body' |
| | | 3214 | | }); |
| | | 3215 | | |
| | | 3216 | | if (popover.tip) { |
| | | 3217 | | popover.setContent({ '.popover-body': hint }); |
| | | 3218 | | } |
| | | 3219 | | } |
| | | 3220 | | } |
| | | 3221 | | |
| | | 3222 | | |
| | | 3223 | | function recalcRanges(inputVal) { |
| | | 3224 | | // Пустое значение — прочерки; иначе диапазон прибыли = цена − введённая цена − комиссия (макс./мин.). |
| | | 3225 | | const isEmptyValue = inputVal === 0 || inputVal === '' || inputVal === null || inputVal === undefined; |
| | | 3226 | | Object.values(_ozonClients).forEach(client => { |
| | | 3227 | | const basePrice = parseFloat(client.prices.price) || 0; |
| | | 3228 | | const minComm = parseFloat(client.prices.min_commission) || 0; |
| | | 3229 | | const maxComm = parseFloat(client.prices.max_commission) || 0; |
| | | 3230 | | client.rangeMin = isEmptyValue ? '—' : Math.round(basePrice - inputVal - maxComm); |
| | | 3231 | | client.rangeMax = isEmptyValue ? '—' : Math.round(basePrice - inputVal - minComm); |
| | | 3232 | | }); |
| | | 3233 | | _ozonGridApi?.refreshCells({ columns: ['priceValue'], force: true }); |
| | | 3234 | | } |
| | | 3235 | | |
| | | 3236 | | document.addEventListener('input', function (e) { |
| | | 3237 | | if (e.target.matches('input[name="PriceValue"]')) { |
| | | 3238 | | const inputVal = parseFloat(e.target.value) || 0; |
| | | 3239 | | console.log("=== Пересчёт диапазонов === Введено:", inputVal); |
| | | 3240 | | recalcRanges(inputVal); |
| | | 3241 | | } |
| | | 3242 | | }); |
| | | 3243 | | |
| | | 3244 | | let selectedOzonWarehouse = null; |
| | | 3245 | | |
| | | 3246 | | document.addEventListener('click', function (e) { |
| | | 3247 | | if (e.target.matches('.warehouse-link')) { |
| | | 3248 | | e.preventDefault(); |
| | | 3249 | | |
| | | 3250 | | const warehouseLink = e.target; |
| | | 3251 | | |
| | | 3252 | | if (selectedOzonWarehouse === warehouseLink) { |
| | | 3253 | | resetOzonWarehouseSelection(); |
| | | 3254 | | return; |
| | | 3255 | | } |
| | | 3256 | | resetOzonWarehouseSelection(); |
| | | 3257 | | selectedOzonWarehouse = warehouseLink; |
| | | 3258 | | warehouseLink.classList.add('ozon-warehouse-selected'); |
| | | 3259 | | showNotification('Теперь нажмите на склад в таблице Bitrix', 'info'); |
| | | 3260 | | } |
| | | 3261 | | |
| | | 3262 | | if (e.target.matches('.bitrix-stock') && selectedOzonWarehouse) { |
| | | 3263 | | e.preventDefault(); |
| | | 3264 | | |
| | | 3265 | | const bitrixLink = e.target; |
| | | 3266 | | const ozonData = { |
| | | 3267 | | OzonClient: selectedOzonWarehouse.getAttribute('data-client'), |
| | | 3268 | | OfferId: selectedOzonWarehouse.getAttribute('data-offer-id'), |
| | | 3269 | | ProductId: Number(selectedOzonWarehouse.getAttribute('data-product-id')), |
| | | 3270 | | Stock: Number(bitrixLink.getAttribute('data-amount')), |
| | | 3271 | | WarehouseId: Number(selectedOzonWarehouse.getAttribute('data-warehouse-id')) |
| | | 3272 | | }; |
| | | 3273 | | |
| | | 3274 | | const bitrixData = { |
| | | 3275 | | amount: bitrixLink.getAttribute('data-amount'), |
| | | 3276 | | name: bitrixLink.getAttribute('data-name') |
| | | 3277 | | }; |
| | | 3278 | | |
| | | 3279 | | |
| | | 3280 | | $("#ozonTableContainer").html(` |
| | | 3281 | | <div class="d-flex justify-content-center align-items-center" style="height:100px;"> |
| | | 3282 | | <div class="spinner-border text-primary" role="status"> |
| | | 3283 | | <span class="visually-hidden">Загрузка Ozon...</span> |
| | | 3284 | | </div> |
| | | 3285 | | </div> |
| | | 3286 | | `); |
| | | 3287 | | |
| | | 3288 | | (async () => { |
| | | 3289 | | try { |
| | | 3290 | | const response = await fetch('/BitrixStock/UpdateProductStocks', { |
| | | 3291 | | method: 'POST', |
| | | 3292 | | headers: {'Content-Type': 'application/json'}, |
| | | 3293 | | body: JSON.stringify(ozonData) |
| | | 3294 | | }); |
| | | 3295 | | |
| | | 3296 | | const data = await response.json(); |
| | | 3297 | | |
| | | 3298 | | if (response.ok) { |
| | | 3299 | | showNotification(data.message ?? "Остатки обновлены", 'success'); |
| | | 3300 | | |
| | | 3301 | | // Создаем контейнер для лайв-обновлений |
| | | 3302 | | createLiveUpdatesContainer(); |
| | | 3303 | | |
| | | 3304 | | // Подключаемся к SignalR хабу для этого запроса |
| | | 3305 | | if (data.connectionId) { |
| | | 3306 | | await connectToPriceUpdates(data.connectionId); |
| | | 3307 | | } |
| | | 3308 | | |
| | | 3309 | | await reloadOzonPanel(ozonData.OfferId, null, ozonData.OzonClient); |
| | | 3310 | | } else { |
| | | 3311 | | console.error('Ответ сервера (ошибка):', data); |
| | | 3312 | | showNotification(data.message ?? "Ошибка при обновлении остатков", 'danger'); |
| | | 3313 | | } |
| | | 3314 | | } catch (error) { |
| | | 3315 | | console.error('Ошибка запроса:', error); |
| | | 3316 | | showNotification('Ошибка при отправке данных на сервер', 'danger'); |
| | | 3317 | | } |
| | | 3318 | | })(); |
| | | 3319 | | |
| | | 3320 | | resetOzonWarehouseSelection(); |
| | | 3321 | | } |
| | | 3322 | | }); |
| | | 3323 | | |
| | | 3324 | | // Хранилища выбранных элементов |
| | | 3325 | | const selectedRefreshItems = new Set(); // Для обновления |
| | | 3326 | | const selectedClearItems = new Set(); // Для обнуления |
| | | 3327 | | const selectedArchiveItems = new Set(); // Для архивации Ozon |
| | | 3328 | | const selectedUnarchiveItems = new Set(); // Для разархивации Ozon |
| | | 3329 | | |
| | | 3330 | | // Обновление интерфейса выбранных элементов |
| | | 3331 | | function updateSelectedUI() { |
| | | 3332 | | const refreshContainer = document.getElementById('refreshSelectedContainer'); |
| | | 3333 | | const clearContainer = document.getElementById('clearSelectedContainer'); |
| | | 3334 | | const archiveContainer = document.getElementById('archiveSelectedContainer'); |
| | | 3335 | | const unarchiveContainer = document.getElementById('unarchiveSelectedContainer'); |
| | | 3336 | | const refreshCountElement = document.getElementById('refreshSelectedCount'); |
| | | 3337 | | const clearCountElement = document.getElementById('clearSelectedCount'); |
| | | 3338 | | const archiveCountElement = document.getElementById('archiveSelectedCount'); |
| | | 3339 | | const unarchiveCountElement = document.getElementById('unarchiveSelectedCount'); |
| | | 3340 | | const selectAllArchiveBtn = document.getElementById('triggerSelectAllArchiveable'); |
| | | 3341 | | const selectAllUnarchiveBtn = document.getElementById('triggerSelectAllUnarchiveable'); |
| | | 3342 | | |
| | | 3343 | | // Для кнопки обновления выбранных |
| | | 3344 | | if (selectedRefreshItems.size > 0) { |
| | | 3345 | | refreshContainer.style.display = 'block'; |
| | | 3346 | | refreshCountElement.textContent = selectedRefreshItems.size; |
| | | 3347 | | } else { |
| | | 3348 | | refreshContainer.style.display = 'none'; |
| | | 3349 | | } |
| | | 3350 | | |
| | | 3351 | | // Для кнопки обнуления выбранных |
| | | 3352 | | if (selectedClearItems.size > 0) { |
| | | 3353 | | clearContainer.style.display = 'block'; |
| | | 3354 | | clearCountElement.textContent = selectedClearItems.size; |
| | | 3355 | | } else { |
| | | 3356 | | clearContainer.style.display = 'none'; |
| | | 3357 | | } |
| | | 3358 | | |
| | | 3359 | | if (selectedArchiveItems.size > 0) { |
| | | 3360 | | archiveContainer.style.display = 'block'; |
| | | 3361 | | archiveCountElement.textContent = selectedArchiveItems.size; |
| | | 3362 | | } else { |
| | | 3363 | | archiveContainer.style.display = 'none'; |
| | | 3364 | | } |
| | | 3365 | | |
| | | 3366 | | if (selectedUnarchiveItems.size > 0) { |
| | | 3367 | | unarchiveContainer.style.display = 'block'; |
| | | 3368 | | unarchiveCountElement.textContent = selectedUnarchiveItems.size; |
| | | 3369 | | } else { |
| | | 3370 | | unarchiveContainer.style.display = 'none'; |
| | | 3371 | | } |
| | | 3372 | | |
| | | 3373 | | const archivePicks = document.querySelectorAll('.ozon-archive-pick'); |
| | | 3374 | | if (selectAllArchiveBtn) { |
| | | 3375 | | selectAllArchiveBtn.style.display = archivePicks.length > 0 ? 'block' : 'none'; |
| | | 3376 | | if (archivePicks.length > 0) { |
| | | 3377 | | const allArchiveSelected = areAllArchivePicksOnPageSelected(archivePicks); |
| | | 3378 | | selectAllArchiveBtn.textContent = allArchiveSelected ? 'Снять «Нет»' : 'Выделить «Нет»'; |
| | | 3379 | | selectAllArchiveBtn.title = allArchiveSelected |
| | | 3380 | | ? 'Снять выделение со всех «Нет» на странице' |
| | | 3381 | | : 'Выделить все не в архиве на странице'; |
| | | 3382 | | } |
| | | 3383 | | } |
| | | 3384 | | |
| | | 3385 | | const unarchivePicks = document.querySelectorAll('.ozon-unarchive-pick'); |
| | | 3386 | | if (selectAllUnarchiveBtn) { |
| | | 3387 | | selectAllUnarchiveBtn.style.display = unarchivePicks.length > 0 ? 'block' : 'none'; |
| | | 3388 | | if (unarchivePicks.length > 0) { |
| | | 3389 | | const allUnarchiveSelected = areAllUnarchivePicksOnPageSelected(unarchivePicks); |
| | | 3390 | | selectAllUnarchiveBtn.textContent = allUnarchiveSelected ? 'Снять «Да»' : 'Выделить «Да»'; |
| | | 3391 | | selectAllUnarchiveBtn.title = allUnarchiveSelected |
| | | 3392 | | ? 'Снять выделение со всех «Да» на странице' |
| | | 3393 | | : 'Выделить все в архиве на странице'; |
| | | 3394 | | } |
| | | 3395 | | } |
| | | 3396 | | } |
| | | 3397 | | |
| | | 3398 | | function areAllArchivePicksOnPageSelected(picks) { |
| | | 3399 | | const nodes = picks || document.querySelectorAll('.ozon-archive-pick'); |
| | | 3400 | | if (!nodes.length) return false; |
| | | 3401 | | for (const pick of nodes) { |
| | | 3402 | | const productId = Number(pick.getAttribute('data-ozon-product-id')); |
| | | 3403 | | const client = pick.getAttribute('data-ozon-client') || ''; |
| | | 3404 | | if (!selectedArchiveItems.has(archiveSelectionKey(client, productId))) { |
| | | 3405 | | return false; |
| | | 3406 | | } |
| | | 3407 | | } |
| | | 3408 | | return true; |
| | | 3409 | | } |
| | | 3410 | | |
| | | 3411 | | function areAllUnarchivePicksOnPageSelected(picks) { |
| | | 3412 | | const nodes = picks || document.querySelectorAll('.ozon-unarchive-pick'); |
| | | 3413 | | if (!nodes.length) return false; |
| | | 3414 | | for (const pick of nodes) { |
| | | 3415 | | const productId = Number(pick.getAttribute('data-ozon-product-id')); |
| | | 3416 | | const client = pick.getAttribute('data-ozon-client') || ''; |
| | | 3417 | | if (!selectedUnarchiveItems.has(unarchiveSelectionKey(client, productId))) { |
| | | 3418 | | return false; |
| | | 3419 | | } |
| | | 3420 | | } |
| | | 3421 | | return true; |
| | | 3422 | | } |
| | | 3423 | | |
| | | 3424 | | function toggleSelectAllArchiveableOnPage() { |
| | | 3425 | | const picks = document.querySelectorAll('.ozon-archive-pick'); |
| | | 3426 | | if (!picks.length) return; |
| | | 3427 | | const deselect = areAllArchivePicksOnPageSelected(picks); |
| | | 3428 | | picks.forEach(pick => { |
| | | 3429 | | const productId = Number(pick.getAttribute('data-ozon-product-id')); |
| | | 3430 | | const client = pick.getAttribute('data-ozon-client') || ''; |
| | | 3431 | | const key = archiveSelectionKey(client, productId); |
| | | 3432 | | if (deselect) { |
| | | 3433 | | selectedArchiveItems.delete(key); |
| | | 3434 | | pick.classList.remove('selected-archive-item'); |
| | | 3435 | | } else { |
| | | 3436 | | selectedArchiveItems.add(key); |
| | | 3437 | | pick.classList.add('selected-archive-item'); |
| | | 3438 | | } |
| | | 3439 | | }); |
| | | 3440 | | updateSelectedUI(); |
| | | 3441 | | showNotification( |
| | | 3442 | | deselect ? 'Выделение «Нет» снято' : `Выделено ${selectedArchiveItems.size} позиций для архивации`, |
| | | 3443 | | 'success'); |
| | | 3444 | | } |
| | | 3445 | | |
| | | 3446 | | function toggleSelectAllUnarchiveableOnPage() { |
| | | 3447 | | const picks = document.querySelectorAll('.ozon-unarchive-pick'); |
| | | 3448 | | if (!picks.length) return; |
| | | 3449 | | const deselect = areAllUnarchivePicksOnPageSelected(picks); |
| | | 3450 | | picks.forEach(pick => { |
| | | 3451 | | const productId = Number(pick.getAttribute('data-ozon-product-id')); |
| | | 3452 | | const client = pick.getAttribute('data-ozon-client') || ''; |
| | | 3453 | | const key = unarchiveSelectionKey(client, productId); |
| | | 3454 | | if (deselect) { |
| | | 3455 | | selectedUnarchiveItems.delete(key); |
| | | 3456 | | pick.classList.remove('selected-unarchive-item'); |
| | | 3457 | | } else { |
| | | 3458 | | selectedUnarchiveItems.add(key); |
| | | 3459 | | pick.classList.add('selected-unarchive-item'); |
| | | 3460 | | } |
| | | 3461 | | }); |
| | | 3462 | | updateSelectedUI(); |
| | | 3463 | | showNotification( |
| | | 3464 | | deselect ? 'Выделение «Да» снято' : `Выделено ${selectedUnarchiveItems.size} позиций для восстановления`, |
| | | 3465 | | 'success'); |
| | | 3466 | | } |
| | | 3467 | | |
| | | 3468 | | function refreshSelectedStocks() { |
| | | 3469 | | if (selectedRefreshItems.size === 0) return; |
| | | 3470 | | const items = collectSelectedRefreshPayload(); |
| | | 3471 | | if (!items.length) { |
| | | 3472 | | showNotification('Не выбраны позиции для наката', 'danger'); |
| | | 3473 | | return; |
| | | 3474 | | } |
| | | 3475 | | openPushConfirmModal(items); |
| | | 3476 | | } |
| | | 3477 | | |
| | 0 | 3478 | | const clearJobStartUrl = '@Url.Action("StartClearJob", "BitrixStock", new { area = "Studio2" })'; |
| | 0 | 3479 | | const clearJobsListUrl = '@Url.Action("GetClearJobs", "BitrixStock", new { area = "Studio2" })'; |
| | 0 | 3480 | | const clearHistoryTabUrl = '@BuildClearHistoryTabUrl()'; |
| | 0 | 3481 | | const archiveJobStartUrl = '@Url.Action("StartArchiveJob", "BitrixStock", new { area = "Studio2" })'; |
| | 0 | 3482 | | const archiveJobsListUrl = '@Url.Action("GetArchiveJobs", "BitrixStock", new { area = "Studio2" })'; |
| | 0 | 3483 | | const unarchiveJobStartUrl = '@Url.Action("StartUnarchiveJob", "BitrixStock", new { area = "Studio2" })'; |
| | 0 | 3484 | | const unarchiveJobsListUrl = '@Url.Action("GetUnarchiveJobs", "BitrixStock", new { area = "Studio2" })'; |
| | 0 | 3485 | | const pushJobStartUrl = '@Url.Action("StartPushJob", "BitrixStock", new { area = "Studio2" })'; |
| | 0 | 3486 | | const pushJobsListUrl = '@Url.Action("GetPushJobs", "BitrixStock", new { area = "Studio2" })'; |
| | 0 | 3487 | | const pushHistoryTabUrl = '@BuildPushHistoryTabUrl()'; |
| | | 3488 | | let pendingClearItems = []; |
| | | 3489 | | let pendingArchiveItems = []; |
| | | 3490 | | let pendingUnarchiveItems = []; |
| | | 3491 | | let pendingPushItems = []; |
| | | 3492 | | let bitrixClearJobConnection = null; |
| | | 3493 | | let bitrixArchiveJobConnection = null; |
| | | 3494 | | let bitrixUnarchiveJobConnection = null; |
| | | 3495 | | let bitrixClearElapsedTimer = null; |
| | | 3496 | | let bitrixClearStartedAt = 0; |
| | | 3497 | | let currentClearExecutionId = null; |
| | | 3498 | | let bitrixArchiveElapsedTimer = null; |
| | | 3499 | | let bitrixArchiveStartedAt = 0; |
| | | 3500 | | let currentArchiveExecutionId = null; |
| | | 3501 | | let currentUnarchiveExecutionId = null; |
| | | 3502 | | let bitrixUnarchiveElapsedTimer = null; |
| | | 3503 | | let bitrixUnarchiveStartedAt = 0; |
| | | 3504 | | |
| | | 3505 | | function newClearJobId() { |
| | | 3506 | | if (window.crypto && typeof window.crypto.randomUUID === 'function') { |
| | | 3507 | | return window.crypto.randomUUID(); |
| | | 3508 | | } |
| | | 3509 | | return 'clear-' + Date.now() + '-' + Math.random().toString(16).slice(2); |
| | | 3510 | | } |
| | | 3511 | | |
| | | 3512 | | function newArchiveJobId() { |
| | | 3513 | | if (window.crypto && typeof window.crypto.randomUUID === 'function') { |
| | | 3514 | | return window.crypto.randomUUID(); |
| | | 3515 | | } |
| | | 3516 | | return 'archive-' + Date.now() + '-' + Math.random().toString(16).slice(2); |
| | | 3517 | | } |
| | | 3518 | | |
| | | 3519 | | function newUnarchiveJobId() { |
| | | 3520 | | if (window.crypto && typeof window.crypto.randomUUID === 'function') { |
| | | 3521 | | return window.crypto.randomUUID(); |
| | | 3522 | | } |
| | | 3523 | | return 'unarchive-' + Date.now() + '-' + Math.random().toString(16).slice(2); |
| | | 3524 | | } |
| | | 3525 | | |
| | | 3526 | | function unarchiveSelectionKey(client, productId) { |
| | | 3527 | | return `unarchive_${(client || '').trim().toLowerCase()}_${productId}`; |
| | | 3528 | | } |
| | | 3529 | | |
| | | 3530 | | function unarchiveItemFromPick(pick) { |
| | | 3531 | | const row = pick.closest('.bitrix-stock-row'); |
| | | 3532 | | const isAuto = pick.getAttribute('data-is-auto-archived') === '1'; |
| | | 3533 | | const isManual = pick.getAttribute('data-is-manual-archived') === '1'; |
| | | 3534 | | return { |
| | | 3535 | | article: row?.getAttribute('data-article') || '', |
| | | 3536 | | offerId: pick.getAttribute('data-offer-id') || '', |
| | | 3537 | | ozonProductId: Number(pick.getAttribute('data-ozon-product-id')), |
| | | 3538 | | ozonClient: pick.getAttribute('data-ozon-client') || '', |
| | | 3539 | | wasInArchive: true, |
| | | 3540 | | isAutoArchived: isAuto, |
| | | 3541 | | isManualArchived: isManual |
| | | 3542 | | }; |
| | | 3543 | | } |
| | | 3544 | | |
| | | 3545 | | function toggleUnarchivePickSelection(pick) { |
| | | 3546 | | if (!pick) return; |
| | | 3547 | | const productId = Number(pick.getAttribute('data-ozon-product-id')); |
| | | 3548 | | const client = pick.getAttribute('data-ozon-client') || ''; |
| | | 3549 | | const key = unarchiveSelectionKey(client, productId); |
| | | 3550 | | if (selectedUnarchiveItems.has(key)) { |
| | | 3551 | | selectedUnarchiveItems.delete(key); |
| | | 3552 | | pick.classList.remove('selected-unarchive-item'); |
| | | 3553 | | } else { |
| | | 3554 | | selectedUnarchiveItems.add(key); |
| | | 3555 | | pick.classList.add('selected-unarchive-item'); |
| | | 3556 | | } |
| | | 3557 | | updateSelectedUI(); |
| | | 3558 | | } |
| | | 3559 | | |
| | | 3560 | | function collectSelectedUnarchivePayload() { |
| | | 3561 | | const picks = document.querySelectorAll('.ozon-unarchive-pick.selected-unarchive-item'); |
| | | 3562 | | const seen = new Set(); |
| | | 3563 | | const items = []; |
| | | 3564 | | picks.forEach(pick => { |
| | | 3565 | | const item = unarchiveItemFromPick(pick); |
| | | 3566 | | const key = unarchiveSelectionKey(item.ozonClient, item.ozonProductId); |
| | | 3567 | | if (!item.ozonProductId || !item.ozonClient || seen.has(key)) return; |
| | | 3568 | | seen.add(key); |
| | | 3569 | | items.push(item); |
| | | 3570 | | }); |
| | | 3571 | | return items; |
| | | 3572 | | } |
| | | 3573 | | |
| | | 3574 | | // Модалки подтверждения массовых действий — гриды AG Grid (сортировка, фильтры, поиск по списку позиций). |
| | | 3575 | | const confirmSet = () => ({ filter: window.BsxGrid.SetFilter, floatingFilterComponent: window.BsxGrid.SetFloatingFil |
| | | 3576 | | const confirmTables = { |
| | | 3577 | | unarchive: window.BsxGrid.localTable('bitrixUnarchiveConfirmTable', { key: 'confirmUnarchive', columnDefs: [ |
| | | 3578 | | { field: 'article', headerName: 'Артикул', width: 160, cellClass: 'fw-semibold' }, |
| | | 3579 | | { field: 'offerId', headerName: 'Offer', flex: 1, minWidth: 160 }, |
| | | 3580 | | { field: 'ozonClient', headerName: 'Клиент Ozon', width: 150, ...confirmSet() }, |
| | | 3581 | | { colId: 'type', headerName: 'Тип архива', width: 130, valueGetter: p => p.data ? (p.data.isManualArchived ? |
| | | 3582 | | { field: 'ozonProductId', headerName: 'product_id', width: 130, type: 'numericColumn' } |
| | | 3583 | | ] }), |
| | | 3584 | | archive: window.BsxGrid.localTable('bitrixArchiveConfirmTable', { key: 'confirmArchive', columnDefs: [ |
| | | 3585 | | { field: 'article', headerName: 'Артикул', width: 160, cellClass: 'fw-semibold' }, |
| | | 3586 | | { field: 'offerId', headerName: 'Offer', flex: 1, minWidth: 160 }, |
| | | 3587 | | { field: 'ozonClient', headerName: 'Клиент Ozon', width: 150, ...confirmSet() }, |
| | | 3588 | | { field: 'ozonProductId', headerName: 'product_id', width: 130, type: 'numericColumn' } |
| | | 3589 | | ] }), |
| | | 3590 | | clear: window.BsxGrid.localTable('bitrixClearConfirmTable', { key: 'confirmClear', columnDefs: [ |
| | | 3591 | | { field: 'article', headerName: 'Артикул', width: 160, cellClass: 'fw-semibold' }, |
| | | 3592 | | { field: 'offerId', headerName: 'Offer', flex: 1, minWidth: 160 }, |
| | | 3593 | | { field: 'ozonClient', headerName: 'Клиент Ozon', width: 150, ...confirmSet() }, |
| | | 3594 | | { colId: 'warehouse', headerName: 'Склад', width: 170, valueGetter: p => p.data ? (p.data.warehouseName || p |
| | | 3595 | | { field: 'previousStock', headerName: 'Было', width: 90, type: 'numericColumn', filter: 'agNumberColumnFilte |
| | | 3596 | | ] }), |
| | | 3597 | | push: window.BsxGrid.localTable('bitrixPushConfirmTable', { key: 'confirmPush', columnDefs: [ |
| | | 3598 | | { field: 'article', headerName: 'Артикул', width: 150, cellClass: 'fw-semibold' }, |
| | | 3599 | | { field: 'offerId', headerName: 'Offer', flex: 1, minWidth: 150 }, |
| | | 3600 | | { field: 'ozonClient', headerName: 'Клиент Ozon', width: 140, ...confirmSet() }, |
| | | 3601 | | { colId: 'warehouse', headerName: 'Склад Ozon', width: 160, valueGetter: p => p.data ? (p.data.warehouseName |
| | | 3602 | | { field: 'linkedStoreName', headerName: 'Связь Bitrix', width: 120, valueFormatter: p => p.value || '—', ... |
| | | 3603 | | { field: 'previousStock', headerName: 'Ozon', width: 85, type: 'numericColumn', filter: 'agNumberColumnFilte |
| | | 3604 | | { field: 'targetStock', headerName: 'Цель', width: 85, type: 'numericColumn', filter: 'agNumberColumnFilter' |
| | | 3605 | | ] }) |
| | | 3606 | | }; |
| | | 3607 | | |
| | | 3608 | | function openUnarchiveConfirmModal(items) { |
| | | 3609 | | pendingUnarchiveItems = items; |
| | | 3610 | | confirmTables.unarchive.setRows(items); |
| | | 3611 | | bootstrap.Modal.getOrCreateInstance(document.getElementById('bitrixUnarchiveConfirmModal')).show(); |
| | | 3612 | | } |
| | | 3613 | | |
| | | 3614 | | function unarchiveSelectedProducts() { |
| | | 3615 | | if (selectedUnarchiveItems.size === 0) return; |
| | | 3616 | | const items = collectSelectedUnarchivePayload(); |
| | | 3617 | | if (!items.length) { |
| | | 3618 | | showNotification('Не выбраны позиции для восстановления', 'danger'); |
| | | 3619 | | return; |
| | | 3620 | | } |
| | | 3621 | | openUnarchiveConfirmModal(items); |
| | | 3622 | | } |
| | | 3623 | | |
| | | 3624 | | async function unarchiveSingleFromYesClick(pick) { |
| | | 3625 | | const item = unarchiveItemFromPick(pick); |
| | | 3626 | | if (!item.ozonProductId || !item.ozonClient) { |
| | | 3627 | | showNotification('Нет product_id Ozon для восстановления', 'warning'); |
| | | 3628 | | return; |
| | | 3629 | | } |
| | | 3630 | | const typeHint = item.isAutoArchived ? '\n(авто-архив: лимит 100/сутки)' : ''; |
| | | 3631 | | const msg = item.offerId |
| | | 3632 | | ? `Восстановить из архива Ozon?\nOffer: ${item.offerId}\nКлиент: ${item.ozonClient}${typeHint}` |
| | | 3633 | | : `Восстановить из архива Ozon?\nКлиент: ${item.ozonClient}${typeHint}`; |
| | | 3634 | | if (!(await St2Dialog.confirm(msg, { confirmText: 'Восстановить' }))) return; |
| | | 3635 | | pendingUnarchiveItems = [item]; |
| | | 3636 | | await startBitrixUnarchiveJob({ silent: true }); |
| | | 3637 | | } |
| | | 3638 | | |
| | | 3639 | | function collectSelectedArchivePayload() { |
| | | 3640 | | const picks = document.querySelectorAll('.ozon-archive-pick.selected-archive-item'); |
| | | 3641 | | const seen = new Set(); |
| | | 3642 | | const items = []; |
| | | 3643 | | picks.forEach(pick => { |
| | | 3644 | | const productId = Number(pick.getAttribute('data-ozon-product-id')); |
| | | 3645 | | const client = pick.getAttribute('data-ozon-client') || ''; |
| | | 3646 | | const offerId = pick.getAttribute('data-offer-id') || ''; |
| | | 3647 | | const key = archiveSelectionKey(client, productId); |
| | | 3648 | | if (!productId || !client || seen.has(key)) { |
| | | 3649 | | return; |
| | | 3650 | | } |
| | | 3651 | | seen.add(key); |
| | | 3652 | | const row = pick.closest('.bitrix-stock-row'); |
| | | 3653 | | items.push({ |
| | | 3654 | | article: row?.getAttribute('data-article') || '', |
| | | 3655 | | offerId: offerId, |
| | | 3656 | | ozonProductId: productId, |
| | | 3657 | | ozonClient: client, |
| | | 3658 | | wasArchived: false |
| | | 3659 | | }); |
| | | 3660 | | }); |
| | | 3661 | | return items; |
| | | 3662 | | } |
| | | 3663 | | |
| | | 3664 | | function openArchiveConfirmModal(items) { |
| | | 3665 | | pendingArchiveItems = items; |
| | | 3666 | | confirmTables.archive.setRows(items); |
| | | 3667 | | bootstrap.Modal.getOrCreateInstance(document.getElementById('bitrixArchiveConfirmModal')).show(); |
| | | 3668 | | } |
| | | 3669 | | |
| | | 3670 | | function archiveSelectedProducts() { |
| | | 3671 | | if (selectedArchiveItems.size === 0) return; |
| | | 3672 | | const items = collectSelectedArchivePayload(); |
| | | 3673 | | if (!items.length) { |
| | | 3674 | | showNotification('Не выбраны позиции для архивации (нужен product_id Ozon)', 'danger'); |
| | | 3675 | | return; |
| | | 3676 | | } |
| | | 3677 | | openArchiveConfirmModal(items); |
| | | 3678 | | } |
| | | 3679 | | |
| | | 3680 | | function collectSelectedClearPayload() { |
| | | 3681 | | const selectedButtons = document.querySelectorAll('.clearOzonStock.selected-clear-item'); |
| | | 3682 | | return Array.from(selectedButtons).map(btn => { |
| | | 3683 | | const row = btn.closest('.bitrix-stock-row'); |
| | | 3684 | | const article = row?.getAttribute('data-article') || ''; |
| | | 3685 | | return { |
| | | 3686 | | article: article, |
| | | 3687 | | offerId: btn.getAttribute('data-offer-id') || '', |
| | | 3688 | | productId: Number(btn.getAttribute('data-product-id')), |
| | | 3689 | | warehouseId: Number(btn.getAttribute('data-warehouse-id')), |
| | | 3690 | | warehouseName: btn.getAttribute('data-warehouse-name') || '', |
| | | 3691 | | ozonClient: btn.getAttribute('data-client') || '', |
| | | 3692 | | previousStock: Number(btn.getAttribute('data-previous-stock')) || 0 |
| | | 3693 | | }; |
| | | 3694 | | }); |
| | | 3695 | | } |
| | | 3696 | | |
| | | 3697 | | function openClearConfirmModal(items) { |
| | | 3698 | | pendingClearItems = items; |
| | | 3699 | | confirmTables.clear.setRows(items); |
| | | 3700 | | bootstrap.Modal.getOrCreateInstance(document.getElementById('bitrixClearConfirmModal')).show(); |
| | | 3701 | | } |
| | | 3702 | | |
| | | 3703 | | function escapeHtml(text) { |
| | | 3704 | | const div = document.createElement('div'); |
| | | 3705 | | div.textContent = text ?? ''; |
| | | 3706 | | return div.innerHTML; |
| | | 3707 | | } |
| | | 3708 | | |
| | | 3709 | | function clearSelectedStocks() { |
| | | 3710 | | if (selectedClearItems.size === 0) return; |
| | | 3711 | | const items = collectSelectedClearPayload(); |
| | | 3712 | | if (!items.length) { |
| | | 3713 | | showNotification('Не выбраны позиции для обнуления', 'danger'); |
| | | 3714 | | return; |
| | | 3715 | | } |
| | | 3716 | | openClearConfirmModal(items); |
| | | 3717 | | } |
| | | 3718 | | |
| | | 3719 | | function appendBitrixClearLog(message) { |
| | | 3720 | | const consoleEl = document.getElementById('bitrixClearLogConsole'); |
| | | 3721 | | if (!consoleEl) return; |
| | | 3722 | | const line = document.createElement('div'); |
| | | 3723 | | line.className = 'log-line'; |
| | | 3724 | | const ts = new Date().toLocaleTimeString('ru-RU'); |
| | | 3725 | | line.textContent = `[${ts}] ${message}`; |
| | | 3726 | | consoleEl.appendChild(line); |
| | | 3727 | | consoleEl.scrollTop = consoleEl.scrollHeight; |
| | | 3728 | | } |
| | | 3729 | | |
| | | 3730 | | function setBitrixClearProgress(current, total, text) { |
| | | 3731 | | const bar = document.getElementById('bitrixClearProgressBar'); |
| | | 3732 | | const txt = document.getElementById('bitrixClearProgressText'); |
| | | 3733 | | if (!bar || !txt) return; |
| | | 3734 | | const pct = total > 0 ? Math.round((current / total) * 100) : 0; |
| | | 3735 | | bar.style.width = pct + '%'; |
| | | 3736 | | bar.textContent = pct + '%'; |
| | | 3737 | | txt.textContent = text || (total > 0 ? `Обработано ${current} из ${total}` : 'Выполнение…'); |
| | | 3738 | | } |
| | | 3739 | | |
| | | 3740 | | function resetBitrixClearProgressModal() { |
| | | 3741 | | const log = document.getElementById('bitrixClearLogConsole'); |
| | | 3742 | | const bar = document.getElementById('bitrixClearProgressBar'); |
| | | 3743 | | const summary = document.getElementById('bitrixClearResultSummary'); |
| | | 3744 | | const reportWrap = document.getElementById('bitrixClearReportTableWrap'); |
| | | 3745 | | const closeBtn = document.getElementById('bitrixClearProgressCloseBtn'); |
| | | 3746 | | const closeFooter = document.getElementById('bitrixClearProgressFooterClose'); |
| | | 3747 | | if (log) log.innerHTML = ''; |
| | | 3748 | | if (bar) { bar.style.width = '0%'; bar.textContent = '0%'; } |
| | | 3749 | | if (summary) { summary.classList.add('d-none'); summary.innerHTML = ''; summary.classList.remove('ok'); } |
| | | 3750 | | if (reportWrap) reportWrap.classList.add('d-none'); |
| | | 3751 | | if (closeBtn) closeBtn.style.display = 'none'; |
| | | 3752 | | if (closeFooter) closeFooter.style.display = 'none'; |
| | | 3753 | | if (bitrixClearElapsedTimer) clearInterval(bitrixClearElapsedTimer); |
| | | 3754 | | document.getElementById('bitrixClearElapsed').textContent = '00:00'; |
| | | 3755 | | } |
| | | 3756 | | |
| | | 3757 | | function startBitrixClearElapsedTimer() { |
| | | 3758 | | bitrixClearStartedAt = Date.now(); |
| | | 3759 | | if (bitrixClearElapsedTimer) clearInterval(bitrixClearElapsedTimer); |
| | | 3760 | | bitrixClearElapsedTimer = setInterval(() => { |
| | | 3761 | | const s = Math.max(0, Math.floor((Date.now() - bitrixClearStartedAt) / 1000)); |
| | | 3762 | | const el = document.getElementById('bitrixClearElapsed'); |
| | | 3763 | | if (el) el.textContent = String(Math.floor(s / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0') |
| | | 3764 | | }, 1000); |
| | | 3765 | | } |
| | | 3766 | | |
| | | 3767 | | function finishBitrixClearModal(success) { |
| | | 3768 | | document.getElementById('bitrixClearProgressCloseBtn').style.display = ''; |
| | | 3769 | | document.getElementById('bitrixClearProgressFooterClose').style.display = ''; |
| | | 3770 | | const historyLink = document.getElementById('bitrixClearProgressHistoryLink'); |
| | | 3771 | | if (historyLink) historyLink.style.display = ''; |
| | | 3772 | | if (bitrixClearElapsedTimer) clearInterval(bitrixClearElapsedTimer); |
| | | 3773 | | const bar = document.getElementById('bitrixClearProgressBar'); |
| | | 3774 | | if (bar) bar.classList.remove('progress-bar-animated'); |
| | | 3775 | | } |
| | | 3776 | | |
| | | 3777 | | async function connectBitrixClearSignalR(jobId) { |
| | | 3778 | | if (typeof signalR === 'undefined') { |
| | | 3779 | | appendBitrixClearLog('SignalR не загружен'); |
| | | 3780 | | return; |
| | | 3781 | | } |
| | | 3782 | | if (bitrixClearJobConnection) { |
| | | 3783 | | try { await bitrixClearJobConnection.stop(); } catch (_) { } |
| | | 3784 | | } |
| | | 3785 | | bitrixClearJobConnection = new signalR.HubConnectionBuilder() |
| | | 3786 | | .withUrl('/jobProgressHub') |
| | | 3787 | | .withAutomaticReconnect() |
| | | 3788 | | .build(); |
| | | 3789 | | |
| | | 3790 | | bitrixClearJobConnection.on('Log', msg => appendBitrixClearLog(msg)); |
| | | 3791 | | bitrixClearJobConnection.on('Progress', (current, total, text) => setBitrixClearProgress(current, total, text)); |
| | | 3792 | | bitrixClearJobConnection.on('Complete', async (success, report) => { |
| | | 3793 | | const summary = document.getElementById('bitrixClearResultSummary'); |
| | | 3794 | | if (summary) { |
| | | 3795 | | summary.innerHTML = report || ''; |
| | | 3796 | | summary.classList.remove('d-none'); |
| | | 3797 | | if (success) summary.classList.add('ok'); |
| | | 3798 | | } |
| | | 3799 | | finishBitrixClearModal(success); |
| | | 3800 | | if (currentClearExecutionId) { |
| | | 3801 | | const historyLink = document.getElementById('bitrixClearProgressHistoryLink'); |
| | | 3802 | | if (historyLink) { |
| | | 3803 | | const sep = clearHistoryTabUrl.includes('?') ? '&' : '?'; |
| | | 3804 | | historyLink.href = `${clearHistoryTabUrl}${sep}executionId=${currentClearExecutionId}`; |
| | | 3805 | | } |
| | | 3806 | | await loadClearJobReportTable(currentClearExecutionId); |
| | | 3807 | | } |
| | | 3808 | | if (document.getElementById('bitrixClearJobsHistoryBody')) { |
| | | 3809 | | loadClearJobsHistory(); |
| | | 3810 | | } else { |
| | | 3811 | | appendBitrixClearLog('Список запусков — вкладка «Обнуление» на этой странице.'); |
| | | 3812 | | } |
| | | 3813 | | document.querySelectorAll('.clearOzonStock.selected-clear-item').forEach(btn => { |
| | | 3814 | | btn.classList.remove('selected-clear-item'); |
| | | 3815 | | const productId = btn.getAttribute('data-product-id'); |
| | | 3816 | | const warehouseId = btn.getAttribute('data-warehouse-id'); |
| | | 3817 | | const stockDiv = document.querySelector( |
| | | 3818 | | `.col-4.text-end[data-product-id="${productId}"][data-warehouse-id="${warehouseId}"]` |
| | | 3819 | | ); |
| | | 3820 | | if (stockDiv) { |
| | | 3821 | | stockDiv.textContent = '0'; |
| | | 3822 | | } |
| | | 3823 | | }); |
| | | 3824 | | selectedClearItems.clear(); |
| | | 3825 | | updateSelectedUI(); |
| | | 3826 | | }); |
| | | 3827 | | bitrixClearJobConnection.on('Error', msg => { |
| | | 3828 | | appendBitrixClearLog('ОШИБКА: ' + msg); |
| | | 3829 | | finishBitrixClearModal(false); |
| | | 3830 | | }); |
| | | 3831 | | |
| | | 3832 | | await bitrixClearJobConnection.start(); |
| | | 3833 | | await bitrixClearJobConnection.invoke('JoinJobGroup', jobId); |
| | | 3834 | | appendBitrixClearLog('Подключено к логам задания'); |
| | | 3835 | | } |
| | | 3836 | | |
| | | 3837 | | async function startBitrixClearJob() { |
| | | 3838 | | if (!pendingClearItems.length) return; |
| | | 3839 | | const jobId = newClearJobId(); |
| | | 3840 | | const progressModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('bitrixClearProgressModal')); |
| | | 3841 | | bootstrap.Modal.getInstance(document.getElementById('bitrixClearConfirmModal'))?.hide(); |
| | | 3842 | | resetBitrixClearProgressModal(); |
| | | 3843 | | progressModal.show(); |
| | | 3844 | | startBitrixClearElapsedTimer(); |
| | | 3845 | | appendBitrixClearLog('Отправка задания на сервер…'); |
| | | 3846 | | |
| | | 3847 | | try { |
| | | 3848 | | const response = await fetch(clearJobStartUrl, { |
| | | 3849 | | method: 'POST', |
| | | 3850 | | headers: { 'Content-Type': 'application/json' }, |
| | | 3851 | | body: JSON.stringify({ jobId: jobId, items: pendingClearItems }) |
| | | 3852 | | }); |
| | | 3853 | | const data = await response.json().catch(() => ({})); |
| | | 3854 | | if (!response.ok) { |
| | | 3855 | | throw new Error(data.error || 'Не удалось запустить задание'); |
| | | 3856 | | } |
| | | 3857 | | currentClearExecutionId = data.executionId; |
| | | 3858 | | appendBitrixClearLog(`Задание #${data.executionId}, jobId=${jobId}`); |
| | | 3859 | | await connectBitrixClearSignalR(jobId); |
| | | 3860 | | } catch (err) { |
| | | 3861 | | appendBitrixClearLog('ОШИБКА: ' + (err.message || err)); |
| | | 3862 | | finishBitrixClearModal(false); |
| | | 3863 | | showNotification(err.message || 'Ошибка запуска', 'danger'); |
| | | 3864 | | } |
| | | 3865 | | } |
| | | 3866 | | |
| | | 3867 | | async function loadClearJobReportTable(executionId) { |
| | | 3868 | | const url = `${clearJobsListUrl}/${executionId}`; |
| | | 3869 | | try { |
| | | 3870 | | const response = await fetch(url); |
| | | 3871 | | const data = await response.json(); |
| | | 3872 | | if (!data.success || !data.execution?.items) return; |
| | | 3873 | | const wrap = document.getElementById('bitrixClearReportTableWrap'); |
| | | 3874 | | if (!wrap) return; |
| | | 3875 | | wrap.classList.remove('d-none'); |
| | | 3876 | | mountJobReportGrid('bitrixClearReportGrid', data.execution.items, 'stock'); |
| | | 3877 | | } catch (e) { |
| | | 3878 | | console.warn('Отчёт не загружен', e); |
| | | 3879 | | } |
| | | 3880 | | } |
| | | 3881 | | |
| | | 3882 | | // Отчёты задач (обнуление, накат, архивация, разархивация) в модалках прогресса — гриды AG Grid. |
| | | 3883 | | const _jobReportGrids = {}; |
| | | 3884 | | function mountJobReportGrid(gridId, items, kind) { |
| | | 3885 | | const el = document.getElementById(gridId); |
| | | 3886 | | if (!el || !window.BsxGrid) return; |
| | | 3887 | | try { _jobReportGrids[gridId]?.destroy(); } catch (_) { } |
| | | 3888 | | const set = { filter: window.BsxGrid.SetFilter, floatingFilterComponent: window.BsxGrid.SetFloatingFilter }; |
| | | 3889 | | const warehouse = { colId: 'warehouse', headerName: 'Склад', width: 170, valueGetter: p => p.data.warehouseName |
| | | 3890 | | const client = { field: 'ozonClient', headerName: 'Клиент', width: 150, ...set }; |
| | | 3891 | | const productId = { field: 'ozonProductId', headerName: 'product_id', width: 120, type: 'numericColumn' }; |
| | | 3892 | | const extra = { |
| | | 3893 | | stock: [warehouse], |
| | | 3894 | | push: [warehouse, { colId: 'stock', headerName: 'Ozon → цель', width: 120, valueGetter: p => `${p.data.previ |
| | | 3895 | | archive: [client, productId], |
| | | 3896 | | unarchive: [client, { colId: 'type', headerName: 'Тип', width: 110, ...set, |
| | | 3897 | | valueGetter: p => p.data.isManualArchived ? 'Вручную' : p.data.isAutoArchived ? 'Системой' : '—' }, prod |
| | | 3898 | | }[kind] || []; |
| | | 3899 | | _jobReportGrids[gridId] = window.BsxGrid.create(el, { |
| | | 3900 | | key: 'bitrixJobReport', |
| | | 3901 | | rowData: items || [], |
| | | 3902 | | columnDefs: [ |
| | | 3903 | | { field: 'article', headerName: 'Артикул', width: 150 }, |
| | | 3904 | | { field: 'offerId', headerName: 'Offer', width: 160 }, |
| | | 3905 | | ...extra, |
| | | 3906 | | { field: 'status', headerName: 'Статус', width: 110, ...set }, |
| | | 3907 | | { field: 'errorMessage', headerName: 'Ошибка', flex: 1, minWidth: 200, tooltipField: 'errorMessage', cel |
| | | 3908 | | ], |
| | | 3909 | | gridOptions: { |
| | | 3910 | | rowClassRules: { |
| | | 3911 | | 'bsx-row-success': p => p.data?.status === 'Success', |
| | | 3912 | | 'bsx-row-danger': p => p.data?.status === 'Failed' |
| | | 3913 | | } |
| | | 3914 | | } |
| | | 3915 | | }); |
| | | 3916 | | } |
| | | 3917 | | |
| | | 3918 | | async function loadClearJobsHistory() { |
| | | 3919 | | const tbody = document.getElementById('bitrixClearJobsHistoryBody'); |
| | | 3920 | | if (!tbody) return; |
| | | 3921 | | try { |
| | | 3922 | | const response = await fetch(`${clearJobsListUrl}?page=1&pageSize=20`); |
| | | 3923 | | const data = await response.json(); |
| | | 3924 | | if (!data.success || !Array.isArray(data.items)) { |
| | | 3925 | | tbody.innerHTML = St2EmptyState.tableRow(8, { title: 'Нет данных', description: 'Для выбранных параметро |
| | | 3926 | | return; |
| | | 3927 | | } |
| | | 3928 | | if (data.items.length === 0) { |
| | | 3929 | | tbody.innerHTML = St2EmptyState.tableRow(8, { title: 'Заданий пока нет', description: 'Запустите операци |
| | | 3930 | | return; |
| | | 3931 | | } |
| | | 3932 | | tbody.innerHTML = data.items.map(e => { |
| | | 3933 | | const started = e.startedAt ? new Date(e.startedAt).toLocaleString('ru-RU') : '—'; |
| | | 3934 | | const statusClass = e.status === 'Success' ? 'text-success' : e.status === 'Failed' ? 'text-danger' : 't |
| | | 3935 | | return `<tr> |
| | | 3936 | | <td>${e.id}</td> |
| | | 3937 | | <td>${started}</td> |
| | | 3938 | | <td>${escapeHtml(e.triggeredByUserName || '—')}</td> |
| | | 3939 | | <td>${e.totalItems}</td> |
| | | 3940 | | <td>${e.successCount}</td> |
| | | 3941 | | <td>${e.failedCount}</td> |
| | | 3942 | | <td class="${statusClass}">${escapeHtml(e.status)}</td> |
| | | 3943 | | <td><button type="button" class="btn btn-sm btn-outline-primary bitrix-clear-report-btn" data-execut |
| | | 3944 | | </tr>`; |
| | | 3945 | | }).join(''); |
| | | 3946 | | } catch (err) { |
| | | 3947 | | tbody.innerHTML = `<tr><td colspan="8" class="text-danger">${escapeHtml(err.message)}</td></tr>`; |
| | | 3948 | | } |
| | | 3949 | | } |
| | | 3950 | | |
| | | 3951 | | async function openClearJobReportModal(executionId) { |
| | | 3952 | | const url = `${clearJobsListUrl}/${executionId}`; |
| | | 3953 | | const response = await fetch(url); |
| | | 3954 | | const data = await response.json(); |
| | | 3955 | | if (!data.success) { |
| | | 3956 | | showNotification(data.error || 'Не удалось загрузить отчёт', 'danger'); |
| | | 3957 | | return; |
| | | 3958 | | } |
| | | 3959 | | const ex = data.execution; |
| | | 3960 | | document.getElementById('bitrixClearReportModalId').textContent = ex.id; |
| | | 3961 | | const errNote = ex.errorMessage ? `, примечание: ${ex.errorMessage}` : ''; |
| | | 3962 | | document.getElementById('bitrixClearReportModalSummary').textContent = |
| | | 3963 | | `Статус: ${ex.status}, всего: ${ex.totalItems}, OK: ${ex.successCount}, ошибок: ${ex.failedCount}${errNote}` |
| | | 3964 | | const tbody = document.getElementById('bitrixClearReportModalBody'); |
| | | 3965 | | tbody.innerHTML = (ex.items || []).map(item => ` |
| | | 3966 | | <tr class="${item.status === 'Success' ? 'table-success' : item.status === 'Failed' ? 'table-danger' : ''}"> |
| | | 3967 | | <td>${escapeHtml(item.article)}</td> |
| | | 3968 | | <td>${escapeHtml(item.offerId)}</td> |
| | | 3969 | | <td>${escapeHtml(item.ozonClient)}</td> |
| | | 3970 | | <td>${escapeHtml(item.warehouseName || item.warehouseId)}</td> |
| | | 3971 | | <td>${item.previousStock}</td> |
| | | 3972 | | <td>${escapeHtml(item.status)}</td> |
| | | 3973 | | <td class="small">${escapeHtml(item.errorMessage || '')}</td> |
| | | 3974 | | </tr> |
| | | 3975 | | `).join(''); |
| | | 3976 | | bootstrap.Modal.getOrCreateInstance(document.getElementById('bitrixClearReportModal')).show(); |
| | | 3977 | | } |
| | | 3978 | | |
| | | 3979 | | function appendBitrixArchiveLog(message) { |
| | | 3980 | | const consoleEl = document.getElementById('bitrixArchiveLogConsole'); |
| | | 3981 | | if (!consoleEl) return; |
| | | 3982 | | const line = document.createElement('div'); |
| | | 3983 | | line.className = 'log-line'; |
| | | 3984 | | const ts = new Date().toLocaleTimeString('ru-RU'); |
| | | 3985 | | line.textContent = `[${ts}] ${message}`; |
| | | 3986 | | consoleEl.appendChild(line); |
| | | 3987 | | consoleEl.scrollTop = consoleEl.scrollHeight; |
| | | 3988 | | } |
| | | 3989 | | |
| | | 3990 | | function setBitrixArchiveProgress(current, total, text) { |
| | | 3991 | | const bar = document.getElementById('bitrixArchiveProgressBar'); |
| | | 3992 | | const txt = document.getElementById('bitrixArchiveProgressText'); |
| | | 3993 | | if (!bar || !txt) return; |
| | | 3994 | | const pct = total > 0 ? Math.round((current / total) * 100) : 0; |
| | | 3995 | | bar.style.width = pct + '%'; |
| | | 3996 | | bar.textContent = pct + '%'; |
| | | 3997 | | txt.textContent = text || (total > 0 ? `Обработано ${current} из ${total}` : 'Выполнение…'); |
| | | 3998 | | } |
| | | 3999 | | |
| | | 4000 | | function resetBitrixArchiveProgressModal() { |
| | | 4001 | | const log = document.getElementById('bitrixArchiveLogConsole'); |
| | | 4002 | | const bar = document.getElementById('bitrixArchiveProgressBar'); |
| | | 4003 | | const summary = document.getElementById('bitrixArchiveResultSummary'); |
| | | 4004 | | const reportWrap = document.getElementById('bitrixArchiveReportTableWrap'); |
| | | 4005 | | const closeBtn = document.getElementById('bitrixArchiveProgressCloseBtn'); |
| | | 4006 | | const closeFooter = document.getElementById('bitrixArchiveProgressFooterClose'); |
| | | 4007 | | if (log) log.innerHTML = ''; |
| | | 4008 | | if (bar) { bar.style.width = '0%'; bar.textContent = '0%'; bar.classList.add('progress-bar-animated'); } |
| | | 4009 | | if (summary) { summary.classList.add('d-none'); summary.innerHTML = ''; summary.classList.remove('ok'); } |
| | | 4010 | | if (reportWrap) reportWrap.classList.add('d-none'); |
| | | 4011 | | if (closeBtn) closeBtn.style.display = 'none'; |
| | | 4012 | | if (closeFooter) closeFooter.style.display = 'none'; |
| | | 4013 | | if (bitrixArchiveElapsedTimer) clearInterval(bitrixArchiveElapsedTimer); |
| | | 4014 | | const el = document.getElementById('bitrixArchiveElapsed'); |
| | | 4015 | | if (el) el.textContent = '00:00'; |
| | | 4016 | | } |
| | | 4017 | | |
| | | 4018 | | function startBitrixArchiveElapsedTimer() { |
| | | 4019 | | bitrixArchiveStartedAt = Date.now(); |
| | | 4020 | | if (bitrixArchiveElapsedTimer) clearInterval(bitrixArchiveElapsedTimer); |
| | | 4021 | | bitrixArchiveElapsedTimer = setInterval(() => { |
| | | 4022 | | const s = Math.max(0, Math.floor((Date.now() - bitrixArchiveStartedAt) / 1000)); |
| | | 4023 | | const el = document.getElementById('bitrixArchiveElapsed'); |
| | | 4024 | | if (el) el.textContent = String(Math.floor(s / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0') |
| | | 4025 | | }, 1000); |
| | | 4026 | | } |
| | | 4027 | | |
| | | 4028 | | function finishBitrixArchiveModal() { |
| | | 4029 | | document.getElementById('bitrixArchiveProgressCloseBtn').style.display = ''; |
| | | 4030 | | document.getElementById('bitrixArchiveProgressFooterClose').style.display = ''; |
| | | 4031 | | if (bitrixArchiveElapsedTimer) clearInterval(bitrixArchiveElapsedTimer); |
| | | 4032 | | const bar = document.getElementById('bitrixArchiveProgressBar'); |
| | | 4033 | | if (bar) bar.classList.remove('progress-bar-animated'); |
| | | 4034 | | } |
| | | 4035 | | |
| | | 4036 | | async function refreshArchiveCellsAfterJob(offerIds) { |
| | | 4037 | | const ids = [...new Set((offerIds || []).filter(Boolean))]; |
| | | 4038 | | if (!ids.length) { |
| | | 4039 | | return; |
| | | 4040 | | } |
| | | 4041 | | for (const offerId of ids) { |
| | | 4042 | | const cell = document.querySelector(`.ozon-archive-cell[data-offer-id="${CSS.escape(offerId)}"]`); |
| | | 4043 | | if (cell) { |
| | | 4044 | | cell.innerHTML = '<span class="text-muted ozon-archive-placeholder small">…</span>'; |
| | | 4045 | | } |
| | | 4046 | | } |
| | | 4047 | | await loadOzonArchiveForGrid(); |
| | | 4048 | | } |
| | | 4049 | | |
| | | 4050 | | async function connectBitrixArchiveSignalR(jobId, options = {}) { |
| | | 4051 | | const silent = options.silent === true; |
| | | 4052 | | if (typeof signalR === 'undefined') { |
| | | 4053 | | if (!silent) { |
| | | 4054 | | appendBitrixArchiveLog('SignalR не загружен'); |
| | | 4055 | | } |
| | | 4056 | | return; |
| | | 4057 | | } |
| | | 4058 | | if (bitrixArchiveJobConnection) { |
| | | 4059 | | try { await bitrixArchiveJobConnection.stop(); } catch (_) { } |
| | | 4060 | | } |
| | | 4061 | | bitrixArchiveJobConnection = new signalR.HubConnectionBuilder() |
| | | 4062 | | .withUrl('/jobProgressHub') |
| | | 4063 | | .withAutomaticReconnect() |
| | | 4064 | | .build(); |
| | | 4065 | | |
| | | 4066 | | bitrixArchiveJobConnection.on('Log', msg => { |
| | | 4067 | | if (!silent) { |
| | | 4068 | | appendBitrixArchiveLog(msg); |
| | | 4069 | | } |
| | | 4070 | | }); |
| | | 4071 | | bitrixArchiveJobConnection.on('Progress', (current, total, text) => { |
| | | 4072 | | if (!silent) { |
| | | 4073 | | setBitrixArchiveProgress(current, total, text); |
| | | 4074 | | } |
| | | 4075 | | }); |
| | | 4076 | | bitrixArchiveJobConnection.on('Complete', async (success, report) => { |
| | | 4077 | | const offerIds = [...new Set(pendingArchiveItems.map(i => i.offerId).filter(Boolean))]; |
| | | 4078 | | document.querySelectorAll('.ozon-archive-pick.selected-archive-item').forEach(pick => { |
| | | 4079 | | pick.classList.remove('selected-archive-item'); |
| | | 4080 | | }); |
| | | 4081 | | selectedArchiveItems.clear(); |
| | | 4082 | | updateSelectedUI(); |
| | | 4083 | | |
| | | 4084 | | if (silent) { |
| | | 4085 | | hideLoadingSpinner(); |
| | | 4086 | | showNotification( |
| | | 4087 | | success ? 'Товар отправлен в архив Ozon' : (report || 'Ошибка архивации'), |
| | | 4088 | | success ? 'success' : 'danger'); |
| | | 4089 | | } else { |
| | | 4090 | | const summary = document.getElementById('bitrixArchiveResultSummary'); |
| | | 4091 | | if (summary) { |
| | | 4092 | | summary.innerHTML = report || ''; |
| | | 4093 | | summary.classList.remove('d-none'); |
| | | 4094 | | if (success) summary.classList.add('ok'); |
| | | 4095 | | } |
| | | 4096 | | finishBitrixArchiveModal(); |
| | | 4097 | | if (currentArchiveExecutionId) { |
| | | 4098 | | await loadArchiveJobReportTable(currentArchiveExecutionId); |
| | | 4099 | | } |
| | | 4100 | | } |
| | | 4101 | | |
| | | 4102 | | if (offerIds.length) { |
| | | 4103 | | await refreshArchiveCellsAfterJob(offerIds); |
| | | 4104 | | } |
| | | 4105 | | }); |
| | | 4106 | | bitrixArchiveJobConnection.on('Error', msg => { |
| | | 4107 | | if (silent) { |
| | | 4108 | | hideLoadingSpinner(); |
| | | 4109 | | showNotification(msg || 'Ошибка архивации', 'danger'); |
| | | 4110 | | } else { |
| | | 4111 | | appendBitrixArchiveLog('ОШИБКА: ' + msg); |
| | | 4112 | | finishBitrixArchiveModal(); |
| | | 4113 | | } |
| | | 4114 | | }); |
| | | 4115 | | |
| | | 4116 | | await bitrixArchiveJobConnection.start(); |
| | | 4117 | | await bitrixArchiveJobConnection.invoke('JoinJobGroup', jobId); |
| | | 4118 | | if (!silent) { |
| | | 4119 | | appendBitrixArchiveLog('Подключено к логам задания'); |
| | | 4120 | | } |
| | | 4121 | | } |
| | | 4122 | | |
| | | 4123 | | async function startBitrixArchiveJob(options = {}) { |
| | | 4124 | | if (!pendingArchiveItems.length) return; |
| | | 4125 | | const silent = options.silent === true || pendingArchiveItems.length === 1; |
| | | 4126 | | const jobId = newArchiveJobId(); |
| | | 4127 | | const progressModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('bitrixArchiveProgressModal')) |
| | | 4128 | | bootstrap.Modal.getInstance(document.getElementById('bitrixArchiveConfirmModal'))?.hide(); |
| | | 4129 | | |
| | | 4130 | | if (!silent) { |
| | | 4131 | | resetBitrixArchiveProgressModal(); |
| | | 4132 | | progressModal.show(); |
| | | 4133 | | startBitrixArchiveElapsedTimer(); |
| | | 4134 | | appendBitrixArchiveLog('Отправка задания на сервер…'); |
| | | 4135 | | } else { |
| | | 4136 | | showLoadingSpinner('Архивация в Ozon…'); |
| | | 4137 | | } |
| | | 4138 | | |
| | | 4139 | | try { |
| | | 4140 | | const response = await fetch(archiveJobStartUrl, { |
| | | 4141 | | method: 'POST', |
| | | 4142 | | headers: { 'Content-Type': 'application/json' }, |
| | | 4143 | | body: JSON.stringify({ jobId: jobId, items: pendingArchiveItems }) |
| | | 4144 | | }); |
| | | 4145 | | const data = await response.json().catch(() => ({})); |
| | | 4146 | | if (!response.ok) { |
| | | 4147 | | throw new Error(data.error || 'Не удалось запустить задание'); |
| | | 4148 | | } |
| | | 4149 | | currentArchiveExecutionId = data.executionId; |
| | | 4150 | | if (!silent) { |
| | | 4151 | | appendBitrixArchiveLog(`Задание #${data.executionId}, jobId=${jobId}`); |
| | | 4152 | | } |
| | | 4153 | | await connectBitrixArchiveSignalR(jobId, { silent }); |
| | | 4154 | | } catch (err) { |
| | | 4155 | | if (silent) { |
| | | 4156 | | hideLoadingSpinner(); |
| | | 4157 | | } else { |
| | | 4158 | | appendBitrixArchiveLog('ОШИБКА: ' + (err.message || err)); |
| | | 4159 | | finishBitrixArchiveModal(); |
| | | 4160 | | } |
| | | 4161 | | showNotification(err.message || 'Ошибка запуска', 'danger'); |
| | | 4162 | | } |
| | | 4163 | | } |
| | | 4164 | | |
| | | 4165 | | async function loadArchiveJobReportTable(executionId) { |
| | | 4166 | | try { |
| | | 4167 | | const response = await fetch(`${archiveJobsListUrl}/${executionId}`); |
| | | 4168 | | const data = await response.json(); |
| | | 4169 | | if (!data.success || !data.execution?.items) return; |
| | | 4170 | | const wrap = document.getElementById('bitrixArchiveReportTableWrap'); |
| | | 4171 | | if (!wrap) return; |
| | | 4172 | | wrap.classList.remove('d-none'); |
| | | 4173 | | mountJobReportGrid('bitrixArchiveReportGrid', data.execution.items, 'archive'); |
| | | 4174 | | } catch (e) { |
| | | 4175 | | console.warn('Отчёт архивации не загружен', e); |
| | | 4176 | | } |
| | | 4177 | | } |
| | | 4178 | | |
| | | 4179 | | function appendBitrixUnarchiveLog(message) { |
| | | 4180 | | const consoleEl = document.getElementById('bitrixUnarchiveLogConsole'); |
| | | 4181 | | if (!consoleEl) return; |
| | | 4182 | | const line = document.createElement('div'); |
| | | 4183 | | line.className = 'log-line'; |
| | | 4184 | | const ts = new Date().toLocaleTimeString('ru-RU'); |
| | | 4185 | | line.textContent = `[${ts}] ${message}`; |
| | | 4186 | | consoleEl.appendChild(line); |
| | | 4187 | | consoleEl.scrollTop = consoleEl.scrollHeight; |
| | | 4188 | | } |
| | | 4189 | | |
| | | 4190 | | function setBitrixUnarchiveProgress(current, total, text) { |
| | | 4191 | | const bar = document.getElementById('bitrixUnarchiveProgressBar'); |
| | | 4192 | | const txt = document.getElementById('bitrixUnarchiveProgressText'); |
| | | 4193 | | if (!bar || !txt) return; |
| | | 4194 | | const pct = total > 0 ? Math.round((current / total) * 100) : 0; |
| | | 4195 | | bar.style.width = pct + '%'; |
| | | 4196 | | bar.textContent = pct + '%'; |
| | | 4197 | | txt.textContent = text || (total > 0 ? `Обработано ${current} из ${total}` : 'Выполнение…'); |
| | | 4198 | | } |
| | | 4199 | | |
| | | 4200 | | function resetBitrixUnarchiveProgressModal() { |
| | | 4201 | | const log = document.getElementById('bitrixUnarchiveLogConsole'); |
| | | 4202 | | const bar = document.getElementById('bitrixUnarchiveProgressBar'); |
| | | 4203 | | const summary = document.getElementById('bitrixUnarchiveResultSummary'); |
| | | 4204 | | const reportWrap = document.getElementById('bitrixUnarchiveReportTableWrap'); |
| | | 4205 | | const closeBtn = document.getElementById('bitrixUnarchiveProgressCloseBtn'); |
| | | 4206 | | const closeFooter = document.getElementById('bitrixUnarchiveProgressFooterClose'); |
| | | 4207 | | if (log) log.innerHTML = ''; |
| | | 4208 | | if (bar) { bar.style.width = '0%'; bar.textContent = '0%'; bar.classList.add('progress-bar-animated'); } |
| | | 4209 | | if (summary) { summary.classList.add('d-none'); summary.innerHTML = ''; summary.classList.remove('ok'); } |
| | | 4210 | | if (reportWrap) reportWrap.classList.add('d-none'); |
| | | 4211 | | if (closeBtn) closeBtn.style.display = 'none'; |
| | | 4212 | | if (closeFooter) closeFooter.style.display = 'none'; |
| | | 4213 | | if (bitrixUnarchiveElapsedTimer) clearInterval(bitrixUnarchiveElapsedTimer); |
| | | 4214 | | const el = document.getElementById('bitrixUnarchiveElapsed'); |
| | | 4215 | | if (el) el.textContent = '00:00'; |
| | | 4216 | | } |
| | | 4217 | | |
| | | 4218 | | function startBitrixUnarchiveElapsedTimer() { |
| | | 4219 | | bitrixUnarchiveStartedAt = Date.now(); |
| | | 4220 | | if (bitrixUnarchiveElapsedTimer) clearInterval(bitrixUnarchiveElapsedTimer); |
| | | 4221 | | bitrixUnarchiveElapsedTimer = setInterval(() => { |
| | | 4222 | | const s = Math.max(0, Math.floor((Date.now() - bitrixUnarchiveStartedAt) / 1000)); |
| | | 4223 | | const el = document.getElementById('bitrixUnarchiveElapsed'); |
| | | 4224 | | if (el) el.textContent = String(Math.floor(s / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0') |
| | | 4225 | | }, 1000); |
| | | 4226 | | } |
| | | 4227 | | |
| | | 4228 | | function finishBitrixUnarchiveModal() { |
| | | 4229 | | document.getElementById('bitrixUnarchiveProgressCloseBtn').style.display = ''; |
| | | 4230 | | document.getElementById('bitrixUnarchiveProgressFooterClose').style.display = ''; |
| | | 4231 | | if (bitrixUnarchiveElapsedTimer) clearInterval(bitrixUnarchiveElapsedTimer); |
| | | 4232 | | const bar = document.getElementById('bitrixUnarchiveProgressBar'); |
| | | 4233 | | if (bar) bar.classList.remove('progress-bar-animated'); |
| | | 4234 | | } |
| | | 4235 | | |
| | | 4236 | | async function connectBitrixUnarchiveSignalR(jobId, options = {}) { |
| | | 4237 | | const silent = options.silent === true; |
| | | 4238 | | if (typeof signalR === 'undefined') { |
| | | 4239 | | if (!silent) appendBitrixUnarchiveLog('SignalR не загружен'); |
| | | 4240 | | return; |
| | | 4241 | | } |
| | | 4242 | | if (bitrixUnarchiveJobConnection) { |
| | | 4243 | | try { await bitrixUnarchiveJobConnection.stop(); } catch (_) { } |
| | | 4244 | | } |
| | | 4245 | | bitrixUnarchiveJobConnection = new signalR.HubConnectionBuilder() |
| | | 4246 | | .withUrl('/jobProgressHub') |
| | | 4247 | | .withAutomaticReconnect() |
| | | 4248 | | .build(); |
| | | 4249 | | |
| | | 4250 | | bitrixUnarchiveJobConnection.on('Log', msg => { if (!silent) appendBitrixUnarchiveLog(msg); }); |
| | | 4251 | | bitrixUnarchiveJobConnection.on('Progress', (c, t, text) => { if (!silent) setBitrixUnarchiveProgress(c, t, text |
| | | 4252 | | bitrixUnarchiveJobConnection.on('Complete', async (success, report) => { |
| | | 4253 | | const offerIds = [...new Set(pendingUnarchiveItems.map(i => i.offerId).filter(Boolean))]; |
| | | 4254 | | document.querySelectorAll('.ozon-unarchive-pick.selected-unarchive-item').forEach(p => p.classList.remove('s |
| | | 4255 | | selectedUnarchiveItems.clear(); |
| | | 4256 | | updateSelectedUI(); |
| | | 4257 | | |
| | | 4258 | | if (silent) { |
| | | 4259 | | hideLoadingSpinner(); |
| | | 4260 | | showNotification( |
| | | 4261 | | success ? 'Товар восстановлен из архива Ozon' : (report || 'Ошибка разархивации'), |
| | | 4262 | | success ? 'success' : 'danger'); |
| | | 4263 | | } else { |
| | | 4264 | | const summary = document.getElementById('bitrixUnarchiveResultSummary'); |
| | | 4265 | | if (summary) { |
| | | 4266 | | summary.innerHTML = report || ''; |
| | | 4267 | | summary.classList.remove('d-none'); |
| | | 4268 | | if (success) summary.classList.add('ok'); |
| | | 4269 | | } |
| | | 4270 | | finishBitrixUnarchiveModal(); |
| | | 4271 | | if (currentUnarchiveExecutionId) await loadUnarchiveJobReportTable(currentUnarchiveExecutionId); |
| | | 4272 | | } |
| | | 4273 | | if (offerIds.length) await refreshArchiveCellsAfterJob(offerIds); |
| | | 4274 | | }); |
| | | 4275 | | bitrixUnarchiveJobConnection.on('Error', msg => { |
| | | 4276 | | if (silent) { |
| | | 4277 | | hideLoadingSpinner(); |
| | | 4278 | | showNotification(msg || 'Ошибка разархивации', 'danger'); |
| | | 4279 | | } else { |
| | | 4280 | | appendBitrixUnarchiveLog('ОШИБКА: ' + msg); |
| | | 4281 | | finishBitrixUnarchiveModal(); |
| | | 4282 | | } |
| | | 4283 | | }); |
| | | 4284 | | |
| | | 4285 | | await bitrixUnarchiveJobConnection.start(); |
| | | 4286 | | await bitrixUnarchiveJobConnection.invoke('JoinJobGroup', jobId); |
| | | 4287 | | if (!silent) appendBitrixUnarchiveLog('Подключено к логам задания'); |
| | | 4288 | | } |
| | | 4289 | | |
| | | 4290 | | async function startBitrixUnarchiveJob(options = {}) { |
| | | 4291 | | if (!pendingUnarchiveItems.length) return; |
| | | 4292 | | const silent = options.silent === true || pendingUnarchiveItems.length === 1; |
| | | 4293 | | const jobId = newUnarchiveJobId(); |
| | | 4294 | | const progressModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('bitrixUnarchiveProgressModal' |
| | | 4295 | | bootstrap.Modal.getInstance(document.getElementById('bitrixUnarchiveConfirmModal'))?.hide(); |
| | | 4296 | | |
| | | 4297 | | if (!silent) { |
| | | 4298 | | resetBitrixUnarchiveProgressModal(); |
| | | 4299 | | progressModal.show(); |
| | | 4300 | | startBitrixUnarchiveElapsedTimer(); |
| | | 4301 | | appendBitrixUnarchiveLog('Отправка задания на сервер…'); |
| | | 4302 | | } else { |
| | | 4303 | | showLoadingSpinner('Восстановление из архива Ozon…'); |
| | | 4304 | | } |
| | | 4305 | | |
| | | 4306 | | try { |
| | | 4307 | | const response = await fetch(unarchiveJobStartUrl, { |
| | | 4308 | | method: 'POST', |
| | | 4309 | | headers: { 'Content-Type': 'application/json' }, |
| | | 4310 | | body: JSON.stringify({ jobId: jobId, items: pendingUnarchiveItems }) |
| | | 4311 | | }); |
| | | 4312 | | const data = await response.json().catch(() => ({})); |
| | | 4313 | | if (!response.ok) throw new Error(data.error || 'Не удалось запустить задание'); |
| | | 4314 | | currentUnarchiveExecutionId = data.executionId; |
| | | 4315 | | if (!silent) appendBitrixUnarchiveLog(`Задание #${data.executionId}, jobId=${jobId}`); |
| | | 4316 | | await connectBitrixUnarchiveSignalR(jobId, { silent }); |
| | | 4317 | | } catch (err) { |
| | | 4318 | | if (silent) hideLoadingSpinner(); |
| | | 4319 | | else { |
| | | 4320 | | appendBitrixUnarchiveLog('ОШИБКА: ' + (err.message || err)); |
| | | 4321 | | finishBitrixUnarchiveModal(); |
| | | 4322 | | } |
| | | 4323 | | showNotification(err.message || 'Ошибка запуска', 'danger'); |
| | | 4324 | | } |
| | | 4325 | | } |
| | | 4326 | | |
| | | 4327 | | async function loadUnarchiveJobReportTable(executionId) { |
| | | 4328 | | try { |
| | | 4329 | | const response = await fetch(`${unarchiveJobsListUrl}/${executionId}`); |
| | | 4330 | | const data = await response.json(); |
| | | 4331 | | if (!data.success || !data.execution?.items) return; |
| | | 4332 | | const wrap = document.getElementById('bitrixUnarchiveReportTableWrap'); |
| | | 4333 | | if (!wrap) return; |
| | | 4334 | | wrap.classList.remove('d-none'); |
| | | 4335 | | mountJobReportGrid('bitrixUnarchiveReportGrid', data.execution.items, 'unarchive'); |
| | | 4336 | | } catch (e) { |
| | | 4337 | | console.warn('Отчёт разархивации не загружен', e); |
| | | 4338 | | } |
| | | 4339 | | } |
| | | 4340 | | |
| | | 4341 | | document.getElementById('bitrixClearConfirmStartBtn')?.addEventListener('click', () => startBitrixClearJob()); |
| | | 4342 | | document.getElementById('bitrixArchiveConfirmStartBtn')?.addEventListener('click', () => startBitrixArchiveJob()); |
| | | 4343 | | document.getElementById('bitrixUnarchiveConfirmStartBtn')?.addEventListener('click', () => startBitrixUnarchiveJob() |
| | | 4344 | | document.getElementById('bitrixClearJobsRefreshBtn')?.addEventListener('click', () => loadClearJobsHistory()); |
| | | 4345 | | document.getElementById('bitrixClearJobsHistoryBody')?.addEventListener('click', e => { |
| | | 4346 | | const btn = e.target.closest('.bitrix-clear-report-btn'); |
| | | 4347 | | if (btn) { |
| | | 4348 | | openClearJobReportModal(Number(btn.getAttribute('data-execution-id'))); |
| | | 4349 | | } |
| | | 4350 | | }); |
| | | 4351 | | document.addEventListener('DOMContentLoaded', () => { |
| | | 4352 | | if (document.getElementById('bitrixClearJobsHistoryBody')) { |
| | | 4353 | | loadClearJobsHistory(); |
| | | 4354 | | } |
| | | 4355 | | if (document.getElementById('bitrixPushJobsHistoryBody')) { |
| | | 4356 | | loadPushJobsHistory(); |
| | | 4357 | | } |
| | | 4358 | | const deepLinkExecId = Number(new URLSearchParams(window.location.search).get('executionId')); |
| | | 4359 | | if (deepLinkExecId > 0) { |
| | | 4360 | | if (document.getElementById('bitrixClearJobsHistoryBody')) { |
| | | 4361 | | void openClearJobReportModal(deepLinkExecId); |
| | | 4362 | | } else if (document.getElementById('bitrixPushJobsHistoryBody')) { |
| | | 4363 | | void openPushJobReportModal(deepLinkExecId); |
| | | 4364 | | } |
| | | 4365 | | } |
| | | 4366 | | }); |
| | | 4367 | | |
| | | 4368 | | function newPushJobId() { |
| | | 4369 | | if (window.crypto && typeof window.crypto.randomUUID === 'function') { |
| | | 4370 | | return window.crypto.randomUUID(); |
| | | 4371 | | } |
| | | 4372 | | return 'push-' + Date.now() + '-' + Math.random().toString(16).slice(2); |
| | | 4373 | | } |
| | | 4374 | | |
| | | 4375 | | function collectSelectedRefreshPayload() { |
| | | 4376 | | const selectedButtons = document.querySelectorAll('.refreshOzonMain.selected-refresh-item'); |
| | | 4377 | | return Array.from(selectedButtons).map(btn => ({ |
| | | 4378 | | article: btn.getAttribute('data-article') || '', |
| | | 4379 | | offerId: btn.getAttribute('data-offer-id') || '', |
| | | 4380 | | productId: Number(btn.getAttribute('data-product-id')), |
| | | 4381 | | warehouseId: Number(btn.getAttribute('data-warehouse-id')), |
| | | 4382 | | warehouseName: btn.getAttribute('data-warehouse-name') || '', |
| | | 4383 | | ozonClient: btn.getAttribute('data-client') || '', |
| | | 4384 | | linkedStoreName: btn.getAttribute('data-linked-store') || '', |
| | | 4385 | | previousStock: Number(btn.getAttribute('data-previous-stock')) || 0, |
| | | 4386 | | targetStock: Number(btn.getAttribute('data-stock')) || 0 |
| | | 4387 | | })); |
| | | 4388 | | } |
| | | 4389 | | |
| | | 4390 | | function openPushConfirmModal(items) { |
| | | 4391 | | pendingPushItems = items; |
| | | 4392 | | confirmTables.push.setRows(items); |
| | | 4393 | | bootstrap.Modal.getOrCreateInstance(document.getElementById('bitrixPushConfirmModal')).show(); |
| | | 4394 | | } |
| | | 4395 | | |
| | | 4396 | | let bitrixPushJobConnection = null; |
| | | 4397 | | let bitrixPushElapsedTimer = null; |
| | | 4398 | | let bitrixPushStartedAt = 0; |
| | | 4399 | | let currentPushExecutionId = null; |
| | | 4400 | | |
| | | 4401 | | function appendBitrixPushLog(message) { |
| | | 4402 | | const consoleEl = document.getElementById('bitrixPushLogConsole'); |
| | | 4403 | | if (!consoleEl) return; |
| | | 4404 | | const line = document.createElement('div'); |
| | | 4405 | | line.className = 'log-line'; |
| | | 4406 | | const ts = new Date().toLocaleTimeString('ru-RU'); |
| | | 4407 | | line.textContent = `[${ts}] ${message}`; |
| | | 4408 | | consoleEl.appendChild(line); |
| | | 4409 | | consoleEl.scrollTop = consoleEl.scrollHeight; |
| | | 4410 | | } |
| | | 4411 | | |
| | | 4412 | | function setBitrixPushProgress(current, total, text) { |
| | | 4413 | | const bar = document.getElementById('bitrixPushProgressBar'); |
| | | 4414 | | const txt = document.getElementById('bitrixPushProgressText'); |
| | | 4415 | | if (!bar || !txt) return; |
| | | 4416 | | const pct = total > 0 ? Math.round((current / total) * 100) : 0; |
| | | 4417 | | bar.style.width = pct + '%'; |
| | | 4418 | | bar.textContent = pct + '%'; |
| | | 4419 | | txt.textContent = text || (total > 0 ? `Обработано ${current} из ${total}` : 'Выполнение…'); |
| | | 4420 | | } |
| | | 4421 | | |
| | | 4422 | | function resetBitrixPushProgressModal() { |
| | | 4423 | | const log = document.getElementById('bitrixPushLogConsole'); |
| | | 4424 | | const bar = document.getElementById('bitrixPushProgressBar'); |
| | | 4425 | | const summary = document.getElementById('bitrixPushResultSummary'); |
| | | 4426 | | const reportWrap = document.getElementById('bitrixPushReportTableWrap'); |
| | | 4427 | | const closeBtn = document.getElementById('bitrixPushProgressCloseBtn'); |
| | | 4428 | | const closeFooter = document.getElementById('bitrixPushProgressFooterClose'); |
| | | 4429 | | if (log) log.innerHTML = ''; |
| | | 4430 | | if (bar) { bar.style.width = '0%'; bar.textContent = '0%'; bar.classList.add('progress-bar-animated'); } |
| | | 4431 | | if (summary) { summary.classList.add('d-none'); summary.innerHTML = ''; summary.classList.remove('ok'); } |
| | | 4432 | | if (reportWrap) reportWrap.classList.add('d-none'); |
| | | 4433 | | if (closeBtn) closeBtn.style.display = 'none'; |
| | | 4434 | | if (closeFooter) closeFooter.style.display = 'none'; |
| | | 4435 | | if (bitrixPushElapsedTimer) clearInterval(bitrixPushElapsedTimer); |
| | | 4436 | | const el = document.getElementById('bitrixPushElapsed'); |
| | | 4437 | | if (el) el.textContent = '00:00'; |
| | | 4438 | | } |
| | | 4439 | | |
| | | 4440 | | function startBitrixPushElapsedTimer() { |
| | | 4441 | | bitrixPushStartedAt = Date.now(); |
| | | 4442 | | if (bitrixPushElapsedTimer) clearInterval(bitrixPushElapsedTimer); |
| | | 4443 | | bitrixPushElapsedTimer = setInterval(() => { |
| | | 4444 | | const s = Math.max(0, Math.floor((Date.now() - bitrixPushStartedAt) / 1000)); |
| | | 4445 | | const el = document.getElementById('bitrixPushElapsed'); |
| | | 4446 | | if (el) el.textContent = String(Math.floor(s / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0') |
| | | 4447 | | }, 1000); |
| | | 4448 | | } |
| | | 4449 | | |
| | | 4450 | | function finishBitrixPushModal(success) { |
| | | 4451 | | document.getElementById('bitrixPushProgressCloseBtn').style.display = ''; |
| | | 4452 | | document.getElementById('bitrixPushProgressFooterClose').style.display = ''; |
| | | 4453 | | const historyLink = document.getElementById('bitrixPushProgressHistoryLink'); |
| | | 4454 | | if (historyLink) historyLink.style.display = ''; |
| | | 4455 | | if (bitrixPushElapsedTimer) clearInterval(bitrixPushElapsedTimer); |
| | | 4456 | | const bar = document.getElementById('bitrixPushProgressBar'); |
| | | 4457 | | if (bar) bar.classList.remove('progress-bar-animated'); |
| | | 4458 | | const summary = document.getElementById('bitrixPushResultSummary'); |
| | | 4459 | | if (summary && success === false && summary.classList.contains('d-none')) { |
| | | 4460 | | summary.innerHTML = '<strong>Задание завершилось с ошибкой.</strong> См. лог выше.'; |
| | | 4461 | | summary.classList.remove('d-none', 'ok'); |
| | | 4462 | | } |
| | | 4463 | | } |
| | | 4464 | | |
| | | 4465 | | async function connectBitrixPushSignalR(jobId) { |
| | | 4466 | | if (typeof signalR === 'undefined') { |
| | | 4467 | | appendBitrixPushLog('SignalR не загружен'); |
| | | 4468 | | return; |
| | | 4469 | | } |
| | | 4470 | | if (bitrixPushJobConnection) { |
| | | 4471 | | try { await bitrixPushJobConnection.stop(); } catch (_) { } |
| | | 4472 | | } |
| | | 4473 | | bitrixPushJobConnection = new signalR.HubConnectionBuilder() |
| | | 4474 | | .withUrl('/jobProgressHub') |
| | | 4475 | | .withAutomaticReconnect() |
| | | 4476 | | .build(); |
| | | 4477 | | |
| | | 4478 | | bitrixPushJobConnection.on('Log', msg => appendBitrixPushLog(msg)); |
| | | 4479 | | bitrixPushJobConnection.on('Progress', (current, total, text) => setBitrixPushProgress(current, total, text)); |
| | | 4480 | | bitrixPushJobConnection.on('Complete', async (success, report) => { |
| | | 4481 | | const summary = document.getElementById('bitrixPushResultSummary'); |
| | | 4482 | | if (summary) { |
| | | 4483 | | summary.innerHTML = report || ''; |
| | | 4484 | | summary.classList.remove('d-none'); |
| | | 4485 | | if (success) summary.classList.add('ok'); |
| | | 4486 | | } |
| | | 4487 | | finishBitrixPushModal(success); |
| | | 4488 | | if (currentPushExecutionId) { |
| | | 4489 | | const historyLink = document.getElementById('bitrixPushProgressHistoryLink'); |
| | | 4490 | | if (historyLink) { |
| | | 4491 | | const sep = pushHistoryTabUrl.includes('?') ? '&' : '?'; |
| | | 4492 | | historyLink.href = `${pushHistoryTabUrl}${sep}executionId=${currentPushExecutionId}`; |
| | | 4493 | | } |
| | | 4494 | | await loadPushJobReportTable(currentPushExecutionId); |
| | | 4495 | | } |
| | | 4496 | | if (document.getElementById('bitrixPushJobsHistoryBody')) { |
| | | 4497 | | loadPushJobsHistory(); |
| | | 4498 | | } |
| | | 4499 | | if (success) { |
| | | 4500 | | document.querySelectorAll('.refreshOzonMain.selected-refresh-item').forEach(btn => { |
| | | 4501 | | btn.classList.remove('selected-refresh-item'); |
| | | 4502 | | const productId = btn.getAttribute('data-product-id'); |
| | | 4503 | | const warehouseId = btn.getAttribute('data-warehouse-id'); |
| | | 4504 | | const target = btn.getAttribute('data-stock'); |
| | | 4505 | | const stockDiv = document.querySelector( |
| | | 4506 | | `.col-4.text-end[data-product-id="${productId}"][data-warehouse-id="${warehouseId}"]` |
| | | 4507 | | ); |
| | | 4508 | | if (stockDiv && target != null) { |
| | | 4509 | | stockDiv.textContent = target; |
| | | 4510 | | } |
| | | 4511 | | }); |
| | | 4512 | | selectedRefreshItems.clear(); |
| | | 4513 | | updateSelectedUI(); |
| | | 4514 | | } |
| | | 4515 | | }); |
| | | 4516 | | bitrixPushJobConnection.on('Error', msg => { |
| | | 4517 | | appendBitrixPushLog('ОШИБКА: ' + msg); |
| | | 4518 | | finishBitrixPushModal(false); |
| | | 4519 | | }); |
| | | 4520 | | |
| | | 4521 | | await bitrixPushJobConnection.start(); |
| | | 4522 | | await bitrixPushJobConnection.invoke('JoinJobGroup', jobId); |
| | | 4523 | | appendBitrixPushLog('Подключено к логам задания'); |
| | | 4524 | | } |
| | | 4525 | | |
| | | 4526 | | async function startBitrixPushJob() { |
| | | 4527 | | if (!pendingPushItems.length) return; |
| | | 4528 | | const jobId = newPushJobId(); |
| | | 4529 | | const progressModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('bitrixPushProgressModal')); |
| | | 4530 | | bootstrap.Modal.getInstance(document.getElementById('bitrixPushConfirmModal'))?.hide(); |
| | | 4531 | | resetBitrixPushProgressModal(); |
| | | 4532 | | progressModal.show(); |
| | | 4533 | | startBitrixPushElapsedTimer(); |
| | | 4534 | | appendBitrixPushLog('Отправка задания на сервер…'); |
| | | 4535 | | |
| | | 4536 | | try { |
| | | 4537 | | const response = await fetch(pushJobStartUrl, { |
| | | 4538 | | method: 'POST', |
| | | 4539 | | headers: { 'Content-Type': 'application/json' }, |
| | | 4540 | | body: JSON.stringify({ jobId: jobId, items: pendingPushItems }) |
| | | 4541 | | }); |
| | | 4542 | | const rawText = await response.text(); |
| | | 4543 | | let data = {}; |
| | | 4544 | | try { data = rawText ? JSON.parse(rawText) : {}; } catch (_) { } |
| | | 4545 | | if (!response.ok) { |
| | | 4546 | | const errMsg = data.error || data.title || rawText?.slice(0, 500) || `HTTP ${response.status}`; |
| | | 4547 | | throw new Error(errMsg); |
| | | 4548 | | } |
| | | 4549 | | currentPushExecutionId = data.executionId; |
| | | 4550 | | appendBitrixPushLog(`Задание #${data.executionId}, jobId=${jobId}`); |
| | | 4551 | | await connectBitrixPushSignalR(jobId); |
| | | 4552 | | } catch (err) { |
| | | 4553 | | appendBitrixPushLog('ОШИБКА: ' + (err.message || err)); |
| | | 4554 | | finishBitrixPushModal(false); |
| | | 4555 | | showNotification(err.message || 'Ошибка запуска', 'danger'); |
| | | 4556 | | } |
| | | 4557 | | } |
| | | 4558 | | |
| | | 4559 | | async function loadPushJobReportTable(executionId) { |
| | | 4560 | | try { |
| | | 4561 | | const response = await fetch(`${pushJobsListUrl}/${executionId}`); |
| | | 4562 | | const data = await response.json(); |
| | | 4563 | | if (!data.success || !data.execution?.items) return; |
| | | 4564 | | const wrap = document.getElementById('bitrixPushReportTableWrap'); |
| | | 4565 | | if (!wrap) return; |
| | | 4566 | | wrap.classList.remove('d-none'); |
| | | 4567 | | mountJobReportGrid('bitrixPushReportGrid', data.execution.items, 'push'); |
| | | 4568 | | } catch (e) { |
| | | 4569 | | console.warn('Отчёт наката не загружен', e); |
| | | 4570 | | } |
| | | 4571 | | } |
| | | 4572 | | |
| | | 4573 | | async function loadPushJobsHistory() { |
| | | 4574 | | const tbody = document.getElementById('bitrixPushJobsHistoryBody'); |
| | | 4575 | | if (!tbody) return; |
| | | 4576 | | try { |
| | | 4577 | | const response = await fetch(`${pushJobsListUrl}?page=1&pageSize=20`); |
| | | 4578 | | const data = await response.json(); |
| | | 4579 | | if (!data.success || !Array.isArray(data.items)) { |
| | | 4580 | | tbody.innerHTML = St2EmptyState.tableRow(8, { title: 'Нет данных', description: 'Для выбранных параметро |
| | | 4581 | | return; |
| | | 4582 | | } |
| | | 4583 | | if (data.items.length === 0) { |
| | | 4584 | | tbody.innerHTML = St2EmptyState.tableRow(8, { title: 'Заданий пока нет', description: 'Запустите операци |
| | | 4585 | | return; |
| | | 4586 | | } |
| | | 4587 | | tbody.innerHTML = data.items.map(e => { |
| | | 4588 | | const started = e.startedAt ? new Date(e.startedAt).toLocaleString('ru-RU') : '—'; |
| | | 4589 | | const statusClass = e.status === 'Success' ? 'text-success' : e.status === 'Failed' ? 'text-danger' : 't |
| | | 4590 | | return `<tr> |
| | | 4591 | | <td>${e.id}</td> |
| | | 4592 | | <td>${started}</td> |
| | | 4593 | | <td>${escapeHtml(e.triggeredByUserName || '—')}</td> |
| | | 4594 | | <td>${e.totalItems}</td> |
| | | 4595 | | <td>${e.successCount}</td> |
| | | 4596 | | <td>${e.failedCount}</td> |
| | | 4597 | | <td class="${statusClass}">${escapeHtml(e.status)}</td> |
| | | 4598 | | <td><button type="button" class="btn btn-sm btn-outline-primary bitrix-push-report-btn" data-executi |
| | | 4599 | | </tr>`; |
| | | 4600 | | }).join(''); |
| | | 4601 | | } catch (err) { |
| | | 4602 | | tbody.innerHTML = `<tr><td colspan="8" class="text-danger">${escapeHtml(err.message)}</td></tr>`; |
| | | 4603 | | } |
| | | 4604 | | } |
| | | 4605 | | |
| | | 4606 | | async function openPushJobReportModal(executionId) { |
| | | 4607 | | const response = await fetch(`${pushJobsListUrl}/${executionId}`); |
| | | 4608 | | const data = await response.json(); |
| | | 4609 | | if (!data.success) { |
| | | 4610 | | showNotification(data.error || 'Не удалось загрузить отчёт', 'danger'); |
| | | 4611 | | return; |
| | | 4612 | | } |
| | | 4613 | | const ex = data.execution; |
| | | 4614 | | document.getElementById('bitrixPushReportModalId').textContent = ex.id; |
| | | 4615 | | const pushErrNote = ex.errorMessage ? `, примечание: ${ex.errorMessage}` : ''; |
| | | 4616 | | document.getElementById('bitrixPushReportModalSummary').textContent = |
| | | 4617 | | `Статус: ${ex.status}, всего: ${ex.totalItems}, OK: ${ex.successCount}, ошибок: ${ex.failedCount}${pushErrNo |
| | | 4618 | | const tbody = document.getElementById('bitrixPushReportModalBody'); |
| | | 4619 | | tbody.innerHTML = (ex.items || []).map(item => ` |
| | | 4620 | | <tr class="${item.status === 'Success' ? 'table-success' : item.status === 'Failed' ? 'table-danger' : ''}"> |
| | | 4621 | | <td>${escapeHtml(item.article)}</td> |
| | | 4622 | | <td>${escapeHtml(item.offerId)}</td> |
| | | 4623 | | <td>${escapeHtml(item.ozonClient)}</td> |
| | | 4624 | | <td>${escapeHtml(item.warehouseName || item.warehouseId)}</td> |
| | | 4625 | | <td>${escapeHtml(item.linkedStoreName || '—')}</td> |
| | | 4626 | | <td>${item.previousStock}</td> |
| | | 4627 | | <td>${item.targetStock}</td> |
| | | 4628 | | <td>${escapeHtml(item.status)}</td> |
| | | 4629 | | <td class="small">${escapeHtml(item.errorMessage || '')}</td> |
| | | 4630 | | </tr> |
| | | 4631 | | `).join(''); |
| | | 4632 | | bootstrap.Modal.getOrCreateInstance(document.getElementById('bitrixPushReportModal')).show(); |
| | | 4633 | | } |
| | | 4634 | | |
| | | 4635 | | document.getElementById('bitrixPushConfirmStartBtn')?.addEventListener('click', () => startBitrixPushJob()); |
| | | 4636 | | document.getElementById('bitrixPushJobsRefreshBtn')?.addEventListener('click', () => loadPushJobsHistory()); |
| | | 4637 | | document.getElementById('bitrixPushJobsHistoryBody')?.addEventListener('click', e => { |
| | | 4638 | | const btn = e.target.closest('.bitrix-push-report-btn'); |
| | | 4639 | | if (btn) { |
| | | 4640 | | openPushJobReportModal(Number(btn.getAttribute('data-execution-id'))); |
| | | 4641 | | } |
| | | 4642 | | }); |
| | | 4643 | | |
| | | 4644 | | |
| | | 4645 | | // Функция показа уведомлений |
| | | 4646 | | function showNotification(message, type = 'info') { |
| | | 4647 | | // Создаем элемент уведомления |
| | | 4648 | | const notification = document.createElement('div'); |
| | | 4649 | | notification.className = `alert alert-${type === 'error' ? 'danger' : 'success'} alert-dismissible fade show pos |
| | | 4650 | | notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;'; |
| | | 4651 | | notification.innerHTML = ` |
| | | 4652 | | ${message} |
| | | 4653 | | <button type="button" class="btn-close" data-bs-dismiss="alert"></button> |
| | | 4654 | | `; |
| | | 4655 | | |
| | | 4656 | | document.body.appendChild(notification); |
| | | 4657 | | |
| | | 4658 | | // Автоматически скрываем через 5 секунд |
| | | 4659 | | setTimeout(() => { |
| | | 4660 | | if (notification.parentNode) { |
| | | 4661 | | notification.remove(); |
| | | 4662 | | } |
| | | 4663 | | }, 5000); |
| | | 4664 | | } |
| | | 4665 | | |
| | | 4666 | | document.addEventListener('click', async function (e) { |
| | | 4667 | | // Обработка одиночного обновления остатков (левая кнопка) |
| | | 4668 | | if (e.target.closest('.refreshOzonMain') && e.button === 0) { |
| | | 4669 | | e.preventDefault(); |
| | | 4670 | | |
| | | 4671 | | const btn = e.target.closest('.refreshOzonMain'); |
| | | 4672 | | |
| | | 4673 | | const ozonData = { |
| | | 4674 | | OzonClient: btn.getAttribute('data-client'), |
| | | 4675 | | OfferId: btn.getAttribute('data-offer-id'), |
| | | 4676 | | ProductId: Number(btn.getAttribute('data-product-id')), |
| | | 4677 | | Stock: Number(btn.getAttribute('data-stock')), |
| | | 4678 | | WarehouseId: Number(btn.getAttribute('data-warehouse-id')) |
| | | 4679 | | }; |
| | | 4680 | | |
| | | 4681 | | // Покажем индикатор загрузки на кнопке |
| | | 4682 | | const originalHtml = btn.innerHTML; |
| | | 4683 | | btn.disabled = true; |
| | | 4684 | | btn.innerHTML = ` |
| | | 4685 | | <span class="spinner-border spinner-border-sm text-primary" role="status" aria-hidden="true"></span> |
| | | 4686 | | `; |
| | | 4687 | | await updateOzonProductStock(ozonData); |
| | | 4688 | | |
| | | 4689 | | // Вернём иконку обратно |
| | | 4690 | | btn.innerHTML = originalHtml; |
| | | 4691 | | btn.disabled = false; |
| | | 4692 | | } |
| | | 4693 | | |
| | | 4694 | | // Обработка одиночного обнуления остатков (левая кнопка) |
| | | 4695 | | if (e.target.closest('.clearOzonStock') && e.button === 0) { |
| | | 4696 | | e.preventDefault(); |
| | | 4697 | | |
| | | 4698 | | const btn = e.target.closest('.clearOzonStock'); |
| | | 4699 | | |
| | | 4700 | | const ozonData = { |
| | | 4701 | | OzonClient: btn.getAttribute('data-client'), |
| | | 4702 | | OfferId: btn.getAttribute('data-offer-id'), |
| | | 4703 | | ProductId: Number(btn.getAttribute('data-product-id')), |
| | | 4704 | | Stock: 0, // Обнуляем остатки |
| | | 4705 | | WarehouseId: Number(btn.getAttribute('data-warehouse-id')) |
| | | 4706 | | }; |
| | | 4707 | | |
| | | 4708 | | // Покажем индикатор загрузки на кнопке |
| | | 4709 | | const originalHtml = btn.innerHTML; |
| | | 4710 | | btn.disabled = true; |
| | | 4711 | | btn.innerHTML = ` |
| | | 4712 | | <span class="spinner-border spinner-border-sm text-danger" role="status" aria-hidden="true"></span> |
| | | 4713 | | `; |
| | | 4714 | | await updateOzonProductStock(ozonData); |
| | | 4715 | | |
| | | 4716 | | // Вернём иконку обратно |
| | | 4717 | | btn.innerHTML = originalHtml; |
| | | 4718 | | btn.disabled = false; |
| | | 4719 | | } |
| | | 4720 | | |
| | | 4721 | | // Обработка обновления выбранных |
| | | 4722 | | if (e.target.closest('#refreshSelectedBtn')) { |
| | | 4723 | | e.preventDefault(); |
| | | 4724 | | refreshSelectedStocks(); |
| | | 4725 | | } |
| | | 4726 | | |
| | | 4727 | | // Обработка обнуления выбранных |
| | | 4728 | | if (e.target.closest('#clearSelectedBtn')) { |
| | | 4729 | | e.preventDefault(); |
| | | 4730 | | await clearSelectedStocks(); |
| | | 4731 | | } |
| | | 4732 | | if (e.target.closest('#archiveSelectedBtn')) { |
| | | 4733 | | e.preventDefault(); |
| | | 4734 | | archiveSelectedProducts(); |
| | | 4735 | | } |
| | | 4736 | | if (e.target.closest('#unarchiveSelectedBtn')) { |
| | | 4737 | | e.preventDefault(); |
| | | 4738 | | unarchiveSelectedProducts(); |
| | | 4739 | | } |
| | | 4740 | | }); |
| | | 4741 | | |
| | | 4742 | | // Обработка правой кнопки мыши на кнопках обновления |
| | | 4743 | | document.addEventListener('contextmenu', function (e) { |
| | | 4744 | | if (e.target.closest('.refreshOzonMain')) { |
| | | 4745 | | e.preventDefault(); |
| | | 4746 | | |
| | | 4747 | | const btn = e.target.closest('.refreshOzonMain'); |
| | | 4748 | | const itemId = `${btn.getAttribute('data-product-id')}_${btn.getAttribute('data-warehouse-id')}`; |
| | | 4749 | | |
| | | 4750 | | // Переключаем выделение |
| | | 4751 | | if (selectedRefreshItems.has(itemId)) { |
| | | 4752 | | selectedRefreshItems.delete(itemId); |
| | | 4753 | | btn.classList.remove('selected-refresh-item'); |
| | | 4754 | | } else { |
| | | 4755 | | selectedRefreshItems.add(itemId); |
| | | 4756 | | btn.classList.add('selected-refresh-item'); |
| | | 4757 | | } |
| | | 4758 | | |
| | | 4759 | | // Обновляем UI |
| | | 4760 | | updateSelectedUI(); |
| | | 4761 | | } |
| | | 4762 | | }); |
| | | 4763 | | |
| | | 4764 | | // Архив: «Нет» — в архив; клиент — выделение. Разархив: «Да» — из архива; клиент — выделение. |
| | | 4765 | | document.addEventListener('click', function (e) { |
| | | 4766 | | const unarchiveYes = e.target.closest('.ozon-unarchive-yes'); |
| | | 4767 | | if (unarchiveYes) { |
| | | 4768 | | e.preventDefault(); |
| | | 4769 | | e.stopPropagation(); |
| | | 4770 | | const pick = unarchiveYes.closest('.ozon-unarchive-pick'); |
| | | 4771 | | if (pick) unarchiveSingleFromYesClick(pick); |
| | | 4772 | | return; |
| | | 4773 | | } |
| | | 4774 | | |
| | | 4775 | | const unarchiveToggle = e.target.closest('.ozon-unarchive-toggle-col'); |
| | | 4776 | | if (unarchiveToggle) { |
| | | 4777 | | e.preventDefault(); |
| | | 4778 | | toggleUnarchivePickSelection(unarchiveToggle.closest('.ozon-unarchive-pick')); |
| | | 4779 | | return; |
| | | 4780 | | } |
| | | 4781 | | |
| | | 4782 | | const noBtn = e.target.closest('.ozon-archive-no'); |
| | | 4783 | | if (noBtn) { |
| | | 4784 | | e.preventDefault(); |
| | | 4785 | | e.stopPropagation(); |
| | | 4786 | | const pick = noBtn.closest('.ozon-archive-pick'); |
| | | 4787 | | if (pick) archiveSingleFromNoClick(pick); |
| | | 4788 | | return; |
| | | 4789 | | } |
| | | 4790 | | |
| | | 4791 | | const toggleCol = e.target.closest('.ozon-archive-toggle-col'); |
| | | 4792 | | if (toggleCol) { |
| | | 4793 | | e.preventDefault(); |
| | | 4794 | | toggleArchivePickSelection(toggleCol.closest('.ozon-archive-pick')); |
| | | 4795 | | } |
| | | 4796 | | }); |
| | | 4797 | | |
| | | 4798 | | document.addEventListener('contextmenu', function (e) { |
| | | 4799 | | const unarchivePick = e.target.closest('.ozon-unarchive-pick'); |
| | | 4800 | | if (unarchivePick && !e.target.closest('.ozon-unarchive-yes')) { |
| | | 4801 | | e.preventDefault(); |
| | | 4802 | | toggleUnarchivePickSelection(unarchivePick); |
| | | 4803 | | return; |
| | | 4804 | | } |
| | | 4805 | | |
| | | 4806 | | const pick = e.target.closest('.ozon-archive-pick'); |
| | | 4807 | | if (pick && !e.target.closest('.ozon-archive-no')) { |
| | | 4808 | | e.preventDefault(); |
| | | 4809 | | toggleArchivePickSelection(pick); |
| | | 4810 | | } |
| | | 4811 | | }); |
| | | 4812 | | |
| | | 4813 | | document.getElementById('triggerSelectAllUnarchiveable')?.addEventListener('click', function (e) { |
| | | 4814 | | e.preventDefault(); |
| | | 4815 | | e.stopPropagation(); |
| | | 4816 | | toggleSelectAllUnarchiveableOnPage(); |
| | | 4817 | | }); |
| | | 4818 | | |
| | | 4819 | | document.getElementById('triggerSelectAllArchiveable')?.addEventListener('click', function (e) { |
| | | 4820 | | e.preventDefault(); |
| | | 4821 | | e.stopPropagation(); |
| | | 4822 | | toggleSelectAllArchiveableOnPage(); |
| | | 4823 | | }); |
| | | 4824 | | |
| | | 4825 | | // Обработка правой кнопки мыши на кнопках обнуления |
| | | 4826 | | document.addEventListener('contextmenu', function (e) { |
| | | 4827 | | if (e.target.closest('.clearOzonStock')) { |
| | | 4828 | | e.preventDefault(); |
| | | 4829 | | |
| | | 4830 | | const btn = e.target.closest('.clearOzonStock'); |
| | | 4831 | | const itemId = `${btn.getAttribute('data-product-id')}_${btn.getAttribute('data-warehouse-id')}`; |
| | | 4832 | | |
| | | 4833 | | // Переключаем выделение |
| | | 4834 | | if (selectedClearItems.has(itemId)) { |
| | | 4835 | | selectedClearItems.delete(itemId); |
| | | 4836 | | btn.classList.remove('selected-clear-item'); |
| | | 4837 | | } else { |
| | | 4838 | | selectedClearItems.add(itemId); |
| | | 4839 | | btn.classList.add('selected-clear-item'); |
| | | 4840 | | } |
| | | 4841 | | |
| | | 4842 | | // Обновляем UI |
| | | 4843 | | updateSelectedUI(); |
| | | 4844 | | } |
| | | 4845 | | }); |
| | | 4846 | | |
| | | 4847 | | // CSS для выделенных элементов |
| | | 4848 | | const style = document.createElement('style'); |
| | | 4849 | | style.textContent = ` |
| | | 4850 | | /* Стили для выделенных элементов обновления */ |
| | | 4851 | | .refreshOzonMain.selected-refresh-item { |
| | | 4852 | | position: relative; |
| | | 4853 | | background-color: #cce7ff !important; |
| | | 4854 | | border-radius: 3px; |
| | | 4855 | | box-shadow: 0 0 0 2px #007bff; |
| | | 4856 | | padding: 2px !important; |
| | | 4857 | | } |
| | | 4858 | | .refreshOzonMain.selected-refresh-item svg { |
| | | 4859 | | fill: #004085 !important; |
| | | 4860 | | transform: scale(1.1); |
| | | 4861 | | } |
| | | 4862 | | |
| | | 4863 | | /* Стили для выделенных элементов обнуления */ |
| | | 4864 | | .clearOzonStock.selected-clear-item { |
| | | 4865 | | position: relative; |
| | | 4866 | | background-color: #f8d7da !important; |
| | | 4867 | | border-radius: 3px; |
| | | 4868 | | box-shadow: 0 0 0 2px #dc3545; |
| | | 4869 | | padding: 2px !important; |
| | | 4870 | | } |
| | | 4871 | | .clearOzonStock.selected-clear-item svg { |
| | | 4872 | | fill: #721c24 !important; |
| | | 4873 | | transform: scale(1.1); |
| | | 4874 | | } |
| | | 4875 | | |
| | | 4876 | | .ozon-archive-cell .ozon-archive-line { |
| | | 4877 | | display: flex; |
| | | 4878 | | align-items: baseline; |
| | | 4879 | | gap: 0.35rem; |
| | | 4880 | | border-radius: 3px; |
| | | 4881 | | margin-left: -2px; |
| | | 4882 | | margin-right: -2px; |
| | | 4883 | | padding: 1px 2px; |
| | | 4884 | | } |
| | | 4885 | | .ozon-archive-cell .ozon-archive-status { |
| | | 4886 | | flex: 0 0 8.75rem; |
| | | 4887 | | min-width: 8.75rem; |
| | | 4888 | | white-space: nowrap; |
| | | 4889 | | } |
| | | 4890 | | .ozon-archive-cell .ozon-archive-client { |
| | | 4891 | | flex: 1 1 auto; |
| | | 4892 | | min-width: 0; |
| | | 4893 | | line-height: 1.2; |
| | | 4894 | | } |
| | | 4895 | | .ozon-archive-no, |
| | | 4896 | | .ozon-archive-no-label { |
| | | 4897 | | color: #198754; |
| | | 4898 | | font-weight: 500; |
| | | 4899 | | } |
| | | 4900 | | .ozon-unarchive-yes, |
| | | 4901 | | .ozon-archive-yes-label { |
| | | 4902 | | color: #dc3545; |
| | | 4903 | | font-weight: 500; |
| | | 4904 | | } |
| | | 4905 | | .ozon-archive-pick.selected-archive-item { |
| | | 4906 | | background-color: #f8d7da !important; |
| | | 4907 | | box-shadow: inset 0 0 0 2px #dc3545; |
| | | 4908 | | } |
| | | 4909 | | .ozon-archive-pick.selected-archive-item .ozon-archive-no { |
| | | 4910 | | color: #dc3545 !important; |
| | | 4911 | | } |
| | | 4912 | | .ozon-unarchive-pick.selected-unarchive-item { |
| | | 4913 | | background-color: #d1e7dd !important; |
| | | 4914 | | box-shadow: inset 0 0 0 2px #198754; |
| | | 4915 | | } |
| | | 4916 | | .ozon-unarchive-pick.selected-unarchive-item .ozon-unarchive-yes { |
| | | 4917 | | color: #198754 !important; |
| | | 4918 | | } |
| | | 4919 | | .ozon-archive-no:hover, |
| | | 4920 | | .ozon-unarchive-yes:hover { |
| | | 4921 | | text-decoration: underline; |
| | | 4922 | | font-weight: 600; |
| | | 4923 | | } |
| | | 4924 | | .ozon-archive-toggle-col:hover, |
| | | 4925 | | .ozon-unarchive-toggle-col:hover { |
| | | 4926 | | color: #212529 !important; |
| | | 4927 | | font-weight: 600; |
| | | 4928 | | } |
| | | 4929 | | |
| | | 4930 | | /* Анимация для кнопок */ |
| | | 4931 | | .refreshOzonMain, .clearOzonStock { |
| | | 4932 | | transition: all 0.2s ease-in-out; |
| | | 4933 | | } |
| | | 4934 | | |
| | | 4935 | | /* Убираем стандартные стили фокуса для этих кнопок */ |
| | | 4936 | | .refreshOzonMain:focus, |
| | | 4937 | | .clearOzonStock:focus, |
| | | 4938 | | .refreshOzonMain:focus-visible, |
| | | 4939 | | .clearOzonStock:focus-visible { |
| | | 4940 | | outline: none !important; |
| | | 4941 | | box-shadow: none !important; |
| | | 4942 | | border: none !important; |
| | | 4943 | | } |
| | | 4944 | | |
| | | 4945 | | /* Убираем выделение при активации */ |
| | | 4946 | | .refreshOzonMain:active, |
| | | 4947 | | .clearOzonStock:active { |
| | | 4948 | | outline: none !important; |
| | | 4949 | | box-shadow: none !important; |
| | | 4950 | | border: none !important; |
| | | 4951 | | } |
| | | 4952 | | |
| | | 4953 | | /* Убираем стандартное выделение браузера */ |
| | | 4954 | | .refreshOzonMain, |
| | | 4955 | | .clearOzonStock { |
| | | 4956 | | -webkit-tap-highlight-color: transparent; |
| | | 4957 | | -webkit-user-select: none; |
| | | 4958 | | -moz-user-select: none; |
| | | 4959 | | -ms-user-select: none; |
| | | 4960 | | user-select: none; |
| | | 4961 | | outline: none !important; |
| | | 4962 | | border: none !important; |
| | | 4963 | | } |
| | | 4964 | | |
| | | 4965 | | /* Специфично для кнопок с type="button" */ |
| | | 4966 | | button.refreshOzonMain, |
| | | 4967 | | button.clearOzonStock { |
| | | 4968 | | outline: none !important; |
| | | 4969 | | border: none !important; |
| | | 4970 | | } |
| | | 4971 | | |
| | | 4972 | | /* Контейнеры для кнопок массовых операций */ |
| | | 4973 | | .mass-action-container { |
| | | 4974 | | position: sticky; |
| | | 4975 | | bottom: 20px; |
| | | 4976 | | z-index: 1000; |
| | | 4977 | | text-align: center; |
| | | 4978 | | margin: 10px 0; |
| | | 4979 | | } |
| | | 4980 | | |
| | | 4981 | | /* Расположение кнопок массовых операций */ |
| | | 4982 | | #refreshSelectedContainer { |
| | | 4983 | | bottom: 80px; |
| | | 4984 | | } |
| | | 4985 | | |
| | | 4986 | | #clearSelectedContainer { |
| | | 4987 | | bottom: 20px; |
| | | 4988 | | } |
| | | 4989 | | `; |
| | | 4990 | | document.head.appendChild(style); |
| | | 4991 | | |
| | | 4992 | | function isBitrixRowInactive(rowEl) { |
| | | 4993 | | return rowEl && String(rowEl.getAttribute('data-bitrix-active') || '').toUpperCase() === 'N'; |
| | | 4994 | | } |
| | | 4995 | | |
| | | 4996 | | function findBitrixStockRowForOzonUpdate(ozonData) { |
| | | 4997 | | if (!ozonData?.OfferId) return null; |
| | | 4998 | | const offer = String(ozonData.OfferId); |
| | | 4999 | | const article = offer.includes('=') ? offer.split('=')[0] : offer; |
| | | 5000 | | return document.querySelector( |
| | | 5001 | | `.bitrix-stock-row[data-article="${CSS.escape(article)}"]`) || |
| | | 5002 | | document.querySelector(`.bitrix-stock-row[data-article="${CSS.escape(offer)}"]`); |
| | | 5003 | | } |
| | | 5004 | | |
| | | 5005 | | async function updateOzonProductStock(ozonData) { |
| | | 5006 | | try { |
| | | 5007 | | const stockRow = findBitrixStockRowForOzonUpdate(ozonData); |
| | | 5008 | | if (isBitrixRowInactive(stockRow)) { |
| | | 5009 | | showNotification('Товар неактивен (Активность = Нет). Остаток в Ozon не изменяется.', 'warning'); |
| | | 5010 | | return false; |
| | | 5011 | | } |
| | | 5012 | | |
| | | 5013 | | if (ozonData) { |
| | | 5014 | | ozonData.BitrixActive = stockRow?.getAttribute('data-bitrix-active') || 'Y'; |
| | | 5015 | | } |
| | | 5016 | | |
| | | 5017 | | if (ozonData?.OzonClient) { |
| | | 5018 | | await ensureWarehouseMappingsForClient(ozonData.OzonClient, getBitrixStoreNamesForOzonFetch()); |
| | | 5019 | | } |
| | | 5020 | | |
| | | 5021 | | const response = await fetch('/BitrixStock/UpdateProductStocks', { |
| | | 5022 | | method: 'POST', |
| | | 5023 | | headers: {'Content-Type': 'application/json'}, |
| | | 5024 | | body: JSON.stringify(ozonData) |
| | | 5025 | | }); |
| | | 5026 | | |
| | | 5027 | | const data = await response.json(); |
| | | 5028 | | |
| | | 5029 | | const resolvedProductId = Number(data.productId ?? data.ProductId ?? ozonData.ProductId) || 0; |
| | | 5030 | | if (resolvedProductId > 0) { |
| | | 5031 | | ozonData.ProductId = resolvedProductId; |
| | | 5032 | | } |
| | | 5033 | | |
| | | 5034 | | if (response.ok) { |
| | | 5035 | | const refreshBtn = document.querySelector( |
| | | 5036 | | `.refreshOzonMain[data-client="${ozonData.OzonClient}"][data-offer-id="${ozonData.OfferId}"][data-wa |
| | | 5037 | | ); |
| | | 5038 | | const clearBtn = document.querySelector( |
| | | 5039 | | `.clearOzonStock[data-client="${ozonData.OzonClient}"][data-offer-id="${ozonData.OfferId}"][data-war |
| | | 5040 | | ); |
| | | 5041 | | if (resolvedProductId > 0) { |
| | | 5042 | | refreshBtn?.setAttribute('data-product-id', String(resolvedProductId)); |
| | | 5043 | | clearBtn?.setAttribute('data-product-id', String(resolvedProductId)); |
| | | 5044 | | } |
| | | 5045 | | |
| | | 5046 | | // Если есть массив data.data, выводим подробности |
| | | 5047 | | if (Array.isArray(data.data) && data.data.length > 0) { |
| | | 5048 | | const messages = data.data.map(item => { |
| | | 5049 | | const stockDiv = document.querySelector( |
| | | 5050 | | `.col-4.text-end[data-client="${ozonData.OzonClient}"][data-offer-id="${ozonData.OfferId}"][ |
| | | 5051 | | ); |
| | | 5052 | | if (stockDiv) { |
| | | 5053 | | if (resolvedProductId > 0) { |
| | | 5054 | | stockDiv.setAttribute('data-product-id', String(resolvedProductId)); |
| | | 5055 | | } |
| | | 5056 | | stockDiv.textContent = ozonData.Stock; |
| | | 5057 | | stockDiv.style.color = ""; |
| | | 5058 | | } |
| | | 5059 | | |
| | | 5060 | | return `${item.offerId || item.offer_id || "Неизвестный оффер"} — updated: ${item.updated}`; |
| | | 5061 | | }).join('<br>'); |
| | | 5062 | | showNotification(`${data.message ?? "Остатки обновлены"}<br>${messages}`, 'success'); |
| | | 5063 | | } else { |
| | | 5064 | | showNotification(data.message ?? "Остатки обновлены", 'success'); |
| | | 5065 | | } |
| | | 5066 | | return true; |
| | | 5067 | | } else { |
| | | 5068 | | // Если есть ошибки в массиве data.errors |
| | | 5069 | | if (Array.isArray(data.errors) && data.errors.length > 0) { |
| | | 5070 | | const errorMessages = data.errors.map(err => { |
| | | 5071 | | const offer = err.offerId ?? err.offer_id ?? "Неизвестный оффер"; |
| | | 5072 | | const text = err.message ?? err.Message ?? err.code ?? "ошибка Ozon"; |
| | | 5073 | | return `${offer} — ${text}`; |
| | | 5074 | | }).join('<br>'); |
| | | 5075 | | showNotification(`${data.message ?? "Ошибка при обновлении остатков"}<br>${errorMessages}`, 'danger' |
| | | 5076 | | } else { |
| | | 5077 | | showNotification(data.message ?? "Ошибка при обновлении остатков", 'danger'); |
| | | 5078 | | } |
| | | 5079 | | console.error('Ответ сервера (ошибка):', data); |
| | | 5080 | | return false; |
| | | 5081 | | } |
| | | 5082 | | } catch (error) { |
| | | 5083 | | console.error('Ошибка запроса:', error); |
| | | 5084 | | showNotification('Ошибка при отправке данных на сервер', 'danger'); |
| | | 5085 | | return false; |
| | | 5086 | | } |
| | | 5087 | | } |
| | | 5088 | | |
| | | 5089 | | |
| | | 5090 | | document.addEventListener('click', function (e) { |
| | | 5091 | | if (e.target.matches('.warehouse-set-link')) { |
| | | 5092 | | e.preventDefault(); |
| | | 5093 | | |
| | | 5094 | | const link = e.target.closest('.warehouse-set-link'); |
| | | 5095 | | const offerId = link.getAttribute('data-offer-id'); |
| | | 5096 | | const productId = Number(link.getAttribute('data-product-id')); |
| | | 5097 | | const clientName = link.getAttribute('data-client'); |
| | | 5098 | | const warehouseId = Number(link.getAttribute('data-warehouse-id')); |
| | | 5099 | | const warehouseName = link.getAttribute('data-warehouse-name'); |
| | | 5100 | | const currentStock = Number(link.getAttribute('data-total')) || 0; |
| | | 5101 | | |
| | | 5102 | | // Модалка Bootstrap |
| | | 5103 | | const modalHtml = ` |
| | | 5104 | | <div class="modal fade" id="setStockModal" tabindex="-1"> |
| | | 5105 | | <div class="modal-dialog modal-dialog-centered"> |
| | | 5106 | | <div class="modal-content"> |
| | | 5107 | | <div class="modal-header"> |
| | | 5108 | | <h5 class="modal-title">Установить остатки</h5> |
| | | 5109 | | <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button> |
| | | 5110 | | </div> |
| | | 5111 | | <div class="modal-body"> |
| | | 5112 | | <p>Склад: <b>${warehouseName}</b></p> |
| | | 5113 | | <label for="newStockInput" class="form-label">Введите новый остаток</label> |
| | | 5114 | | <input type="number" step="1" id="newStockInput" class="form-control" value="${currentStock}" /> |
| | | 5115 | | </div> |
| | | 5116 | | <div class="modal-footer"> |
| | | 5117 | | <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button> |
| | | 5118 | | <button type="button" class="btn btn-primary" id="confirmSetStock">Установить</button> |
| | | 5119 | | </div> |
| | | 5120 | | </div> |
| | | 5121 | | </div> |
| | | 5122 | | </div>`; |
| | | 5123 | | |
| | | 5124 | | // Если модалки нет — добавляем |
| | | 5125 | | if (!document.querySelector('#setStockModal')) { |
| | | 5126 | | document.body.insertAdjacentHTML('beforeend', modalHtml); |
| | | 5127 | | } else { |
| | | 5128 | | document.querySelector('#setStockModal .modal-body').innerHTML = |
| | | 5129 | | `<p>Склад: <b>${warehouseName}</b></p> |
| | | 5130 | | <label for="newStockInput" class="form-label">Введите новый остаток</label> |
| | | 5131 | | <input type="number" step="1" id="newStockInput" class="form-control" value="${currentStock}" />`; |
| | | 5132 | | } |
| | | 5133 | | |
| | | 5134 | | const modal = new bootstrap.Modal(document.getElementById('setStockModal')); |
| | | 5135 | | modal.show(); |
| | | 5136 | | |
| | | 5137 | | // Обработка кнопки "Установить" |
| | | 5138 | | document.getElementById('confirmSetStock').onclick = async function () { |
| | | 5139 | | const newStock = parseInt(document.getElementById('newStockInput').value, 10); |
| | | 5140 | | if (isNaN(newStock) || newStock < 0) { |
| | | 5141 | | St2Dialog.alert('Введите корректное количество остатков', { variant: 'warning' }); |
| | | 5142 | | return; |
| | | 5143 | | } |
| | | 5144 | | |
| | | 5145 | | modal.hide(); |
| | | 5146 | | |
| | | 5147 | | const payload = { |
| | | 5148 | | OzonClient: clientName, |
| | | 5149 | | OfferId: offerId, |
| | | 5150 | | ProductId: productId, |
| | | 5151 | | Stock: newStock, |
| | | 5152 | | WarehouseId: warehouseId |
| | | 5153 | | }; |
| | | 5154 | | |
| | | 5155 | | $("#ozonTableContainer").html(` |
| | | 5156 | | <div class="d-flex justify-content-center align-items-center" style="height:100px;"> |
| | | 5157 | | <div class="spinner-border text-primary" role="status"> |
| | | 5158 | | <span class="visually-hidden">Загрузка Ozon...</span> |
| | | 5159 | | </div> |
| | | 5160 | | </div> |
| | | 5161 | | `); |
| | | 5162 | | |
| | | 5163 | | try { |
| | | 5164 | | const response = await fetch('/BitrixStock/UpdateProductStocks', { |
| | | 5165 | | method: 'POST', |
| | | 5166 | | headers: {'Content-Type': 'application/json'}, |
| | | 5167 | | body: JSON.stringify(payload) |
| | | 5168 | | }); |
| | | 5169 | | |
| | | 5170 | | const data = await response.json(); |
| | | 5171 | | |
| | | 5172 | | if (response.ok) { |
| | | 5173 | | showNotification(data.message ?? "Остатки обновлены", 'success'); |
| | | 5174 | | |
| | | 5175 | | await reloadOzonPanel(payload.OfferId, null, payload.OzonClient); |
| | | 5176 | | } else { |
| | | 5177 | | console.error('Ошибка сервера:', data); |
| | | 5178 | | showNotification(data.message ?? "Ошибка при обновлении остатков", 'danger'); |
| | | 5179 | | } |
| | | 5180 | | } catch (error) { |
| | | 5181 | | console.error('Ошибка запроса:', error); |
| | | 5182 | | showNotification('Ошибка при отправке данных на сервер', 'danger'); |
| | | 5183 | | } |
| | | 5184 | | }; |
| | | 5185 | | } |
| | | 5186 | | }); |
| | | 5187 | | |
| | | 5188 | | // Функция сброса выбора склада Ozon |
| | | 5189 | | function resetOzonWarehouseSelection() { |
| | | 5190 | | if (selectedOzonWarehouse) { |
| | | 5191 | | selectedOzonWarehouse.classList.remove('ozon-warehouse-selected'); |
| | | 5192 | | selectedOzonWarehouse = null; |
| | | 5193 | | } |
| | | 5194 | | } |
| | | 5195 | | |
| | | 5196 | | document.addEventListener('click', function (e) { |
| | | 5197 | | const updatePriceLink = e.target.closest('.update_price'); |
| | | 5198 | | if (updatePriceLink && !e.target.matches('.constant-index') && !e.target.matches('.percent')) { |
| | | 5199 | | e.preventDefault(); |
| | | 5200 | | |
| | | 5201 | | const link = updatePriceLink; |
| | | 5202 | | if (window.bootstrap?.Popover) { |
| | | 5203 | | const existingPopover = bootstrap.Popover.getInstance(link); |
| | | 5204 | | existingPopover?.hide(); |
| | | 5205 | | } |
| | | 5206 | | link.blur(); |
| | | 5207 | | |
| | | 5208 | | const originalHtml = link.innerHTML; |
| | | 5209 | | const offerId = normalizeOfferId(link.getAttribute('data-offer-id')); |
| | | 5210 | | const clientName = link.getAttribute('data-client'); |
| | | 5211 | | const constantIndex = parseFloat(link.getAttribute('data-constant-index')); |
| | | 5212 | | const percent = parseFloat(link.getAttribute('data-percent')); |
| | | 5213 | | |
| | | 5214 | | showNotification(`Дождитесь, пока Ozon обновит цены и индексы для ${offerId}`, 'warning'); |
| | | 5215 | | |
| | | 5216 | | |
| | | 5217 | | $("#ozonTableContainer").html(` |
| | | 5218 | | <div class="d-flex justify-content-center align-items-center" style="height:100px;"> |
| | | 5219 | | <div class="spinner-border text-primary" role="status"> |
| | | 5220 | | <span class="visually-hidden">Загрузка Ozon...</span> |
| | | 5221 | | </div> |
| | | 5222 | | </div> |
| | | 5223 | | `); |
| | | 5224 | | link.disabled = true; |
| | | 5225 | | |
| | | 5226 | | (async () => { |
| | | 5227 | | try { |
| | | 5228 | | const response = await fetch('/BitrixStock/UpdateProductPrice', { |
| | | 5229 | | method: 'POST', |
| | | 5230 | | headers: {'Content-Type': 'application/json'}, |
| | | 5231 | | body: JSON.stringify({ |
| | | 5232 | | article: offerId, |
| | | 5233 | | clientName: clientName, |
| | | 5234 | | constantIndex: constantIndex, |
| | | 5235 | | percent: percent |
| | | 5236 | | }) |
| | | 5237 | | }); |
| | | 5238 | | |
| | | 5239 | | const data = await response.json(); |
| | | 5240 | | await reloadOzonPanel(offerId, null, clientName); |
| | | 5241 | | |
| | | 5242 | | if (response.ok) { |
| | | 5243 | | if (data.result && data.result.length > 0) { |
| | | 5244 | | data.result.forEach(item => { |
| | | 5245 | | if (item.updated) { |
| | | 5246 | | showNotification(`Цена для товара ${item.offerId} успешно обновлена <br/> ${item.mes |
| | | 5247 | | } else if (item.errors && item.errors.length > 0) { |
| | | 5248 | | const errorList = item.errors.map(e => e.message).join('; '); |
| | | 5249 | | showNotification(`Ошибка для ${item.offerId}: ${errorList}`, 'danger'); |
| | | 5250 | | } else { |
| | | 5251 | | showNotification(`Для ${item.offerId} не удалось обновить цену`, 'warning'); |
| | | 5252 | | } |
| | | 5253 | | }); |
| | | 5254 | | } else { |
| | | 5255 | | showNotification('Сервер вернул пустой результат', 'warning'); |
| | | 5256 | | } |
| | | 5257 | | } else { |
| | | 5258 | | showNotification(data.message ?? 'Ошибка при обновлении цены', 'danger'); |
| | | 5259 | | } |
| | | 5260 | | } catch (error) { |
| | | 5261 | | console.error('Ошибка запроса:', error); |
| | | 5262 | | showNotification('Ошибка при отправке данных на сервер', 'danger'); |
| | | 5263 | | } finally { |
| | | 5264 | | link.innerHTML = originalHtml; |
| | | 5265 | | link.disabled = false; |
| | | 5266 | | updateUpdatePriceHint(link); |
| | | 5267 | | } |
| | | 5268 | | })(); |
| | | 5269 | | } |
| | | 5270 | | }); |
| | | 5271 | | |
| | | 5272 | | document.addEventListener('click', function (e) { |
| | | 5273 | | if (e.target.matches('.set_price')) { |
| | | 5274 | | e.preventDefault(); |
| | | 5275 | | |
| | | 5276 | | const link = e.target.closest('.set_price'); |
| | | 5277 | | const offerId = normalizeOfferId(link.getAttribute('data-offer-id')); |
| | | 5278 | | const clientName = link.getAttribute('data-client'); |
| | | 5279 | | const priceValue = parseFloat(link.getAttribute('data-price-value')) || 0; |
| | | 5280 | | |
| | | 5281 | | // Рассчитываем цену до скидки (на 25% больше) |
| | | 5282 | | const oldPriceValue = priceValue * 1.25; |
| | | 5283 | | |
| | | 5284 | | const modalHtml = ` |
| | | 5285 | | <div class="modal fade" id="setPriceModal" tabindex="-1"> |
| | | 5286 | | <div class="modal-dialog modal-dialog-centered"> |
| | | 5287 | | <div class="modal-content"> |
| | | 5288 | | <div class="modal-header"> |
| | | 5289 | | <h5 class="modal-title">Установить цены для <b>${offerId}</b></h5> |
| | | 5290 | | <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button> |
| | | 5291 | | </div> |
| | | 5292 | | <div class="modal-body"> |
| | | 5293 | | <div class="row"> |
| | | 5294 | | <div class="col-md-6 mb-3"> |
| | | 5295 | | <label for="yourPriceInput" class="form-label">Ваша цена, ₽</label> |
| | | 5296 | | <input type="number" step="0.01" id="yourPriceInput" class="form-control" value="${Math.round(priceV |
| | | 5297 | | </div> |
| | | 5298 | | <div class="col-md-6 mb-3"> |
| | | 5299 | | <label for="oldPriceInput" class="form-label">Цена до скидки, ₽</label> |
| | | 5300 | | <input type="number" step="0.01" id="oldPriceInput" class="form-control" value="${Math.round(oldPric |
| | | 5301 | | </div> |
| | | 5302 | | </div> |
| | | 5303 | | <div class="row"> |
| | | 5304 | | <div class="col-md-6 mb-3"> |
| | | 5305 | | <label for="minPriceInput" class="form-label">Минимальная цена, ₽</label> |
| | | 5306 | | <input type="number" step="0.01" id="minPriceInput" class="form-control" value="${Math.round(priceVa |
| | | 5307 | | </div> |
| | | 5308 | | <div class="col-md-6 mb-3"> |
| | | 5309 | | <label for="costPriceInput" class="form-label">Себестоимость, ₽</label> |
| | | 5310 | | <input type="number" step="0.01" id="costPriceInput" class="form-control" placeholder="0.00" /> |
| | | 5311 | | </div> |
| | | 5312 | | </div> |
| | | 5313 | | </div> |
| | | 5314 | | <div class="modal-footer"> |
| | | 5315 | | <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button> |
| | | 5316 | | <button type="button" class="btn btn-primary" id="confirmSetPrice">Установить цены</button> |
| | | 5317 | | </div> |
| | | 5318 | | </div> |
| | | 5319 | | </div> |
| | | 5320 | | </div>`; |
| | | 5321 | | |
| | | 5322 | | if (!document.querySelector('#setPriceModal')) { |
| | | 5323 | | document.body.insertAdjacentHTML('beforeend', modalHtml); |
| | | 5324 | | } else { |
| | | 5325 | | document.querySelector('#setPriceModal .modal-body').innerHTML = ` |
| | | 5326 | | <div class="row"> |
| | | 5327 | | <div class="col-md-6 mb-3"> |
| | | 5328 | | <label for="yourPriceInput" class="form-label">Ваша цена, ₽</label> |
| | | 5329 | | <input type="number" step="0.01" id="yourPriceInput" class="form-control" value="${Math.round(priceV |
| | | 5330 | | </div> |
| | | 5331 | | <div class="col-md-6 mb-3"> |
| | | 5332 | | <label for="oldPriceInput" class="form-label">Цена до скидки, ₽</label> |
| | | 5333 | | <input type="number" step="0.01" id="oldPriceInput" class="form-control" value="${Math.round(oldPric |
| | | 5334 | | </div> |
| | | 5335 | | </div> |
| | | 5336 | | <div class="row"> |
| | | 5337 | | <div class="col-md-6 mb-3"> |
| | | 5338 | | <label for="minPriceInput" class="form-label">Минимальная цена, ₽</label> |
| | | 5339 | | <input type="number" step="0.01" id="minPriceInput" class="form-control" value="${Math.round(priceVa |
| | | 5340 | | </div> |
| | | 5341 | | <div class="col-md-6 mb-3"> |
| | | 5342 | | <label for="costPriceInput" class="form-label">Себестоимость, ₽</label> |
| | | 5343 | | <input type="number" step="0.01" id="costPriceInput" class="form-control" placeholder="0.00" /> |
| | | 5344 | | </div> |
| | | 5345 | | </div>`; |
| | | 5346 | | } |
| | | 5347 | | |
| | | 5348 | | const modal = new bootstrap.Modal(document.getElementById('setPriceModal')); |
| | | 5349 | | modal.show(); |
| | | 5350 | | |
| | | 5351 | | // Функция для перерасчета цены до скидки |
| | | 5352 | | function recalculateOldPrice() { |
| | | 5353 | | const yourPrice = parseFloat(document.getElementById('yourPriceInput').value); |
| | | 5354 | | if (!isNaN(yourPrice) && yourPrice > 0) { |
| | | 5355 | | const oldPriceValue = yourPrice * 1.25; // +25% |
| | | 5356 | | document.getElementById('oldPriceInput').value = Math.round(oldPriceValue); |
| | | 5357 | | document.getElementById('minPriceInput').value = Math.round(yourPrice); |
| | | 5358 | | } |
| | | 5359 | | } |
| | | 5360 | | |
| | | 5361 | | // Обработчик изменения поля "Ваша цена" |
| | | 5362 | | document.getElementById('yourPriceInput').addEventListener('input', recalculateOldPrice); |
| | | 5363 | | document.getElementById('yourPriceInput').addEventListener('change', recalculateOldPrice); |
| | | 5364 | | |
| | | 5365 | | // Обработка кнопки "Установить цены" |
| | | 5366 | | document.getElementById('confirmSetPrice').onclick = async function () { |
| | | 5367 | | const yourPrice = parseFloat(document.getElementById('yourPriceInput').value); |
| | | 5368 | | const oldPrice = parseFloat(document.getElementById('oldPriceInput').value); |
| | | 5369 | | const minPrice = parseFloat(document.getElementById('minPriceInput').value); |
| | | 5370 | | const costPrice = parseFloat(document.getElementById('costPriceInput').value); |
| | | 5371 | | |
| | | 5372 | | // Валидация - хотя бы одна цена должна быть заполнена |
| | | 5373 | | if (isNaN(yourPrice) && isNaN(oldPrice) && isNaN(minPrice) && isNaN(costPrice)) { |
| | | 5374 | | St2Dialog.alert('Введите хотя бы одну цену', { variant: 'warning' }); |
| | | 5375 | | return; |
| | | 5376 | | } |
| | | 5377 | | |
| | | 5378 | | modal.hide(); |
| | | 5379 | | |
| | | 5380 | | $("#ozonTableContainer").html(` |
| | | 5381 | | <div class="d-flex justify-content-center align-items-center" style="height:100px;"> |
| | | 5382 | | <div class="spinner-border text-primary" role="status"> |
| | | 5383 | | <span class="visually-hidden">Загрузка Ozon...</span> |
| | | 5384 | | </div> |
| | | 5385 | | </div> |
| | | 5386 | | `); |
| | | 5387 | | |
| | | 5388 | | try { |
| | | 5389 | | const response = await fetch('/BitrixStock/SetProductPrice', { |
| | | 5390 | | method: 'POST', |
| | | 5391 | | headers: {'Content-Type': 'application/json'}, |
| | | 5392 | | body: JSON.stringify({ |
| | | 5393 | | article: offerId, |
| | | 5394 | | clientName: clientName, |
| | | 5395 | | yourPrice: isNaN(yourPrice) ? 0 : yourPrice, |
| | | 5396 | | oldPrice: isNaN(oldPrice) ? 0 : oldPrice, |
| | | 5397 | | minPrice: isNaN(minPrice) ? 0 : minPrice, |
| | | 5398 | | costPrice: isNaN(costPrice) ? 0 : costPrice |
| | | 5399 | | }) |
| | | 5400 | | }); |
| | | 5401 | | |
| | | 5402 | | const data = await response.json(); |
| | | 5403 | | await reloadOzonPanel(offerId, null, clientName); |
| | | 5404 | | |
| | | 5405 | | if (response.ok) { |
| | | 5406 | | data.result.forEach(offer => { |
| | | 5407 | | showNotification( |
| | | 5408 | | `Цены для товара ${offerId} успешно установлены<br/>${offer.message}`, |
| | | 5409 | | 'success' |
| | | 5410 | | ); |
| | | 5411 | | }); |
| | | 5412 | | |
| | | 5413 | | } else { |
| | | 5414 | | showNotification(data.message ?? 'Ошибка при установке цен', 'danger'); |
| | | 5415 | | } |
| | | 5416 | | } catch (error) { |
| | | 5417 | | console.error(error); |
| | | 5418 | | showNotification('Ошибка при отправке данных на сервер', 'danger'); |
| | | 5419 | | } |
| | | 5420 | | }; |
| | | 5421 | | } |
| | | 5422 | | }); |
| | | 5423 | | |
| | | 5424 | | function recalculateOldPrice() { |
| | | 5425 | | const yourPrice = parseFloat(document.getElementById('yourPriceInput').value); |
| | | 5426 | | if (!isNaN(yourPrice) && yourPrice > 0) { |
| | | 5427 | | const oldPriceValue = yourPrice * 1.25; // +25% |
| | | 5428 | | document.getElementById('oldPriceInput').value = oldPriceValue.toFixed(2); |
| | | 5429 | | } |
| | | 5430 | | } |
| | | 5431 | | |
| | | 5432 | | document.addEventListener('input', function (e) { |
| | | 5433 | | if (e.target.matches('.constant-index') || e.target.matches('.percent')) { |
| | | 5434 | | const link = e.target.closest('.update_price'); |
| | | 5435 | | const constantIndex = parseFloat(link.querySelector('.constant-index').innerText.replace(',', '.')) || 0; |
| | | 5436 | | const percent = parseFloat(link.querySelector('.percent').innerText.replace(',', '.')) || 0; |
| | | 5437 | | |
| | | 5438 | | // обновляем data-атрибуты |
| | | 5439 | | link.setAttribute('data-constant-index', constantIndex.toFixed(2)); |
| | | 5440 | | link.setAttribute('data-percent', percent.toFixed(1)); |
| | | 5441 | | updateUpdatePriceHint(link); |
| | | 5442 | | } |
| | | 5443 | | }); |
| | | 5444 | | |
| | | 5445 | | document.addEventListener('mouseover', function (e) { |
| | | 5446 | | const link = e.target.closest('.update_price'); |
| | | 5447 | | if (link) { |
| | | 5448 | | updateUpdatePriceHint(link); |
| | | 5449 | | } |
| | | 5450 | | }); |
| | | 5451 | | |
| | | 5452 | | document.addEventListener('focusin', function (e) { |
| | | 5453 | | const link = e.target.closest('.update_price'); |
| | | 5454 | | if (link) { |
| | | 5455 | | updateUpdatePriceHint(link); |
| | | 5456 | | } |
| | | 5457 | | }); |
| | | 5458 | | |
| | | 5459 | | document.addEventListener('click', function (e) { |
| | | 5460 | | if (e.target.classList.contains('weight-value')) { |
| | | 5461 | | let weight = e.target.getAttribute('data-weight') || ''; |
| | | 5462 | | const orderId = e.target.getAttribute('data-order-id'); |
| | | 5463 | | const weightInput = document.querySelector(`#ProductWeight_${orderId}`); |
| | | 5464 | | |
| | | 5465 | | if (weightInput) { |
| | | 5466 | | // Очистка строки: убираем "кг" и пробелы |
| | | 5467 | | weight = weight.replace(/кг/gi, '').trim(); |
| | | 5468 | | |
| | | 5469 | | // Заменяем запятые на точки |
| | | 5470 | | weight = weight.replace(/,/g, '.'); |
| | | 5471 | | |
| | | 5472 | | // Берём первое число из строки (если несколько значений через запятую/пробел) |
| | | 5473 | | const match = weight.match(/[\d.]+/g); |
| | | 5474 | | const firstWeight = match ? match[0] : null; |
| | | 5475 | | |
| | | 5476 | | if (firstWeight) { |
| | | 5477 | | // Форматируем вес (parseFloat убирает лишние нули) |
| | | 5478 | | const formattedWeight = parseFloat(firstWeight).toString(); |
| | | 5479 | | |
| | | 5480 | | // Устанавливаем значение |
| | | 5481 | | weightInput.value = formattedWeight; |
| | | 5482 | | |
| | | 5483 | | // Триггерим события |
| | | 5484 | | const eventsToTrigger = ['input', 'change', 'blur']; |
| | | 5485 | | eventsToTrigger.forEach(eventType => { |
| | | 5486 | | const event = new Event(eventType, {bubbles: true, cancelable: true}); |
| | | 5487 | | weightInput.dispatchEvent(event); |
| | | 5488 | | }); |
| | | 5489 | | |
| | | 5490 | | // Для jQuery (если используется) |
| | | 5491 | | if (typeof jQuery !== 'undefined') { |
| | | 5492 | | $(weightInput).trigger('input').trigger('change'); |
| | | 5493 | | } |
| | | 5494 | | |
| | | 5495 | | // Подсвечиваем поле на 1 секунду |
| | | 5496 | | |
| | | 5497 | | weightInput.style.backgroundColor = '#e6ffe6'; |
| | | 5498 | | setTimeout(() => { |
| | | 5499 | | weightInput.style.backgroundColor = ''; |
| | | 5500 | | }, 1000); |
| | | 5501 | | |
| | | 5502 | | // Фокусируем поле ввода |
| | | 5503 | | weightInput.focus(); |
| | | 5504 | | } else { |
| | | 5505 | | console.warn(`Не удалось распарсить вес: "${weight}"`); |
| | | 5506 | | } |
| | | 5507 | | |
| | | 5508 | | sortAndRerenderTable(orderId, 'costPrice', 'number', 'acs'); |
| | | 5509 | | } else { |
| | | 5510 | | console.warn(`Не найдено поле ввода веса для orderId: ${orderId}`); |
| | | 5511 | | } |
| | | 5512 | | } |
| | | 5513 | | }); |
| | | 5514 | | |
| | | 5515 | | function applyBitrixActiveUiAfterToggle(activeEl, isActive) { |
| | | 5516 | | const display = isActive ? 'Да' : 'Нет'; |
| | | 5517 | | activeEl.textContent = display; |
| | | 5518 | | activeEl.style.opacity = '1'; |
| | | 5519 | | activeEl.classList.toggle('text-danger', !isActive); |
| | | 5520 | | |
| | | 5521 | | const row = activeEl.closest('.bitrix-stock-row'); |
| | | 5522 | | if (!row) { |
| | | 5523 | | return; |
| | | 5524 | | } |
| | | 5525 | | |
| | | 5526 | | row.setAttribute('data-bitrix-active', isActive ? 'Y' : 'N'); |
| | | 5527 | | row.querySelectorAll('.refreshOzonMain').forEach(btn => { |
| | | 5528 | | btn.style.display = isActive ? '' : 'none'; |
| | | 5529 | | }); |
| | | 5530 | | row.querySelectorAll('.clearOzonStock').forEach(btn => { |
| | | 5531 | | btn.style.display = !isActive ? '' : 'none'; |
| | | 5532 | | }); |
| | | 5533 | | } |
| | | 5534 | | |
| | | 5535 | | document.addEventListener('DOMContentLoaded', function () { |
| | | 5536 | | document.querySelectorAll('[data-active-id]').forEach(element => { |
| | | 5537 | | element.addEventListener('click', async function (e) { |
| | | 5538 | | e.preventDefault(); |
| | | 5539 | | |
| | | 5540 | | const productId = this.getAttribute('data-active-id'); |
| | | 5541 | | const currentStatus = this.textContent.trim(); |
| | | 5542 | | const wasActive = currentStatus === 'Да'; |
| | | 5543 | | |
| | | 5544 | | const confirmationMessage = wasActive |
| | | 5545 | | ? 'Вы уверены, что хотите деактивировать этот элемент? Остатки Ozon не изменятся автоматически.' |
| | | 5546 | | : 'Вы уверены, что хотите активировать этот элемент? Остатки Ozon не изменятся автоматически.'; |
| | | 5547 | | |
| | | 5548 | | const confirmed = await St2Dialog.confirm(confirmationMessage); |
| | | 5549 | | if (!confirmed) { |
| | | 5550 | | return; |
| | | 5551 | | } |
| | | 5552 | | |
| | | 5553 | | const originalText = this.textContent; |
| | | 5554 | | this.textContent = 'Загрузка...'; |
| | | 5555 | | this.style.opacity = '0.6'; |
| | | 5556 | | |
| | 0 | 5557 | | fetch('@Url.Action("ToggleActiveStatus", "BitrixStock", new { area = "Studio2" })', { |
| | | 5558 | | method: 'POST', |
| | | 5559 | | headers: { |
| | | 5560 | | 'Content-Type': 'application/json', |
| | | 5561 | | }, |
| | | 5562 | | body: JSON.stringify(parseInt(productId)) |
| | | 5563 | | }) |
| | | 5564 | | .then(response => response.json()) |
| | | 5565 | | .then(data => { |
| | | 5566 | | if (data.success) { |
| | | 5567 | | showNotification( |
| | | 5568 | | (data.message || 'Статус обновлён') + |
| | | 5569 | | ' Накат/обнуление Ozon — только кнопками в таблице.', |
| | | 5570 | | 'success'); |
| | | 5571 | | applyBitrixActiveUiAfterToggle(this, !!data.isActive); |
| | | 5572 | | } else { |
| | | 5573 | | showNotification(data.message || 'Произошла ошибка', 'danger'); |
| | | 5574 | | this.textContent = originalText; |
| | | 5575 | | this.style.opacity = '1'; |
| | | 5576 | | } |
| | | 5577 | | }) |
| | | 5578 | | .catch(error => { |
| | | 5579 | | console.error('Error:', error); |
| | | 5580 | | showNotification('Ошибка сети', 'danger'); |
| | | 5581 | | this.textContent = originalText; |
| | | 5582 | | this.style.opacity = '1'; |
| | | 5583 | | }); |
| | | 5584 | | }); |
| | | 5585 | | }); |
| | | 5586 | | }); |
| | | 5587 | | |
| | | 5588 | | $(document).on('click', '.refreshOzon', async function () { |
| | | 5589 | | const offerId = normalizeOfferId($(this).data("article")); |
| | | 5590 | | if (!offerId) { |
| | | 5591 | | console.warn('Offer ID не найден'); |
| | | 5592 | | return; |
| | | 5593 | | } |
| | | 5594 | | |
| | | 5595 | | // Показываем спиннер |
| | | 5596 | | $("#ozonTableContainer").html(` |
| | | 5597 | | <div class="d-flex justify-content-center align-items-center" style="height:100px;"> |
| | | 5598 | | <div class="spinner-border text-primary" role="status"> |
| | | 5599 | | <span class="visually-hidden">Загрузка Ozon...</span> |
| | | 5600 | | </div> |
| | | 5601 | | </div> |
| | | 5602 | | `); |
| | | 5603 | | |
| | | 5604 | | try { |
| | | 5605 | | await reloadOzonPanel(offerId); |
| | | 5606 | | } catch (error) { |
| | | 5607 | | console.error('Ошибка при обновлении данных Ozon:', error); |
| | | 5608 | | $("#ozonTableContainer").empty(); |
| | | 5609 | | } |
| | | 5610 | | }); |
| | | 5611 | | |
| | | 5612 | | const triggerAllClearOzonStockBtn = document.getElementById('triggerAllClearOzonStock'); |
| | | 5613 | | if (triggerAllClearOzonStockBtn) { |
| | | 5614 | | triggerAllClearOzonStockBtn.addEventListener('click', function () { |
| | | 5615 | | // Находим все кнопки обнуления |
| | | 5616 | | const clearButtons = document.querySelectorAll('.clearOzonStock'); |
| | | 5617 | | |
| | | 5618 | | // Создаем событие правого клика мыши |
| | | 5619 | | const rightClickEvent = new MouseEvent('contextmenu', { |
| | | 5620 | | bubbles: true, |
| | | 5621 | | cancelable: true, |
| | | 5622 | | view: window, |
| | | 5623 | | button: 2, // Правая кнопка мыши |
| | | 5624 | | buttons: 2 |
| | | 5625 | | }); |
| | | 5626 | | |
| | | 5627 | | // Инициируем правый клик на каждой кнопке |
| | | 5628 | | clearButtons.forEach(button => { |
| | | 5629 | | button.dispatchEvent(rightClickEvent); |
| | | 5630 | | }); |
| | | 5631 | | showNotification(`Выделено ${clearButtons.length} позиций для обнуления`, 'success'); |
| | | 5632 | | }); |
| | | 5633 | | } |
| | | 5634 | | |
| | | 5635 | | // Обработчик для кнопки массового обновления |
| | | 5636 | | const triggerAllRefreshOzonStockBtn = document.getElementById('triggerAllRefreshOzonStock'); |
| | | 5637 | | if (triggerAllRefreshOzonStockBtn) { |
| | | 5638 | | triggerAllRefreshOzonStockBtn.addEventListener('click', function () { |
| | | 5639 | | // Находим все кнопки обновления |
| | | 5640 | | const refreshButtons = document.querySelectorAll('.refreshOzonMain'); |
| | | 5641 | | |
| | | 5642 | | // Создаем событие правого клика мыши |
| | | 5643 | | const rightClickEvent = new MouseEvent('contextmenu', { |
| | | 5644 | | bubbles: true, |
| | | 5645 | | cancelable: true, |
| | | 5646 | | view: window, |
| | | 5647 | | button: 2, // Правая кнопка мыши |
| | | 5648 | | buttons: 2 |
| | | 5649 | | }); |
| | | 5650 | | |
| | | 5651 | | // Инициируем правый клик на каждой кнопке обновления |
| | | 5652 | | refreshButtons.forEach(button => { |
| | | 5653 | | button.dispatchEvent(rightClickEvent); |
| | | 5654 | | }); |
| | | 5655 | | |
| | | 5656 | | showNotification(`Выделено ${refreshButtons.length} позиций для обновления`, 'success'); |
| | | 5657 | | }); |
| | | 5658 | | } |
| | | 5659 | | |
| | | 5660 | | // Функция для проверки и отображения массовых кнопок |
| | | 5661 | | function updateMassActionButtons() { |
| | | 5662 | | const refreshButtons = document.querySelectorAll('.refreshOzonMain'); |
| | | 5663 | | const clearButtons = document.querySelectorAll('.clearOzonStock'); |
| | | 5664 | | |
| | | 5665 | | const refreshMassButton = document.getElementById('triggerAllRefreshOzonStock'); |
| | | 5666 | | const clearMassButton = document.getElementById('triggerAllClearOzonStock'); |
| | | 5667 | | |
| | | 5668 | | if (refreshMassButton) { |
| | | 5669 | | refreshMassButton.style.display = refreshButtons.length > 0 ? 'block' : 'none'; |
| | | 5670 | | } |
| | | 5671 | | |
| | | 5672 | | if (clearMassButton) { |
| | | 5673 | | clearMassButton.style.display = clearButtons.length > 0 ? 'block' : 'none'; |
| | | 5674 | | } |
| | | 5675 | | |
| | | 5676 | | console.log(`На странице: ${refreshButtons.length} кнопок обновления, ${clearButtons.length} кнопок обнуления`); |
| | | 5677 | | } |
| | | 5678 | | |
| | | 5679 | | // Вызываем при загрузке страницы |
| | | 5680 | | document.addEventListener('DOMContentLoaded', function () { |
| | | 5681 | | updateMassActionButtons(); |
| | | 5682 | | }); |
| | | 5683 | | |
| | | 5684 | | // Также можно вызывать после фильтрации или обновления данных |
| | | 5685 | | function handleDataUpdate() { |
| | | 5686 | | setTimeout(updateMassActionButtons, 100); // Небольшая задержка для обновления DOM |
| | | 5687 | | } |
| | | 5688 | | |
| | | 5689 | | // Показать спиннер загрузки |
| | | 5690 | | function showLoadingSpinner(message) { |
| | | 5691 | | const spinner = document.getElementById('loadingSpinner'); |
| | | 5692 | | const spinnerText = document.getElementById('loadingSpinnerText'); |
| | | 5693 | | if (spinner) { |
| | | 5694 | | spinner.style.display = 'flex'; |
| | | 5695 | | if (spinnerText) { |
| | | 5696 | | spinnerText.textContent = message || 'Загрузка...'; |
| | | 5697 | | } |
| | | 5698 | | |
| | | 5699 | | // Блокируем страницу |
| | | 5700 | | document.body.classList.add('spinner-active'); |
| | | 5701 | | |
| | | 5702 | | // Предотвращаем закрытие спиннера кликом на фон (опционально) |
| | | 5703 | | spinner.addEventListener('click', function (e) { |
| | | 5704 | | e.stopPropagation(); |
| | | 5705 | | }); |
| | | 5706 | | } |
| | | 5707 | | } |
| | | 5708 | | |
| | | 5709 | | // Скрыть спиннер загрузки |
| | | 5710 | | function hideLoadingSpinner() { |
| | | 5711 | | const spinner = document.getElementById('loadingSpinner'); |
| | | 5712 | | if (spinner) { |
| | | 5713 | | spinner.style.display = 'none'; |
| | | 5714 | | |
| | | 5715 | | // Разблокируем страницу |
| | | 5716 | | document.body.classList.remove('spinner-active'); |
| | | 5717 | | } |
| | | 5718 | | } |
| | | 5719 | | |
| | | 5720 | | // Отправка формы фильтров перехвачена гридом «Остатки» (bsx-stocks.js): страница не перезагружается. |
| | | 5721 | | </script> |
| | | 5722 | | |
| | | 5723 | | |
| | | 5724 | | <script> |
| | | 5725 | | // ========== Функционал сохранения и применения фильтров ========== |
| | | 5726 | | |
| | | 5727 | | document.addEventListener('DOMContentLoaded', function () { |
| | | 5728 | | console.log('Инициализация функционала фильтров...'); |
| | | 5729 | | |
| | | 5730 | | // Проверяем наличие необходимых элементов |
| | | 5731 | | console.log('saveFilterBtn:', document.getElementById('saveFilterBtn')); |
| | | 5732 | | console.log('saveFilterModal:', document.getElementById('saveFilterModal')); |
| | | 5733 | | console.log('savedFiltersSelect:', document.getElementById('savedFiltersSelect')); |
| | | 5734 | | |
| | | 5735 | | // Функция для получения текущих параметров фильтра |
| | | 5736 | | function getCurrentFilterParams() { |
| | | 5737 | | try { |
| | | 5738 | | const searchParams = new URLSearchParams(window.location.search); |
| | | 5739 | | |
| | | 5740 | | // Добавляем параметры из формы фильтрации |
| | | 5741 | | const filterForm = document.getElementById('filterForm'); |
| | | 5742 | | if (filterForm) { |
| | | 5743 | | const formData = new FormData(filterForm); |
| | | 5744 | | formData.forEach((value, key) => { |
| | | 5745 | | if (value && value !== '') { |
| | | 5746 | | searchParams.set(key, value); |
| | | 5747 | | } |
| | | 5748 | | }); |
| | | 5749 | | } |
| | | 5750 | | |
| | | 5751 | | // Очищаем стандартные параметры пагинации |
| | | 5752 | | searchParams.delete('page'); |
| | | 5753 | | searchParams.delete('pageSize'); |
| | | 5754 | | |
| | | 5755 | | // Преобразуем в строку |
| | | 5756 | | const paramsString = searchParams.toString(); |
| | | 5757 | | console.log('Строка параметров фильтра:', paramsString); |
| | | 5758 | | |
| | | 5759 | | return paramsString; |
| | | 5760 | | } catch (error) { |
| | | 5761 | | console.error('Ошибка при получении параметров фильтра:', error); |
| | | 5762 | | return ''; |
| | | 5763 | | } |
| | | 5764 | | } |
| | | 5765 | | |
| | | 5766 | | // Функция для показа уведомлений |
| | | 5767 | | function showNotification(message, type = 'info') { |
| | | 5768 | | // Если у вас уже есть функция showNotification, можете использовать её |
| | | 5769 | | // Или создайте простую реализацию |
| | | 5770 | | console.log(`${type}: ${message}`); |
| | | 5771 | | |
| | | 5772 | | // Пример простой реализации |
| | | 5773 | | const notification = document.createElement('div'); |
| | | 5774 | | notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`; |
| | | 5775 | | notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999;'; |
| | | 5776 | | notification.innerHTML = ` |
| | | 5777 | | ${message} |
| | | 5778 | | <button type="button" class="btn-close" data-bs-dismiss="alert"></button> |
| | | 5779 | | `; |
| | | 5780 | | |
| | | 5781 | | document.body.appendChild(notification); |
| | | 5782 | | |
| | | 5783 | | // Автоматическое скрытие через 5 секунд |
| | | 5784 | | setTimeout(() => { |
| | | 5785 | | if (notification.parentNode) { |
| | | 5786 | | notification.parentNode.removeChild(notification); |
| | | 5787 | | } |
| | | 5788 | | }, 5000); |
| | | 5789 | | } |
| | | 5790 | | |
| | | 5791 | | // ========== Обработчик кнопки сохранения фильтра ========== |
| | | 5792 | | const saveFilterBtn = document.getElementById('saveFilterBtn'); |
| | | 5793 | | if (saveFilterBtn) { |
| | | 5794 | | saveFilterBtn.addEventListener('click', function () { |
| | | 5795 | | console.log('Кнопка сохранения фильтра нажата'); |
| | | 5796 | | const filterData = getCurrentFilterParams(); |
| | | 5797 | | console.log('Собранные параметры фильтра:', filterData); |
| | | 5798 | | |
| | | 5799 | | if (!filterData) { |
| | | 5800 | | showNotification('Нет параметров для сохранения', 'warning'); |
| | | 5801 | | return; |
| | | 5802 | | } |
| | | 5803 | | |
| | | 5804 | | const modalElement = document.getElementById('saveFilterModal'); |
| | | 5805 | | if (!modalElement) { |
| | | 5806 | | console.error('Модальное окно не найдено'); |
| | | 5807 | | return; |
| | | 5808 | | } |
| | | 5809 | | |
| | | 5810 | | // Очищаем поле ввода и скрываем ошибку |
| | | 5811 | | const filterNameInput = document.getElementById('filterNameInput'); |
| | | 5812 | | const filterNameError = document.getElementById('filterNameError'); |
| | | 5813 | | |
| | | 5814 | | if (filterNameInput) { |
| | | 5815 | | filterNameInput.value = ''; |
| | | 5816 | | filterNameInput.focus(); |
| | | 5817 | | } |
| | | 5818 | | if (filterNameError) { |
| | | 5819 | | filterNameError.style.display = 'none'; |
| | | 5820 | | } |
| | | 5821 | | |
| | | 5822 | | // Показываем модальное окно |
| | | 5823 | | try { |
| | | 5824 | | // Используем существующий экземпляр или создаем новый |
| | | 5825 | | let modal = bootstrap.Modal.getInstance(modalElement); |
| | | 5826 | | if (!modal) { |
| | | 5827 | | modal = new bootstrap.Modal(modalElement); |
| | | 5828 | | } |
| | | 5829 | | modal.show(); |
| | | 5830 | | } catch (error) { |
| | | 5831 | | console.error('Ошибка при открытии модального окна:', error); |
| | | 5832 | | // Альтернативный способ открытия |
| | | 5833 | | modalElement.style.display = 'block'; |
| | | 5834 | | modalElement.classList.add('show'); |
| | | 5835 | | document.body.classList.add('modal-open'); |
| | | 5836 | | } |
| | | 5837 | | }); |
| | | 5838 | | } else { |
| | | 5839 | | console.warn('Кнопка saveFilterBtn не найдена'); |
| | | 5840 | | } |
| | | 5841 | | |
| | | 5842 | | // ========== Обработчик подтверждения сохранения фильтра ========== |
| | | 5843 | | const confirmSaveFilterBtn = document.getElementById('confirmSaveFilterBtn'); |
| | | 5844 | | if (confirmSaveFilterBtn) { |
| | | 5845 | | confirmSaveFilterBtn.addEventListener('click', async function () { |
| | | 5846 | | const filterNameInput = document.getElementById('filterNameInput'); |
| | | 5847 | | const filterName = filterNameInput ? filterNameInput.value.trim() : ''; |
| | | 5848 | | const errorElement = document.getElementById('filterNameError'); |
| | | 5849 | | |
| | | 5850 | | if (!filterName) { |
| | | 5851 | | if (errorElement) { |
| | | 5852 | | errorElement.textContent = 'Введите название фильтра'; |
| | | 5853 | | errorElement.style.display = 'block'; |
| | | 5854 | | filterNameInput?.focus(); |
| | | 5855 | | } |
| | | 5856 | | return; |
| | | 5857 | | } |
| | | 5858 | | |
| | | 5859 | | if (errorElement) { |
| | | 5860 | | errorElement.style.display = 'none'; |
| | | 5861 | | } |
| | | 5862 | | |
| | | 5863 | | const filterData = getCurrentFilterParams(); |
| | | 5864 | | console.log('Сохранение фильтра:', {Name: filterName, FilterData: filterData}); |
| | | 5865 | | |
| | | 5866 | | try { |
| | | 5867 | | const saveUrl = '/Studio2/BitrixStock/SaveFilter'; |
| | | 5868 | | console.log('URL для сохранения:', saveUrl); |
| | | 5869 | | |
| | | 5870 | | // Получаем CSRF токен, если используется |
| | | 5871 | | const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value; |
| | | 5872 | | |
| | | 5873 | | const headers = { |
| | | 5874 | | 'Content-Type': 'application/json', |
| | | 5875 | | }; |
| | | 5876 | | |
| | | 5877 | | if (token) { |
| | | 5878 | | headers['RequestVerificationToken'] = token; |
| | | 5879 | | } |
| | | 5880 | | |
| | | 5881 | | const response = await fetch(saveUrl, { |
| | | 5882 | | method: 'POST', |
| | | 5883 | | headers: headers, |
| | | 5884 | | body: JSON.stringify({ |
| | | 5885 | | Name: filterName, |
| | | 5886 | | FilterData: filterData || '' |
| | | 5887 | | }) |
| | | 5888 | | }); |
| | | 5889 | | |
| | | 5890 | | console.log('Ответ сервера:', response.status, response.statusText); |
| | | 5891 | | |
| | | 5892 | | if (!response.ok) { |
| | | 5893 | | const errorText = await response.text(); |
| | | 5894 | | console.error('Ошибка ответа:', errorText); |
| | | 5895 | | showNotification('Ошибка сервера: ' + response.status, 'danger'); |
| | | 5896 | | return; |
| | | 5897 | | } |
| | | 5898 | | |
| | | 5899 | | const result = await response.json(); |
| | | 5900 | | console.log('Результат сохранения:', result); |
| | | 5901 | | |
| | | 5902 | | if (result.success) { |
| | | 5903 | | showNotification(result.message || 'Фильтр успешно сохранен', 'success'); |
| | | 5904 | | |
| | | 5905 | | // Закрываем модальное окно |
| | | 5906 | | const modalElement = document.getElementById('saveFilterModal'); |
| | | 5907 | | if (modalElement) { |
| | | 5908 | | const modal = bootstrap.Modal.getInstance(modalElement); |
| | | 5909 | | if (modal) { |
| | | 5910 | | modal.hide(); |
| | | 5911 | | } |
| | | 5912 | | } |
| | | 5913 | | |
| | | 5914 | | // Обновляем список сохраненных фильтров |
| | | 5915 | | setTimeout(() => { |
| | | 5916 | | window.location.reload(); |
| | | 5917 | | }, 1000); |
| | | 5918 | | } else { |
| | | 5919 | | showNotification(result.message || 'Ошибка при сохранении фильтра', 'danger'); |
| | | 5920 | | } |
| | | 5921 | | } catch (error) { |
| | | 5922 | | console.error('Ошибка при сохранении фильтра:', error); |
| | | 5923 | | showNotification('Ошибка при сохранении фильтра: ' + error.message, 'danger'); |
| | | 5924 | | } |
| | | 5925 | | }); |
| | | 5926 | | } else { |
| | | 5927 | | console.warn('Кнопка confirmSaveFilterBtn не найдена'); |
| | | 5928 | | } |
| | | 5929 | | |
| | | 5930 | | // ========== Обработчик Enter в поле имени фильтра ========== |
| | | 5931 | | const filterNameInput = document.getElementById('filterNameInput'); |
| | | 5932 | | if (filterNameInput) { |
| | | 5933 | | filterNameInput.addEventListener('keypress', function (e) { |
| | | 5934 | | if (e.key === 'Enter') { |
| | | 5935 | | e.preventDefault(); |
| | | 5936 | | const confirmBtn = document.getElementById('confirmSaveFilterBtn'); |
| | | 5937 | | if (confirmBtn) { |
| | | 5938 | | confirmBtn.click(); |
| | | 5939 | | } |
| | | 5940 | | } |
| | | 5941 | | }); |
| | | 5942 | | } |
| | | 5943 | | |
| | | 5944 | | // ========== Обработчики для работы с сохраненными фильтрами ========== |
| | | 5945 | | const savedFiltersSelect = document.getElementById('savedFiltersSelect'); |
| | | 5946 | | const applyFilterBtn = document.getElementById('applyFilterBtn'); |
| | | 5947 | | const deleteFilterBtn = document.getElementById('deleteFilterBtn'); |
| | | 5948 | | |
| | | 5949 | | if (savedFiltersSelect && applyFilterBtn && deleteFilterBtn) { |
| | | 5950 | | // Инициализируем состояние кнопок |
| | | 5951 | | const hasSelection = savedFiltersSelect.value !== ''; |
| | | 5952 | | applyFilterBtn.disabled = !hasSelection; |
| | | 5953 | | deleteFilterBtn.disabled = !hasSelection; |
| | | 5954 | | |
| | | 5955 | | // Обработчик изменения выбранного фильтра |
| | | 5956 | | savedFiltersSelect.addEventListener('change', function () { |
| | | 5957 | | const hasSelection = this.value !== ''; |
| | | 5958 | | applyFilterBtn.disabled = !hasSelection; |
| | | 5959 | | deleteFilterBtn.disabled = !hasSelection; |
| | | 5960 | | }); |
| | | 5961 | | |
| | | 5962 | | // Обработчик применения фильтра |
| | | 5963 | | applyFilterBtn.addEventListener('click', function () { |
| | | 5964 | | const selectedOption = savedFiltersSelect.options[savedFiltersSelect.selectedIndex]; |
| | | 5965 | | let filterData = selectedOption.getAttribute('data-filter-data'); |
| | | 5966 | | |
| | | 5967 | | if (!filterData) { |
| | | 5968 | | showNotification('Ошибка: данные фильтра не найдены', 'danger'); |
| | | 5969 | | return; |
| | | 5970 | | } |
| | | 5971 | | |
| | | 5972 | | // Исправляем HTML-кодирование (& -> &), чтобы параметры корректно передавались в URL |
| | | 5973 | | filterData = filterData.replace(/&/g, '&'); |
| | | 5974 | | |
| | | 5975 | | // Перенаправляем на страницу с параметрами фильтра |
| | | 5976 | | const baseUrl = '/Studio2/BitrixStock'; |
| | | 5977 | | const url = filterData ? `${baseUrl}?${filterData}` : baseUrl; |
| | | 5978 | | window.location.href = url; |
| | | 5979 | | }); |
| | | 5980 | | |
| | | 5981 | | // Обработчик удаления фильтра |
| | | 5982 | | deleteFilterBtn.addEventListener('click', async function () { |
| | | 5983 | | const filterId = savedFiltersSelect.value; |
| | | 5984 | | |
| | | 5985 | | if (!filterId) { |
| | | 5986 | | showNotification('Выберите фильтр для удаления', 'warning'); |
| | | 5987 | | return; |
| | | 5988 | | } |
| | | 5989 | | |
| | | 5990 | | const selectedOption = savedFiltersSelect.options[savedFiltersSelect.selectedIndex]; |
| | | 5991 | | const filterName = selectedOption.text; |
| | | 5992 | | |
| | | 5993 | | if (!(await St2Dialog.confirm(`Вы уверены, что хотите удалить фильтр "${filterName}"?`, { variant: 'dang |
| | | 5994 | | return; |
| | | 5995 | | } |
| | | 5996 | | |
| | | 5997 | | try { |
| | | 5998 | | const deleteUrl = `/Studio2/BitrixStock/DeleteFilter?id=${filterId}`; |
| | | 5999 | | |
| | | 6000 | | // Получаем CSRF токен, если используется |
| | | 6001 | | const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value; |
| | | 6002 | | |
| | | 6003 | | const headers = { |
| | | 6004 | | 'Content-Type': 'application/json', |
| | | 6005 | | }; |
| | | 6006 | | |
| | | 6007 | | if (token) { |
| | | 6008 | | headers['RequestVerificationToken'] = token; |
| | | 6009 | | } |
| | | 6010 | | |
| | | 6011 | | const response = await fetch(deleteUrl, { |
| | | 6012 | | method: 'POST', |
| | | 6013 | | headers: headers |
| | | 6014 | | }); |
| | | 6015 | | |
| | | 6016 | | const result = await response.json(); |
| | | 6017 | | |
| | | 6018 | | if (result.success) { |
| | | 6019 | | showNotification(result.message || 'Фильтр успешно удален', 'success'); |
| | | 6020 | | |
| | | 6021 | | // Обновляем список сохраненных фильтров |
| | | 6022 | | setTimeout(() => { |
| | | 6023 | | window.location.reload(); |
| | | 6024 | | }, 1000); |
| | | 6025 | | } else { |
| | | 6026 | | showNotification(result.message || 'Ошибка при удалении фильтра', 'danger'); |
| | | 6027 | | } |
| | | 6028 | | } catch (error) { |
| | | 6029 | | console.error('Ошибка при удалении фильтра:', error); |
| | | 6030 | | showNotification('Ошибка при удалении фильтра: ' + error.message, 'danger'); |
| | | 6031 | | } |
| | | 6032 | | }); |
| | | 6033 | | } else { |
| | | 6034 | | console.warn('Элементы для работы с сохраненными фильтрами не найдены'); |
| | | 6035 | | } |
| | | 6036 | | |
| | | 6037 | | // ========== Обработчик закрытия модального окна по ESC ========== |
| | | 6038 | | document.addEventListener('keydown', function (e) { |
| | | 6039 | | if (e.key === 'Escape') { |
| | | 6040 | | const modalElement = document.getElementById('saveFilterModal'); |
| | | 6041 | | if (modalElement && modalElement.classList.contains('show')) { |
| | | 6042 | | const modal = bootstrap.Modal.getInstance(modalElement); |
| | | 6043 | | if (modal) { |
| | | 6044 | | modal.hide(); |
| | | 6045 | | } |
| | | 6046 | | } |
| | | 6047 | | } |
| | | 6048 | | }); |
| | | 6049 | | |
| | | 6050 | | console.log('Функционал фильтров инициализирован'); |
| | | 6051 | | }); |
| | | 6052 | | |
| | | 6053 | | // Экспорт функций для использования в других скриптах (если нужно) |
| | | 6054 | | if (typeof module !== 'undefined' && module.exports) { |
| | | 6055 | | module.exports = { |
| | | 6056 | | getCurrentFilterParams, |
| | | 6057 | | showNotification |
| | | 6058 | | }; |
| | | 6059 | | } |
| | | 6060 | | </script> |
| | | 6061 | | } |
| | | 6062 | | |
| | | 6063 | | @functions { |
| | | 6064 | | |
| | | 6065 | | |
| | | 6066 | | } |