< Summary

Information
Class: Services.FiltersServices.DataFilterManagers.OrderDataFilterManager
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/FiltersServices/DataFilterManagers/OrderDataFilterManager.cs
Line coverage
37%
Covered lines: 365
Uncovered lines: 606
Coverable lines: 971
Total lines: 2283
Line coverage: 37.5%
Branch coverage
39%
Covered branches: 194
Total branches: 486
Branch coverage: 39.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)0%311865.47%
NarrowByElasticsearchAsync()94.44%1818100%
IsElasticsearchTextSearchEnabled()100%11100%
GetElasticsearchMaxResults()100%11100%
GetElasticsearchLargeResultThreshold()100%11100%
HasElasticsearchSearchCriteria(...)100%22100%
HasElasticsearchTextCriteria(...)100%88100%
HasElasticsearchExactFilterCriteria(...)87.5%1616100%
GetElasticsearchSearchDisplayText(...)90%1010100%
BuildElasticsearchFilterSummary(...)65%352066.66%
FilterByRadioButton()0%110100%
NormalizeShipmentNumberForSearch(...)100%22100%
ShipmentNumberMatchesSearch(...)100%44100%
NormalizeArticleForSearch(...)100%44100%
ArticleMatchesSearch(...)75%8890%
ApplyArticleFilter(...)0%7280%
ApplyShipmentNumberFilter(...)58.33%121294.28%
MatchesGlobalSearch(...)77.77%191886.66%
ApplyGlobalSearchQueryable(...)50%2290%
ApplyPrimaryTextSearchQueryable(...)80%492058.33%
ApplyPrimaryTextSearchInMemory(...)0%110100%
FilterByFilterData()100%210%
FilterByFilterDataQueryable(...)50%4075651.79%
FilterAndSortQueryable(...)0%2040%
ApplyFinalSortQueryable(...)50%22100%
ApplySortOrder(...)0%620%
GetSortOrderViewData(...)0%620%
FilterByFilterData()100%210%
FilterByFilterData()0%1190340%
FilterTransactionsOrdersByFilterData()0%930300%
FilterByFilterDataFromList(...)0%812280%
FilterFromFile()100%210%
GetWarehousesWithZasylAsync()100%210%
AreAllPropertiesNull(...)91.17%3434100%
StringToIntDay(...)100%44100%
StringToIntHours(...)50%5466.66%
FilterOrdersByColumns(...)0%210140%
ApplyFilters(...)0%210140%
CountNonEmptyPriorityFields(...)0%110100%
NormalizeString(...)0%620%
HasMultipleShipmentNumbers(...)100%22100%
GetShipmentNumbersFromFilter(...)100%88100%
Normalize(...)100%210%
GetArticleSearchTerms(...)0%620%
GetRelatedArticlesSync(...)6.25%1121627.77%
ApplyShipmentGroupingQueryable(...)100%210%
ExpandByShipmentIdAsync()0%2040%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/FiltersServices/DataFilterManagers/OrderDataFilterManager.cs

#LineLine coverage
 1using System.Diagnostics;
 2using System.Linq.Expressions;
 3using OServices.FiltersServices.FilterModels;
 4using OzonDomains.Models;
 5using OzonDomains.Models.AccessControl;
 6using Services.AccessControl;
 7using Services.DataServices;
 8using Services.CacheServices.Cache.OzonOrdersCache;
 9using System.Reflection;
 10using System.Text.RegularExpressions;
 11using OzonRepositories.Context;
 12using Microsoft.EntityFrameworkCore;
 13using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
 14using Services.FiltersServices.SortModels;
 15using Services.FiltersServices.SortManagers;
 16using Services.SearchServices.OrderSearch;
 17using Microsoft.Extensions.Configuration;
 18using Microsoft.Extensions.Logging;
 19
 20namespace Services.FiltersServices.DataFilterManagers
 21
 22{
 23    public class OrderDataFilterManager : IFilterManagerAsync<Order, OrderFilterModel>
 24
 25    {
 26        private QueryableDataFilter<Order> _filter;
 27
 28        private DataFilter<Order> _forTransactionOrdersFilter;
 29
 30        private readonly OrdersDataServices _ordersDataServices;
 31
 32        private readonly OrderCache _orderCache;
 33
 34        private readonly ApplicationDbContext _context;
 35
 36        private readonly OrderSortManager _sortManager;
 37
 38        private readonly IDataScopeEvaluator _dataScopeEvaluator;
 39
 40        private readonly IOrderSearchService _orderSearchService;
 41
 42        private readonly IOrderSearchDiagnosticsStore _orderSearchDiagnostics;
 43
 44        private readonly IConfiguration _configuration;
 45
 46        private readonly ILogger<OrderDataFilterManager> _logger;
 47
 3248        public Dictionary<string, Func<Order, object?>> ColumnMappings = new Dictionary<string, Func<Order, object?>>
 3249
 3250        {
 051            { "Номер заказа", order => order.ShipmentNumber },
 3252
 053            { "Клиент", order => order.OzonClient?.Name },
 3254
 055            { "Принят в обработку", order => order.FullFormattedProcessingDate },
 3256
 057            { "Дата отгрузки", order => order.FullFormattedShippingDate },
 3258
 059            { "Срок доставки", order => order.FormattedDeliveryPeriod },
 3260
 061            { "Статус клиента", order => order.ClientStatus != null ? order.ClientStatus.Name : null },
 3262
 063            { "Статус", order => order.AppStatus?.Name },
 3264
 065            { "Наименование товара", order => order.ProductName },
 3266
 067            { "Артикул", order => order.Article },
 3268
 069            { "Производитель", order => order.EtProducer?.Name },
 3270
 071            { "Склад отгрузки", order => order.ShipmentWarehouse?.Name },
 3272
 073            { "Поставщик", order => order.Supplier?.Name },
 3274
 075            { "Номер заказа поставщику", order => order.OrderNumberToSupplier },
 3276
 077            { "Цена сайта", order => order.ProductInfo?.CurrentPriceWithDiscount },
 3278
 079            { "Цена", order => order.Price },
 3280
 081            { "Количество", order => order.Quantity },
 3282
 083            { "Сумма отправления", order => order.ShipmentAmount },
 3284
 085            { "Категория", order => order.ProductInfo?.CommercialCategory },
 3286
 087            { "Объемный вес", order => order.ProductInfo?.VolumetricWeight },
 3288
 089            { "Цена закупки", order => order.PurchasePrice },
 3290
 091            { "Оригинальная цена", order => order.OriginalPurchasePrice },
 3292
 093            { "Себестоимость", order => order.CostPrice },
 3294
 095            { "Комиссия ОЗОН (мин.)", order => order.MinOzonCommission },
 3296
 097            { "Комиссия ОЗОН (макс.)", order => order.MaxOzonCommission },
 3298
 099            { "Прибыль (мин.)", order => order.MinProfit },
 32100
 0101            { "Прибыль (макс.)", order => order.MaxProfit },
 32102
 0103            { "Наценка % (мин.)", order => order.MinDiscount },
 32104
 0105            { "Наценка % (макс.)", order => order.MaxDiscount },
 32106
 0107            { "Город доставки", order => order.DeliveryCity }
 32108        };
 109
 110
 32111        public OrderDataFilterManager(QueryableDataFilter<Order> dataFilter,
 32112            DataFilter<Order> forTransactionOrdersFilter,
 32113            OrdersDataServices ordersDataServices,
 32114            OrderCache orderCache,
 32115            ApplicationDbContext context,
 32116            OrderSortManager sortManager,
 32117            IDataScopeEvaluator dataScopeEvaluator,
 32118            IOrderSearchService orderSearchService,
 32119            IOrderSearchDiagnosticsStore orderSearchDiagnostics,
 32120            IConfiguration configuration,
 32121            ILogger<OrderDataFilterManager> logger)
 122
 123        {
 32124            _filter = dataFilter;
 125
 32126            _forTransactionOrdersFilter = forTransactionOrdersFilter;
 127
 32128            _ordersDataServices = ordersDataServices;
 129
 32130            _orderCache = orderCache;
 131
 32132            _context = context;
 133
 32134            _sortManager = sortManager;
 135
 32136            _dataScopeEvaluator = dataScopeEvaluator;
 137
 32138            _orderSearchService = orderSearchService;
 139
 32140            _orderSearchDiagnostics = orderSearchDiagnostics;
 141
 32142            _configuration = configuration;
 143
 32144            _logger = logger;
 32145        }
 146
 147        /// <summary>
 148        /// Сужает <paramref name="ordersQuery"/> до заказов, найденных Elasticsearch по тексту
 149        /// (Search, ShipmentNumber, ProductName, Article) и/или точным фильтрам
 150        /// (AppStatus/Supplier/Manufacturer/ShipmentWarehouse/OzonClient).
 151        /// Вызывается ПОСЛЕ FilterAndSortQueryable и ДО применения RLS (ApplyOrderDataScopeAsync) —
 152        /// ES только сужает кандидатов по Id, авторизация и загрузка остаются в MySQL/EF без изменений.
 153        /// При выключенном флаге OrdersSearch:UseElasticsearch или ошибке ES — fallback на LIKE-фильтрацию.
 154        /// </summary>
 155        public async Task<IQueryable<Order>> NarrowByElasticsearchAsync(IQueryable<Order> ordersQuery, OrderFilterModel 
 156        {
 12157            if (!_configuration.GetValue<bool>("OrdersSearch:UseElasticsearch"))
 158            {
 1159                return ordersQuery;
 160            }
 161
 11162            if (!HasElasticsearchSearchCriteria(filterData))
 163            {
 1164                return ordersQuery;
 165            }
 166
 167            try
 168            {
 10169                var sw = Stopwatch.StartNew();
 10170                IReadOnlyList<string>? articleProductKeys = null;
 10171                var article = string.IsNullOrEmpty(filterData.Article) ? filterData.Article2 : filterData.Article;
 10172                if (!string.IsNullOrEmpty(article))
 173                {
 1174                    var relatedArticles = GetRelatedArticlesSync(article);
 1175                    if (relatedArticles.Count > 0)
 176                    {
 1177                        articleProductKeys = relatedArticles;
 178                    }
 179                }
 180
 10181                var maxResults = GetElasticsearchMaxResults();
 10182                var searchResult = await _orderSearchService.SearchOrderIdsAsync(filterData, articleProductKeys, maxResu
 9183                sw.Stop();
 184
 9185                var displayText = GetElasticsearchSearchDisplayText(filterData);
 9186                var largeThreshold = GetElasticsearchLargeResultThreshold();
 9187                var isLargeResult = searchResult.Truncated
 9188                    || searchResult.Ids.Count >= largeThreshold
 9189                    || searchResult.TotalHits >= largeThreshold;
 190
 9191                if (isLargeResult)
 192                {
 2193                    _logger.LogInformation(
 2194                        "ES-поиск заказов: {Returned} id (всего {Total}, лимит {Max}, обрезка={Truncated}) — «{Search}»"
 2195                        searchResult.Ids.Count,
 2196                        searchResult.TotalHits,
 2197                        searchResult.MaxResults,
 2198                        searchResult.Truncated,
 2199                        displayText);
 200                }
 201
 9202                _orderSearchDiagnostics.RecordSearch(
 9203                    displayText,
 9204                    BuildElasticsearchFilterSummary(filterData),
 9205                    sw.ElapsedMilliseconds,
 9206                    searchResult.Ids.Count,
 9207                    usedFallback: false,
 9208                    totalHits: searchResult.TotalHits,
 9209                    truncated: searchResult.Truncated,
 9210                    isLargeResult: isLargeResult);
 211
 9212                if (searchResult.Ids.Count == 0)
 213                {
 1214                    return ordersQuery.Where(o => false);
 215                }
 216
 8217                return ordersQuery.Where(o => searchResult.Ids.Contains(o.Id));
 218            }
 1219            catch (Exception ex)
 220            {
 1221                _logger.LogWarning(ex, "Elasticsearch-поиск заказов недоступен — fallback на LIKE-фильтрацию");
 1222                _orderSearchDiagnostics.RecordSearch(
 1223                    GetElasticsearchSearchDisplayText(filterData),
 1224                    BuildElasticsearchFilterSummary(filterData),
 1225                    0,
 1226                    0,
 1227                    usedFallback: true);
 1228                return ApplyPrimaryTextSearchQueryable(ordersQuery, filterData, forceMysql: true);
 229            }
 12230        }
 231
 232        private bool IsElasticsearchTextSearchEnabled() =>
 4233            _configuration.GetValue<bool>("OrdersSearch:UseElasticsearch");
 234
 235        private int GetElasticsearchMaxResults() =>
 10236            Math.Clamp(_configuration.GetValue("OrdersSearch:MaxResults", 2000), 1, 10_000);
 237
 238        private int GetElasticsearchLargeResultThreshold() =>
 9239            Math.Clamp(_configuration.GetValue("OrdersSearch:LargeResultLogThreshold", 500), 1, 10_000);
 240
 241        public static bool HasElasticsearchSearchCriteria(OrderFilterModel filter) =>
 16242            HasElasticsearchTextCriteria(filter) || HasElasticsearchExactFilterCriteria(filter);
 243
 244        public static bool HasElasticsearchTextCriteria(OrderFilterModel filter) =>
 25245            !string.IsNullOrWhiteSpace(filter.Search)
 25246            || !string.IsNullOrWhiteSpace(filter.ShipmentNumber)
 25247            || !string.IsNullOrWhiteSpace(filter.ProductName)
 25248            || !string.IsNullOrWhiteSpace(filter.Article)
 25249            || !string.IsNullOrWhiteSpace(filter.Article2);
 250
 251        private static bool HasElasticsearchExactFilterCriteria(OrderFilterModel filter) =>
 10252            (!string.IsNullOrWhiteSpace(filter.AppStatus) && filter.AppStatus != "Все")
 10253            || (!string.IsNullOrWhiteSpace(filter.Supplier) && filter.Supplier != "Все")
 10254            || !string.IsNullOrWhiteSpace(filter.Manufacturer)
 10255            || (!string.IsNullOrWhiteSpace(filter.ShipmentWarehouse) && filter.ShipmentWarehouse != "Все")
 10256            || (!string.IsNullOrWhiteSpace(filter.OzonClient) && filter.OzonClient != "Все");
 257
 258        internal static string GetElasticsearchSearchDisplayText(OrderFilterModel filter)
 259        {
 15260            if (!string.IsNullOrWhiteSpace(filter.Search))
 261            {
 3262                return filter.Search.Trim();
 263            }
 264
 12265            if (!string.IsNullOrWhiteSpace(filter.ShipmentNumber))
 266            {
 3267                return $"отправление: {filter.ShipmentNumber.Trim()}";
 268            }
 269
 9270            if (!string.IsNullOrWhiteSpace(filter.ProductName))
 271            {
 2272                return $"товар: {filter.ProductName.Trim()}";
 273            }
 274
 7275            var article = string.IsNullOrEmpty(filter.Article) ? filter.Article2 : filter.Article;
 7276            if (!string.IsNullOrWhiteSpace(article))
 277            {
 2278                return $"артикул: {article.Trim()}";
 279            }
 280
 5281            return "—";
 282        }
 283
 284        private static string BuildElasticsearchFilterSummary(OrderFilterModel filter)
 285        {
 10286            var parts = new List<string>();
 10287            if (!string.IsNullOrWhiteSpace(filter.AppStatus) && filter.AppStatus != "Все")
 288            {
 0289                parts.Add($"статус={filter.AppStatus}");
 290            }
 291
 10292            if (!string.IsNullOrWhiteSpace(filter.Supplier) && filter.Supplier != "Все")
 293            {
 4294                parts.Add($"поставщик={filter.Supplier}");
 295            }
 296
 10297            if (!string.IsNullOrWhiteSpace(filter.Manufacturer))
 298            {
 0299                parts.Add($"производитель={filter.Manufacturer}");
 300            }
 301
 10302            if (!string.IsNullOrWhiteSpace(filter.ShipmentWarehouse) && filter.ShipmentWarehouse != "Все")
 303            {
 0304                parts.Add($"склад={filter.ShipmentWarehouse}");
 305            }
 306
 10307            if (!string.IsNullOrWhiteSpace(filter.OzonClient) && filter.OzonClient != "Все")
 308            {
 0309                parts.Add($"клиент={filter.OzonClient}");
 310            }
 311
 10312            return parts.Count == 0 ? "—" : string.Join(", ", parts);
 313        }
 314
 315
 316        public async Task<List<Order>> FilterByRadioButton(List<Order> orders, string buttonState)
 317
 318        {
 0319            await Task.CompletedTask;
 0320            if (buttonState == "returnable")
 321
 322            {
 323                orders = orders.Where(d => d.IsReturnable == true).ToList();
 324            }
 325
 326
 0327            if (buttonState == "activated")
 328
 329            {
 330                orders = orders.Where(d => d.IsVerified == true).ToList();
 331            }
 332
 0333            else if (buttonState == "notActivated")
 334
 335            {
 336                orders = orders.Where(d => d.IsVerified == false).ToList();
 337            }
 338
 0339            else if (buttonState == "changed")
 340
 341            {
 342                orders = orders.Where(d => d.UpdatedColumns != null && d.UpdatedColumns.Count() != 0).ToList();
 343            }
 344
 0345            else if (buttonState == "accepted")
 346
 347            {
 348                orders = orders.Where(d => d.IsAccepted == true).ToList();
 349            }
 350
 0351            else if (buttonState == "all")
 352
 353            {
 354            }
 355
 356
 0357            return orders;
 0358        }
 359
 360        public static string NormalizeShipmentNumberForSearch(string? shipmentNumber)
 361        {
 50362            if (string.IsNullOrWhiteSpace(shipmentNumber))
 363            {
 6364                return string.Empty;
 365            }
 366
 44367            return Regex.Replace(shipmentNumber.Trim(), @"/\d+$", string.Empty);
 368        }
 369
 370        public static bool ShipmentNumberMatchesSearch(string? shipmentNumber, string? searchValue)
 371        {
 12372            var normalizedShipmentNumber = NormalizeShipmentNumberForSearch(shipmentNumber);
 12373            var normalizedSearchValue = NormalizeShipmentNumberForSearch(searchValue);
 374
 12375            return !string.IsNullOrEmpty(normalizedShipmentNumber) &&
 12376                   !string.IsNullOrEmpty(normalizedSearchValue) &&
 12377                   normalizedShipmentNumber.Contains(normalizedSearchValue, StringComparison.OrdinalIgnoreCase);
 378        }
 379
 380        /// <summary>
 381        /// Нормализует артикул для поиска: обрезает часть после «=», убирает спецсимволы (оставляет буквы и цифры).
 382        /// </summary>
 383        public static string NormalizeArticleForSearch(string? input)
 384        {
 56385            if (string.IsNullOrWhiteSpace(input))
 386            {
 5387                return string.Empty;
 388            }
 389
 51390            var trimmed = input.Trim();
 51391            var equalsIndex = trimmed.IndexOf('=');
 51392            if (equalsIndex >= 0)
 393            {
 10394                trimmed = trimmed.Substring(0, equalsIndex);
 395            }
 396
 51397            return Regex.Replace(trimmed, @"[^\p{L}\p{Nd}]", "");
 398        }
 399
 400        /// <summary>
 401        /// Совпадение артикула после нормализации: искомая строка входит как подпоследовательность
 402        /// в ProductKey или Order.Article (в любой позиции, аналог SQL LIKE '%...%').
 403        /// </summary>
 404        public static bool ArticleMatchesSearch(string? productKey, string? orderArticle, string? searchArticle)
 405        {
 10406            var normalizedSearch = NormalizeArticleForSearch(searchArticle);
 10407            if (string.IsNullOrEmpty(normalizedSearch))
 408            {
 0409                return false;
 410            }
 411
 10412            var normalizedProductKey = NormalizeArticleForSearch(productKey);
 10413            if (!string.IsNullOrEmpty(normalizedProductKey) &&
 10414                normalizedProductKey.Contains(normalizedSearch, StringComparison.OrdinalIgnoreCase))
 415            {
 7416                return true;
 417            }
 418
 3419            var normalizedOrderArticle = NormalizeArticleForSearch(orderArticle);
 3420            return !string.IsNullOrEmpty(normalizedOrderArticle) &&
 3421                   normalizedOrderArticle.Contains(normalizedSearch, StringComparison.OrdinalIgnoreCase);
 422        }
 423
 424        private static IQueryable<Order> ApplyArticleFilter(IQueryable<Order> ordersQuery, IEnumerable<string> searchArt
 425        {
 0426            var normalizedArticles = searchArticles
 0427                .Select(NormalizeArticleForSearch)
 0428                .Where(article => !string.IsNullOrWhiteSpace(article))
 0429                .Distinct(StringComparer.OrdinalIgnoreCase)
 0430                .ToList();
 431
 0432            if (normalizedArticles.Count == 0)
 433            {
 0434                return ordersQuery;
 435            }
 436
 0437            var orderParameter = Expression.Parameter(typeof(Order), "o");
 0438            var productKeyProperty = Expression.Property(orderParameter, nameof(Order.ProductKey));
 0439            var articleProperty = Expression.Property(orderParameter, nameof(Order.Article));
 0440            var toLowerMethod = typeof(string).GetMethod(nameof(string.ToLower), Type.EmptyTypes)!;
 0441            var containsMethod = typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) })!;
 442
 0443            Expression? matchesAnyExpression = null;
 444
 0445            foreach (var article in normalizedArticles)
 446            {
 0447                var articleLower = article.ToLowerInvariant();
 0448                var articleConst = Expression.Constant(articleLower);
 449
 0450                var productKeyNotNull = Expression.NotEqual(
 0451                    productKeyProperty,
 0452                    Expression.Constant(null, typeof(string)));
 0453                var productKeyLower = Expression.Call(productKeyProperty, toLowerMethod);
 0454                var productKeyContains = Expression.Call(productKeyLower, containsMethod, articleConst);
 0455                var productKeyMatch = Expression.AndAlso(productKeyNotNull, productKeyContains);
 456
 0457                var articleNotNull = Expression.NotEqual(
 0458                    articleProperty,
 0459                    Expression.Constant(null, typeof(string)));
 0460                var articlePropertyLower = Expression.Call(articleProperty, toLowerMethod);
 0461                var articleContains = Expression.Call(articlePropertyLower, containsMethod, articleConst);
 0462                var articleMatch = Expression.AndAlso(articleNotNull, articleContains);
 463
 0464                var termMatch = Expression.OrElse(productKeyMatch, articleMatch);
 0465                matchesAnyExpression = matchesAnyExpression == null
 0466                    ? termMatch
 0467                    : Expression.OrElse(matchesAnyExpression, termMatch);
 468            }
 469
 0470            if (matchesAnyExpression == null)
 471            {
 0472                return ordersQuery;
 473            }
 474
 0475            var predicate = Expression.Lambda<Func<Order, bool>>(matchesAnyExpression, orderParameter);
 0476            return ordersQuery.Where(predicate);
 477        }
 478
 479        private static IQueryable<Order> ApplyShipmentNumberFilter(IQueryable<Order> ordersQuery,
 480            IEnumerable<string> normalizedShipmentNumbers)
 481        {
 1482            var normalizedNumbers = normalizedShipmentNumbers
 1483                .Where(number => !string.IsNullOrWhiteSpace(number))
 1484                .Distinct(StringComparer.OrdinalIgnoreCase)
 1485                .ToList();
 486
 1487            if (!normalizedNumbers.Any())
 488            {
 0489                return ordersQuery;
 490            }
 491
 1492            var orderParameter = Expression.Parameter(typeof(Order), "pr");
 1493            var shipmentNumberProperty = Expression.Property(orderParameter, nameof(Order.ShipmentNumber));
 1494            var notNullExpression = Expression.NotEqual(
 1495                shipmentNumberProperty,
 1496                Expression.Constant(null, typeof(string)));
 497
 1498            Expression? matchesAnyExpression = null;
 1499            Expression? scanitMatchesExpression = null;
 1500            var containsMethod = typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) })!;
 1501            var scanitProperty = Expression.Property(orderParameter, nameof(Order.Scanit));
 502
 4503            foreach (var number in normalizedNumbers)
 504            {
 1505                var containsExpression = Expression.Call(
 1506                    shipmentNumberProperty,
 1507                    containsMethod,
 1508                    Expression.Constant(number));
 509
 1510                matchesAnyExpression = matchesAnyExpression == null
 1511                    ? containsExpression
 1512                    : Expression.OrElse(matchesAnyExpression, containsExpression);
 513
 514                // ScanIt с этикетки ищется там же, где номер заказа, но только точным совпадением.
 1515                var scanitEquals = Expression.Equal(scanitProperty, Expression.Constant(number, typeof(string)));
 1516                scanitMatchesExpression = scanitMatchesExpression == null
 1517                    ? scanitEquals
 1518                    : Expression.OrElse(scanitMatchesExpression, scanitEquals);
 519            }
 520
 1521            if (matchesAnyExpression == null || scanitMatchesExpression == null)
 522            {
 0523                return ordersQuery;
 524            }
 525
 1526            var predicate = Expression.Lambda<Func<Order, bool>>(
 1527                Expression.OrElse(
 1528                    Expression.AndAlso(notNullExpression, matchesAnyExpression),
 1529                    scanitMatchesExpression),
 1530                orderParameter);
 531
 1532            return ordersQuery.Where(predicate);
 533        }
 534
 535        public static bool MatchesGlobalSearch(Order order, string? search)
 536        {
 5537            if (order == null || string.IsNullOrWhiteSpace(search))
 538            {
 0539                return false;
 540            }
 541
 5542            var term = search.Trim();
 543
 5544            if (ShipmentNumberMatchesSearch(order.ShipmentNumber, term))
 545            {
 1546                return true;
 547            }
 548
 4549            if (!string.IsNullOrEmpty(order.ProductName) &&
 4550                order.ProductName.Contains(term, StringComparison.OrdinalIgnoreCase))
 551            {
 1552                return true;
 553            }
 554
 3555            if (!string.IsNullOrEmpty(order.ProductKey) &&
 3556                order.ProductKey.Contains(term, StringComparison.OrdinalIgnoreCase))
 557            {
 1558                return true;
 559            }
 560
 2561            if (!string.IsNullOrEmpty(order.Article) &&
 2562                order.Article.Contains(term, StringComparison.OrdinalIgnoreCase))
 563            {
 0564                return true;
 565            }
 566
 2567            return false;
 568        }
 569
 570        private static IQueryable<Order> ApplyGlobalSearchQueryable(IQueryable<Order> ordersQuery, string search)
 571        {
 1572            var term = search.Trim();
 1573            if (string.IsNullOrWhiteSpace(term))
 574            {
 0575                return ordersQuery;
 576            }
 577
 1578            var termLower = term.ToLowerInvariant();
 579
 1580            return ordersQuery.Where(o =>
 1581                (o.ShipmentNumber != null && o.ShipmentNumber.ToLower().Contains(termLower)) ||
 1582                (o.Scanit != null && o.Scanit == term) ||
 1583                (o.ProductName != null && o.ProductName.ToLower().Contains(termLower)) ||
 1584                (o.ProductKey != null && o.ProductKey.ToLower().Contains(termLower)) ||
 1585                (o.Article != null && o.Article.ToLower().Contains(termLower)));
 586        }
 587
 588        private IQueryable<Order> ApplyPrimaryTextSearchQueryable(IQueryable<Order> ordersQuery,
 589            OrderFilterModel filterData, bool forceMysql = false)
 590        {
 5591            if (!forceMysql && IsElasticsearchTextSearchEnabled() && HasElasticsearchTextCriteria(filterData))
 592            {
 2593                return ordersQuery;
 594            }
 595
 3596            if (!string.IsNullOrWhiteSpace(filterData.Search))
 597            {
 1598                return ApplyGlobalSearchQueryable(ordersQuery, filterData.Search);
 599            }
 600
 2601            if (!string.IsNullOrWhiteSpace(filterData.ShipmentNumber))
 602            {
 1603                var parts = GetShipmentNumbersFromFilter(filterData.ShipmentNumber);
 1604                if (parts.Count > 1)
 605                {
 0606                    var normalizedParts = parts
 0607                        .Select(NormalizeShipmentNumberForSearch)
 0608                        .Where(part => !string.IsNullOrWhiteSpace(part))
 0609                        .Distinct(StringComparer.OrdinalIgnoreCase)
 0610                        .ToList();
 611
 0612                    ordersQuery = ApplyShipmentNumberFilter(ordersQuery, normalizedParts);
 613                }
 1614                else if (parts.Count == 1)
 615                {
 1616                    var normalizedShipmentNumber = NormalizeShipmentNumberForSearch(parts[0]);
 1617                    ordersQuery = ApplyShipmentNumberFilter(ordersQuery, new[] { normalizedShipmentNumber });
 618                }
 619                else
 620                {
 0621                    var normalizedShipmentNumber = NormalizeShipmentNumberForSearch(filterData.ShipmentNumber);
 0622                    ordersQuery = ApplyShipmentNumberFilter(ordersQuery, new[] { normalizedShipmentNumber });
 623                }
 624            }
 625
 2626            ordersQuery = _filter.FilterByString(ordersQuery, pr => pr.ProductName, filterData.ProductName);
 627
 2628            if (string.IsNullOrEmpty(filterData.Article) && !string.IsNullOrEmpty(filterData.Article2))
 629            {
 0630                filterData.Article = filterData.Article2;
 631            }
 632
 2633            if (!string.IsNullOrEmpty(filterData.Article))
 634            {
 0635                ordersQuery = ApplyArticleFilter(ordersQuery, GetArticleSearchTerms(filterData.Article));
 636            }
 637
 2638            return ordersQuery;
 639        }
 640
 641        private IQueryable<Order> ApplyPrimaryTextSearchInMemory(IQueryable<Order> orders,
 642            OrderFilterModel filterData)
 643        {
 0644            if (!string.IsNullOrWhiteSpace(filterData.Search))
 645            {
 0646                var termLower = filterData.Search.Trim().ToLowerInvariant();
 647
 0648                return orders.Where(o =>
 0649                    (o.ShipmentNumber != null && o.ShipmentNumber.ToLower().Contains(termLower)) ||
 0650                    (o.ProductName != null && o.ProductName.ToLower().Contains(termLower)) ||
 0651                    (o.ProductKey != null && o.ProductKey.ToLower().Contains(termLower)) ||
 0652                    (o.Article != null && o.Article.ToLower().Contains(termLower)));
 653            }
 654
 0655            if (!string.IsNullOrWhiteSpace(filterData.ShipmentNumber))
 656            {
 0657                var parts = GetShipmentNumbersFromFilter(filterData.ShipmentNumber);
 0658                if (parts.Count > 1)
 659                {
 0660                    orders = orders.Where(pr => parts.Any(part => ShipmentNumberMatchesSearch(pr.ShipmentNumber, part)))
 661                }
 0662                else if (parts.Count == 1)
 663                {
 0664                    orders = orders.Where(pr => ShipmentNumberMatchesSearch(pr.ShipmentNumber, parts[0]));
 665                }
 666                else
 667                {
 0668                    orders = orders.Where(pr => ShipmentNumberMatchesSearch(pr.ShipmentNumber, filterData.ShipmentNumber
 669                }
 670            }
 671
 0672            orders = _filter.FilterByString(orders, pr => pr.ProductName, filterData.ProductName);
 673
 0674            if (!string.IsNullOrEmpty(filterData.Article))
 675            {
 0676                orders = orders.Where(o =>
 0677                    GetArticleSearchTerms(filterData.Article)
 0678                        .Any(term => ArticleMatchesSearch(o.ProductKey, o.Article, term)));
 679            }
 680
 0681            return orders;
 682        }
 683
 684
 685        // Реализация интерфейса
 686
 687        public async Task<List<Order>> FilterByFilterData(List<Order> standartOrders, OrderFilterModel filterData)
 688
 689        {
 0690            return await FilterByFilterData(standartOrders, filterData, null);
 0691        }
 692
 693
 694        /// <summary>
 695        /// Применяет фильтры к IQueryable на уровне БД (максимальная оптимизация).
 696        /// Возвращает IQueryable для дальнейшего применения фильтров или загрузки.
 697        /// Поддерживает группировку по ShipmentId если включен флаг GroupByShipment.
 698        /// </summary>
 699        public IQueryable<Order> FilterByFilterDataQueryable(IQueryable<Order> ordersQuery, OrderFilterModel filterData,
 700            AccessProfile? accessProfile)
 701
 702        {
 4703            bool filterIsNull = AreAllPropertiesNull(filterData);
 704
 705
 4706            if (filterIsNull)
 707
 708            {
 0709                return ordersQuery.OrderByDescending(o => o.ProcessingDate);
 710            }
 711
 712
 713            // Применяем фильтр по архиву
 714
 4715            if (!filterData.IncludeArchived)
 716
 717            {
 4718                ordersQuery = ordersQuery.Where(o => o.OrderArchive == null);
 719            }
 720
 721
 722            // Применяем фильтрацию по правам доступа пользователя
 723
 4724            var scope = _dataScopeEvaluator.Evaluate(accessProfile);
 4725            if (!scope.IsUnrestricted)
 726            {
 0727                ordersQuery = _dataScopeEvaluator.ApplyToOrders(ordersQuery, scope);
 728            }
 729
 4730            if (filterData.HasOzonPromotion.HasValue)
 731            {
 0732                var hasPromotion = filterData.HasOzonPromotion.Value;
 0733                ordersQuery = ordersQuery.Where(order =>
 0734                    order.OzonPromotionSnapshot != null &&
 0735                    order.OzonPromotionSnapshot.HasAnyPromotion == hasPromotion);
 736            }
 737
 738
 739            // Фильтрация по строковым полям - применяем на уровне БД где возможно
 740
 4741            ordersQuery = _filter.FilterByString(ordersQuery, pr => pr.Key, filterData.Key);
 742
 4743            ordersQuery = ApplyPrimaryTextSearchQueryable(ordersQuery, filterData);
 744
 745            // Приоритетная фильтрация: сначала склад отгрузки (включая засыл), затем поставщик.
 4746            if (!string.IsNullOrEmpty(filterData.ShipmentWarehouse) && filterData.ShipmentWarehouse != "Все")
 747            {
 0748                ordersQuery = ordersQuery.Where(o =>
 0749                    (o.ShipmentWarehouse != null && o.ShipmentWarehouse.Name == filterData.ShipmentWarehouse) ||
 0750                    o.OrderExtensions.Any(e => e.Key == "Засыл" && e.Value == filterData.ShipmentWarehouse));
 751            }
 752
 4753            ordersQuery = _filter.FilterByStringExactSimple(ordersQuery,
 4754                pr => pr.Supplier != null ? pr.Supplier.Name : "", filterData.Supplier);
 755
 4756            ordersQuery = _filter.FilterByStringExactSimple(ordersQuery,
 4757                pr => pr.ClientStatus != null ? pr.ClientStatus.Name : null, filterData.Status);
 758
 4759            ordersQuery = _filter.FilterByStringExactSimple(ordersQuery,
 4760                pr => pr.AppStatus != null ? pr.AppStatus.Name : "", filterData.AppStatus);
 761
 4762            if (filterData.SupplierStatusId.HasValue)
 763            {
 0764                var supplierStatusId = filterData.SupplierStatusId.Value;
 0765                ordersQuery = ordersQuery.Where(o => _context.OrderSupplierStates.Any(state =>
 0766                    state.OrderId == o.Id &&
 0767                    state.ActiveOrderId != null &&
 0768                    state.SupplierStatusId == supplierStatusId));
 769            }
 770
 4771            ordersQuery = _filter.FilterByString(ordersQuery,
 4772                pr => pr.ProductInfo != null ? pr.ProductInfo.Barcode : null, filterData.Barcode);
 773
 4774            ordersQuery = _filter.FilterByStringExactSimple(ordersQuery,
 4775                pr => pr.ProductInfo != null ? pr.ProductInfo.CommercialCategory : "", filterData.CommercialCategory);
 776
 4777            ordersQuery = _filter.FilterByString(ordersQuery, pr => pr.NewCategory, filterData.NewCategory);
 778
 4779            ordersQuery = _filter.FilterByStringExactSimple(ordersQuery,
 4780                pr => pr.Сurrency != null ? pr.Сurrency.Name : "", filterData.Сurrency);
 781
 4782            ordersQuery = _filter.FilterByString(ordersQuery, pr => pr.DeliveryCity, filterData.DeliveryCity);
 783
 4784            ordersQuery = _filter.FilterByString(ordersQuery, pr => pr.OrderNumberToSupplier,
 4785                filterData.OrderNumberToSupplier);
 786
 4787            ordersQuery = _filter.FilterByStringExactSimple(ordersQuery,
 4788                pr => pr.EtProducer != null ? pr.EtProducer.Name : "", filterData.Manufacturer);
 789
 4790            ordersQuery = _filter.FilterByString(ordersQuery, pr => pr.OzonClient != null ? pr.OzonClient.Name : "",
 4791                filterData.OzonClient);
 792
 4793            ordersQuery = _filter.FilterByStringExactSimple(ordersQuery,
 4794                pr => pr.Delivery != null ? pr.Delivery.Provider.Name : "", filterData.Provider);
 795
 4796            ordersQuery = _filter.FilterByStringExactSimple(ordersQuery,
 4797                pr => pr.Delivery != null ? pr.Delivery.Name : "", filterData.DeliveryName);
 798
 4799            ordersQuery = _filter.FilterByString(ordersQuery, pr => pr.TrackingNumber, filterData.TrackingNumber);
 4800            ordersQuery = _filter.FilterByString(ordersQuery, pr => pr.Scanit, filterData.Scanit);
 801
 802
 803            // Фильтрация по датам
 804
 4805            ordersQuery = _filter.FilterByDate(ordersQuery, pr => pr.ProcessingDate, filterData.ProcessingDate);
 806
 807
 4808            ordersQuery = _filter.FilterByDate(ordersQuery, pr => pr.ShippingDate, filterData.ShippingDate);
 809
 4810            ordersQuery = _filter.FilterByDate(ordersQuery, pr => pr.LastStatusChangeDate,
 4811                filterData.LastStatusChangeDate);
 812
 813
 814            // Фильтрация по числовым полям
 815
 4816            ordersQuery = _filter.FilterByDecimal(ordersQuery, pr => pr.ProductInfo!.CurrentPriceWithDiscount,
 4817                filterData.CurrentPriceWithDiscount);
 818
 4819            ordersQuery = _filter.FilterByDecimal(ordersQuery, pr => pr.Quantity, filterData.Quantity, tolerance: 0);
 820
 4821            ordersQuery = _filter.FilterByDecimal(ordersQuery, pr => pr.ShipmentAmount, filterData.ShipmentAmount);
 822
 4823            ordersQuery = _filter.FilterByDecimal(ordersQuery, pr => pr.Price, filterData.Price);
 824
 4825            ordersQuery = _filter.FilterByDecimal(ordersQuery, pr => pr.PurchasePrice, filterData.PurchasePrice);
 826
 4827            ordersQuery = _filter.FilterByDecimal(ordersQuery, pr => pr.MinOzonCommission, filterData.OzonCommission);
 828
 4829            ordersQuery = _filter.FilterByDecimal(ordersQuery, pr => pr.MinProfit, filterData.Profit);
 4830            if (filterData.ProfitMin.HasValue)
 0831                ordersQuery = ordersQuery.Where(o => o.MinProfit > filterData.ProfitMin.Value);
 4832            if (filterData.ProfitMax.HasValue)
 0833                ordersQuery = ordersQuery.Where(o => o.MinProfit < filterData.ProfitMax.Value);
 4834            if (filterData.OnlyNegativeProfit)
 0835                ordersQuery = ordersQuery.Where(o => o.MinProfit < 0);
 4836            if (filterData.OnlyPositiveProfit)
 0837                ordersQuery = ordersQuery.Where(o => o.MinProfit > 0);
 838
 4839            ordersQuery = _filter.FilterByDecimal(ordersQuery, pr => pr.MinDiscount, filterData.Discount);
 4840            if (filterData.DiscountMin.HasValue)
 0841                ordersQuery = ordersQuery.Where(o => o.MinDiscount > filterData.DiscountMin.Value);
 4842            if (filterData.DiscountMax.HasValue)
 0843                ordersQuery = ordersQuery.Where(o => o.MinDiscount < filterData.DiscountMax.Value);
 4844            if (filterData.OnlyNegativeDiscount)
 0845                ordersQuery = ordersQuery.Where(o => o.MinDiscount < 0);
 846
 4847            ordersQuery = _filter.FilterByDecimal(ordersQuery, pr => pr.CostPrice, filterData.CostPrice);
 848
 4849            ordersQuery = _filter.FilterByDouble(ordersQuery, pr => pr.ProductInfo!.VolumetricWeight,
 4850                filterData.VolumetricWeight);
 851
 852            // Фильтрация по оставшимся дням до отгрузки (TimeLeftDay = (ShippingDate - Now).Days)
 4853            if (filterData.TimeLeftDay.HasValue)
 854            {
 0855                var today = DateTime.Today;
 0856                var targetDays = filterData.TimeLeftDay.Value;
 0857                if (targetDays == 0)
 858                {
 0859                    ordersQuery = ordersQuery.Where(o =>
 0860                        !o.ShippingDate.HasValue ||
 0861                        MySqlDbFunctionsExtensions.DateDiffDay(EF.Functions, today, o.ShippingDate.Value) == 0);
 862                }
 863                else
 864                {
 0865                    ordersQuery = ordersQuery.Where(o =>
 0866                        o.ShippingDate.HasValue &&
 0867                        MySqlDbFunctionsExtensions.DateDiffDay(EF.Functions, today, o.ShippingDate.Value) == targetDays)
 868                }
 869            }
 870
 871            // Фильтрация по сроку доставки (DeliveryPeriod = ShippingDate - ProcessingDate)
 4872            if (!string.IsNullOrEmpty(filterData.DeliveryPeriod))
 873            {
 0874                var day = StringToIntDay(filterData.DeliveryPeriod);
 0875                var hours = StringToIntHours(filterData.DeliveryPeriod);
 0876                if (day.HasValue && hours.HasValue)
 877                {
 0878                    var dayVal = day.Value;
 0879                    var hoursVal = hours.Value;
 0880                    ordersQuery = ordersQuery.Where(o =>
 0881                        o.ProcessingDate.HasValue && o.ShippingDate.HasValue &&
 0882                        MySqlDbFunctionsExtensions.DateDiffDay(EF.Functions, o.ProcessingDate.Value, o.ShippingDate.Valu
 0883                        MySqlDbFunctionsExtensions.DateDiffHour(EF.Functions, o.ProcessingDate.Value, o.ShippingDate.Val
 884                }
 885            }
 886
 887            // Фильтрация по наличию засыла
 4888            if (filterData.OnlyWithZasyl)
 889            {
 0890                ordersQuery = ordersQuery.Where(o =>
 0891                    o.OrderExtensions.Any(e => e.Key == "Засыл" && e.Value != null && e.Value != ""));
 892            }
 893
 894            // Фильтрация по возвратам
 4895            if (filterData.OnlyWithReturns)
 896            {
 0897                ordersQuery = ordersQuery.Where(o => o.IsReturned);
 898            }
 899
 900            // Фильтрация по штрихкоду возврата (LogisticBarcode из OrderReturns)
 4901            if (!string.IsNullOrWhiteSpace(filterData.ReturnLogisticBarcode))
 902            {
 0903                var barcode = filterData.ReturnLogisticBarcode.Trim();
 0904                ordersQuery = ordersQuery.Where(o =>
 0905                    _context.Set<OrderReturn>().Any(r => r.OrderId == o.Id && r.LogisticBarcode != null && r.LogisticBar
 906            }
 907
 908            // Фильтрация по статусу возврата (VisualStatusSysName из OrderReturns)
 4909            if (!string.IsNullOrWhiteSpace(filterData.ReturnStatusSysName))
 910            {
 0911                var returnStatusSysName = filterData.ReturnStatusSysName.Trim();
 0912                ordersQuery = ordersQuery.Where(o =>
 0913                    _context.Set<OrderReturn>().Any(r =>
 0914                        r.OrderId == o.Id &&
 0915                        r.VisualStatusSysName != null &&
 0916                        r.VisualStatusSysName == returnStatusSysName));
 917            }
 918
 919            // Фильтрация по складу возврата (PlaceName/TargetPlaceName из OrderReturns)
 4920            if (!string.IsNullOrWhiteSpace(filterData.ReturnWarehouseName))
 921            {
 0922                var returnWarehouseName = filterData.ReturnWarehouseName.Trim();
 0923                ordersQuery = ordersQuery.Where(o =>
 0924                    _context.Set<OrderReturn>().Any(r =>
 0925                        r.OrderId == o.Id &&
 0926                        (
 0927                            (r.PlaceName != null && r.PlaceName.Contains(returnWarehouseName)) ||
 0928                            (r.TargetPlaceName != null && r.TargetPlaceName.Contains(returnWarehouseName))
 0929                        )));
 930            }
 931
 932            // Фильтрация по дате изменения статуса возврата (VisualStatusChangeMoment из OrderReturns)
 4933            if (filterData.ReturnStatusChangeDate.HasValue)
 934            {
 0935                var returnStatusDate = filterData.ReturnStatusChangeDate.Value.Date;
 0936                ordersQuery = ordersQuery.Where(o =>
 0937                    _context.Set<OrderReturn>().Any(r =>
 0938                        r.OrderId == o.Id &&
 0939                        r.VisualStatusChangeMoment.HasValue &&
 0940                        r.VisualStatusChangeMoment.Value.Date == returnStatusDate));
 941            }
 942
 943            // Фильтрация по наличию трек-номера
 4944            if (filterData.OnlyWithTrackingNumber)
 945            {
 0946                ordersQuery = ordersQuery.Where(o => o.TrackingNumber != null && o.TrackingNumber != "");
 947            }
 948
 949            // Группировка по ShipmentId (оптимизированная версия на уровне БД)
 950
 4951            if (filterData.GroupByShipment)
 952
 953            {
 0954                ordersQuery = ApplyShipmentGroupingQueryable(ordersQuery, filterData.IncludeArchived);
 955            }
 956
 957            // Сортировка
 958
 4959            ordersQuery = ordersQuery.OrderByDescending(o => o.ProcessingDate);
 960
 4961            return ordersQuery;
 962        }
 963
 964
 965        /// <summary>
 966        /// Применяет фильтры и сортировку к IQueryable на уровне БД (максимальная оптимизация).
 967        /// Возвращает IQueryable для дальнейшего применения фильтров или загрузки.
 968        /// </summary>
 969        public IQueryable<Order> FilterAndSortQueryable(IQueryable<Order> ordersQuery, OrderFilterModel filterData,
 970            AccessProfile? accessProfile, OrderSortState sortOrder)
 971        {
 972            // Сначала применяем фильтрацию
 0973            ordersQuery = FilterByFilterDataQueryable(ordersQuery, filterData, accessProfile);
 974
 975            // Затем применяем сортировку на уровне БД
 0976            if (sortOrder != OrderSortState.StandardState && _sortManager != null)
 977            {
 0978                ordersQuery = _sortManager.ApplySortOrderQueryable(ordersQuery, sortOrder);
 979            }
 980
 0981            return ordersQuery;
 982        }
 983
 984        /// <summary>
 985        /// Гарантированно переустанавливает финальный ORDER BY непосредственно перед пагинацией —
 986        /// вызывать последним, после NarrowByElasticsearchAsync и применения RLS-скоупа. Те методы
 987        /// лишь дописывают Where к уже отсортированному запросу и в норме не должны ломать порядок,
 988        /// но это правило (сортировка по дате принятия, а не по релевантности ES) должно соблюдаться
 989        /// строго для всех поисков, поэтому порядок переустанавливается явно как последний шаг.
 990        /// </summary>
 991        public IQueryable<Order> ApplyFinalSortQueryable(IQueryable<Order> ordersQuery, OrderSortState sortOrder) =>
 2992            _sortManager != null
 2993                ? _sortManager.ApplySortOrderQueryable(ordersQuery, sortOrder)
 2994                : ordersQuery.OrderByDescending(o => o.ProcessingDate);
 995
 996
 997        /// <summary>
 998        /// Применяет сортировку к списку заказов в памяти.
 999        /// </summary>
 1000        public IEnumerable<Order> ApplySortOrder(IEnumerable<Order> orders, OrderSortState sortOrder)
 1001        {
 01002            if (_sortManager == null)
 1003            {
 01004                return orders.OrderByDescending(o => o.ProcessingDate);
 1005            }
 1006
 01007            return _sortManager.ApplySortOrder(orders, sortOrder);
 1008        }
 1009
 1010
 1011        /// <summary>
 1012        /// Получает данные ViewData для сортировки.
 1013        /// </summary>
 1014        public Dictionary<string, OrderSortState> GetSortOrderViewData(OrderSortState sortOrder)
 1015        {
 01016            if (_sortManager == null)
 1017            {
 01018                return new Dictionary<string, OrderSortState>();
 1019            }
 1020
 01021            return _sortManager.GetSortOrderViewData(sortOrder);
 1022        }
 1023
 1024
 1025        // Перегрузка с поддержкой прав доступа
 1026
 1027        public async Task<List<Order>> FilterByFilterData(List<Order> standartOrders, OrderFilterModel filterData,
 1028            AccessProfile? accessProfile, OrderSortState sortOrder)
 1029        {
 01030            var filtered = await FilterByFilterData(standartOrders, filterData, accessProfile);
 01031            return ApplySortOrder(filtered, sortOrder).ToList();
 01032        }
 1033
 1034
 1035        public async Task<List<Order>> FilterByFilterData(List<Order> standartOrders, OrderFilterModel filterData,
 1036            AccessProfile? accessProfile)
 1037        {
 01038            bool filterIsNull = AreAllPropertiesNull(filterData);
 1039
 1040
 01041            if (filterIsNull)
 1042
 1043            {
 1044                return standartOrders.OrderByDescending(o => o.ProcessingDate).ToList();
 1045            }
 1046
 1047
 1048            // ВАЖНО: Если есть фильтры, загружаем данные из БД, а не фильтруем переданные заказы
 1049
 1050            // Это обеспечивает корректную фильтрацию по всем данным из БД
 1051
 01052            bool hasOtherFilters = !filterIsNull; // Есть любые фильтры кроме проверки на null
 1053
 1054
 01055            if (hasOtherFilters)
 1056
 1057            {
 01058                IQueryable<Order> ordersQuery = _ordersDataServices.GetOrdersQueryable(filterData.IncludeArchived);
 01059                ordersQuery = FilterByFilterDataQueryable(ordersQuery, filterData, accessProfile);
 1060
 1061
 1062                // Получаем только ID отфильтрованных заказов (быстрый запрос)
 1063
 01064                var filteredOrderIds = await ordersQuery.Select(o => o.Id).ToListAsync();
 1065
 1066
 01067                if (!filteredOrderIds.Any())
 1068
 1069                {
 01070                    return new List<Order>();
 1071                }
 1072
 1073
 1074                // Загружаем полные данные только для отфильтрованных заказов БЕЗ OrderExtensions
 1075
 01076                var filteredOrders =
 01077                    await _ordersDataServices.GetOrdersByIds(filteredOrderIds, filterData.IncludeArchived);
 1078
 1079
 1080                // Применяем сложные фильтры, требующие OrderExtensions (засылы) в памяти
 1081
 01082                if (!string.IsNullOrEmpty(filterData.ShipmentWarehouse) && filterData.ShipmentWarehouse != "Все")
 1083
 1084                {
 1085                    // Загружаем OrderExtensions только для отфильтрованных заказов
 1086
 01087                    await _ordersDataServices.LoadOrderExtensionsForOrders(filteredOrders);
 1088
 1089
 1090                    // Применяем фильтр по складу с учетом засылов
 1091
 01092                    filteredOrders = filteredOrders.Where(pr =>
 01093                        (pr.ShipmentWarehouse != null && pr.ShipmentWarehouse.Name == filterData.ShipmentWarehouse) ||
 01094                        (pr.OrderExtensions != null && pr.OrderExtensions.Any(e =>
 01095                            e.Key == "Засыл" && e.Value == filterData.ShipmentWarehouse
 01096                        ))
 01097                    ).ToList();
 1098                }
 1099
 1100
 1101                // Применяем фильтры, которые требуют вычислений в памяти (TimeLeftDay, DeliveryPeriod)
 1102
 01103                if (filterData.TimeLeftDay.HasValue)
 1104
 1105                {
 01106                    filteredOrders = filteredOrders.Where(o => o.TimeLeftDay == filterData.TimeLeftDay.Value).ToList();
 1107                }
 1108
 1109
 01110                if (!string.IsNullOrEmpty(filterData.DeliveryPeriod))
 1111
 1112                {
 01113                    var day = StringToIntDay(filterData.DeliveryPeriod);
 1114
 01115                    var hours = StringToIntHours(filterData.DeliveryPeriod);
 1116
 01117                    if (day.HasValue && hours.HasValue)
 1118
 1119                    {
 01120                        filteredOrders = filteredOrders.Where(o =>
 01121                            o.DeliveryPeriod.HasValue &&
 01122                            o.DeliveryPeriod.Value.Days == day.Value &&
 01123                            o.DeliveryPeriod.Value.Hours == hours.Value
 01124                        ).ToList();
 1125                    }
 1126                }
 1127
 1128
 1129                // Сортируем по исходному порядку filteredOrderIds
 1130
 1131                var orderDict = filteredOrders.ToDictionary(o => o.Id);
 1132
 01133                filteredOrders = filteredOrderIds
 01134                    .Select(id => orderDict.GetValueOrDefault(id))
 01135                    .OfType<Order>()
 01136                    .ToList();
 1137
 1138
 1139                // Применяем группировку по ShipmentId если включено
 1140
 01141                if (filterData.GroupByShipment && filteredOrders.Any())
 1142
 1143                {
 01144                    filteredOrders = await ExpandByShipmentIdAsync(filteredOrders, filterData.IncludeArchived);
 1145                }
 1146
 1147
 1148                return filteredOrders.OrderByDescending(o => o.ProcessingDate).ToList();
 1149            }
 1150
 1151
 1152            // Если нет фильтров, используем существующую логику фильтрации по переданным заказам
 1153
 1154            // ОПТИМИЗАЦИЯ: Используем уже загруженные заказы из кэша
 1155
 01156            IQueryable<Order> orders = standartOrders.AsQueryable();
 1157
 1158
 1159            // Применяем фильтр по архиву к уже загруженным заказам (если указан)
 1160
 01161            if (!filterData.IncludeArchived)
 1162
 1163            {
 01164                orders = orders.Where(o => o.OrderArchive == null);
 1165            }
 1166
 1167
 1168            // Применяем фильтрацию по правам доступа пользователя
 1169
 01170            var scopeInMemory = _dataScopeEvaluator.Evaluate(accessProfile);
 01171            if (!scopeInMemory.IsUnrestricted)
 1172            {
 01173                orders = _dataScopeEvaluator.ApplyToOrders(orders, scopeInMemory);
 1174            }
 1175
 1176
 1177            // Фильтрация по строковым полям - точное совпадение для связанных полей
 1178
 01179            orders = _filter.FilterByString(orders, pr => pr.Key, filterData.Key);
 1180
 01181            orders = ApplyPrimaryTextSearchInMemory(orders, filterData);
 1182
 1183            // Приоритетная фильтрация: сначала склад отгрузки, затем поставщик.
 01184            if (!string.IsNullOrEmpty(filterData.ShipmentWarehouse) && filterData.ShipmentWarehouse != "Все")
 1185            {
 01186                orders = orders.Where(pr =>
 01187
 01188                    // Обычный склад
 01189                    (pr.ShipmentWarehouse != null && pr.ShipmentWarehouse.Name == filterData.ShipmentWarehouse) ||
 01190
 01191                    // Или засыл на этот склад
 01192                    (pr.OrderExtensions != null && pr.OrderExtensions.Any(e =>
 01193                        e.Key == "Засыл" && e.Value == filterData.ShipmentWarehouse
 01194                    ))
 01195                );
 1196            }
 1197
 01198            orders = _filter.FilterByStringExactSimple(orders, pr => pr.Supplier != null ? pr.Supplier.Name : "",
 01199                filterData.Supplier);
 1200
 1201
 01202            orders = _filter.FilterByStringExactSimple(orders,
 01203                pr => pr.ClientStatus != null ? pr.ClientStatus.Name : null, filterData.Status);
 1204
 01205            orders = _filter.FilterByStringExactSimple(orders, pr => pr.AppStatus != null ? pr.AppStatus.Name : "",
 01206                filterData.AppStatus);
 1207
 01208            orders = _filter.FilterByString(orders, pr => pr.ProductInfo != null ? pr.ProductInfo.Barcode : null,
 01209                filterData.Barcode);
 1210
 01211            orders = _filter.FilterByStringExactSimple(orders,
 01212                pr => pr.ProductInfo != null ? pr.ProductInfo.CommercialCategory : "", filterData.CommercialCategory);
 1213
 01214            orders = _filter.FilterByString(orders, pr => pr.NewCategory, filterData.NewCategory);
 1215
 01216            orders = _filter.FilterByStringExactSimple(orders, pr => pr.Сurrency != null ? pr.Сurrency.Name : "",
 01217                filterData.Сurrency);
 1218
 01219            orders = _filter.FilterByString(orders, pr => pr.DeliveryCity, filterData.DeliveryCity);
 1220
 01221            orders = _filter.FilterByString(orders, pr => pr.OrderNumberToSupplier, filterData.OrderNumberToSupplier);
 01222            orders = _filter.FilterByString(orders, pr => pr.Scanit, filterData.Scanit);
 1223
 01224            orders = _filter.FilterByStringExactSimple(orders,
 01225                pr => pr.EtProducer != null ? pr.EtProducer.Name : "",
 01226                filterData.Manufacturer);
 1227
 01228            orders = _filter.FilterByString(orders, pr => pr.OzonClient != null ? pr.OzonClient.Name : "",
 01229                filterData.OzonClient);
 1230
 01231            orders = _filter.FilterByStringExactSimple(orders,
 01232                pr => pr.Delivery != null ? pr.Delivery.Provider.Name : "",
 01233                filterData.Provider);
 1234
 1235
 1236            // Фильтрация по датам
 1237
 01238            orders = _filter.FilterByDate(orders, pr => pr.ProcessingDate, filterData.ProcessingDate);
 1239
 01240            orders = _filter.FilterByDate(orders, pr => pr.ShippingDate, filterData.ShippingDate);
 1241
 01242            orders = _filter.FilterByDate(orders, pr => pr.LastStatusChangeDate, filterData.LastStatusChangeDate);
 1243
 1244
 1245            // Фильтрация по числовым полям
 1246
 01247            orders = _filter.FilterByDecimal(orders, pr => pr.ProductInfo!.CurrentPriceWithDiscount,
 01248                filterData.CurrentPriceWithDiscount);
 1249
 01250            orders = _filter.FilterByDecimal(orders, pr => pr.Quantity, filterData.Quantity, tolerance: 0);
 1251
 01252            orders = _filter.FilterByDecimal(orders, pr => pr.ShipmentAmount, filterData.ShipmentAmount);
 1253
 01254            orders = _filter.FilterByDecimal(orders, pr => pr.Price, filterData.Price);
 1255
 01256            orders = _filter.FilterByDecimal(orders, pr => pr.PurchasePrice, filterData.PurchasePrice);
 1257
 01258            orders = _filter.FilterByDecimal(orders, pr => pr.MinOzonCommission, filterData.OzonCommission);
 1259
 01260            orders = _filter.FilterByDecimal(orders, pr => pr.MinProfit, filterData.Profit);
 1261
 01262            orders = _filter.FilterByDecimal(orders, pr => pr.MinDiscount, filterData.Discount);
 1263
 01264            orders = _filter.FilterByDecimal(orders, pr => pr.CostPrice, filterData.CostPrice);
 1265
 1266
 01267            orders = _filter.FilterByDouble(orders, pr => pr.ProductInfo!.VolumetricWeight, filterData.VolumetricWeight)
 1268
 1269
 1270            // Фильтрация по целым числам (преобразование в List)
 1271
 01272            List<Order> result = _forTransactionOrdersFilter
 1273                .FilterByInt(orders.ToList(), pr => pr.TimeLeftDay, filterData.TimeLeftDay).ToList();
 1274
 1275
 1276            // Фильтрация по периоду доставки
 1277
 01278            if (filterData.DeliveryPeriod != null)
 1279
 1280            {
 01281                var filterByDay = result
 01282                    .AsEnumerable()
 01283                    .Where(o => o.DeliveryPeriod.HasValue &&
 01284                                o.DeliveryPeriod.Value.Days == StringToIntDay(filterData.DeliveryPeriod));
 1285
 1286
 01287                result = filterByDay
 01288                    .Where(o => o.DeliveryPeriod.HasValue &&
 01289                                o.DeliveryPeriod.Value.Hours == StringToIntHours(filterData.DeliveryPeriod))
 01290                    .ToList();
 1291            }
 1292
 1293
 1294            return result.OrderBy(o => o.ProcessingDate).ToList();
 01295        }
 1296
 1297
 1298        public async Task<List<Order>> FilterTransactionsOrdersByFilterData(List<Order> orders,
 1299            OrderFilterModel filterData)
 1300
 1301        {
 01302            await Task.CompletedTask;
 01303            bool filterIsNull = AreAllPropertiesNull(filterData);
 1304
 01305            bool exactMatch = true;
 1306
 1307
 01308            if (filterIsNull)
 1309
 1310            {
 1311                return orders.OrderBy(pr => pr.ProcessingDate).ToList();
 1312            }
 1313
 1314
 1315            orders = _forTransactionOrdersFilter.FilterByString(orders, pr => pr.Key, filterData.Key).ToList();
 1316
 01317            if (!string.IsNullOrWhiteSpace(filterData.ShipmentNumber))
 1318            {
 01319                var parts = GetShipmentNumbersFromFilter(filterData.ShipmentNumber);
 01320                if (parts.Count > 1)
 1321                {
 01322                    orders = orders.Where(pr => parts.Any(part => ShipmentNumberMatchesSearch(pr.ShipmentNumber, part)))
 1323                }
 01324                else if (parts.Count == 1)
 1325                {
 01326                    orders = orders.Where(pr => ShipmentNumberMatchesSearch(pr.ShipmentNumber, parts[0])).ToList();
 1327                }
 1328                else
 1329                {
 01330                    orders = orders.Where(pr => ShipmentNumberMatchesSearch(pr.ShipmentNumber, filterData.ShipmentNumber
 1331                }
 1332            }
 1333
 1334            // Приоритетная фильтрация: сначала склад, затем поставщик.
 01335            orders = _forTransactionOrdersFilter.FilterByString(orders,
 1336                pr => pr.ShipmentWarehouse?.Name ?? string.Empty, filterData.ShipmentWarehouse, exactMatch).ToList();
 1337
 1338            orders = _forTransactionOrdersFilter.FilterByString(orders, pr => pr.Supplier?.Name ?? string.Empty,
 01339                filterData.Supplier, exactMatch).ToList();
 1340
 1341
 01342            orders = _forTransactionOrdersFilter.FilterByString(orders,
 1343                    pr => pr.ClientStatus != null ? pr.ClientStatus.Name : null, filterData.Status, exactMatch)
 01344                .ToList();
 1345
 1346            orders = _forTransactionOrdersFilter.FilterByString(orders, pr => pr.AppStatus?.Name ?? string.Empty,
 01347                filterData.AppStatus, exactMatch).ToList();
 1348
 1349            orders = _forTransactionOrdersFilter.FilterByString(orders, pr => pr.ProductName, filterData.ProductName)
 01350                .ToList();
 1351
 1352
 1353            // Фильтрация по артикулу с учетом связей продуктов
 1354
 01355            if (!string.IsNullOrEmpty(filterData.Article))
 1356
 1357            {
 01358                orders = orders
 01359                    .Where(o => GetArticleSearchTerms(filterData.Article)
 01360                        .Any(term => ArticleMatchesSearch(o.ProductKey, o.Article, term)))
 01361                    .ToList();
 1362            }
 1363
 1364
 01365            if (!string.IsNullOrEmpty(filterData.Barcode))
 1366
 1367            {
 01368                orders = orders.Where(pr => pr.ProductInfo != null &&
 01369                                            !string.IsNullOrEmpty(pr.ProductInfo.Barcode) &&
 01370                                            pr.ProductInfo.Barcode.Contains(filterData.Barcode)).ToList();
 1371            }
 1372
 1373
 01374            orders = _forTransactionOrdersFilter.FilterByString(orders,
 1375                    pr => pr.ProductInfo?.CommercialCategory ?? string.Empty, filterData.CommercialCategory, exactMatch)
 01376                .ToList();
 1377
 1378            orders = _forTransactionOrdersFilter.FilterByString(orders, pr => pr.NewCategory, filterData.NewCategory)
 01379                .ToList();
 1380
 1381            orders = _forTransactionOrdersFilter.FilterByString(orders, pr => pr.Сurrency?.Name ?? string.Empty,
 01382                filterData.Сurrency, exactMatch).ToList();
 1383
 1384            orders = _forTransactionOrdersFilter.FilterByString(orders, pr => pr.DeliveryCity, filterData.DeliveryCity)
 01385                .ToList();
 1386
 01387            orders = _forTransactionOrdersFilter
 1388                .FilterByString(orders, pr => pr.OrderNumberToSupplier, filterData.OrderNumberToSupplier).ToList();
 1389
 1390            orders = _forTransactionOrdersFilter.FilterByString(orders, pr => pr.EtProducer?.Name ?? string.Empty,
 01391                filterData.Manufacturer, exactMatch).ToList();
 1392
 1393
 01394            orders = _forTransactionOrdersFilter
 1395                .FilterByString(orders, pr => pr.OzonClient?.Name ?? string.Empty, filterData.OzonClient).ToList();
 1396
 1397
 01398            orders = _forTransactionOrdersFilter
 1399                .FilterByDate(orders, pr => pr.ProcessingDate, filterData.ProcessingDate).ToList();
 1400
 1401            orders = _forTransactionOrdersFilter.FilterByDate(orders, pr => pr.ShippingDate, filterData.ShippingDate)
 01402                .ToList();
 1403
 01404            orders = _forTransactionOrdersFilter
 1405                .FilterByDate(orders, pr => pr.LastStatusChangeDate, filterData.LastStatusChangeDate).ToList();
 1406
 1407
 01408            orders = _forTransactionOrdersFilter.FilterByDecimal(orders,
 1409                pr => pr.ProductInfo?.CurrentPriceWithDiscount ?? 0, filterData.CurrentPriceWithDiscount).ToList();
 1410
 01411            orders = _forTransactionOrdersFilter
 1412                .FilterByDecimal(orders, pr => pr.Quantity, filterData.Quantity, tolerance: 0).ToList();
 1413
 01414            orders = _forTransactionOrdersFilter
 1415                .FilterByDecimal(orders, pr => pr.ShipmentAmount, filterData.ShipmentAmount).ToList();
 1416
 1417            orders = _forTransactionOrdersFilter.FilterByDecimal(orders, pr => pr.Price, filterData.Price).ToList();
 1418
 01419            orders = _forTransactionOrdersFilter
 1420                .FilterByDecimal(orders, pr => pr.PurchasePrice, filterData.PurchasePrice).ToList();
 1421
 01422            orders = _forTransactionOrdersFilter
 1423                .FilterByDecimal(orders, pr => pr.MinOzonCommission, filterData.OzonCommission).ToList();
 1424
 1425            orders = _forTransactionOrdersFilter.FilterByDecimal(orders, pr => pr.MinProfit, filterData.Profit)
 01426                .ToList();
 01427            if (filterData.ProfitMin.HasValue)
 01428                orders = orders.Where(o => o.MinProfit > filterData.ProfitMin.Value).ToList();
 01429            if (filterData.ProfitMax.HasValue)
 01430                orders = orders.Where(o => o.MinProfit < filterData.ProfitMax.Value).ToList();
 01431            if (filterData.OnlyNegativeProfit)
 1432                orders = orders.Where(o => o.MinProfit < 0).ToList();
 01433            if (filterData.OnlyPositiveProfit)
 1434                orders = orders.Where(o => o.MinProfit > 0).ToList();
 1435
 1436            orders = _forTransactionOrdersFilter.FilterByDecimal(orders, pr => pr.MinDiscount, filterData.Discount)
 01437                .ToList();
 01438            if (filterData.DiscountMin.HasValue)
 01439                orders = orders.Where(o => o.MinDiscount > filterData.DiscountMin.Value).ToList();
 01440            if (filterData.DiscountMax.HasValue)
 01441                orders = orders.Where(o => o.MinDiscount < filterData.DiscountMax.Value).ToList();
 01442            if (filterData.OnlyNegativeDiscount)
 1443                orders = orders.Where(o => o.MinDiscount < 0).ToList();
 1444
 1445            orders = _forTransactionOrdersFilter.FilterByDecimal(orders, pr => pr.CostPrice, filterData.CostPrice)
 01446                .ToList();
 1447
 1448
 1449            orders = _forTransactionOrdersFilter.FilterByDouble(orders, pr => pr.ProductInfo?.VolumetricWeight ?? 0,
 01450                filterData.VolumetricWeight).ToList();
 1451
 1452
 1453            orders = _forTransactionOrdersFilter.FilterByInt(orders, pr => pr.TimeLeftDay, filterData.TimeLeftDay)
 01454                .ToList();
 1455
 1456
 1457            List<Order> result;
 1458
 1459
 01460            if (filterData.DeliveryPeriod != null)
 1461
 1462            {
 01463                var filterByDay = orders
 01464                    .AsEnumerable()
 01465                    .Where(o => o.DeliveryPeriod.HasValue &&
 01466                                o.DeliveryPeriod.Value.Days == StringToIntDay(filterData.DeliveryPeriod));
 1467
 1468
 01469                result = filterByDay
 01470                    .Where(o => o.DeliveryPeriod.HasValue &&
 01471                                o.DeliveryPeriod.Value.Hours == StringToIntHours(filterData.DeliveryPeriod))
 01472                    .ToList();
 1473            }
 1474
 1475            else
 1476
 1477            {
 1478                result = orders.OrderBy(o => o.ProcessingDate).ToList();
 1479            }
 1480
 1481
 1482            return result.OrderBy(o => o.ProcessingDate).ToList();
 1483
 1484            ;
 01485        }
 1486
 1487
 1488        // Фильтрация только по переданному списку, без БД
 1489
 1490        public List<Order> FilterByFilterDataFromList(
 1491            List<Order> standartOrders,
 1492            OrderFilterModel? filterData,
 1493            AccessProfile? accessProfile)
 1494
 1495        {
 01496            bool filterIsNull = AreAllPropertiesNull(filterData);
 1497
 1498
 01499            if (filterIsNull)
 1500
 1501            {
 01502                return standartOrders
 01503                    .OrderByDescending(o => o.ProcessingDate)
 01504                    .ToList();
 1505            }
 1506
 01507            filterData ??= new OrderFilterModel();
 1508
 01509            IQueryable<Order> orders = standartOrders.AsQueryable();
 1510
 1511
 1512            // Фильтрация по правам доступа
 1513
 01514            var scopeFromList = _dataScopeEvaluator.Evaluate(accessProfile);
 01515            if (!scopeFromList.IsUnrestricted)
 1516            {
 01517                orders = _dataScopeEvaluator.ApplyToOrders(orders, scopeFromList);
 1518            }
 1519
 1520
 1521            // Строковые поля
 1522
 01523            orders = _filter.FilterByString(orders, pr => pr.Key, filterData.Key);
 1524
 01525            if (!string.IsNullOrWhiteSpace(filterData.ShipmentNumber))
 1526            {
 01527                var parts = GetShipmentNumbersFromFilter(filterData.ShipmentNumber);
 01528                if (parts.Count > 1)
 1529                {
 01530                    orders = _filter.FilterByStringList(orders, pr => pr.ShipmentNumber, parts, true);
 1531                }
 01532                else if (parts.Count == 1)
 1533                {
 01534                    orders = _filter.FilterByString(orders, pr => pr.ShipmentNumber, parts[0]);
 1535                }
 1536                else
 1537                {
 01538                    orders = _filter.FilterByString(orders, pr => pr.ShipmentNumber, filterData.ShipmentNumber);
 1539                }
 1540            }
 1541
 1542            // Приоритетная фильтрация: сначала склад + засыл, затем поставщик.
 01543            if (!string.IsNullOrEmpty(filterData.ShipmentWarehouse) &&
 01544                filterData.ShipmentWarehouse != "Все")
 1545
 1546            {
 01547                orders = orders.Where(pr =>
 01548                    (pr.ShipmentWarehouse != null &&
 01549                     pr.ShipmentWarehouse.Name == filterData.ShipmentWarehouse) ||
 01550                    (pr.OrderExtensions != null &&
 01551                     pr.OrderExtensions.Any(e =>
 01552                         e.Key == "Засыл" &&
 01553                         e.Value == filterData.ShipmentWarehouse))
 01554                );
 1555            }
 1556
 01557            orders = _filter.FilterByStringExactSimple(
 01558                orders,
 01559                pr => pr.Supplier != null ? pr.Supplier.Name : "",
 01560                filterData.Supplier);
 1561
 1562
 01563            orders = _filter.FilterByStringExactSimple(orders,
 01564                pr => pr.ClientStatus != null ? pr.ClientStatus.Name : null, filterData.Status);
 1565
 01566            orders = _filter.FilterByStringExactSimple(
 01567                orders,
 01568                pr => pr.AppStatus != null ? pr.AppStatus.Name : "",
 01569                filterData.AppStatus);
 1570
 01571            if (filterData.SupplierStatusId.HasValue)
 1572            {
 01573                var supplierStatusId = filterData.SupplierStatusId.Value;
 01574                var orderIdsWithStatus = _context.OrderSupplierStates
 01575                    .AsNoTracking()
 01576                    .Where(state => state.ActiveOrderId != null && state.SupplierStatusId == supplierStatusId)
 01577                    .Select(state => state.OrderId)
 01578                    .ToHashSet();
 1579
 01580                orders = orders.Where(order => orderIdsWithStatus.Contains(order.Id));
 1581            }
 1582
 1583
 01584            orders = _filter.FilterByString(orders, pr => pr.ProductName, filterData.ProductName);
 1585
 1586
 1587            // Фильтрация по артикулу с учетом связей продуктов
 1588
 01589            if (!string.IsNullOrEmpty(filterData.Article))
 1590
 1591            {
 01592                orders = orders.Where(o =>
 01593                    GetArticleSearchTerms(filterData.Article)
 01594                        .Any(term => ArticleMatchesSearch(o.ProductKey, o.Article, term)));
 1595            }
 1596
 1597
 01598            orders = _filter.FilterByString(orders, pr => pr.ProductInfo != null ? pr.ProductInfo.Barcode : null,
 01599                filterData.Barcode);
 1600
 01601            orders = _filter.FilterByStringExactSimple(
 01602                orders,
 01603                pr => pr.ProductInfo != null ? pr.ProductInfo.CommercialCategory : "",
 01604                filterData.CommercialCategory);
 1605
 1606
 01607            orders = _filter.FilterByString(orders, pr => pr.NewCategory, filterData.NewCategory);
 1608
 01609            orders = _filter.FilterByStringExactSimple(
 01610                orders,
 01611                pr => pr.Сurrency != null ? pr.Сurrency.Name : "",
 01612                filterData.Сurrency);
 1613
 1614
 01615            orders = _filter.FilterByString(orders, pr => pr.DeliveryCity, filterData.DeliveryCity);
 1616
 01617            orders = _filter.FilterByString(orders, pr => pr.OrderNumberToSupplier, filterData.OrderNumberToSupplier);
 01618            orders = _filter.FilterByString(orders, pr => pr.Scanit, filterData.Scanit);
 1619
 1620
 01621            orders = _filter.FilterByStringExactSimple(
 01622                orders,
 01623                pr => pr.EtProducer != null ? pr.EtProducer.Name : "",
 01624                filterData.Manufacturer);
 1625
 1626
 01627            orders = _filter.FilterByString(
 01628                orders,
 01629                pr => pr.OzonClient != null ? pr.OzonClient.Name : "",
 01630                filterData.OzonClient);
 1631
 1632
 01633            orders = _filter.FilterByStringExactSimple(
 01634                orders,
 01635                pr => pr.Delivery != null ? pr.Delivery.Provider.Name : "",
 01636                filterData.Provider);
 1637
 1638
 1639            // Даты
 1640
 01641            orders = _filter.FilterByDate(orders, pr => pr.ProcessingDate, filterData.ProcessingDate);
 1642
 01643            orders = _filter.FilterByDate(orders, pr => pr.ShippingDate, filterData.ShippingDate);
 1644
 01645            orders = _filter.FilterByDate(orders, pr => pr.LastStatusChangeDate, filterData.LastStatusChangeDate);
 1646
 1647
 1648            // Числовые
 1649
 01650            orders = _filter.FilterByDecimal(
 01651                orders,
 01652                pr => pr.ProductInfo!.CurrentPriceWithDiscount,
 01653                filterData.CurrentPriceWithDiscount);
 1654
 1655
 01656            orders = _filter.FilterByDecimal(orders, pr => pr.Quantity, filterData.Quantity, tolerance: 0);
 1657
 01658            orders = _filter.FilterByDecimal(orders, pr => pr.ShipmentAmount, filterData.ShipmentAmount);
 1659
 01660            orders = _filter.FilterByDecimal(orders, pr => pr.Price, filterData.Price);
 1661
 01662            orders = _filter.FilterByDecimal(orders, pr => pr.PurchasePrice, filterData.PurchasePrice);
 1663
 01664            orders = _filter.FilterByDecimal(orders, pr => pr.MinOzonCommission, filterData.OzonCommission);
 1665
 01666            orders = _filter.FilterByDecimal(orders, pr => pr.MinProfit, filterData.Profit);
 1667
 01668            orders = _filter.FilterByDecimal(orders, pr => pr.MinDiscount, filterData.Discount);
 1669
 01670            orders = _filter.FilterByDecimal(orders, pr => pr.CostPrice, filterData.CostPrice);
 1671
 1672
 01673            orders = _filter.FilterByDouble(
 01674                orders,
 01675                pr => pr.ProductInfo!.VolumetricWeight,
 01676                filterData.VolumetricWeight);
 1677
 1678
 1679            // Фильтрация по int (через List)
 1680
 01681            List<Order> result = _forTransactionOrdersFilter
 01682                .FilterByInt(orders.ToList(), pr => pr.TimeLeftDay, filterData.TimeLeftDay)
 01683                .ToList();
 1684
 1685
 1686            // Период доставки
 1687
 01688            if (filterData.DeliveryPeriod != null)
 1689
 1690            {
 01691                result = result
 01692                    .Where(o => o.DeliveryPeriod.HasValue &&
 01693                                o.DeliveryPeriod.Value.Days ==
 01694                                StringToIntDay(filterData.DeliveryPeriod) &&
 01695                                o.DeliveryPeriod.Value.Hours ==
 01696                                StringToIntHours(filterData.DeliveryPeriod))
 01697                    .ToList();
 1698            }
 1699
 1700
 01701            return result
 01702                .OrderBy(o => o.ProcessingDate)
 01703                .ToList();
 1704        }
 1705
 1706
 1707        public async Task<IEnumerable<Order>> FilterFromFile(bool filter)
 1708
 1709        {
 01710            IQueryable<Order> orders = await _ordersDataServices.GetOrders();
 1711
 1712
 01713            return orders.Where(o => o.FromFile == filter).OrderBy(pr => pr.ProcessingDate).ToList();
 01714        }
 1715
 1716
 1717
 1718
 1719        /// <summary>
 1720        /// Получает список всех складов включая засылы для отображения в фильтре
 1721        /// </summary>
 1722        public async Task<List<string>> GetWarehousesWithZasylAsync()
 1723
 1724        {
 01725            var warehouses = await _context.Warehouses
 01726                .Where(w => w.Name != null)
 01727                .Select(w => w.Name!)
 01728                .ToListAsync();
 1729
 1730
 01731            var zasylValues = await _ordersDataServices.GetAllZasylValuesAsync();
 1732
 1733
 1734            // Объединяем обычные склады и засылы, убираем дубликаты
 1735
 01736            var allWarehouses = warehouses.Union(zasylValues)
 1737                .Where(w => !string.IsNullOrEmpty(w))
 01738                .Distinct()
 1739                .OrderBy(w => w)
 01740                .ToList();
 1741
 1742
 01743            return allWarehouses;
 01744        }
 1745
 1746
 1747        public bool AreAllPropertiesNull(OrderFilterModel? model)
 1748
 1749        {
 101750            if (model == null)
 1751
 11752                return true;
 1753
 1754
 91755            PropertyInfo[] properties = typeof(OrderFilterModel).GetProperties();
 1756
 1757
 4831758            foreach (PropertyInfo prop in properties)
 1759
 1760            {
 1761                // Игнорируем служебные поля, которые не являются фильтрами данных
 1762
 2361763                if (prop.Name == "IncludeArchived" ||
 2361764                    prop.Name == "ArticleExactMatch" || prop.Name == "GroupByShipment" ||
 2361765                    prop.Name == "SyncClientStatusesOnLoad")
 1766
 1767                    continue;
 1768
 1769
 2241770                object? value = prop.GetValue(model);
 1771
 1772
 1773                // Проверяем, не является ли значение null или пустой строкой
 1774
 2241775                if (value != null)
 1776
 1777                {
 1778                    // Для строк проверяем, не пустая ли она
 1779
 241780                    if (value is string stringValue && string.IsNullOrWhiteSpace(stringValue))
 1781
 1782                        continue;
 1783
 1784
 1785                    // Для числовых типов можно также проверять на 0, если нужно
 1786
 1787                    // Например, если свойство Price имеет значение 0, это тоже можно считать отсутствием фильтра
 1788
 231789                    if (value is int intValue && intValue == 0)
 1790
 1791                        continue;
 1792
 1793
 231794                    if (value is decimal decimalValue && decimalValue == 0)
 1795
 1796                        continue;
 1797
 1798
 231799                    if (value is double doubleValue && doubleValue == 0)
 1800
 1801                        continue;
 1802
 1803                    // bool = false (default) is not an active filter
 231804                    if (value is bool boolValue && !boolValue)
 1805
 1806                        continue;
 1807
 1808
 71809                    return false;
 1810                }
 1811            }
 1812
 1813
 21814            return true;
 1815        }
 1816
 1817
 1818        public int? StringToIntDay(string deliveryPeriod)
 1819
 1820        {
 61821            if (deliveryPeriod != null)
 1822
 1823            {
 51824                MatchCollection matches = Regex.Matches(deliveryPeriod, @"\d+");
 1825
 1826
 51827                if (matches.Count == 2)
 1828
 1829                {
 21830                    return int.Parse(matches[0].Value);
 1831
 1832                    ;
 1833                }
 1834
 1835                else
 1836
 1837                {
 31838                    return null;
 1839                }
 1840            }
 1841
 1842            else
 1843
 1844            {
 11845                return null;
 1846            }
 1847        }
 1848
 1849
 1850        public int? StringToIntHours(string deliveryPeriod)
 1851
 1852        {
 21853            if (deliveryPeriod != null)
 1854
 1855            {
 21856                MatchCollection matches = Regex.Matches(deliveryPeriod, @"\d+");
 1857
 1858
 21859                if (matches.Count == 2)
 1860
 1861                {
 21862                    return int.Parse(matches[1].Value);
 1863                }
 1864
 1865                else
 1866
 1867                {
 01868                    return null;
 1869                }
 1870            }
 1871
 1872            else
 1873
 1874            {
 01875                return null;
 1876            }
 1877        }
 1878
 1879
 1880        public List<Order> FilterOrdersByColumns(
 1881            Dictionary<string, string> filters,
 1882            IEnumerable<Order> orders,
 1883            List<string>? priorityColumns = null)
 1884
 1885        {
 01886            if (filters == null || !filters.Any() || orders == null)
 1887
 01888                return orders?.ToList() ?? new List<Order>();
 1889
 1890
 01891            var filteredOrders = orders
 01892                .Where(order => ApplyFilters(order, filters))
 01893                .ToList();
 1894
 1895
 01896            if (priorityColumns != null && priorityColumns.Any())
 1897
 1898            {
 01899                filteredOrders = filteredOrders
 01900                    .OrderByDescending(order => CountNonEmptyPriorityFields(order, priorityColumns))
 01901                    .ToList();
 1902            }
 1903
 1904
 01905            return filteredOrders;
 1906        }
 1907
 1908
 1909        private bool ApplyFilters(Order order, Dictionary<string, string> filters)
 1910
 1911        {
 01912            foreach (var filter in filters)
 1913
 1914            {
 01915                if (string.IsNullOrWhiteSpace(filter.Value))
 1916
 1917                    continue;
 1918
 1919
 01920                var mappingKey = ColumnMappings.Keys
 01921                    .FirstOrDefault(k => k.Equals(filter.Key.Trim(), StringComparison.OrdinalIgnoreCase));
 1922
 1923
 01924                if (mappingKey == null || !ColumnMappings.TryGetValue(mappingKey, out var selector))
 1925
 01926                    return false;
 1927
 1928
 01929                var value = selector(order);
 1930
 1931
 01932                if (value == null)
 1933
 01934                    return false;
 1935
 1936
 01937                var normalizedValue = NormalizeString(value.ToString());
 1938
 01939                var normalizedFilter = NormalizeString(filter.Value);
 1940
 1941
 01942                if (!normalizedValue.Contains(normalizedFilter) && !normalizedFilter.Contains(normalizedValue))
 1943
 01944                    return false;
 1945            }
 1946
 1947
 01948            return true;
 01949        }
 1950
 1951
 1952        private int CountNonEmptyPriorityFields(Order order, List<string> priorityColumns)
 1953
 1954        {
 01955            int count = 0;
 1956
 1957
 01958            foreach (var column in priorityColumns)
 1959
 1960            {
 01961                var mappingKey = ColumnMappings.Keys
 01962                    .FirstOrDefault(k => k.Equals(column.Trim(), StringComparison.OrdinalIgnoreCase));
 1963
 1964
 01965                if (mappingKey != null && ColumnMappings.TryGetValue(mappingKey, out var selector))
 1966
 1967                {
 01968                    var value = selector(order);
 1969
 01970                    if (value != null && !string.IsNullOrWhiteSpace(value.ToString()))
 1971
 1972                    {
 01973                        count++;
 1974                    }
 1975                }
 1976            }
 1977
 1978
 01979            return count;
 1980        }
 1981
 1982
 1983        private string NormalizeString(string? input)
 1984
 1985        {
 01986            if (string.IsNullOrEmpty(input))
 1987
 01988                return string.Empty;
 1989
 1990
 01991            return new string(input
 01992                    .Where(c => char.IsLetterOrDigit(c))
 01993                    .ToArray())
 01994                .ToLower();
 1995        }
 1996
 1997
 1998        /// <summary>
 1999        /// Проверяет, содержит ли фильтр ShipmentNumber несколько номеров.
 2000        /// Если несколько номеров найдено, рекомендуется отключить пагинацию.
 2001        /// </summary>
 2002        public static bool HasMultipleShipmentNumbers(string filterValue)
 2003        {
 72004            if (string.IsNullOrWhiteSpace(filterValue)) return false;
 2005
 32006            var parts = GetShipmentNumbersFromFilter(filterValue);
 32007            return parts.Count > 1;
 2008        }
 2009
 2010        /// <summary>
 2011        /// Извлекает номера отправлений из строки фильтра (поддержка разделителей и слитного текста).
 2012        /// </summary>
 2013        public static List<string> GetShipmentNumbersFromFilter(string raw)
 2014        {
 162015            if (string.IsNullOrWhiteSpace(raw)) return new List<string>();
 2016
 142017            var parts = new List<string>();
 2018
 2019            // Сначала пробуем разбить по переносам строк, точкам с запятой и запятым (НЕ по пробелу)
 2020            // Это нужно чтобы не ломать номера вида "СП 0192357", "Q8 0009457" с пробелом внутри
 142021            if (raw.IndexOfAny(new[] { '\n', ';', ',' }) >= 0)
 2022            {
 62023                parts = raw
 62024                    .Split(new[] { '\n', '\r', ';', ',' }, StringSplitOptions.RemoveEmptyEntries)
 142025                    .Select(p => p.Trim())
 142026                    .Where(p => !string.IsNullOrWhiteSpace(p))
 62027                    .Distinct()
 62028                    .Take(500)
 62029                    .ToList();
 2030            }
 2031
 2032            // Пробел НЕ используется как разделитель — номера вида "СП 0192357", "Q8 0009457" содержат пробелы внутри.
 2033            // Пользователи должны разделять несколько номеров переносом строки, точкой с запятой или запятой.
 2034
 142035            if (parts.Count == 0)
 2036            {
 82037                var regex = new Regex(@"\d{6,}-\d{3,}-\d{1,3}?", RegexOptions.Compiled);
 82038                var matches = regex.Matches(raw);
 82039                parts = matches
 42040                    .Select(m => m.Value.Trim())
 42041                    .Where(p => !string.IsNullOrWhiteSpace(p))
 82042                    .Distinct()
 82043                    .Take(500)
 82044                    .ToList();
 2045            }
 2046
 142047            if (parts.Count == 0)
 52048                parts.Add(raw.Trim());
 2049
 142050            return parts;
 2051        }
 2052
 02053        string Normalize(string? input) => NormalizeArticleForSearch(input);
 2054
 2055        private List<string> GetArticleSearchTerms(string article)
 2056        {
 02057            var relatedArticles = GetRelatedArticlesSync(article);
 02058            return relatedArticles.Count > 0
 02059                ? relatedArticles
 02060                : new List<string> { article };
 2061        }
 2062
 2063
 2064        /// <summary>
 2065        /// Получает список всех связанных артикулов для указанного артикула (работает в обе стороны)
 2066        /// Синхронная версия для использования в синхронных методах
 2067        /// </summary>
 2068        /// <param name="article">Оригинальный артикул</param>
 2069        /// <returns>Список артикулов, включая оригинальный и все связанные</returns>
 2070        private List<string> GetRelatedArticlesSync(string article)
 2071
 2072        {
 12073            if (string.IsNullOrEmpty(article))
 2074
 02075                return new List<string>();
 2076
 2077
 12078            var normalizedArticle = NormalizeArticleForSearch(article);
 2079
 12080            var relatedArticles = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { normalizedArticle };
 2081
 2082
 2083            try
 2084
 2085            {
 2086                // Находим все продукты с указанным артикулом (нормализованным)
 2087
 12088                var allProducts = _context.Products
 12089                    .Where(p => p.Article != null)
 12090                    .Include(p => p.ReplacementProduct)
 12091                    .ToList();
 2092
 2093
 2094                // Нормализуем артикулы в памяти и находим продукты с указанным артикулом
 2095
 02096                var matchingProducts = allProducts
 02097                    .Where(p => Normalize(p.Article) == normalizedArticle)
 02098                    .ToList();
 2099
 2100
 02101                if (!matchingProducts.Any())
 2102
 2103                {
 02104                    return relatedArticles.ToList();
 2105                }
 2106
 2107
 02108                var productIds = matchingProducts.Select(p => p.Id).ToList();
 2109
 2110
 2111                // Находим все продукты, которые ссылаются на найденные продукты (прямая связь)
 2112
 2113                // То есть продукты, у которых ReplacementProductId указывает на найденные продукты
 2114
 02115                var directlyLinkedProducts = allProducts
 02116                    .Where(p => p.ReplacementProductId.HasValue && productIds.Contains(p.ReplacementProductId.Value))
 02117                    .ToList();
 2118
 2119
 02120                foreach (var linkedProduct in directlyLinkedProducts)
 2121
 2122                {
 02123                    if (!string.IsNullOrEmpty(linkedProduct.Article))
 2124
 2125                    {
 02126                        relatedArticles.Add(Normalize(linkedProduct.Article));
 2127                    }
 2128                }
 2129
 2130
 2131                // Находим все продукты, на которые ссылаются найденные продукты (обратная связь)
 2132
 2133                // То есть продукты, на которые указывает ReplacementProductId найденных продуктов
 2134
 02135                var reverseLinkedProductIds = matchingProducts
 02136                    .Where(p => p.ReplacementProductId.HasValue)
 02137                    .Select(p => p.ReplacementProductId!.Value)
 02138                    .Distinct()
 02139                    .ToList();
 2140
 2141
 02142                if (reverseLinkedProductIds.Any())
 2143
 2144                {
 02145                    var reverseLinkedProducts = allProducts
 02146                        .Where(p => reverseLinkedProductIds.Contains(p.Id))
 02147                        .ToList();
 2148
 2149
 02150                    foreach (var reverseLinkedProduct in reverseLinkedProducts)
 2151
 2152                    {
 02153                        if (!string.IsNullOrEmpty(reverseLinkedProduct.Article))
 2154
 2155                        {
 02156                            relatedArticles.Add(Normalize(reverseLinkedProduct.Article));
 2157                        }
 2158                    }
 2159                }
 2160
 2161
 02162                return relatedArticles.ToList();
 2163            }
 2164
 12165            catch (Exception)
 2166
 2167            {
 2168                // В случае ошибки возвращаем только оригинальный артикул
 2169
 12170                return relatedArticles.ToList();
 2171            }
 12172        }
 2173
 2174
 2175        /// <summary>
 2176        /// Применяет группировку по ShipmentId на уровне IQueryable (оптимизированно для БД).
 2177        /// Получает все ShipmentId из отфильтрованного запроса и возвращает все заказы с этими ShipmentId.
 2178        /// Заказы без ShipmentId остаются как одиночные в результате.
 2179        /// </summary>
 2180        /// <param name="filteredQuery">Отфильтрованный IQueryable заказов</param>
 2181        /// <param name="includeArchived">Включать ли архивные заказы</param>
 2182        /// <returns>IQueryable с расширенным набором заказов по ShipmentId</returns>
 2183        private IQueryable<Order> ApplyShipmentGroupingQueryable(IQueryable<Order> filteredQuery, bool includeArchived)
 2184
 2185        {
 2186            // Получаем уникальные ShipmentId из отфильтрованного запроса (исключаем null)
 2187
 02188            var shipmentIdsQuery = filteredQuery
 02189                .Where(o => o.ShipmentId.HasValue)
 02190                .Select(o => o.ShipmentId!.Value)
 02191                .Distinct();
 2192
 2193            // Получаем ID заказов без ShipmentId из отфильтрованного запроса
 2194
 02195            var filteredIdsWithoutShipment = filteredQuery
 02196                .Where(o => !o.ShipmentId.HasValue)
 02197                .Select(o => o.Id);
 2198
 2199            // Используем GetOrdersQueryable с тем же includeArchived — это гарантирует
 2200            // идентичную цепочку Include, что и у filteredQuery (обе из OrderRepository).
 2201            // Это позволяет расширить результат до всех заказов с найденными ShipmentId,
 2202            // а не только тех, что прошли фильтр.
 2203
 02204            var fullQuery = _ordersDataServices.GetOrdersQueryable(includeArchived);
 2205
 02206            return fullQuery
 02207                .Where(o =>
 02208                    // Заказы с ShipmentId, которые есть среди найденных ShipmentId
 02209                    (o.ShipmentId.HasValue && shipmentIdsQuery.Contains(o.ShipmentId.Value))
 02210                    ||
 02211                    // Заказы без ShipmentId из исходной фильтрации
 02212                    (!o.ShipmentId.HasValue && filteredIdsWithoutShipment.Contains(o.Id))
 02213                );
 2214        }
 2215
 2216
 2217        /// <summary>
 2218        /// Расширяет список заказов, добавляя все заказы с тем же ShipmentId.
 2219        /// Заказы без ShipmentId остаются как одиночные.
 2220        /// Выполняет один запрос к БД для получения связанных заказов.
 2221        /// </summary>
 2222        /// <param name="filteredOrders">Исходный список отфильтрованных заказов</param>
 2223        /// <param name="includeArchived">Включать ли архивные заказы</param>
 2224        /// <returns>Расширенный список заказов с учетом группировки по ShipmentId</returns>
 2225        private async Task<List<Order>> ExpandByShipmentIdAsync(List<Order> filteredOrders, bool includeArchived)
 2226
 2227        {
 02228            if (!filteredOrders.Any())
 2229
 02230                return filteredOrders;
 2231
 2232
 2233            // Собираем уникальные ShipmentId из найденных заказов (исключаем null)
 2234
 02235            var shipmentIds = filteredOrders
 2236                .Where(o => o.ShipmentId.HasValue)
 2237                .Select(o => o.ShipmentId!.Value)
 02238                .Distinct()
 02239                .ToList();
 2240
 2241
 2242            // Если нет ShipmentId, возвращаем исходный список
 2243
 02244            if (!shipmentIds.Any())
 2245
 02246                return filteredOrders;
 2247
 2248
 2249            // Собираем ID уже найденных заказов для исключения дубликатов
 2250
 2251            var existingOrderIds = filteredOrders.Select(o => o.Id).ToHashSet();
 2252
 2253
 2254            // Одним запросом получаем все заказы с этими ShipmentId, которых ещё нет в результате
 2255
 02256            var relatedOrders = await _context.Orders
 02257                .Where(o => o.ShipmentId.HasValue && shipmentIds.Contains(o.ShipmentId.Value))
 02258                .Where(o => !existingOrderIds.Contains(o.Id))
 02259                .Where(o => includeArchived || o.OrderArchive == null)
 02260                .Include(o => o.ClientStatus)
 02261                .Include(o => o.AppStatus!)
 02262                .ThenInclude(s => s.StatusColor)
 02263                .Include(o => o.EtProducer)
 02264                .Include(o => o.ShipmentWarehouse)
 02265                .Include(o => o.Supplier)
 02266                .Include(o => o.OzonClient)
 02267                .Include(o => o.ProductInfo)
 02268                .Include(o => o.Delivery!)
 02269                .ThenInclude(d => d.Provider)
 02270                .ToListAsync();
 2271
 2272
 2273            // Объединяем результаты
 2274
 02275            var result = new List<Order>(filteredOrders);
 2276
 02277            result.AddRange(relatedOrders);
 2278
 2279
 02280            return result;
 02281        }
 2282    }
 2283}

Methods/Properties

.ctor(Services.FiltersServices.QueryableDataFilter`1<OzonDomains.Models.Order>,Services.FiltersServices.DataFilter`1<OzonDomains.Models.Order>,Services.DataServices.OrdersDataServices,Services.CacheServices.Cache.OzonOrdersCache.OrderCache,OzonRepositories.Context.ApplicationDbContext,Services.FiltersServices.SortManagers.OrderSortManager,Services.AccessControl.IDataScopeEvaluator,Services.SearchServices.OrderSearch.IOrderSearchService,Services.SearchServices.OrderSearch.IOrderSearchDiagnosticsStore,Microsoft.Extensions.Configuration.IConfiguration,Microsoft.Extensions.Logging.ILogger`1<Services.FiltersServices.DataFilterManagers.OrderDataFilterManager>)
NarrowByElasticsearchAsync()
IsElasticsearchTextSearchEnabled()
GetElasticsearchMaxResults()
GetElasticsearchLargeResultThreshold()
HasElasticsearchSearchCriteria(OServices.FiltersServices.FilterModels.OrderFilterModel)
HasElasticsearchTextCriteria(OServices.FiltersServices.FilterModels.OrderFilterModel)
HasElasticsearchExactFilterCriteria(OServices.FiltersServices.FilterModels.OrderFilterModel)
GetElasticsearchSearchDisplayText(OServices.FiltersServices.FilterModels.OrderFilterModel)
BuildElasticsearchFilterSummary(OServices.FiltersServices.FilterModels.OrderFilterModel)
FilterByRadioButton()
NormalizeShipmentNumberForSearch(System.String)
ShipmentNumberMatchesSearch(System.String,System.String)
NormalizeArticleForSearch(System.String)
ArticleMatchesSearch(System.String,System.String,System.String)
ApplyArticleFilter(System.Linq.IQueryable`1<OzonDomains.Models.Order>,System.Collections.Generic.IEnumerable`1<System.String>)
ApplyShipmentNumberFilter(System.Linq.IQueryable`1<OzonDomains.Models.Order>,System.Collections.Generic.IEnumerable`1<System.String>)
MatchesGlobalSearch(OzonDomains.Models.Order,System.String)
ApplyGlobalSearchQueryable(System.Linq.IQueryable`1<OzonDomains.Models.Order>,System.String)
ApplyPrimaryTextSearchQueryable(System.Linq.IQueryable`1<OzonDomains.Models.Order>,OServices.FiltersServices.FilterModels.OrderFilterModel,System.Boolean)
ApplyPrimaryTextSearchInMemory(System.Linq.IQueryable`1<OzonDomains.Models.Order>,OServices.FiltersServices.FilterModels.OrderFilterModel)
FilterByFilterData()
FilterByFilterDataQueryable(System.Linq.IQueryable`1<OzonDomains.Models.Order>,OServices.FiltersServices.FilterModels.OrderFilterModel,OzonDomains.Models.AccessControl.AccessProfile)
FilterAndSortQueryable(System.Linq.IQueryable`1<OzonDomains.Models.Order>,OServices.FiltersServices.FilterModels.OrderFilterModel,OzonDomains.Models.AccessControl.AccessProfile,Services.FiltersServices.SortModels.OrderSortState)
ApplyFinalSortQueryable(System.Linq.IQueryable`1<OzonDomains.Models.Order>,Services.FiltersServices.SortModels.OrderSortState)
ApplySortOrder(System.Collections.Generic.IEnumerable`1<OzonDomains.Models.Order>,Services.FiltersServices.SortModels.OrderSortState)
GetSortOrderViewData(Services.FiltersServices.SortModels.OrderSortState)
FilterByFilterData()
FilterByFilterData()
FilterTransactionsOrdersByFilterData()
FilterByFilterDataFromList(System.Collections.Generic.List`1<OzonDomains.Models.Order>,OServices.FiltersServices.FilterModels.OrderFilterModel,OzonDomains.Models.AccessControl.AccessProfile)
FilterFromFile()
GetWarehousesWithZasylAsync()
AreAllPropertiesNull(OServices.FiltersServices.FilterModels.OrderFilterModel)
StringToIntDay(System.String)
StringToIntHours(System.String)
FilterOrdersByColumns(System.Collections.Generic.Dictionary`2<System.String,System.String>,System.Collections.Generic.IEnumerable`1<OzonDomains.Models.Order>,System.Collections.Generic.List`1<System.String>)
ApplyFilters(OzonDomains.Models.Order,System.Collections.Generic.Dictionary`2<System.String,System.String>)
CountNonEmptyPriorityFields(OzonDomains.Models.Order,System.Collections.Generic.List`1<System.String>)
NormalizeString(System.String)
HasMultipleShipmentNumbers(System.String)
GetShipmentNumbersFromFilter(System.String)
Normalize(System.String)
GetArticleSearchTerms(System.String)
GetRelatedArticlesSync(System.String)
ApplyShipmentGroupingQueryable(System.Linq.IQueryable`1<OzonDomains.Models.Order>,System.Boolean)
ExpandByShipmentIdAsync()