using FruitBank.Common.Server; using LinqToDB; using Microsoft.AspNetCore.Mvc; using Nop.Core; using Nop.Core.Domain.Catalog; using Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models; using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer; using Nop.Plugin.Misc.FruitBankPlugin.Services; 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 PreorderAvailabilityController : BasePluginController { private readonly IPermissionService _permissionService; private readonly FruitBankDbContext _dbContext; private readonly FruitBankAttributeService _fruitBankAttributeService; private readonly IStoreContext _storeContext; public PreorderAvailabilityController( IPermissionService permissionService, FruitBankDbContext dbContext, FruitBankAttributeService fruitBankAttributeService, IStoreContext storeContext) { _permissionService = permissionService; _dbContext = dbContext; _fruitBankAttributeService = fruitBankAttributeService; _storeContext = storeContext; } // ── INDEX ───────────────────────────────────────────────────────────────── [HttpGet] [Route("Admin/PreorderAvailability")] public async Task Index() { if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) return AccessDeniedView(); return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/PreorderAvailability/Index.cshtml"); } // ── ALL PRODUCTS — DataTables server-side ───────────────────────────────── [HttpPost] [Route("Admin/PreorderAvailability/ProductList")] public async Task ProductList() { 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 globalSearch = Request.Form["search[value]"].FirstOrDefault()?.Trim() ?? ""; var storeId = (await _storeContext.GetCurrentStoreAsync()).Id; // 1. All published products var products = await _dbContext.Products.Table .Where(p => !p.Deleted && p.Published) .OrderBy(p => p.Name) .Select(p => new { p.Id, p.Name, p.Sku }) .ToListAsync(); // 2. All 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 == storeId) .ToListAsync(); var gaEnd = await _dbContext.GenericAttributes.Table .Where(ga => ga.KeyGroup == nameof(Product) && ga.Key == FruitBankConst.PreorderWindowEnd && ga.StoreId == storeId) .ToListAsync(); var startByProduct = gaStart.ToDictionary(g => g.EntityId, g => g.Value); var endByProduct = gaEnd.ToDictionary(g => g.EntityId, g => g.Value); var today = DateTime.UtcNow.Date; // 3. Build rows var rows = products.Select(p => { DateTime.TryParse(startByProduct.GetValueOrDefault(p.Id), out var ws); DateTime.TryParse(endByProduct.GetValueOrDefault(p.Id), out var we); var hasStart = startByProduct.ContainsKey(p.Id); var hasEnd = endByProduct.ContainsKey(p.Id); return new PreorderAvailabilityRow { ProductId = p.Id, ProductName = p.Name, Sku = p.Sku, WindowStart = hasStart ? ws.ToString("yyyy-MM-dd") : null, WindowEnd = hasEnd ? we.ToString("yyyy-MM-dd") : null, IsAvailableToday = hasStart && hasEnd && ws.Date <= today && today <= we.Date }; }).ToList(); int recordsTotal = rows.Count; // 4. Global search if (!string.IsNullOrWhiteSpace(globalSearch)) { rows = rows.Where(r => r.ProductName.Contains(globalSearch, StringComparison.OrdinalIgnoreCase) || (r.Sku?.Contains(globalSearch, StringComparison.OrdinalIgnoreCase) ?? false) ).ToList(); } int recordsFiltered = rows.Count; // 5. Paginate var page = rows.Skip(start).Take(length).ToList(); return Json(new { draw, recordsTotal, recordsFiltered, data = page }); } // ── AVAILABLE TODAY — DataTables server-side ────────────────────────────── [HttpPost] [Route("Admin/PreorderAvailability/AvailableTodayList")] public async Task AvailableTodayList() { 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 storeId = (await _storeContext.GetCurrentStoreAsync()).Id; var today = DateTime.UtcNow.Date; // Reuse same build logic — filter to available today only var products = await _dbContext.Products.Table .Where(p => !p.Deleted && p.Published) .OrderBy(p => p.Name) .Select(p => new { p.Id, p.Name, p.Sku }) .ToListAsync(); var gaStart = await _dbContext.GenericAttributes.Table .Where(ga => ga.KeyGroup == nameof(Product) && ga.Key == FruitBankConst.PreorderWindowStart && ga.StoreId == storeId) .ToListAsync(); var gaEnd = await _dbContext.GenericAttributes.Table .Where(ga => ga.KeyGroup == nameof(Product) && ga.Key == FruitBankConst.PreorderWindowEnd && ga.StoreId == storeId) .ToListAsync(); var startByProduct = gaStart.ToDictionary(g => g.EntityId, g => g.Value); var endByProduct = gaEnd.ToDictionary(g => g.EntityId, g => g.Value); var rows = products .Where(p => { if (!startByProduct.TryGetValue(p.Id, out var sRaw)) return false; if (!endByProduct.TryGetValue(p.Id, out var eRaw)) return false; if (!DateTime.TryParse(sRaw, out var ws)) return false; if (!DateTime.TryParse(eRaw, out var we)) return false; return ws.Date <= today && today <= we.Date; }) .Select(p => { DateTime.TryParse(startByProduct[p.Id], out var ws); DateTime.TryParse(endByProduct[p.Id], out var we); return new PreorderAvailabilityRow { ProductId = p.Id, ProductName = p.Name, Sku = p.Sku, WindowStart = ws.ToString("yyyy-MM-dd"), WindowEnd = we.ToString("yyyy-MM-dd"), IsAvailableToday = true }; }) .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 }); } // ── SAVE WINDOW DATES for a product ─────────────────────────────────────── [HttpPost] [Route("Admin/PreorderAvailability/SaveWindow")] public async Task SaveWindow(int productId, string? windowStart, string? windowEnd) { if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL)) return Json(new { success = false, error = "Access denied" }); try { var storeId = (await _storeContext.GetCurrentStoreAsync()).Id; // WindowStart if (string.IsNullOrWhiteSpace(windowStart)) { await _fruitBankAttributeService .DeleteGenericAttributeAsync(productId, FruitBankConst.PreorderWindowStart, storeId); } else if (DateTime.TryParse(windowStart, out var ws)) { await _fruitBankAttributeService .InsertOrUpdateGenericAttributeAsync( productId, FruitBankConst.PreorderWindowStart, ws.Date, storeId); } else return Json(new { success = false, error = $"Invalid start date: {windowStart}" }); // WindowEnd if (string.IsNullOrWhiteSpace(windowEnd)) { await _fruitBankAttributeService .DeleteGenericAttributeAsync(productId, FruitBankConst.PreorderWindowEnd, storeId); } else if (DateTime.TryParse(windowEnd, out var we)) { await _fruitBankAttributeService .InsertOrUpdateGenericAttributeAsync( productId, FruitBankConst.PreorderWindowEnd, we.Date, storeId); } else return Json(new { success = false, error = $"Invalid end date: {windowEnd}" }); // Return the new availability state var today = DateTime.UtcNow.Date; DateTime.TryParse(windowStart, out var startParsed); DateTime.TryParse(windowEnd, out var endParsed); bool isAvailableToday = !string.IsNullOrWhiteSpace(windowStart) && !string.IsNullOrWhiteSpace(windowEnd) && startParsed.Date <= today && today <= endParsed.Date; return Json(new { success = true, isAvailableToday }); } catch (Exception ex) { return Json(new { success = false, error = ex.Message }); } } }