using FruitBank.Common.Entities; using FruitBank.Common.Server; using LinqToDB; using Microsoft.AspNetCore.Mvc; using Nop.Core; using Nop.Core.Domain.Catalog; using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; using Nop.Plugin.Misc.FruitBankPlugin.Services; using Nop.Services.Catalog; using Nop.Services.Customers; using Nop.Services.Localization; using Nop.Web.Framework.Controllers; namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers; [AutoValidateAntiforgeryToken] public class PreorderController : BasePluginController { private readonly IWorkContext _workContext; private readonly IStoreContext _storeContext; private readonly ICustomerService _customerService; private readonly ILocalizationService _localizationService; private readonly FruitBankDbContext _dbContext; private readonly PreorderDbContext _preorderDbContext; private readonly FruitBankAttributeService _fruitBankAttributeService; private readonly CustomPriceCalculationService _customPriceCalculationService; private const string PendingDeliveryDateTimeKey = "PreorderPendingDeliveryDateTime"; private const string Prefix = "Plugins.Misc.FruitBankPlugin.Preorder."; public PreorderController( IWorkContext workContext, IStoreContext storeContext, ICustomerService customerService, ILocalizationService localizationService, FruitBankDbContext dbContext, PreorderDbContext preorderDbContext, FruitBankAttributeService fruitBankAttributeService, IPriceCalculationService priceCalculationService) { _workContext = workContext; _storeContext = storeContext; _customerService = customerService; _localizationService = localizationService; _dbContext = dbContext; _preorderDbContext = preorderDbContext; _fruitBankAttributeService = fruitBankAttributeService; _customPriceCalculationService = priceCalculationService as CustomPriceCalculationService; } private Task L(string keySuffix) => _localizationService.GetResourceAsync(Prefix + keySuffix); // ── INDEX ───────────────────────────────────────────────────────────────── [HttpGet] public async Task Index() { var customer = await _workContext.GetCurrentCustomerAsync(); if (await _customerService.IsGuestAsync(customer)) return Challenge(); return View("~/Plugins/Misc.FruitBankPlugin/Views/Preorder/Index.cshtml"); } // ── GET SAVED DELIVERY DATETIME (page restore) ──────────────────────────── [HttpGet] public async Task GetDeliveryDateTime() { var customer = await _workContext.GetCurrentCustomerAsync(); if (await _customerService.IsGuestAsync(customer)) return Json(new { success = false }); var store = await _storeContext.GetCurrentStoreAsync(); var saved = await _fruitBankAttributeService .GetGenericAttributeValueAsync( customer.Id, PendingDeliveryDateTimeKey, store.Id); if (!saved.HasValue) return Json(new { success = true, hasValue = false }); return Json(new { success = true, hasValue = true, date = saved.Value.ToString("yyyy-MM-dd"), time = saved.Value.ToString("HH:mm"), iso = saved.Value.ToString("yyyy-MM-ddTHH:mm") }); } // ── SET DELIVERY DATETIME ───────────────────────────────────────────────── [HttpPost] public async Task SetDeliveryDateTime(string deliveryDateTime) { var customer = await _workContext.GetCurrentCustomerAsync(); if (await _customerService.IsGuestAsync(customer)) return Json(new { success = false, message = await L("NotLoggedIn") }); if (string.IsNullOrWhiteSpace(deliveryDateTime)) return Json(new { success = false, message = await L("NoDeliveryDateTimeProvided") }); if (!DateTime.TryParse(deliveryDateTime, out var parsed)) return Json(new { success = false, message = await L("InvalidDeliveryDateTime") }); var store = await _storeContext.GetCurrentStoreAsync(); await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync( customer.Id, PendingDeliveryDateTimeKey, parsed, store.Id); return Json(new { success = true }); } // ── GET AVAILABLE PRODUCTS (filtered by preorder window) ────────────────── [HttpGet] public async Task GetAvailableProducts() { var customer = await _workContext.GetCurrentCustomerAsync(); if (await _customerService.IsGuestAsync(customer)) return Json(new { success = false, message = await L("NotLoggedIn") }); try { var store = await _storeContext.GetCurrentStoreAsync(); var today = DateTime.UtcNow.Date; // Load preorder window generic attributes — two queries, no N+1 var gaStart = await _dbContext.GenericAttributes.Table .Where(ga => ga.KeyGroup == nameof(Product) && ga.Key == FruitBankConst.PreorderWindowStart && ga.StoreId == store.Id) .ToListAsync(); var gaEnd = await _dbContext.GenericAttributes.Table .Where(ga => ga.KeyGroup == nameof(Product) && ga.Key == FruitBankConst.PreorderWindowEnd && ga.StoreId == store.Id) .ToListAsync(); var startById = gaStart.ToDictionary(g => g.EntityId, g => g.Value); var endById = gaEnd.ToDictionary(g => g.EntityId, g => g.Value); // Product IDs that are available today for preorder var availableProductIds = startById.Keys .Intersect(endById.Keys) .Where(id => { DateTime.TryParse(startById[id], out var ws); DateTime.TryParse(endById[id], out var we); return ws.Date <= today && today <= we.Date; }) .ToHashSet(); if (!availableProductIds.Any()) return Json(new { success = true, products = Array.Empty() }); // Load product DTOs for those IDs var productDtos = await _dbContext.ProductDtos .GetAll(true) .Where(p => availableProductIds.Contains(p.Id)) .ToListAsync(); var result = new List(); foreach (var dto in productDtos.OrderBy(p => p.Name)) { decimal? unitPrice = null; if (!dto.IsMeasurable && _customPriceCalculationService != null) { var product = await _dbContext.Products.GetByIdAsync(dto.Id); if (product != null) { var priceResult = await _customPriceCalculationService.GetFinalPriceAsync( product, customer, store, null, 0, true, 1, null, null); unitPrice = priceResult.finalPrice; } } result.Add(new { id = dto.Id, name = dto.Name, isMeasurable = dto.IsMeasurable, unitPrice, stockQuantity = dto.AvailableQuantity }); } return Json(new { success = true, products = result }); } catch (Exception ex) { Console.WriteLine($"[Preorder] GetAvailableProducts error: {ex.Message}"); return Json(new { success = false, message = $"Hiba: {ex.Message}" }); } } // ── PLACE PREORDER ──────────────────────────────────────────────────────── [HttpPost] public async Task PlacePreorder([FromBody] PlacePreorderRequest request) { var customer = await _workContext.GetCurrentCustomerAsync(); if (await _customerService.IsGuestAsync(customer)) return Json(new { success = false, message = await L("NotLoggedIn") }); if (request?.Items == null || !request.Items.Any()) return Json(new { success = false, message = await L("NoItemsSelected") }); if (!DateTime.TryParse(request.DeliveryDateTime, out var deliveryDateTime)) return Json(new { success = false, message = await L("InvalidDeliveryDateTime") }); try { var store = await _storeContext.GetCurrentStoreAsync(); var preorder = new Preorder { CustomerId = customer.Id, StoreId = store.Id, DateOfReceipt = deliveryDateTime, CustomerNote = request.CustomerNote?.Trim() }; var items = new List(); foreach (var req in request.Items.Where(i => i.Quantity > 0)) { var product = await _dbContext.Products.GetByIdAsync(req.ProductId); if (product == null || product.Deleted || !product.Published) continue; decimal unitPrice = 0; if (_customPriceCalculationService != null) { var pr = await _customPriceCalculationService.GetFinalPriceAsync( product, customer, store, null, 0, true, req.Quantity, null, null); unitPrice = pr.finalPrice; } items.Add(new PreorderItem { ProductId = req.ProductId, RequestedQuantity = req.Quantity, UnitPriceInclTax = unitPrice }); } if (!items.Any()) return Json(new { success = false, message = await L("NoValidItems") }); var saved = await _preorderDbContext.InsertPreorderAsync(preorder, items); // Clean up the pending delivery datetime attribute await _fruitBankAttributeService .DeleteGenericAttributeAsync( customer.Id, PendingDeliveryDateTimeKey, store.Id); Console.WriteLine($"[Preorder] Placed #{saved.Id} — customer #{customer.Id}, {items.Count} items, delivery {deliveryDateTime:u}"); return Json(new { success = true, preorderId = saved.Id, message = await L("PlacedSuccessfully") }); } catch (Exception ex) { Console.WriteLine($"[Preorder] PlacePreorder error: {ex.Message}"); return Json(new { success = false, message = $"Hiba: {ex.Message}" }); } } // ── INNER MODELS ────────────────────────────────────────────────────────── public class PlacePreorderRequest { public string? DeliveryDateTime { get; set; } public string? CustomerNote { get; set; } public List Items { get; set; } = new(); } public class PreorderItemRequest { public int ProductId { get; set; } public int Quantity { get; set; } } }