| | | 1 | | using System.Security.Claims; |
| | | 2 | | using Microsoft.EntityFrameworkCore; |
| | | 3 | | using OzonDomains; |
| | | 4 | | using OzonRepositories.Context; |
| | | 5 | | |
| | | 6 | | namespace OzonOrdersWeb.WebServices.Orders.Grid; |
| | | 7 | | |
| | | 8 | | public sealed class OrdersGridOrderCardDto |
| | | 9 | | { |
| | 0 | 10 | | public OrderGridRowDto Row { get; set; } = new(); |
| | | 11 | | |
| | 0 | 12 | | public List<OrdersGridHistoryItemDto> History { get; set; } = []; |
| | | 13 | | |
| | 0 | 14 | | public bool HistoryTruncated { get; set; } |
| | | 15 | | } |
| | | 16 | | |
| | | 17 | | public sealed class OrdersGridHistoryItemDto |
| | | 18 | | { |
| | | 19 | | public DateTime ChangedAt { get; set; } |
| | | 20 | | |
| | | 21 | | public string? Field { get; set; } |
| | | 22 | | |
| | | 23 | | public string? OldValue { get; set; } |
| | | 24 | | |
| | | 25 | | public string? NewValue { get; set; } |
| | | 26 | | |
| | | 27 | | public string? ChangedBy { get; set; } |
| | | 28 | | } |
| | | 29 | | |
| | | 30 | | public sealed class OrdersGridStatusHistoryItemDto |
| | | 31 | | { |
| | | 32 | | public DateTime ChangedAt { get; set; } |
| | | 33 | | |
| | | 34 | | /// <summary>app | client | market | supplier — какой статус менялся.</summary> |
| | | 35 | | public string Kind { get; set; } = string.Empty; |
| | | 36 | | |
| | | 37 | | public string? Field { get; set; } |
| | | 38 | | |
| | | 39 | | public string? OldValue { get; set; } |
| | | 40 | | |
| | | 41 | | public string? NewValue { get; set; } |
| | | 42 | | |
| | | 43 | | public string? ChangedBy { get; set; } |
| | | 44 | | } |
| | | 45 | | |
| | | 46 | | public sealed class OrdersGridOrderTransactionDto |
| | | 47 | | { |
| | | 48 | | public int Id { get; set; } |
| | | 49 | | |
| | | 50 | | /// <summary>Название типа транзакции («Заказан реализатору»).</summary> |
| | | 51 | | public string Type { get; set; } = string.Empty; |
| | | 52 | | |
| | | 53 | | /// <summary>Цвет типа транзакции (#rrggbb) из настроек; null — не задан.</summary> |
| | | 54 | | public string? Color { get; set; } |
| | | 55 | | |
| | | 56 | | public DateTime? CreatedAt { get; set; } |
| | | 57 | | |
| | | 58 | | /// <summary>Ник пользователя, проведшего транзакцию.</summary> |
| | | 59 | | public string? CreatedBy { get; set; } |
| | | 60 | | |
| | | 61 | | public string? Comment { get; set; } |
| | | 62 | | |
| | | 63 | | /// <summary>Транзакция, к которой заказ привязан сейчас.</summary> |
| | | 64 | | public bool IsCurrent { get; set; } |
| | | 65 | | } |
| | | 66 | | |
| | | 67 | | public interface IOrdersGridOrderCardService |
| | | 68 | | { |
| | | 69 | | /// <summary>Строка грида (все колонки, с гейтингом) и последние изменения по заказу; null — заказ не найден.</summa |
| | | 70 | | Task<OrdersGridOrderCardDto?> GetCardAsync(int orderId, ClaimsPrincipal principal, CancellationToken cancellationTok |
| | | 71 | | |
| | | 72 | | /// <summary>Только переходы статусов (наш / клиента / маркетплейса / поставщика) по orders_history, новые сверху; n |
| | | 73 | | Task<List<OrdersGridStatusHistoryItemDto>?> GetStatusHistoryAsync(int orderId, ClaimsPrincipal principal, Cancellati |
| | | 74 | | |
| | | 75 | | /// <summary>Транзакции, в которых участвовал заказ (текущая привязка + смены TransactionId в orders_history), новые |
| | | 76 | | Task<List<OrdersGridOrderTransactionDto>?> GetOrderTransactionsAsync(int orderId, ClaimsPrincipal principal, Cancell |
| | | 77 | | } |
| | | 78 | | |
| | | 79 | | /// <summary>Данные боковой карточки заказа: та же строка, что в гриде, плюс лента orders_history.</summary> |
| | | 80 | | public sealed class OrdersGridOrderCardService : IOrdersGridOrderCardService |
| | | 81 | | { |
| | | 82 | | public const int HistoryLimit = 60; |
| | | 83 | | public const int StatusHistoryLimit = 100; |
| | | 84 | | public const int TransactionsLimit = 50; |
| | | 85 | | private const int TransactionCommentMaxLength = 300; |
| | | 86 | | |
| | | 87 | | /// <summary>ColumnName в orders_history → вид статуса.</summary> |
| | | 88 | | private static readonly Dictionary<string, string> StatusKinds = new(StringComparer.OrdinalIgnoreCase) |
| | | 89 | | { |
| | | 90 | | ["AppStatusId"] = "app", |
| | | 91 | | ["ClientStatusId"] = "client", |
| | | 92 | | ["Status"] = "market", |
| | | 93 | | ["SupplierOrderStatus"] = "supplier" |
| | | 94 | | }; |
| | | 95 | | |
| | | 96 | | private readonly ApplicationDbContext _context; |
| | | 97 | | private readonly IOrdersGridRowHydrator _hydrator; |
| | | 98 | | private readonly IOrdersGridScopedQuery _scoped; |
| | | 99 | | |
| | | 100 | | public OrdersGridOrderCardService(ApplicationDbContext context, IOrdersGridRowHydrator hydrator, IOrdersGridScopedQu |
| | | 101 | | { |
| | | 102 | | _context = context; |
| | | 103 | | _hydrator = hydrator; |
| | | 104 | | _scoped = scoped; |
| | | 105 | | } |
| | | 106 | | |
| | | 107 | | public async Task<OrdersGridOrderCardDto?> GetCardAsync(int orderId, ClaimsPrincipal principal, CancellationToken ca |
| | | 108 | | { |
| | | 109 | | var rows = await _hydrator.HydrateAsync([orderId], principal, cancellationToken); |
| | | 110 | | if (rows.Count == 0) |
| | | 111 | | { |
| | | 112 | | return null; |
| | | 113 | | } |
| | | 114 | | |
| | | 115 | | var history = await _context.OrderHistories.AsNoTracking() |
| | | 116 | | .Where(h => h.OrderId == orderId) |
| | | 117 | | .OrderByDescending(h => h.ChangedAt) |
| | | 118 | | .ThenByDescending(h => h.Id) |
| | | 119 | | .Take(HistoryLimit + 1) |
| | | 120 | | .Select(h => new OrdersGridHistoryItemDto |
| | | 121 | | { |
| | | 122 | | ChangedAt = h.ChangedAt, |
| | | 123 | | Field = h.ColumnDisplayName ?? h.ColumnName, |
| | | 124 | | OldValue = h.OldValue, |
| | | 125 | | NewValue = h.NewValue, |
| | | 126 | | ChangedBy = h.ChangedBy |
| | | 127 | | }) |
| | | 128 | | .ToListAsync(cancellationToken); |
| | | 129 | | |
| | | 130 | | return new OrdersGridOrderCardDto |
| | | 131 | | { |
| | | 132 | | Row = rows[0], |
| | | 133 | | History = history.Take(HistoryLimit).ToList(), |
| | | 134 | | HistoryTruncated = history.Count > HistoryLimit |
| | | 135 | | }; |
| | | 136 | | } |
| | | 137 | | |
| | | 138 | | public async Task<List<OrdersGridStatusHistoryItemDto>?> GetStatusHistoryAsync(int orderId, ClaimsPrincipal principa |
| | | 139 | | { |
| | | 140 | | // Доступ к заказу — тем же RLS-запросом, что и список; иначе история чужого заказа читалась бы по Id. |
| | | 141 | | var user = await _scoped.ResolveUserAsync(principal); |
| | | 142 | | if (user is null) return null; |
| | | 143 | | var accessible = await _scoped.FilterAccessibleAsync(user, [orderId], includeArchived: true, cancellationToken); |
| | | 144 | | if (accessible.Count == 0) return null; |
| | | 145 | | |
| | | 146 | | var names = StatusKinds.Keys.ToList(); |
| | | 147 | | var rows = await _context.OrderHistories.AsNoTracking() |
| | | 148 | | .Where(h => h.OrderId == orderId && h.ColumnName != null && names.Contains(h.ColumnName)) |
| | | 149 | | .OrderByDescending(h => h.ChangedAt) |
| | | 150 | | .ThenByDescending(h => h.Id) |
| | | 151 | | .Take(StatusHistoryLimit) |
| | | 152 | | .Select(h => new { h.ChangedAt, h.ColumnName, h.ColumnDisplayName, h.OldValue, h.NewValue, h.ChangedBy }) |
| | | 153 | | .ToListAsync(cancellationToken); |
| | | 154 | | |
| | | 155 | | return rows.Select(h => new OrdersGridStatusHistoryItemDto |
| | | 156 | | { |
| | | 157 | | ChangedAt = h.ChangedAt, |
| | | 158 | | Kind = StatusKinds.GetValueOrDefault(h.ColumnName!, "app"), |
| | | 159 | | Field = h.ColumnDisplayName ?? h.ColumnName, |
| | | 160 | | OldValue = h.OldValue, |
| | | 161 | | NewValue = h.NewValue, |
| | | 162 | | ChangedBy = h.ChangedBy |
| | | 163 | | }).ToList(); |
| | | 164 | | } |
| | | 165 | | public async Task<List<OrdersGridOrderTransactionDto>?> GetOrderTransactionsAsync(int orderId, ClaimsPrincipal princ |
| | | 166 | | { |
| | | 167 | | var user = await _scoped.ResolveUserAsync(principal); |
| | | 168 | | if (user is null) return null; |
| | | 169 | | var accessible = await _scoped.FilterAccessibleAsync(user, [orderId], includeArchived: true, cancellationToken); |
| | | 170 | | if (accessible.Count == 0) return null; |
| | | 171 | | |
| | | 172 | | var currentId = await _context.Orders.AsNoTracking() |
| | | 173 | | .Where(o => o.Id == orderId) |
| | | 174 | | .Select(o => o.TransactionId) |
| | | 175 | | .FirstOrDefaultAsync(cancellationToken); |
| | | 176 | | |
| | | 177 | | var changes = await _context.OrderHistories.AsNoTracking() |
| | | 178 | | .Where(h => h.OrderId == orderId && h.ColumnName == "TransactionId") |
| | | 179 | | .Select(h => new { h.OldValue, h.NewValue }) |
| | | 180 | | .ToListAsync(cancellationToken); |
| | | 181 | | |
| | | 182 | | var ids = new HashSet<int>(); |
| | | 183 | | if (currentId is > 0) ids.Add(currentId.Value); |
| | | 184 | | foreach (var c in changes) |
| | | 185 | | { |
| | | 186 | | if (int.TryParse(c.NewValue, out var n) && n > 0) ids.Add(n); |
| | | 187 | | if (int.TryParse(c.OldValue, out var o) && o > 0) ids.Add(o); |
| | | 188 | | } |
| | | 189 | | |
| | | 190 | | if (ids.Count == 0) return []; |
| | | 191 | | |
| | | 192 | | var transactions = await _context.Transactions.AsNoTracking() |
| | | 193 | | .Where(t => ids.Contains(t.Id)) |
| | | 194 | | .OrderByDescending(t => t.CreatedDateTime) |
| | | 195 | | .ThenByDescending(t => t.Id) |
| | | 196 | | .Take(TransactionsLimit) |
| | | 197 | | .Select(t => new { t.Id, t.Type, t.CreatedDateTime, t.CreateBy, t.Comment }) |
| | | 198 | | .ToListAsync(cancellationToken); |
| | | 199 | | |
| | | 200 | | var settings = await _context.TransactionTypeSettings.AsNoTracking() |
| | | 201 | | .ToDictionaryAsync(s => s.TransactionType, s => new { s.Title, s.Color }, cancellationToken); |
| | | 202 | | |
| | | 203 | | return transactions.Select(t => |
| | | 204 | | { |
| | | 205 | | settings.TryGetValue(t.Type ?? TransactionType.All, out var setting); |
| | | 206 | | var comment = t.Comment?.Trim(); |
| | | 207 | | if (comment is { Length: > TransactionCommentMaxLength }) |
| | | 208 | | { |
| | | 209 | | comment = comment[..TransactionCommentMaxLength] + "…"; |
| | | 210 | | } |
| | | 211 | | |
| | | 212 | | return new OrdersGridOrderTransactionDto |
| | | 213 | | { |
| | | 214 | | Id = t.Id, |
| | | 215 | | Type = !string.IsNullOrWhiteSpace(setting?.Title) |
| | | 216 | | ? setting!.Title! |
| | | 217 | | : t.Type?.GetTransactionDisplayName() ?? "Транзакция", |
| | | 218 | | Color = setting?.Color, |
| | | 219 | | CreatedAt = t.CreatedDateTime, |
| | | 220 | | CreatedBy = t.CreateBy, |
| | | 221 | | Comment = string.IsNullOrWhiteSpace(comment) ? null : comment, |
| | | 222 | | IsCurrent = t.Id == currentId |
| | | 223 | | }; |
| | | 224 | | }).ToList(); |
| | | 225 | | } |
| | | 226 | | } |