< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.OzonChatController
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/OzonChatController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 1228
Coverable lines: 1228
Total lines: 1996
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 590
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/OzonChatController.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Authorization;
 2using Microsoft.AspNetCore.Mvc;
 3using Microsoft.Extensions.Caching.Memory;
 4using Microsoft.Extensions.Http;
 5using Microsoft.Extensions.Logging;
 6using OzonOrdersWeb.Observability;
 7using System.Text.RegularExpressions;
 8using AppRepository.ApiServices.OzonApi;
 9using AppRepository.ApiServices.OzonApi.Filters;
 10using OzonDomains;
 11using OzonDomains.Models;
 12using OzonOrdersWeb.Areas.Studio2.ViewModels.OzonChatViewModels;
 13using Services.DataServices;
 14using OzonRepositories.Data;
 15
 16namespace OzonOrdersWeb.Areas.Studio2.Controllers
 17{
 18    [Authorize(Roles = "User,Admin,SuperAdmin")]
 19    [Area("Studio2")]
 20    public class OzonChatController : Controller
 21    {
 22        private readonly OzonClientServices _ozonClientServices;
 23        private readonly IHttpClientFactory _httpClientFactory;
 24        private readonly IServiceScopeFactory _serviceScopeFactory;
 25        private readonly OrderRepository _orderRepository;
 26        private readonly OzonChatOrderDataServices _chatOrderService;
 27        private readonly OzonChatResponseStatisticDataServices _chatStatisticService;
 28        private readonly OzonChatResponseTimeDataServices _chatTimeService;
 29        private readonly IMemoryCache _memoryCache;
 30        private readonly ILogger<OzonChatController>? _logger;
 31
 32        // Ключ и TTL кэша суммарного счётчика непрочитанных: бейдж в шапке опрашивается
 33        // каждые несколько секунд с каждой вкладки, а под капотом — живые вызовы Ozon Chat API
 34        // по всем клиентам. Кэш ограничивает частоту обращений к Ozon одним разом в минуту.
 35        private const string TotalUnreadCacheKey = "OzonChat:TotalUnreadCount";
 036        private static readonly TimeSpan TotalUnreadCacheTtl = TimeSpan.FromSeconds(60);
 37
 38        // Часовой пояс Москвы (UTC+3)
 039        private static readonly TimeZoneInfo MoscowTimeZone = TimeZoneInfo.FindSystemTimeZoneById(
 040            OperatingSystem.IsWindows() ? "Russian Standard Time" : "Europe/Moscow");
 41
 42        // Регулярные выражения для извлечения артикула и названия товара из текста сообщения
 043        private static readonly Regex ArticleRegex = new Regex(
 044            @"артикул[\u044c]?\s+([\w\d][\w\d\-\.=]+)",
 045            RegexOptions.IgnoreCase | RegexOptions.Compiled);
 46
 047        private static readonly Regex ProductNameRegex = new Regex(
 048            @"""([^""]{5,120})""",
 049            RegexOptions.Compiled);
 50
 51        // Примеры формата номера отправления: 0159318549-0007, 0127136132-0231-1
 052        private static readonly Regex PostingNumberRegex = new Regex(
 053            @"^\d{4,12}-\d{1,6}(-\d{1,6})?$",
 054            RegexOptions.Compiled);
 55
 056        public OzonChatController(
 057            OzonClientServices ozonClientServices,
 058            IHttpClientFactory httpClientFactory,
 059            IServiceScopeFactory serviceScopeFactory,
 060            OrderRepository orderRepository,
 061            OzonChatOrderDataServices chatOrderService,
 062            OzonChatResponseStatisticDataServices chatStatisticService,
 063            OzonChatResponseTimeDataServices chatTimeService,
 064            IMemoryCache memoryCache,
 065            ILogger<OzonChatController>? logger = null)
 66        {
 067            _ozonClientServices = ozonClientServices;
 068            _httpClientFactory = httpClientFactory;
 069            _serviceScopeFactory = serviceScopeFactory;
 070            _orderRepository = orderRepository;
 071            _chatOrderService = chatOrderService;
 072            _chatStatisticService = chatStatisticService;
 073            _chatTimeService = chatTimeService;
 074            _memoryCache = memoryCache;
 075            _logger = logger;
 076        }
 77
 78        /// <summary>
 79        /// Единственная страница — выбор клиента + чаты с пагинацией через AJAX
 80        /// </summary>
 81        public async Task<IActionResult> Index()
 82        {
 83            try
 84            {
 085                var ozonClients = await _ozonClientServices.GetOzonClients();
 086                var viewModel = new OzonChatIndexViewModel
 087                {
 088                    OzonClients = ozonClients
 089                        .Where(c => !string.IsNullOrEmpty(c.ClientId) && !string.IsNullOrEmpty(c.ApiKey) &&
 090                                    c.ClientType == ClientType.OZON).ToList()
 091                };
 92
 093                return View(viewModel);
 94            }
 095            catch (Exception ex)
 96            {
 097                TempData["ErrorMessage"] = $"Ошибка при загрузке клиентов: {ex.Message}";
 098                return View(new OzonChatIndexViewModel());
 99            }
 0100        }
 101
 102        /// <summary>
 103        /// AJAX: Получение списка чатов с пагинацией (JSON)
 104        /// Если clientId == 0, загружает чаты по ВСЕМ клиентам
 105        /// </summary>
 106        [HttpGet]
 107        public async Task<IActionResult> GetChatsJson(int clientId = 0, string status = "All", string chatType = "",
 108            bool unreadOnly = false, int page = 1, int pageSize = 30, string sortField = "createdAt", string sortOrder =
 109        {
 110            try
 111            {
 0112                if (page < 1) page = 1;
 0113                pageSize = Math.Clamp(pageSize, 1, 100);
 114
 115                // Режим «все клиенты»
 0116                if (clientId == 0)
 117                {
 0118                    return await GetAllClientsChatsJson(status, chatType, unreadOnly, page, pageSize, sortField, sortOrd
 119                }
 120
 0121                var ozonClient = await _ozonClientServices.GetOzonClientAsync(clientId);
 0122                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 0123                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 124                {
 0125                    return Json(new { success = false, message = "Клиент Ozon не найден или некорректные данные API" });
 126                }
 127
 0128                long offset = (long)(page - 1) * pageSize;
 129
 0130                var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 0131                    ozonClient.DecryptApiKey);
 132
 0133                var request = new ChatListRequest
 0134                {
 0135                    Filter = new ChatFilter
 0136                    {
 0137                        ChatStatus = status,
 0138                        UnreadOnly = unreadOnly
 0139                    },
 0140                    Limit = pageSize,
 0141                    Offset = offset
 0142                };
 143
 0144                var response = await chatManager.GetChatListAsync(request);
 145
 0146                var chats = response?.Chats ?? new List<ChatItem>();
 147
 148                // Фильтрация по типу чата на стороне клиента (API не поддерживает этот фильтр)
 0149                if (!string.IsNullOrEmpty(chatType))
 150                {
 0151                    chats = chats.Where(c =>
 0152                        string.Equals(c.Chat.ChatType, chatType, StringComparison.OrdinalIgnoreCase)).ToList();
 153                }
 154
 155                // Сортировка
 0156                if (sortField == "lastMessage")
 157                {
 0158                    chats = chats.OrderByDescending(c => c.LastMessageId).ToList();
 0159                    if (sortOrder == "asc") chats.Reverse();
 160                }
 161                else // createdAt
 162                {
 0163                    chats = chats.OrderByDescending(c => c.Chat.CreatedAt).ToList();
 0164                    if (sortOrder == "asc") chats.Reverse();
 165                }
 166
 0167                var totalChats = response?.TotalChatsCount ?? 0;
 0168                var totalPages = totalChats > 0 ? (int)Math.Ceiling((double)totalChats / pageSize) : 1;
 169
 170                // Обновляем метаданные чатов в фоне (не блокируем ответ)
 0171                _ = Task.Run(async () =>
 0172                {
 0173                    using var scope = _serviceScopeFactory.CreateScope();
 0174                    var chatOrderService = scope.ServiceProvider.GetRequiredService<OzonChatOrderDataServices>();
 0175                    var orderRepository = scope.ServiceProvider.GetRequiredService<OrderRepository>();
 0176                    var ozonClientService = scope.ServiceProvider.GetRequiredService<OzonClientServices>();
 0177
 0178                    try
 0179                    {
 0180                        await UpdateChatMetadataAsync(clientId, chats, chatOrderService, orderRepository,
 0181                            ozonClientService);
 0182                    }
 0183                    catch (Exception ex)
 0184                    {
 0185                        Console.WriteLine($"Ошибка обновления метаданных чатов: {ex.Message}");
 0186                    }
 0187                });
 188
 0189                return Json(new
 0190                {
 0191                    success = true,
 0192                    clientName = ozonClient.Name,
 0193                    chats = chats.Select(c => new
 0194                    {
 0195                        chatId = c.Chat.ChatId,
 0196                        chatStatus = c.Chat.ChatStatus,
 0197                        chatType = c.Chat.ChatType,
 0198                        createdAt = ToMoscowTime(c.Chat.CreatedAt).ToString("yyyy-MM-dd HH:mm:ss"),
 0199                        createdAtFormatted = ToMoscowTime(c.Chat.CreatedAt).ToString("dd.MM.yyyy HH:mm"),
 0200                        unreadCount = c.UnreadCount,
 0201                        lastMessageId = c.LastMessageId,
 0202                        clientId = clientId,
 0203                        clientName = ozonClient.Name
 0204                    }),
 0205                    totalChatsCount = totalChats,
 0206                    totalUnreadCount = response?.TotalUnreadCount ?? 0,
 0207                    currentPage = page,
 0208                    pageSize,
 0209                    totalPages,
 0210                    hasNext = response?.HasNext ?? false
 0211                });
 212            }
 0213            catch (Exception ex)
 214            {
 0215                return Json(new { success = false, message = $"Ошибка при загрузке чатов: {ex.Message}" });
 216            }
 0217        }
 218
 219        /// <summary>
 220        /// Загрузка чатов по всем клиентам, объединение и сортировка по дате
 221        /// </summary>
 222        private async Task<IActionResult> GetAllClientsChatsJson(string status, string chatType,
 223            bool unreadOnly, int page, int pageSize, string sortField = "createdAt", string sortOrder = "desc")
 224        {
 225            try
 226            {
 0227                var ozonClients = await _ozonClientServices.GetOzonClients();
 0228                var validClients = ozonClients
 0229                    .Where(c => !string.IsNullOrEmpty(c.DecryptClientId) && !string.IsNullOrEmpty(c.DecryptApiKey) &&
 0230                                c.ClientType == ClientType.OZON).ToList();
 231
 0232                if (!validClients.Any())
 0233                    return Json(new { success = false, message = "Нет настроенных клиентов Ozon" });
 234
 235                // Параллельно загружаем чаты от всех клиентов
 0236                var tasks = validClients.Select(async client =>
 0237                {
 0238                    try
 0239                    {
 0240                        var chatManager = new OzonChatManager(_httpClientFactory, client.DecryptClientId!,
 0241                            client.DecryptApiKey!);
 0242
 0243                        var request = new ChatListRequest
 0244                        {
 0245                            Filter = new ChatFilter
 0246                            {
 0247                                ChatStatus = status,
 0248                                UnreadOnly = unreadOnly
 0249                            },
 0250                            Limit = 100 // берём максимум от каждого клиента
 0251                        };
 0252
 0253                        var response = await chatManager.GetChatListAsync(request);
 0254                        var chats = response?.Chats ?? new List<ChatItem>();
 0255
 0256                        return (Client: client, Chats: (IList<ChatItem>)chats,
 0257                            TotalChats: response?.TotalChatsCount ?? 0L,
 0258                            TotalUnread: response?.TotalUnreadCount ?? 0L);
 0259                    }
 0260                    catch
 0261                    {
 0262                        return (Client: client, Chats: (IList<ChatItem>)new List<ChatItem>(),
 0263                            TotalChats: 0L, TotalUnread: 0L);
 0264                    }
 0265                });
 266
 0267                var results = await Task.WhenAll(tasks);
 268
 269                // Объединяем все чаты с информацией о клиенте
 0270                var allChats = results.SelectMany(r => r.Chats.Select(c => new
 0271                {
 0272                    Chat = c,
 0273                    ClientId = r.Client.Id,
 0274                    ClientName = r.Client.Name ?? $"Клиент {r.Client.Id}"
 0275                })).ToList();
 276
 277                // Фильтрация по типу чата
 0278                if (!string.IsNullOrEmpty(chatType))
 279                {
 0280                    allChats = allChats.Where(c =>
 0281                        string.Equals(c.Chat.Chat.ChatType, chatType, StringComparison.OrdinalIgnoreCase)).ToList();
 282                }
 283
 284                // Сортировка
 0285                if (sortField == "lastMessage")
 286                {
 0287                    allChats = allChats.OrderByDescending(c => c.Chat.LastMessageId).ToList();
 0288                    if (sortOrder == "asc") allChats.Reverse();
 289                }
 290                else // createdAt
 291                {
 0292                    allChats = allChats.OrderByDescending(c => c.Chat.Chat.CreatedAt).ToList();
 0293                    if (sortOrder == "asc") allChats.Reverse();
 294                }
 295
 0296                var totalAllChats = allChats.Count;
 0297                var totalAllUnread = results.Sum(r => r.TotalUnread);
 0298                var totalPages = totalAllChats > 0 ? (int)Math.Ceiling((double)totalAllChats / pageSize) : 1;
 299
 300                // Пагинация на объединённом списке
 0301                var pagedChats = allChats.Skip((page - 1) * pageSize).Take(pageSize).ToList();
 302
 303                // Обновляем метаданные чатов в фоне (не блокируем ответ)
 0304                _ = Task.Run(async () =>
 0305                {
 0306                    using var scope = _serviceScopeFactory.CreateScope();
 0307                    var chatOrderService = scope.ServiceProvider.GetRequiredService<OzonChatOrderDataServices>();
 0308                    var orderRepository = scope.ServiceProvider.GetRequiredService<OrderRepository>();
 0309                    var ozonClientService = scope.ServiceProvider.GetRequiredService<OzonClientServices>();
 0310
 0311                    try
 0312                    {
 0313                        foreach (var result in results)
 0314                        {
 0315                            await UpdateChatMetadataAsync(result.Client.Id, result.Chats.ToList(), chatOrderService,
 0316                                orderRepository, ozonClientService);
 0317                        }
 0318                    }
 0319                    catch (Exception ex)
 0320                    {
 0321                        Console.WriteLine($"Ошибка обновления метаданных чатов (все клиенты): {ex.Message}");
 0322                    }
 0323                });
 324
 0325                return Json(new
 0326                {
 0327                    success = true,
 0328                    clientName = "Все клиенты",
 0329                    chats = pagedChats.Select(c => new
 0330                    {
 0331                        chatId = c.Chat.Chat.ChatId,
 0332                        chatStatus = c.Chat.Chat.ChatStatus,
 0333                        chatType = c.Chat.Chat.ChatType,
 0334                        createdAt = ToMoscowTime(c.Chat.Chat.CreatedAt).ToString("yyyy-MM-dd HH:mm:ss"),
 0335                        createdAtFormatted = ToMoscowTime(c.Chat.Chat.CreatedAt).ToString("dd.MM.yyyy HH:mm"),
 0336                        unreadCount = c.Chat.UnreadCount,
 0337                        lastMessageId = c.Chat.LastMessageId,
 0338                        clientId = c.ClientId,
 0339                        clientName = c.ClientName
 0340                    }),
 0341                    totalChatsCount = (long)totalAllChats,
 0342                    totalUnreadCount = totalAllUnread,
 0343                    currentPage = page,
 0344                    pageSize,
 0345                    totalPages,
 0346                    hasNext = page < totalPages
 0347                });
 348            }
 0349            catch (Exception ex)
 350            {
 0351                return Json(new { success = false, message = $"Ошибка при загрузке чатов: {ex.Message}" });
 352            }
 0353        }
 354
 355        /// <summary>
 356        /// API для получения обновлённого количества непрочитанных сообщений
 357        /// </summary>
 358        [HttpGet]
 359        public async Task<IActionResult> GetUnreadCount(int clientId)
 360        {
 361            try
 362            {
 0363                var ozonClient = await _ozonClientServices.GetOzonClientAsync(clientId);
 0364                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 0365                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 366                {
 0367                    return Json(new { success = false, message = "Клиент не найден или некорректные данные API" });
 368                }
 369
 0370                var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 0371                    ozonClient.DecryptApiKey);
 0372                var response =
 0373                    await chatManager.GetUnreadOpenChatsAsync(1); // Запрашиваем минимум для получения общего счётчика
 374
 0375                return Json(new
 0376                {
 0377                    success = true,
 0378                    unreadCount = response?.TotalUnreadCount ?? 0
 0379                });
 380            }
 0381            catch (Exception ex)
 382            {
 0383                return Json(new
 0384                {
 0385                    success = false,
 0386                    message = ex.Message
 0387                });
 388            }
 0389        }
 390
 391        /// <summary>
 392        /// API для получения общего количества непрочитанных сообщений по всем клиентам
 393        /// Оптимизированная версия для периодического опроса
 394        /// </summary>
 395        [HttpGet]
 396        public async Task<IActionResult> GetTotalUnreadCount()
 397        {
 398            try
 399            {
 0400                var totalUnread = await _memoryCache.GetOrCreateAsync(TotalUnreadCacheKey, async entry =>
 0401                {
 0402                    entry.AbsoluteExpirationRelativeToNow = TotalUnreadCacheTtl;
 0403                    return await FetchTotalUnreadFromOzonAsync();
 0404                });
 405
 0406                return Json(new { success = true, totalUnread });
 407            }
 0408            catch (Exception ex)
 409            {
 0410                HttpContext.Items[BusinessFailureLoggingFilter.LoggedItemKey] = true;
 0411                _logger?.LogInformation(ex, "GetTotalUnreadCount: не удалось получить количество непрочитанных сообщений
 0412                return Json(new { success = false, message = ex.Message, totalUnread = 0 });
 413            }
 0414        }
 415
 416        private async Task<long> FetchTotalUnreadFromOzonAsync()
 417        {
 0418            var ozonClients = await _ozonClientServices.GetOzonClients();
 0419            var validClients = ozonClients
 0420                .Where(c => !string.IsNullOrEmpty(c.DecryptClientId) && !string.IsNullOrEmpty(c.DecryptApiKey) &&
 0421                            c.ClientType == ClientType.OZON).ToList();
 422
 0423            if (validClients.Count == 0)
 0424                return 0L;
 425
 426            // Параллельно получаем количество непрочитанных от всех клиентов
 0427            var tasks = validClients.Select(async client =>
 0428            {
 0429                try
 0430                {
 0431                    var chatManager = new OzonChatManager(_httpClientFactory, client.DecryptClientId!,
 0432                        client.DecryptApiKey!);
 0433                    var response = await chatManager.GetUnreadOpenChatsAsync(1);
 0434                    return response?.TotalUnreadCount ?? 0L;
 0435                }
 0436                catch
 0437                {
 0438                    return 0L;
 0439                }
 0440            });
 441
 0442            var results = await Task.WhenAll(tasks);
 0443            return results.Sum();
 0444        }
 445
 446        /// <summary>
 447        /// AJAX: Получение последнего сообщения чата
 448        /// </summary>
 449        [HttpGet]
 450        public async Task<IActionResult> GetLastMessageJson(int clientId, string chatId)
 451        {
 452            try
 453            {
 0454                if (string.IsNullOrEmpty(chatId))
 0455                    return Json(new { success = false, message = "Не указан ID чата" });
 456
 0457                var ozonClient = await _ozonClientServices.GetOzonClientAsync(clientId);
 0458                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 0459                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 460                {
 0461                    return Json(new { success = false, message = "Клиент Ozon не найден" });
 462                }
 463
 0464                var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 0465                    ozonClient.DecryptApiKey);
 466
 0467                var response = await chatManager.GetChatHistoryAsync(chatId, 1, null, "Backward");
 468
 0469                if (response == null || response.Messages == null || !response.Messages.Any())
 0470                    return Json(new { success = true, hasMessage = false });
 471
 0472                var lastMessage = response.Messages.First();
 0473                var senderType = lastMessage.User?.Type ?? "unknown";
 0474                var senderLabel = senderType.ToLower() switch
 0475                {
 0476                    "customer" => "Покупатель",
 0477                    "seller" => "Продавец",
 0478                    "system" => "Система",
 0479                    "support" => "Поддержка",
 0480                    _ => "Неизвестно"
 0481                };
 482
 483                // Определяем текст сообщения
 0484                var messageText = "";
 0485                if (lastMessage.IsImage)
 486                {
 0487                    messageText = "[Изображение]";
 488                }
 0489                else if (lastMessage.Data != null && lastMessage.Data.Any())
 490                {
 0491                    var fullText = string.Join(" ", lastMessage.Data);
 0492                    messageText = fullText.Length > 100
 0493                        ? fullText.Substring(0, 100) + "..."
 0494                        : fullText;
 495                }
 496
 0497                return Json(new
 0498                {
 0499                    success = true,
 0500                    hasMessage = true,
 0501                    sender = senderLabel,
 0502                    senderType = senderType.ToLower(),
 0503                    text = messageText,
 0504                    createdAt = ToMoscowTime(lastMessage.CreatedAt).ToString("yyyy-MM-ddTHH:mm:ss"),
 0505                    createdAtFormatted = ToMoscowTime(lastMessage.CreatedAt).ToString("dd.MM HH:mm")
 0506                });
 507            }
 0508            catch (Exception ex)
 509            {
 0510                return Json(new { success = false, message = ex.Message });
 511            }
 0512        }
 513
 514        /// <summary>
 515        /// AJAX: Получение истории сообщений чата (JSON)
 516        /// </summary>
 517        [HttpGet]
 518        public async Task<IActionResult> GetHistoryJson(int clientId, string chatId, int limit = 50,
 519            long? fromMessageId = null, string direction = "Backward")
 520        {
 521            try
 522            {
 0523                if (string.IsNullOrEmpty(chatId))
 0524                    return Json(new { success = false, message = "Не указан ID чата" });
 525
 0526                var ozonClient = await _ozonClientServices.GetOzonClientAsync(clientId);
 0527                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 0528                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 529                {
 0530                    return Json(new { success = false, message = "Клиент Ozon не найден или некорректные данные API" });
 531                }
 532
 0533                var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 0534                    ozonClient.DecryptApiKey);
 535
 0536                var response = await chatManager.GetChatHistoryAsync(chatId, limit, fromMessageId, direction);
 537
 0538                if (response == null)
 0539                    return Json(new { success = false, message = "Не удалось получить историю чата" });
 540
 0541                if (response.IsPremiumRequired)
 542                {
 0543                    var ozonUrl = $"https://seller.ozon.ru/app/messenger/?group=customers_v2&id={chatId}";
 544                    // Формируем сообщение с ссылкой на сайте Ozon
 0545                    var message = $"Для работы с чатами в данной системе необходима подписка Premium Plus. Но вы можете 
 0546                    return Json(new {
 0547                        success = false,
 0548                        premiumRequired = true,
 0549                        message,
 0550                        ozonUrl
 0551                    });
 552                }
 553
 554                // Синхронизация чатов с заказами в фоне (не блокируем ответ)
 0555                _ = Task.Run(async () =>
 0556                {
 0557                    using var scope = _serviceScopeFactory.CreateScope();
 0558                    var chatOrderService = scope.ServiceProvider.GetRequiredService<OzonChatOrderDataServices>();
 0559                    var orderRepository = scope.ServiceProvider.GetRequiredService<OrderRepository>();
 0560                    var chatStatisticService =
 0561                        scope.ServiceProvider.GetRequiredService<OzonChatResponseStatisticDataServices>();
 0562
 0563                    try
 0564                    {
 0565                        await SyncChatWithOrdersAsync(clientId, chatId, response.Messages ?? new List<ChatMessage>(), ch
 0566                            orderRepository);
 0567
 0568                        // Обработка статистики ответов
 0569                        await ProcessChatStatisticsAsync(clientId, chatId, response.Messages ?? new List<ChatMessage>(),
 0570                            chatOrderService);
 0571                    }
 0572                    catch (Exception ex)
 0573                    {
 0574                        // Логируем ошибку, но не прерываем основной поток
 0575                        Console.WriteLine($"Ошибка синхронизации чата {chatId}: {ex.Message}");
 0576                    }
 0577                });
 578
 0579                return Json(new
 0580                {
 0581                    success = true,
 0582                    hasNext = response.HasNext,
 0583                    messages = (response.Messages ?? new List<ChatMessage>()).Select(m => new
 0584                    {
 0585                        messageId = m.MessageId,
 0586                        createdAt = ToMoscowTime(m.CreatedAt).ToString("yyyy-MM-dd HH:mm:ss"),
 0587                        createdAtFormatted = ToMoscowTime(m.CreatedAt).ToString("dd.MM.yyyy HH:mm"),
 0588                        data = m.Data,
 0589                        isImage = m.IsImage,
 0590                        isRead = m.IsRead,
 0591                        moderateImageStatus = m.ModerateImageStatus,
 0592                        userId = m.User?.Id,
 0593                        userType = m.User?.Type,
 0594                        orderNumber = m.Context?.OrderNumber,
 0595                        sku = m.Context?.Sku
 0596                    })
 0597                });
 598            }
 0599            catch (Exception ex)
 600            {
 0601                return Json(new { success = false, message = $"Ошибка при загрузке истории: {ex.Message}" });
 602            }
 0603        }
 604
 605        /// <summary>
 606        /// AJAX: Отметить сообщения как прочитанные
 607        /// </summary>
 608        [HttpPost]
 609        public async Task<IActionResult> MarkAsRead([FromBody] MarkAsReadRequest request)
 610        {
 611            try
 612            {
 0613                if (request == null || string.IsNullOrEmpty(request.ChatId))
 0614                    return Json(new { success = false, message = "Не указан ID чата" });
 615
 0616                var ozonClient = await _ozonClientServices.GetOzonClientAsync(request.ClientId);
 0617                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 0618                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 619                {
 0620                    return Json(new { success = false, message = "Клиент Ozon не найден или некорректные данные API" });
 621                }
 622
 0623                var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 0624                    ozonClient.DecryptApiKey);
 625
 0626                var response = await chatManager.MarkAsReadAsync(request.ChatId, request.FromMessageId);
 627
 0628                if (response == null)
 0629                    return Json(new { success = false, message = "Не удалось отметить сообщения как прочитанные" });
 630
 0631                return Json(new
 0632                {
 0633                    success = true,
 0634                    message = "Сообщения отмечены как прочитанные",
 0635                    unreadCount = response.UnreadCount
 0636                });
 637            }
 0638            catch (Exception ex)
 639            {
 0640                return Json(new { success = false, message = $"Ошибка: {ex.Message}" });
 641            }
 0642        }
 643
 644        /// <summary>
 645        /// Обновление метаданных чатов (статус, тип) и создание новых записей для несвязанных чатов
 646        /// </summary>
 647        private async Task UpdateChatMetadataAsync(int clientId, List<ChatItem> chats,
 648            OzonChatOrderDataServices chatOrderService, OrderRepository orderRepository,
 649            OzonClientServices ozonClientService)
 650        {
 0651            var ozonClient = await ozonClientService.GetOzonClientAsync(clientId);
 0652            if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 0653                string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 0654                return;
 655
 0656            var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 0657                ozonClient.DecryptApiKey);
 658
 0659            foreach (var chatItem in chats)
 660            {
 661                try
 662                {
 0663                    if (string.IsNullOrWhiteSpace(chatItem.Chat.ChatId))
 0664                        continue;
 665
 666                    // Получить все записи OzonChatOrder для этого чата
 0667                    var chatOrders = await chatOrderService.GetByChatIdAsync(chatItem.Chat.ChatId);
 668
 669                    // Если есть существующие связи - обновляем их
 0670                    if (chatOrders.Any())
 671                    {
 0672                        foreach (var chatOrder in chatOrders)
 673                        {
 674                            // Обновить статус и тип чата
 0675                            chatOrder.ChatStatus = chatItem.Chat.ChatStatus;
 0676                            chatOrder.ChatType = chatItem.Chat.ChatType;
 0677                            chatOrder.UnreadMessagesCount = chatItem.UnreadCount;
 678
 0679                            await chatOrderService.Update(chatOrder);
 680                        }
 681                    }
 682                    // Если нет связей - пытаемся создать новую запись
 683                    else
 684                    {
 685                        // Получаем последнее сообщение чата для извлечения order_number
 686                        // Делаем это только для открытых чатов или с непрочитанными сообщениями
 0687                        if (chatItem.Chat.ChatType == "BUYER_SELLER" || chatItem.UnreadCount > 0)
 688                        {
 0689                            var history = await chatManager.GetChatHistoryAsync(
 0690                                chatItem.Chat.ChatId,
 0691                                limit: 50, // Берем несколько последних, чтобы найти с order_number
 0692                                fromMessageId: null
 0693                            );
 694
 0695                            if (history?.Messages != null && history.Messages.Any())
 696                            {
 697                                // Ищем первое сообщение с order_number
 0698                                var messageWithOrder = history.Messages
 0699                                    .FirstOrDefault(m => !string.IsNullOrEmpty(m.Context?.OrderNumber));
 700
 0701                                if (messageWithOrder != null)
 702                                {
 0703                                    var orderNumber = messageWithOrder.Context!.OrderNumber;
 0704                                    var sku = messageWithOrder.Context!.Sku;
 705
 0706                                    var ordersToLink = new List<Order>();
 707
 708                                    // Сначала пытаемся найти заказ по номеру отправления
 0709                                    if (!string.IsNullOrEmpty(orderNumber))
 710                                    {
 0711                                        var order = await orderRepository.GetOrderByShipmentNumberAsync(orderNumber!,
 0712                                            clientId);
 0713                                        if (order != null)
 0714                                            ordersToLink.Add(order);
 715                                    }
 716
 717                                    // Если не нашли по номеру, пытаемся найти по SKU (может быть несколько заказов)
 0718                                    if (!ordersToLink.Any() && !string.IsNullOrEmpty(sku))
 719                                    {
 0720                                        var ordersBySku = await orderRepository.GetOrdersBySkuAsync(sku!, clientId);
 0721                                        ordersToLink.AddRange(ordersBySku);
 722                                    }
 723
 0724                                    if (ordersToLink.Any())
 725                                    {
 0726                                        foreach (var order in ordersToLink)
 727                                        {
 728                                            // postingNumber берем из orderNumber если есть, иначе из ShipmentNumber зак
 0729                                            var postingNumber = !string.IsNullOrEmpty(orderNumber)
 0730                                                ? orderNumber
 0731                                                : order.ShipmentNumber;
 732
 733                                            // Создаем новую связь
 0734                                            await chatOrderService.CreateOrUpdateAsync(
 0735                                                chatId: chatItem.Chat.ChatId,
 0736                                                orderId: order.Id,
 0737                                                clientId: clientId,
 0738                                                postingNumber: postingNumber,
 0739                                                unreadCount: chatItem.UnreadCount,
 0740                                                chatStatus: chatItem.Chat.ChatStatus,
 0741                                                chatType: chatItem.Chat.ChatType
 0742                                            );
 743
 744                                            // Обновляем информацию о последнем сообщении
 0745                                            var lastMessage = history.Messages.First();
 0746                                            var messageText = lastMessage.Data != null && lastMessage.Data.Any()
 0747                                                ? string.Join(" ", lastMessage.Data)
 0748                                                : string.Empty;
 749
 0750                                            var lastMessageFromCustomer = lastMessage.User?.Type?
 0751                                                .Equals("Customer", StringComparison.OrdinalIgnoreCase) == true;
 752
 0753                                            await chatOrderService.UpdateLastMessageAsync(
 0754                                                chatId: chatItem.Chat.ChatId,
 0755                                                orderId: order.Id,
 0756                                                messageDate: lastMessage.CreatedAt.ToUniversalTime(),
 0757                                                messageText: messageText,
 0758                                                fromCustomer: lastMessageFromCustomer
 0759                                            );
 760
 0761                                            Console.WriteLine(
 0762                                                $"Создана связь: чат {chatItem.Chat.ChatId} → заказ {order.Id} ({order.S
 0763                                        }
 764                                    }
 765                                    else
 766                                    {
 0767                                        Console.WriteLine(
 0768                                            $"Заказ с номером {orderNumber} / SKU {sku} не найден для чата {chatItem.Cha
 769                                    }
 0770                                }
 771                            }
 0772                        }
 773                    }
 0774                }
 0775                catch (Exception ex)
 776                {
 0777                    Console.WriteLine($"Ошибка обработки чата {chatItem.Chat.ChatId}: {ex.Message}");
 0778                }
 0779            }
 0780        }
 781
 782        /// <summary>
 783        /// Синхронизация чата с заказами на основе сообщений
 784        /// </summary>
 785        private async Task SyncChatWithOrdersAsync(int clientId, string chatId, List<ChatMessage> messages,
 786            OzonChatOrderDataServices chatOrderService, OrderRepository orderRepository)
 787        {
 0788            if (messages == null || !messages.Any())
 0789                return;
 790
 791            // Группируем сообщения по orderNumber или SKU
 0792            var messagesWithContext = messages
 0793                .Where(m => m.Context != null &&
 0794                            (!string.IsNullOrEmpty(m.Context.OrderNumber) || !string.IsNullOrEmpty(m.Context.Sku)))
 0795                .ToList();
 796
 797            // Создаем словарь для группировки: ключ - OrderNumber или SKU
 0798            var messageGroups = new Dictionary<string, List<ChatMessage>>();
 799
 0800            foreach (var message in messagesWithContext)
 801            {
 0802                var key = !string.IsNullOrEmpty(message.Context!.OrderNumber)
 0803                    ? message.Context.OrderNumber
 0804                    : message.Context.Sku;
 805
 0806                if (!string.IsNullOrEmpty(key))
 807                {
 0808                    if (!messageGroups.ContainsKey(key))
 0809                        messageGroups[key] = new List<ChatMessage>();
 810
 0811                    messageGroups[key].Add(message);
 812                }
 813            }
 814
 0815            foreach (var kvp in messageGroups)
 816            {
 0817                var identifier = kvp.Key;
 0818                var orderMessages = kvp.Value.OrderByDescending(m => m.CreatedAt).ToList();
 0819                var lastMessage = orderMessages.First();
 820
 0821                var orderNumber = lastMessage.Context?.OrderNumber;
 0822                var sku = lastMessage.Context?.Sku;
 823
 0824                var ordersToLink = new List<Order>();
 825
 826                // Сначала пытаемся найти заказ по номеру отправления
 0827                if (!string.IsNullOrEmpty(orderNumber))
 828                {
 0829                    var order = await orderRepository.GetOrderByShipmentNumberAsync(orderNumber!, clientId);
 0830                    if (order != null)
 0831                        ordersToLink.Add(order);
 832                }
 833
 834                // Если не нашли по номеру, пытаемся найти по SKU (может быть несколько заказов)
 0835                if (!ordersToLink.Any() && !string.IsNullOrEmpty(sku))
 836                {
 0837                    var ordersBySku = await orderRepository.GetOrdersBySkuAsync(sku!, clientId);
 0838                    ordersToLink.AddRange(ordersBySku);
 839                }
 840
 0841                if (!ordersToLink.Any())
 842                {
 0843                    Console.WriteLine(
 0844                        $"Заказ с идентификатором {identifier} (orderNumber: {orderNumber}, sku: {sku}) не найден в БД д
 0845                    continue;
 846                }
 847
 848                // Подсчитать непрочитанные сообщения от покупателя
 0849                var unreadCount = orderMessages.Count(m =>
 0850                    !m.IsRead &&
 0851                    m.User?.Type?.Equals("Customer", StringComparison.OrdinalIgnoreCase) == true);
 852
 853                // Определить тип последнего сообщения
 0854                var lastMessageFromCustomer =
 0855                    lastMessage.User?.Type?.Equals("Customer", StringComparison.OrdinalIgnoreCase) == true;
 856
 0857                foreach (var order in ordersToLink)
 858                {
 859                    // Создать или обновить запись OzonChatOrder
 860                    // postingNumber берем из orderNumber если есть, иначе из ShipmentNumber заказа
 0861                    var postingNumber = !string.IsNullOrEmpty(orderNumber) ? orderNumber : order.ShipmentNumber;
 862
 0863                    await chatOrderService.CreateOrUpdateAsync(
 0864                        chatId: chatId,
 0865                        orderId: order.Id,
 0866                        clientId: clientId,
 0867                        postingNumber: postingNumber,
 0868                        unreadCount: unreadCount,
 0869                        chatStatus: null, // Статус чата будет обновлен отдельно
 0870                        chatType: null
 0871                    );
 872
 873                    // Обновить информацию о последнем сообщении
 0874                    var messageText = lastMessage.Data != null && lastMessage.Data.Any()
 0875                        ? string.Join(" ", lastMessage.Data)
 0876                        : string.Empty;
 877
 0878                    await chatOrderService.UpdateLastMessageAsync(
 0879                        chatId: chatId,
 0880                        orderId: order.Id,
 0881                        messageDate: lastMessage.CreatedAt.ToUniversalTime(),
 0882                        messageText: messageText,
 0883                        fromCustomer: lastMessageFromCustomer
 0884                    );
 885
 0886                    Console.WriteLine($"Создана связь: чат {chatId} → заказ {order.Id} ({order.ShipmentNumber})");
 0887                }
 0888            }
 0889        }
 890
 891        /// <summary>
 892        /// Обработка статистики ответов в чате
 893        /// </summary>
 894        private async Task ProcessChatStatisticsAsync(int clientId, string chatId, List<ChatMessage> messages,
 895            OzonChatResponseStatisticDataServices statisticService, OzonChatOrderDataServices chatOrderService)
 896        {
 0897            if (messages == null || !messages.Any())
 0898                return;
 899
 900            try
 901            {
 902                // Получаем информацию о чате (orderId, postingNumber, chatType, chatStatus)
 0903                var chatOrders = await chatOrderService.GetByChatIdAsync(chatId);
 0904                var chatOrder = chatOrders.FirstOrDefault();
 905
 0906                var orderId = chatOrder?.OrderId;
 0907                var postingNumber = chatOrder?.PostingNumber;
 0908                var chatType = chatOrder?.ChatType;
 0909                var chatStatus = chatOrder?.ChatStatus;
 910
 911                // Сортируем сообщения по дате (от старых к новым) для правильной обработки диалога
 0912                var sortedMessages = messages.OrderBy(m => m.CreatedAt).ToList();
 913
 914                // Словарь для отслеживания последнего необработанного сообщения от покупателя
 0915                long? lastCustomerMessageId = null;
 0916                DateTime? lastCustomerMessageDate = null;
 0917                string? lastCustomerMessageText = null;
 918
 0919                foreach (var message in sortedMessages)
 920                {
 0921                    var userType = message.User?.Type;
 0922                    var messageText = message.Data != null && message.Data.Any()
 0923                        ? string.Join(" ", message.Data)
 0924                        : string.Empty;
 925
 926                    // Если это сообщение от покупателя (Customer)
 0927                    if (userType?.Equals("Customer", StringComparison.OrdinalIgnoreCase) == true)
 928                    {
 929                        // Проверяем, есть ли уже запись для этого сообщения
 0930                        var existingStatistic =
 0931                            await statisticService.GetByClientMessageIdAsync(chatId, message.MessageId);
 932
 0933                        if (existingStatistic == null)
 934                        {
 935                            // Создаем новую запись статистики для сообщения клиента
 0936                            await statisticService.CreateOrUpdateClientMessageAsync(
 0937                                chatId: chatId,
 0938                                clientMessageId: message.MessageId,
 0939                                clientMessageText: messageText,
 0940                                clientMessageDate: message.CreatedAt.ToUniversalTime(),
 0941                                clientId: clientId,
 0942                                orderId: orderId,
 0943                                postingNumber: postingNumber,
 0944                                chatType: chatType,
 0945                                chatStatus: chatStatus
 0946                            );
 947                        }
 948
 949                        // Сохраняем как последнее сообщение от покупателя для потенциального ответа
 0950                        lastCustomerMessageId = message.MessageId;
 0951                        lastCustomerMessageDate = message.CreatedAt.ToUniversalTime();
 0952                        lastCustomerMessageText = messageText;
 953                    }
 954                    // Если это сообщение от продавца (Seller)
 0955                    else if (userType?.Equals("Seller", StringComparison.OrdinalIgnoreCase) == true ||
 0956                             userType?.Equals("System", StringComparison.OrdinalIgnoreCase) == true)
 957                    {
 958                        // Если есть необработанное сообщение от покупателя, это может быть ответом на него
 0959                        if (lastCustomerMessageId.HasValue && lastCustomerMessageDate.HasValue)
 960                        {
 961                            // Проверяем, не обработан ли уже ответ
 0962                            var statistic =
 0963                                await statisticService.GetByClientMessageIdAsync(chatId, lastCustomerMessageId.Value);
 964
 0965                            if (statistic != null && !statistic.IsAnswered)
 966                            {
 967                                // Получаем имя оператора (если доступно)
 0968                                var responderName = message.User?.Type ?? "Seller";
 969
 970                                // Обновляем статистику с информацией об ответе
 0971                                await statisticService.UpdateResponseAsync(
 0972                                    chatId: chatId,
 0973                                    clientMessageId: lastCustomerMessageId.Value,
 0974                                    sellerResponseMessageId: message.MessageId,
 0975                                    sellerResponseText: messageText,
 0976                                    sellerResponseDate: message.CreatedAt.ToUniversalTime(),
 0977                                    responderName: responderName
 0978                                );
 979
 980                                // Сбрасываем последнее сообщение покупателя, т.к. на него уже ответили
 0981                                lastCustomerMessageId = null;
 0982                                lastCustomerMessageDate = null;
 0983                                lastCustomerMessageText = null;
 984                            }
 985                        }
 986                    }
 0987                }
 988
 0989                Console.WriteLine($"Статистика обработана для чата {chatId}: {sortedMessages.Count} сообщений");
 0990            }
 0991            catch (Exception ex)
 992            {
 0993                Console.WriteLine($"Ошибка обработки статистики для чата {chatId}: {ex.Message}");
 0994            }
 0995        }
 996
 997        /// <summary>
 998        /// AJAX: Отправить сообщение в чат
 999        /// </summary>
 1000        [HttpPost]
 1001        public async Task<IActionResult> SendMessage([FromBody] SendMessageRequest request)
 1002        {
 1003            try
 1004            {
 01005                if (request == null || string.IsNullOrEmpty(request.ChatId) || string.IsNullOrWhiteSpace(request.Text))
 01006                    return Json(new { success = false, message = "Не указан ID чата или текст сообщения" });
 1007
 01008                if (request.Text.Length > 1000)
 01009                    return Json(new { success = false, message = "Текст сообщения не должен превышать 1000 символов" });
 1010
 01011                var ozonClient = await _ozonClientServices.GetOzonClientAsync(request.ClientId);
 01012                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 01013                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 1014                {
 01015                    return Json(new { success = false, message = "Клиент Ozon не найден или некорректные данные API" });
 1016                }
 1017
 01018                var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 01019                    ozonClient.DecryptApiKey);
 1020
 01021                var response = await chatManager.SendMessageAsync(request.ChatId, request.Text);
 1022
 01023                if (response == null || response.Result != "success")
 01024                    return Json(new { success = false, message = "Не удалось отправить сообщение" });
 1025
 1026                // Получаем имя пользователя
 01027                var responderName = !string.IsNullOrEmpty(User.Identity?.Name)
 01028                    ? User.Identity.Name
 01029                    : User.Identity?.IsAuthenticated == true
 01030                        ? "Authenticated User"
 01031                        : "System";
 1032
 1033                // Сохраняем данные для фоновой задачи
 01034                var chatIdCopy = request.ChatId;
 01035                var textCopy = request.Text;
 01036                var clientIdCopy = request.ClientId;
 01037                var responderNameCopy = responderName;
 1038
 1039                // Записываем ответ менеджера в статистику в фоне (не блокируем ответ)
 01040                _ = Task.Run(async () =>
 01041                {
 01042                    try
 01043                    {
 01044                        using var scope = _serviceScopeFactory.CreateScope();
 01045                        var chatStatisticService = scope.ServiceProvider
 01046                            .GetRequiredService<OzonChatResponseStatisticDataServices>();
 01047                        var chatOrderService = scope.ServiceProvider
 01048                            .GetRequiredService<OzonChatOrderDataServices>();
 01049
 01050                        // Получаем данные чата для orderId и postingNumber
 01051                        var chatOrders = await chatOrderService.GetByChatIdAsync(chatIdCopy);
 01052                        var chatOrder = chatOrders.FirstOrDefault();
 01053
 01054                        await chatStatisticService.AddManagerMessageAsync(
 01055                            chatId: chatIdCopy,
 01056                            clientId: clientIdCopy,
 01057                            messageText: textCopy,
 01058                            responderName: responderNameCopy,
 01059                            orderId: chatOrder?.OrderId,
 01060                            postingNumber: chatOrder?.PostingNumber,
 01061                            chatType: chatOrder?.ChatType,
 01062                            chatStatus: chatOrder?.ChatStatus
 01063                        );
 01064
 01065                        Console.WriteLine($"Записан ответ менеджера {responderNameCopy} в чате {chatIdCopy}");
 01066                    }
 01067                    catch (Exception ex)
 01068                    {
 01069                        Console.WriteLine($"Ошибка записи ответа менеджера: {ex.Message}");
 01070                    }
 01071                });
 1072
 01073                return Json(new
 01074                {
 01075                    success = true,
 01076                    message = "Сообщение отправлено успешно"
 01077                });
 1078            }
 01079            catch (Exception ex)
 1080            {
 01081                return Json(new { success = false, message = $"Ошибка: {ex.Message}" });
 1082            }
 01083        }
 1084
 1085        /// <summary>
 1086        /// AJAX: Отправить файл в чат
 1087        /// </summary>
 1088        [HttpPost]
 1089        public async Task<IActionResult> SendFile([FromBody] SendFileRequest request)
 1090        {
 1091            try
 1092            {
 01093                if (request == null || string.IsNullOrEmpty(request.ChatId) ||
 01094                    string.IsNullOrWhiteSpace(request.Base64Content) || string.IsNullOrWhiteSpace(request.FileName))
 01095                    return Json(new { success = false, message = "Не указан ID чата, содержимое файла или имя файла" });
 1096
 01097                var ozonClient = await _ozonClientServices.GetOzonClientAsync(request.ClientId);
 01098                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 01099                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 1100                {
 01101                    return Json(new { success = false, message = "Клиент Ozon не найден или некорректные данные API" });
 1102                }
 1103
 01104                var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 01105                    ozonClient.DecryptApiKey);
 1106
 01107                var response = await chatManager.SendFileAsync(request.ChatId, request.Base64Content, request.FileName);
 1108
 01109                if (response == null || response.Result != "success")
 01110                    return Json(new { success = false, message = "Не удалось отправить файл" });
 1111
 1112                // Получаем имя пользователя из Identity до запуска фонового Task
 01113                var currentUserName = !string.IsNullOrEmpty(User.Identity?.Name)
 01114                    ? User.Identity.Name
 01115                    : User.Identity?.IsAuthenticated == true
 01116                        ? "Authenticated User"
 01117                        : "System";
 1118
 1119                // Сохраняем параметры для фонового Task
 01120                var chatIdForTask = request.ChatId;
 01121                var fileNameForTask = request.FileName;
 01122                var clientIdForTask = request.ClientId;
 1123
 1124                // Обновление статистики ответов в фоне (не блокируем ответ)
 01125                _ = Task.Run(async () =>
 01126                {
 01127                    // Создаем новый scope для scoped сервисов (важно! иначе сервис может быть disposed)
 01128                    using var scope = _serviceScopeFactory.CreateScope();
 01129                    var chatStatisticService =
 01130                        scope.ServiceProvider.GetRequiredService<OzonChatResponseStatisticDataServices>();
 01131                    var ozonClientServices = scope.ServiceProvider.GetRequiredService<OzonClientServices>();
 01132
 01133                    try
 01134                    {
 01135                        // Небольшая задержка для того чтобы файл точно появился в истории
 01136                        await Task.Delay(500);
 01137
 01138                        // Найти последнее неотвеченное сообщение от покупателя в этом чате
 01139                        var unansweredMessages = await chatStatisticService.GetByChatIdAsync(chatIdForTask);
 01140                        var lastUnanswered = unansweredMessages
 01141                            .Where(m => !m.IsAnswered)
 01142                            .OrderByDescending(m => m.ClientMessageDate)
 01143                            .FirstOrDefault();
 01144
 01145                        if (lastUnanswered != null)
 01146                        {
 01147                            // Получаем клиента для создания нового chatManager
 01148                            var ozonClientForTask = await ozonClientServices.GetOzonClientAsync(clientIdForTask);
 01149                            if (ozonClientForTask == null || string.IsNullOrEmpty(ozonClientForTask.DecryptClientId) ||
 01150                                string.IsNullOrEmpty(ozonClientForTask.DecryptApiKey))
 01151                            {
 01152                                Console.WriteLine($"Не удалось получить клиента Ozon для обновления статистики");
 01153                                return;
 01154                            }
 01155
 01156                            var httpClientFactory = scope.ServiceProvider.GetRequiredService<IHttpClientFactory>();
 01157                            var newChatManager = new OzonChatManager(httpClientFactory,
 01158                                ozonClientForTask.DecryptClientId,
 01159                                ozonClientForTask.DecryptApiKey);
 01160
 01161                            // Получаем историю чата для получения messageId отправленного файла
 01162                            var history = await newChatManager.GetChatHistoryAsync(chatIdForTask, 10, null, "Backward");
 01163                            if (history?.Messages != null && history.Messages.Any())
 01164                            {
 01165                                // Находим последнее сообщение от продавца (это должен быть наш файл)
 01166                                var sentMessage = history.Messages
 01167                                    .Where(m => m.User?.Type?.Equals("Seller", StringComparison.OrdinalIgnoreCase) ==
 01168                                                true)
 01169                                    .OrderByDescending(m => m.CreatedAt)
 01170                                    .FirstOrDefault();
 01171
 01172                                if (sentMessage != null)
 01173                                {
 01174                                    var fileText = $"[Файл: {fileNameForTask}]";
 01175
 01176                                    await chatStatisticService.UpdateResponseAsync(
 01177                                        chatId: chatIdForTask,
 01178                                        clientMessageId: lastUnanswered.ClientMessageId,
 01179                                        sellerResponseMessageId: sentMessage.MessageId,
 01180                                        sellerResponseText: fileText,
 01181                                        sellerResponseDate: sentMessage.CreatedAt.ToUniversalTime(),
 01182                                        responderName: currentUserName
 01183                                    );
 01184
 01185                                    Console.WriteLine(
 01186                                        $"Статистика обновлена: ответ (файл) от {currentUserName} на сообщение {lastUnan
 01187                                }
 01188                                else
 01189                                {
 01190                                    Console.WriteLine(
 01191                                        $"Не найдено сообщение от продавца в истории чата {chatIdForTask}");
 01192                                }
 01193                            }
 01194                            else
 01195                            {
 01196                                Console.WriteLine($"История чата {chatIdForTask} пуста или не загружена");
 01197                            }
 01198                        }
 01199                        else
 01200                        {
 01201                            Console.WriteLine($"Нет неотвеченных сообщений в чате {chatIdForTask}");
 01202                        }
 01203                    }
 01204                    catch (Exception ex)
 01205                    {
 01206                        Console.WriteLine($"Ошибка обновления статистики ответа (файл): {ex.Message}");
 01207                    }
 01208                });
 1209
 01210                return Json(new
 01211                {
 01212                    success = true,
 01213                    message = "Файл отправлен успешно"
 01214                });
 1215            }
 01216            catch (Exception ex)
 1217            {
 01218                return Json(new { success = false, message = $"Ошибка: {ex.Message}" });
 1219            }
 01220        }
 1221
 1222        /// <summary>
 1223        /// Экспорт чатов в Excel
 1224        /// </summary>
 1225        [HttpPost]
 1226        public async Task<IActionResult> ExportToExcel(int clientId, string status = "All", bool unreadOnly = false)
 1227        {
 1228            try
 1229            {
 01230                var ozonClient = await _ozonClientServices.GetOzonClientAsync(clientId);
 01231                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 01232                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 1233                {
 01234                    TempData["ErrorMessage"] = "Клиент Ozon не найден или некорректные данные API";
 01235                    return RedirectToAction("Index");
 1236                }
 1237
 01238                var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 01239                    ozonClient.DecryptApiKey);
 01240                var allChats = await chatManager.GetAllChatsAsync(status, unreadOnly);
 1241
 1242                // Создаём простой CSV для экспорта
 01243                var csvData = "ID чата,Статус,Тип,Дата создания,Непрочитанных сообщений\n";
 01244                foreach (var chat in allChats)
 1245                {
 01246                    csvData += $"{chat.Chat.ChatId},{chat.Chat.ChatStatus},{chat.Chat.ChatType}," +
 01247                               $"{chat.Chat.CreatedAt:yyyy-MM-dd HH:mm:ss},{chat.UnreadCount}\n";
 1248                }
 1249
 01250                var bytes = System.Text.Encoding.UTF8.GetBytes(csvData);
 01251                var fileName = $"ozon_chats_{ozonClient.Name}_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
 1252
 01253                return File(bytes, "text/csv", fileName);
 1254            }
 01255            catch (Exception ex)
 1256            {
 01257                TempData["ErrorMessage"] = $"Ошибка при экспорте: {ex.Message}";
 01258                return RedirectToAction("Index");
 1259            }
 01260        }
 1261
 1262        /// <summary>
 1263        /// API: Получить все чаты для конкретного заказа
 1264        /// </summary>
 1265        [HttpGet]
 1266        public async Task<IActionResult> GetOrderChats(int orderId)
 1267        {
 1268            try
 1269            {
 01270                if (orderId <= 0)
 01271                    return Json(new { success = false, message = "Некорректный ID заказа" });
 1272
 01273                var chatOrders = await _chatOrderService.GetByOrderIdAsync(orderId);
 1274
 01275                return Json(new
 01276                {
 01277                    success = true,
 01278                    orderId,
 01279                    chats = chatOrders.Select(co => new
 01280                    {
 01281                        id = co.Id,
 01282                        chatId = co.ChatId,
 01283                        clientId = co.ClientId,
 01284                        clientName = co.Client?.Name,
 01285                        postingNumber = co.PostingNumber,
 01286                        unreadMessagesCount = co.UnreadMessagesCount,
 01287                        lastMessageDate = ToMoscowTime(co.LastMessageDate)?.ToString("yyyy-MM-dd HH:mm:ss"),
 01288                        lastMessageText = co.LastMessageText,
 01289                        lastMessageFromCustomer = co.LastMessageFromCustomer,
 01290                        chatStatus = co.ChatStatus,
 01291                        chatType = co.ChatType,
 01292                        createdAt = ToMoscowTime(co.CreatedAt).ToString("yyyy-MM-dd HH:mm:ss"),
 01293                        updatedAt = ToMoscowTime(co.UpdatedAt).ToString("yyyy-MM-dd HH:mm:ss")
 01294                    }),
 01295                    totalUnreadCount = chatOrders.Sum(co => co.UnreadMessagesCount)
 01296                });
 1297            }
 01298            catch (Exception ex)
 1299            {
 01300                return Json(new { success = false, message = $"Ошибка при получении чатов заказа: {ex.Message}" });
 1301            }
 01302        }
 1303
 1304        /// <summary>
 1305        /// API: Получить все заказы для конкретного чата
 1306        /// </summary>
 1307        [HttpGet]
 1308        public async Task<IActionResult> GetChatOrders(string chatId)
 1309        {
 1310            try
 1311            {
 01312                if (string.IsNullOrEmpty(chatId))
 01313                    return Json(new { success = false, message = "Не указан ID чата" });
 1314
 01315                var chatOrders = await _chatOrderService.GetByChatIdAsync(chatId);
 1316
 01317                return Json(new
 01318                {
 01319                    success = true,
 01320                    chatId,
 01321                    orders = chatOrders.Select(co => new
 01322                    {
 01323                        id = co.Id,
 01324                        orderId = co.OrderId,
 01325                        orderShipmentNumber = co.Order?.ShipmentNumber,
 01326                        orderProcessingDate = ToMoscowTime(co.Order?.ProcessingDate)?.ToString("yyyy-MM-dd HH:mm:ss"),
 01327                        orderShippingDate = ToMoscowTime(co.Order?.ShippingDate)?.ToString("yyyy-MM-dd HH:mm:ss"),
 01328                        orderAppStatus = co.Order?.AppStatus?.Name,
 01329                        orderClientStatus = co.Order?.ClientStatus?.Name,
 01330                        clientId = co.ClientId,
 01331                        clientName = co.Client?.Name,
 01332                        postingNumber = co.PostingNumber,
 01333                        unreadMessagesCount = co.UnreadMessagesCount,
 01334                        lastMessageDate = ToMoscowTime(co.LastMessageDate)?.ToString("yyyy-MM-dd HH:mm:ss"),
 01335                        lastMessageText = co.LastMessageText,
 01336                        lastMessageFromCustomer = co.LastMessageFromCustomer,
 01337                        chatStatus = co.ChatStatus,
 01338                        chatType = co.ChatType
 01339                    }),
 01340                    totalUnreadCount = chatOrders.Sum(co => co.UnreadMessagesCount)
 01341                });
 1342            }
 01343            catch (Exception ex)
 1344            {
 01345                return Json(new { success = false, message = $"Ошибка при получении заказов чата: {ex.Message}" });
 1346            }
 01347        }
 1348
 1349        /// <summary>
 1350        /// API: Связать чат с заказом вручную
 1351        /// </summary>
 1352        [HttpPost]
 1353        public async Task<IActionResult> LinkChatToOrder([FromBody] LinkChatToOrderRequest request)
 1354        {
 1355            try
 1356            {
 01357                if (string.IsNullOrEmpty(request.ChatId) || request.OrderId <= 0 || request.ClientId <= 0)
 01358                    return Json(new { success = false, message = "Некорректные параметры запроса" });
 1359
 01360                var chatOrder = await _chatOrderService.CreateOrUpdateAsync(
 01361                    chatId: request.ChatId,
 01362                    orderId: request.OrderId,
 01363                    clientId: request.ClientId,
 01364                    postingNumber: request.PostingNumber,
 01365                    unreadCount: 0
 01366                );
 1367
 01368                return Json(new
 01369                {
 01370                    success = true,
 01371                    message = "Чат успешно связан с заказом",
 01372                    chatOrder = new
 01373                    {
 01374                        id = chatOrder.Id,
 01375                        chatId = chatOrder.ChatId,
 01376                        orderId = chatOrder.OrderId,
 01377                        clientId = chatOrder.ClientId,
 01378                        postingNumber = chatOrder.PostingNumber
 01379                    }
 01380                });
 1381            }
 01382            catch (Exception ex)
 1383            {
 01384                return Json(new { success = false, message = $"Ошибка при связывании чата с заказом: {ex.Message}" });
 1385            }
 01386        }
 1387
 1388        /// <summary>
 1389        /// API: Подсказки для поиска (артикулы, товары, номера заказов)
 1390        /// </summary>
 1391        [HttpGet]
 1392        public async Task<IActionResult> GetChatSearchSuggestions(int clientId = 0, string query = "")
 1393        {
 1394            try
 1395            {
 01396                if (string.IsNullOrWhiteSpace(query))
 01397                    return Json(new { success = true, suggestions = new object[0] });
 1398
 1399                // Если запрос совпадает с форматом номера отправления — попробуем вернуть точную запись сразу
 01400                if (PostingNumberRegex.IsMatch(query))
 1401                {
 01402                    var exact = await _chatOrderService.GetByPostingNumberAsync(query);
 01403                    if (exact != null)
 1404                    {
 1405                        // Отдаем сразу одну запись, без массива suggestions
 01406                        return Json(new
 01407                        {
 01408                            success = true,
 01409                            found = true,
 01410                            record = new
 01411                            {
 01412                                type = "order",
 01413                                label = exact.PostingNumber,
 01414                                icon = "fas fa-box",
 01415                                hint = "Номер отправления",
 01416                                chatId = exact.ChatId,
 01417                                clientId = exact.ClientId
 01418                            }
 01419                        });
 1420                    }
 1421                }
 1422
 01423                var suggestions = new List<object>();
 1424
 1425                // 1. Номера заказов (PostingNumber) из таблицы OzonChatOrders (подсказки)
 01426                var orderNumbers = await _chatOrderService.GetDistinctPostingNumbersAsync(query, clientId, 10);
 01427                suggestions.AddRange(orderNumbers.Select(n => new
 01428                {
 01429                    type = "order",
 01430                    label = n,
 01431                    icon = "fas fa-box",
 01432                    hint = "Номер заказа"
 01433                } as object));
 1434
 1435                // 2. Артикулы и названия товаров из сообщений покупателей
 01436                var buyerMessages = await _chatStatisticService.GetSampleBuyerMessagesAsync(query, clientId, 100);
 1437
 01438                var seenArticles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 01439                var seenProducts = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 1440
 01441                foreach (var (_, msgText, _, _) in buyerMessages)
 1442                {
 1443                    // Извлекаем артикулы
 01444                    var articleMatches = ArticleRegex.Matches(msgText);
 01445                    foreach (Match m in articleMatches)
 1446                    {
 01447                        var art = m.Groups[1].Value.Trim();
 01448                        if (art.Length >= 2 && seenArticles.Add(art) && seenArticles.Count <= 10)
 1449                        {
 01450                            suggestions.Add(new
 01451                            {
 01452                                type = "article",
 01453                                label = art,
 01454                                icon = "fas fa-tag",
 01455                                hint = "Артикул"
 01456                            } as object);
 1457                        }
 1458                    }
 1459
 1460                    // Извлекаем названия товаров (text in quotes)
 01461                    var productMatches = ProductNameRegex.Matches(msgText);
 01462                    foreach (Match m in productMatches)
 1463                    {
 01464                        var prod = m.Groups[1].Value.Trim();
 01465                        if (prod.Length >= 5 && seenProducts.Add(prod) && seenProducts.Count <= 10)
 1466                        {
 01467                            suggestions.Add(new
 01468                            {
 01469                                type = "product",
 01470                                label = prod,
 01471                                icon = "fas fa-cube",
 01472                                hint = "Товар"
 01473                            } as object);
 1474                        }
 1475                    }
 1476                }
 1477
 01478                return Json(new { success = true, suggestions });
 1479            }
 01480            catch (Exception ex)
 1481            {
 01482                return Json(new { success = false, message = ex.Message, suggestions = new object[0] });
 1483            }
 01484        }
 1485
 1486        /// <summary>
 1487        /// API: Поиск чатов по артикулу, названию товара, номеру заказа (из БД, без Ozon API)
 1488        /// </summary>
 1489        [HttpGet]
 1490        public async Task<IActionResult> SearchChatsDb(int clientId = 0, string query = "")
 1491        {
 1492            try
 1493            {
 01494                if (string.IsNullOrWhiteSpace(query) || query.Length < 2)
 01495                    return Json(new { success = false, message = "Запрос должен содержать минимум 2 символа" });
 1496
 01497                var chatOrdersDict = new Dictionary<string, OzonChatOrder>(StringComparer.OrdinalIgnoreCase);
 1498
 1499                // Если запрос выглядит как точный номер отправления — ищем по нему и возвращаем связанный чат
 01500                if (PostingNumberRegex.IsMatch(query))
 1501                {
 01502                    var exact = await _chatOrderService.GetByPostingNumberAsync(query);
 01503                    if (exact != null)
 1504                    {
 01505                        chatOrdersDict[exact.ChatId] = exact;
 01506                        var allChatOrdersSingle = chatOrdersDict.Values
 01507                            .OrderByDescending(co => co.LastMessageDate ?? co.CreatedAt)
 01508                            .ToList();
 1509
 01510                        return Json(new
 01511                        {
 01512                            success = true,
 01513                            query,
 01514                            chats = allChatOrdersSingle.Select(co => new
 01515                            {
 01516                                chatId = co.ChatId,
 01517                                chatStatus = co.ChatStatus ?? "UNSPECIFIED",
 01518                                chatType = co.ChatType ?? "UNSPECIFIED",
 01519                                createdAt = ToMoscowTime(co.CreatedAt).ToString("yyyy-MM-dd HH:mm:ss"),
 01520                                createdAtFormatted = ToMoscowTime(co.CreatedAt).ToString("dd.MM.yyyy HH:mm"),
 01521                                unreadCount = co.UnreadMessagesCount,
 01522                                lastMessageId = 0,
 01523                                clientId = co.ClientId,
 01524                                clientName = co.Client?.Name ?? $"Клиент {co.ClientId}",
 01525                                postingNumber = co.PostingNumber,
 01526                                lastMessageText = co.LastMessageText ?? "",
 01527                                lastMessageDate = co.LastMessageDate.HasValue
 01528                                    ? ToMoscowTime(co.LastMessageDate.Value).ToString("dd.MM HH:mm")
 01529                                    : null,
 01530                                lastMessageFromCustomer = co.LastMessageFromCustomer
 01531                            }),
 01532                            totalChatsCount = allChatOrdersSingle.Count
 01533                        });
 1534                    }
 1535                }
 1536
 1537                // 1. Поиск по PostingNumber в OzonChatOrders
 01538                var byPosting = await _chatOrderService.SearchByPostingNumberAsync(query, clientId, 50);
 01539                foreach (var co in byPosting)
 01540                    chatOrdersDict[co.ChatId] = co;
 1541
 1542                // 2. Поиск по тексту сообщений покупателей в OzonChatResponseStatistics
 01543                var chatIdsFromMessages =
 01544                    await _chatStatisticService.GetChatIdsByMessageQueryAsync(query, clientId, 100);
 01545                if (chatIdsFromMessages.Any())
 1546                {
 01547                    var newChatIds = chatIdsFromMessages.Where(id => !chatOrdersDict.ContainsKey(id)).ToList();
 01548                    if (newChatIds.Any())
 1549                    {
 01550                        var byMessages = await _chatOrderService.GetByChatIdsAsync(newChatIds, clientId);
 01551                        foreach (var co in byMessages)
 01552                            chatOrdersDict.TryAdd(co.ChatId, co);
 1553                    }
 1554                }
 1555
 01556                var allChatOrders = chatOrdersDict.Values
 01557                    .OrderByDescending(co => co.LastMessageDate ?? co.CreatedAt)
 01558                    .ToList();
 1559
 01560                if (!allChatOrders.Any())
 01561                    return Json(new { success = true, query, chats = new object[0], totalChatsCount = 0 });
 1562
 01563                return Json(new
 01564                {
 01565                    success = true,
 01566                    query,
 01567                    chats = allChatOrders.Select(co => new
 01568                    {
 01569                        chatId = co.ChatId,
 01570                        chatStatus = co.ChatStatus ?? "UNSPECIFIED",
 01571                        chatType = co.ChatType ?? "UNSPECIFIED",
 01572                        createdAt = ToMoscowTime(co.CreatedAt).ToString("yyyy-MM-dd HH:mm:ss"),
 01573                        createdAtFormatted = ToMoscowTime(co.CreatedAt).ToString("dd.MM.yyyy HH:mm"),
 01574                        unreadCount = co.UnreadMessagesCount,
 01575                        lastMessageId = 0,
 01576                        clientId = co.ClientId,
 01577                        clientName = co.Client?.Name ?? $"Клиент {co.ClientId}",
 01578                        postingNumber = co.PostingNumber,
 01579                        lastMessageText = co.LastMessageText ?? "",
 01580                        lastMessageDate = co.LastMessageDate.HasValue
 01581                            ? ToMoscowTime(co.LastMessageDate.Value).ToString("dd.MM HH:mm")
 01582                            : null,
 01583                        lastMessageFromCustomer = co.LastMessageFromCustomer
 01584                    }),
 01585                    totalChatsCount = allChatOrders.Count
 01586                });
 1587            }
 01588            catch (Exception ex)
 1589            {
 01590                return Json(new { success = false, message = $"Ошибка поиска: {ex.Message}" });
 1591            }
 01592        }
 1593
 1594        /// <summary>
 1595        /// Прокси для загрузки файлов из Ozon Chat API
 1596        /// (файлы требуют авторизации через Client-Id и Api-Key)
 1597        /// </summary>
 1598        [HttpGet]
 1599        public async Task<IActionResult> GetChatFile(int clientId, string fileUrl)
 1600        {
 1601            try
 1602            {
 01603                if (string.IsNullOrWhiteSpace(fileUrl))
 1604                {
 01605                    return BadRequest("fileUrl is required");
 1606                }
 1607
 01608                var ozonClient = await _ozonClientServices.GetOzonClientAsync(clientId);
 01609                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 01610                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 1611                {
 01612                    return NotFound("Ozon client not found");
 1613                }
 1614
 1615                // Проверяем, что URL относится к api-seller.ozon.ru
 01616                if (!fileUrl.StartsWith("https://api-seller.ozon.ru/"))
 1617                {
 01618                    return BadRequest("Invalid file URL");
 1619                }
 1620
 01621                using var httpClient = _httpClientFactory.CreateClient();
 01622                httpClient.Timeout = TimeSpan.FromSeconds(30);
 01623                httpClient.DefaultRequestHeaders.Clear();
 01624                httpClient.DefaultRequestHeaders.Add("Client-Id", ozonClient.DecryptClientId);
 01625                httpClient.DefaultRequestHeaders.Add("Api-Key", ozonClient.DecryptApiKey);
 1626
 01627                var response = await httpClient.GetAsync(fileUrl);
 1628
 01629                if (!response.IsSuccessStatusCode)
 1630                {
 01631                    return StatusCode((int)response.StatusCode, "Failed to fetch file from Ozon");
 1632                }
 1633
 01634                var contentType = response.Content.Headers.ContentType?.MediaType ?? "application/octet-stream";
 01635                var fileBytes = await response.Content.ReadAsByteArrayAsync();
 1636
 01637                return File(fileBytes, contentType);
 1638            }
 01639            catch (Exception ex)
 1640            {
 01641                return StatusCode(500, $"Error fetching file: {ex.Message}");
 1642            }
 01643        }
 1644
 1645        /// <summary>
 1646        /// API: Получить статистику ответов менеджеров
 1647        /// </summary>
 1648        [HttpGet]
 1649        public async Task<IActionResult> GetResponseStatistics(int clientId = 0, DateTime? from = null,
 1650            DateTime? to = null, string? responderName = null, int page = 1, int pageSize = 50)
 1651        {
 1652            try
 1653            {
 01654                if (page < 1) page = 1;
 01655                pageSize = Math.Clamp(pageSize, 1, 100);
 1656
 1657                // Если даты не указаны - берем последние 30 дней
 01658                var dateFrom = from ?? DateTime.UtcNow.AddDays(-30);
 1659                // Для dateTo используем конец дня, чтобы включить все записи за этот день
 01660                var dateTo = to.HasValue
 01661                    ? to.Value.Date.AddDays(1).AddTicks(-1) // Конец дня (23:59:59.9999999)
 01662                    : DateTime.UtcNow.AddDays(1); // Завтра, чтобы включить сегодняшние записи
 1663
 1664                List<OzonChatResponseStatistic> statistics;
 1665
 01666                if (clientId > 0)
 1667                {
 01668                    statistics = await _chatStatisticService.GetByClientIdAsync(clientId, dateFrom, dateTo);
 1669                }
 1670                else
 1671                {
 1672                    // Получаем по всем клиентам
 01673                    var allStats = new List<OzonChatResponseStatistic>();
 01674                    var clients = await _ozonClientServices.GetOzonClients();
 01675                    foreach (var client in clients.Where(c => c.ClientType == ClientType.OZON))
 1676                    {
 01677                        var clientStats = await _chatStatisticService.GetByClientIdAsync(client.Id, dateFrom, dateTo);
 01678                        allStats.AddRange(clientStats);
 1679                    }
 1680
 01681                    statistics = allStats.OrderByDescending(s => s.SellerResponseDate ?? s.CreatedAt).ToList();
 01682                }
 1683
 1684                // Список всех уникальных менеджеров (для выпадающего списка)
 01685                var allResponders = statistics
 01686                    .Where(s => !string.IsNullOrEmpty(s.ResponderName) && s.SellerResponseDate.HasValue)
 01687                    .Select(s => s.ResponderName!)
 01688                    .Distinct()
 01689                    .OrderBy(n => n)
 01690                    .ToList();
 1691
 1692                // Исключаем записи без даты ответа
 01693                statistics = statistics.Where(s => s.SellerResponseDate.HasValue).ToList();
 1694
 1695                // Фильтрация по менеджеру
 01696                if (!string.IsNullOrEmpty(responderName))
 1697                {
 01698                    statistics = statistics
 01699                        .Where(s => s.ResponderName != null &&
 01700                                    s.ResponderName.Equals(responderName, StringComparison.OrdinalIgnoreCase))
 01701                        .ToList();
 1702                }
 1703
 1704                // Статистика для сводки
 01705                var totalResponses = statistics.Count;
 01706                var respondersStats = statistics
 01707                    .Where(s => !string.IsNullOrEmpty(s.ResponderName))
 01708                    .GroupBy(s => s.ResponderName)
 01709                    .Select(g => new
 01710                    {
 01711                        name = g.Key,
 01712                        count = g.Count(),
 01713                        avgResponseTime = g.Where(x => x.ResponseTimeSeconds.HasValue)
 01714                            .Average(x => (double?)x.ResponseTimeSeconds) ?? 0
 01715                    })
 01716                    .OrderByDescending(x => x.count)
 01717                    .ToList();
 1718
 01719                var avgResponseTime = statistics
 01720                    .Where(s => s.ResponseTimeSeconds.HasValue && s.ResponseTimeSeconds > 0)
 01721                    .Average(s => (double?)s.ResponseTimeSeconds) ?? 0;
 1722
 1723                // Пагинация
 01724                var totalItems = statistics.Count;
 01725                var totalPages = totalItems > 0 ? (int)Math.Ceiling((double)totalItems / pageSize) : 1;
 01726                var pagedStats = statistics
 01727                    .OrderByDescending(s => s.SellerResponseDate ?? s.CreatedAt)
 01728                    .Skip((page - 1) * pageSize)
 01729                    .Take(pageSize)
 01730                    .ToList();
 1731
 01732                return Json(new
 01733                {
 01734                    success = true,
 01735                    allResponders, // Список всех менеджеров для выпадающего списка
 01736                    summary = new
 01737                    {
 01738                        totalResponses,
 01739                        avgResponseTimeSeconds = Math.Round(avgResponseTime, 0),
 01740                        avgResponseTimeFormatted = FormatSeconds((int)avgResponseTime),
 01741                        responders = respondersStats,
 01742                        dateFrom = dateFrom.ToString("dd.MM.yyyy"),
 01743                        dateTo = dateTo.ToString("dd.MM.yyyy")
 01744                    },
 01745                    statistics = pagedStats.Select(s => new
 01746                    {
 01747                        id = s.Id,
 01748                        chatId = s.ChatId,
 01749                        clientId = s.ClientId,
 01750                        orderId = s.OrderId,
 01751                        postingNumber = s.PostingNumber,
 01752                        sellerResponseText = s.SellerResponseText != null && s.SellerResponseText.Length > 100
 01753                            ? s.SellerResponseText.Substring(0, 100) + "..."
 01754                            : s.SellerResponseText,
 01755                        sellerResponseDate = ToMoscowTime(s.SellerResponseDate)?.ToString("dd.MM.yyyy HH:mm"),
 01756                        responseTimeSeconds = s.ResponseTimeSeconds,
 01757                        responseTimeFormatted = FormatSeconds(s.ResponseTimeSeconds ?? 0),
 01758                        responderName = s.ResponderName ?? "Неизвестно",
 01759                        chatType = s.ChatType,
 01760                        chatStatus = s.ChatStatus
 01761                    }),
 01762                    pagination = new
 01763                    {
 01764                        currentPage = page,
 01765                        pageSize,
 01766                        totalPages,
 01767                        totalItems
 01768                    }
 01769                });
 1770            }
 01771            catch (Exception ex)
 1772            {
 01773                return Json(new { success = false, message = $"Ошибка получения статистики: {ex.Message}" });
 1774            }
 01775        }
 1776
 1777        /// <summary>
 1778        /// API: Получить записи трекинга времени по чату
 1779        /// </summary>
 1780        [HttpGet]
 1781        public async Task<IActionResult> GetResponseTimes(string chatId)
 1782        {
 1783            try
 1784            {
 01785                if (string.IsNullOrEmpty(chatId))
 01786                    return Json(new { success = false, message = "Не указан ID чата" });
 1787
 01788                var records = await _chatTimeService.GetByChatIdAsync(chatId);
 01789                var list = records.Select(r => new
 01790                {
 01791                    r.Id,
 01792                    r.ChatId,
 01793                    r.ClientId,
 01794                    userName = r.UserName,
 01795                    minutes = r.Minutes,
 01796                    recordedAt = ToMoscowTime(r.RecordedAt).ToString("yyyy-MM-dd HH:mm:ss")
 01797                }).ToList();
 1798
 01799                return Json(new { success = true, times = list });
 1800            }
 01801            catch (Exception ex)
 1802            {
 01803                return Json(new { success = false, message = ex.Message });
 1804            }
 01805        }
 1806
 1807        /// <summary>
 1808        /// API: Добавить запись времени ответа
 1809        /// </summary>
 1810        [HttpPost]
 1811        public async Task<IActionResult> AddResponseTime([FromBody] AddResponseTimeRequest request)
 1812        {
 1813            try
 1814            {
 01815                if (request == null || string.IsNullOrEmpty(request.ChatId) || request.ClientId <= 0 || request.Minutes 
 01816                    return Json(new { success = false, message = "Некорректные параметры запроса" });
 1817
 01818                var record = new OzonDomains.Models.OzonChatResponseTime
 01819                {
 01820                    ChatId = request.ChatId,
 01821                    ClientId = request.ClientId,
 01822                    UserName = !string.IsNullOrEmpty(User.Identity?.Name) ? User.Identity.Name : "Unknown",
 01823                    Minutes = request.Minutes,
 01824                    RecordedAt = DateTime.UtcNow,
 01825                    CreatedAt = DateTime.UtcNow
 01826                };
 1827
 01828                await _chatTimeService.Add(record);
 1829
 01830                return Json(new { success = true, recordId = record.Id });
 1831            }
 01832            catch (Exception ex)
 1833            {
 01834                return Json(new { success = false, message = ex.Message });
 1835            }
 01836        }
 1837
 1838        /// <summary>
 1839        /// DTO для добавления времени ответа
 1840        /// </summary>
 1841        public class AddResponseTimeRequest
 1842        {
 01843            public string ChatId { get; set; } = string.Empty;
 01844            public int ClientId { get; set; }
 01845            public int Minutes { get; set; }
 1846        }
 1847
 1848        /// <summary>
 1849        /// API: получить статистику времени (ручной трекинг)
 1850        /// </summary>
 1851        [HttpGet]
 1852        public async Task<IActionResult> GetResponseTimeStats(int clientId = 0, DateTime? from = null,
 1853            DateTime? to = null, int page = 1, int pageSize = 50)
 1854        {
 1855            try
 1856            {
 01857                if (page < 1) page = 1;
 01858                pageSize = Math.Clamp(pageSize, 1, 100);
 1859
 01860                var dateFrom = from ?? DateTime.UtcNow.AddDays(-30);
 01861                var dateTo = to.HasValue
 01862                    ? to.Value.Date.AddDays(1).AddTicks(-1)
 01863                    : DateTime.UtcNow.AddDays(1);
 1864
 1865                List<OzonChatResponseTime> records;
 01866                if (clientId > 0)
 1867                {
 01868                    records = await _chatTimeService.GetByClientIdAsync(clientId);
 1869                }
 1870                else
 1871                {
 01872                    records = new List<OzonChatResponseTime>();
 01873                    var clients = await _ozonClientServices.GetOzonClients();
 01874                    foreach (var c in clients.Where(c => c.ClientType == ClientType.OZON))
 1875                    {
 01876                        var r = await _chatTimeService.GetByClientIdAsync(c.Id);
 01877                        records.AddRange(r);
 1878                    }
 1879                }
 1880
 1881                // фильтруем по диапазону дат
 01882                records = records
 01883                    .Where(r => r.RecordedAt >= dateFrom && r.RecordedAt <= dateTo)
 01884                    .OrderByDescending(r => r.RecordedAt)
 01885                    .ToList();
 1886
 01887                var totalItems = records.Count;
 01888                var totalMinutes = records.Sum(r => r.Minutes);
 01889                var avgMinutes = totalItems > 0 ? (double)totalMinutes / totalItems : 0;
 1890
 01891                var paged = records.Skip((page - 1) * pageSize).Take(pageSize).ToList();
 1892
 01893                return Json(new
 01894                {
 01895                    success = true,
 01896                    summary = new
 01897                    {
 01898                        totalRecords = totalItems,
 01899                        totalMinutes,
 01900                        avgMinutes = Math.Round(avgMinutes, 1),
 01901                        dateFrom = dateFrom.ToString("dd.MM.yyyy"),
 01902                        dateTo = dateTo.ToString("dd.MM.yyyy")
 01903                    },
 01904                    items = paged.Select(r => new
 01905                    {
 01906                        r.Id,
 01907                        r.ChatId,
 01908                        r.ClientId,
 01909                        userName = r.UserName,
 01910                        minutes = r.Minutes,
 01911                        recordedAt = ToMoscowTime(r.RecordedAt).ToString("yyyy-MM-dd HH:mm"),
 01912                        clientName = r.Client?.Name
 01913                    }),
 01914                    pagination = new
 01915                    {
 01916                        currentPage = page,
 01917                        pageSize,
 01918                        totalPages = totalItems > 0 ? (int)Math.Ceiling((double)totalItems / pageSize) : 1,
 01919                        totalItems
 01920                    }
 01921                });
 1922            }
 01923            catch (Exception ex)
 1924            {
 01925                return Json(new { success = false, message = ex.Message });
 1926            }
 01927        }
 1928
 1929        /// <summary>
 1930        /// Форматирование секунд в читаемый вид
 1931        /// </summary>
 1932        private static string FormatSeconds(int totalSeconds)
 1933        {
 01934            if (totalSeconds < 60)
 01935                return $"{totalSeconds} сек";
 01936            if (totalSeconds < 3600)
 01937                return $"{totalSeconds / 60} мин {totalSeconds % 60} сек";
 1938
 01939            var hours = totalSeconds / 3600;
 01940            var minutes = (totalSeconds % 3600) / 60;
 01941            return $"{hours} ч {minutes} мин";
 1942        }
 1943
 1944        /// <summary>
 1945        /// Конвертация времени UTC в московское время (UTC+3)
 1946        /// </summary>
 1947        private static DateTime ToMoscowTime(DateTime utcTime)
 1948        {
 01949            if (utcTime.Kind == DateTimeKind.Local)
 01950                utcTime = utcTime.ToUniversalTime();
 01951            else if (utcTime.Kind == DateTimeKind.Unspecified)
 01952                utcTime = DateTime.SpecifyKind(utcTime, DateTimeKind.Utc);
 1953
 01954            return TimeZoneInfo.ConvertTimeFromUtc(utcTime, MoscowTimeZone);
 1955        }
 1956
 1957        /// <summary>
 1958        /// Конвертация nullable времени UTC в московское время
 1959        /// </summary>
 1960        private static DateTime? ToMoscowTime(DateTime? utcTime)
 1961        {
 01962            return utcTime.HasValue ? ToMoscowTime(utcTime.Value) : null;
 1963        }
 1964    }
 1965
 1966    /// <summary>
 1967    /// DTO для запроса отметки сообщений как прочитанных
 1968    /// </summary>
 1969    public class MarkAsReadRequest
 1970    {
 1971        public int ClientId { get; set; }
 1972        public string ChatId { get; set; } = string.Empty;
 1973        public long? FromMessageId { get; set; }
 1974    }
 1975
 1976    /// <summary>
 1977    /// DTO для запроса отправки сообщения
 1978    /// </summary>
 1979    public class SendMessageRequest
 1980    {
 1981        public int ClientId { get; set; }
 1982        public string ChatId { get; set; } = string.Empty;
 1983        public string Text { get; set; } = string.Empty;
 1984    }
 1985
 1986    /// <summary>
 1987    /// DTO для запроса отправки файла
 1988    /// </summary>
 1989    public class SendFileRequest
 1990    {
 1991        public int ClientId { get; set; }
 1992        public string ChatId { get; set; } = string.Empty;
 1993        public string Base64Content { get; set; } = string.Empty;
 1994        public string FileName { get; set; } = string.Empty;
 1995    }
 1996}