using FruitBank.Common.Entities; using FruitBank.Common.Enums; using Microsoft.AspNetCore.Mvc; using Nop.Core.Domain.Customers; using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models; using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; using Nop.Plugin.Misc.FruitBankPlugin.Services; using Nop.Services.Customers; using Nop.Services.Security; using Nop.Web.Framework; using Nop.Web.Framework.Controllers; using Nop.Web.Framework.Mvc.Filters; namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers; [AuthorizeAdmin] [Area(AreaNames.ADMIN)] [AutoValidateAntiforgeryToken] public class PreorderAdminController : BasePluginController { private readonly IPermissionService _permissionService; private readonly PreorderDbContext _preorderDbContext; private readonly FruitBankDbContext _dbContext; private readonly ICustomerService _customerService; private readonly PreorderConversionService _preorderConversionService; private static readonly Dictionary StatusLabels = new() { { PreorderStatus.Pending, "Függőben" }, { PreorderStatus.Confirmed, "Megerősítve" }, { PreorderStatus.PartiallyFulfilled, "Részben teljesítve" }, { PreorderStatus.Cancelled, "Törölve" } }; private static readonly Dictionary ItemStatusLabels = new() { { PreorderItemStatus.Pending, "Függőben" }, { PreorderItemStatus.Fulfilled, "Teljesítve" }, { PreorderItemStatus.PartiallyFulfilled, "Részben" }, { PreorderItemStatus.Dropped, "Ejtve" } }; public PreorderAdminController( IPermissionService permissionService, PreorderDbContext preorderDbContext, FruitBankDbContext dbContext, ICustomerService customerService, PreorderConversionService preorderConversionService) { _permissionService = permissionService; _preorderDbContext = preorderDbContext; _dbContext = dbContext; _customerService = customerService; _preorderConversionService = preorderConversionService; } // ── LIST PAGE ───────────────────────────────────────────────────────────── [HttpGet] [Route("Admin/Preorders")] public async Task List() { if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) return AccessDeniedView(); return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Preorder/List.cshtml"); } // ── DATATABLES SERVER-SIDE ──────────────────────────────────────────────── [HttpPost] [Route("Admin/Preorders/PreorderList")] public async Task PreorderList() { if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) return Forbid(); _ = int.TryParse(Request.Form["draw"].FirstOrDefault(), out int draw); draw = Math.Max(draw, 1); _ = int.TryParse(Request.Form["start"].FirstOrDefault(), out int start); start = Math.Max(start, 0); _ = int.TryParse(Request.Form["length"].FirstOrDefault(), out int length); length = length < 1 ? 25 : Math.Min(length, 500); _ = int.TryParse(Request.Form["order[0][column]"].FirstOrDefault(), out int sortColIdx); var sortDir = Request.Form["order[0][dir]"].FirstOrDefault() ?? "desc"; var sortColName = Request.Form[$"columns[{sortColIdx}][name]"].FirstOrDefault() ?? "CreatedOnUtc"; var globalSearch = Request.Form["search[value]"].FirstOrDefault()?.Trim() ?? ""; var statusFilter = Request.Form["statusFilter"].FirstOrDefault()?.Trim() ?? ""; // 1. All preorders with items — two queries var preorders = await _preorderDbContext.Preorders.GetAll(false).ToListAsync(); var allItems = await _preorderDbContext.PreorderItems.GetAll().ToListAsync(); var itemsByPreorder = allItems .GroupBy(i => i.PreorderId) .ToDictionary(g => g.Key, g => g.ToList()); // 2. Customers — batch var customerIds = preorders.Select(p => p.CustomerId).Distinct().ToList(); var customers = await _dbContext.Customers.Table .Where(c => customerIds.Contains(c.Id)) .Select(c => new { c.Id, c.Email, c.FirstName, c.LastName }) .ToListAsync(); var customerById = customers.ToDictionary(c => c.Id); // 3. Linked orders — find orders created from preorders via CustomOrderNumber lookup // We store the preorder id in the order note, but the simplest link is checking // OrderNotes for "előrendelésből" text matching preorderId. // For now we surface the link on the detail page only. // 4. Build rows — derive status from quantities, not enum (LinqToDB enum reads unreliable) var rows = preorders.Select(p => { customerById.TryGetValue(p.CustomerId, out var c); var items = itemsByPreorder.TryGetValue(p.Id, out var its) ? its : new(); // Derive status from quantities rather than relying on the enum read var fulfilledCount = items.Count(i => i.FulfilledQuantity > 0); var allFulfilled = items.Any() && items.All(i => i.FulfilledQuantity >= i.RequestedQuantity); var anyFulfilled = items.Any(i => i.FulfilledQuantity > 0); var hasOrderId = p.OrderId.HasValue; // Derive a display status: use the DB enum if it looks valid (non-zero), // otherwise infer from quantities var effectiveStatus = (int)p.Status != 0 ? p.Status : allFulfilled ? PreorderStatus.Confirmed : anyFulfilled ? PreorderStatus.PartiallyFulfilled : PreorderStatus.Pending; return new PreorderListRow { PreorderId = p.Id, CustomerId = p.CustomerId, CustomerName = c != null ? $"{c.FirstName} {c.LastName}".Trim() : $"#{p.CustomerId}", CustomerEmail = c?.Email ?? string.Empty, DateOfReceipt = p.DateOfReceipt.ToLocalTime().ToString("yyyy.MM.dd HH:mm"), CreatedOnUtc = p.CreatedOnUtc.ToLocalTime().ToString("yyyy.MM.dd HH:mm"), Status = effectiveStatus, StatusLabel = StatusLabels.TryGetValue(effectiveStatus, out var sl) ? sl : effectiveStatus.ToString(), ItemCount = items.Count, FulfilledCount = fulfilledCount, OrderId = p.OrderId }; }).ToList(); int recordsTotal = rows.Count; // 5. Filter by status if (!string.IsNullOrWhiteSpace(statusFilter) && Enum.TryParse(statusFilter, out var statusEnum)) rows = rows.Where(r => r.Status == statusEnum).ToList(); // 6. Global search if (!string.IsNullOrWhiteSpace(globalSearch)) rows = rows.Where(r => r.CustomerName.Contains(globalSearch, StringComparison.OrdinalIgnoreCase) || r.CustomerEmail.Contains(globalSearch, StringComparison.OrdinalIgnoreCase) || r.PreorderId.ToString().Contains(globalSearch) ).ToList(); int recordsFiltered = rows.Count; // 7. Sort bool asc = sortDir == "asc"; rows = sortColName switch { "CustomerName" => asc ? rows.OrderBy(r => r.CustomerName).ToList() : rows.OrderByDescending(r => r.CustomerName).ToList(), "DateOfReceipt" => asc ? rows.OrderBy(r => r.DateOfReceipt).ToList() : rows.OrderByDescending(r => r.DateOfReceipt).ToList(), "Status" => asc ? rows.OrderBy(r => r.Status).ToList() : rows.OrderByDescending(r => r.Status).ToList(), _ => asc ? rows.OrderBy(r => r.CreatedOnUtc).ToList() : rows.OrderByDescending(r => r.CreatedOnUtc).ToList() }; // 8. Paginate var page = rows.Skip(start).Take(length).ToList(); return Json(new { draw, recordsTotal, recordsFiltered, data = page }); } // ── DETAIL PAGE ─────────────────────────────────────────────────────────── [HttpGet] [Route("Admin/Preorders/Detail/{id:int}")] public async Task Detail(int id) { if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) return AccessDeniedView(); var preorder = await _preorderDbContext.Preorders.GetByIdAsync(id, loadRelations: false); if (preorder == null) return NotFound(); var items = await _preorderDbContext.PreorderItems .GetAllByPreorderIdAsync(id) .ToListAsync(); var customer = await _customerService.GetCustomerByIdAsync(preorder.CustomerId); // Resolve product names in one batch var productIds = items.Select(i => i.ProductId).Distinct().ToList(); var productDtos = await _dbContext.ProductDtos .GetAll(false) .Where(p => productIds.Contains(p.Id)) .ToListAsync(); var productById = productDtos.ToDictionary(p => p.Id); // Use preorder.OrderId directly — stored on the entity at conversion time int? linkedOrderId = preorder.OrderId; var model = new PreorderDetailModel { PreorderId = preorder.Id, CustomerId = preorder.CustomerId, CustomerName = customer != null ? $"{customer.FirstName} {customer.LastName}".Trim() : $"#{preorder.CustomerId}", CustomerEmail = customer?.Email ?? string.Empty, DateOfReceipt = preorder.DateOfReceipt.ToLocalTime().ToString("yyyy.MM.dd HH:mm"), CreatedOnUtc = preorder.CreatedOnUtc.ToLocalTime().ToString("yyyy.MM.dd HH:mm"), UpdatedOnUtc = preorder.UpdatedOnUtc.ToLocalTime().ToString("yyyy.MM.dd HH:mm"), Status = preorder.Status, CustomerNote = preorder.CustomerNote, OrderId = linkedOrderId, Items = items.Select(i => { productById.TryGetValue(i.ProductId, out var dto); // Derive item status from quantities — enum reads unreliable in LinqToDB var derivedStatus = i.FulfilledQuantity == 0 ? PreorderItemStatus.Pending : i.FulfilledQuantity >= i.RequestedQuantity ? PreorderItemStatus.Fulfilled : PreorderItemStatus.PartiallyFulfilled; // If DB enum read as non-zero, prefer it; otherwise use derived var effectiveItemStatus = (int)i.Status != 0 ? i.Status : derivedStatus; return new PreorderDetailItemRow { ItemId = i.Id, ProductId = i.ProductId, ProductName = dto?.Name ?? $"Product #{i.ProductId}", IsMeasurable = dto?.IsMeasurable ?? false, RequestedQuantity = i.RequestedQuantity, FulfilledQuantity = i.FulfilledQuantity, UnitPriceInclTax = i.UnitPriceInclTax, Status = effectiveItemStatus, StatusLabel = ItemStatusLabels.TryGetValue(effectiveItemStatus, out var isl) ? isl : effectiveItemStatus.ToString() }; }).ToList() }; return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Preorder/Detail.cshtml", model); } // ── CREATE (admin phone order) ─────────────────────────────────────────── [HttpPost] [Route("Admin/Preorders/CreatePreorder")] public async Task CreatePreorder( int customerId, string deliveryDateTime, string? customerNote, string productsJson) { if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) return Json(new { success = false, error = "Hozzáférés megtagadva" }); try { // Validate customer var customer = await _customerService.GetCustomerByIdAsync(customerId); if (customer == null) return Json(new { success = false, error = "Az ügyfél nem található" }); // Validate delivery date if (!DateTime.TryParse(deliveryDateTime, out var deliveryDate)) return Json(new { success = false, error = "Érvénytelen szállítási dátum" }); // Parse products if (string.IsNullOrWhiteSpace(productsJson)) return Json(new { success = false, error = "Nincs termék megadva" }); var productItems = System.Text.Json.JsonSerializer.Deserialize>(productsJson); if (productItems == null || !productItems.Any()) return Json(new { success = false, error = "Nincs érvényes termék" }); // Get store var storeId = (await _dbContext.Shippings.GetAll().Select(s => s.Id).FirstOrDefaultAsync() > 0) ? 1 : 1; // fallback to store 1 // Use first available store from generic attributes context var gaStore = await _dbContext.GenericAttributes.Table .Select(g => g.StoreId).FirstOrDefaultAsync(); storeId = gaStore > 0 ? gaStore : 1; var preorder = new Preorder { CustomerId = customerId, StoreId = storeId, DateOfReceipt = deliveryDate, CustomerNote = customerNote?.Trim() }; var items = new List(); foreach (var pi in productItems.Where(p => p.quantity > 0)) { var product = await _dbContext.Products.GetByIdAsync(pi.id); if (product == null || product.Deleted || !product.Published) continue; items.Add(new PreorderItem { ProductId = pi.id, RequestedQuantity = pi.quantity, UnitPriceInclTax = (decimal)pi.price }); } if (!items.Any()) return Json(new { success = false, error = "Nincs érvényes termék az előrendelésben" }); var saved = await _preorderDbContext.InsertPreorderAsync(preorder, items); Console.WriteLine($"[Admin] Created preorder #{saved.Id} for customer #{customerId} " + $"by admin, {items.Count} items, delivery {deliveryDate:u}"); // Immediately check if any items can be fulfilled from current stock — // same inline conversion as the customer-facing PlacePreorder endpoint. var productIds = items.Select(i => i.ProductId).Distinct().ToList(); await _preorderConversionService.ConvertPreordersForProductsAsync(productIds, 0); // Re-read to pick up OrderId if conversion created a real order var refreshed = await _preorderDbContext.Preorders.GetByIdAsync(saved.Id); return Json(new { success = true, preorderId = saved.Id, orderId = refreshed?.OrderId }); } catch (Exception ex) { return Json(new { success = false, error = ex.Message }); } } private class ProductItemRequest { public int id { get; set; } public string? name { get; set; } public int quantity { get; set; } public double price { get; set; } } // ── CANCEL ──────────────────────────────────────────────────────────────── // ── CANCEL ─────────────────────────────────────────────────────────── [HttpPost] [Route("Admin/Preorders/Cancel/{id:int}")] public async Task Cancel(int id) { if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) return Json(new { success = false, error = "Access denied" }); var preorder = await _preorderDbContext.Preorders.GetByIdAsync(id); if (preorder == null) return Json(new { success = false, error = "Preorder not found" }); if (preorder.Status != PreorderStatus.Pending) return Json(new { success = false, error = "Only pending preorders can be cancelled" }); await _preorderDbContext.CancelPreorderAsync(id); return Json(new { success = true }); } // ── DEMAND LIST ─────────────────────────────────────────────────────────── [HttpPost] [Route("Admin/Preorders/DemandList")] public async Task DemandList(bool openOnly = true) { if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) return Forbid(); _ = int.TryParse(Request.Form["draw"].FirstOrDefault(), out int draw); draw = Math.Max(draw, 1); _ = int.TryParse(Request.Form["start"].FirstOrDefault(), out int start); start = Math.Max(start, 0); _ = int.TryParse(Request.Form["length"].FirstOrDefault(), out int length); length = length < 1 ? 25 : Math.Min(length, 500); var openOnlyParam = Request.Form["openOnly"].FirstOrDefault(); openOnly = openOnlyParam != "false"; // Fetch all preorder items + preorders in two queries var allItems = await _preorderDbContext.PreorderItems.GetAll().ToListAsync(); var allPreorders = await _preorderDbContext.Preorders.GetAll(false).ToListAsync(); // For "open only": include only items from preorders that still have // unfulfilled demand (FulfilledQuantity < RequestedQuantity). // We use quantities rather than Status enum (enum reads unreliable). IEnumerable items = allItems; if (openOnly) { // Open preorders: those where at least one item still needs fulfillment var openPreorderIds = allPreorders .Where(p => allItems .Where(i => i.PreorderId == p.Id) .Any(i => i.FulfilledQuantity < i.RequestedQuantity)) .Select(p => p.Id) .ToHashSet(); items = allItems.Where(i => openPreorderIds.Contains(i.PreorderId)); } // Group by product var grouped = items .GroupBy(i => i.ProductId) .Select(g => new { ProductId = g.Key, TotalRequested = g.Sum(i => i.RequestedQuantity), TotalFulfilled = g.Sum(i => i.FulfilledQuantity), TotalUnfulfilled = g.Sum(i => i.RequestedQuantity - i.FulfilledQuantity), PreorderCount = g.Select(i => i.PreorderId).Distinct().Count(), AvgUnitPrice = g.Where(i => i.UnitPriceInclTax > 0).Any() ? g.Where(i => i.UnitPriceInclTax > 0).Average(i => i.UnitPriceInclTax) : 0m }) .OrderByDescending(g => g.TotalUnfulfilled) .ThenByDescending(g => g.TotalRequested) .ToList(); // Resolve product names in one batch var productIds = grouped.Select(g => g.ProductId).Distinct().ToList(); var productDtos = await _dbContext.ProductDtos .GetAll(false) .Where(p => productIds.Contains(p.Id)) .ToListAsync(); var productById = productDtos.ToDictionary(p => p.Id); var rows = grouped.Select(g => { productById.TryGetValue(g.ProductId, out var dto); return new PreorderDemandRow { ProductId = g.ProductId, ProductName = dto?.Name ?? $"Product #{g.ProductId}", Sku = dto?.Id.ToString(), IsMeasurable = dto?.IsMeasurable ?? false, TotalRequested = g.TotalRequested, TotalFulfilled = g.TotalFulfilled, TotalUnfulfilled = g.TotalUnfulfilled, PreorderCount = g.PreorderCount, AvgUnitPrice = Math.Round(g.AvgUnitPrice, 0) }; }).ToList(); int recordsTotal = rows.Count; int recordsFiltered = rows.Count; var page = rows.Skip(start).Take(length).ToList(); return Json(new { draw, recordsTotal, recordsFiltered, data = page }); } }