| | | 1 | | @using OzonDomains |
| | | 2 | | @using OzonDomains.Models |
| | | 3 | | @using OzonOrdersWeb.Areas.Studio2.ViewModels.Grid |
| | | 4 | | @model List<StatusTransition> |
| | | 5 | | @{ |
| | 0 | 6 | | ViewData["Title"] = "Переходы между статусами"; |
| | 0 | 7 | | Layout = "~/Views/Shared/_Layout.cshtml"; |
| | 0 | 8 | | var rows = Model.Select(t => new |
| | 0 | 9 | | { |
| | 0 | 10 | | id = t.Id, |
| | 0 | 11 | | displayOrder = t.DisplayOrder, |
| | 0 | 12 | | fromName = t.FromStatus?.Name ?? "—", |
| | 0 | 13 | | fromColor = t.FromStatus?.GetStatusColor(), |
| | 0 | 14 | | fromFinal = t.FromStatus?.IsFinal == true, |
| | 0 | 15 | | toName = t.ToStatus?.Name ?? "—", |
| | 0 | 16 | | toColor = t.ToStatus?.GetStatusColor(), |
| | 0 | 17 | | toFinal = t.ToStatus?.IsFinal == true, |
| | 0 | 18 | | transactionType = t.TransactionType.GetDisplayName(), |
| | 0 | 19 | | transactionIcon = t.TransactionType.GetTransactionTypeIcon(), |
| | 0 | 20 | | transactionCss = t.TransactionType.GetTransactionTypeCssClass(), |
| | 0 | 21 | | displayName = t.DisplayName, |
| | 0 | 22 | | isActive = t.IsActive |
| | 0 | 23 | | }); |
| | | 24 | | } |
| | | 25 | | |
| | 0 | 26 | | @await Html.PartialAsync("_BsxGridShell") |
| | | 27 | | <link rel="stylesheet" href="~/css/Studio2/StatusTransition/status-transition-index.css" asp-append-version="true"/> |
| | | 28 | | |
| | | 29 | | <div class="container-fluid py-3 st-page bsx-surface"> |
| | | 30 | | <div class="app-section-hero app-section-hero--import d-flex flex-wrap justify-content-between align-items-start gap-3"> |
| | | 31 | | <div> |
| | | 32 | | <h1><i class="fas fa-project-diagram me-2 text-primary"></i>Переходы между статусами</h1> |
| | | 33 | | <div class="subtitle">Правила смены статуса заказа при выполнении транзакций — граф и справочник записей</div> |
| | | 34 | | <div class="subtitle-help"> |
| | | 35 | | Каждый переход связывает <strong>тип транзакции</strong> с парой статусов «из → в». |
| | | 36 | | Система использует эти правила в меню транзакций на странице заказов и при валидации проводок. |
| | | 37 | | На графе можно визуально связать статусы; в таблице — отредактировать или удалить запись. |
| | | 38 | | </div> |
| | | 39 | | <div class="app-section-hero__quick-links"> |
| | | 40 | | <a asp-area="Studio2" asp-controller="AppStatus" asp-action="Index"><i class="fas fa-tags"></i>Статусы заказ |
| | | 41 | | <a asp-area="Studio2" asp-controller="Orders" asp-action="Index"><i class="fas fa-list"></i>Активные заказы< |
| | | 42 | | <a asp-area="Studio2" asp-controller="TransactionTypeSettings" asp-action="Index"><i class="fas fa-sliders-h |
| | | 43 | | </div> |
| | | 44 | | </div> |
| | | 45 | | <div class="d-flex flex-wrap gap-2 align-items-center"> |
| | | 46 | | <div class="bsx-dir-stats"> |
| | 0 | 47 | | <span class="bsx-dir-stat">Переходов <b>@Model.Count</b></span> |
| | 0 | 48 | | <span class="bsx-dir-stat">Активных <b>@Model.Count(t => t.IsActive)</b></span> |
| | | 49 | | </div> |
| | | 50 | | <a asp-action="Create" class="btn btn-primary btn-sm"><i class="fas fa-plus me-1"></i>Добавить</a> |
| | | 51 | | </div> |
| | | 52 | | </div> |
| | 0 | 53 | | @if (TempData["StatusResult"] != null) |
| | | 54 | | { |
| | | 55 | | <div class="alert alert-success alert-dismissible mb-3"> |
| | | 56 | | <button type="button" class="btn-close" data-bs-dismiss="alert"></button> |
| | 0 | 57 | | @TempData["StatusResult"] |
| | | 58 | | </div> |
| | | 59 | | } |
| | | 60 | | <!-- ── Graph card ──────────────────────────────────────────── --> |
| | | 61 | | <div class="graph-card"> |
| | | 62 | | <div class="graph-card-header"> |
| | | 63 | | <div class="graph-card-title"> |
| | | 64 | | <i class="fas fa-sitemap"></i> |
| | | 65 | | Граф переходов |
| | | 66 | | </div> |
| | | 67 | | <div class="graph-toolbar"> |
| | | 68 | | <button class="g-btn active" id="btnLayoutHier" onclick="setLayout('hierarchical')" title="Слева направо по |
| | | 69 | | <i class="fas fa-stream"></i> Слева направо |
| | | 70 | | </button> |
| | | 71 | | <button class="g-btn" id="btnLayoutForce" onclick="setLayout('force')" title="Силовая раскладка — самооргани |
| | | 72 | | <i class="fas fa-atom"></i> Свободный |
| | | 73 | | </button> |
| | | 74 | | <div class="g-divider"></div> |
| | | 75 | | <button class="g-btn" onclick="zoomGraphOut()" title="Уменьшить"> |
| | | 76 | | <i class="fas fa-search-minus"></i> |
| | | 77 | | </button> |
| | | 78 | | <button class="g-btn" onclick="zoomGraphIn()" title="Увеличить"> |
| | | 79 | | <i class="fas fa-search-plus"></i> |
| | | 80 | | </button> |
| | | 81 | | <button class="g-btn" onclick="fitGraph()" title="Вписать всё в экран"> |
| | | 82 | | <i class="fas fa-compress-arrows-alt"></i> Вписать |
| | | 83 | | </button> |
| | | 84 | | <button class="g-btn" onclick="rebuildGraph()" title="Перестроить расположение"> |
| | | 85 | | <i class="fas fa-redo"></i> |
| | | 86 | | </button> |
| | | 87 | | <div class="g-divider"></div> |
| | | 88 | | <button class="g-btn" id="editModeBtn" onclick="toggleEditMode()" title="Клик на два узла создаёт переход"> |
| | | 89 | | <i class="fas fa-magic"></i> Связать |
| | | 90 | | </button> |
| | | 91 | | <div class="g-divider"></div> |
| | | 92 | | <button class="g-btn" onclick="toggleGraph()" id="graphToggleBtn"> |
| | | 93 | | <i class="fas fa-chevron-up"></i> |
| | | 94 | | </button> |
| | | 95 | | </div> |
| | | 96 | | </div> |
| | | 97 | | |
| | | 98 | | <div id="graphContainer"> |
| | | 99 | | <div id="transitionGraph"></div> |
| | | 100 | | <div class="graph-legend"> |
| | | 101 | | <div class="lg-item"><div class="lg-node"></div><span>Статус</span></div> |
| | | 102 | | <div class="lg-item"><div class="lg-node lg-node--final"></div><span>Конечный статус</span></div> |
| | | 103 | | <div class="lg-item"><div class="lg-arrow"></div><span>Активный переход</span></div> |
| | | 104 | | <div class="lg-item"><div class="lg-arrow lg-arrow--dash"></div><span>Неактивный</span></div> |
| | | 105 | | <div class="lg-item"> |
| | | 106 | | <i class="fas fa-mouse-pointer bsx-muted" style="font-size:10px;"></i> |
| | | 107 | | <span>ПКМ — добавить статус · колесо — масштаб</span> |
| | | 108 | | </div> |
| | | 109 | | <div class="edit-hint" id="editModeHint"></div> |
| | | 110 | | </div> |
| | | 111 | | </div> |
| | | 112 | | </div> |
| | | 113 | | |
| | 0 | 114 | | @await Html.PartialAsync("_BsxDirectoryGrid", new BsxDirectoryGridViewModel("statusTransition", "Список переходов", "fa- |
| | 0 | 115 | | "Фильтры в колонках: исходный и целевой статус, тип транзакции, активность. Двойной клик — страница изменения.")) |
| | | 116 | | <!-- ── Модальное окно создания перехода из графа ──────────── --> |
| | | 117 | | <div class="modal fade" id="createTransitionModal" tabindex="-1" aria-hidden="true"> |
| | | 118 | | <div class="modal-dialog"> |
| | | 119 | | <div class="modal-content"> |
| | | 120 | | <div class="modal-header"> |
| | | 121 | | <h5 class="modal-title"><i class="fas fa-plus-circle me-2 text-primary"></i>Новый переход</h5> |
| | | 122 | | <button type="button" class="btn-close" data-bs-dismiss="modal"></button> |
| | | 123 | | </div> |
| | | 124 | | <form asp-action="Create" method="post" id="quickCreateForm"> |
| | | 125 | | <div class="modal-body"> |
| | | 126 | | <input type="hidden" id="modalFromStatusId" name="FromStatusId"/> |
| | | 127 | | <input type="hidden" id="modalToStatusId" name="ToStatusId"/> |
| | | 128 | | |
| | | 129 | | <div class="d-flex align-items-center gap-3 mb-4 p-3 rounded st-modal-route"> |
| | | 130 | | <div id="modalFromStatusDisplay" class="flex-fill"></div> |
| | | 131 | | <i class="fas fa-long-arrow-alt-right fa-lg" style="flex-shrink:0;"></i> |
| | | 132 | | <div id="modalToStatusDisplay" class="flex-fill"></div> |
| | | 133 | | </div> |
| | | 134 | | |
| | | 135 | | <div class="mb-3"> |
| | | 136 | | <label class="form-label fw-bold" style="font-size:12px;">Тип транзакции <span class="text-dange |
| | | 137 | | <select class="form-select" name="TransactionType" id="modalTransactionType" required> |
| | | 138 | | <option value="">— выберите тип —</option> |
| | 0 | 139 | | @if (ViewBag.TransactionTypes != null) |
| | | 140 | | { |
| | 0 | 141 | | foreach (var tt in (List<TransactionType>)ViewBag.TransactionTypes) |
| | | 142 | | { |
| | 0 | 143 | | <option value="@((int)tt)">@tt.GetDisplayName()</option> |
| | | 144 | | } |
| | | 145 | | } |
| | | 146 | | </select> |
| | | 147 | | </div> |
| | | 148 | | |
| | | 149 | | <div class="mb-3"> |
| | | 150 | | <label class="form-label fw-bold" style="font-size:12px;">Название (для UI)</label> |
| | | 151 | | <input type="text" class="form-control" name="DisplayName" id="modalDisplayName" placeholder="По |
| | | 152 | | </div> |
| | | 153 | | |
| | | 154 | | <div class="row g-3 mb-3"> |
| | | 155 | | <div class="col"> |
| | | 156 | | <label class="form-label fw-bold" style="font-size:12px;">Порядок</label> |
| | | 157 | | <input type="number" class="form-control" name="DisplayOrder" value="0"/> |
| | | 158 | | </div> |
| | | 159 | | <div class="col d-flex align-items-end gap-3 pb-1"> |
| | | 160 | | <div class="form-check mb-0"> |
| | | 161 | | <input type="hidden" name="IsActive" value="false"/> |
| | | 162 | | <input type="checkbox" class="form-check-input" name="IsActive" id="modalIsActive" value |
| | | 163 | | <label class="form-check-label" for="modalIsActive" style="font-size:12px;">Активен</lab |
| | | 164 | | </div> |
| | | 165 | | <div class="form-check mb-0"> |
| | | 166 | | <input type="hidden" name="IsRepeatable" value="false"/> |
| | | 167 | | <input type="checkbox" class="form-check-input" name="IsRepeatable" id="modalIsRepeatabl |
| | | 168 | | <label class="form-check-label" for="modalIsRepeatable" style="font-size:12px;">В обе ст |
| | | 169 | | </div> |
| | | 170 | | </div> |
| | | 171 | | </div> |
| | | 172 | | </div> |
| | | 173 | | <div class="modal-footer"> |
| | | 174 | | <button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Отмена</butto |
| | | 175 | | <button type="submit" class="btn btn-primary btn-sm"><i class="fas fa-save me-1"></i>Создать</button |
| | | 176 | | </div> |
| | | 177 | | </form> |
| | | 178 | | </div> |
| | | 179 | | </div> |
| | | 180 | | </div> |
| | | 181 | | |
| | | 182 | | <!-- ── Контекстное меню ────────────────────────────────────── --> |
| | | 183 | | <div class="graph-ctx-menu" id="graphContextMenu"> |
| | | 184 | | <div class="ctx-header"><i class="fas fa-plus-circle me-1"></i>Добавить статус на граф</div> |
| | | 185 | | <div id="contextMenuItems"></div> |
| | | 186 | | </div> |
| | | 187 | | </div> |
| | | 188 | | |
| | | 189 | | @section Scripts { |
| | | 190 | | <script> |
| | | 191 | | window.statusTransitionGraph = { |
| | 0 | 192 | | hasData: @Html.Raw(Model != null && Model.Any() ? "true" : "false"), |
| | 0 | 193 | | allStatuses: @if (ViewBag.Statuses != null) |
| | | 194 | | { |
| | 0 | 195 | | @Html.Raw(System.Text.Json.JsonSerializer.Serialize( |
| | 0 | 196 | | ((List<AppStatus>)ViewBag.Statuses).Select(s => new { |
| | 0 | 197 | | id = s.Id, |
| | 0 | 198 | | name = s.Name, |
| | 0 | 199 | | color = s.GetStatusColor(), |
| | 0 | 200 | | isFinal = s.IsFinal |
| | 0 | 201 | | }) |
| | 0 | 202 | | )) |
| | | 203 | | } |
| | | 204 | | else |
| | | 205 | | { |
| | | 206 | | @:[] |
| | | 207 | | }, |
| | 0 | 208 | | transitions: @Html.Raw(System.Text.Json.JsonSerializer.Serialize( |
| | 0 | 209 | | (Model ?? new List<StatusTransition>()).Select(t => new { |
| | 0 | 210 | | fromStatusId = t.FromStatusId, |
| | 0 | 211 | | fromStatusName = t.FromStatus?.Name ?? "—", |
| | 0 | 212 | | fromStatusColor = t.FromStatus?.GetStatusColor() ?? "#94a3b8", |
| | 0 | 213 | | toStatusId = t.ToStatusId, |
| | 0 | 214 | | toStatusName = t.ToStatus?.Name ?? "—", |
| | 0 | 215 | | toStatusColor = t.ToStatus?.GetStatusColor() ?? "#94a3b8", |
| | 0 | 216 | | transactionType = t.TransactionType.GetDisplayName(), |
| | 0 | 217 | | transactionCssClass = t.TransactionType.GetTransactionTypeCssClass(), |
| | 0 | 218 | | isActive = t.IsActive, |
| | 0 | 219 | | displayName = t.DisplayName ?? t.TransactionType.GetDisplayName() |
| | 0 | 220 | | }) |
| | 0 | 221 | | )) |
| | | 222 | | }; |
| | | 223 | | </script> |
| | | 224 | | <script src="~/js/Studio2/Grid/bsx-directory.js" asp-append-version="true"></script> |
| | | 225 | | <script> |
| | | 226 | | (function () { |
| | | 227 | | const D = window.BsxDirectory; |
| | | 228 | | const esc = BsxGrid.esc; |
| | 0 | 229 | | const editUrl = id => '@Url.Action("Edit", "StatusTransition", new { area = "Studio2" })/' + id; |
| | | 230 | | // Класс типа транзакции (link-*) → тон темы. |
| | | 231 | | const TONES = { 'link-success': 'success', 'link-danger': 'danger', 'link-warning': 'warning', 'link-info': |
| | | 232 | | 'link-primary': 'accent', 'link-secondary': 'muted', 'link-purple': 'purple' }; |
| | | 233 | | const finalMark = on => on ? ' <span class="bsx-tone bsx-tone--warning" title="Конечный статус">F</span>' : |
| | | 234 | | |
| | | 235 | | D.grid({ |
| | | 236 | | key: 'dirStatusTransition', |
| | | 237 | | prefix: 'statusTransition', |
| | 0 | 238 | | rows: @Json.Serialize(rows), |
| | | 239 | | rowUrl: r => editUrl(r.id), |
| | | 240 | | columnDefs: [ |
| | | 241 | | D.numCol({ field: 'displayOrder', headerName: '#', headerTooltip: 'Порядок в меню', width: 80, sort: |
| | | 242 | | D.setCol({ field: 'fromName', headerName: 'Исходный статус', flex: 1, minWidth: 200, |
| | | 243 | | cellRenderer: p => D.colorDot(p.value, p.data.fromColor, finalMark(p.data.fromFinal)) }), |
| | | 244 | | { colId: 'arrow', headerName: '', width: 44, sortable: false, filter: false, resizable: false, suppr |
| | | 245 | | cellRenderer: () => '<i class="fas fa-long-arrow-alt-right bsx-muted"></i>' }, |
| | | 246 | | D.setCol({ field: 'toName', headerName: 'Целевой статус', flex: 1, minWidth: 200, |
| | | 247 | | cellRenderer: p => D.colorDot(p.value, p.data.toColor, finalMark(p.data.toFinal)) }), |
| | | 248 | | D.setCol({ field: 'transactionType', headerName: 'Тип транзакции', width: 240, |
| | | 249 | | cellRenderer: p => D.tone(p.value, TONES[p.data.transactionCss] || 'muted', p.data.transactionIcon |
| | | 250 | | { field: 'displayName', headerName: 'Название', flex: 1, minWidth: 180, valueFormatter: p => p.value |
| | | 251 | | D.boolCol({ field: 'isActive', headerName: 'Активен', width: 110 }), |
| | | 252 | | D.actionsCol(r => [ |
| | | 253 | | { icon: 'fa-pencil-alt', title: 'Изменить', href: editUrl(r.id) }, |
| | | 254 | | { icon: 'fa-up-right-from-square', title: 'Изменить в новой вкладке', href: editUrl(r.id), targe |
| | 0 | 255 | | { icon: 'fa-trash', title: 'Удалить', kind: 'danger', href: '@Url.Action("Delete", "StatusTransi |
| | | 256 | | ], { width: 130 }) |
| | | 257 | | ] |
| | | 258 | | }); |
| | | 259 | | })(); |
| | | 260 | | </script> |
| | | 261 | | <link href="https://cdn.jsdelivr.net/npm/vis-network@9.1.9/dist/dist/vis-network.min.css" rel="stylesheet"/> |
| | | 262 | | <script src="~/js/Studio2/StatusTransition/status-transition-graph.js" asp-append-version="true"></script> |
| | | 263 | | <script> |
| | | 264 | | (function () { |
| | | 265 | | const graphScript = document.createElement('script'); |
| | | 266 | | graphScript.src = 'https://cdn.jsdelivr.net/npm/vis-network@9.1.9/standalone/umd/vis-network.min.js'; |
| | | 267 | | graphScript.async = false; |
| | | 268 | | graphScript.onload = function () { |
| | | 269 | | if (typeof initStatusTransitionGraph === 'function') { |
| | | 270 | | initStatusTransitionGraph(); |
| | | 271 | | } |
| | | 272 | | }; |
| | | 273 | | graphScript.onerror = function () { |
| | | 274 | | const el = document.getElementById('transitionGraph'); |
| | | 275 | | if (el) { |
| | | 276 | | el.innerHTML = '<div class="st-graph-error"><i class="fas fa-exclamation-triangle fa-2x d-block mb-2 |
| | | 277 | | } |
| | | 278 | | }; |
| | | 279 | | document.head.appendChild(graphScript); |
| | | 280 | | })(); |
| | | 281 | | </script> |
| | | 282 | | } |