| | | 1 | | @model OzonDomains.Models.Order |
| | | 2 | | @using System.Text.Json |
| | | 3 | | @{ |
| | 0 | 4 | | Layout = "~/Views/Shared/_Layout.cshtml"; |
| | 0 | 5 | | var statusColors = ViewBag.StatusColors as Dictionary<string, string> ?? new Dictionary<string, string>(); |
| | 0 | 6 | | var currentStatus = ViewBag.CurrentStatus as string ?? "Не указан"; |
| | 0 | 7 | | var currentStatusId = ViewBag.CurrentStatusId as int?; |
| | 0 | 8 | | var currentStatusColor = statusColors.TryGetValue(currentStatus, out var c) ? c : "#e9ecef"; |
| | 0 | 9 | | var order = ViewBag.Order as OzonDomains.Models.Order; |
| | 0 | 10 | | var orderTitle = order?.ShipmentNumber ?? $"Заказ #{order?.Id}"; |
| | 0 | 11 | | var transitionsJson = ViewBag.TransitionsJson as string ?? "[]"; |
| | | 12 | | } |
| | | 13 | | |
| | | 14 | | <div class="container-fluid py-3"> |
| | | 15 | | <div class="d-flex justify-content-between align-items-center mb-3"> |
| | | 16 | | <div> |
| | 0 | 17 | | <h4 class="mb-1">Статус заказа: @orderTitle</h4> |
| | | 18 | | <div class="text-muted"> |
| | | 19 | | Текущий статус: |
| | | 20 | | <span class="badge-status" |
| | | 21 | | style="background-color:@currentStatusColor; color:#fff; padding:4px 8px; border-radius:6px;"> |
| | 0 | 22 | | @currentStatus |
| | | 23 | | </span> |
| | | 24 | | </div> |
| | | 25 | | </div> |
| | | 26 | | <div> |
| | | 27 | | <a class="btn btn-outline-secondary btn-sm" asp-area="Studio2" asp-controller="Orders" asp-action="Index" |
| | | 28 | | target="_blank"> |
| | | 29 | | К списку заказов |
| | | 30 | | </a> |
| | | 31 | | </div> |
| | | 32 | | </div> |
| | | 33 | | |
| | | 34 | | <div class="graph-panel mb-4"> |
| | | 35 | | <div id="orderStatusGraph" style="height: 600px; border: 1px solid #dee2e6; border-radius: 0.25rem; background-c |
| | | 36 | | </div> |
| | | 37 | | |
| | | 38 | | <div class="alert alert-info"> |
| | | 39 | | <i class="fas fa-info-circle me-2"></i> |
| | | 40 | | Текущий статус заказа подсвечен оранжевой рамкой. Масштаб — колёсиком мыши. Нажмите на название транзакции (на л |
| | | 41 | | </div> |
| | | 42 | | </div> |
| | | 43 | | |
| | | 44 | | <style> |
| | | 45 | | .graph-panel { |
| | | 46 | | background-color: #fff; |
| | | 47 | | border: 1px solid #e9ecef; |
| | | 48 | | border-radius: 0.375rem; |
| | | 49 | | padding: 1rem; |
| | | 50 | | } |
| | | 51 | | </style> |
| | | 52 | | |
| | | 53 | | <script> |
| | 0 | 54 | | var orderStatusTransitions = @Html.Raw(transitionsJson); |
| | 0 | 55 | | var orderStatusCurrentStatus = '@Html.Raw(currentStatus?.Replace("'", "\\'"))'.trim(); |
| | 0 | 56 | | var orderStatusCurrentStatusId = @(currentStatusId.HasValue ? currentStatusId.Value.ToString() : "null"); |
| | 0 | 57 | | var orderStatusColors = @Html.Raw(JsonSerializer.Serialize(statusColors)); |
| | 0 | 58 | | var orderTitleText = '@Html.Raw(orderTitle?.Replace("'", "\\'").Replace("\r", "").Replace("\n", " "))'; |
| | 0 | 59 | | var orderIdForTransaction = @(order?.Id ?? 0); |
| | | 60 | | |
| | | 61 | | function darkenColor(color, percent) { |
| | | 62 | | if (!color || !color.startsWith('#')) return color; |
| | | 63 | | var num = parseInt(color.replace('#', ''), 16); |
| | | 64 | | var amt = Math.round(2.55 * percent); |
| | | 65 | | var R = Math.max(0, (num >> 16) - amt); |
| | | 66 | | var G = Math.max(0, (num >> 8 & 0x00FF) - amt); |
| | | 67 | | var B = Math.max(0, (num & 0x0000FF) - amt); |
| | | 68 | | return '#' + (0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1); |
| | | 69 | | } |
| | | 70 | | function isLightColor(color) { |
| | | 71 | | if (!color || !color.startsWith('#') || color.length < 7) return true; |
| | | 72 | | var hex = color.replace('#', ''); |
| | | 73 | | var r = parseInt(hex.substr(0, 2), 16); |
| | | 74 | | var g = parseInt(hex.substr(2, 2), 16); |
| | | 75 | | var b = parseInt(hex.substr(4, 2), 16); |
| | | 76 | | return ((r * 299) + (g * 587) + (b * 114)) / 1000 > 155; |
| | | 77 | | } |
| | | 78 | | function getEdgeColorFromCssClass(cssClass) { |
| | | 79 | | var map = { 'link-success': '#198754', 'link-secondary': '#6c757d', 'link-info': '#0dcaf0', 'link-warning': '#ff |
| | | 80 | | return map[cssClass] || '#6c757d'; |
| | | 81 | | } |
| | | 82 | | function normalizeStatus(s) { |
| | | 83 | | if (!s) return ''; |
| | | 84 | | var t = s.trim().toLowerCase().replace(/\s+/g, ' '); |
| | | 85 | | if (t === 'принятнаскладе') return 'принят на складе'; |
| | | 86 | | return t; |
| | | 87 | | } |
| | | 88 | | var transactionTypeToUrl = { |
| | | 89 | | 0: '/Studio2/OrderToSupplierTransaction/CreateOrderToSupplierTransactionV2', |
| | | 90 | | 2: '/Studio2/ShippedBySupplierTransaction/CreateShippedBySupplierTransaction', |
| | | 91 | | 3: '/Studio2/ShippedToSellerTransaction/CreateShippedToSellerTransaction', |
| | | 92 | | 4: '/Studio2/OrderedToSellerTransaction/CreateOrderedToSellerTransaction', |
| | | 93 | | 6: '/Studio2/ShippedToClientTransaction/CreateShippedToClientTransaction', |
| | | 94 | | 9: '/Studio2/AcceptedToWarehouseTransaction/AcceptedToWarehouseTransaction', |
| | | 95 | | 10: '/Studio2/ToCancellationTransaction/CreateToCancellationTransaction', |
| | | 96 | | 11: '/Studio2/CancelledTransaction/CreateCancelledTransaction', |
| | | 97 | | 12: '/Studio2/CustomerReturnTransaction/CreateCustomerReturnTransaction', |
| | | 98 | | 13: '/Studio2/WarehouseChangeTransaction/CreateWarehouseChangeTransaction', |
| | | 99 | | 14: '/Studio2/ReturnToSupplierTransaction/CreateReturnToSupplierTransaction', |
| | | 100 | | 15: '/Studio2/ReturnToWarehouseTransaction/CreateReturnToWarehouseTransaction', |
| | | 101 | | 16: '/Studio2/WarehouseTransferTransaction/CreateWarehouseTransferTransaction', |
| | | 102 | | 17: '/Studio2/ShippedToCommissionerTransaction/CreateShippedToCommissionerTransaction' |
| | | 103 | | }; |
| | | 104 | | function getTransactionUrl(transactionType, orderId) { |
| | | 105 | | var path = transactionTypeToUrl[transactionType]; |
| | | 106 | | if (!path) return null; |
| | | 107 | | return path + '?ids=' + orderId + '&page=1'; |
| | | 108 | | } |
| | | 109 | | |
| | | 110 | | function initializeOrderStatusGraph() { |
| | | 111 | | if (typeof vis === 'undefined') { |
| | | 112 | | document.getElementById('orderStatusGraph').innerHTML = '<div style="padding: 2rem; text-align: center; colo |
| | | 113 | | return; |
| | | 114 | | } |
| | | 115 | | var transitions = orderStatusTransitions || []; |
| | | 116 | | var statusMap = {}; |
| | | 117 | | transitions.forEach(function(t) { |
| | | 118 | | if (!statusMap[t.fromStatusId]) { |
| | | 119 | | statusMap[t.fromStatusId] = { id: t.fromStatusId, label: t.fromStatusName, color: t.fromStatusColor || o |
| | | 120 | | } |
| | | 121 | | if (!statusMap[t.toStatusId]) { |
| | | 122 | | statusMap[t.toStatusId] = { id: t.toStatusId, label: t.toStatusName, color: t.toStatusColor || orderStat |
| | | 123 | | } |
| | | 124 | | }); |
| | | 125 | | if (transitions.length === 0 && Object.keys(orderStatusColors).length > 0) { |
| | | 126 | | var id = 1; |
| | | 127 | | Object.keys(orderStatusColors).forEach(function(name) { |
| | | 128 | | statusMap[id] = { id: id, label: name, color: orderStatusColors[name] || '#e9ecef' }; |
| | | 129 | | id++; |
| | | 130 | | }); |
| | | 131 | | } |
| | | 132 | | var nodeList = Object.keys(statusMap).map(function(k) { return statusMap[k]; }); |
| | | 133 | | var normalizedCurrent = normalizeStatus(orderStatusCurrentStatus); |
| | | 134 | | var currentNodeId = orderStatusCurrentStatusId; |
| | | 135 | | if (currentNodeId == null && normalizedCurrent) { |
| | | 136 | | var found = nodeList.find(function(s) { return normalizeStatus(s.label) === normalizedCurrent; }); |
| | | 137 | | if (found) currentNodeId = found.id; |
| | | 138 | | } |
| | | 139 | | var graphNodes = new vis.DataSet(nodeList.map(function(s) { |
| | | 140 | | var isCurrent = (currentNodeId != null && s.id === currentNodeId); |
| | | 141 | | var color = s.color; |
| | | 142 | | return { |
| | | 143 | | id: s.id, |
| | | 144 | | label: s.label + (isCurrent ? '\n(' + orderTitleText + ')' : ''), |
| | | 145 | | color: { |
| | | 146 | | background: color, |
| | | 147 | | border: isCurrent ? '#ff6b35' : darkenColor(color, 20), |
| | | 148 | | highlight: { background: color, border: isCurrent ? '#ff6b35' : darkenColor(color, 40) } |
| | | 149 | | }, |
| | | 150 | | font: { color: isLightColor(color) ? '#000' : '#fff', size: 14, face: 'Arial' }, |
| | | 151 | | shape: 'box', |
| | | 152 | | margin: 10, |
| | | 153 | | borderWidth: isCurrent ? 6 : 2 |
| | | 154 | | }; |
| | | 155 | | })); |
| | | 156 | | var edgesData = transitions.map(function(t, i) { |
| | | 157 | | return { |
| | | 158 | | id: i, |
| | | 159 | | from: t.fromStatusId, |
| | | 160 | | to: t.toStatusId, |
| | | 161 | | label: t.displayName, |
| | | 162 | | transactionType: t.transactionType != null ? t.transactionType : -1, |
| | | 163 | | color: { color: getEdgeColorFromCssClass(t.transactionCssClass), opacity: 1 }, |
| | | 164 | | arrows: { to: { enabled: true, scaleFactor: 0.8 } }, |
| | | 165 | | font: { size: 11, color: '#495057' }, |
| | | 166 | | smooth: { enabled: true, type: 'curvedCW', roundness: 0.2 } |
| | | 167 | | }; |
| | | 168 | | }); |
| | | 169 | | var graphEdges = new vis.DataSet(edgesData); |
| | | 170 | | var container = document.getElementById('orderStatusGraph'); |
| | | 171 | | var data = { nodes: graphNodes, edges: graphEdges }; |
| | | 172 | | var options = { |
| | | 173 | | layout: { randomSeed: 1 }, |
| | | 174 | | physics: { |
| | | 175 | | enabled: true, |
| | | 176 | | stabilization: { iterations: 200, updateInterval: 25, onlyDynamicEdges: false, fit: true }, |
| | | 177 | | barnesHut: { |
| | | 178 | | gravitationalConstant: -1400, |
| | | 179 | | centralGravity: 0.35, |
| | | 180 | | springLength: 110, |
| | | 181 | | springConstant: 0.06, |
| | | 182 | | damping: 0.25, |
| | | 183 | | avoidOverlap: 0.9 |
| | | 184 | | } |
| | | 185 | | }, |
| | | 186 | | interaction: { |
| | | 187 | | hover: true, |
| | | 188 | | tooltipDelay: 100, |
| | | 189 | | zoomView: true, |
| | | 190 | | dragView: false, |
| | | 191 | | dragNodes: false, |
| | | 192 | | zoomSpeed: 1.2 |
| | | 193 | | }, |
| | | 194 | | edges: { smooth: { enabled: true, type: 'cubicBezier', roundness: 0.25 } } |
| | | 195 | | }; |
| | | 196 | | var network = new vis.Network(container, data, options); |
| | | 197 | | network.on('click', function(params) { |
| | | 198 | | if (params.edges.length > 0 && orderIdForTransaction > 0) { |
| | | 199 | | var edgeId = params.edges[0]; |
| | | 200 | | var edgeData = edgesData[edgeId]; |
| | | 201 | | if (edgeData && edgeData.transactionType >= 0) { |
| | | 202 | | var url = getTransactionUrl(edgeData.transactionType, orderIdForTransaction); |
| | | 203 | | if (url) window.location.href = url; |
| | | 204 | | } |
| | | 205 | | } |
| | | 206 | | }); |
| | | 207 | | network.on('hoverEdge', function() { container.style.cursor = 'pointer'; }); |
| | | 208 | | network.on('blurEdge', function() { container.style.cursor = 'default'; }); |
| | | 209 | | network.once('stabilizationIterationsDone', function() { |
| | | 210 | | network.setOptions({ physics: { enabled: false } }); |
| | | 211 | | network.fit({ animation: { duration: 400, easingFunction: 'easeInOutQuad' } }); |
| | | 212 | | if (currentNodeId != null) { |
| | | 213 | | network.focus(currentNodeId, { scale: 1.1, animation: { duration: 400, easingFunction: 'easeInOutQuad' } |
| | | 214 | | } |
| | | 215 | | }); |
| | | 216 | | } |
| | | 217 | | </script> |
| | | 218 | | |
| | | 219 | | @section Scripts { |
| | | 220 | | <link href="https://cdn.jsdelivr.net/npm/vis-network@9.1.9/dist/dist/vis-network.min.css" rel="stylesheet" type="tex |
| | | 221 | | <script> |
| | | 222 | | (function() { |
| | | 223 | | var link = document.createElement('link'); |
| | | 224 | | link.rel = 'stylesheet'; |
| | | 225 | | link.href = 'https://cdn.jsdelivr.net/npm/vis-network@9.1.9/dist/dist/vis-network.min.css'; |
| | | 226 | | link.type = 'text/css'; |
| | | 227 | | document.head.appendChild(link); |
| | | 228 | | var script = document.createElement('script'); |
| | | 229 | | script.src = 'https://cdn.jsdelivr.net/npm/vis-network@9.1.9/standalone/umd/vis-network.min.js'; |
| | | 230 | | script.async = false; |
| | | 231 | | script.onload = function() { |
| | | 232 | | if (typeof vis !== 'undefined' && typeof initializeOrderStatusGraph === 'function') { |
| | | 233 | | initializeOrderStatusGraph(); |
| | | 234 | | } |
| | | 235 | | }; |
| | | 236 | | script.onerror = function() { |
| | | 237 | | var el = document.getElementById('orderStatusGraph'); |
| | | 238 | | if (el) el.innerHTML = '<div style="padding: 2rem; text-align: center; color: #dc3545;"><i class="fas fa |
| | | 239 | | }; |
| | | 240 | | document.head.appendChild(script); |
| | | 241 | | })(); |
| | | 242 | | </script> |
| | | 243 | | } |