diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPage.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPage.razor
index 7e4bda8f..c346c32e 100644
--- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPage.razor
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPage.razor
@@ -1257,8 +1257,8 @@
Nguyên liệu
- @foreach (var (ing, idx) in _recipeIngredients.Select((x,i) => (x,i)))
+ @for (var idx = 0; idx < _recipeIngredients.Count; idx++)
{
+ var i = idx;
-
-
-
-
-
+ { var t = _recipeIngredients[i]; _recipeIngredients[i] = (e.Value?.ToString() ?? "", t.Unit, t.Qty, t.Quantity, t.Cost); })" placeholder="Tên nguyên liệu" 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;" />
+ { var t = _recipeIngredients[i]; _recipeIngredients[i] = (t.Name, t.Unit, t.Qty, decimal.TryParse(e.Value?.ToString(), out var v) ? v : 0, t.Cost); })" type="number" placeholder="Qty" 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;" />
+ { var t = _recipeIngredients[i]; _recipeIngredients[i] = (t.Name, e.Value?.ToString() ?? "", t.Qty, t.Quantity, t.Cost); })" placeholder="Đơn vị" 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;" />
+ { var t = _recipeIngredients[i]; _recipeIngredients[i] = (t.Name, t.Unit, t.Qty, t.Quantity, decimal.TryParse(e.Value?.ToString(), out var v) ? v : 0); })" type="number" placeholder="Chi phí" 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;" />
+
}
@@ -2503,4 +2504,234 @@
await DataService.DeleteMemberAsync(memberId);
_members = await DataService.GetMembersAsync();
}
+
+ // ═══ TABLE CRUD ═══
+ private void EditTable(PosDataService.TableInfo table)
+ {
+ _editingTableId = table.Id;
+ _newTableNumber = table.TableNumber;
+ _newTableCapacity = table.Capacity;
+ _newTableZone = table.Zone ?? "";
+ _tableFormMessage = null;
+ _showTableForm = true;
+ }
+
+ private async Task AddTable()
+ {
+ _tableFormMessage = null;
+ if (string.IsNullOrWhiteSpace(_newTableNumber) || !_shopGuid.HasValue)
+ {
+ _tableFormMessage = "Vui lòng nhập số bàn."; _tableFormSuccess = false; return;
+ }
+ try
+ {
+ await DataService.CreateTableAsync(new PosDataService.CreateTableRequest(_shopGuid.Value, _newTableNumber, _newTableCapacity, _newTableZone));
+ _tableFormMessage = $"Đã thêm bàn '{_newTableNumber}' thành công!"; _tableFormSuccess = true;
+ _newTableNumber = ""; _newTableCapacity = 4; _newTableZone = "";
+ if (_shopGuid.HasValue) _tables = await DataService.GetTablesAsync(_shopGuid.Value);
+ }
+ catch (Exception ex) { _tableFormMessage = $"Lỗi: {ex.Message}"; _tableFormSuccess = false; }
+ }
+
+ private async Task SaveTable()
+ {
+ _tableFormMessage = null;
+ if (string.IsNullOrWhiteSpace(_newTableNumber) || !_shopGuid.HasValue || !_editingTableId.HasValue)
+ {
+ _tableFormMessage = "Vui lòng nhập số bàn."; _tableFormSuccess = false; return;
+ }
+ try
+ {
+ await DataService.UpdateTableAsync(_editingTableId.Value, new PosDataService.CreateTableRequest(_shopGuid.Value, _newTableNumber, _newTableCapacity, _newTableZone));
+ _tableFormMessage = $"Đã cập nhật bàn '{_newTableNumber}' thành công!"; _tableFormSuccess = true;
+ _editingTableId = null;
+ if (_shopGuid.HasValue) _tables = await DataService.GetTablesAsync(_shopGuid.Value);
+ }
+ catch (Exception ex) { _tableFormMessage = $"Lỗi: {ex.Message}"; _tableFormSuccess = false; }
+ }
+
+ private async Task DeleteTableItem(Guid id)
+ {
+ try
+ {
+ await DataService.DeleteTableAsync(id);
+ if (_shopGuid.HasValue) _tables = await DataService.GetTablesAsync(_shopGuid.Value);
+ }
+ catch (Exception ex) { _errorMessage = $"Không thể xóa bàn: {ex.Message}"; }
+ }
+
+ // ═══ APPOINTMENT CRUD ═══
+ private async Task AddAppointment()
+ {
+ _apptFormMessage = null;
+ if (!_shopGuid.HasValue)
+ {
+ _apptFormMessage = "Thiếu thông tin cửa hàng."; _apptFormSuccess = false; return;
+ }
+ try
+ {
+ await DataService.CreateAppointmentAsync(new PosDataService.CreateAppointmentRequest(
+ _shopGuid.Value, null, null, null, null, _newApptStart, _newApptEnd));
+ _apptFormMessage = "Đã thêm lịch hẹn thành công!"; _apptFormSuccess = true;
+ _showApptForm = false;
+ _appointments = await DataService.GetAppointmentsAsync(_shopGuid.Value);
+ }
+ catch (Exception ex) { _apptFormMessage = $"Lỗi: {ex.Message}"; _apptFormSuccess = false; }
+ }
+
+ private async Task CancelAppt(Guid apptId)
+ {
+ try
+ {
+ await DataService.CancelAppointmentAsync(apptId);
+ if (_shopGuid.HasValue) _appointments = await DataService.GetAppointmentsAsync(_shopGuid.Value);
+ }
+ catch (Exception ex) { _errorMessage = $"Không thể hủy lịch hẹn: {ex.Message}"; }
+ }
+
+ // ═══ RESOURCE CRUD ═══
+ private void EditResource(PosDataService.ResourceInfo r)
+ {
+ _editingResourceId = r.Id;
+ _newResourceName = r.Name;
+ _newResourceType = r.ResourceType ?? "Room";
+ _newResourceCapacity = r.Capacity;
+ _resourceFormMessage = null;
+ _showResourceForm = true;
+ }
+
+ private async Task AddResource()
+ {
+ _resourceFormMessage = null;
+ if (string.IsNullOrWhiteSpace(_newResourceName) || !_shopGuid.HasValue)
+ {
+ _resourceFormMessage = "Vui lòng nhập tên tài nguyên."; _resourceFormSuccess = false; return;
+ }
+ try
+ {
+ await DataService.CreateResourceAsync(new PosDataService.CreateResourceRequest(_shopGuid.Value, _newResourceName, _newResourceType, _newResourceCapacity));
+ _resourceFormMessage = $"Đã thêm '{_newResourceName}' thành công!"; _resourceFormSuccess = true;
+ _newResourceName = ""; _newResourceType = "Room"; _newResourceCapacity = 1;
+ _resources = await DataService.GetResourcesAsync(_shopGuid.Value);
+ }
+ catch (Exception ex) { _resourceFormMessage = $"Lỗi: {ex.Message}"; _resourceFormSuccess = false; }
+ }
+
+ private async Task SaveResource()
+ {
+ _resourceFormMessage = null;
+ if (string.IsNullOrWhiteSpace(_newResourceName) || !_shopGuid.HasValue || !_editingResourceId.HasValue)
+ {
+ _resourceFormMessage = "Vui lòng nhập tên tài nguyên."; _resourceFormSuccess = false; return;
+ }
+ try
+ {
+ await DataService.UpdateResourceAsync(_editingResourceId.Value, new PosDataService.CreateResourceRequest(_shopGuid.Value, _newResourceName, _newResourceType, _newResourceCapacity));
+ _resourceFormMessage = $"Đã cập nhật '{_newResourceName}' thành công!"; _resourceFormSuccess = true;
+ _editingResourceId = null;
+ _resources = await DataService.GetResourcesAsync(_shopGuid.Value);
+ }
+ catch (Exception ex) { _resourceFormMessage = $"Lỗi: {ex.Message}"; _resourceFormSuccess = false; }
+ }
+
+ private async Task DeleteResourceItem(Guid id)
+ {
+ try
+ {
+ await DataService.DeleteResourceAsync(id);
+ if (_shopGuid.HasValue) _resources = await DataService.GetResourcesAsync(_shopGuid.Value);
+ }
+ catch (Exception ex) { _errorMessage = $"Không thể xóa tài nguyên: {ex.Message}"; }
+ }
+
+ // ═══ SCHEDULE CRUD ═══
+ private async Task AddSchedule()
+ {
+ _schedFormMessage = null;
+ if (!Guid.TryParse(_newSchedStaffIdStr, out var staffId) || !_shopGuid.HasValue)
+ {
+ _schedFormMessage = "Vui lòng nhập đúng Staff ID."; _schedFormSuccess = false; return;
+ }
+ try
+ {
+ _newSchedStaffId = staffId;
+ await DataService.CreateScheduleAsync(new PosDataService.CreateScheduleRequest(_shopGuid.Value, _newSchedStaffId, _newSchedDay, _newSchedStart, _newSchedEnd));
+ _schedFormMessage = "Đã thêm lịch làm việc thành công!"; _schedFormSuccess = true;
+ _showScheduleForm = false;
+ _staffSchedules = await DataService.GetStaffSchedulesAsync(_shopGuid);
+ }
+ catch (Exception ex) { _schedFormMessage = $"Lỗi: {ex.Message}"; _schedFormSuccess = false; }
+ }
+
+ private async Task DeleteScheduleItem(Guid id)
+ {
+ try
+ {
+ await DataService.DeleteScheduleAsync(id);
+ _staffSchedules = await DataService.GetStaffSchedulesAsync(_shopGuid);
+ }
+ catch (Exception ex) { _errorMessage = $"Không thể xóa lịch làm việc: {ex.Message}"; }
+ }
+
+ // ═══ KITCHEN ═══
+ private async Task LoadKitchenTickets(string status)
+ {
+ _kitchenStatusFilter = status;
+ try
+ {
+ if (_shopGuid.HasValue)
+ _kitchenTickets = await DataService.GetKitchenTicketsAsync(_shopGuid, status);
+ }
+ catch (Exception ex) { _errorMessage = $"Không thể tải kitchen tickets: {ex.Message}"; }
+ StateHasChanged();
+ }
+
+ private async Task MarkTicketDone(Guid ticketId)
+ {
+ try
+ {
+ await DataService.UpdateTicketStatusAsync(ticketId, new PosDataService.UpdateTicketStatusRequest("completed"));
+ if (_shopGuid.HasValue)
+ _kitchenTickets = await DataService.GetKitchenTicketsAsync(_shopGuid, _kitchenStatusFilter);
+ }
+ catch (Exception ex) { _errorMessage = $"Không thể cập nhật trạng thái: {ex.Message}"; }
+ StateHasChanged();
+ }
+
+ // ═══ RECIPE CRUD ═══
+ private async Task SaveRecipe()
+ {
+ _recipeFormMessage = null;
+ if (string.IsNullOrWhiteSpace(_newRecipeName) || !_shopGuid.HasValue)
+ {
+ _recipeFormMessage = "Vui lòng nhập tên công thức."; _recipeFormSuccess = false; return;
+ }
+ try
+ {
+ var ingredients = _recipeIngredients
+ .Where(i => !string.IsNullOrWhiteSpace(i.Name))
+ .Select(i => new PosDataService.RecipeIngredientRequest(i.Name, i.Quantity, i.Unit, i.Cost))
+ .ToList();
+ var req = new PosDataService.CreateRecipeRequest(_shopGuid.Value, Guid.Empty, _newRecipeName, _newRecipeInstructions, _newRecipePrepTime, ingredients);
+ bool ok;
+ if (_editingRecipeId.HasValue)
+ ok = await DataService.UpdateRecipeAsync(_editingRecipeId.Value, req);
+ else
+ ok = await DataService.CreateRecipeAsync(req);
+ _recipeFormMessage = ok ? (_editingRecipeId.HasValue ? "Đã cập nhật công thức!" : "Đã thêm công thức!") : "Lỗi khi lưu công thức.";
+ _recipeFormSuccess = ok;
+ if (ok) { _showRecipeForm = false; _editingRecipeId = null; _recipes = await DataService.GetRecipesAsync(_shopGuid); }
+ }
+ catch (Exception ex) { _recipeFormMessage = $"Lỗi: {ex.Message}"; _recipeFormSuccess = false; }
+ }
+
+ private async Task DeleteRecipeItem(Guid id)
+ {
+ try
+ {
+ await DataService.DeleteRecipeAsync(id);
+ _recipes = await DataService.GetRecipesAsync(_shopGuid);
+ }
+ catch (Exception ex) { _errorMessage = $"Không thể xóa công thức: {ex.Message}"; }
+ }
}
diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs
index 9c0f56d1..c81c442f 100644
--- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs
@@ -1689,8 +1689,8 @@ public class BffDataController : ControllerBase
if (merchantId == null) return Ok(Array.Empty