diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopRecipes.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopRecipes.razor
index 2057624e..d8931b79 100644
--- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopRecipes.razor
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopRecipes.razor
@@ -6,7 +6,7 @@
@_recipes.Count công thức
- { _showRecipeForm = !_showRecipeForm; _editingRecipeId = null; _newRecipeName = ""; _newRecipeInstructions = ""; _newRecipePrepTime = 5; _recipeIngredients = new(); _recipeFormMessage = null; }'>
+ { _showRecipeForm = !_showRecipeForm; _editingRecipeId = null; _selectedProductId = Guid.Empty; _newRecipeName = ""; _newRecipeInstructions = ""; _newRecipePrepTime = 5; _recipeIngredients = new(); _recipeFormMessage = null; }'>
Thêm công thức
@@ -18,6 +18,16 @@
@@ -26,10 +36,10 @@
{
var i = idx;
- @* EN: Inventory item dropdown — select raw material from inventory / VI: Dropdown chọn nguyên liệu từ kho *@
+ @* EN: Inventory item dropdown — select from all inventory items / VI: Dropdown chọn từ tất cả mặt hàng tồn kho *@
OnInventoryItemSelected(i, e.Value?.ToString()))" style="padding:6px 10px;border-radius:6px;border:1px solid var(--admin-border-subtle);background:var(--admin-bg-elevated);color:var(--admin-text-primary);font-size:12px;">
-- Chọn kho / Select inventory --
- @foreach (var item in _inventoryItems.Where(x => x.ItemType == "RawMaterial" || x.ItemType == null))
+ @foreach (var item in _inventoryItems)
{
@(item.ProductName ?? item.Id.ToString()[..8]) (@item.Unit - @(item.CostPerUnit?.ToString("N0") ?? "0")d)
}
@@ -71,10 +81,29 @@ else
@recipe.Name
- DeleteRecipeItem(recipe.Id)' style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:26px;height:26px;display:flex;align-items:center;justify-content:center;cursor:pointer;">
+ StartEditRecipe(recipe)' style="background:rgba(255,92,0,0.1);border:none;border-radius:6px;width:26px;height:26px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Sửa">
+ DeleteRecipeItem(recipe.Id)' style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:26px;height:26px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Xóa">
@recipe.PrepTimeMinutes phút chuẩn bị
+ @{ var linkedProduct = _products.FirstOrDefault(p => p.Id == recipe.ProductId); }
+ @if (linkedProduct != null)
+ {
+ @linkedProduct.Name (@(linkedProduct.Price.ToString("N0"))đ)
+ }
+ @if (recipe.Ingredients != null && recipe.Ingredients.Any())
+ {
+
+
Nguyên liệu:
+ @foreach (var ing in recipe.Ingredients)
+ {
+
+ @ing.IngredientName
+ @(ing.Quantity % 1 == 0 ? ((int)ing.Quantity).ToString() : ing.Quantity.ToString("0.##")) @ing.Unit
+
+ }
+
+ }
@if (isExpanded && !string.IsNullOrEmpty(recipe.Instructions))
{
@recipe.Instructions
@@ -102,8 +131,10 @@ else
// EN: Recipes state / VI: Trạng thái công thức
private List _recipes = new();
private List _inventoryItems = new();
+ private List _products = new();
private bool _showRecipeForm;
private Guid? _editingRecipeId;
+ private Guid _selectedProductId = Guid.Empty;
private string _newRecipeName = "";
private string _newRecipeInstructions = "";
private int _newRecipePrepTime = 5;
@@ -117,13 +148,15 @@ else
{
if (ShopId != Guid.Empty)
{
- // EN: Load recipes and inventory items in parallel.
- // VI: Tải công thức và mặt hàng tồn kho song song.
+ // EN: Load recipes, inventory items, and products in parallel.
+ // VI: Tải công thức, mặt hàng tồn kho và sản phẩm song song.
var recipesTask = DataService.GetRecipesAsync(ShopId);
var inventoryTask = DataService.GetInventoryAsync(ShopId);
- await Task.WhenAll(recipesTask, inventoryTask);
+ var productsTask = DataService.GetProductsAsync(ShopId);
+ await Task.WhenAll(recipesTask, inventoryTask, productsTask);
_recipes = recipesTask.Result;
_inventoryItems = inventoryTask.Result;
+ _products = productsTask.Result;
}
}
@@ -146,6 +179,26 @@ else
_recipeIngredients[index].Cost = item.CostPerUnit ?? 0;
}
+ private void StartEditRecipe(PosDataService.RecipeInfo recipe)
+ {
+ _editingRecipeId = recipe.Id;
+ _selectedProductId = recipe.ProductId;
+ _newRecipeName = recipe.Name;
+ _newRecipeInstructions = recipe.Instructions ?? "";
+ _newRecipePrepTime = recipe.PrepTimeMinutes;
+ _recipeIngredients = recipe.Ingredients?.Select(ing => new IngredientRow
+ {
+ Name = ing.IngredientName,
+ Unit = ing.Unit,
+ Quantity = ing.Quantity,
+ Cost = ing.CostPerUnit,
+ InventoryItemId = ing.InventoryItemId,
+ QuantityPerServing = ing.QuantityPerServing
+ }).ToList() ?? new();
+ _showRecipeForm = true;
+ _recipeFormMessage = null;
+ }
+
// ═══ RECIPE CRUD ═══
private async Task SaveRecipe()
{
@@ -160,7 +213,11 @@ else
.Where(i => !string.IsNullOrWhiteSpace(i.Name))
.Select(i => new PosDataService.RecipeIngredientRequest(i.Name, i.Quantity, i.Unit, i.Cost, i.InventoryItemId, i.QuantityPerServing))
.ToList();
- var req = new PosDataService.CreateRecipeRequest(ShopId, Guid.Empty, _newRecipeName, _newRecipeInstructions, _newRecipePrepTime, ingredients);
+ if (_selectedProductId == Guid.Empty)
+ {
+ _recipeFormMessage = "Vui lòng chọn sản phẩm liên kết."; _recipeFormSuccess = false; return;
+ }
+ var req = new PosDataService.CreateRecipeRequest(ShopId, _selectedProductId, _newRecipeName, _newRecipeInstructions, _newRecipePrepTime, ingredients);
bool ok;
if (_editingRecipeId.HasValue)
ok = await DataService.UpdateRecipeAsync(_editingRecipeId.Value, req);
diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs
index 51a6e2f3..a715785c 100644
--- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs
@@ -944,7 +944,8 @@ public class PosDataService
// VI: Thông tin nguyên liệu công thức — bao gồm liên kết tùy chọn đến mặt hàng tồn kho để tính giá vốn.
public record RecipeIngredientInfo(Guid Id, Guid RecipeId, string IngredientName, decimal Quantity, string Unit, decimal CostPerUnit,
Guid? InventoryItemId = null, decimal QuantityPerServing = 0);
- public record RecipeInfo(Guid Id, Guid ProductId, Guid ShopId, string Name, string? Instructions, int PrepTimeMinutes, bool IsActive, DateTime CreatedAt);
+ public record RecipeInfo(Guid Id, Guid ProductId, Guid ShopId, string Name, string? Instructions, int PrepTimeMinutes, bool IsActive, DateTime CreatedAt,
+ List? Ingredients = null);
public record CreateRecipeRequest(Guid ShopId, Guid ProductId, string Name, string? Instructions, int PrepTimeMinutes, List? Ingredients);
public record RecipeIngredientRequest(string IngredientName, decimal Quantity, string Unit, decimal CostPerUnit,
Guid? InventoryItemId = null, decimal QuantityPerServing = 0);