Mango.Nop.Plugins/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Controllers/PreorderAdminController.cs

453 lines
21 KiB
C#

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<PreorderStatus, string> 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<PreorderItemStatus, string> 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<IActionResult> 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<IActionResult> 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<PreorderStatus>(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<IActionResult> 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<IActionResult> 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<List<ProductItemRequest>>(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<PreorderItem>();
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<IActionResult> 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<IActionResult> 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<PreorderItem> 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 });
}
}