| | | 1 | | @model List<OzonDomains.Models.OrderHistory> |
| | | 2 | | |
| | | 3 | | @{ |
| | 0 | 4 | | ViewData["Title"] = "История изменений заказов"; |
| | 0 | 5 | | Layout = "~/Views/Shared/_Layout.cshtml"; |
| | | 6 | | } |
| | | 7 | | |
| | | 8 | | <style> |
| | | 9 | | .card { |
| | | 10 | | border: none; |
| | | 11 | | box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); |
| | | 12 | | border-radius: 0.5rem; |
| | | 13 | | margin-bottom: 1.5rem; |
| | | 14 | | } |
| | | 15 | | |
| | | 16 | | .card-header { |
| | | 17 | | background-color: #f8f9fa; |
| | | 18 | | border-bottom: 1px solid #e9ecef; |
| | | 19 | | padding: 1.25rem 1.5rem; |
| | | 20 | | display: flex; |
| | | 21 | | justify-content: space-between; |
| | | 22 | | align-items: center; |
| | | 23 | | } |
| | | 24 | | |
| | | 25 | | .card-body { |
| | | 26 | | padding: 0; |
| | | 27 | | } |
| | | 28 | | |
| | | 29 | | .table th { |
| | | 30 | | border-top: none; |
| | | 31 | | font-weight: 600; |
| | | 32 | | color: #495057; |
| | | 33 | | font-size: 0.9rem; |
| | | 34 | | padding: 1rem; |
| | | 35 | | background-color: #f8f9fa; |
| | | 36 | | } |
| | | 37 | | |
| | | 38 | | .table td { |
| | | 39 | | padding: 1rem; |
| | | 40 | | vertical-align: middle; |
| | | 41 | | font-size: 0.9rem; |
| | | 42 | | } |
| | | 43 | | |
| | | 44 | | .btn-primary { |
| | | 45 | | background-color: #007bff; |
| | | 46 | | border-color: #007bff; |
| | | 47 | | padding: 0.5rem 1rem; |
| | | 48 | | font-size: 0.9rem; |
| | | 49 | | } |
| | | 50 | | |
| | | 51 | | .btn-outline-secondary { |
| | | 52 | | border-color: #6c757d; |
| | | 53 | | color: #6c757d; |
| | | 54 | | padding: 0.5rem 1rem; |
| | | 55 | | font-size: 0.9rem; |
| | | 56 | | } |
| | | 57 | | |
| | | 58 | | .table-hover tbody tr:hover { |
| | | 59 | | background-color: #f8f9fa; |
| | | 60 | | } |
| | | 61 | | |
| | | 62 | | .text-muted-small { |
| | | 63 | | font-size: 0.8rem; |
| | | 64 | | color: #6c757d; |
| | | 65 | | } |
| | | 66 | | |
| | | 67 | | .action-buttons { |
| | | 68 | | display: flex; |
| | | 69 | | gap: 0.5rem; |
| | | 70 | | align-items: center; |
| | | 71 | | } |
| | | 72 | | |
| | | 73 | | .spinner-border-sm { |
| | | 74 | | width: 1rem; |
| | | 75 | | height: 1rem; |
| | | 76 | | } |
| | | 77 | | |
| | | 78 | | .status-indicator { |
| | | 79 | | display: inline-flex; |
| | | 80 | | align-items: center; |
| | | 81 | | gap: 0.5rem; |
| | | 82 | | font-size: 0.8rem; |
| | | 83 | | } |
| | | 84 | | |
| | | 85 | | .status-connected { |
| | | 86 | | color: #28a745; |
| | | 87 | | } |
| | | 88 | | |
| | | 89 | | .status-disconnected { |
| | | 90 | | color: #dc3545; |
| | | 91 | | } |
| | | 92 | | |
| | | 93 | | .status-connecting { |
| | | 94 | | color: #ffc107; |
| | | 95 | | } |
| | | 96 | | |
| | | 97 | | .history-container { |
| | | 98 | | max-height: 600px; |
| | | 99 | | overflow-y: auto; |
| | | 100 | | } |
| | | 101 | | </style> |
| | | 102 | | |
| | | 103 | | <link rel="stylesheet" href="~/css/Studio2/shared/legacy-directory.css" asp-append-version="true"/> |
| | | 104 | | |
| | | 105 | | <div class="container py-3 ld-page"> |
| | | 106 | | <div class="row"> |
| | | 107 | | <div class="col-12"> |
| | | 108 | | <div class="app-section-hero app-section-hero--import d-flex flex-wrap justify-content-between align-items-s |
| | | 109 | | <div> |
| | | 110 | | <h1><i class="fas fa-clock-rotate-left me-2 text-primary"></i>История изменений заказов</h1> |
| | | 111 | | <div class="subtitle">Журнал изменений заказов в реальном времени</div> |
| | | 112 | | <div class="subtitle-help">Наблюдайте за полями, авторами и временем изменений; обновление не переза |
| | | 113 | | <div class="app-section-hero__quick-links"><a asp-area="Studio2" asp-controller="Orders" asp-action= |
| | | 114 | | </div> |
| | | 115 | | <div id="connectionStatus" class="status-indicator status-connecting"><i class="fas fa-circle"></i><span |
| | | 116 | | </div> |
| | | 117 | | <div class="card ld-card"> |
| | | 118 | | <div class="card-header"> |
| | | 119 | | <span class="fw-semibold">Последние изменения</span> |
| | | 120 | | <div class="action-buttons"> |
| | | 121 | | <button id="refreshBtn" class="btn btn-primary"> |
| | | 122 | | <span id="buttonText">Обновить</span> |
| | | 123 | | <span id="spinner" class="spinner-border spinner-border-sm visually-hidden" role="status"></ |
| | | 124 | | </button> |
| | | 125 | | </div> |
| | | 126 | | </div> |
| | | 127 | | <div class="card-body"> |
| | | 128 | | <div class="history-container"> |
| | | 129 | | <div class="table-responsive"> |
| | | 130 | | <table class="table table-hover"> |
| | | 131 | | <thead> |
| | | 132 | | <tr> |
| | | 133 | | <th width="15%">Время изменения</th> |
| | | 134 | | <th width="10%">ID заказа</th> |
| | | 135 | | <th width="15%">Измененное поле</th> |
| | | 136 | | <th width="20%">Старое значение</th> |
| | | 137 | | <th width="20%">Новое значение</th> |
| | | 138 | | <th width="15%">Автор изменения</th> |
| | | 139 | | </tr> |
| | | 140 | | </thead> |
| | | 141 | | <tbody id="historyTableBody"> |
| | 0 | 142 | | @foreach (var item in Model) |
| | | 143 | | { |
| | | 144 | | <tr> |
| | | 145 | | <td> |
| | 0 | 146 | | <span class="text-muted-small">@item.FormattedChangedAt</span> |
| | | 147 | | </td> |
| | | 148 | | <td> |
| | 0 | 149 | | <strong>@item.OrderId</strong> |
| | | 150 | | </td> |
| | | 151 | | <td> |
| | 0 | 152 | | <span class="text-muted-small">@item.ColumnDisplayName</span> |
| | | 153 | | </td> |
| | | 154 | | <td> |
| | 0 | 155 | | <span class="text-muted">@item.FormattedOldValue</span> |
| | | 156 | | </td> |
| | | 157 | | <td> |
| | 0 | 158 | | <span class="text-success">@item.FormattedNewValue</span> |
| | | 159 | | </td> |
| | | 160 | | <td> |
| | 0 | 161 | | <span class="text-muted-small">@item.ChangedBy</span> |
| | | 162 | | </td> |
| | | 163 | | </tr> |
| | | 164 | | } |
| | | 165 | | </tbody> |
| | | 166 | | </table> |
| | | 167 | | </div> |
| | | 168 | | </div> |
| | | 169 | | |
| | | 170 | | <!-- Информация о записях --> |
| | | 171 | | <div class="text-center text-muted mt-3 mx-3"> |
| | | 172 | | <small> |
| | 0 | 173 | | Показано <span id="recordCount">@Model.Count</span> последних изменений |
| | | 174 | | <span id="lastUpdate" class="ms-2"></span> |
| | | 175 | | </small> |
| | | 176 | | </div> |
| | | 177 | | </div> |
| | | 178 | | </div> |
| | | 179 | | </div> |
| | | 180 | | </div> |
| | | 181 | | </div> |
| | | 182 | | |
| | | 183 | | @section Scripts { |
| | | 184 | | <script src="~/lib/microsoft-signalr/signalr.min.js" asp-append-version="true"></script> |
| | | 185 | | <script> |
| | | 186 | | document.addEventListener('DOMContentLoaded', () => { |
| | | 187 | | // Элементы интерфейса |
| | | 188 | | const connectionStatus = document.getElementById('connectionStatus'); |
| | | 189 | | const refreshBtn = document.getElementById('refreshBtn'); |
| | | 190 | | const spinner = document.getElementById('spinner'); |
| | | 191 | | const buttonText = document.getElementById('buttonText'); |
| | | 192 | | const recordCount = document.getElementById('recordCount'); |
| | | 193 | | const lastUpdate = document.getElementById('lastUpdate'); |
| | | 194 | | |
| | | 195 | | // Функция обновления статуса подключения |
| | | 196 | | function updateConnectionStatus(status, message) { |
| | | 197 | | const statusIcon = connectionStatus.querySelector('i'); |
| | | 198 | | const statusText = connectionStatus.querySelector('span:last-child'); |
| | | 199 | | |
| | | 200 | | connectionStatus.className = `status-indicator status-${status}`; |
| | | 201 | | statusText.textContent = message; |
| | | 202 | | |
| | | 203 | | switch(status) { |
| | | 204 | | case 'connected': |
| | | 205 | | statusIcon.className = 'fas fa-circle'; |
| | | 206 | | break; |
| | | 207 | | case 'disconnected': |
| | | 208 | | statusIcon.className = 'fas fa-circle'; |
| | | 209 | | break; |
| | | 210 | | case 'connecting': |
| | | 211 | | statusIcon.className = 'fas fa-circle'; |
| | | 212 | | break; |
| | | 213 | | case 'reconnecting': |
| | | 214 | | statusIcon.className = 'fas fa-sync-alt fa-spin'; |
| | | 215 | | break; |
| | | 216 | | } |
| | | 217 | | } |
| | | 218 | | |
| | | 219 | | // Функция обновления времени последнего обновления |
| | | 220 | | function updateLastUpdateTime() { |
| | | 221 | | const now = new Date(); |
| | | 222 | | lastUpdate.textContent = `Последнее обновление: ${now.toLocaleTimeString('ru-RU')}`; |
| | | 223 | | } |
| | | 224 | | |
| | | 225 | | // Функция для форматирования числовых значений |
| | | 226 | | function formatNumericValue(value) { |
| | | 227 | | if (!value) return ''; |
| | | 228 | | |
| | | 229 | | const number = parseFloat(value); |
| | | 230 | | if (!isNaN(number)) { |
| | | 231 | | return number.toFixed(2).replace(/\.?0+$/, ''); |
| | | 232 | | } |
| | | 233 | | |
| | | 234 | | return value; |
| | | 235 | | } |
| | | 236 | | |
| | | 237 | | // Функция для обновления таблицы |
| | | 238 | | function updateTable() { |
| | | 239 | | refreshBtn.disabled = true; |
| | | 240 | | spinner.classList.remove('visually-hidden'); |
| | | 241 | | buttonText.textContent = 'Обновление...'; |
| | | 242 | | |
| | | 243 | | connection.invoke('RequestRecentChanges') |
| | | 244 | | .catch(err => { |
| | | 245 | | console.error('Ошибка запроса обновлений:', err); |
| | | 246 | | restoreButtonState(); |
| | | 247 | | }); |
| | | 248 | | } |
| | | 249 | | |
| | | 250 | | function restoreButtonState() { |
| | | 251 | | refreshBtn.disabled = false; |
| | | 252 | | spinner.classList.add('visually-hidden'); |
| | | 253 | | buttonText.textContent = 'Обновить'; |
| | | 254 | | } |
| | | 255 | | |
| | | 256 | | // SignalR подключение |
| | | 257 | | const connection = new signalR.HubConnectionBuilder() |
| | | 258 | | .withUrl('/orderHistoryHub') |
| | | 259 | | .withAutomaticReconnect({ |
| | | 260 | | nextRetryDelayInMilliseconds: retryContext => { |
| | | 261 | | if (retryContext.elapsedMilliseconds < 60000) { |
| | | 262 | | return 2000; |
| | | 263 | | } else { |
| | | 264 | | return 5000; |
| | | 265 | | } |
| | | 266 | | } |
| | | 267 | | }) |
| | | 268 | | .build(); |
| | | 269 | | |
| | | 270 | | // Обработчики событий подключения |
| | | 271 | | connection.onreconnecting(() => { |
| | | 272 | | updateConnectionStatus('reconnecting', 'Переподключение...'); |
| | | 273 | | }); |
| | | 274 | | |
| | | 275 | | connection.onreconnected(() => { |
| | | 276 | | updateConnectionStatus('connected', 'Подключено'); |
| | | 277 | | updateTable(); |
| | | 278 | | }); |
| | | 279 | | |
| | | 280 | | connection.onclose(() => { |
| | | 281 | | updateConnectionStatus('disconnected', 'Отключено'); |
| | | 282 | | }); |
| | | 283 | | |
| | | 284 | | // Обработчик получения изменений |
| | | 285 | | connection.on("ReceiveChange", function (change) { |
| | | 286 | | console.log('Получено новое изменение:', change); |
| | | 287 | | addChangeToTable(change); |
| | | 288 | | updateLastUpdateTime(); |
| | | 289 | | }); |
| | | 290 | | |
| | | 291 | | // Обработчик получения списка изменений |
| | | 292 | | connection.on("ReceiveRecentChanges", function (changes) { |
| | | 293 | | console.log('Получены обновления:', changes.length, 'записей'); |
| | | 294 | | updateTableWithData(changes); |
| | | 295 | | restoreButtonState(); |
| | | 296 | | updateLastUpdateTime(); |
| | | 297 | | }); |
| | | 298 | | |
| | | 299 | | // Функция добавления одной записи в таблицу |
| | | 300 | | function addChangeToTable(change) { |
| | | 301 | | const tbody = document.getElementById('historyTableBody'); |
| | | 302 | | if (!tbody) return; |
| | | 303 | | |
| | | 304 | | const row = document.createElement('tr'); |
| | | 305 | | |
| | | 306 | | const changedAt = new Date(change.changedAt); |
| | | 307 | | const formattedDate = changedAt.toLocaleString('ru-RU', { |
| | | 308 | | day: '2-digit', |
| | | 309 | | month: '2-digit', |
| | | 310 | | year: 'numeric', |
| | | 311 | | hour: '2-digit', |
| | | 312 | | minute: '2-digit', |
| | | 313 | | second: '2-digit' |
| | | 314 | | }); |
| | | 315 | | |
| | | 316 | | const formattedOldValue = formatNumericValue(change.oldValue); |
| | | 317 | | const formattedNewValue = formatNumericValue(change.newValue); |
| | | 318 | | |
| | | 319 | | row.innerHTML = ` |
| | | 320 | | <td><span class="text-muted-small">${formattedDate}</span></td> |
| | | 321 | | <td><strong>${change.orderId}</strong></td> |
| | | 322 | | <td><span class="text-muted-small">${change.columnDisplayName || ''}</span></td> |
| | | 323 | | <td><span class="text-muted">${formattedOldValue}</span></td> |
| | | 324 | | <td><span class="text-success">${formattedNewValue}</span></td> |
| | | 325 | | <td><span class="text-muted-small">${change.changedBy || ''}</span></td> |
| | | 326 | | `; |
| | | 327 | | |
| | | 328 | | tbody.insertBefore(row, tbody.firstChild); |
| | | 329 | | |
| | | 330 | | // Ограничиваем количество отображаемых записей |
| | | 331 | | if (tbody.children.length > 100) { |
| | | 332 | | tbody.removeChild(tbody.lastChild); |
| | | 333 | | } |
| | | 334 | | |
| | | 335 | | // Обновляем счетчик записей |
| | | 336 | | recordCount.textContent = tbody.children.length; |
| | | 337 | | } |
| | | 338 | | |
| | | 339 | | // Функция полного обновления таблицы |
| | | 340 | | function updateTableWithData(changes) { |
| | | 341 | | const tbody = document.getElementById('historyTableBody'); |
| | | 342 | | if (!tbody) return; |
| | | 343 | | |
| | | 344 | | tbody.innerHTML = ''; |
| | | 345 | | |
| | | 346 | | const sortedChanges = changes.sort((a, b) => |
| | | 347 | | new Date(b.changedAt) - new Date(a.changedAt) |
| | | 348 | | ); |
| | | 349 | | |
| | | 350 | | sortedChanges.forEach(change => { |
| | | 351 | | const row = document.createElement('tr'); |
| | | 352 | | |
| | | 353 | | const changedAt = new Date(change.changedAt); |
| | | 354 | | const formattedDate = changedAt.toLocaleString('ru-RU', { |
| | | 355 | | day: '2-digit', |
| | | 356 | | month: '2-digit', |
| | | 357 | | year: 'numeric', |
| | | 358 | | hour: '2-digit', |
| | | 359 | | minute: '2-digit', |
| | | 360 | | second: '2-digit' |
| | | 361 | | }); |
| | | 362 | | |
| | | 363 | | const formattedOldValue = formatNumericValue(change.oldValue); |
| | | 364 | | const formattedNewValue = formatNumericValue(change.newValue); |
| | | 365 | | |
| | | 366 | | row.innerHTML = ` |
| | | 367 | | <td><span class="text-muted-small">${formattedDate}</span></td> |
| | | 368 | | <td><strong>${change.orderId}</strong></td> |
| | | 369 | | <td><span class="text-muted-small">${change.columnDisplayName || ''}</span></td> |
| | | 370 | | <td><span class="text-muted">${formattedOldValue}</span></td> |
| | | 371 | | <td><span class="text-success">${formattedNewValue}</span></td> |
| | | 372 | | <td><span class="text-muted-small">${change.changedBy || ''}</span></td> |
| | | 373 | | `; |
| | | 374 | | |
| | | 375 | | tbody.appendChild(row); |
| | | 376 | | }); |
| | | 377 | | |
| | | 378 | | recordCount.textContent = changes.length; |
| | | 379 | | } |
| | | 380 | | |
| | | 381 | | // Обработчик кнопки обновления |
| | | 382 | | refreshBtn.addEventListener('click', updateTable); |
| | | 383 | | |
| | | 384 | | // Запуск соединения |
| | | 385 | | connection.start() |
| | | 386 | | .then(() => { |
| | | 387 | | updateConnectionStatus('connected', 'Подключено'); |
| | | 388 | | console.log('SignalR connected to orderHistoryHub'); |
| | | 389 | | updateTable(); |
| | | 390 | | updateLastUpdateTime(); |
| | | 391 | | }) |
| | | 392 | | .catch(err => { |
| | | 393 | | updateConnectionStatus('disconnected', 'Ошибка подключения'); |
| | | 394 | | console.error('SignalR connection error:', err.toString()); |
| | | 395 | | restoreButtonState(); |
| | | 396 | | }); |
| | | 397 | | |
| | | 398 | | // Автообновление каждые 10 секунд |
| | | 399 | | setInterval(() => { |
| | | 400 | | if (connection.state === signalR.HubConnectionState.Connected) { |
| | | 401 | | updateTable(); |
| | | 402 | | } |
| | | 403 | | }, 10000); |
| | | 404 | | }); |
| | | 405 | | </script> |
| | | 406 | | } |