453 lines
21 KiB
C#
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 });
|
|
}
|
|
}
|