Rename Preorder domain to PreOrder across codebase

Comprehensively renamed all "Preorder" domain types, services, controllers, models, enums, database contexts, routes, views, JS, localization resources, and documentation to "PreOrder" (PascalCase, capital "O"). Updated all references in backend, frontend, localization, and schema docs for consistency. No business logic changes; this is a naming and normalization refactor.
This commit is contained in:
Loretta 2026-05-30 16:26:51 +02:00
parent 2a8dfeaca9
commit 1468aa651c
42 changed files with 910 additions and 746 deletions

View File

@ -176,7 +176,7 @@
</table>
</div>
<div class="card-footer text-muted small">
<a href="/Admin/Preorders">Összes előrendelés &rarr;</a>
<a href="/Admin/PreOrders">Összes előrendelés &rarr;</a>
</div>
</div>
@ -277,7 +277,7 @@
if (alert.type === 'credit_exceeded')
return '<a href="/Admin/CustomerCredit/Details/' + alert.customerId + '" class="btn btn-xs btn-outline-warning">Hitelkeret</a>';
if (alert.type === 'old_preorder')
return '<a href="/Admin/Preorders" class="btn btn-xs btn-outline-info">Előrendelések</a>';
return '<a href="/Admin/PreOrders" class="btn btn-xs btn-outline-info">Előrendelések</a>';
return '';
}
@ -374,10 +374,10 @@
}
// ── Pending preorders ────────────────────────────────────────────
if (data.pendingPreorders && data.pendingPreorders.length > 0) {
$('#fb-preorders-count').text(data.pendingPreorders.length);
if (data.pendingPreOrders && data.pendingPreOrders.length > 0) {
$('#fb-preorders-count').text(data.pendingPreOrders.length);
var $pb = $('#fb-preorders-body').empty();
data.pendingPreorders.forEach(function (p) {
data.pendingPreOrders.forEach(function (p) {
$pb.append(
'<tr>' +
'<td>' + p.company + '</td>' +
@ -385,7 +385,7 @@
'<td class="text-center">' + p.itemCount + '</td>' +
'<td class="text-center">' + p.fulfilledCount + ' / ' + p.itemCount + '</td>' +
'<td class="text-center">' + preorderStatusBadge(p.status) + '</td>' +
'<td><a href="/Admin/Preorders/Details/' + p.id + '" class="btn btn-xs btn-outline-secondary">Részletek</a></td>' +
'<td><a href="/Admin/PreOrders/Details/' + p.id + '" class="btn btn-xs btn-outline-secondary">Részletek</a></td>' +
'</tr>'
);
});

View File

@ -37,7 +37,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
protected readonly IWorkContext _workContext;
protected readonly AICalculationService _aiCalculationService;
protected readonly FruitBankDbContext _fruitBankDbContext;
protected readonly PreorderDbContext _preorderDbContext;
protected readonly PreOrderDbContext _preorderDbContext;
#endregion
@ -55,7 +55,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
IWorkContext workContext,
AICalculationService aiCalculationService,
FruitBankDbContext fruitBankDbContext,
PreorderDbContext preorderDbContext)
PreOrderDbContext preorderDbContext)
{
_adminAreaSettings = adminAreaSettings;
_commonModelFactory = commonModelFactory;
@ -186,32 +186,32 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
var today = DateTime.Now.Date;
// ── Batch 1: parallel queries ─────────────────────────────────────
// NOTE: PreorderItems is not a LinqToDB [Association] — never use LoadWith on it.
// Preorders and items are loaded separately and joined in memory below.
// NOTE: PreOrderItems is not a LinqToDB [Association] — never use LoadWith on it.
// PreOrders and items are loaded separately and joined in memory below.
var allOrdersTask = _fruitBankDbContext.OrderDtos.GetAll(true).ToListAsync();
var allCreditsTask = _fruitBankDbContext.CustomerCredits.GetAll().ToListAsync();
var allPreordersTask = _preorderDbContext.Preorders.GetAll().ToListAsync();
var allPreOrdersTask = _preorderDbContext.PreOrders.GetAll().ToListAsync();
var unprocessedDocsTask = _fruitBankDbContext.ShippingDocuments.GetAllNotMeasured(true).ToListAsync();
await Task.WhenAll(allOrdersTask, allCreditsTask, allPreordersTask, unprocessedDocsTask);
await Task.WhenAll(allOrdersTask, allCreditsTask, allPreOrdersTask, unprocessedDocsTask);
var allOrders = await allOrdersTask;
var credits = await allCreditsTask;
var unprocessedDocs = await unprocessedDocsTask;
// Filter pending preorders in memory — LinqToDB cannot translate enum comparisons to SQL
var pendingStatuses = new[] { PreorderStatus.Pending, PreorderStatus.PartiallyFulfilled };
var pendingPreorders = (await allPreordersTask)
var pendingStatuses = new[] { PreOrderStatus.Pending, PreOrderStatus.PartiallyFulfilled };
var pendingPreOrders = (await allPreOrdersTask)
.Where(p => pendingStatuses.Contains(p.Status))
.ToList();
// ── Batch 1b: preorder items for pending preorders only ────────────
var pendingPreorderIds = pendingPreorders.Select(p => p.Id).ToList();
var pendingItems = pendingPreorderIds.Any()
? await _preorderDbContext.PreorderItems.GetAll()
.Where(i => pendingPreorderIds.Contains(i.PreorderId))
var pendingPreOrderIds = pendingPreOrders.Select(p => p.Id).ToList();
var pendingItems = pendingPreOrderIds.Any()
? await _preorderDbContext.PreOrderItems.GetAll()
.Where(i => pendingPreOrderIds.Contains(i.PreOrderId))
.ToListAsync()
: new List<PreorderItem>();
: new List<PreOrderItem>();
// ── Today's orders (in-memory filter, same pattern as AICalculationService)
var todaysOrders = allOrders
@ -249,8 +249,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
return $"#{customerId}";
}
// ── Preorder customer names (may not appear in allOrders) ──────────
var preorderCustomerIds = pendingPreorders
// ── PreOrder customer names (may not appear in allOrders) ──────────
var preorderCustomerIds = pendingPreOrders
.Select(p => p.CustomerId).Distinct()
.Where(id => allOrders.All(o => o.CustomerId != id))
.ToList();
@ -267,7 +267,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
preorderCustomerLookup[c.Id] = !string.IsNullOrEmpty(c.Company) ? c.Company : c.Email;
}
string PreorderCustomerName(int customerId)
string PreOrderCustomerName(int customerId)
=> preorderCustomerLookup.TryGetValue(customerId, out var name)
? name
: CustomerName(customerId);
@ -361,15 +361,15 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
// Pending preorders older than 7 days
var sevenDaysAgo = DateTime.UtcNow.AddDays(-7);
foreach (var p in pendingPreorders.Where(p =>
p.Status == PreorderStatus.Pending &&
foreach (var p in pendingPreOrders.Where(p =>
p.Status == PreOrderStatus.Pending &&
p.CreatedOnUtc < sevenDaysAgo))
{
alerts.Add(new
{
type = "old_preorder",
preorderId = p.Id,
company = PreorderCustomerName(p.CustomerId),
company = PreOrderCustomerName(p.CustomerId),
createdAt = p.CreatedOnUtc.ToLocalTime().ToString("yyyy.MM.dd"),
message = "Régi, nyitott előrendelés"
});
@ -404,20 +404,20 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
// ─────────────────────────────────────────────────────────────────
// Section 4: Pending preorders (items joined in memory)
// ─────────────────────────────────────────────────────────────────
var preorderRows = pendingPreorders
var preorderRows = pendingPreOrders
.OrderBy(p => p.CreatedOnUtc)
.Select(p =>
{
var items = pendingItems.Where(i => i.PreorderId == p.Id).ToList();
var items = pendingItems.Where(i => i.PreOrderId == p.Id).ToList();
return new
{
id = p.Id,
customerId = p.CustomerId,
company = PreorderCustomerName(p.CustomerId),
company = PreOrderCustomerName(p.CustomerId),
status = p.Status.ToString(),
createdAt = p.CreatedOnUtc.ToLocalTime().ToString("yyyy.MM.dd"),
itemCount = items.Count,
fulfilledCount = items.Count(i => i.Status == PreorderItemStatus.Fulfilled)
fulfilledCount = items.Count(i => i.Status == PreOrderItemStatus.Fulfilled)
};
})
.ToList();
@ -443,7 +443,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
pipeline,
alerts,
creditStatus = creditRows,
pendingPreorders = preorderRows,
pendingPreOrders = preorderRows,
unprocessedDocs = docRows
});
}

View File

@ -1076,7 +1076,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
[HttpGet]
[CheckPermission(StandardPermission.Catalog.PRODUCTS_VIEW)]
public virtual async Task<IActionResult> PreorderProductSearchAutoComplete(string term)
public virtual async Task<IActionResult> PreOrderProductSearchAutoComplete(string term)
{
if (string.IsNullOrWhiteSpace(term) || term.Length < 2)
return Json(new List<object>());
@ -1088,13 +1088,13 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
// Load preorder window attributes in two batch queries
var gaStart = await _dbContext.GenericAttributes.Table
.Where(ga => ga.KeyGroup == nameof(Product)
&& ga.Key == FruitBankConst.PreorderWindowStart
&& 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.Key == FruitBankConst.PreOrderWindowEnd
&& ga.StoreId == store.Id)
.ToListAsync();

View File

@ -41,7 +41,7 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers
private readonly FileStorageService _fileStorageService;
private readonly FruitBankAttributeService _fruitBankAttributeService;
private readonly IStoreContext _storeContext;
private readonly PreorderConversionService _preorderConversionService;
private readonly PreOrderConversionService _preorderConversionService;
public FileManagerController(
IPermissionService permissionService,
@ -55,7 +55,7 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers
FileStorageService fileStorageService,
FruitBankAttributeService fruitBankAttributeService,
IStoreContext storeContext,
PreorderConversionService preorderConversionService)
PreOrderConversionService preorderConversionService)
{
_permissionService = permissionService;
_aiApiService = aiApiService;
@ -1140,12 +1140,12 @@ namespace Nop.Plugin.Misc.FruitBank.Controllers
try
{
await _preorderConversionService
.ConvertPreordersForProductsAsync(productIdsWithIncoming, shippingDocument.Id);
.ConvertPreOrdersForProductsAsync(productIdsWithIncoming, shippingDocument.Id);
}
catch (Exception convEx)
{
Console.Error.WriteLine(
$"[PreorderConversion] Error during conversion for document #{shippingDocument.Id}: {convEx.Message}");
$"[PreOrderConversion] Error during conversion for document #{shippingDocument.Id}: {convEx.Message}");
}
});
}

View File

@ -16,36 +16,36 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers;
[AuthorizeAdmin]
[Area(AreaNames.ADMIN)]
[AutoValidateAntiforgeryToken]
public class PreorderAdminController : BasePluginController
public class PreOrderAdminController : BasePluginController
{
private readonly IPermissionService _permissionService;
private readonly PreorderDbContext _preorderDbContext;
private readonly PreOrderDbContext _preorderDbContext;
private readonly FruitBankDbContext _dbContext;
private readonly ICustomerService _customerService;
private readonly PreorderConversionService _preorderConversionService;
private readonly PreOrderConversionService _preorderConversionService;
private static readonly Dictionary<PreorderStatus, string> StatusLabels = new()
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" }
{ 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()
private static readonly Dictionary<PreOrderItemStatus, string> ItemStatusLabels = new()
{
{ PreorderItemStatus.Pending, "Függőben" },
{ PreorderItemStatus.Fulfilled, "Teljesítve" },
{ PreorderItemStatus.PartiallyFulfilled, "Részben" },
{ PreorderItemStatus.Dropped, "Ejtve" }
{ PreOrderItemStatus.Pending, "Függőben" },
{ PreOrderItemStatus.Fulfilled, "Teljesítve" },
{ PreOrderItemStatus.PartiallyFulfilled, "Részben" },
{ PreOrderItemStatus.Dropped, "Ejtve" }
};
public PreorderAdminController(
public PreOrderAdminController(
IPermissionService permissionService,
PreorderDbContext preorderDbContext,
PreOrderDbContext preorderDbContext,
FruitBankDbContext dbContext,
ICustomerService customerService,
PreorderConversionService preorderConversionService)
PreOrderConversionService preorderConversionService)
{
_permissionService = permissionService;
_preorderDbContext = preorderDbContext;
@ -57,20 +57,20 @@ public class PreorderAdminController : BasePluginController
// ── LIST PAGE ─────────────────────────────────────────────────────────────
[HttpGet]
[Route("Admin/Preorders")]
[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");
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/PreOrder/List.cshtml");
}
// ── DATATABLES SERVER-SIDE ────────────────────────────────────────────────
[HttpPost]
[Route("Admin/Preorders/PreorderList")]
public async Task<IActionResult> PreorderList()
[Route("Admin/PreOrders/PreOrderList")]
public async Task<IActionResult> PreOrderList()
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return Forbid();
@ -87,11 +87,11 @@ public class PreorderAdminController : BasePluginController
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 preorders = await _preorderDbContext.PreOrders.GetAll(false).ToListAsync();
var allItems = await _preorderDbContext.PreOrderItems.GetAll().ToListAsync();
var itemsByPreorder = allItems
.GroupBy(i => i.PreorderId)
var itemsByPreOrder = allItems
.GroupBy(i => i.PreOrderId)
.ToDictionary(g => g.Key, g => g.ToList());
// 2. Customers — batch
@ -111,7 +111,7 @@ public class PreorderAdminController : BasePluginController
var rows = preorders.Select(p =>
{
customerById.TryGetValue(p.CustomerId, out var c);
var items = itemsByPreorder.TryGetValue(p.Id, out var its) ? its : new();
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);
@ -123,13 +123,13 @@ public class PreorderAdminController : BasePluginController
// otherwise infer from quantities
var effectiveStatus = (int)p.Status != 0
? p.Status
: allFulfilled ? PreorderStatus.Confirmed
: anyFulfilled ? PreorderStatus.PartiallyFulfilled
: PreorderStatus.Pending;
: allFulfilled ? PreOrderStatus.Confirmed
: anyFulfilled ? PreOrderStatus.PartiallyFulfilled
: PreOrderStatus.Pending;
return new PreorderListRow
return new PreOrderListRow
{
PreorderId = p.Id,
PreOrderId = p.Id,
CustomerId = p.CustomerId,
CustomerName = c != null ? $"{c.FirstName} {c.LastName}".Trim() : $"#{p.CustomerId}",
CustomerEmail = c?.Email ?? string.Empty,
@ -146,7 +146,7 @@ public class PreorderAdminController : BasePluginController
int recordsTotal = rows.Count;
// 5. Filter by status
if (!string.IsNullOrWhiteSpace(statusFilter) && Enum.TryParse<PreorderStatus>(statusFilter, out var statusEnum))
if (!string.IsNullOrWhiteSpace(statusFilter) && Enum.TryParse<PreOrderStatus>(statusFilter, out var statusEnum))
rows = rows.Where(r => r.Status == statusEnum).ToList();
// 6. Global search
@ -154,7 +154,7 @@ public class PreorderAdminController : BasePluginController
rows = rows.Where(r =>
r.CustomerName.Contains(globalSearch, StringComparison.OrdinalIgnoreCase) ||
r.CustomerEmail.Contains(globalSearch, StringComparison.OrdinalIgnoreCase) ||
r.PreorderId.ToString().Contains(globalSearch)
r.PreOrderId.ToString().Contains(globalSearch)
).ToList();
int recordsFiltered = rows.Count;
@ -178,17 +178,17 @@ public class PreorderAdminController : BasePluginController
// ── DETAIL PAGE ───────────────────────────────────────────────────────────
[HttpGet]
[Route("Admin/Preorders/Detail/{id:int}")]
[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);
var preorder = await _preorderDbContext.PreOrders.GetByIdAsync(id, loadRelations: false);
if (preorder == null) return NotFound();
var items = await _preorderDbContext.PreorderItems
.GetAllByPreorderIdAsync(id)
var items = await _preorderDbContext.PreOrderItems
.GetAllByPreOrderIdAsync(id)
.ToListAsync();
var customer = await _customerService.GetCustomerByIdAsync(preorder.CustomerId);
@ -204,9 +204,9 @@ public class PreorderAdminController : BasePluginController
// Use preorder.OrderId directly — stored on the entity at conversion time
int? linkedOrderId = preorder.OrderId;
var model = new PreorderDetailModel
var model = new PreOrderDetailModel
{
PreorderId = preorder.Id,
PreOrderId = preorder.Id,
CustomerId = preorder.CustomerId,
CustomerName = customer != null ? $"{customer.FirstName} {customer.LastName}".Trim() : $"#{preorder.CustomerId}",
CustomerEmail = customer?.Email ?? string.Empty,
@ -222,15 +222,15 @@ public class PreorderAdminController : BasePluginController
// Derive item status from quantities — enum reads unreliable in LinqToDB
var derivedStatus = i.FulfilledQuantity == 0
? PreorderItemStatus.Pending
? PreOrderItemStatus.Pending
: i.FulfilledQuantity >= i.RequestedQuantity
? PreorderItemStatus.Fulfilled
: PreorderItemStatus.PartiallyFulfilled;
? 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
return new PreOrderDetailItemRow
{
ItemId = i.Id,
ProductId = i.ProductId,
@ -245,14 +245,14 @@ public class PreorderAdminController : BasePluginController
}).ToList()
};
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Preorder/Detail.cshtml", model);
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(
[Route("Admin/PreOrders/CreatePreOrder")]
public async Task<IActionResult> CreatePreOrder(
int customerId,
string deliveryDateTime,
string? customerNote,
@ -288,7 +288,7 @@ public class PreorderAdminController : BasePluginController
.Select(g => g.StoreId).FirstOrDefaultAsync();
storeId = gaStore > 0 ? gaStore : 1;
var preorder = new Preorder
var preorder = new PreOrder
{
CustomerId = customerId,
StoreId = storeId,
@ -296,13 +296,13 @@ public class PreorderAdminController : BasePluginController
CustomerNote = customerNote?.Trim()
};
var items = new List<PreorderItem>();
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
items.Add(new PreOrderItem
{
ProductId = pi.id,
RequestedQuantity = pi.quantity,
@ -313,18 +313,18 @@ public class PreorderAdminController : BasePluginController
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);
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.
// same inline conversion as the customer-facing PlacePreOrder endpoint.
var productIds = items.Select(i => i.ProductId).Distinct().ToList();
await _preorderConversionService.ConvertPreordersForProductsAsync(productIds, 0);
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);
var refreshed = await _preorderDbContext.PreOrders.GetByIdAsync(saved.Id);
return Json(new { success = true, preorderId = saved.Id, orderId = refreshed?.OrderId });
}
@ -347,27 +347,27 @@ public class PreorderAdminController : BasePluginController
// ── CANCEL ───────────────────────────────────────────────────────────
[HttpPost]
[Route("Admin/Preorders/Cancel/{id:int}")]
[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);
var preorder = await _preorderDbContext.PreOrders.GetByIdAsync(id);
if (preorder == null)
return Json(new { success = false, error = "Preorder not found" });
return Json(new { success = false, error = "PreOrder not found" });
if (preorder.Status != PreorderStatus.Pending)
if (preorder.Status != PreOrderStatus.Pending)
return Json(new { success = false, error = "Only pending preorders can be cancelled" });
await _preorderDbContext.CancelPreorderAsync(id);
await _preorderDbContext.CancelPreOrderAsync(id);
return Json(new { success = true });
}
// ── DEMAND LIST ───────────────────────────────────────────────────────────
[HttpPost]
[Route("Admin/Preorders/DemandList")]
[Route("Admin/PreOrders/DemandList")]
public async Task<IActionResult> DemandList(bool openOnly = true)
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
@ -380,24 +380,24 @@ public class PreorderAdminController : BasePluginController
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();
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;
IEnumerable<PreOrderItem> items = allItems;
if (openOnly)
{
// Open preorders: those where at least one item still needs fulfillment
var openPreorderIds = allPreorders
var openPreOrderIds = allPreOrders
.Where(p => allItems
.Where(i => i.PreorderId == p.Id)
.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));
items = allItems.Where(i => openPreOrderIds.Contains(i.PreOrderId));
}
// Group by product
@ -409,7 +409,7 @@ public class PreorderAdminController : BasePluginController
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(),
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
@ -429,7 +429,7 @@ public class PreorderAdminController : BasePluginController
var rows = grouped.Select(g =>
{
productById.TryGetValue(g.ProductId, out var dto);
return new PreorderDemandRow
return new PreOrderDemandRow
{
ProductId = g.ProductId,
ProductName = dto?.Name ?? $"Product #{g.ProductId}",
@ -438,7 +438,7 @@ public class PreorderAdminController : BasePluginController
TotalRequested = g.TotalRequested,
TotalFulfilled = g.TotalFulfilled,
TotalUnfulfilled = g.TotalUnfulfilled,
PreorderCount = g.PreorderCount,
PreOrderCount = g.PreOrderCount,
AvgUnitPrice = Math.Round(g.AvgUnitPrice, 0)
};
}).ToList();

View File

@ -16,14 +16,14 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers;
[AuthorizeAdmin]
[Area(AreaNames.ADMIN)]
[AutoValidateAntiforgeryToken]
public class PreorderAvailabilityController : BasePluginController
public class PreOrderAvailabilityController : BasePluginController
{
private readonly IPermissionService _permissionService;
private readonly FruitBankDbContext _dbContext;
private readonly FruitBankAttributeService _fruitBankAttributeService;
private readonly IStoreContext _storeContext;
public PreorderAvailabilityController(
public PreOrderAvailabilityController(
IPermissionService permissionService,
FruitBankDbContext dbContext,
FruitBankAttributeService fruitBankAttributeService,
@ -38,19 +38,19 @@ public class PreorderAvailabilityController : BasePluginController
// ── INDEX ─────────────────────────────────────────────────────────────────
[HttpGet]
[Route("Admin/PreorderAvailability")]
[Route("Admin/PreOrderAvailability")]
public async Task<IActionResult> Index()
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
return AccessDeniedView();
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/PreorderAvailability/Index.cshtml");
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/PreOrderAvailability/Index.cshtml");
}
// ── ALL PRODUCTS — DataTables server-side ─────────────────────────────────
[HttpPost]
[Route("Admin/PreorderAvailability/ProductList")]
[Route("Admin/PreOrderAvailability/ProductList")]
public async Task<IActionResult> ProductList()
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
@ -74,13 +74,13 @@ public class PreorderAvailabilityController : BasePluginController
// 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.Key == FruitBankConst.PreOrderWindowStart
&& ga.StoreId == storeId)
.ToListAsync();
var gaEnd = await _dbContext.GenericAttributes.Table
.Where(ga => ga.KeyGroup == nameof(Product)
&& ga.Key == FruitBankConst.PreorderWindowEnd
&& ga.Key == FruitBankConst.PreOrderWindowEnd
&& ga.StoreId == storeId)
.ToListAsync();
@ -98,7 +98,7 @@ public class PreorderAvailabilityController : BasePluginController
var hasStart = startByProduct.ContainsKey(p.Id);
var hasEnd = endByProduct.ContainsKey(p.Id);
return new PreorderAvailabilityRow
return new PreOrderAvailabilityRow
{
ProductId = p.Id,
ProductName = p.Name,
@ -131,7 +131,7 @@ public class PreorderAvailabilityController : BasePluginController
// ── AVAILABLE TODAY — DataTables server-side ──────────────────────────────
[HttpPost]
[Route("Admin/PreorderAvailability/AvailableTodayList")]
[Route("Admin/PreOrderAvailability/AvailableTodayList")]
public async Task<IActionResult> AvailableTodayList()
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
@ -153,13 +153,13 @@ public class PreorderAvailabilityController : BasePluginController
var gaStart = await _dbContext.GenericAttributes.Table
.Where(ga => ga.KeyGroup == nameof(Product)
&& ga.Key == FruitBankConst.PreorderWindowStart
&& 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.Key == FruitBankConst.PreOrderWindowEnd
&& ga.StoreId == storeId)
.ToListAsync();
@ -179,7 +179,7 @@ public class PreorderAvailabilityController : BasePluginController
{
DateTime.TryParse(startByProduct[p.Id], out var ws);
DateTime.TryParse(endByProduct[p.Id], out var we);
return new PreorderAvailabilityRow
return new PreOrderAvailabilityRow
{
ProductId = p.Id,
ProductName = p.Name,
@ -201,7 +201,7 @@ public class PreorderAvailabilityController : BasePluginController
// ── SAVE WINDOW DATES for a product ───────────────────────────────────────
[HttpPost]
[Route("Admin/PreorderAvailability/SaveWindow")]
[Route("Admin/PreOrderAvailability/SaveWindow")]
public async Task<IActionResult> SaveWindow(int productId, string? windowStart, string? windowEnd)
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
@ -215,13 +215,13 @@ public class PreorderAvailabilityController : BasePluginController
if (string.IsNullOrWhiteSpace(windowStart))
{
await _fruitBankAttributeService
.DeleteGenericAttributeAsync<Product>(productId, FruitBankConst.PreorderWindowStart, storeId);
.DeleteGenericAttributeAsync<Product>(productId, FruitBankConst.PreOrderWindowStart, storeId);
}
else if (DateTime.TryParse(windowStart, out var ws))
{
await _fruitBankAttributeService
.InsertOrUpdateGenericAttributeAsync<Product, DateTime>(
productId, FruitBankConst.PreorderWindowStart, ws.Date, storeId);
productId, FruitBankConst.PreOrderWindowStart, ws.Date, storeId);
}
else return Json(new { success = false, error = $"Invalid start date: {windowStart}" });
@ -229,13 +229,13 @@ public class PreorderAvailabilityController : BasePluginController
if (string.IsNullOrWhiteSpace(windowEnd))
{
await _fruitBankAttributeService
.DeleteGenericAttributeAsync<Product>(productId, FruitBankConst.PreorderWindowEnd, storeId);
.DeleteGenericAttributeAsync<Product>(productId, FruitBankConst.PreOrderWindowEnd, storeId);
}
else if (DateTime.TryParse(windowEnd, out var we))
{
await _fruitBankAttributeService
.InsertOrUpdateGenericAttributeAsync<Product, DateTime>(
productId, FruitBankConst.PreorderWindowEnd, we.Date, storeId);
productId, FruitBankConst.PreOrderWindowEnd, we.Date, storeId);
}
else return Json(new { success = false, error = $"Invalid end date: {windowEnd}" });

View File

@ -2,37 +2,37 @@ using FruitBank.Common.Enums;
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
public class PreorderListRow
public class PreOrderListRow
{
public int PreorderId { get; set; }
public int PreOrderId { get; set; }
public int CustomerId { get; set; }
public string CustomerName { get; set; } = string.Empty;
public string CustomerEmail { get; set; } = string.Empty;
public string DateOfReceipt { get; set; } = string.Empty; // formatted
public string CreatedOnUtc { get; set; } = string.Empty; // formatted
public PreorderStatus Status { get; set; }
public PreOrderStatus Status { get; set; }
public string StatusLabel { get; set; } = string.Empty;
public int ItemCount { get; set; }
public int FulfilledCount { get; set; }
public int? OrderId { get; set; } // linked real order, if created
}
public class PreorderDetailModel
public class PreOrderDetailModel
{
public int PreorderId { get; set; }
public int PreOrderId { get; set; }
public int CustomerId { get; set; }
public string CustomerName { get; set; } = string.Empty;
public string CustomerEmail { get; set; } = string.Empty;
public string DateOfReceipt { get; set; } = string.Empty;
public string CreatedOnUtc { get; set; } = string.Empty;
public string UpdatedOnUtc { get; set; } = string.Empty;
public PreorderStatus Status { get; set; }
public PreOrderStatus Status { get; set; }
public string? CustomerNote { get; set; }
public int? OrderId { get; set; }
public List<PreorderDetailItemRow> Items { get; set; } = new();
public List<PreOrderDetailItemRow> Items { get; set; } = new();
}
public class PreorderDetailItemRow
public class PreOrderDetailItemRow
{
public int ItemId { get; set; }
public int ProductId { get; set; }
@ -41,11 +41,11 @@ public class PreorderDetailItemRow
public int RequestedQuantity { get; set; }
public int FulfilledQuantity { get; set; }
public decimal UnitPriceInclTax { get; set; }
public PreorderItemStatus Status { get; set; }
public PreOrderItemStatus Status { get; set; }
public string StatusLabel { get; set; } = string.Empty;
}
public class PreorderDemandRow
public class PreOrderDemandRow
{
public int ProductId { get; set; }
public string ProductName { get; set; } = string.Empty;
@ -54,6 +54,6 @@ public class PreorderDemandRow
public int TotalRequested { get; set; } // sum of RequestedQuantity
public int TotalFulfilled { get; set; } // sum of FulfilledQuantity
public int TotalUnfulfilled { get; set; } // TotalRequested - TotalFulfilled
public int PreorderCount { get; set; } // distinct preorders containing this product
public int PreOrderCount { get; set; } // distinct preorders containing this product
public decimal AvgUnitPrice { get; set; } // average snapshot price
}

View File

@ -1,6 +1,6 @@
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models;
public class PreorderAvailabilityRow
public class PreOrderAvailabilityRow
{
public int ProductId { get; set; }
public string ProductName { get; set; } = string.Empty;

View File

@ -1,23 +1,23 @@
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.PreorderDetailModel
@model Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models.PreOrderDetailModel
@using FruitBank.Common.Enums
@{
ViewBag.PageTitle = $"Előrendelés #{Model.PreorderId}";
ViewBag.PageTitle = $"Előrendelés #{Model.PreOrderId}";
Layout = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/_FruitBankAdminLayout.cshtml";
var statusClass = Model.Status switch
{
PreorderStatus.Confirmed => "po-status-confirmed",
PreorderStatus.PartiallyFulfilled => "po-status-partial",
PreorderStatus.Cancelled => "po-status-cancelled",
PreOrderStatus.Confirmed => "po-status-confirmed",
PreOrderStatus.PartiallyFulfilled => "po-status-partial",
PreOrderStatus.Cancelled => "po-status-cancelled",
_ => "po-status-pending"
};
var statusLabel = Model.Status switch
{
PreorderStatus.Confirmed => "Megerősítve",
PreorderStatus.PartiallyFulfilled => "Részben teljesítve",
PreorderStatus.Cancelled => "Törölve",
PreOrderStatus.Confirmed => "Megerősítve",
PreOrderStatus.PartiallyFulfilled => "Részben teljesítve",
PreOrderStatus.Cancelled => "Törölve",
_ => "Függőben"
};
}
@ -46,14 +46,14 @@
</style>
<!-- Back link -->
<a href="/Admin/Preorders" class="btn btn-default btn-sm mb-3">
<a href="/Admin/PreOrders" class="btn btn-default btn-sm mb-3">
<i class="fas fa-arrow-left"></i> Vissza a listához
</a>
<div class="content-header clearfix">
<h1 class="float-left">
<i class="fas fa-calendar-plus" style="color:#2d7a3a;"></i>
Előrendelés <strong>#@Model.PreorderId</strong>
Előrendelés <strong>#@Model.PreOrderId</strong>
<span class="@statusClass ml-2">@statusLabel</span>
</h1>
<div class="float-right">
@ -63,7 +63,7 @@
<i class="fas fa-external-link-alt"></i> Rendelés #@Model.OrderId
</a>
}
@if (Model.Status == PreorderStatus.Pending)
@if (Model.Status == PreOrderStatus.Pending)
{
<button id="cancelBtn" class="btn btn-danger btn-sm ml-2">
<i class="fas fa-times"></i> Visszavonás
@ -112,10 +112,10 @@
<div class="card-header">
<strong>Tételek (@Model.Items.Count)</strong>
@{
var fulfilled = Model.Items.Count(i => i.Status == PreorderItemStatus.Fulfilled);
var partial = Model.Items.Count(i => i.Status == PreorderItemStatus.PartiallyFulfilled);
var dropped = Model.Items.Count(i => i.Status == PreorderItemStatus.Dropped);
var pending = Model.Items.Count(i => i.Status == PreorderItemStatus.Pending);
var fulfilled = Model.Items.Count(i => i.Status == PreOrderItemStatus.Fulfilled);
var partial = Model.Items.Count(i => i.Status == PreOrderItemStatus.PartiallyFulfilled);
var dropped = Model.Items.Count(i => i.Status == PreOrderItemStatus.Dropped);
var pending = Model.Items.Count(i => i.Status == PreOrderItemStatus.Pending);
}
<span class="ml-2 text-muted" style="font-size:13px;">
@if (fulfilled > 0) { <span class="badge badge-success">@fulfilled teljesítve</span> }
@ -142,9 +142,9 @@
{
var rowClass = item.Status switch
{
PreorderItemStatus.Fulfilled => "item-fulfilled",
PreorderItemStatus.PartiallyFulfilled => "item-partial",
PreorderItemStatus.Dropped => "item-dropped",
PreOrderItemStatus.Fulfilled => "item-fulfilled",
PreOrderItemStatus.PartiallyFulfilled => "item-partial",
PreOrderItemStatus.Dropped => "item-dropped",
_ => "item-pending"
};
var pct = item.RequestedQuantity > 0
@ -188,7 +188,7 @@
</tbody>
@{
var totalEstimated = Model.Items
.Where(i => !i.IsMeasurable && (i.Status == PreorderItemStatus.Fulfilled || i.Status == PreorderItemStatus.PartiallyFulfilled))
.Where(i => !i.IsMeasurable && (i.Status == PreOrderItemStatus.Fulfilled || i.Status == PreOrderItemStatus.PartiallyFulfilled))
.Sum(i => i.UnitPriceInclTax * i.FulfilledQuantity);
}
<tfoot>
@ -205,19 +205,19 @@
</div>
</section>
@if (Model.Status == PreorderStatus.Pending)
@if (Model.Status == PreOrderStatus.Pending)
{
<script>
$(function () {
$('#cancelBtn').click(function () {
if (!confirm('Biztosan visszavonod ezt az előrendelést? Ez a művelet nem visszafordítható.')) return;
$.ajax({
url : '/Admin/Preorders/Cancel/@Model.PreorderId',
url : '/Admin/PreOrders/Cancel/@Model.PreOrderId',
type : 'POST',
data : { __RequestVerificationToken: $('input[name="__RequestVerificationToken"]').val() },
success: function (res) {
if (res.success) {
location.href = '/Admin/Preorders';
location.href = '/Admin/PreOrders';
} else {
alert('Hiba: ' + (res.error || 'Ismeretlen hiba'));
}

View File

@ -1,6 +1,6 @@
@{
ViewBag.PageTitle = "Előrendelések";
NopHtml.SetActiveMenuItemSystemName("Preorders.List");
NopHtml.SetActiveMenuItemSystemName("PreOrders.List");
Layout = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/_FruitBankAdminLayout.cshtml";
}
@ -194,7 +194,7 @@ $(function () {
return diff >= 0 && diff <= 4;
}
// ── Preorder list grid ──────────────────────────────────────────────────
// ── PreOrder list grid ──────────────────────────────────────────────────
var poTable = $('#po-grid').DataTable({
serverSide: true, processing: true, pageLength: 25,
lengthMenu: [[25,50,100],[25,50,100]], order: [[3,'desc']],
@ -202,13 +202,13 @@ $(function () {
info:'_START__END_ / _TOTAL_ előrendelés', infoEmpty:'0 előrendelés',
infoFiltered:'(szűrve _MAX_-ból)', emptyTable:'Nincs előrendelés', zeroRecords:'Nincs találat',
paginate:{first:'««',previous:'«',next:'»',last:'»»'} },
ajax: { url:'/Admin/Preorders/PreorderList', type:'POST',
ajax: { url:'/Admin/PreOrders/PreOrderList', type:'POST',
data: function(d){ d.__RequestVerificationToken=_token; d.statusFilter=activeStatus; } },
createdRow: function(row, data) {
if (isUrgentRow(data)) $(row).addClass('po-urgent-row');
},
columns: [
{ data:'PreorderId', name:'PreorderId', render:function(d){ return '<strong>#'+d+'</strong>'; } },
{ data:'PreOrderId', name:'PreOrderId', render:function(d){ return '<strong>#'+d+'</strong>'; } },
{ data:'CustomerName', name:'CustomerName', render:function(d,t,row){ return '<div>'+d+'</div><small class="text-muted">'+row.CustomerEmail+'</small>'; } },
{ data:'DateOfReceipt',name:'DateOfReceipt',render:function(d,t,row){
var icon = '<i class="fas fa-calendar-day text-muted mr-1"></i>';
@ -218,8 +218,8 @@ $(function () {
{ data:'CreatedOnUtc', name:'CreatedOnUtc', render:function(d){ return '<small>'+d+'</small>'; } },
{ data:'Status', name:'Status', orderable:false, render:function(d,t,row){ return statusBadge(row); } },
{ data:'ItemCount', orderable:false, className:'text-center', render:function(d,t,row){ return itemProgress(row); } },
{ data:'PreorderId', orderable:false, searchable:false, className:'text-center', width:'60px',
render:function(d){ return '<a href="/Admin/Preorders/Detail/'+d+'" class="btn btn-xs btn-default" title="Részletek"><i class="fas fa-eye"></i></a>'; } }
{ data:'PreOrderId', orderable:false, searchable:false, className:'text-center', width:'60px',
render:function(d){ return '<a href="/Admin/PreOrders/Detail/'+d+'" class="btn btn-xs btn-default" title="Részletek"><i class="fas fa-eye"></i></a>'; } }
]
});
$(document).on('click','.po-filter',function(){
@ -235,7 +235,7 @@ $(function () {
info:'_START__END_ / _TOTAL_ termék', infoEmpty:'Nincs adat',
infoFiltered:'(szűrve _MAX_-ból)', emptyTable:'Nincs előrendelési igény', zeroRecords:'Nincs találat',
paginate:{first:'««',previous:'«',next:'»',last:'»»'} },
ajax:{ url:'/Admin/Preorders/DemandList', type:'POST',
ajax:{ url:'/Admin/PreOrders/DemandList', type:'POST',
data:function(d){ d.__RequestVerificationToken=_token; d.openOnly=demandOpenOnly?'true':'false'; },
dataSrc:function(json){
var n=(json.data||[]).filter(function(r){return r.TotalUnfulfilled>0;}).length;
@ -253,7 +253,7 @@ $(function () {
var cls=pct===100?'bg-success':pct>0?'bg-warning':'bg-secondary';
return '<div>'+fmtQty(d)+'</div><div class="progress mt-1" style="height:4px;"><div class="progress-bar '+cls+'" style="width:'+pct+'%"></div></div>'; } },
{ data:'TotalUnfulfilled',orderable:false, className:'text-center', render:function(d){ return unfulfilledCell(d); } },
{ data:'PreorderCount', orderable:false, className:'text-center', render:function(d){ return '<span class="badge badge-secondary">'+d+'</span>'; } },
{ data:'PreOrderCount', orderable:false, className:'text-center', render:function(d){ return '<span class="badge badge-secondary">'+d+'</span>'; } },
{ data:'AvgUnitPrice', orderable:false, className:'text-right', render:function(d){ return fmtPrice(d); } }
]
});
@ -270,7 +270,7 @@ $(function () {
demandTable.ajax.reload();
});
// ── Create Order / Preorder Modal (mode-aware) ──────────────────────────
// ── Create Order / PreOrder Modal (mode-aware) ──────────────────────────
var cpProducts = [];
var cpMode = null;
var CP_CUTOFF = 4; // ≤4 days → order, >4 days → preorder
@ -332,7 +332,7 @@ $(function () {
if (!cpMode){ resp([]); return; }
$.get(cpMode==='order'
?'/Admin/CustomOrder/ProductSearchAutoComplete'
:'/Admin/CustomOrder/PreorderProductSearchAutoComplete',
:'/Admin/CustomOrder/PreOrderProductSearchAutoComplete',
{term:req.term}, resp);
},
select:function(e,ui){ addCpProduct(ui.item); $('#cp-product-search').val(''); return false; }
@ -402,7 +402,7 @@ $(function () {
}
});
} else {
$.ajax({url:'/Admin/Preorders/CreatePreorder', type:'POST',
$.ajax({url:'/Admin/PreOrders/CreatePreOrder', type:'POST',
data:{ customerId:$('#cp-customer-id').val(), deliveryDateTime:$('#cp-delivery').val(),
customerNote:$('#cp-note').val().trim(), productsJson:$('#cp-products-json').val(),
__RequestVerificationToken:_token },

View File

@ -1,6 +1,6 @@
@{
ViewBag.PageTitle = "Előrendelés — termékelérhetőség";
NopHtml.SetActiveMenuItemSystemName("PreorderAvailability");
NopHtml.SetActiveMenuItemSystemName("PreOrderAvailability");
Layout = "~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/_FruitBankAdminLayout.cshtml";
}
@ -149,7 +149,7 @@ $(function () {
zeroRecords : 'Nincs találat'
},
ajax: {
url : '/Admin/PreorderAvailability/ProductList',
url : '/Admin/PreOrderAvailability/ProductList',
type: 'POST',
data: function (d) { d.__RequestVerificationToken = _token; }
},
@ -197,7 +197,7 @@ $(function () {
else rowData.WindowEnd = newVal || null;
$.ajax({
url : '/Admin/PreorderAvailability/SaveWindow',
url : '/Admin/PreOrderAvailability/SaveWindow',
type : 'POST',
data : {
__RequestVerificationToken : _token,
@ -243,7 +243,7 @@ $(function () {
zeroRecords : 'Nincs találat'
},
ajax: {
url : '/Admin/PreorderAvailability/AvailableTodayList',
url : '/Admin/PreOrderAvailability/AvailableTodayList',
type: 'POST',
data: function (d) { d.__RequestVerificationToken = _token; },
dataSrc: function (json) {

View File

@ -3,10 +3,10 @@ using Nop.Web.Framework.Components;
namespace Nop.Plugin.Misc.FruitBankPlugin.Components;
public class CustomerPreorderNavViewComponent : NopViewComponent
public class CustomerPreOrderNavViewComponent : NopViewComponent
{
public IViewComponentResult Invoke(string widgetZone, object additionalData)
{
return View("~/Plugins/Misc.FruitBankPlugin/Views/CustomerPreorder/NavItem.cshtml");
return View("~/Plugins/Misc.FruitBankPlugin/Views/CustomerPreOrder/NavItem.cshtml");
}
}

View File

@ -5,20 +5,21 @@ using Nop.Core;
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
using Nop.Services.Customers;
using Nop.Web.Framework.Controllers;
using static Nop.Plugin.Misc.FruitBankPlugin.Controllers.CustomerPreOrderController;
namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers;
public class CustomerPreorderController : BasePluginController
public class CustomerPreOrderController : BasePluginController
{
private readonly IWorkContext _workContext;
private readonly ICustomerService _customerService;
private readonly PreorderDbContext _preorderDbContext;
private readonly PreOrderDbContext _preorderDbContext;
private readonly FruitBankDbContext _dbContext;
public CustomerPreorderController(
public CustomerPreOrderController(
IWorkContext workContext,
ICustomerService customerService,
PreorderDbContext preorderDbContext,
PreOrderDbContext preorderDbContext,
FruitBankDbContext dbContext)
{
_workContext = workContext;
@ -35,13 +36,13 @@ public class CustomerPreorderController : BasePluginController
return Challenge();
// Load this customer's preorders, newest first
var preorders = await _preorderDbContext.Preorders
var preorders = await _preorderDbContext.PreOrders
.GetAllByCustomerIdAsync(customer.Id, false)
.OrderByDescending(p => p.CreatedOnUtc)
.ToListAsync();
var allItems = await _preorderDbContext.PreorderItems.GetAll()
.Where(i => preorders.Select(p => p.Id).Contains(i.PreorderId))
var allItems = await _preorderDbContext.PreOrderItems.GetAll()
.Where(i => preorders.Select(p => p.Id).Contains(i.PreOrderId))
.ToListAsync();
// Resolve product names
@ -54,7 +55,7 @@ public class CustomerPreorderController : BasePluginController
var rows = preorders.Select(p =>
{
var items = allItems.Where(i => i.PreorderId == p.Id).ToList();
var items = allItems.Where(i => i.PreOrderId == p.Id).ToList();
// Derive status from quantities (enum reads unreliable in LinqToDB)
var allFulfilled = items.Any() && items.All(i => i.FulfilledQuantity >= i.RequestedQuantity);
@ -63,13 +64,13 @@ public class CustomerPreorderController : BasePluginController
i.RequestedQuantity > 0);
var effectiveStatus = (int)p.Status != 0 ? p.Status
: allFulfilled ? PreorderStatus.Confirmed
: anyFulfilled ? PreorderStatus.PartiallyFulfilled
: PreorderStatus.Pending;
: allFulfilled ? PreOrderStatus.Confirmed
: anyFulfilled ? PreOrderStatus.PartiallyFulfilled
: PreOrderStatus.Pending;
return new CustomerPreorderRow
return new CustomerPreOrderRow
{
PreorderId = p.Id,
PreOrderId = p.Id,
OrderId = p.OrderId,
DateOfReceipt = p.DateOfReceipt,
CreatedOnUtc = p.CreatedOnUtc,
@ -78,7 +79,7 @@ public class CustomerPreorderController : BasePluginController
Items = items.Select(i =>
{
productById.TryGetValue(i.ProductId, out var dto);
return new CustomerPreorderItemRow
return new CustomerPreOrderItemRow
{
ProductName = dto?.Name ?? $"Termék #{i.ProductId}",
IsMeasurable = dto?.IsMeasurable ?? false,
@ -86,38 +87,38 @@ public class CustomerPreorderController : BasePluginController
FulfilledQuantity = i.FulfilledQuantity,
UnitPriceInclTax = i.UnitPriceInclTax,
Status = i.FulfilledQuantity == 0
? PreorderItemStatus.Pending
? PreOrderItemStatus.Pending
: i.FulfilledQuantity >= i.RequestedQuantity
? PreorderItemStatus.Fulfilled
: PreorderItemStatus.PartiallyFulfilled
? PreOrderItemStatus.Fulfilled
: PreOrderItemStatus.PartiallyFulfilled
};
}).ToList()
};
}).ToList();
return View("~/Plugins/Misc.FruitBankPlugin/Views/CustomerPreorder/List.cshtml", rows);
return View("~/Plugins/Misc.FruitBankPlugin/Views/CustomerPreOrder/List.cshtml", rows);
}
// ── Inner models ──────────────────────────────────────────────────────────
public class CustomerPreorderRow
public class CustomerPreOrderRow
{
public int PreorderId { get; set; }
public int PreOrderId { get; set; }
public int? OrderId { get; set; }
public DateTime DateOfReceipt { get; set; }
public DateTime CreatedOnUtc { get; set; }
public PreorderStatus Status { get; set; }
public PreOrderStatus Status { get; set; }
public string? CustomerNote { get; set; }
public List<CustomerPreorderItemRow> Items { get; set; } = new();
public List<CustomerPreOrderItemRow> Items { get; set; } = new();
}
public class CustomerPreorderItemRow
public class CustomerPreOrderItemRow
{
public string ProductName { get; set; } = string.Empty;
public bool IsMeasurable { get; set; }
public int RequestedQuantity { get; set; }
public int FulfilledQuantity { get; set; }
public decimal UnitPriceInclTax { get; set; }
public PreorderItemStatus Status { get; set; }
public PreOrderItemStatus Status { get; set; }
}
}

View File

@ -39,7 +39,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
ICustomerService customerService,
ICustomerRegistrationService customerRegistrationService,
ILocalizationService localizationService,
PreorderConversionService preorderConversionService,
PreOrderConversionService preorderConversionService,
IEnumerable<IAcLogWriterBase> logWriters)
: BasePluginController, IFruitBankDataControllerServer
{
@ -187,7 +187,14 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
public async Task<CargoTruck> GetCargoTruckById(int id)
{
_logger.Detail($"GetCargoTruckById invoked; id: {id}");
return await ctx.CargoTrucks.GetByIdAsync(id);
return await ctx.CargoTrucks.GetByIdAsync(id, true);
}
[SignalR(SignalRTags.GetCargoTrucksByCargoPartnerId)]
public async Task<List<CargoTruck>> GetCargoTrucksByCargoPartnerId(int cargoPartnerId)
{
_logger.Detail($"GetCargoTrucksByCargoPartnerId invoked; cargoPartnerId: {cargoPartnerId}");
return await ctx.CargoTrucks.GetByCargoPartnerId(cargoPartnerId, true).ToListAsync();
}
[SignalR(SignalRTags.AddCargoTruck)]
@ -198,7 +205,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
_logger.Detail($"AddCargoTruck invoked; id: {cargoTruck.Id}");
await ctx.CargoTrucks.InsertAsync(cargoTruck);
return await ctx.CargoTrucks.GetByIdAsync(cargoTruck.Id);
return await ctx.CargoTrucks.GetByIdAsync(cargoTruck.Id, true);
}
[SignalR(SignalRTags.UpdateCargoTruck)]
@ -209,7 +216,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
_logger.Detail($"UpdateCargoTruck invoked; id: {cargoTruck.Id}");
await ctx.CargoTrucks.UpdateAsync(cargoTruck);
return await ctx.CargoTrucks.GetByIdAsync(cargoTruck.Id);
return await ctx.CargoTrucks.GetByIdAsync(cargoTruck.Id, true);
}
[SignalR(SignalRTags.GetShippings)]
@ -360,7 +367,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers
// (EventConsumer also fires this, double-call is idempotent)
if (shippingItem.QuantityOnDocument > oldItem.QuantityOnDocument)
_ = Task.Run(async () => await preorderConversionService
.ConvertPreordersForProductsAsync(
.ConvertPreOrdersForProductsAsync(
new List<int> { shippingItem.ProductId.Value },
shippingItem.ShippingDocumentId));
}

View File

@ -28,14 +28,14 @@ public class OrderController : BasePluginController
private readonly ICustomerService _customerService;
private readonly ILocalizationService _localizationService;
private readonly FruitBankDbContext _dbContext;
private readonly PreorderDbContext _preorderDbContext;
private readonly PreOrderDbContext _preorderDbContext;
private readonly FruitBankAttributeService _fruitBankAttributeService;
private readonly CustomPriceCalculationService _customPriceCalculationService;
private readonly IShoppingCartService _shoppingCartService;
private readonly IProductService _productService;
private readonly OpenAIApiService _aiApiService;
private readonly CerebrasAPIService _cerebrasApiService;
private readonly PreorderConversionService _preorderConversionService;
private readonly PreOrderConversionService _preorderConversionService;
private const string PendingDeliveryKey = "OrderFlowPendingDeliveryDateTime";
@ -45,14 +45,14 @@ public class OrderController : BasePluginController
ICustomerService customerService,
ILocalizationService localizationService,
FruitBankDbContext dbContext,
PreorderDbContext preorderDbContext,
PreOrderDbContext preorderDbContext,
FruitBankAttributeService fruitBankAttributeService,
IPriceCalculationService priceCalculationService,
IShoppingCartService shoppingCartService,
IProductService productService,
OpenAIApiService aiApiService,
CerebrasAPIService cerebrasApiService,
PreorderConversionService preorderConversionService)
PreOrderConversionService preorderConversionService)
{
_workContext = workContext;
_storeContext = storeContext;
@ -105,7 +105,7 @@ public class OrderController : BasePluginController
// Quick Order: delivery needs current stock (before Thursday)
// OR goods already arrived (Thu-Sun) and delivery still this week
// Preorder: delivery is Thursday+ but today is still Mon/Tue/Wed (goods not yet here)
// PreOrder: delivery is Thursday+ but today is still Mon/Tue/Wed (goods not yet here)
return (deliveryBeforeThursday || (isLateWeek && deliveryThisWeek))
? "quickorder"
: "preorder";
@ -216,10 +216,10 @@ public class OrderController : BasePluginController
}
}
// ── PRODUCTS — Preorder flow (curated window list) ────────────────────────
// ── PRODUCTS — PreOrder flow (curated window list) ────────────────────────
[HttpGet]
public async Task<IActionResult> GetPreorderProducts()
public async Task<IActionResult> GetPreOrderProducts()
{
var customer = await _workContext.GetCurrentCustomerAsync();
if (await _customerService.IsGuestAsync(customer))
@ -232,12 +232,12 @@ public class OrderController : BasePluginController
var gaStart = await _dbContext.GenericAttributes.Table
.Where(ga => ga.KeyGroup == nameof(Product)
&& ga.Key == FruitBankConst.PreorderWindowStart
&& 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.Key == FruitBankConst.PreOrderWindowEnd
&& ga.StoreId == store.Id)
.ToListAsync();
@ -403,10 +403,10 @@ public class OrderController : BasePluginController
return Json(new { success = true, cartItems = await GetCartItemsJson(customer, store) });
}
// ── PLACE PREORDER (Preorder flow) ────────────────────────────────────────
// ── PLACE PREORDER (PreOrder flow) ────────────────────────────────────────
[HttpPost]
public async Task<IActionResult> PlacePreorder([FromBody] PlacePreorderRequest request)
public async Task<IActionResult> PlacePreOrder([FromBody] PlacePreOrderRequest request)
{
var customer = await _workContext.GetCurrentCustomerAsync();
if (await _customerService.IsGuestAsync(customer))
@ -422,7 +422,7 @@ public class OrderController : BasePluginController
{
var store = await _storeContext.GetCurrentStoreAsync();
var preorder = new Preorder
var preorder = new PreOrder
{
CustomerId = customer.Id,
StoreId = store.Id,
@ -430,7 +430,7 @@ public class OrderController : BasePluginController
CustomerNote = request.CustomerNote?.Trim()
};
var items = new List<PreorderItem>();
var items = new List<PreOrderItem>();
foreach (var req in request.Items.Where(i => i.Quantity > 0))
{
var product = await _dbContext.Products.GetByIdAsync(req.ProductId);
@ -444,7 +444,7 @@ public class OrderController : BasePluginController
unitPrice = pr.finalPrice;
}
items.Add(new PreorderItem
items.Add(new PreOrderItem
{
ProductId = req.ProductId,
RequestedQuantity = req.Quantity,
@ -455,7 +455,7 @@ public class OrderController : BasePluginController
if (!items.Any())
return Json(new { success = false, message = "Nincs érvényes termék az előrendelésben" });
var saved = await _preorderDbContext.InsertPreorderAsync(preorder, items);
var saved = await _preorderDbContext.InsertPreOrderAsync(preorder, items);
// Clean up the pending datetime attribute
await _fruitBankAttributeService
@ -466,12 +466,12 @@ public class OrderController : BasePluginController
// Awaited inline (not fire-and-forget) so we can return the order ID if one is created.
// shippingDocumentId = 0 signals this was triggered at preorder placement, not by a document.
var productIds = items.Select(i => i.ProductId).Distinct().ToList();
await _preorderConversionService.ConvertPreordersForProductsAsync(productIds, 0);
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);
var refreshed = await _preorderDbContext.PreOrders.GetByIdAsync(saved.Id);
Console.WriteLine($"[OrderFlow] PlacePreorder #{saved.Id} — orderId={refreshed?.OrderId}");
Console.WriteLine($"[OrderFlow] PlacePreOrder #{saved.Id} — orderId={refreshed?.OrderId}");
return Json(new
{
@ -607,14 +607,14 @@ OUTPUT FORMAT: [{""product"": ""narancs"", ""quantity"": 100}]";
// ── Inner models ──────────────────────────────────────────────────────────
public class PlacePreorderRequest
public class PlacePreOrderRequest
{
public string? DeliveryDateTime { get; set; }
public string? CustomerNote { get; set; }
public List<PreorderItemRequest> Items { get; set; } = new();
public List<PreOrderItemRequest> Items { get; set; } = new();
}
public class PreorderItemRequest
public class PreOrderItemRequest
{
public int ProductId { get; set; }
public int Quantity { get; set; }

View File

@ -10,31 +10,32 @@ using Nop.Services.Catalog;
using Nop.Services.Customers;
using Nop.Services.Localization;
using Nop.Web.Framework.Controllers;
using static Nop.Plugin.Misc.FruitBankPlugin.Controllers.OrderController;
namespace Nop.Plugin.Misc.FruitBankPlugin.Controllers;
[AutoValidateAntiforgeryToken]
public class PreorderController : BasePluginController
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 PreOrderDbContext _preorderDbContext;
private readonly FruitBankAttributeService _fruitBankAttributeService;
private readonly CustomPriceCalculationService _customPriceCalculationService;
private const string PendingDeliveryDateTimeKey = "PreorderPendingDeliveryDateTime";
private const string Prefix = "Plugins.Misc.FruitBankPlugin.Preorder.";
private const string PendingDeliveryDateTimeKey = "PreOrderPendingDeliveryDateTime";
private const string Prefix = "Plugins.Misc.FruitBankPlugin.PreOrder.";
public PreorderController(
public PreOrderController(
IWorkContext workContext,
IStoreContext storeContext,
ICustomerService customerService,
ILocalizationService localizationService,
FruitBankDbContext dbContext,
PreorderDbContext preorderDbContext,
PreOrderDbContext preorderDbContext,
FruitBankAttributeService fruitBankAttributeService,
IPriceCalculationService priceCalculationService)
{
@ -60,7 +61,7 @@ public class PreorderController : BasePluginController
if (await _customerService.IsGuestAsync(customer))
return Challenge();
return View("~/Plugins/Misc.FruitBankPlugin/Views/Preorder/Index.cshtml");
return View("~/Plugins/Misc.FruitBankPlugin/Views/PreOrder/Index.cshtml");
}
// ── GET SAVED DELIVERY DATETIME (page restore) ────────────────────────────
@ -129,13 +130,13 @@ public class PreorderController : BasePluginController
// 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.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.Key == FruitBankConst.PreOrderWindowEnd
&& ga.StoreId == store.Id)
.ToListAsync();
@ -192,7 +193,7 @@ public class PreorderController : BasePluginController
}
catch (Exception ex)
{
Console.WriteLine($"[Preorder] GetAvailableProducts error: {ex.Message}");
Console.WriteLine($"[PreOrder] GetAvailableProducts error: {ex.Message}");
return Json(new { success = false, message = $"Hiba: {ex.Message}" });
}
}
@ -200,7 +201,7 @@ public class PreorderController : BasePluginController
// ── PLACE PREORDER ────────────────────────────────────────────────────────
[HttpPost]
public async Task<IActionResult> PlacePreorder([FromBody] PlacePreorderRequest request)
public async Task<IActionResult> PlacePreOrder([FromBody] PlacePreOrderRequest request)
{
var customer = await _workContext.GetCurrentCustomerAsync();
if (await _customerService.IsGuestAsync(customer))
@ -216,7 +217,7 @@ public class PreorderController : BasePluginController
{
var store = await _storeContext.GetCurrentStoreAsync();
var preorder = new Preorder
var preorder = new PreOrder
{
CustomerId = customer.Id,
StoreId = store.Id,
@ -224,7 +225,7 @@ public class PreorderController : BasePluginController
CustomerNote = request.CustomerNote?.Trim()
};
var items = new List<PreorderItem>();
var items = new List<PreOrderItem>();
foreach (var req in request.Items.Where(i => i.Quantity > 0))
{
var product = await _dbContext.Products.GetByIdAsync(req.ProductId);
@ -239,7 +240,7 @@ public class PreorderController : BasePluginController
unitPrice = pr.finalPrice;
}
items.Add(new PreorderItem
items.Add(new PreOrderItem
{
ProductId = req.ProductId,
RequestedQuantity = req.Quantity,
@ -250,14 +251,14 @@ public class PreorderController : BasePluginController
if (!items.Any())
return Json(new { success = false, message = await L("NoValidItems") });
var saved = await _preorderDbContext.InsertPreorderAsync(preorder, items);
var saved = await _preorderDbContext.InsertPreOrderAsync(preorder, items);
// Clean up the pending delivery datetime attribute
await _fruitBankAttributeService
.DeleteGenericAttributeAsync<Nop.Core.Domain.Customers.Customer>(
customer.Id, PendingDeliveryDateTimeKey, store.Id);
Console.WriteLine($"[Preorder] Placed #{saved.Id} — customer #{customer.Id}, {items.Count} items, delivery {deliveryDateTime:u}");
Console.WriteLine($"[PreOrder] Placed #{saved.Id} — customer #{customer.Id}, {items.Count} items, delivery {deliveryDateTime:u}");
return Json(new
{
@ -268,21 +269,21 @@ public class PreorderController : BasePluginController
}
catch (Exception ex)
{
Console.WriteLine($"[Preorder] PlacePreorder error: {ex.Message}");
Console.WriteLine($"[PreOrder] PlacePreOrder error: {ex.Message}");
return Json(new { success = false, message = $"Hiba: {ex.Message}" });
}
}
// ── INNER MODELS ──────────────────────────────────────────────────────────
public class PlacePreorderRequest
public class PlacePreOrderRequest
{
public string? DeliveryDateTime { get; set; }
public string? CustomerNote { get; set; }
public List<PreorderItemRequest> Items { get; set; } = new();
public List<PreOrderItemRequest> Items { get; set; } = new();
}
public class PreorderItemRequest
public class PreOrderItemRequest
{
public int ProductId { get; set; }
public int Quantity { get; set; }

View File

@ -1,4 +1,5 @@
using FruitBank.Common.Entities;
using LinqToDB;
using Mango.Nop.Data.Repositories;
using Nop.Core.Caching;
using Nop.Core.Configuration;
@ -18,9 +19,9 @@ public class CargoTruckDbTable : MgDbTableBase<CargoTruck>
public IQueryable<CargoTruck> GetAll(bool loadRelations)
{
return GetAll();
//return loadRelations ? GetAll().LoadWith(sd => sd.CargoTrucks) : GetAll();
return loadRelations ? GetAll().LoadWith(sd => sd.CargoPartner) : GetAll();
}
public Task<CargoTruck> GetByIdAsync(int id, bool loadRelations) => GetAll(loadRelations).FirstOrDefaultAsync(p => p.Id == id);
public IQueryable<CargoTruck> GetByCargoPartnerId(int cargoPartnerId, bool loadRelations) => GetAll(loadRelations).Where(p => p.CargoPartnerId == cargoPartnerId);
}

View File

@ -4,7 +4,7 @@ using Nop.Data;
namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer.Interfaces;
public interface IPreorderDbSet<TDbTable> : IMgDbTableBase where TDbTable : IRepository<Preorder>
public interface IPreOrderDbSet<TDbTable> : IMgDbTableBase where TDbTable : IRepository<PreOrder>
{
public TDbTable Preorders { get; set; }
public TDbTable PreOrders { get; set; }
}

View File

@ -4,7 +4,7 @@ using Nop.Data;
namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer.Interfaces;
public interface IPreorderItemDbSet<TDbTable> : IMgDbTableBase where TDbTable : IRepository<PreorderItem>
public interface IPreOrderItemDbSet<TDbTable> : IMgDbTableBase where TDbTable : IRepository<PreOrderItem>
{
public TDbTable PreorderItems { get; set; }
public TDbTable PreOrderItems { get; set; }
}

View File

@ -15,14 +15,14 @@ using Nop.Plugin.Misc.FruitBankPlugin.Services;
namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
public class PreorderDbContext :
IPreorderDbSet<PreorderDbTable>,
IPreorderItemDbSet<PreorderItemDbTable>
public class PreOrderDbContext :
IPreOrderDbSet<PreOrderDbTable>,
IPreOrderItemDbSet<PreOrderItemDbTable>
{
private readonly ILogger _logger;
public PreorderDbTable Preorders { get; set; }
public PreorderItemDbTable PreorderItems { get; set; }
public PreOrderDbTable PreOrders { get; set; }
public PreOrderItemDbTable PreOrderItems { get; set; }
// Read-only access to related NopCommerce repositories needed during conversion
public IRepository<Customer> Customers { get; set; }
@ -30,112 +30,112 @@ public class PreorderDbContext :
public IRepository<Order> Orders { get; set; }
public IRepository<OrderItem> OrderItems { get; set; }
public PreorderDbContext(
PreorderDbTable preorderDbTable,
PreorderItemDbTable preorderItemDbTable,
public PreOrderDbContext(
PreOrderDbTable preorderDbTable,
PreOrderItemDbTable preorderItemDbTable,
IRepository<Customer> customerRepository,
IRepository<Product> productRepository,
IRepository<Order> orderRepository,
IRepository<OrderItem> orderItemRepository,
IEnumerable<IAcLogWriterBase> logWriters)
{
Preorders = preorderDbTable;
PreorderItems = preorderItemDbTable;
PreOrders = preorderDbTable;
PreOrderItems = preorderItemDbTable;
Customers = customerRepository;
Products = productRepository;
Orders = orderRepository;
OrderItems = orderItemRepository;
_logger = new Logger<PreorderDbContext>(logWriters.ToArray());
_logger = new Logger<PreOrderDbContext>(logWriters.ToArray());
}
/// <summary>
/// Insert a complete preorder with all its items in one operation.
/// Returns the saved preorder (with Id populated).
/// </summary>
public async Task<Preorder> InsertPreorderAsync(Preorder preorder, IList<PreorderItem> items)
public async Task<PreOrder> InsertPreOrderAsync(PreOrder preorder, IList<PreOrderItem> items)
{
preorder.CreatedOnUtc = DateTime.UtcNow;
preorder.UpdatedOnUtc = DateTime.UtcNow;
preorder.Status = PreorderStatus.Pending;
preorder.Status = PreOrderStatus.Pending;
await Preorders.InsertAsync(preorder);
await PreOrders.InsertAsync(preorder);
foreach (var item in items)
{
item.PreorderId = preorder.Id;
item.PreOrderId = preorder.Id;
item.FulfilledQuantity = 0;
item.Status = PreorderItemStatus.Pending;
await PreorderItems.InsertAsync(item);
item.Status = PreOrderItemStatus.Pending;
await PreOrderItems.InsertAsync(item);
}
_logger.Info($"PreorderDbContext: inserted Preorder #{preorder.Id} with {items.Count} items for customer #{preorder.CustomerId}");
_logger.Info($"PreOrderDbContext: inserted PreOrder #{preorder.Id} with {items.Count} items for customer #{preorder.CustomerId}");
return preorder;
}
/// <summary>
/// Returns all pending preorder items for a set of productIds, ordered by PreorderId (FCFS).
/// Used by PreorderConversionService after IncomingQuantity is written.
/// Returns all pending preorder items for a set of productIds, ordered by PreOrderId (FCFS).
/// Used by PreOrderConversionService after IncomingQuantity is written.
/// </summary>
public async Task<List<PreorderItem>> GetPendingItemsForProductsAsync(IList<int> productIds)
public async Task<List<PreOrderItem>> GetPendingItemsForProductsAsync(IList<int> productIds)
{
// Fetch all items for these products first, then filter by status in memory
// LinqToDB cannot translate enum comparisons to SQL in this codebase
var all = await PreorderItems.Table
var all = await PreOrderItems.Table
.Where(i => productIds.Contains(i.ProductId))
.OrderBy(i => i.PreorderId)
.OrderBy(i => i.PreOrderId)
.ToListAsync();
return all.Where(i =>
i.Status == PreorderItemStatus.Pending ||
i.Status == PreorderItemStatus.PartiallyFulfilled)
i.Status == PreOrderItemStatus.Pending ||
i.Status == PreOrderItemStatus.PartiallyFulfilled)
.ToList();
}
/// <summary>
/// After conversion: check if all items in a preorder are resolved and update the preorder's status.
/// </summary>
public async Task RefreshPreorderStatusAsync(int preorderId)
public async Task RefreshPreOrderStatusAsync(int preorderId)
{
var preorder = await Preorders.GetByIdAsync(preorderId);
var preorder = await PreOrders.GetByIdAsync(preorderId);
if (preorder == null) return;
var items = await PreorderItems.GetAllByPreorderIdAsync(preorderId).ToListAsync();
var items = await PreOrderItems.GetAllByPreOrderIdAsync(preorderId).ToListAsync();
var hasDropped = items.Any(i => i.Status == PreorderItemStatus.Dropped);
var hasPartial = items.Any(i => i.Status == PreorderItemStatus.PartiallyFulfilled);
var hasPending = items.Any(i => i.Status == PreorderItemStatus.Pending);
var allFulfilled = items.All(i => i.Status == PreorderItemStatus.Fulfilled);
var hasDropped = items.Any(i => i.Status == PreOrderItemStatus.Dropped);
var hasPartial = items.Any(i => i.Status == PreOrderItemStatus.PartiallyFulfilled);
var hasPending = items.Any(i => i.Status == PreOrderItemStatus.Pending);
var allFulfilled = items.All(i => i.Status == PreOrderItemStatus.Fulfilled);
preorder.Status = (hasDropped || hasPartial) && !hasPending ? PreorderStatus.PartiallyFulfilled
: allFulfilled ? PreorderStatus.Confirmed
: PreorderStatus.Pending;
preorder.Status = (hasDropped || hasPartial) && !hasPending ? PreOrderStatus.PartiallyFulfilled
: allFulfilled ? PreOrderStatus.Confirmed
: PreOrderStatus.Pending;
preorder.UpdatedOnUtc = DateTime.UtcNow;
await Preorders.UpdateAsync(preorder);
await PreOrders.UpdateAsync(preorder);
_logger.Info($"PreorderDbContext: Preorder #{preorderId} status → {preorder.Status}");
_logger.Info($"PreOrderDbContext: PreOrder #{preorderId} status → {preorder.Status}");
}
/// <summary>
/// Mark a preorder as cancelled (customer or admin action).
/// </summary>
public async Task CancelPreorderAsync(int preorderId)
public async Task CancelPreOrderAsync(int preorderId)
{
var preorder = await Preorders.GetByIdAsync(preorderId);
var preorder = await PreOrders.GetByIdAsync(preorderId);
if (preorder == null) return;
preorder.Status = PreorderStatus.Cancelled;
preorder.Status = PreOrderStatus.Cancelled;
preorder.UpdatedOnUtc = DateTime.UtcNow;
await Preorders.UpdateAsync(preorder);
await PreOrders.UpdateAsync(preorder);
var items = await PreorderItems.GetAllByPreorderIdAsync(preorderId).ToListAsync();
var cancellableStatuses = new[] { PreorderItemStatus.Pending, PreorderItemStatus.PartiallyFulfilled };
var items = await PreOrderItems.GetAllByPreOrderIdAsync(preorderId).ToListAsync();
var cancellableStatuses = new[] { PreOrderItemStatus.Pending, PreOrderItemStatus.PartiallyFulfilled };
foreach (var item in items.Where(i => cancellableStatuses.Contains(i.Status)))
{
item.Status = PreorderItemStatus.Dropped;
await PreorderItems.UpdateAsync(item);
item.Status = PreOrderItemStatus.Dropped;
await PreOrderItems.UpdateAsync(item);
}
_logger.Info($"PreorderDbContext: Preorder #{preorderId} cancelled");
_logger.Info($"PreOrderDbContext: PreOrder #{preorderId} cancelled");
}
}

View File

@ -10,9 +10,9 @@ using Nop.Data;
namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
public class PreorderDbTable : MgDbTableBase<Preorder>
public class PreOrderDbTable : MgDbTableBase<PreOrder>
{
public PreorderDbTable(
public PreOrderDbTable(
IEventPublisher eventPublisher,
INopDataProvider dataProvider,
IShortTermCacheManager shortTermCacheManager,
@ -22,23 +22,23 @@ public class PreorderDbTable : MgDbTableBase<Preorder>
{
}
public IQueryable<Preorder> GetAll(bool loadRelations)
public IQueryable<PreOrder> GetAll(bool loadRelations)
{
return loadRelations
? GetAll()
.LoadWith(p => p.PreorderItems)
.LoadWith(p => p.PreOrderItems)
: GetAll();
}
public Task<Preorder?> GetByIdAsync(int id, bool loadRelations)
public Task<PreOrder?> GetByIdAsync(int id, bool loadRelations)
=> GetAll(loadRelations).FirstOrDefaultAsync(p => p.Id == id);
public IQueryable<Preorder> GetAllByCustomerIdAsync(int customerId, bool loadRelations)
public IQueryable<PreOrder> GetAllByCustomerIdAsync(int customerId, bool loadRelations)
=> GetAll(loadRelations).Where(p => p.CustomerId == customerId);
public IQueryable<Preorder> GetAllPendingAsync(bool loadRelations)
public IQueryable<PreOrder> GetAllPendingAsync(bool loadRelations)
{
var pendingStatuses = new[] { PreorderStatus.Pending, PreorderStatus.PartiallyFulfilled };
var pendingStatuses = new[] { PreOrderStatus.Pending, PreOrderStatus.PartiallyFulfilled };
return GetAll(loadRelations).Where(p => pendingStatuses.Contains(p.Status));
}
}

View File

@ -10,9 +10,9 @@ using Nop.Data;
namespace Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
public class PreorderItemDbTable : MgDbTableBase<PreorderItem>
public class PreOrderItemDbTable : MgDbTableBase<PreOrderItem>
{
public PreorderItemDbTable(
public PreOrderItemDbTable(
IEventPublisher eventPublisher,
INopDataProvider dataProvider,
IShortTermCacheManager shortTermCacheManager,
@ -22,21 +22,21 @@ public class PreorderItemDbTable : MgDbTableBase<PreorderItem>
{
}
public IQueryable<PreorderItem> GetAllByPreorderIdAsync(int preorderId)
=> GetAll().Where(i => i.PreorderId == preorderId);
public IQueryable<PreOrderItem> GetAllByPreOrderIdAsync(int preorderId)
=> GetAll().Where(i => i.PreOrderId == preorderId);
public IQueryable<PreorderItem> GetAllByProductIdAsync(int productId)
public IQueryable<PreOrderItem> GetAllByProductIdAsync(int productId)
=> GetAll().Where(i => i.ProductId == productId);
/// <summary>
/// All pending/partially-fulfilled items for a product, ordered by their parent preorder's
/// CreatedOnUtc for first-come-first-served allocation.
/// </summary>
public IQueryable<PreorderItem> GetPendingByProductIdOrderedAsync(int productId)
public IQueryable<PreOrderItem> GetPendingByProductIdOrderedAsync(int productId)
{
var pendingStatuses = new[] { PreorderItemStatus.Pending, PreorderItemStatus.PartiallyFulfilled };
var pendingStatuses = new[] { PreOrderItemStatus.Pending, PreOrderItemStatus.PartiallyFulfilled };
return GetAll()
.Where(i => i.ProductId == productId && pendingStatuses.Contains(i.Status))
.OrderBy(i => i.PreorderId);
.OrderBy(i => i.PreOrderId);
}
}

View File

@ -38,11 +38,11 @@ public class FruitBankEventConsumer :
private readonly FruitBankDbContext _ctx;
private readonly MeasurementService _measurementService;
private readonly FruitBankAttributeService _fruitBankAttributeService;
private readonly PreorderConversionService _preorderConversionService;
private readonly PreOrderConversionService _preorderConversionService;
private readonly IServiceScopeFactory _serviceScopeFactory;
public FruitBankEventConsumer(IHttpContextAccessor httpContextAcc, FruitBankDbContext ctx, MeasurementService measurementService,
FruitBankAttributeService fruitBankAttributeService, PreorderConversionService preorderConversionService,
FruitBankAttributeService fruitBankAttributeService, PreOrderConversionService preorderConversionService,
IServiceScopeFactory serviceScopeFactory, IEnumerable<IAcLogWriterBase> logWriters) : base(ctx, httpContextAcc, logWriters)
{
_ctx = ctx;
@ -212,9 +212,9 @@ public class FruitBankEventConsumer :
System.Transactions.TransactionScopeOption.Suppress,
System.Transactions.TransactionScopeAsyncFlowOption.Enabled);
using var scope = _serviceScopeFactory.CreateScope();
var conversion = scope.ServiceProvider.GetRequiredService<PreorderConversionService>();
try { await conversion.ConvertPreordersForProductsAsync(new List<int> { item.ProductId.Value }, item.ShippingDocumentId); }
catch (Exception ex) { Logger.Error($"[FruitBankEventConsumer] Preorder conversion failed for ProductId={item.ProductId}: {ex.Message}", ex); }
var conversion = scope.ServiceProvider.GetRequiredService<PreOrderConversionService>();
try { await conversion.ConvertPreOrdersForProductsAsync(new List<int> { item.ProductId.Value }, item.ShippingDocumentId); }
catch (Exception ex) { Logger.Error($"[FruitBankEventConsumer] PreOrder conversion failed for ProductId={item.ProductId}: {ex.Message}", ex); }
});
}
}
@ -238,9 +238,9 @@ public class FruitBankEventConsumer :
System.Transactions.TransactionScopeOption.Suppress,
System.Transactions.TransactionScopeAsyncFlowOption.Enabled);
using var scope = _serviceScopeFactory.CreateScope();
var conversion = scope.ServiceProvider.GetRequiredService<PreorderConversionService>();
try { await conversion.ConvertPreordersForProductsAsync(new List<int> { shippingItem.ProductId.Value }, shippingItem.ShippingDocumentId); }
catch (Exception ex) { Logger.Error($"[FruitBankEventConsumer] Preorder conversion failed for ProductId={shippingItem.ProductId}: {ex.Message}", ex); }
var conversion = scope.ServiceProvider.GetRequiredService<PreOrderConversionService>();
try { await conversion.ConvertPreOrdersForProductsAsync(new List<int> { shippingItem.ProductId.Value }, shippingItem.ShippingDocumentId); }
catch (Exception ex) { Logger.Error($"[FruitBankEventConsumer] PreOrder conversion failed for ProductId={shippingItem.ProductId}: {ex.Message}", ex); }
});
}
}

View File

@ -18,12 +18,12 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
public static class FruitBankPluginConst
{
/// <summary>
/// Preorders whose DateOfReceipt is further than this many days in the future
/// PreOrders whose DateOfReceipt is further than this many days in the future
/// are NOT converted at the current conversion run.
/// Based on the bi-weekly truck cycle (~3-4 days between arrivals):
/// if delivery is more than 4 days away, the next truck will arrive before
/// that delivery date, and its document processing will be the correct trigger.
/// </summary>
public const int PreorderConversionWindowDays = 4;
public const int PreOrderConversionWindowDays = 4;
}
}

View File

@ -228,92 +228,92 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.QuickOrder.InvalidProductOrQuantity", "Invalid product or quantity", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.QuickOrder.InvalidProductOrQuantity", "\u00c9rv\u00e9nytelen term\u00e9k vagy mennyis\u00e9g", hu);
// ── Preorder page ───────────────────────────────────────────────────
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.PageTitle", "Preorder", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.PageTitle", "El\u0151rendel\u00e9s", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.MenuLabel", "Preorder", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.MenuLabel", "El\u0151rendel\u00e9s", hu);
// ── PreOrder page ───────────────────────────────────────────────────
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.PageTitle", "PreOrder", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.PageTitle", "El\u0151rendel\u00e9s", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.MenuLabel", "PreOrder", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.MenuLabel", "El\u0151rendel\u00e9s", hu);
// Delivery step
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Title", "When do you want to receive your preorder?", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Title", "Mikor k\u00e9red a rendel\u00e9st?", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Subtitle", "Choose a delivery day and time (we\u2019ll confirm availability)", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Subtitle", "V\u00e1lassz sz\u00e1ll\u00edt\u00e1si napot \u00e9s id\u0151pontot (az el\u00e9rhet\u0151s\u00e9get meger\u0151s\u00edtj\u00fck)", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.DayLabel", "Delivery day", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.DayLabel", "K\u00edv\u00e1nt sz\u00e1ll\u00edt\u00e1si nap", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.TimeLabel", "Delivery time", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.TimeLabel", "K\u00edv\u00e1nt id\u0151pont", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.TimeHint", "Choose an exact time", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.TimeHint", "V\u00e1lassz pontos id\u0151pontot", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.ConfirmButton", "Show available products", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.ConfirmButton", "El\u00e9rhet\u0151 term\u00e9kek mutat\u00e1sa", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.ChangeLabel", "Delivery:", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.ChangeLabel", "Sz\u00e1ll\u00edt\u00e1s:", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.ChangeButton", "Change", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.ChangeButton", "M\u00f3dos\u00edt\u00e1s", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Today", "Today", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Today", "Ma", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Tomorrow", "Tomorrow", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Tomorrow", "Holnap", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Saving", "Saving...", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Saving", "Ment\u00e9s...", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Title", "When do you want to receive your preorder?", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Title", "Mikor k\u00e9red a rendel\u00e9st?", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Subtitle", "Choose a delivery day and time (we\u2019ll confirm availability)", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Subtitle", "V\u00e1lassz sz\u00e1ll\u00edt\u00e1si napot \u00e9s id\u0151pontot (az el\u00e9rhet\u0151s\u00e9get meger\u0151s\u00edtj\u00fck)", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.DayLabel", "Delivery day", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.DayLabel", "K\u00edv\u00e1nt sz\u00e1ll\u00edt\u00e1si nap", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.TimeLabel", "Delivery time", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.TimeLabel", "K\u00edv\u00e1nt id\u0151pont", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.TimeHint", "Choose an exact time", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.TimeHint", "V\u00e1lassz pontos id\u0151pontot", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.ConfirmButton", "Show available products", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.ConfirmButton", "El\u00e9rhet\u0151 term\u00e9kek mutat\u00e1sa", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.ChangeLabel", "Delivery:", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.ChangeLabel", "Sz\u00e1ll\u00edt\u00e1s:", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.ChangeButton", "Change", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.ChangeButton", "M\u00f3dos\u00edt\u00e1s", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Today", "Today", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Today", "Ma", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Tomorrow", "Tomorrow", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Tomorrow", "Holnap", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Saving", "Saving...", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Saving", "Ment\u00e9s...", hu);
// Products
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.InfoBanner", "Preorders are wishes \u2014 we will confirm availability when the shipment arrives and notify you of any changes.", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.InfoBanner", "Az el\u0151rendel\u00e9s egy k\u00edv\u00e1ns\u00e1glista \u2014 az áruk meger\u0151s\u00edt\u00e9se a sz\u00e1ll\u00edtm\u00e1ny be\u00e9rkez\u00e9sekor t\u00f6rt\u00e9nik, \u00e9s az esetleges v\u00e1ltoz\u00e1sokr\u00f3l \u00e9rtes\u00edt\u00fcnk.", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.LoadingProducts", "Loading available products...", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.LoadingProducts", "El\u00e9rhet\u0151 term\u00e9kek bet\u00f6lt\u00e9se...", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.NoProductsAvailable", "No products are currently available for preorder. Please check back later.", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.NoProductsAvailable", "Jelenleg nincs el\u0151rendelhet\u0151 term\u00e9k. K\u00e9rj\u00fck, l\u00e1togass vissza k\u00e9s\u0151bb.", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.ProductsLabel", "Available for preorder \u2014 set quantities:", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.ProductsLabel", "El\u0151rendelhet\u0151 term\u00e9kek \u2014 add meg a mennyis\u00e9geket:", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.MeasurableBadge", "Requires weighing", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.MeasurableBadge", "S\u00falym\u00e9r\u00e9st ig\u00e9nyel", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.PricePerPiece", "Ft/pcs", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.PricePerPiece", "Ft/db", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.PieceUnit", "pcs", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.PieceUnit", "db", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.StockLabel", "Incoming stock:", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.StockLabel", "V\u00e1rhat\u00f3 k\u00e9szlet:", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.InfoBanner", "PreOrders are wishes \u2014 we will confirm availability when the shipment arrives and notify you of any changes.", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.InfoBanner", "Az el\u0151rendel\u00e9s egy k\u00edv\u00e1ns\u00e1glista \u2014 az áruk meger\u0151s\u00edt\u00e9se a sz\u00e1ll\u00edtm\u00e1ny be\u00e9rkez\u00e9sekor t\u00f6rt\u00e9nik, \u00e9s az esetleges v\u00e1ltoz\u00e1sokr\u00f3l \u00e9rtes\u00edt\u00fcnk.", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.LoadingProducts", "Loading available products...", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.LoadingProducts", "El\u00e9rhet\u0151 term\u00e9kek bet\u00f6lt\u00e9se...", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.NoProductsAvailable", "No products are currently available for preorder. Please check back later.", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.NoProductsAvailable", "Jelenleg nincs el\u0151rendelhet\u0151 term\u00e9k. K\u00e9rj\u00fck, l\u00e1togass vissza k\u00e9s\u0151bb.", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.ProductsLabel", "Available for preorder \u2014 set quantities:", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.ProductsLabel", "El\u0151rendelhet\u0151 term\u00e9kek \u2014 add meg a mennyis\u00e9geket:", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.MeasurableBadge", "Requires weighing", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.MeasurableBadge", "S\u00falym\u00e9r\u00e9st ig\u00e9nyel", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.PricePerPiece", "Ft/pcs", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.PricePerPiece", "Ft/db", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.PieceUnit", "pcs", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.PieceUnit", "db", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.StockLabel", "Incoming stock:", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.StockLabel", "V\u00e1rhat\u00f3 k\u00e9szlet:", hu);
// Note + submit
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.NoteLabel", "Additional note (optional)", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.NoteLabel", "Megjegyz\u00e9s (nem k\u00f6telez\u0151)", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.NotePlaceholder", "Any special requests or notes for this preorder...", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.NotePlaceholder", "Esetleges megjegyz\u00e9sek az el\u0151rendel\u00e9ssel kapcsolatban...", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.SelectionNone", "No products selected yet", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.SelectionNone", "M\u00e9g nincs kiv\u00e1lasztott term\u00e9k", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.SelectionItems", "product(s) selected", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.SelectionItems", "term\u00e9k kiv\u00e1lasztva", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.SubmitButton", "Place preorder", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.SubmitButton", "El\u0151rendel\u00e9s lead\u00e1sa", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.Submitting", "Placing preorder...", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.Submitting", "El\u0151rendel\u00e9s ment\u00e9se...", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.NoteLabel", "Additional note (optional)", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.NoteLabel", "Megjegyz\u00e9s (nem k\u00f6telez\u0151)", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.NotePlaceholder", "Any special requests or notes for this preorder...", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.NotePlaceholder", "Esetleges megjegyz\u00e9sek az el\u0151rendel\u00e9ssel kapcsolatban...", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.SelectionNone", "No products selected yet", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.SelectionNone", "M\u00e9g nincs kiv\u00e1lasztott term\u00e9k", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.SelectionItems", "product(s) selected", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.SelectionItems", "term\u00e9k kiv\u00e1lasztva", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.SubmitButton", "Place preorder", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.SubmitButton", "El\u0151rendel\u00e9s lead\u00e1sa", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.Submitting", "Placing preorder...", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.Submitting", "El\u0151rendel\u00e9s ment\u00e9se...", hu);
// Summary panel
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.SummaryTitle", "Your preorder", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.SummaryTitle", "El\u0151rendel\u00e9sed", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.SummaryEmpty", "Set quantities above to build your preorder.", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.SummaryEmpty", "Add meg a mennyis\u00e9geket a term\u00e9kekn\u00e9l.", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.SummaryNote", "Prices for weighed items will be finalised after measurement. Preorder quantities may change depending on actual shipment.", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.SummaryNote", "A s\u00falym\u00e9r\u00e9st ig\u00e9nyl\u0151 t\u00e9teleikn\u00e9l az \u00e1r a m\u00e9r\u00e9s ut\u00e1n v\u00e9glegesedik. A mennyis\u00e9gek a t\u00e9nyleges sz\u00e1ll\u00edtm\u00e1nyt\u00f3l f\u00fcgg\u0151en v\u00e1ltozhatnak.", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.SummaryTitle", "Your preorder", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.SummaryTitle", "El\u0151rendel\u00e9sed", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.SummaryEmpty", "Set quantities above to build your preorder.", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.SummaryEmpty", "Add meg a mennyis\u00e9geket a term\u00e9kekn\u00e9l.", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.SummaryNote", "Prices for weighed items will be finalised after measurement. PreOrder quantities may change depending on actual shipment.", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.SummaryNote", "A s\u00falym\u00e9r\u00e9st ig\u00e9nyl\u0151 t\u00e9teleikn\u00e9l az \u00e1r a m\u00e9r\u00e9s ut\u00e1n v\u00e9glegesedik. A mennyis\u00e9gek a t\u00e9nyleges sz\u00e1ll\u00edtm\u00e1nyt\u00f3l f\u00fcgg\u0151en v\u00e1ltozhatnak.", hu);
// Success + errors
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.SuccessTitle", "Preorder placed!", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.SuccessTitle", "El\u0151rendel\u00e9s leadva!", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.SuccessMessage", "Your preorder #{0} has been received. We will notify you when the shipment arrives.", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.SuccessMessage", "#{0} sz\u00e1m\u00fa el\u0151rendel\u00e9sed be\u00e9rkezett. A sz\u00e1ll\u00edtm\u00e1ny meger\u0151s\u00edt\u00e9sekor \u00e9rtes\u00edt\u00fcnk.", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.BackToHome", "Back to home", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.BackToHome", "Vissza a f\u0151oldalra", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.ErrorPrefix", "Error: ", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.ErrorPrefix", "Hiba: ", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.NotLoggedIn", "Not logged in", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.NotLoggedIn", "Nincs bejelentkezve", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.NoItemsSelected", "No items selected", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.NoItemsSelected", "Nincs kiv\u00e1lasztott term\u00e9k", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.NoValidItems", "No valid items in preorder", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.NoValidItems", "Nincs \u00e9rv\u00e9nyes term\u00e9k az el\u0151rendel\u00e9sben", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.NoDeliveryDateTimeProvided", "No delivery date/time provided", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.NoDeliveryDateTimeProvided", "Nincs sz\u00e1ll\u00edt\u00e1si id\u0151pont megadva", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.InvalidDeliveryDateTime", "Invalid delivery date/time format", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.InvalidDeliveryDateTime", "\u00c9rv\u00e9nytelen sz\u00e1ll\u00edt\u00e1si d\u00e1tum/id\u0151 form\u00e1tum", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.PlacedSuccessfully", "Preorder placed successfully", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.Preorder.PlacedSuccessfully", "El\u0151rendel\u00e9s sikeresen leadva", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.SuccessTitle", "PreOrder placed!", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.SuccessTitle", "El\u0151rendel\u00e9s leadva!", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.SuccessMessage", "Your preorder #{0} has been received. We will notify you when the shipment arrives.", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.SuccessMessage", "#{0} sz\u00e1m\u00fa el\u0151rendel\u00e9sed be\u00e9rkezett. A sz\u00e1ll\u00edtm\u00e1ny meger\u0151s\u00edt\u00e9sekor \u00e9rtes\u00edt\u00fcnk.", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.BackToHome", "Back to home", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.BackToHome", "Vissza a f\u0151oldalra", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.ErrorPrefix", "Error: ", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.ErrorPrefix", "Hiba: ", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.NotLoggedIn", "Not logged in", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.NotLoggedIn", "Nincs bejelentkezve", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.NoItemsSelected", "No items selected", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.NoItemsSelected", "Nincs kiv\u00e1lasztott term\u00e9k", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.NoValidItems", "No valid items in preorder", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.NoValidItems", "Nincs \u00e9rv\u00e9nyes term\u00e9k az el\u0151rendel\u00e9sben", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.NoDeliveryDateTimeProvided", "No delivery date/time provided", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.NoDeliveryDateTimeProvided", "Nincs sz\u00e1ll\u00edt\u00e1si id\u0151pont megadva", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.InvalidDeliveryDateTime", "Invalid delivery date/time format", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.InvalidDeliveryDateTime", "\u00c9rv\u00e9nytelen sz\u00e1ll\u00edt\u00e1si d\u00e1tum/id\u0151 form\u00e1tum", hu);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.PlacedSuccessfully", "PreOrder placed successfully", en);
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.PreOrder.PlacedSuccessfully", "El\u0151rendel\u00e9s sikeresen leadva", hu);
// ── Customer Credit ────────────────────────────────────────────────────
await _localizationService.AddOrUpdateLocaleResourceAsync("Plugins.Misc.FruitBankPlugin.CustomerCredit.PageTitle", "Customer Credit Management", en);
@ -464,7 +464,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin
}
else if (widgetZone == PublicWidgetZones.AccountNavigationAfter)
{
return typeof(CustomerPreorderNavViewComponent);
return typeof(CustomerPreOrderNavViewComponent);
}
}

View File

@ -94,9 +94,9 @@ public class PluginNopStartup : INopStartup
services.AddScoped<StockTakingItemDbTable>();
services.AddScoped<StockTakingItemPalletDbTable>();
services.AddScoped<CustomerCreditDbTable>();
services.AddScoped<PreorderDbTable>();
services.AddScoped<PreorderItemDbTable>();
services.AddScoped<PreorderDbContext>();
services.AddScoped<PreOrderDbTable>();
services.AddScoped<PreOrderItemDbTable>();
services.AddScoped<PreOrderDbContext>();
services.AddScoped<StockTakingDbContext>();
services.AddScoped<FruitBankDbContext>();
@ -151,7 +151,7 @@ public class PluginNopStartup : INopStartup
services.AddScoped<PdfToImageService>();
services.AddScoped<FruitBankNotificationService>();
services.AddScoped<FruitBankOrderItemService>();
services.AddScoped<PreorderConversionService>();
services.AddScoped<PreOrderConversionService>();
services.AddSingleton<IFileStorageProvider>(sp =>
new LocalFileStorageProvider() // Uses default wwwroot/uploads
// Or specify custom path:

View File

@ -197,57 +197,57 @@ public class RouteProvider : IRouteProvider
pattern: "Admin/CustomerCredit/UpdateCreditLimit",
defaults: new { controller = "CustomerCredit", action = "UpdateCreditLimit", area = AreaNames.ADMIN });
// ── Admin: Preorder list ───────────────────────────────────────────
// ── Admin: PreOrder list ───────────────────────────────────────────
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Preorders.List",
pattern: "Admin/Preorders",
defaults: new { controller = "PreorderAdmin", action = "List", area = AreaNames.ADMIN });
name: "Plugin.FruitBank.PreOrders.List",
pattern: "Admin/PreOrders",
defaults: new { controller = "PreOrderAdmin", action = "List", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Preorders.PreorderList",
pattern: "Admin/Preorders/PreorderList",
defaults: new { controller = "PreorderAdmin", action = "PreorderList", area = AreaNames.ADMIN });
name: "Plugin.FruitBank.PreOrders.PreOrderList",
pattern: "Admin/PreOrders/PreOrderList",
defaults: new { controller = "PreOrderAdmin", action = "PreOrderList", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Preorders.Detail",
pattern: "Admin/Preorders/Detail/{id:int}",
defaults: new { controller = "PreorderAdmin", action = "Detail", area = AreaNames.ADMIN });
name: "Plugin.FruitBank.PreOrders.Detail",
pattern: "Admin/PreOrders/Detail/{id:int}",
defaults: new { controller = "PreOrderAdmin", action = "Detail", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Preorders.Cancel",
pattern: "Admin/Preorders/Cancel/{id:int}",
defaults: new { controller = "PreorderAdmin", action = "Cancel", area = AreaNames.ADMIN });
name: "Plugin.FruitBank.PreOrders.Cancel",
pattern: "Admin/PreOrders/Cancel/{id:int}",
defaults: new { controller = "PreOrderAdmin", action = "Cancel", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Preorders.CreatePreorder",
pattern: "Admin/Preorders/CreatePreorder",
defaults: new { controller = "PreorderAdmin", action = "CreatePreorder", area = AreaNames.ADMIN });
name: "Plugin.FruitBank.PreOrders.CreatePreOrder",
pattern: "Admin/PreOrders/CreatePreOrder",
defaults: new { controller = "PreOrderAdmin", action = "CreatePreOrder", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Preorders.DemandList",
pattern: "Admin/Preorders/DemandList",
defaults: new { controller = "PreorderAdmin", action = "DemandList", area = AreaNames.ADMIN });
name: "Plugin.FruitBank.PreOrders.DemandList",
pattern: "Admin/PreOrders/DemandList",
defaults: new { controller = "PreOrderAdmin", action = "DemandList", area = AreaNames.ADMIN });
// ── Admin: Preorder availability ─────────────────────────────────
// ── Admin: PreOrder availability ─────────────────────────────────
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.PreorderAvailability.Index",
pattern: "Admin/PreorderAvailability",
defaults: new { controller = "PreorderAvailability", action = "Index", area = AreaNames.ADMIN });
name: "Plugin.FruitBank.PreOrderAvailability.Index",
pattern: "Admin/PreOrderAvailability",
defaults: new { controller = "PreOrderAvailability", action = "Index", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.PreorderAvailability.ProductList",
pattern: "Admin/PreorderAvailability/ProductList",
defaults: new { controller = "PreorderAvailability", action = "ProductList", area = AreaNames.ADMIN });
name: "Plugin.FruitBank.PreOrderAvailability.ProductList",
pattern: "Admin/PreOrderAvailability/ProductList",
defaults: new { controller = "PreOrderAvailability", action = "ProductList", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.PreorderAvailability.AvailableTodayList",
pattern: "Admin/PreorderAvailability/AvailableTodayList",
defaults: new { controller = "PreorderAvailability", action = "AvailableTodayList", area = AreaNames.ADMIN });
name: "Plugin.FruitBank.PreOrderAvailability.AvailableTodayList",
pattern: "Admin/PreOrderAvailability/AvailableTodayList",
defaults: new { controller = "PreOrderAvailability", action = "AvailableTodayList", area = AreaNames.ADMIN });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.PreorderAvailability.SaveWindow",
pattern: "Admin/PreorderAvailability/SaveWindow",
defaults: new { controller = "PreorderAvailability", action = "SaveWindow", area = AreaNames.ADMIN });
name: "Plugin.FruitBank.PreOrderAvailability.SaveWindow",
pattern: "Admin/PreOrderAvailability/SaveWindow",
defaults: new { controller = "PreOrderAvailability", action = "SaveWindow", area = AreaNames.ADMIN });
// ── Public: Unified Order flow ─────────────────────────────────────────
endpointRouteBuilder.MapControllerRoute(
@ -271,9 +271,9 @@ public class RouteProvider : IRouteProvider
defaults: new { controller = "Order", action = "GetAllProducts" });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Order.GetPreorderProducts",
name: "Plugin.FruitBank.Order.GetPreOrderProducts",
pattern: "rendeles/elozetes-termekek",
defaults: new { controller = "Order", action = "GetPreorderProducts" });
defaults: new { controller = "Order", action = "GetPreOrderProducts" });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Order.SearchProducts",
@ -296,9 +296,9 @@ public class RouteProvider : IRouteProvider
defaults: new { controller = "Order", action = "GetCartItems" });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Order.PlacePreorder",
name: "Plugin.FruitBank.Order.PlacePreOrder",
pattern: "rendeles/elozetes-leadás",
defaults: new { controller = "Order", action = "PlacePreorder" });
defaults: new { controller = "Order", action = "PlacePreOrder" });
// ── Public: Help page ───────────────────────────────────────────────────
endpointRouteBuilder.MapControllerRoute(
@ -308,35 +308,35 @@ public class RouteProvider : IRouteProvider
// ── Public: Customer preorder list ───────────────────────────────────────────
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.CustomerPreorder.List",
name: "Plugin.FruitBank.CustomerPreOrder.List",
pattern: "fiokom/elorerendeles-aim",
defaults: new { controller = "CustomerPreorder", action = "List" });
defaults: new { controller = "CustomerPreOrder", action = "List" });
// ── Public: Preorder (legacy, kept for backward compat) ───────────────
// ── Public: PreOrder (legacy, kept for backward compat) ───────────────
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Preorder.Index",
name: "Plugin.FruitBank.PreOrder.Index",
pattern: "elozetes-rendeles",
defaults: new { controller = "Preorder", action = "Index" });
defaults: new { controller = "PreOrder", action = "Index" });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Preorder.GetDeliveryDateTime",
name: "Plugin.FruitBank.PreOrder.GetDeliveryDateTime",
pattern: "elozetes-rendeles/szallitas-idopont",
defaults: new { controller = "Preorder", action = "GetDeliveryDateTime" });
defaults: new { controller = "PreOrder", action = "GetDeliveryDateTime" });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Preorder.SetDeliveryDateTime",
name: "Plugin.FruitBank.PreOrder.SetDeliveryDateTime",
pattern: "elozetes-rendeles/szallitas-idopont-beallitas",
defaults: new { controller = "Preorder", action = "SetDeliveryDateTime" });
defaults: new { controller = "PreOrder", action = "SetDeliveryDateTime" });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Preorder.GetAvailableProducts",
name: "Plugin.FruitBank.PreOrder.GetAvailableProducts",
pattern: "elozetes-rendeles/termekek",
defaults: new { controller = "Preorder", action = "GetAvailableProducts" });
defaults: new { controller = "PreOrder", action = "GetAvailableProducts" });
endpointRouteBuilder.MapControllerRoute(
name: "Plugin.FruitBank.Preorder.PlacePreorder",
name: "Plugin.FruitBank.PreOrder.PlacePreOrder",
pattern: "elozetes-rendeles/leadás",
defaults: new { controller = "Preorder", action = "PlacePreorder" });
defaults: new { controller = "PreOrder", action = "PlacePreOrder" });
// ── Public: Quick Order ──────────────────────────────────────────────
endpointRouteBuilder.MapControllerRoute(

View File

@ -2,142 +2,142 @@
<Language Name="English" IsDefault="false" IsRightToLeft="false">
<!-- ═══════════════════════════════════════════════════════════
Preorder page — Plugins.Misc.FruitBankPlugin.Preorder.*
PreOrder page — Plugins.Misc.FruitBankPlugin.PreOrder.*
Import: Admin > Configuration > Languages > [English] > Import resources
═══════════════════════════════════════════════════════════ -->
<!-- General -->
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.PageTitle">
<Value><![CDATA[Preorder]]></Value>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.PageTitle">
<Value><![CDATA[PreOrder]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.MenuLabel">
<Value><![CDATA[Preorder]]></Value>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.MenuLabel">
<Value><![CDATA[PreOrder]]></Value>
</LocaleResource>
<!-- Delivery step -->
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Title">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Title">
<Value><![CDATA[When do you want to receive your preorder?]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Subtitle">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Subtitle">
<Value><![CDATA[Choose a delivery day and time (we'll confirm availability)]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.DayLabel">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.DayLabel">
<Value><![CDATA[Delivery day]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.TimeLabel">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.TimeLabel">
<Value><![CDATA[Delivery time]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.TimeHint">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.TimeHint">
<Value><![CDATA[Choose an exact time]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.ConfirmButton">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.ConfirmButton">
<Value><![CDATA[Show available products]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.ChangeLabel">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.ChangeLabel">
<Value><![CDATA[Delivery:]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.ChangeButton">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.ChangeButton">
<Value><![CDATA[Change]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Today">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Today">
<Value><![CDATA[Today]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Tomorrow">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Tomorrow">
<Value><![CDATA[Tomorrow]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Saving">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Saving">
<Value><![CDATA[Saving...]]></Value>
</LocaleResource>
<!-- Product list -->
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.InfoBanner">
<Value><![CDATA[Preorders are wishes — we will confirm availability when the shipment arrives and notify you of any changes.]]></Value>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.InfoBanner">
<Value><![CDATA[PreOrders are wishes — we will confirm availability when the shipment arrives and notify you of any changes.]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.LoadingProducts">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.LoadingProducts">
<Value><![CDATA[Loading available products...]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.NoProductsAvailable">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.NoProductsAvailable">
<Value><![CDATA[No products are currently available for preorder. Please check back later.]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.ProductsLabel">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.ProductsLabel">
<Value><![CDATA[Available for preorder — set quantities:]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.MeasurableBadge">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.MeasurableBadge">
<Value><![CDATA[Requires weighing]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.PricePerPiece">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.PricePerPiece">
<Value><![CDATA[Ft/pcs]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.PieceUnit">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.PieceUnit">
<Value><![CDATA[pcs]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.StockLabel">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.StockLabel">
<Value><![CDATA[Incoming stock:]]></Value>
</LocaleResource>
<!-- Note and submit -->
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.NoteLabel">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.NoteLabel">
<Value><![CDATA[Additional note (optional)]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.NotePlaceholder">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.NotePlaceholder">
<Value><![CDATA[Any special requests or notes for this preorder...]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.SelectionNone">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.SelectionNone">
<Value><![CDATA[No products selected yet]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.SelectionItems">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.SelectionItems">
<Value><![CDATA[product(s) selected]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.SubmitButton">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.SubmitButton">
<Value><![CDATA[Place preorder]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.Submitting">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.Submitting">
<Value><![CDATA[Placing preorder...]]></Value>
</LocaleResource>
<!-- Summary panel -->
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.SummaryTitle">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.SummaryTitle">
<Value><![CDATA[Your preorder]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.SummaryEmpty">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.SummaryEmpty">
<Value><![CDATA[Set quantities above to build your preorder.]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.SummaryNote">
<Value><![CDATA[Prices for weighed items will be finalised after measurement. Preorder quantities may change depending on actual shipment.]]></Value>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.SummaryNote">
<Value><![CDATA[Prices for weighed items will be finalised after measurement. PreOrder quantities may change depending on actual shipment.]]></Value>
</LocaleResource>
<!-- Success -->
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.SuccessTitle">
<Value><![CDATA[Preorder placed!]]></Value>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.SuccessTitle">
<Value><![CDATA[PreOrder placed!]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.SuccessMessage">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.SuccessMessage">
<Value><![CDATA[Your preorder #{0} has been received. We will notify you when the shipment arrives.]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.BackToHome">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.BackToHome">
<Value><![CDATA[Back to home]]></Value>
</LocaleResource>
<!-- Error messages -->
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.ErrorPrefix">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.ErrorPrefix">
<Value><![CDATA[Error: ]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.NotLoggedIn">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.NotLoggedIn">
<Value><![CDATA[Not logged in]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.NoItemsSelected">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.NoItemsSelected">
<Value><![CDATA[No items selected]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.NoValidItems">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.NoValidItems">
<Value><![CDATA[No valid items in preorder]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.NoDeliveryDateTimeProvided">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.NoDeliveryDateTimeProvided">
<Value><![CDATA[No delivery date/time provided]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.InvalidDeliveryDateTime">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.InvalidDeliveryDateTime">
<Value><![CDATA[Invalid delivery date/time format]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.PlacedSuccessfully">
<Value><![CDATA[Preorder placed successfully]]></Value>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.PlacedSuccessfully">
<Value><![CDATA[PreOrder placed successfully]]></Value>
</LocaleResource>
</Language>

View File

@ -2,141 +2,141 @@
<Language Name="Hungarian" IsDefault="false" IsRightToLeft="false">
<!-- ═══════════════════════════════════════════════════════════
Előrendelés oldal — Plugins.Misc.FruitBankPlugin.Preorder.*
Előrendelés oldal — Plugins.Misc.FruitBankPlugin.PreOrder.*
Import: Admin > Konfiguráció > Nyelvek > [Magyar] > Erőforrások importálása
═══════════════════════════════════════════════════════════ -->
<!-- Általános -->
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.PageTitle">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.PageTitle">
<Value><![CDATA[Előrendelés]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.MenuLabel">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.MenuLabel">
<Value><![CDATA[Előrendelés]]></Value>
</LocaleResource>
<!-- Szállítási időpont lépés -->
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Title">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Title">
<Value><![CDATA[Mikor kéred a rendelést?]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Subtitle">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Subtitle">
<Value><![CDATA[Válassz szállítási napot és időpontot (az elérhetőséget megerősítjük)]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.DayLabel">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.DayLabel">
<Value><![CDATA[Kívánt szállítási nap]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.TimeLabel">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.TimeLabel">
<Value><![CDATA[Kívánt időpont]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.TimeHint">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.TimeHint">
<Value><![CDATA[Válassz pontos időpontot]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.ConfirmButton">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.ConfirmButton">
<Value><![CDATA[Elérhető termékek mutatása]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.ChangeLabel">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.ChangeLabel">
<Value><![CDATA[Szállítás:]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.ChangeButton">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.ChangeButton">
<Value><![CDATA[Módosítás]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Today">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Today">
<Value><![CDATA[Ma]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Tomorrow">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Tomorrow">
<Value><![CDATA[Holnap]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Saving">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Saving">
<Value><![CDATA[Mentés...]]></Value>
</LocaleResource>
<!-- Terméklista -->
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.InfoBanner">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.InfoBanner">
<Value><![CDATA[Az előrendelés egy kívánságlista — az áruk megerősítése a szállítmány beérkezésekor történik, és az esetleges változásokról értesítünk.]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.LoadingProducts">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.LoadingProducts">
<Value><![CDATA[Elérhető termékek betöltése...]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.NoProductsAvailable">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.NoProductsAvailable">
<Value><![CDATA[Jelenleg nincs előrendelhető termék. Kérjük, látogass vissza később.]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.ProductsLabel">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.ProductsLabel">
<Value><![CDATA[Előrendelhető termékek — add meg a mennyiségeket:]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.MeasurableBadge">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.MeasurableBadge">
<Value><![CDATA[Súlymérést igényel]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.PricePerPiece">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.PricePerPiece">
<Value><![CDATA[Ft/db]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.PieceUnit">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.PieceUnit">
<Value><![CDATA[db]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.StockLabel">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.StockLabel">
<Value><![CDATA[Várható készlet:]]></Value>
</LocaleResource>
<!-- Megjegyzés és leadás -->
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.NoteLabel">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.NoteLabel">
<Value><![CDATA[Megjegyzés (nem kötelező)]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.NotePlaceholder">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.NotePlaceholder">
<Value><![CDATA[Esetleges megjegyzések az előrendeléssel kapcsolatban...]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.SelectionNone">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.SelectionNone">
<Value><![CDATA[Még nincs kiválasztott termék]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.SelectionItems">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.SelectionItems">
<Value><![CDATA[termék kiválasztva]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.SubmitButton">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.SubmitButton">
<Value><![CDATA[Előrendelés leadása]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.Submitting">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.Submitting">
<Value><![CDATA[Előrendelés mentése...]]></Value>
</LocaleResource>
<!-- Összefoglaló panel -->
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.SummaryTitle">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.SummaryTitle">
<Value><![CDATA[Előrendelésed]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.SummaryEmpty">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.SummaryEmpty">
<Value><![CDATA[Add meg a mennyiségeket a termékeknél.]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.SummaryNote">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.SummaryNote">
<Value><![CDATA[A súlymérést igénylő tételeknél az ár a mérés után véglegesedik. A mennyiségek a tényleges szállítmánytól függően változhatnak.]]></Value>
</LocaleResource>
<!-- Sikeres leadás -->
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.SuccessTitle">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.SuccessTitle">
<Value><![CDATA[Előrendelés leadva!]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.SuccessMessage">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.SuccessMessage">
<Value><![CDATA[#{0} számú előrendelésed beérkezett. A szállítmány megerősítésekor értesítünk.]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.BackToHome">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.BackToHome">
<Value><![CDATA[Vissza a főoldalra]]></Value>
</LocaleResource>
<!-- Hibaüzenetek -->
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.ErrorPrefix">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.ErrorPrefix">
<Value><![CDATA[Hiba: ]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.NotLoggedIn">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.NotLoggedIn">
<Value><![CDATA[Nincs bejelentkezve]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.NoItemsSelected">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.NoItemsSelected">
<Value><![CDATA[Nincs kiválasztott termék]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.NoValidItems">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.NoValidItems">
<Value><![CDATA[Nincs érvényes termék az előrendelésben]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.NoDeliveryDateTimeProvided">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.NoDeliveryDateTimeProvided">
<Value><![CDATA[Nincs szállítási időpont megadva]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.InvalidDeliveryDateTime">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.InvalidDeliveryDateTime">
<Value><![CDATA[Érvénytelen szállítási dátum/idő formátum]]></Value>
</LocaleResource>
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.Preorder.PlacedSuccessfully">
<LocaleResource Name="Plugins.Misc.FruitBankPlugin.PreOrder.PlacedSuccessfully">
<Value><![CDATA[Előrendelés sikeresen leadva]]></Value>
</LocaleResource>

View File

@ -44,8 +44,8 @@ public partial class NameCompatibility : INameCompatibility
{ typeof(StockTakingItemPallet), FruitBankConstClient.StockTakingItemPalletDbTableName},
{ typeof(CustomerCredit), FruitBankConstClient.CustomerCreditDbTableName},
{ typeof(Preorder), FruitBankConstClient.PreOrderDbTableName},
{ typeof(PreorderItem), FruitBankConstClient.PreOrderItemDbTableName},
{ typeof(PreOrder), FruitBankConstClient.PreOrderDbTableName},
{ typeof(PreOrderItem), FruitBankConstClient.PreOrderItemDbTableName},
{ typeof(CargoPartner), FruitBankConstClient.CargoPartnerDbTableName},
{ typeof(CargoTruck), FruitBankConstClient.CargoTruckDbTableName},

View File

@ -224,13 +224,13 @@
<None Update="Areas\Admin\Views\Order\List.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Areas\Admin\Views\PreorderAvailability\Index.cshtml">
<None Update="Areas\Admin\Views\PreOrderAvailability\Index.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Areas\Admin\Views\Preorder\Detail.cshtml">
<None Update="Areas\Admin\Views\PreOrder\Detail.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Areas\Admin\Views\Preorder\List.cshtml">
<None Update="Areas\Admin\Views\PreOrder\List.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Areas\Admin\Views\Product\List.cshtml">
@ -677,10 +677,10 @@
<None Update="Views\CustomerCreditWidget.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Views\CustomerPreorder\List.cshtml">
<None Update="Views\CustomerPreOrder\List.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Views\CustomerPreorder\NavItem.cshtml">
<None Update="Views\CustomerPreOrder\NavItem.cshtml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Views\Help\Index.cshtml">
@ -689,7 +689,7 @@
<None Update="Views\Order\Index.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Views\Preorder\Index.cshtml">
<None Update="Views\PreOrder\Index.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Views\ProductAIListWidget.cshtml">

View File

@ -260,10 +260,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
var preorderAvailabilityMenuItem = new AdminMenuItem
{
Visible = true,
SystemName = "PreorderAvailability",
SystemName = "PreOrderAvailability",
Title = "Előrendelés — elérhetőség",
IconClass = "fas fa-calendar-check",
Url = _adminMenu.GetMenuItemUrl("PreorderAvailability", "Index")
Url = _adminMenu.GetMenuItemUrl("PreOrderAvailability", "Index")
};
//shippingConfigurationItem.ChildNodes.Insert(4, preorderAvailabilityMenuItem);
@ -271,10 +271,10 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
var preorderListMenuItem = new AdminMenuItem
{
Visible = true,
SystemName = "Preorders.List",
SystemName = "PreOrders.List",
Title = "Előrendelések",
IconClass = "fas fa-calendar-plus",
Url = _adminMenu.GetMenuItemUrl("PreorderAdmin", "List")
Url = _adminMenu.GetMenuItemUrl("PreOrderAdmin", "List")
};
var preordersRootMenuItem = new AdminMenuItem
@ -282,7 +282,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
Visible = true,
SystemName = "FruitBank",
Title = "Előrendelés",
//Title = await _localizationService.GetResourceAsync("Plugins.Misc.FruitBankPlugin.Menu.Preorders"), // You can localize this with await _localizationService.GetResourceAsync("...")
//Title = await _localizationService.GetResourceAsync("Plugins.Misc.FruitBankPlugin.Menu.PreOrders"), // You can localize this with await _localizationService.GetResourceAsync("...")
IconClass = "fas fa-heart",
//Url = _adminMenu.GetMenuItemUrl("Shipping", "List")
//ChildNodes = [shippingsListMenuItem, createShippingMenuItem, editShippingMenuItem]

View File

@ -20,7 +20,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services;
/// <summary>
/// Shared service for creating, adding and removing order items.
/// Extracted from CustomOrderController so the same logic can be reused
/// by PreorderConversionService without duplication.
/// by PreOrderConversionService without duplication.
/// </summary>
public class FruitBankOrderItemService
{

View File

@ -10,13 +10,13 @@ using Nop.Services.Orders;
namespace Nop.Plugin.Misc.FruitBankPlugin.Services;
/// <summary>
/// Extension methods for product replacement logic on PreorderConversionService.
/// Extension methods for product replacement logic on PreOrderConversionService.
///
/// SETUP REQUIRED: Add this as a partial class by:
/// 1. Add `partial` keyword to PreorderConversionService class declaration
/// 1. Add `partial` keyword to PreOrderConversionService class declaration
/// 2. This file uses the same injected fields — no extra DI needed
/// </summary>
public partial class PreorderConversionService
public partial class PreOrderConversionService
{
// ── Product replacement ───────────────────────────────────────────────────
@ -127,32 +127,32 @@ public partial class PreorderConversionService
// ── Swap preorder items ───────────────────────────────────────────────
if (affectedOrderIds.Any())
{
var preorders = (await _preorderDbContext.Preorders.GetAll(false).ToListAsync())
var preorders = (await _preorderDbContext.PreOrders.GetAll(false).ToListAsync())
.Where(p => p.OrderId.HasValue && affectedOrderIds.Contains(p.OrderId.Value))
.ToList();
foreach (var preorder in preorders)
{
var piList = await _preorderDbContext.PreorderItems
.GetAllByPreorderIdAsync(preorder.Id)
var piList = await _preorderDbContext.PreOrderItems
.GetAllByPreOrderIdAsync(preorder.Id)
.ToListAsync();
foreach (var pi in piList.Where(i => i.ProductId == oldProductId))
{
pi.ProductId = newProductId;
await _preorderDbContext.PreorderItems.UpdateAsync(pi);
await _preorderDbContext.PreOrderItems.UpdateAsync(pi);
}
}
}
// ── Trigger conversion for new product ────────────────────────────────
// New product may now have pending preorders that can be fulfilled
await ConvertPreordersForProductsAsync(
await ConvertPreOrdersForProductsAsync(
new List<int> { newProductId },
oldItem.ShippingDocumentId);
// TODO: SignalR notification to admin hub
// TODO: SendPreorderProductReplacedNotificationAsync per affected customer
// TODO: SendPreOrderProductReplacedNotificationAsync per affected customer
Console.WriteLine($"[ReplaceShippingItemProduct] Complete: " +
$"{affectedOrderIds.Count} orders swapped, budget remaining={replacementBudget}");
@ -161,7 +161,7 @@ public partial class PreorderConversionService
private async Task<List<OrderDto>> GetAffectedOpenOrdersAsync(int oldProductId)
{
// Orders that are referenced by a preorder AND contain the old product
var preorderOrderIds = (await _preorderDbContext.Preorders.GetAll(false).ToListAsync())
var preorderOrderIds = (await _preorderDbContext.PreOrders.GetAll(false).ToListAsync())
.Where(p => p.OrderId.HasValue)
.Select(p => p.OrderId!.Value)
.ToHashSet();

View File

@ -24,17 +24,17 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services;
/// Called once per shipping document save, after all IncomingQuantity
/// attributes have been written for that document's product set.
///
/// Allocation strategy: first-come-first-served by PreorderId (insertion order).
/// Allocation strategy: first-come-first-served by PreOrderId (insertion order).
///
/// Multi-document design:
/// - Preorder.OrderId tracks the linked real order once created.
/// - First partial fulfillment → creates the order, saves OrderId on Preorder.
/// - PreOrder.OrderId tracks the linked real order once created.
/// - First partial fulfillment → creates the order, saves OrderId on PreOrder.
/// - Subsequent documents → appends only newly-fulfilled items to that same order.
/// - Dropped items are recorded in an order note but never become OrderItems.
/// </summary>
public partial class PreorderConversionService
public partial class PreOrderConversionService
{
private readonly PreorderDbContext _preorderDbContext;
private readonly PreOrderDbContext _preorderDbContext;
private readonly FruitBankDbContext _dbContext;
private readonly ICustomerService _customerService;
private readonly IProductService _productService;
@ -45,8 +45,8 @@ public partial class PreorderConversionService
private readonly FruitBankOrderItemService _orderItemService;
private readonly IStoreContext _storeContext;
public PreorderConversionService(
PreorderDbContext preorderDbContext,
public PreOrderConversionService(
PreOrderDbContext preorderDbContext,
FruitBankDbContext dbContext,
ICustomerService customerService,
IProductService productService,
@ -71,54 +71,54 @@ public partial class PreorderConversionService
// ── Entry point ───────────────────────────────────────────────────────────
public async Task ConvertPreordersForProductsAsync(IList<int> productIds, int shippingDocumentId)
public async Task ConvertPreOrdersForProductsAsync(IList<int> productIds, int shippingDocumentId)
{
Console.WriteLine($"[PreorderConversion] Starting for {productIds.Count} products, shippingDocumentId={shippingDocumentId}");
Console.WriteLine($"[PreOrderConversion] Starting for {productIds.Count} products, shippingDocumentId={shippingDocumentId}");
// Always sweep expired preorders first — any preorder whose DateOfReceipt
// is in the past is closed regardless of stock, before we allocate anything
await SweepExpiredPreordersAsync();
await SweepExpiredPreOrdersAsync();
var pendingItems = await _preorderDbContext.GetPendingItemsForProductsAsync(productIds);
if (!pendingItems.Any())
{
Console.WriteLine("[PreorderConversion] No pending preorder items — done.");
Console.WriteLine("[PreOrderConversion] No pending preorder items — done.");
return;
}
// Filter out preorders whose delivery date is more than PreorderConversionWindowDays
// Filter out preorders whose delivery date is more than PreOrderConversionWindowDays
// (4 days) away. With bi-weekly trucks, a delivery that far out will be served
// by the next truck's document — converting now would steal stock from
// earlier deliveries that legitimately need it.
var conversionCutoff = DateTime.UtcNow.Date.AddDays(FruitBankPluginConst.PreorderConversionWindowDays);
var pendingPreorderIds = pendingItems.Select(i => i.PreorderId).Distinct().ToList();
var parentPreorders = await _preorderDbContext.Preorders
var conversionCutoff = DateTime.UtcNow.Date.AddDays(FruitBankPluginConst.PreOrderConversionWindowDays);
var pendingPreOrderIds = pendingItems.Select(i => i.PreOrderId).Distinct().ToList();
var parentPreOrders = await _preorderDbContext.PreOrders
.GetAll(false)
.Where(p => pendingPreorderIds.Contains(p.Id))
.Where(p => pendingPreOrderIds.Contains(p.Id))
.ToListAsync();
var eligiblePreorderIds = parentPreorders
var eligiblePreOrderIds = parentPreOrders
.Where(p => p.DateOfReceipt.Date <= conversionCutoff)
.Select(p => p.Id)
.ToHashSet();
pendingItems = pendingItems.Where(i => eligiblePreorderIds.Contains(i.PreorderId)).ToList();
pendingItems = pendingItems.Where(i => eligiblePreOrderIds.Contains(i.PreOrderId)).ToList();
if (!pendingItems.Any())
{
Console.WriteLine($"[PreorderConversion] All pending preorders are beyond the " +
$"{FruitBankPluginConst.PreorderConversionWindowDays}-day window — skipped.");
Console.WriteLine($"[PreOrderConversion] All pending preorders are beyond the " +
$"{FruitBankPluginConst.PreOrderConversionWindowDays}-day window — skipped.");
return;
}
Console.WriteLine($"[PreorderConversion] {pendingItems.Count} items eligible " +
$"(within {FruitBankPluginConst.PreorderConversionWindowDays}-day window).");
Console.WriteLine($"[PreOrderConversion] {pendingItems.Count} items eligible " +
$"(within {FruitBankPluginConst.PreOrderConversionWindowDays}-day window).");
var incomingPool = await BuildIncomingQuantityPoolAsync(productIds);
// Track which items were newly resolved in THIS run, grouped by preorder
// Key: preorderId Value: list of items whose status changed in this run
var newlyResolvedByPreorder = new Dictionary<int, List<PreorderItem>>();
var newlyResolvedByPreOrder = new Dictionary<int, List<PreOrderItem>>();
foreach (var item in pendingItems)
{
@ -138,49 +138,49 @@ public partial class PreorderConversionService
incomingPool[item.ProductId] -= fulfill;
item.Status = item.FulfilledQuantity >= item.RequestedQuantity
? PreorderItemStatus.Fulfilled
? PreOrderItemStatus.Fulfilled
: item.FulfilledQuantity > 0
? PreorderItemStatus.PartiallyFulfilled
: PreorderItemStatus.Dropped;
? PreOrderItemStatus.PartiallyFulfilled
: PreOrderItemStatus.Dropped;
await _preorderDbContext.PreorderItems.UpdateAsync(item);
await _preorderDbContext.PreOrderItems.UpdateAsync(item);
}
// Only track this item if something actually changed this run
// (i.e. it gained fulfilled quantity or got dropped)
var gainedQuantity = item.FulfilledQuantity - prevFulfilled;
bool wasDropped = item.Status == PreorderItemStatus.Dropped && prevFulfilled == 0;
bool wasDropped = item.Status == PreOrderItemStatus.Dropped && prevFulfilled == 0;
if (gainedQuantity > 0 || wasDropped)
{
if (!newlyResolvedByPreorder.ContainsKey(item.PreorderId))
newlyResolvedByPreorder[item.PreorderId] = new List<PreorderItem>();
newlyResolvedByPreorder[item.PreorderId].Add(item);
if (!newlyResolvedByPreOrder.ContainsKey(item.PreOrderId))
newlyResolvedByPreOrder[item.PreOrderId] = new List<PreOrderItem>();
newlyResolvedByPreOrder[item.PreOrderId].Add(item);
}
Console.WriteLine($"[PreorderConversion] Item #{item.Id} (product {item.ProductId}): " +
Console.WriteLine($"[PreOrderConversion] Item #{item.Id} (product {item.ProductId}): " +
$"requested={item.RequestedQuantity}, fulfilled={item.FulfilledQuantity}, " +
$"gained={item.FulfilledQuantity - prevFulfilled}, status={item.Status}");
}
// Process each affected preorder
foreach (var (preorderId, changedItems) in newlyResolvedByPreorder)
foreach (var (preorderId, changedItems) in newlyResolvedByPreOrder)
{
await _preorderDbContext.RefreshPreorderStatusAsync(preorderId);
await _preorderDbContext.RefreshPreOrderStatusAsync(preorderId);
var preorder = await _preorderDbContext.Preorders.GetByIdAsync(preorderId);
var preorder = await _preorderDbContext.PreOrders.GetByIdAsync(preorderId);
if (preorder == null) continue;
// Items newly gaining fulfilled quantity in this run
var newlyFulfilled = changedItems
.Where(i => i.FulfilledQuantity - 0 > 0 &&
(i.Status == PreorderItemStatus.Fulfilled ||
i.Status == PreorderItemStatus.PartiallyFulfilled))
(i.Status == PreOrderItemStatus.Fulfilled ||
i.Status == PreOrderItemStatus.PartiallyFulfilled))
.ToList();
// Items dropped in this run (no stock at all)
var newlyDropped = changedItems
.Where(i => i.Status == PreorderItemStatus.Dropped)
.Where(i => i.Status == PreOrderItemStatus.Dropped)
.ToList();
if (preorder.OrderId == null)
@ -198,7 +198,7 @@ public partial class PreorderConversionService
}
}
Console.WriteLine($"[PreorderConversion] Done. {newlyResolvedByPreorder.Count} preorders affected.");
Console.WriteLine($"[PreOrderConversion] Done. {newlyResolvedByPreOrder.Count} preorders affected.");
}
// ── Expiry sweep ───────────────────────────────────────────────────────────
@ -210,71 +210,71 @@ public partial class PreorderConversionService
/// quantities already made it into a real order).
/// Called at the start of every conversion run.
/// </summary>
private async Task SweepExpiredPreordersAsync()
private async Task SweepExpiredPreOrdersAsync()
{
var now = DateTime.UtcNow;
var activePreorderStatuses = new[] { PreorderStatus.Pending, PreorderStatus.PartiallyFulfilled };
var activePreOrderStatuses = new[] { PreOrderStatus.Pending, PreOrderStatus.PartiallyFulfilled };
// Find preorders that are past their receipt date — fetch by date only,
// then filter by status in memory (LinqToDB can't translate enum comparisons)
var expiredPreorders = (await _preorderDbContext.Preorders
var expiredPreOrders = (await _preorderDbContext.PreOrders
.GetAll(false)
.Where(p => p.DateOfReceipt < now)
.ToListAsync())
.Where(p => p.Status == PreorderStatus.Pending ||
p.Status == PreorderStatus.PartiallyFulfilled)
.Where(p => p.Status == PreOrderStatus.Pending ||
p.Status == PreOrderStatus.PartiallyFulfilled)
.ToList();
if (!expiredPreorders.Any()) return;
if (!expiredPreOrders.Any()) return;
Console.WriteLine($"[PreorderConversion] Sweeping {expiredPreorders.Count} expired preorders");
Console.WriteLine($"[PreOrderConversion] Sweeping {expiredPreOrders.Count} expired preorders");
foreach (var preorder in expiredPreorders)
foreach (var preorder in expiredPreOrders)
{
var items = await _preorderDbContext.PreorderItems
.GetAllByPreorderIdAsync(preorder.Id)
var items = await _preorderDbContext.PreOrderItems
.GetAllByPreOrderIdAsync(preorder.Id)
.ToListAsync();
// Drop only the items that were never fulfilled — already-fulfilled
// items stay as-is since they are already on a real order
var stillPending = items.Where(i => i.Status == PreorderItemStatus.Pending).ToList();
var stillPending = items.Where(i => i.Status == PreOrderItemStatus.Pending).ToList();
foreach (var item in stillPending)
{
item.Status = PreorderItemStatus.Dropped;
await _preorderDbContext.PreorderItems.UpdateAsync(item);
item.Status = PreOrderItemStatus.Dropped;
await _preorderDbContext.PreOrderItems.UpdateAsync(item);
}
// Recalculate header status
await _preorderDbContext.RefreshPreorderStatusAsync(preorder.Id);
await _preorderDbContext.RefreshPreOrderStatusAsync(preorder.Id);
var hadAnyFulfillment = items.Any(i =>
i.Status == PreorderItemStatus.Fulfilled ||
i.Status == PreorderItemStatus.PartiallyFulfilled);
i.Status == PreOrderItemStatus.Fulfilled ||
i.Status == PreOrderItemStatus.PartiallyFulfilled);
Console.WriteLine($"[PreorderConversion] Expired preorder #{preorder.Id}: " +
Console.WriteLine($"[PreOrderConversion] Expired preorder #{preorder.Id}: " +
$"{stillPending.Count} items dropped, " +
$"hadFulfillment={hadAnyFulfillment}, orderId={preorder.OrderId}");
// TODO: Send expiry notification if nothing was ever fulfilled
// (fully unfulfilled preorders — customer should be notified)
// if (!hadAnyFulfillment)
// await _fruitBankNotificationService.SendPreorderExpiredNotificationAsync(preorder);
// await _fruitBankNotificationService.SendPreOrderExpiredNotificationAsync(preorder);
}
}
// ── Create new order (first document that fulfills anything) ──────────────
private async Task CreateOrderAsync(
Preorder preorder,
List<PreorderItem> fulfilledItems,
List<PreorderItem> droppedItems,
PreOrder preorder,
List<PreOrderItem> fulfilledItems,
List<PreOrderItem> droppedItems,
int shippingDocumentId)
{
var customer = await _customerService.GetCustomerByIdAsync(preorder.CustomerId);
if (customer == null)
{
Console.WriteLine($"[PreorderConversion] Customer {preorder.CustomerId} not found — skipping order creation for preorder #{preorder.Id}");
Console.WriteLine($"[PreOrderConversion] Customer {preorder.CustomerId} not found — skipping order creation for preorder #{preorder.Id}");
return;
}
@ -289,7 +289,7 @@ public partial class PreorderConversionService
if (billingAddressId == 0)
{
Console.WriteLine($"[PreorderConversion] No billing address for customer {customer.Id} — skipping for preorder #{preorder.Id}");
Console.WriteLine($"[PreOrderConversion] No billing address for customer {customer.Id} — skipping for preorder #{preorder.Id}");
return;
}
@ -351,10 +351,10 @@ public partial class PreorderConversionService
order.CustomOrderNumber = order.Id.ToString();
await _dbContext.Orders.UpdateAsync(order);
// Save OrderId back on the Preorder so future documents can find it
// Save OrderId back on the PreOrder so future documents can find it
preorder.OrderId = order.Id;
preorder.UpdatedOnUtc = DateTime.UtcNow;
await _preorderDbContext.Preorders.UpdateAsync(preorder);
await _preorderDbContext.PreOrders.UpdateAsync(preorder);
// DateOfReceipt generic attribute
await _dbContext.GenericAttributes.InsertAsync(new Nop.Core.Domain.Common.GenericAttribute
@ -370,26 +370,26 @@ public partial class PreorderConversionService
// Fire event so existing handlers (EventConsumer etc.) run
await _eventPublisher.PublishAsync(new OrderPlacedEvent(order));
// TODO: Send "FruitBank.PreorderConverted.CustomerNotification" email
// TODO: Send "FruitBank.PreOrderConverted.CustomerNotification" email
// summarising fulfilled items, dropped items, order ID, DateOfReceipt
// await _fruitBankNotificationService.SendPreorderConvertedNotificationAsync(order, preorder, fulfilledItems, droppedItems);
// await _fruitBankNotificationService.SendPreOrderConvertedNotificationAsync(order, preorder, fulfilledItems, droppedItems);
Console.WriteLine($"[PreorderConversion] Created Order #{order.Id} from Preorder #{preorder.Id} — " +
Console.WriteLine($"[PreOrderConversion] Created Order #{order.Id} from PreOrder #{preorder.Id} — " +
$"{fulfilledItems.Count} fulfilled, {droppedItems.Count} dropped, total {orderTotal:N0} Ft");
}
// ── Append to existing order (subsequent documents) ───────────────────────
private async Task AppendItemsToOrderAsync(
Preorder preorder,
List<PreorderItem> newlyFulfilled,
List<PreorderItem> newlyDropped,
PreOrder preorder,
List<PreOrderItem> newlyFulfilled,
List<PreOrderItem> newlyDropped,
int shippingDocumentId)
{
var order = await _dbContext.Orders.GetByIdAsync(preorder.OrderId!.Value);
if (order == null)
{
Console.WriteLine($"[PreorderConversion] Preorder #{preorder.Id} references Order #{preorder.OrderId} which no longer exists — creating fresh");
Console.WriteLine($"[PreOrderConversion] PreOrder #{preorder.Id} references Order #{preorder.OrderId} which no longer exists — creating fresh");
preorder.OrderId = null;
await CreateOrderAsync(preorder, newlyFulfilled, newlyDropped, shippingDocumentId);
return;
@ -397,7 +397,7 @@ public partial class PreorderConversionService
if (!newlyFulfilled.Any() && !newlyDropped.Any())
{
Console.WriteLine($"[PreorderConversion] Preorder #{preorder.Id}: no new items to append to Order #{order.Id}");
Console.WriteLine($"[PreOrderConversion] PreOrder #{preorder.Id}: no new items to append to Order #{order.Id}");
return;
}
@ -422,16 +422,16 @@ public partial class PreorderConversionService
await InsertOrderNoteAsync(order.Id, preorder.Id, shippingDocumentId, newlyFulfilled, newlyDropped);
// TODO: Send update notification email (same template as initial, but framed as an update)
// await _fruitBankNotificationService.SendPreorderConvertedNotificationAsync(order, preorder, newlyFulfilled, newlyDropped);
// await _fruitBankNotificationService.SendPreOrderConvertedNotificationAsync(order, preorder, newlyFulfilled, newlyDropped);
Console.WriteLine($"[PreorderConversion] Appended {newlyFulfilled.Count} items to Order #{order.Id} " +
$"from Preorder #{preorder.Id} via document #{shippingDocumentId}. " +
Console.WriteLine($"[PreOrderConversion] Appended {newlyFulfilled.Count} items to Order #{order.Id} " +
$"from PreOrder #{preorder.Id} via document #{shippingDocumentId}. " +
$"New total: {newTotal:N0} Ft");
}
// ── Shared helpers ────────────────────────────────────────────────────────
private async Task InsertOrderItemsAsync(Order order, List<PreorderItem> items)
private async Task InsertOrderItemsAsync(Order order, List<PreOrderItem> items)
{
foreach (var item in items)
{
@ -475,13 +475,13 @@ public partial class PreorderConversionService
product,
-item.FulfilledQuantity,
string.Empty,
$"Előrendelés #{item.PreorderId} — rendelés #{order.Id} létrehozása");
$"Előrendelés #{item.PreOrderId} — rendelés #{order.Id} létrehozása");
}
}
private async Task InsertOrderNoteAsync(
int orderId, int preorderId, int shippingDocumentId,
List<PreorderItem> fulfilled, List<PreorderItem> dropped)
List<PreOrderItem> fulfilled, List<PreOrderItem> dropped)
{
var fulfilledDesc = fulfilled.Any()
? $"Teljesített: {string.Join(", ", fulfilled.Select(i => $"#{i.ProductId} ({i.FulfilledQuantity} db)"))}"
@ -521,10 +521,10 @@ public partial class PreorderConversionService
await _fruitBankAttributeService
.InsertOrUpdateGenericAttributeAsync<Product, int>(
productId, nameof(IIncomingQuantity.IncomingQuantity), updated, storeId);
Console.WriteLine($"[PreorderConversion] SyncIncomingQty product #{productId}: {current}+({delta})={updated}");
Console.WriteLine($"[PreOrderConversion] SyncIncomingQty product #{productId}: {current}+({delta})={updated}");
}
private async Task<decimal> CalculateTotalAsync(List<PreorderItem> items)
private async Task<decimal> CalculateTotalAsync(List<PreOrderItem> items)
{
var total = 0m;
foreach (var item in items)
@ -549,17 +549,17 @@ public partial class PreorderConversionService
p => p.Id,
p => p.AvailableQuantity);
var activeItemStatuses = new[] { PreorderItemStatus.Fulfilled, PreorderItemStatus.PartiallyFulfilled };
var activeItemStatuses = new[] { PreOrderItemStatus.Fulfilled, PreOrderItemStatus.PartiallyFulfilled };
// 2. Subtract quantities already committed to preorders in previous runs
// Fetch by productId only, filter by status in memory
var allCommittedItems = await _preorderDbContext.PreorderItems.Table
var allCommittedItems = await _preorderDbContext.PreOrderItems.Table
.Where(i => productIds.Contains(i.ProductId))
.ToListAsync();
var alreadyAllocated = allCommittedItems
.Where(i => i.Status == PreorderItemStatus.Fulfilled ||
i.Status == PreorderItemStatus.PartiallyFulfilled)
.Where(i => i.Status == PreOrderItemStatus.Fulfilled ||
i.Status == PreOrderItemStatus.PartiallyFulfilled)
.GroupBy(i => i.ProductId)
.Select(g => new { ProductId = g.Key, Allocated = g.Sum(i => i.FulfilledQuantity) })
.ToList();

View File

@ -1,6 +1,6 @@
@using FruitBank.Common.Enums
@using Nop.Plugin.Misc.FruitBankPlugin.Controllers
@model List<CustomerPreorderController.CustomerPreorderRow>
@model List<CustomerPreOrderController.CustomerPreOrderRow>
@{
Layout = "_ColumnsTwo";
@ -28,23 +28,23 @@
{
var statusClass = preorder.Status switch
{
PreorderStatus.Confirmed => "po-status-confirmed",
PreorderStatus.PartiallyFulfilled => "po-status-partial",
PreorderStatus.Cancelled => "po-status-cancelled",
PreOrderStatus.Confirmed => "po-status-confirmed",
PreOrderStatus.PartiallyFulfilled => "po-status-partial",
PreOrderStatus.Cancelled => "po-status-cancelled",
_ => "po-status-pending"
};
var statusLabel = preorder.Status switch
{
PreorderStatus.Confirmed => "Megerősítve",
PreorderStatus.PartiallyFulfilled => "Részben teljesítve",
PreorderStatus.Cancelled => "Törölve / Lejárt",
PreOrderStatus.Confirmed => "Megerősítve",
PreOrderStatus.PartiallyFulfilled => "Részben teljesítve",
PreOrderStatus.Cancelled => "Törölve / Lejárt",
_ => "Függőben"
};
<div class="po-customer-card">
<div class="po-card-header">
<div class="po-card-meta">
<span class="po-card-id">#@preorder.PreorderId előrendelés</span>
<span class="po-card-id">#@preorder.PreOrderId előrendelés</span>
<span class="po-card-date">
<i class="fa fa-calendar"></i>
Kért szállítás: <strong>@preorder.DateOfReceipt.ToLocalTime().ToString("yyyy. MM. dd. HH:mm")</strong>
@ -89,16 +89,16 @@
{
var itemStatusLabel = item.Status switch
{
PreorderItemStatus.Fulfilled => "✓ Teljesítve",
PreorderItemStatus.PartiallyFulfilled => "◑ Részben",
PreorderItemStatus.Dropped => "✕ Ejtve",
PreOrderItemStatus.Fulfilled => "✓ Teljesítve",
PreOrderItemStatus.PartiallyFulfilled => "◑ Részben",
PreOrderItemStatus.Dropped => "✕ Ejtve",
_ => "⏳ Vár"
};
var itemStatusClass = item.Status switch
{
PreorderItemStatus.Fulfilled => "item-fulfilled",
PreorderItemStatus.PartiallyFulfilled => "item-partial",
PreorderItemStatus.Dropped => "item-dropped",
PreOrderItemStatus.Fulfilled => "item-fulfilled",
PreOrderItemStatus.PartiallyFulfilled => "item-partial",
PreOrderItemStatus.Dropped => "item-dropped",
_ => "item-pending"
};
var unitPrice = item.IsMeasurable
@ -147,7 +147,7 @@
.no-data p { margin-bottom: 16px; font-size: 15px; }
/* ── Preorder card ────────────────────────────────────────────── */
/* ── PreOrder card ────────────────────────────────────────────── */
.po-customer-card {
background: #fff;
border: 1px solid #dde8da;

View File

@ -1,5 +1,5 @@
<li class="customer-navigation-item @(Context.Request.Path.Value?.Contains("elorerendeles") == true ? "active" : "")">
<a href="@Url.Action("List", "CustomerPreorder")">
<a href="@Url.Action("List", "CustomerPreOrder")">
Előrendeléseim
</a>
</li>

View File

@ -499,7 +499,7 @@
<i class="fa fa-chevron-down"></i>
</button>
<div class="help-faq-a">
A <a href="@Url.Action("List", "CustomerPreorder")" style="color:#2d7a3a;font-weight:600;">Saját fiók → Előrendeléseim</a> oldalon látod az összes leadott előrendelést, azok állapotát és a létrejött rendelésekre mutató hivatkozást.
A <a href="@Url.Action("List", "CustomerPreOrder")" style="color:#2d7a3a;font-weight:600;">Saját fiók → Előrendeléseim</a> oldalon látod az összes leadott előrendelést, azok állapotát és a létrejött rendelésekre mutató hivatkozást.
</div>
</div>
</div>

View File

@ -177,7 +177,7 @@
</div><!-- /#sectionQuickOrder -->
<!-- ══ PREORDER SECTION ══════════════════════════════════════════════ -->
<div id="sectionPreorder" style="display:none;">
<div id="sectionPreOrder" style="display:none;">
<div class="po-info-banner">
<i class="fa fa-info-circle"></i>
Az előrendelés egy kívánságlista — az áruk megerősítése a szállítmány beérkezésekor történik, és az esetleges változásokról értesítünk.
@ -211,7 +211,7 @@
<div class="po-submit-row">
<div id="poSelectionSummary" class="po-selection-summary">Még nincs kiválasztott termék</div>
<button type="button" id="submitPreorderBtn" class="po-submit-btn" disabled>
<button type="button" id="submitPreOrderBtn" class="po-submit-btn" disabled>
<i class="fa fa-paper-plane"></i> Előrendelés leadása
</button>
</div>
@ -237,11 +237,11 @@
</div>
</div>
</div>
</div><!-- /#sectionPreorder -->
</div><!-- /#sectionPreOrder -->
</div><!-- /#mainContent -->
<!-- ── Preorder success state ────────────────────────────────────────── -->
<!-- ── PreOrder success state ────────────────────────────────────────── -->
<div id="successState" style="display:none;" class="po-success-state">
<div class="po-success-icon"><i class="fa fa-check-circle"></i></div>
<h2>Előrendelés leadva!</h2>
@ -408,7 +408,7 @@
}
@@keyframes fadeIn { from { opacity:0; transform:translateY(-4px); } to { opacity:1; transform:translateY(0); } }
/* ── Preorder stock label variant ──────────────────────────── */
/* ── PreOrder stock label variant ──────────────────────────── */
.pc-stock.stock-preorder {
color: #c87500;
font-style: italic;
@ -500,7 +500,7 @@
var selectedDayLabel = null;
var currentFlowType = null; // "quickorder" | "preorder"
// Preorder state
// PreOrder state
var poProducts = [];
var poQuantities = {};
@ -547,7 +547,7 @@
$('#recordBtn').click(startRecording);
$('#stopBtn').click(function () { stopRecording(false); });
$('#submitPreorderBtn').click(submitPreorder);
$('#submitPreOrderBtn').click(submitPreOrder);
// Restore saved datetime
$.ajax({
@ -674,12 +674,12 @@
if (flowType === 'quickorder') {
$('#sectionQuickOrder').show();
$('#sectionPreorder').hide();
$('#sectionPreOrder').hide();
loadAllProducts();
} else {
$('#sectionPreorder').show();
$('#sectionPreOrder').show();
$('#sectionQuickOrder').hide();
loadPreorderProducts();
loadPreOrderProducts();
}
}
@ -942,21 +942,21 @@
// PREORDER FLOW
// ══════════════════════════════════════════════════════════════════════════
function loadPreorderProducts() {
function loadPreOrderProducts() {
$('#poLoadingState').show(); $('#poNoProducts, #poProductSection').hide();
$.ajax({ url: '@Url.Action("GetPreorderProducts", "Order")', type: 'GET',
$.ajax({ url: '@Url.Action("GetPreOrderProducts", "Order")', type: 'GET',
success: function (r) {
$('#poLoadingState').hide();
if (!r.success || !r.products || r.products.length === 0) { $('#poNoProducts').show(); return; }
poProducts = r.products; poQuantities = {};
renderPreorderProducts(); $('#poProductSection').show();
renderPreOrderProducts(); $('#poProductSection').show();
},
error: function () { $('#poLoadingState').hide(); $('#poNoProducts').show(); }
});
}
function renderPreorderProducts() {
function renderPreOrderProducts() {
var grid = $('#poProductGrid').empty();
$.each(poProducts, function (_, p) {
poQuantities[p.id] = 0;
@ -1004,7 +1004,7 @@
var selected = poProducts.filter(function (p) { return (poQuantities[p.id] || 0) > 0; });
var count = selected.length;
$('#poItemCountBadge').text(count);
$('#submitPreorderBtn').prop('disabled', count === 0);
$('#submitPreOrderBtn').prop('disabled', count === 0);
$('#poSelectionSummary').text(count === 0 ? 'Még nincs kiválasztott termék' : count + ' termék kiválasztva');
if (count === 0) { $('#poSummaryEmpty').show(); $('#poSummaryList, #poSummaryNote').hide(); return; }
@ -1022,14 +1022,14 @@
if (hasMeasurable) $('#poSummaryNote').show(); else $('#poSummaryNote').hide();
}
function submitPreorder() {
function submitPreOrder() {
var selected = poProducts.filter(function (p) { return (poQuantities[p.id] || 0) > 0; });
if (!selected.length) return;
var btn = $('#submitPreorderBtn').prop('disabled', true)
var btn = $('#submitPreOrderBtn').prop('disabled', true)
.html('<i class="fa fa-spinner fa-spin"></i> Előrendelés mentése...');
$.ajax({
url : '@Url.Action("PlacePreorder", "Order")',
url : '@Url.Action("PlacePreOrder", "Order")',
type : 'POST',
contentType: 'application/json',
data : JSON.stringify({

View File

@ -1,7 +1,7 @@
@using System.Text.Encodings.Web
@{
Layout = "_Root";
ViewBag.Title = T("Plugins.Misc.FruitBankPlugin.Preorder.PageTitle").Text;
ViewBag.Title = T("Plugins.Misc.FruitBankPlugin.PreOrder.PageTitle").Text;
}
<link rel="stylesheet" href="~/Plugins/Misc.FruitBankPlugin/css/quick-order.css" />
@ -14,25 +14,25 @@
<div class="ds-header">
<i class="fa fa-calendar"></i>
<div>
<div class="ds-title">@T("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Title")</div>
<div class="ds-subtitle">@T("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Subtitle")</div>
<div class="ds-title">@T("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Title")</div>
<div class="ds-subtitle">@T("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Subtitle")</div>
</div>
</div>
<div class="ds-body">
<div class="ds-section-label">@T("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.DayLabel")</div>
<div class="ds-section-label">@T("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.DayLabel")</div>
<div class="ds-day-buttons" id="dayButtons"></div>
<div class="ds-section-label" style="margin-top:20px;">
@T("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.TimeLabel")
@T("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.TimeLabel")
</div>
<div class="ds-time-wrapper">
<input type="time" id="deliveryTimePicker" class="ds-time-input" value="08:00" min="05:00" max="22:00" />
<span class="ds-time-hint">@T("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.TimeHint")</span>
<span class="ds-time-hint">@T("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.TimeHint")</span>
</div>
</div>
<div class="ds-footer">
<button type="button" class="ds-confirm-btn" id="deliveryConfirmBtn" disabled>
<i class="fa fa-arrow-right"></i> @T("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.ConfirmButton")
<i class="fa fa-arrow-right"></i> @T("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.ConfirmButton")
</button>
</div>
</div>
@ -40,10 +40,10 @@
<!-- ── Delivery chip ────────────────────────────────────────────────── -->
<div id="deliveryChip" class="qo-delivery-chip" style="display:none;">
<i class="fa fa-calendar-check-o"></i>
<span class="dc-label">@T("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.ChangeLabel")</span>
<span class="dc-label">@T("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.ChangeLabel")</span>
<strong id="deliveryChipText"></strong>
<button type="button" class="dc-change-btn" id="deliveryChangeBtn">
<i class="fa fa-pencil"></i> @T("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.ChangeButton")
<i class="fa fa-pencil"></i> @T("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.ChangeButton")
</button>
</div>
@ -53,7 +53,7 @@
<!-- Info banner -->
<div class="po-info-banner">
<i class="fa fa-info-circle"></i>
@T("Plugins.Misc.FruitBankPlugin.Preorder.InfoBanner")
@T("Plugins.Misc.FruitBankPlugin.PreOrder.InfoBanner")
</div>
<div class="qo-layout">
@ -64,20 +64,20 @@
<!-- Loading -->
<div id="productsLoadingState" class="products-empty-state">
<i class="fa fa-spinner fa-spin"></i>
<p>@T("Plugins.Misc.FruitBankPlugin.Preorder.LoadingProducts")</p>
<p>@T("Plugins.Misc.FruitBankPlugin.PreOrder.LoadingProducts")</p>
</div>
<!-- No products -->
<div id="noProductsCard" class="no-results-card" style="display:none;">
<i class="fa fa-calendar-times-o"></i>
<p>@T("Plugins.Misc.FruitBankPlugin.Preorder.NoProductsAvailable")</p>
<p>@T("Plugins.Misc.FruitBankPlugin.PreOrder.NoProductsAvailable")</p>
</div>
<!-- Product grid -->
<div id="productSection" style="display:none;">
<div class="matches-label">
<i class="fa fa-cubes"></i>
<span>@T("Plugins.Misc.FruitBankPlugin.Preorder.ProductsLabel")</span>
<span>@T("Plugins.Misc.FruitBankPlugin.PreOrder.ProductsLabel")</span>
</div>
<div id="productGrid" class="product-grid"></div>
@ -85,19 +85,19 @@
<div class="po-note-section">
<label class="po-note-label" for="customerNote">
<i class="fa fa-comment-o"></i>
@T("Plugins.Misc.FruitBankPlugin.Preorder.NoteLabel")
@T("Plugins.Misc.FruitBankPlugin.PreOrder.NoteLabel")
</label>
<textarea id="customerNote" class="po-note-input"
placeholder="@T("Plugins.Misc.FruitBankPlugin.Preorder.NotePlaceholder")"
placeholder="@T("Plugins.Misc.FruitBankPlugin.PreOrder.NotePlaceholder")"
rows="3" maxlength="1000"></textarea>
</div>
<!-- Submit -->
<div class="po-submit-row">
<div id="selectionSummary" class="po-selection-summary"></div>
<button type="button" id="submitPreorderBtn" class="po-submit-btn" disabled>
<button type="button" id="submitPreOrderBtn" class="po-submit-btn" disabled>
<i class="fa fa-paper-plane"></i>
@T("Plugins.Misc.FruitBankPlugin.Preorder.SubmitButton")
@T("Plugins.Misc.FruitBankPlugin.PreOrder.SubmitButton")
</button>
</div>
</div>
@ -108,13 +108,13 @@
<div class="qo-cart-panel">
<div class="qo-section-title">
<i class="fa fa-list-ul"></i>
@T("Plugins.Misc.FruitBankPlugin.Preorder.SummaryTitle")
@T("Plugins.Misc.FruitBankPlugin.PreOrder.SummaryTitle")
<span id="itemCountBadge" class="cart-count-badge">0</span>
</div>
<div id="summaryEmpty" class="cart-empty">
<i class="fa fa-list-ul"></i>
<p>@T("Plugins.Misc.FruitBankPlugin.Preorder.SummaryEmpty")</p>
<p>@T("Plugins.Misc.FruitBankPlugin.PreOrder.SummaryEmpty")</p>
</div>
<div id="summaryList" class="cart-items-list" style="display:none;"></div>
@ -122,7 +122,7 @@
<div id="summaryNote" class="cart-total-row" style="display:none;">
<div class="cart-total-note">
<i class="fa fa-info-circle"></i>
<small>@T("Plugins.Misc.FruitBankPlugin.Preorder.SummaryNote")</small>
<small>@T("Plugins.Misc.FruitBankPlugin.PreOrder.SummaryNote")</small>
</div>
</div>
</div>
@ -133,10 +133,10 @@
<!-- ── SUCCESS STATE ─────────────────────────────────────────────────── -->
<div id="successState" style="display:none;" class="po-success-state">
<div class="po-success-icon"><i class="fa fa-check-circle"></i></div>
<h2>@T("Plugins.Misc.FruitBankPlugin.Preorder.SuccessTitle")</h2>
<h2>@T("Plugins.Misc.FruitBankPlugin.PreOrder.SuccessTitle")</h2>
<p id="successMessage"></p>
<a href="@Url.RouteUrl("Homepage")" class="po-back-btn">
<i class="fa fa-home"></i> @T("Plugins.Misc.FruitBankPlugin.Preorder.BackToHome")
<i class="fa fa-home"></i> @T("Plugins.Misc.FruitBankPlugin.PreOrder.BackToHome")
</a>
</div>
@ -146,19 +146,19 @@
<script asp-location="Footer">
var poStr = {
dsToday : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Today").Text))',
dsTomorrow : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Tomorrow").Text))',
dsSaving : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.Saving").Text))',
dsConfirm : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.Preorder.DeliveryStep.ConfirmButton").Text))',
measurable : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.Preorder.MeasurableBadge").Text))',
pricePerPc : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.Preorder.PricePerPiece").Text))',
pieceUnit : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.Preorder.PieceUnit").Text))',
stockLabel : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.Preorder.StockLabel").Text))',
selNone : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.Preorder.SelectionNone").Text))',
selItems : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.Preorder.SelectionItems").Text))',
submitting : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.Preorder.Submitting").Text))',
successMsg : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.Preorder.SuccessMessage").Text))',
errorPfx : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.Preorder.ErrorPrefix").Text))',
dsToday : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Today").Text))',
dsTomorrow : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Tomorrow").Text))',
dsSaving : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.Saving").Text))',
dsConfirm : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.PreOrder.DeliveryStep.ConfirmButton").Text))',
measurable : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.PreOrder.MeasurableBadge").Text))',
pricePerPc : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.PreOrder.PricePerPiece").Text))',
pieceUnit : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.PreOrder.PieceUnit").Text))',
stockLabel : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.PreOrder.StockLabel").Text))',
selNone : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.PreOrder.SelectionNone").Text))',
selItems : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.PreOrder.SelectionItems").Text))',
submitting : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.PreOrder.Submitting").Text))',
successMsg : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.PreOrder.SuccessMessage").Text))',
errorPfx : '@Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.PreOrder.ErrorPrefix").Text))',
huDayNames : ['vas\u00e1rnap','h\u00e9tf\u0151','kedd','szerda','cs\u00fct\u00f6rt\u00f6k','p\u00e9ntek','szombat']
};
</script>
@ -196,11 +196,11 @@ var poStr = {
$('#deliveryStep').show();
});
$('#submitPreorderBtn').click(submitPreorder);
$('#submitPreOrderBtn').click(submitPreOrder);
// Restore saved delivery datetime if revisiting
$.ajax({
url: '@Url.Action("GetDeliveryDateTime", "Preorder")',
url: '@Url.Action("GetDeliveryDateTime", "PreOrder")',
type: 'GET',
success: function (result) {
if (!result.success || !result.hasValue) return;
@ -251,7 +251,7 @@ var poStr = {
var deliveryDateTime = selectedDeliveryDate + 'T' + selectedDeliveryTime;
$.ajax({
url : '@Url.Action("SetDeliveryDateTime", "Preorder")',
url : '@Url.Action("SetDeliveryDateTime", "PreOrder")',
type: 'POST',
data: {
deliveryDateTime: deliveryDateTime,
@ -288,7 +288,7 @@ var poStr = {
$('#productSection').hide();
$.ajax({
url : '@Url.Action("GetAvailableProducts", "Preorder")',
url : '@Url.Action("GetAvailableProducts", "PreOrder")',
type: 'GET',
success: function (result) {
$('#productsLoadingState').hide();
@ -369,7 +369,7 @@ var poStr = {
var count = selectedItems.length;
$('#itemCountBadge').text(count);
$('#submitPreorderBtn').prop('disabled', count === 0);
$('#submitPreOrderBtn').prop('disabled', count === 0);
// Selection summary text
if (count === 0) {
@ -413,11 +413,11 @@ var poStr = {
// ── Submit ────────────────────────────────────────────────────────────────
function submitPreorder() {
function submitPreOrder() {
var selectedItems = products.filter(function (p) { return (quantities[p.id] || 0) > 0; });
if (!selectedItems.length) return;
var btn = $('#submitPreorderBtn');
var btn = $('#submitPreOrderBtn');
btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> ' + poStr.submitting);
var payload = {
@ -429,7 +429,7 @@ var poStr = {
};
$.ajax({
url : '@Url.Action("PlacePreorder", "Preorder")',
url : '@Url.Action("PlacePreOrder", "PreOrder")',
type : 'POST',
contentType: 'application/json',
data : JSON.stringify(payload),
@ -442,13 +442,13 @@ var poStr = {
} else {
alert(poStr.errorPfx + (result.message || ''));
btn.prop('disabled', false)
.html('<i class="fa fa-paper-plane"></i> @Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.Preorder.SubmitButton").Text))');
.html('<i class="fa fa-paper-plane"></i> @Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.PreOrder.SubmitButton").Text))');
}
},
error: function () {
alert(poStr.errorPfx);
btn.prop('disabled', false)
.html('<i class="fa fa-paper-plane"></i> @Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.Preorder.SubmitButton").Text))');
.html('<i class="fa fa-paper-plane"></i> @Html.Raw(JavaScriptEncoder.Default.Encode(T("Plugins.Misc.FruitBankPlugin.PreOrderPreOrder.SubmitButton").Text))');
}
});
}

View File

@ -1,5 +1,5 @@
/*
* Preorder page supplemental styles
* PreOrder page supplemental styles
* Inherits all base styles from quick-order.css
*/

File diff suppressed because one or more lines are too long