@@ -89,12 +91,11 @@
// ═══ OVERVIEW DASHBOARD ═══
case "overview":
- @* Vertical-specific overview KPIs *@
@if (_posVertical == "restaurant" && _ovTables.Any())
{
@@ -140,7 +141,7 @@
{
@o.Id.ToString()[..8]
- @FormatVND(o.TotalAmount)
+ @ShopHelpers.FormatVND(o.TotalAmount)
@o.CreatedAt.ToString("dd/MM HH:mm")
}
@@ -188,2405 +189,146 @@
break;
- // ═══ MENU / PRODUCTS ═══
+ // ═══ EXTRACTED COMPONENTS ═══
case "menu":
case "products":
-
+ break;
+
+ // ═══ SERVICES (reuses products, keep inline) ═══
+ case "services":
+ @if (!_serviceProducts.Any())
{
-
+ @RenderEmpty("sparkles", "#EC4899", "Chưa có dịch vụ", "Thêm dịch vụ trong mục Menu/Sản phẩm", "coffee", "Quản lý sản phẩm", $"/admin/shop/{ShopId}/menu")
}
- @if (!_products.Any())
- {
- @RenderEmpty("coffee", "#F59E0B", "Chưa có sản phẩm", "Thêm sản phẩm để bắt đầu bán hàng", "plus-circle", "Thêm sản phẩm", $"/admin/shop/{ShopId}/menu")
- }
- else if (_productView == "grid")
+ else
{
- @foreach (var p in PagedProducts)
+ @foreach (var p in _serviceProducts)
{
- var typeColor = (p.Type ?? "") switch { "Service" => "#EC4899", "Physical" => "#3B82F6", _ => "#F59E0B" };
- var typeLabel = (p.Type ?? "") switch { "Service" => "Dịch vụ", "Physical" => "Vật lý", _ => "Đồ uống" };
-
+
-
- EditProduct(p))" style="background:rgba(59,130,246,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Sửa">
- DeleteProduct(p.Id))" style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Xóa">
-
- @if (!string.IsNullOrWhiteSpace(p.ImageUrl))
- {
-
- }
- else
- {
-
- }
+
@p.Name
-
@(p.CategoryName ?? "—")
-
@typeLabel
@p.Price.ToString("N0")₫
}
}
- else
- {
- @* LIST VIEW *@
-
-
-
- Tên
- Danh mục
- Loại
- Giá
-
-
- @foreach (var p in PagedProducts)
- {
- var typeColor = (p.Type ?? "") switch { "Service" => "#EC4899", "Physical" => "#3B82F6", _ => "#F59E0B" };
- var typeLabel = (p.Type ?? "") switch { "Service" => "Dịch vụ", "Physical" => "Vật lý", _ => "Đồ uống" };
-
- @p.Name
- @(p.CategoryName ?? "—")
- @typeLabel
- @p.Price.ToString("N0")₫
-
-
- EditProduct(p))" style="background:rgba(59,130,246,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Sửa">
- DeleteProduct(p.Id))" style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Xóa">
-
-
-
- }
-
-
-
- }
- @* ═══ PAGINATION ═══ *@
- @if (ProductTotalPages > 1)
- {
-
- _productPage--)"
- style="padding:6px 12px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:var(--admin-bg-elevated);color:var(--admin-text-secondary);cursor:pointer;font-size:12px;">
-
-
- @for (var i = 1; i <= ProductTotalPages; i++)
- {
- var pg = i;
- _productPage = pg)"
- style="min-width:32px;height:32px;border-radius:8px;border:1px solid @(pg == _productPage ? "var(--admin-orange-primary)" : "var(--admin-border-subtle)");background:@(pg == _productPage ? "var(--admin-orange-primary)" : "var(--admin-bg-elevated)");color:@(pg == _productPage ? "#FFF" : "var(--admin-text-secondary)");cursor:pointer;font-size:12px;font-weight:600;">
- @pg
-
- }
- _productPage++)"
- style="padding:6px 12px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:var(--admin-bg-elevated);color:var(--admin-text-secondary);cursor:pointer;font-size:12px;">
-
-
- Trang @_productPage / @ProductTotalPages
-
- }
- @* Categories Management *@
-
-
- @if (_showCategoryForm)
- {
-
-
@(_editingCategoryId.HasValue ? "Chỉnh sửa danh mục" : "Thêm danh mục mới")
-
-
Tên
-
Mô tả
-
Hình ảnh
-
- @if (_categoryImagePreview != null) {
}
-
-
-
-
-
- @(_editingCategoryId.HasValue ? "Cập nhật" : "Lưu")
- _showCategoryForm = false)" style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-secondary);cursor:pointer;"> Hủy
-
- @if (_categoryFormMessage != null) {
@_categoryFormMessage
}
-
- }
-
-
-
- TÊN MÔ TẢ THỨ TỰ HÀNH ĐỘNG
-
-
- @foreach (var c in _categories)
- {
-
- @c.Name
- @c.Description
- @c.DisplayOrder
-
-
- EditCategory(c))" style="background:rgba(59,130,246,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Sửa">
- DeleteCategoryItem(c.Id))" style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Xóa">
-
-
-
- }
-
-
- @if (!_categories.Any()) {
Chưa có danh mục. Nhấn "Thêm danh mục" để tạo mới.
}
-
-
break;
- // ═══ INVENTORY ═══
- case "inventory":
- @if (!_inventory.Any() && _invSubTab == "levels")
- {
- @RenderEmpty("warehouse", "#3B82F6", "Chưa có tồn kho", "Tồn kho sẽ hiển thị khi có sản phẩm", "package", "Thêm sản phẩm trước", $"/admin/shop/{ShopId}/menu")
- }
- else
- {
-
-
-
@_inventory.Count(i => i.Quantity > 10) Còn hàng
-
@_inventory.Count(i => i.Quantity > 0 && i.Quantity <= 10) Sắp hết
-
@_inventory.Count(i => i.Quantity <= 0) Hết hàng
-
-
-
-
- @foreach (var (label, val, icon) in new[] { ("Tồn kho", "levels", "package"), ("Nhập kho", "stock-in", "arrow-down-to-line"), ("Xuất kho", "stock-out", "arrow-up-from-line"), ("Điều chỉnh", "adjust", "settings-2"), ("Lịch sử", "transactions", "history"), ("Cảnh báo", "low-stock", "alert-triangle") })
- {
- SwitchInvSubTab(val))"
- style="padding:8px 14px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;border:none;display:flex;align-items:center;gap:6px;
- @(_invSubTab == val ? "background:var(--admin-orange-primary);color:#FFF;box-shadow:0 1px 3px rgba(0,0,0,0.1);" : "background:transparent;color:var(--admin-text-tertiary);")">
- @label
-
- }
-
-
- @if (_invFormMessage != null)
- {
-
- @_invFormMessage
-
- }
-
- @switch (_invSubTab)
- {
- case "levels":
-
-
-
- Sản phẩm
- Số lượng
- Mức nhập lại
- Thao tác
-
- @foreach (var item in _inventory)
- {
- var qtyColor = item.Quantity <= 0 ? "#EF4444" : item.Quantity <= (item.ReorderLevel > 0 ? item.ReorderLevel : 10) ? "#F59E0B" : "#22C55E";
- var bgColor = item.Quantity <= 0 ? "rgba(239,68,68,0.05)" : item.Quantity <= (item.ReorderLevel > 0 ? item.ReorderLevel : 10) ? "rgba(245,158,11,0.05)" : "transparent";
-
- @(item.ProductName ?? item.ProductId.ToString()[..8])
- @item.Quantity
- @item.ReorderLevel
-
-
- { _invSubTab = "stock-in"; _invSelectedProductId = item.ProductId; _invAmount = 0; _invNotes = ""; StateHasChanged(); })"
- style="background:rgba(34,197,94,0.1);border:none;border-radius:6px;padding:4px 10px;font-size:11px;font-weight:600;color:#16A34A;cursor:pointer;">+Nhập
- { _invSubTab = "stock-out"; _invSelectedProductId = item.ProductId; _invAmount = 0; _invNotes = ""; StateHasChanged(); })"
- style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;padding:4px 10px;font-size:11px;font-weight:600;color:#DC2626;cursor:pointer;">-Xuất
-
-
-
- }
-
-
-
- break;
-
- case "stock-in":
-
-
-
-
- Sản phẩm *
-
- -- Chọn sản phẩm --
- @foreach (var item in _inventory)
- {
- @(item.ProductName ?? item.ProductId.ToString()[..8]) (Tồn: @item.Quantity)
- }
-
-
-
- Số lượng nhập *
-
-
-
- Ghi chú
-
-
-
- Nhập kho
-
-
-
- break;
-
- case "stock-out":
-
-
-
-
- Sản phẩm *
-
- -- Chọn sản phẩm --
- @foreach (var item in _inventory)
- {
- @(item.ProductName ?? item.ProductId.ToString()[..8]) (Tồn: @item.Quantity)
- }
-
-
-
- Số lượng xuất *
-
-
-
- Ghi chú
-
-
-
- Xuất kho
-
-
-
- break;
-
- case "adjust":
-
-
-
-
- Sản phẩm *
-
- -- Chọn sản phẩm --
- @foreach (var item in _inventory)
- {
- @(item.ProductName ?? item.ProductId.ToString()[..8]) (Tồn: @item.Quantity)
- }
-
-
-
- Số lượng mới *
-
-
-
- Lý do *
-
-
-
- Điều chỉnh
-
-
-
- break;
-
- case "transactions":
-
-
-
- @if (_invTxns.Any())
- {
-
- Thời gian
- Loại
- Số lượng
- Lý do
-
- @foreach (var tx in _invTxns.OrderByDescending(t => t.CreatedAt).Take(50))
- {
- var txColor = tx.QuantityChange > 0 ? "#22C55E" : tx.QuantityChange < 0 ? "#EF4444" : "#6B7280";
- var txLabel = tx.TransactionType switch { "StockIn" => "Nhập kho", "StockOut" => "Xuất kho", "Adjustment" => "Điều chỉnh", "OrderDeduction" => "Đơn hàng", _ => tx.TransactionType ?? "N/A" };
-
- @tx.CreatedAt.ToLocalTime().ToString("dd/MM HH:mm")
-
- @txLabel
-
- @(tx.QuantityChange > 0 ? "+" : "")@tx.QuantityChange
- @(tx.Reason ?? "—")
-
- }
-
- }
- else
- {
-
Chưa có giao dịch kho nào.
- }
-
-
- break;
-
- case "low-stock":
-
-
-
- @if (_lowStockItems.Any())
- {
-
- Sản phẩm
- Tồn kho
- Ngưỡng
- Hành động
-
- @foreach (var item in _lowStockItems)
- {
-
- @(item.ProductName ?? item.ProductId.ToString()[..8])
- @item.Quantity
- @item.LowStockThreshold
-
- { _invSubTab = "stock-in"; _invSelectedProductId = item.ProductId; _invAmount = item.LowStockThreshold * 2; _invNotes = "Bổ sung hàng tồn kho thấp"; StateHasChanged(); })"
- style="background:rgba(34,197,94,0.1);border:none;border-radius:6px;padding:4px 12px;font-size:11px;font-weight:600;color:#16A34A;cursor:pointer;">
- Nhập kho nhanh
-
-
-
- }
-
- }
- else
- {
-
-
- Tất cả sản phẩm đều đủ hàng!
-
- }
-
-
- break;
- }
- }
- break;
-
- // ═══ FINANCE ═══
- case "finance":
-
-
- Đơn hàng theo cửa hàng · Ví tiền chung cho tài khoản
-
- var finOrders = _financePeriod switch {
- "7d" => _orders.Where(o => o.CreatedAt >= DateTime.UtcNow.AddDays(-7)).ToList(),
- "30d" => _orders.Where(o => o.CreatedAt >= DateTime.UtcNow.AddDays(-30)).ToList(),
- _ => _orders };
-
-
Tài chính
-
- @foreach (var (label, val) in new[] { ("7 ngày", "7d"), ("30 ngày", "30d"), ("Tất cả", "all") })
- {
- { _financePeriod = val; StateHasChanged(); })"
- style="padding:6px 14px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;border:none;
- background:@(_financePeriod == val ? "var(--admin-orange-primary)" : "transparent");
- color:@(_financePeriod == val ? "#FFF" : "var(--admin-text-tertiary)");">
- @label
-
- }
-
-
-
-
@FormatVND(finOrders.Sum(o => o.TotalAmount)) Tổng doanh thu
-
-
@FormatVND(finOrders.Any() ? finOrders.Average(o => o.TotalAmount) : 0) TB/đơn
-
@FormatVND(_wallets.Sum(w => w.Balance)) Số dư ví
-
- @if (_walletTxns.Any())
- {
-
-
-
-
- Mô tả
- Số tiền
- Ngày
-
- @foreach (var t in _walletTxns.Take(15))
- {
-
- @(t.Description ?? t.ItemName ?? "—")
- @(t.Amount >= 0 ? "+" : "")@FormatVND(t.Amount)
- @t.CreatedAt.ToString("dd/MM HH:mm")
-
- }
-
-
-
- }
- @if (_orders.Any())
- {
-
-
-
-
- ID
- Số tiền
- Trạng thái
- Ngày
-
-
- @foreach (var o in _orders.Take(20))
- {
- var isOrderExpanded = _selectedOrderId == o.Id;
- ViewOrderDetail(o.Id))">
- @o.Id.ToString()[..8]
- @FormatVND(o.TotalAmount)
- @(o.Status ?? "—")
- @o.CreatedAt.ToString("dd/MM HH:mm")
-
-
- @if (isOrderExpanded && _orderDetail != null)
- {
-
-
-
-
💳 Thanh toán @(_orderDetail.Order?.PaymentMethod ?? "—")
-
📝 Ghi chú @(_orderDetail.Order?.Notes ?? "—")
-
🕐 Thời gian @(_orderDetail.Order?.CreatedAt.ToString("dd/MM/yyyy HH:mm") ?? "—")
-
- @if (_orderDetail.Items?.Any() == true)
- {
-
- Sản phẩm SL Đơn giá Thành tiền
-
- @foreach (var item in _orderDetail.Items)
- {
-
- @(item.ProductName ?? "—")
- @item.Quantity
- @FormatVND(item.UnitPrice)
- @FormatVND(item.Subtotal)
-
- }
-
- }
-
- CancelOrderItem(o.Id))" style="padding:6px 14px;border-radius:8px;border:1px solid rgba(239,68,68,0.3);background:rgba(239,68,68,0.1);color:#EF4444;font-size:12px;cursor:pointer;display:flex;align-items:center;gap:6px;">
- Hủy đơn
-
-
-
-
- }
- }
-
-
-
- }
- else
- {
- @RenderEmpty("bar-chart-3", "#22C55E", "Chưa có dữ liệu tài chính", "Dữ liệu sẽ tự động cập nhật khi có đơn hàng", "monitor", "Mở POS bán hàng", $"/pos/{ShopId}/{_posVertical}")
- }
- break;
-
- // ═══ STAFF ═══
- case "staff":
-
-
@(_staff.Count) nhân viên
- { _editingStaffId = null; _newStaffCode = ""; _newStaffPhone = ""; _newStaffEmail = ""; _newStaffRole = "Cashier"; _staffFormMessage = null; _createStaffAccount = false; _newStaffFirstName = ""; _newStaffLastName = ""; _newStaffPassword = ""; _newStaffAddress = ""; _staffDocFrontFile = null; _staffDocBackFile = null; _staffDocFrontPreview = null; _staffDocBackPreview = null; _showStaffForm = !_showStaffForm; })">
-
- Thêm nhân viên
-
-
- @if (_showStaffForm)
- {
-
-
-
-
-
-
CCCD / Giấy tờ tùy thân
-
-
-
Mặt trước
- @if (_staffDocFrontPreview != null)
- {
-
- }
-
-
-
-
Mặt sau
- @if (_staffDocBackPreview != null)
- {
-
- }
-
-
-
-
- @if (!_editingStaffId.HasValue)
- {
-
-
- Tạo tài khoản đăng nhập (IAM)
-
- @if (_createStaffAccount)
- {
-
- }
-
- }
-
- @(_editingStaffId.HasValue ? "Cập nhật" : "Lưu")
- _showStaffForm = false)" style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-secondary);cursor:pointer;"> Hủy
-
- @if (!string.IsNullOrEmpty(_staffFormMessage))
- {
-
@_staffFormMessage
- }
-
-
- }
- @if (!_staff.Any() && !_showStaffForm)
- {
-
-
-
-
-
Chưa có nhân viên
-
Thêm nhân viên để quản lý cửa hàng
-
{ _editingStaffId = null; _newStaffCode = ""; _newStaffPhone = ""; _newStaffEmail = ""; _newStaffRole = "Cashier"; _staffFormMessage = null; _createStaffAccount = false; _showStaffForm = true; })">
- Thêm nhân viên
-
-
- }
- else if (_staff.Any())
- {
-
-
@_staff.Count(s => s.Status == "Active") Đang hoạt động
-
@_staff.Count Tổng nhân viên
-
-
-
-
- Nhân viên
- Mã NV
- Vai trò
- Trạng thái
- SĐT
- Hành động
-
- @foreach (var s in _staff)
- {
- var staffDisplayName = !string.IsNullOrWhiteSpace(s.LastName) || !string.IsNullOrWhiteSpace(s.FirstName) ? $"{s.LastName} {s.FirstName}".Trim() : null;
-
- @(staffDisplayName ?? s.EmployeeCode ?? s.Id.ToString()[..6])
- @(s.EmployeeCode ?? "—")
- @(s.Role ?? "—")
- @(s.Status ?? "—")
- @(s.Phone ?? s.Email ?? "—")
-
-
- EditStaff(s))" style="background:rgba(59,130,246,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Sửa">
- DeleteStaffMember(s.Id))" style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Xóa">
-
-
-
- }
-
-
-
- }
- break;
-
- // ═══ CUSTOMERS + MEMBERSHIP LEVELS ═══
- case "customers":
-
-
- Dữ liệu khách hàng chung cho tất cả cửa hàng trong thương hiệu
-
-
- @foreach (var (tab, label) in new[] { ("members","Khách hàng"), ("levels","Cấp bậc"), ("exp","Điểm EXP") })
- {
- _customerSubTab = tab)"
- style="padding:8px 16px;border-radius:8px 8px 0 0;border:none;font-size:13px;font-weight:600;cursor:pointer;background:@(_customerSubTab == tab ? "var(--admin-orange-primary)" : "transparent");color:@(_customerSubTab == tab ? "#FFF" : "var(--admin-text-secondary)");">
- @label
-
- }
-
- @if (_customerSubTab == "levels")
- {
- @* ─── Level CRUD ─── *@
-
-
@_memberLevels.Count cấp bậc
- { _showLevelForm = !_showLevelForm; _editingLevelId = null; _newLevelNumber = _memberLevels.Count + 1; _newLevelName = ""; _newLevelRequiredExp = 0; _newLevelDescription = ""; _newLevelBadgeColor = "#CD7F32"; }'>
- Thêm cấp bậc
-
-
- @if (_showLevelForm)
- {
-
-
-
-
- @if (_levelFormMessage != null)
- {
-
@_levelFormMessage
- }
-
- Lưu
- _showLevelForm = false">Hủy
-
-
-
- }
-
-
-
- Level
- Tên
- EXP cần
- Mô tả
- Màu
- Thành viên
-
-
- @foreach (var lvl in _memberLevels.OrderBy(l => l.Level))
- {
-
- @lvl.Level
- @lvl.Name
- @lvl.RequiredExp.ToString("N0")
- @(lvl.Description ?? "—")
-
- @lvl.MemberCount
-
- EditLevel(lvl))" style="padding:4px 8px;border-radius:6px;border:none;background:rgba(59,130,246,0.1);color:#3B82F6;font-size:11px;cursor:pointer;margin-right:4px;">Sửa
- DeleteLevel(lvl.Id))" style="padding:4px 8px;border-radius:6px;border:none;background:rgba(239,68,68,0.1);color:#EF4444;font-size:11px;cursor:pointer;">Xóa
-
-
- }
-
-
-
- }
- else if (_customerSubTab == "exp")
- {
- @* ─── EXP Management ─── *@
-
-
-
-
-
Thành viên
-
- -- Chọn --
- @foreach (var m in _members)
- {
- @(m.DisplayName ?? m.Id.ToString()[..8]) (@m.TotalExpEarned EXP)
- }
-
-
-
Số điểm
-
Nguồn
-
- Mua hàng Giới thiệu Hoạt động
- Khuyến mãi Đánh giá Check-in Admin
-
-
-
- @if (_expFormMessage != null)
- {
-
@_expFormMessage
- }
-
Cộng EXP
-
-
- @if (_memberProgress != null)
- {
-
-
-
-
-
EXP hiện tại @_memberProgress.CurrentExp.ToString("N0")
-
Tổng EXP @_memberProgress.TotalExpEarned.ToString("N0")
-
Cần thêm @_memberProgress.ExpToNextLevel.ToString("N0")
-
Level kế @(_memberProgress.NextLevelName ?? "Max")
-
-
-
@_memberProgress.ProgressPercent%
-
-
- }
- @if (_expHistory.Any())
- {
-
-
-
-
- Ngày
- Điểm
- Nguồn
- Tham chiếu
- Level
-
- @foreach (var tx in _expHistory)
- {
-
- @tx.CreatedAt.ToString("dd/MM/yyyy HH:mm")
- @(tx.Points > 0 ? "+" : "")@tx.Points
- @(tx.Source ?? "—")
- @(tx.ReferenceId ?? "—")
- @tx.LevelAtTime
-
- }
-
-
-
- }
- }
- else
- {
- @* ─── Members list (existing) ─── *@
-
-
@_members.Count khách hàng
-
-
-
-
-
-
{ _showMemberForm = !_showMemberForm; _editingMemberId = null; _newMemberGender = ""; _newMemberCountry = "VN"; _newMemberName = ""; _newMemberPhone = ""; }'>
- Thêm khách hàng
-
-
-
- @if (_showMemberForm)
- {
-
-
-
-
-
Tên khách hàng *
-
Số điện thoại
-
Giới tính
-
- -- Chọn --
- Nam
- Nữ
- Khác
-
-
-
Mã quốc gia
-
- @if (_memberFormMessage != null)
- {
-
@_memberFormMessage
- }
-
- Lưu
- { _showMemberForm = false; _memberFormMessage = null; }'>Hủy
-
-
-
- }
- var filteredMembers = string.IsNullOrWhiteSpace(_customerSearch)
- ? _members
- : _members.Where(m => m.Id.ToString().Contains(_customerSearch, StringComparison.OrdinalIgnoreCase)
- || (m.DisplayName ?? "").Contains(_customerSearch, StringComparison.OrdinalIgnoreCase)
- || (m.Phone ?? "").Contains(_customerSearch, StringComparison.OrdinalIgnoreCase)
- || (m.LevelName ?? "").Contains(_customerSearch, StringComparison.OrdinalIgnoreCase)).ToList();
- @if (!filteredMembers.Any())
- {
- @RenderEmpty("heart", "#EF4444", "Chưa có khách hàng", "Khách hàng sẽ hiển thị khi có giao dịch", "monitor", "Mở POS bán hàng", $"/pos/{ShopId}/{_posVertical}")
- }
- else
- {
-
-
@filteredMembers.Count Tổng khách hàng
-
@_memberLevels.Count Cấp bậc
-
@(filteredMembers.Any() ? filteredMembers.Max(m => m.TotalExpEarned).ToString("N0") : "0") EXP cao nhất
-
- @if (_memberLevels.Any())
- {
-
-
-
-
- Level
- Tên
- EXP cần
- Thành viên
-
- @foreach (var lvl in _memberLevels.OrderBy(l => l.Level))
- {
-
- @lvl.Level
- @lvl.Name
- @lvl.MinExp.ToString("N0") — @lvl.MaxExp.ToString("N0")
- @lvl.MemberCount
-
- }
-
-
-
- }
-
-
-
-
- Tên
- SĐT
- Cấp bậc
- EXP
- Ngày tham gia
-
-
- @foreach (var m in filteredMembers)
- {
- var isExpanded = _selectedCustomerId == m.Id;
- { _selectedCustomerId = isExpanded ? null : m.Id; StateHasChanged(); }'>
- @(m.DisplayName ?? m.Id.ToString()[..8])
- @(m.Phone ?? "—")
- @(m.LevelName ?? "—")
- @m.TotalExpEarned.ToString("N0")
- @m.CreatedAt.ToString("dd/MM/yyyy")
-
-
- @if (isExpanded)
- {
-
-
-
-
-
🆔 Mã KH
-
@m.Id.ToString()
-
-
-
⭐ Cấp bậc hiện tại
-
@(m.LevelName ?? "Chưa xếp hạng") • @m.TotalExpEarned.ToString("N0") EXP
-
-
-
📅 Tham gia từ
-
@m.CreatedAt.ToString("dd/MM/yyyy HH:mm")
-
-
-
-
- Tặng ưu đãi
-
-
- Gửi tin
-
-
- Lịch sử đơn
-
- EditMember(m))" style="padding:6px 14px;border-radius:8px;border:none;background:rgba(59,130,246,0.1);color:#3B82F6;font-size:12px;cursor:pointer;display:flex;align-items:center;gap:6px;">
- Sửa
-
- DeleteMemberItem(m.Id))" style="padding:6px 14px;border-radius:8px;border:none;background:rgba(239,68,68,0.1);color:#EF4444;font-size:12px;cursor:pointer;display:flex;align-items:center;gap:6px;">
- Xóa
-
-
-
-
- }
- }
-
-
-
- }
- }
- break;
-
- // ═══ TABLES (Restaurant) ═══
- case "tables":
-
-
- Trống: @_tables.Count(t => t.Status == "available")
- Đang dùng: @_tables.Count(t => t.Status == "occupied")
- Đã đặt: @_tables.Count(t => t.Status == "reserved")
-
-
{ _editingTableId = null; _newTableNumber = ""; _newTableCapacity = 4; _newTableZone = ""; _tableFormMessage = null; _showTableForm = !_showTableForm; }'>
- Thêm bàn
-
-
- @if (_showTableForm)
- {
-
-
-
-
-
Số bàn *
-
Sức chứa
-
Khu vực — Chưa chọn — @foreach (var z in AllZoneNames) { @z }
-
-
- @(_editingTableId.HasValue ? "Cập nhật" : "Lưu")
- _showTableForm = false' style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-secondary);cursor:pointer;"> Hủy
-
- @if (_tableFormMessage != null) {
@_tableFormMessage
}
-
-
- }
- @if (!_tables.Any())
- {
- @RenderEmpty("grid-3x3", "#F59E0B", "Chưa có bàn nào", "Thêm bàn để quản lý sơ đồ phục vụ")
- }
- else
- {
-
- @foreach (var table in _tables)
- {
- var bgColor = table.Status switch { "available" => "rgba(34,197,94,0.08)", "occupied" => "rgba(239,68,68,0.08)", "reserved" => "rgba(245,158,11,0.08)", _ => "rgba(107,107,111,0.08)" };
- var borderColor = table.Status switch { "available" => "rgba(34,197,94,0.3)", "occupied" => "rgba(239,68,68,0.3)", "reserved" => "rgba(245,158,11,0.3)", _ => "rgba(107,107,111,0.3)" };
- var statusColor = table.Status switch { "available" => "#22C55E", "occupied" => "#EF4444", "reserved" => "#F59E0B", _ => "#6B6B6F" };
- var statusText = table.Status switch { "available" => "Trống", "occupied" => "Đang dùng", "reserved" => "Đã đặt", "cleaning" => "Dọn dẹp", _ => table.Status };
-
-
- EditTable(table)' style="background:rgba(59,130,246,0.1);border:none;border-radius:6px;width:26px;height:26px;display:flex;align-items:center;justify-content:center;cursor:pointer;">
- DeleteTableItem(table.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;">
-
-
@table.TableNumber
-
@(table.Zone ?? "Chung") • @table.Capacity chỗ
-
-
- @statusText
-
- @if (table.SessionId.HasValue)
- {
-
- @table.GuestCount khách • @(table.StartedAt?.ToString("HH:mm") ?? "—")
-
- }
-
- }
-
- }
- break;
-
- // ═══ ROOMS (Karaoke — reuse tables structure) ═══
- case "rooms":
- @if (!_tables.Any())
- {
- @RenderEmpty("door-open", "#8B5CF6", "Chưa có phòng nào", "Thêm phòng để quản lý Karaoke", "plus-circle", "Thêm phòng")
- }
- else
- {
-
-
- Trống: @_tables.Count(t => t.Status == "available")
- Đang hát: @_tables.Count(t => t.Status == "occupied")
- Đã đặt: @_tables.Count(t => t.Status == "reserved")
-
-
@_tables.Count phòng
-
-
- @foreach (var room in _tables)
- {
- var bgColor = room.Status switch { "available" => "rgba(139,92,246,0.08)", "occupied" => "rgba(239,68,68,0.08)", "reserved" => "rgba(245,158,11,0.08)", _ => "rgba(107,107,111,0.08)" };
- var borderColor = room.Status switch { "available" => "rgba(139,92,246,0.3)", "occupied" => "rgba(239,68,68,0.3)", "reserved" => "rgba(245,158,11,0.3)", _ => "rgba(107,107,111,0.3)" };
- var statusColor = room.Status switch { "available" => "#8B5CF6", "occupied" => "#EF4444", "reserved" => "#F59E0B", _ => "#6B6B6F" };
- var statusText = room.Status switch { "available" => "Trống", "occupied" => "Đang hát", "reserved" => "Đã đặt", "cleaning" => "Dọn dẹp", _ => room.Status };
- var roomType = (room.Zone ?? "").ToLower() switch { var z when z.Contains("vip") => ("VIP", "#F59E0B", 200000m), var z when z.Contains("party") => ("Party", "#EC4899", 350000m), _ => ("Standard", "#8B5CF6", 120000m) };
-
-
-
-
- Phòng @room.TableNumber
-
-
@roomType.Item1
-
-
@(room.Zone ?? "Chung") • @room.Capacity chỗ
-
@FormatVND(roomType.Item3)/giờ
-
-
- @statusText
-
- @if (room.SessionId.HasValue)
- {
- var elapsed = DateTime.UtcNow - (room.StartedAt ?? DateTime.UtcNow);
- var hours = Math.Max(1, (int)Math.Ceiling(elapsed.TotalHours));
- var bill = hours * roomType.Item3;
-
-
@room.GuestCount khách • Bắt đầu @(room.StartedAt?.ToString("HH:mm") ?? "—")
-
- @hours giờ
- @FormatVND(bill)
-
-
- }
-
- }
-
- }
- break;
-
- // ═══ APPOINTMENTS (Spa / Thẩm mỹ) — Calendar View + CRUD ═══
- case "appointments":
-
- { _showApptForm = !_showApptForm; _apptFormMessage = null; _newApptStart = DateTime.Today.AddHours(9); _newApptEnd = DateTime.Today.AddHours(10); }'>
- Thêm lịch hẹn
-
-
- @if (_showApptForm)
- {
-
-
-
-
-
- Lưu
- _showApptForm = false' style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-secondary);cursor:pointer;"> Hủy
-
- @if (_apptFormMessage != null) {
@_apptFormMessage
}
-
-
- }
- var calWeekStart = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + 1 + _calendarWeekOffset * 7);
- var calWeekEnd = calWeekStart.AddDays(7);
- var weekAppts = _appointments.Where(a => a.StartTime >= calWeekStart && a.StartTime < calWeekEnd).ToList();
-
-
-
@_appointments.Count Tổng lịch hẹn
-
@_appointments.Count(a => a.Status == "Confirmed") Đã xác nhận
-
@_appointments.Count(a => a.Status == "Pending") Chờ xác nhận
-
-
-
-
-
-
-
- @for (int d = 0; d < 7; d++)
- {
- var day = calWeekStart.AddDays(d);
- var dayAppts = weekAppts.Where(a => a.StartTime.Date == day.Date).OrderBy(a => a.StartTime).ToList();
- var isToday = day.Date == DateTime.Today;
-
-
-
@DayLabel((int)day.DayOfWeek)
-
@day.Day
-
- @foreach (var appt in dayAppts)
- {
- var apptColor = appt.Status switch { "Confirmed" => "#22C55E", "Pending" => "#F59E0B", "Completed" => "#3B82F6", _ => "#6B6B6F" };
-
-
@appt.StartTime.ToString("HH:mm")-@appt.EndTime.ToString("HH:mm")
-
@(appt.ResourceName ?? "—")
- @if (appt.Status != "Cancelled" && appt.Status != "Completed")
- {
-
CancelAppt(appt.Id)' style="margin-top:4px;padding:2px 6px;border-radius:4px;border:none;background:rgba(239,68,68,0.15);color:#EF4444;font-size:10px;cursor:pointer;">Hủy
- }
-
- }
- @if (!dayAppts.Any())
- {
-
—
- }
-
- }
-
-
-
- break;
-
- // ═══ SERVICES (Spa — products with type=Service) ═══
- case "services":
- @if (!_products.Any(p => p.Type == "Service"))
- {
- @RenderEmpty("sparkles", "#EC4899", "Chưa có dịch vụ", "Thêm dịch vụ để khách có thể đặt lịch", "plus-circle", "Thêm dịch vụ", $"/admin/shop/{ShopId}/menu")
- }
- else
- {
-
-
-
- Dịch vụ
- Giá
- Trạng thái
-
- @foreach (var s in _products.Where(p => p.Type == "Service"))
- {
-
- @s.Name
@(s.Description ?? "—")
- @FormatVND(s.Price)
- @(s.IsActive ? "Hoạt động" : "Tạm ngưng")
-
- }
-
-
-
- }
- break;
-
- // ═══ POS — Redirect prompt ═══
+ // ═══ POS redirect ═══
case "pos":
-
-
-
-
-
POS Bán hàng
-
Mở giao diện bán hàng tại điểm để tạo đơn, thanh toán và in hóa đơn.
-
-
- Mở POS
+
+
Mở POS bán hàng
+
+ Mở POS
break;
- // ═══ REPORTS — Enhanced with top products ═══
- case "reports":
-
-
@FormatVND(_reportOrders.Sum(o => o.TotalAmount)) Tổng doanh thu
-
@_reportOrders.Count Tổng đơn hàng
-
@FormatVND(_reportOrders.Any() ? _reportOrders.Average(o => o.TotalAmount) : 0) Giá trị TB / đơn
-
@_reportProducts.Count Sản phẩm
-
- @* Revenue Report *@
-
-
-
- @if (_revenueReport.Any())
- {
-
-
- KỲ ĐƠN HÀNG DOANH THU
-
-
- @foreach (var r in _revenueReport)
- {
-
- @r.Period.ToString("dd/MM/yyyy")
- @r.OrderCount
- @FormatVND(r.Revenue)
-
- }
-
-
- }
- else
- {
-
Nhấn Ngày / Tuần / Tháng để tải dữ liệu doanh thu.
- }
-
-
- @* ─── Top products from real order_items data ─── *@
- @if (_topProducts.Any())
- {
-
-
-
-
- #
- Tên SP
- Đã bán
- Doanh thu
-
- @{ var tpRank = 1; }
- @foreach (var tp in _topProducts)
- {
-
- @(tpRank++)
- @(tp.ProductName ?? "—")
- @tp.TotalSold
- @FormatVND(tp.TotalRevenue)
-
- }
-
-
-
- }
- @if (_reportOrders.Any())
- {
-
-
-
-
- Mã đơn
- Giá trị
- Trạng thái
- Ngày tạo
-
- @foreach (var o in _reportOrders.Take(20))
- {
-
- @o.Id.ToString()[..8]
- @FormatVND(o.TotalAmount)
- @(o.Status ?? "—")
- @o.CreatedAt.ToString("dd/MM/yyyy HH:mm")
-
- }
-
-
-
- }
- else
- {
- @RenderEmpty("bar-chart-2", "#22C55E", "Chưa có dữ liệu báo cáo", "Dữ liệu sẽ hiển thị khi có đơn hàng và hoạt động kinh doanh")
- }
- break;
-
- // ═══ KITCHEN — KDS with real ticket data ═══
- case "kitchen":
-
-
- @foreach (var st in new[] { ("all", "", "Tất cả"), ("pending", "clock", "Chờ"), ("preparing", "flame", "Đang làm"), ("completed", "check-circle", "Xong") })
- {
- LoadKitchenTickets(st.Item1)'
- style="padding:6px 14px;border-radius:6px;border:none;font-size:12px;font-weight:600;cursor:pointer;transition:all 0.2s;@(_kitchenStatusFilter == st.Item1 ? "background:var(--admin-orange-primary);color:white;" : "background:transparent;color:var(--admin-text-secondary);")">
- @if (!string.IsNullOrEmpty(st.Item2)) { }@st.Item3
-
- }
-
-
- Chờ: @_kitchenTickets.Count(t => t.Status == "pending")
- Đang làm: @_kitchenTickets.Count(t => t.Status == "preparing")
- Xong: @_kitchenTickets.Count(t => t.Status == "completed")
-
-
- @if (!_kitchenTickets.Any())
- {
- @RenderEmpty("flame", "#F59E0B", "Không có ticket bếp", "Ticket sẽ xuất hiện khi có đơn từ POS")
- }
- else
- {
-
- @foreach (var ticket in _kitchenTickets)
- {
- var ticketColor = ticket.Status switch { "pending" => "#F59E0B", "preparing" => "#3B82F6", "completed" => "#22C55E", _ => "#6B6B6F" };
- var ticketLabel = ticket.Status switch { "pending" => "Chờ", "preparing" => "Đang làm", "completed" => "Hoàn thành", _ => ticket.Status };
- var elapsed = (DateTime.UtcNow - ticket.CreatedAt).TotalMinutes;
-
-
- P@(ticket.Priority)
- @ticketLabel
-
-
@ticket.ItemName
-
@(ticket.Station ?? "Bếp chính")
-
- @((int)elapsed) phút
- @if (ticket.Status != "completed")
- {
- MarkTicketDone(ticket.Id)' style="padding:4px 10px;border-radius:6px;border:none;background:rgba(34,197,94,0.15);color:#22C55E;font-size:11px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:3px;"> Xong
- }
-
-
- }
-
- }
- break;
-
- // ═══ RESOURCES (Spa/Beauty — phòng, giường, thiết bị) ═══
- case "resources":
-
-
-
-
@_resources.Count(r => r.IsActive) Hoạt động
-
-
{ _editingResourceId = null; _newResourceName = ""; _newResourceType = "Room"; _newResourceCapacity = 1; _resourceFormMessage = null; _showResourceForm = !_showResourceForm; }'>
- Thêm tài nguyên
-
-
- @if (_showResourceForm)
- {
-
-
-
-
-
Tên *
-
Loại
-
- Phòng
- Giường
- Ghế
- Thiết bị
-
-
-
Sức chứa
-
-
- @(_editingResourceId.HasValue ? "Cập nhật" : "Lưu")
- _showResourceForm = false' style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-secondary);cursor:pointer;"> Hủy
-
- @if (_resourceFormMessage != null) {
@_resourceFormMessage
}
-
-
- }
- @if (!_resources.Any())
- {
- @RenderEmpty("door-open", "#EC4899", "Chưa có tài nguyên", "Thêm phòng, giường, thiết bị cho cửa hàng")
- }
- else
- {
-
-
-
- Tên
- Loại
- Sức chứa
- Trạng thái
-
-
- @foreach (var r in _resources)
- {
-
- @r.Name
- @(r.ResourceType ?? "—")
- @r.Capacity
- @(r.IsActive ? "Active" : "Inactive")
-
-
- EditResource(r)' style="background:rgba(59,130,246,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;">
- DeleteResourceItem(r.Id)' style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;">
-
-
-
- }
-
-
-
- }
- break;
-
- // ═══ TREATMENTS (Beauty — Liệu trình) ═══
+ // ═══ STUB SECTIONS ═══
case "treatments":
-
- @foreach (var tab in new[] { ("treatment", "📋 Liệu trình"), ("medical", "🏥 Hồ sơ y tế"), ("photos", "📷 Ảnh Before/After") })
- {
- { _treatmentTab = tab.Item1; StateHasChanged(); }'
- style="padding:8px 16px;border-radius:6px;border:none;font-size:12px;font-weight:600;cursor:pointer;transition:all 0.2s;@(_treatmentTab == tab.Item1 ? "background:var(--admin-orange-primary);color:white;" : "background:transparent;color:var(--admin-text-secondary);")">
- @tab.Item2
-
- }
-
- @if (_treatmentTab == "treatment")
- {
-
-
-
-
-
-
-
Quản lý liệu trình dài hạn
-
Theo dõi tiến trình điều trị nhiều buổi, lịch tái khám sau phẫu thuật.
-
-
- }
- else if (_treatmentTab == "medical")
- {
-
-
-
-
-
-
🦠 Dị ứng
-
📝 Tiền sử bệnh
-
-
📅 Lịch tái khám
-
-
- }
- else
- {
-
-
-
-
-
Khách hàng
-
Liệu trình Chọn liệu trình... Trị nám 5 buổi Trẻ hóa da 3 buổi
-
-
- @foreach (var (label, icon) in new[] { ("Before", "image"), ("After", "image-plus") })
- {
-
-
-
Upload ảnh @label
-
Kéo thả hoặc click để chọn ảnh
-
JPG, PNG, HEIC • Max 10MB
-
- }
-
-
-
📅 Lịch sử ảnh
-
- @foreach (var (date, desc, color) in new[] { ("15/02", "Buổi 1 — Before", "#3B82F6"), ("22/02", "Buổi 2 — After", "#22C55E"), ("01/03", "Buổi 3 — After", "#8B5CF6") })
- {
-
- }
-
-
-
-
- }
+ @RenderStubSection("clipboard-list", "#A855F7", "Liệu trình", "Theo dõi liệu trình điều trị — tính năng đang phát triển.")
break;
- // ═══ STAFF SCHEDULE (Spa/Beauty — Lịch làm việc) ═══
- case "schedule":
-
-
-
@_staffSchedules.Select(s => s.StaffId).Distinct().Count() NV có lịch
-
@_staffSchedules.Count Ca làm việc
-
-
{ _showScheduleForm = !_showScheduleForm; _newSchedStaffId = Guid.Empty; _newSchedDay = 1; _newSchedStart = "08:00"; _newSchedEnd = "17:00"; _schedFormMessage = null; }'>
- Thêm ca
-
-
- @if (_showScheduleForm)
- {
-
-
-
-
-
Nhân viên
-
- -- Chọn NV --
- @foreach (var st in _staff)
- {
- var stName = !string.IsNullOrWhiteSpace(st.LastName) || !string.IsNullOrWhiteSpace(st.FirstName) ? $"{st.LastName} {st.FirstName}".Trim() : (st.EmployeeCode ?? st.Id.ToString()[..8]);
- @stName
- }
-
-
-
Ngày
-
- Thứ 2 Thứ 3 Thứ 4
- Thứ 5 Thứ 6 Thứ 7 Chủ nhật
-
-
-
Bắt đầu
-
Kết thúc
-
-
- Lưu
- _showScheduleForm = false' style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-secondary);cursor:pointer;"> Hủy
-
- @if (_schedFormMessage != null) {
@_schedFormMessage
}
-
-
- }
- @if (!_staffSchedules.Any())
- {
- @RenderEmpty("calendar-clock", "#8B5CF6", "Chưa có lịch làm việc", "Thiết lập lịch ca cho nhân viên")
- }
- else
- {
-
-
-
-
- Nhân viên
- Vai trò
- Thứ
- Bắt đầu
- Kết thúc
-
-
- @foreach (var s in _staffSchedules.OrderBy(x => x.DayOfWeek).ThenBy(x => x.StartTime))
- {
- var schedStaff = _staff.FirstOrDefault(st => st.Id == s.StaffId);
- var schedStaffName = schedStaff != null && (!string.IsNullOrWhiteSpace(schedStaff.LastName) || !string.IsNullOrWhiteSpace(schedStaff.FirstName)) ? $"{schedStaff.LastName} {schedStaff.FirstName}".Trim() : (s.EmployeeCode ?? s.StaffId.ToString()[..8]);
-
- @schedStaffName
- @(s.Role ?? "—")
- @DayLabel(s.DayOfWeek)
- @s.StartTime
- @s.EndTime
- DeleteScheduleItem(s.Id)' style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;">
-
- }
-
-
-
- }
- break;
-
- // ═══ RECIPES (Cafe/Restaurant — Công thức & Nguyên liệu) ═══
- case "recipes":
-
-
@_recipes.Count công thức
- { _showRecipeForm = !_showRecipeForm; _editingRecipeId = null; _newRecipeName = ""; _newRecipeInstructions = ""; _newRecipePrepTime = 5; _recipeIngredients = new(); _recipeFormMessage = null; }'>
- Thêm công thức
-
-
- @if (_showRecipeForm)
- {
-
-
-
-
-
Tên công thức *
-
Thời gian chuẩn bị (phút)
-
Hướng dẫn
-
-
-
Nguyên liệu _recipeIngredients.Add(new("","","",0,0))' style="padding:4px 10px;border-radius:6px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-primary);font-size:12px;cursor:pointer;">+ Thêm
- @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;" />
- _recipeIngredients.RemoveAt(i)' style="padding:6px;border-radius:6px;border:none;background:rgba(239,68,68,0.1);color:#EF4444;cursor:pointer;">✕
-
- }
-
-
- Lưu
- _showRecipeForm = false' style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-secondary);cursor:pointer;"> Hủy
-
- @if (_recipeFormMessage != null) {
@_recipeFormMessage
}
-
-
- }
- @if (!_recipes.Any())
- {
- @RenderEmpty("flask-conical", "#FF5C00", "Chưa có công thức", "Thêm công thức và nguyên liệu pha chế")
- }
- else
- {
-
- @foreach (var recipe in _recipes)
- {
- var isExpanded = _expandedRecipeId == recipe.Id;
-
{ _expandedRecipeId = isExpanded ? null : recipe.Id; StateHasChanged(); }'>
-
-
-
@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;">
-
-
-
@recipe.PrepTimeMinutes phút chuẩn bị
- @if (isExpanded && !string.IsNullOrEmpty(recipe.Instructions))
- {
-
@recipe.Instructions
- }
-
-
- }
-
- }
- break;
-
- // ═══ PROMOTIONS / CAMPAIGNS (CRUD) ═══
- case "promotions":
-
-
- Chiến dịch khuyến mãi chung cho tất cả cửa hàng trong thương hiệu
-
- @* ─── Sub-tabs: Campaigns | Vouchers ─── *@
-
- @{ var promoTabs = new[] { ("campaigns", "Chiến dịch", "tag"), ("vouchers", "Mã voucher", "ticket") }; }
- @foreach (var (tab, label, icon) in promoTabs)
- {
- var t = tab;
- var isActive = _promoSubTab == t;
- SwitchPromoTab(t))"
- class="@(isActive ? "admin-btn-primary" : "")"
- style="padding:8px 16px;border-radius:8px;border:1px solid var(--admin-border-subtle);font-size:13px;display:inline-flex;align-items:center;gap:6px;cursor:pointer;@(isActive ? "background:var(--admin-orange-primary);color:#FFF;font-weight:700;border-color:var(--admin-orange-primary);" : "background:var(--admin-bg-elevated);color:var(--admin-text-secondary);font-weight:500;")">
- @label
-
- }
-
- @if (_promoSubTab == "campaigns")
- {
-
-
@_campaigns.Count chiến dịch
- { _showCampaignForm = !_showCampaignForm; _editingCampaignId = null; _newCampaignName = ""; _newCampaignDesc = ""; _newCampaignValue = 0; _newCampaignVouchers = 0; _newCampaignDiscountType = "fixed"; _newCampaignStart = DateTime.Today; _newCampaignEnd = DateTime.Today.AddMonths(1); _campaignFormMessage = null; }'>
- Thêm chiến dịch
-
-
- @if (_showCampaignForm)
- {
-
-
-
-
- @if (_campaignFormMessage != null)
- {
-
@_campaignFormMessage
- }
-
- Lưu
- { _showCampaignForm = false; _campaignFormMessage = null; }'>Hủy
-
-
-
- }
- @if (!_campaigns.Any())
- {
- @RenderEmpty("tag", "#22C55E", "Chưa có chiến dịch", "Tạo chiến dịch voucher, khuyến mãi cho khách hàng", "plus-circle", "Thêm chiến dịch")
- }
- else
- {
-
-
@_campaigns.Count Tổng chiến dịch
-
@_campaigns.Count(c => c.Status == "Active") Đang hoạt động
-
@_campaigns.Sum(c => c.TotalVouchers) Tổng voucher
-
@_campaigns.Sum(c => c.IssuedVouchers) Đã phát
-
-
-
-
-
- Tên
- Loại
- Giá trị
- Đã phát/Tổng
- Bắt đầu
- Kết thúc
-
-
- @foreach (var c in _campaigns)
- {
-
- @c.Name
- @(c.Description?.Contains("%") == true ? "%" : "₫")
- @(c.Description?.Contains("%") == true ? $"{c.FaceValue}%" : FormatVND(c.FaceValue))
- @c.IssuedVouchers / @c.TotalVouchers
- @(c.StartDate?.ToString("dd/MM/yy") ?? "—")
- @(c.EndDate?.ToString("dd/MM/yy") ?? "—")
-
-
- EditCampaign(c))" style="background:rgba(59,130,246,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Sửa">
- DeleteCampaignItem(c.Id))" style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Xóa">
-
-
-
- }
-
-
-
- } @* end else *@
- } @* end campaigns sub-tab *@
- @if (_promoSubTab == "vouchers")
- {
-
-
@_vouchers.Count mã voucher
-
- Làm mới
-
-
- @if (!_vouchers.Any())
- {
- @RenderEmpty("ticket", "#EC4899", "Chưa có voucher", "Tạo chiến dịch để tự động sinh mã voucher", "tag", "Tạo chiến dịch")
- }
- else
- {
-
-
-
-
- Mã
- Chiến dịch
- Mệnh giá
- Còn lại
- Trạng thái
- Ngày tạo
-
-
- @foreach (var v in _vouchers)
- {
-
- @v.Code
- @(v.CampaignName ?? "—")
- @FormatVND(v.FaceValue)
- @FormatVND(v.RemainingValue)
- @GetVoucherStatusLabel(v.Status)
- @(v.CreatedAt?.ToLocalTime().ToString("dd/MM/yy") ?? "—")
-
- @if (v.Status?.ToLower() == "available")
- {
- RevokeVoucher(v.Id))" style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;padding:4px 10px;font-size:11px;color:#EF4444;font-weight:600;cursor:pointer;" title="Thu hồi">Thu hồi
- }
-
-
- }
-
-
-
- }
- }
- break;
-
- case "drive":
-
-
Tìm
-
Thư mục mới Upload
-
- @if (_showFolderForm)
- {
-
- }
- @if (_currentFolderId.HasValue)
- {
-
Quay lại
- }
- @if (_storageFolders.Any())
- {
-
- @foreach (var folder in _storageFolders)
- {
-
NavigateToFolder(folder.Id))" style="padding:16px;border-radius:12px;border:1px solid var(--admin-border-subtle);background:var(--admin-bg-elevated);cursor:pointer;display:flex;align-items:center;gap:10px;">
@folder.Name
@folder.CreatedAt.ToString("dd/MM/yyyy")
DeleteFolder(folder.Id))" @onclick:stopPropagation style="padding:4px;border:none;background:transparent;color:var(--admin-text-tertiary);cursor:pointer;">
- }
-
- }
-
- @if (!_storageFiles.Any())
- {
-
- }
- else
- {
-
Tên tệp Loại Kích thước Ngày upload Thao tác
- @foreach (var f in _storageFiles)
- {
- @f.FileName @(f.ContentType ?? "—") @FormatFileSize(f.FileSizeBytes) @f.UploadedAt.ToString("dd/MM/yyyy HH:mm") DownloadStorageFile(f.Id))" style="padding:4px 8px;border-radius:6px;border:1px solid var(--admin-border-subtle);background:var(--admin-bg-elevated);cursor:pointer;font-size:11px;"> DeleteStorageFile(f.Id))" style="padding:4px 8px;border-radius:6px;border:none;background:rgba(239,68,68,0.1);color:#EF4444;cursor:pointer;font-size:11px;">
- }
-
- }
-
- break;
-
- // ═══ SETTINGS ═══
- case "settings":
- @* ─── Shop info (read-only) ─── *@
-
-
-
-
-
Tên cửa hàng @(_shopName ?? "—")
-
Ngành hàng @_verticalLabel
-
-
-
- @* ─── Opening hours + business days ─── *@
-
-
-
-
-
-
Ngày kinh doanh
-
- @foreach (var (day, code) in new[] { ("T2","Monday"),("T3","Tuesday"),("T4","Wednesday"),("T5","Thursday"),("T6","Friday"),("T7","Saturday"),("CN","Sunday") })
- {
- var isOn = _settingsOpenDays.Contains(code);
- ToggleDay(code))"
- style="width:40px;height:40px;border-radius:10px;border:1px solid @(isOn ? "var(--admin-orange-primary)" : "var(--admin-border-subtle)");background:@(isOn ? "rgba(255,92,0,0.15)" : "var(--admin-bg-elevated)");color:@(isOn ? "var(--admin-orange-primary)" : "var(--admin-text-tertiary)");font-size:12px;font-weight:@(isOn ? "700" : "500");cursor:pointer;">
- @day
-
- }
-
-
-
-
- @* ─── Features config toggles ─── *@
-
-
-
-
- @{ void RenderToggle(string label, bool isOn, Action
setter) {
- { setter(!isOn); StateHasChanged(); })">
-
-
@label
-
;
- } }
- @{ RenderToggle("Quản lý tồn kho", _featHasInventory, v => _featHasInventory = v); }
- @{ RenderToggle("Đặt lịch hẹn", _featHasBooking, v => _featHasBooking = v); }
- @{ RenderToggle("Quản lý bàn", _featHasTables, v => _featHasTables = v); }
- @{ RenderToggle("Hiển thị bếp", _featHasKitchen, v => _featHasKitchen = v); }
- @{ RenderToggle("Vận chuyển", _featHasShipping, v => _featHasShipping = v); }
- @{ RenderToggle("Giao hàng", _featHasDelivery, v => _featHasDelivery = v); }
-
-
-
- @* ─── Save button ─── *@
-
-
- Lưu thiết lập
-
- @if (_settingsMessage != null)
- {
- @_settingsMessage
- }
-
- break;
-
- // ═══ R3: RESERVATIONS / ĐẶT BÀN (Nhà hàng) ═══
case "reservations":
-
-
-
-
-
- Khách
- Số người
- Thời gian
- Bàn
- Trạng thái
- Ghi chú
-
- @foreach (var (guest, ppl, time, table, status, note, color) in new[] {
- ("Nguyễn Văn A", "4", "18:00", "Bàn 5", "Xác nhận", "Sinh nhật", "#22C55E"),
- ("Trần Thị B", "2", "19:00", "Bàn 2", "Xác nhận", "", "#22C55E"),
- ("Lê Minh C", "8", "19:30", "VIP 1", "Chờ duyệt", "Tiệc công ty", "#F59E0B"),
- ("Phạm Dương D", "6", "20:00", "Bàn 7", "Xác nhận", "Kỷ niệm", "#22C55E"),
- ("Hoàng E", "3", "20:30", "Bàn 3", "Chờ duyệt", "", "#F59E0B") })
- {
-
- @guest
- @ppl
- @time
- @table
- @status
- @note
-
- }
-
-
-
+ @RenderStubSection("calendar-check", "#3B82F6", "Đặt bàn", "Quản lý đặt bàn trước — tính năng đang phát triển.")
break;
- // ═══ K3: HAPPY HOUR (Karaoke) ═══
case "happy-hour":
-
-
-
-
-
-
Ngày áp dụng
-
- @foreach (var day in new[] { "T2", "T3", "T4", "T5", "T6", "T7", "CN" })
- {
- var isActive = day == "T2" || day == "T3" || day == "T4" || day == "T5";
- @day
- }
-
-
-
Lưu cấu hình
-
-
-
-
-
-
- @foreach (var (combo, items, oldPrice, newPrice) in new[] {
- ("Combo Đôi", "2 giờ phòng + 2 bia", "350,000₫", "245,000₫"),
- ("Combo Nhóm", "3 giờ phòng + 5 đồ uống", "750,000₫", "525,000₫"),
- ("Combo VIP", "2 giờ VIP + trái cây + 4 bia", "980,000₫", "686,000₫") })
- {
-
-
@combo
-
@items
-
- @oldPrice
- @newPrice
-
-
- }
-
-
-
+ @RenderStubSection("clock", "#F59E0B", "Happy Hour", "Cấu hình khung giờ giảm giá — tính năng đang phát triển.")
break;
- // ═══ S4: SERVICE PACKAGES / GÓI DỊCH VỤ (Spa) ═══
case "packages":
-
-
-
-
- @foreach (var (pkg, sessions, services, price, savings) in new[] {
- ("Gói Thư Giãn", "5 buổi", "Massage body + Xông hơi", "2,500,000₫", "Tiết kiệm 500,000₫"),
- ("Gói VIP", "10 buổi", "Massage + Facial + Xông hơi + Chăm sóc da", "6,800,000₫", "Tiết kiệm 2,200,000₫"),
- ("Gói Cặp Đôi", "4 buổi", "2 người — Massage + Xông hơi", "3,600,000₫", "Tiết kiệm 800,000₫") })
- {
-
-
-
-
@services
-
- @price
- @savings
-
-
-
- }
-
-
-
+ @RenderStubSection("gift", "#8B5CF6", "Gói dịch vụ", "Quản lý gói combo dịch vụ — tính năng đang phát triển.")
break;
- // ═══ B5: CONSENT FORM / CAM KẾT KH (Thẩm mỹ) ═══
case "consent":
-
-
-
-
- @foreach (var (title, desc, icon, fields) in new[] {
- ("Cam kết Phẫu thuật", "Biểu mẫu đồng ý trước phẫu thuật thẩm mỹ", "syringe", "12 trường"),
- ("Cam kết Tiêm Filler", "Xác nhận rủi ro và đồng ý tiêm filler", "droplets", "8 trường"),
- ("Cam kết Laser", "Biểu mẫu đồng ý điều trị laser da", "zap", "10 trường"),
- ("Cam kết Chung", "Mẫu cam kết dịch vụ thẩm mỹ tổng quát", "file-text", "6 trường") })
- {
-
-
-
@desc
-
- Xem
- Sửa
-
-
- }
-
-
-
+ @RenderStubSection("file-check", "#EC4899", "Cam kết KH", "Biểu mẫu đồng ý khách hàng — tính năng đang phát triển.")
break;
- // ═══ B6: DOCTORS / BÁC SĨ (Thẩm mỹ) ═══
case "doctors":
-
-
-
-
- @foreach (var (name, spec, cert, exp, color) in new[] {
- ("BS. Nguyễn Văn A", "Phẫu thuật thẩm mỹ", "Chứng chỉ BVTM", "15 năm", "#3B82F6"),
- ("BS. Trần Thị B", "Da liễu", "Thạc sĩ Y khoa", "10 năm", "#8B5CF6"),
- ("BS. Lê Minh C", "Nội tiết", "Ph.D. Nội tiết học", "12 năm", "#EC4899"),
- ("KTV. Phạm D", "Chăm sóc da", "Chứng chỉ Quốc tế CIDESCO", "8 năm", "#22C55E") })
- {
-
-
-
- @cert
- @exp
-
-
- Lịch làm
- Hồ sơ
-
-
- }
-
-
-
+ @RenderStubSection("stethoscope", "#3B82F6", "Bác sĩ / CK", "Quản lý bác sĩ và chuyên gia — tính năng đang phát triển.")
break;
- // ═══ B7: FOLLOW-UP / TÁI KHÁM (Thẩm mỹ) ═══
case "followup":
-
-
-
-
-
- Khách hàng
- Dịch vụ đã làm
- Ngày tái khám
- Bác sĩ
- Trạng thái
-
- @foreach (var (patient, service, date, doctor, status, color) in new[] {
- ("Nguyễn Thị Hương", "Nâng mũi sụn", "05/03/2026", "BS. Nguyễn A", "Sắp đến", "#F59E0B"),
- ("Trần Văn Nam", "Cấy mỡ tự thân", "07/03/2026", "BS. Trần B", "Sắp đến", "#F59E0B"),
- ("Lê Thu Trang", "Trị nám laser", "01/03/2026", "BS. Lê C", "Hôm nay", "#EF4444"),
- ("Phạm Minh Tuấn", "Filler môi", "28/02/2026", "BS. Nguyễn A", "Hoàn thành", "#22C55E"),
- ("Hoàng Lan", "Botox trán", "25/02/2026", "BS. Trần B", "Hoàn thành", "#22C55E") })
- {
-
- @patient
- @service
- @date
- @doctor
- @status
-
- }
-
-
-
+ @RenderStubSection("calendar-heart", "#EC4899", "Tái khám", "Lịch tái khám sau điều trị — tính năng đang phát triển.")
break;
- // ═══ R4: ZONES / KHU VỰC (Nhà hàng) ═══
- case "zones":
- var tableZones = _tables.GroupBy(t => t.Zone ?? "Chung").Select(g => new { Name = g.Key, Count = g.Count() }).ToList();
- var allZones = tableZones.Select(z => z.Name).Union(_customZones).Distinct().OrderBy(z => z).ToList();
- var zoneGroups = allZones.Select((z, i) => new { Name = z, Count = tableZones.FirstOrDefault(tz => tz.Name == z)?.Count ?? 0, Color = _zoneColors[i % _zoneColors.Length], Icon = _zoneIcons[i % _zoneIcons.Length] }).ToList();
-
-
-
- @if (_showZoneForm)
- {
-
-
@(_editingZoneOriginalName != null ? "Đổi tên khu vực" : "Thêm khu vực mới")
-
-
- Lưu
- _showZoneForm = false' style="padding:8px 12px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-secondary);cursor:pointer;font-size:12px;">
-
- @if (_zoneFormMessage != null) {
@_zoneFormMessage
}
-
- }
- @if (!zoneGroups.Any())
- {
- @RenderEmpty("map-pin", "#F59E0B", "Chưa có khu vực nào", "Nhấn 'Thêm khu vực' ở trên để tạo khu vực đầu tiên", "", "", "")
- }
- else
- {
-
- @foreach (var zone in zoneGroups)
- {
- var rgbVal = zone.Color switch { "#3B82F6" => "59,130,246", "#A855F7" => "168,85,247", "#22C55E" => "34,197,94", "#F59E0B" => "245,158,11", "#EC4899" => "236,72,153", _ => "99,102,241" };
-
-
-
-
-
-
-
@zone.Name
-
@zone.Count bàn
-
-
-
- Đang hoạt động
-
-
- { _editingZoneOriginalName = zone.Name; _newZoneName = zone.Name; _showZoneForm = true; _zoneFormMessage = null; }' style="font-size:12px;color:var(--admin-orange-primary);background:none;border:none;cursor:pointer;display:flex;align-items:center;gap:4px;"> Đổi tên
-
-
- }
-
- }
-
-
- break;
-
- // ═══ S6: COMBO DỊCH VỤ (Spa) ═══
case "combos":
-
-
-
-
- @foreach (var (name, services, duration, price, orig, sold, color) in new[] {
- ("Combo Thư Giãn Toàn Thân", "Massage body 60' + Xông hơi 30' + Đắp mặt nạ 20'", "110 phút", "800,000₫", "1,050,000₫", 45, "#3B82F6"),
- ("Combo Detox & Làm Đẹp", "Tẩy tế bào chết + Massage mặt + Chăm sóc da 5 bước", "90 phút", "650,000₫", "850,000₫", 32, "#A855F7"),
- ("Combo Cặp Đôi Premium", "2 Massage body 90' + 2 Xông hơi + Trà thảo mộc", "120 phút", "1,500,000₫", "2,100,000₫", 18, "#EC4899") })
- {
-
-
-
@name
- Đang bán
-
-
@services
-
-
- @duration
-
-
-
- @price
- @orig
-
-
Đã bán: @sold
-
-
- }
-
-
-
- break;
-
- // ═══ C5: CA LÀM VIỆC / SHIFTS (Café) ═══
- case "shifts":
-
-
@_staffSchedules.Count Ca đã phân
-
-
@_staffSchedules.Select(s => s.DayOfWeek).Distinct().Count() Ngày có ca
-
- @* ─── Add Schedule Form ─── *@
-
-
- @if (_showScheduleForm)
- {
-
-
-
Nhân viên
-
- -- Chọn NV --
- @foreach (var s in _staff) { @(s.EmployeeCode ?? s.Id.ToString()[..8]) }
-
-
-
Ngày
-
- Thứ 2 Thứ 3 Thứ 4
- Thứ 5 Thứ 6 Thứ 7 Chủ nhật
-
-
-
Giờ bắt đầu
-
Giờ kết thúc
-
-
- Lưu
- _showScheduleForm = false)">Hủy
-
- @if (_schedFormMessage != null) {
@_schedFormMessage
}
-
- }
-
- @* ─── Weekly Grid ─── *@
-
-
-
- @if (!_staff.Any())
- {
-
Chưa có nhân viên. Thêm nhân viên trong mục
Nhân sự .
- }
- else
- {
-
- Nhân viên
- @foreach (var d in new[] { "T2", "T3", "T4", "T5", "T6", "T7", "CN" })
- {
- @d
- }
-
- @foreach (var emp in _staff)
- {
-
- @(emp.EmployeeCode ?? emp.Id.ToString()[..8])
- @foreach (var dow in new[] { 1, 2, 3, 4, 5, 6, 0 })
- {
- var sched = _staffSchedules.FirstOrDefault(s => s.StaffId == emp.Id && s.DayOfWeek == dow);
- if (sched != null)
- {
- var isMorning = sched.StartTime?.CompareTo("12:00") < 0;
- var bg = isMorning ? "rgba(59,130,246,0.12)" : "rgba(168,85,247,0.12)";
- var fg = isMorning ? "#3B82F6" : "#A855F7";
- var label = $"{sched.StartTime?[..5]}";
-
- @label
- DeleteScheduleItem(sched.Id))" style="background:none;border:none;cursor:pointer;margin-left:2px;" title="Xóa">
-
- }
- else
- {
-
- —
-
- }
- }
-
- }
-
- }
-
-
-
-
Sáng (<12:00)
-
Chiều (≥12:00)
-
— = Nghỉ
-
+ @RenderStubSection("layers", "#A855F7", "Combo dịch vụ", "Quản lý combo dịch vụ — tính năng đang phát triển.")
break;
// ═══ UNKNOWN SECTIONS ═══
@@ -2631,119 +373,19 @@
private string? _errorMessage;
private string _posVertical = "cafe";
private Guid? _shopGuid;
+ private bool _showNotifications;
+ private string? _toastMessage;
- // ═══ DATA ═══
- private List
_products = new();
- private List _inventory = new();
- private List _orders = new();
- private List _staff = new();
- private List _members = new();
- private List _tables = new();
- private List _appointments = new();
- // Reports data (separate from section-specific _orders)
- private List _reportOrders = new();
- private List _reportProducts = new();
// Overview data
private List _ovOrders = new();
private List _ovProducts = new();
private List _ovStaff = new();
private List _ovTables = new();
private List _ovAppts = new();
- // Product form state
- private bool _showProductForm;
- private Guid? _editingProductId;
- private string _newProductName = "";
- private decimal _newProductPrice;
- private string _newProductType = "PreparedFood";
- private string _newProductDesc = "";
- private string _newProductCategoryId = "";
- private string? _formMessage;
- private bool _formSuccess;
- // Product pagination + view state
- private int _productPage = 1;
- private int _productPageSize = 20;
- private string _productView = "grid"; // grid | list
- private string _productCategoryFilter = "";
- private int ProductTotalPages => Math.Max(1, (int)Math.Ceiling((double)FilteredProducts.Count / _productPageSize));
- private List FilteredProducts =>
- string.IsNullOrEmpty(_productCategoryFilter) ? _products :
- _products.Where(p => (p.CategoryId?.ToString() ?? "") == _productCategoryFilter).ToList();
- private List PagedProducts =>
- FilteredProducts.Skip((_productPage - 1) * _productPageSize).Take(_productPageSize).ToList();
- // Staff form state
- private bool _showStaffForm;
- private Guid? _editingStaffId;
- private string _newStaffCode = "";
- private string _newStaffRole = "Cashier";
- private string _newStaffPhone = "";
- private string _newStaffEmail = "";
- private string? _staffFormMessage;
- private bool _staffFormSuccess;
- private bool _createStaffAccount;
- private string _newStaffFirstName = "";
- private string _newStaffLastName = "";
- private string _newStaffPassword = "";
- private Guid? _merchantId;
- // New data: wallets, promotions, campaigns, member levels, schedules, inv txns
- private List _wallets = new();
- private List _walletTxns = new();
- private List _promotions = new();
- private List _campaigns = new();
- private List _memberLevels = new();
- // Campaign form state
- private bool _showCampaignForm;
- private Guid? _editingCampaignId;
- private string _newCampaignName = "";
- private string _newCampaignDesc = "";
- private decimal _newCampaignValue;
- private int _newCampaignVouchers;
- private DateTime _newCampaignStart = DateTime.Today;
- private DateTime _newCampaignEnd = DateTime.Today.AddMonths(1);
- private string _newCampaignDiscountType = "fixed";
- private string? _campaignFormMessage;
- private bool _campaignFormSuccess;
- // Member form state
- private bool _showMemberForm;
- private Guid? _editingMemberId;
- private string _newMemberGender = "";
- private string _newMemberCountry = "VN";
- private string _newMemberName = "";
- private string _newMemberPhone = "";
- private List _staffSchedules = new();
- private List _invTxns = new();
- // P2 state: calendar, KDS, treatments
- private int _calendarWeekOffset;
- private string _kdsStation = "all";
- private string _treatmentTab = "treatment";
- // P4 state: notifications, customer detail
- private bool _showNotifications;
- private Guid? _selectedCustomerId;
- private List _resources = new();
- // Customer filter state
- private string _customerSearch = "";
- // Inventory sub-tab and form state
- private string _invSubTab = "levels"; // levels, stock-in, stock-out, adjust, transactions, low-stock
- private Guid _invSelectedProductId;
- private int _invAmount;
- private int _invNewQty;
- private string _invNotes = "";
- private string? _invFormMessage;
- private bool _invFormSuccess;
- private List _lowStockItems = new();
- // Finance date range filter state
- private string _financePeriod = "all"; // 7d, 30d, all
- // Category form state
- private bool _showCategoryForm;
- private Guid? _editingCategoryId;
- private string _newCategoryName = "";
- private string _newCategoryDesc = "";
- private int _newCategoryOrder;
- private string? _categoryFormMessage;
- private bool _categoryFormSuccess;
- private List _categories = new();
- // Order detail state
- private Guid? _selectedOrderId;
- private PosDataService.OrderDetailResponse? _orderDetail;
+
+ // Services section (reuses products)
+ private List _serviceProducts = new();
+
// Shop edit state
private bool _editingShop;
private string _shopEditName = "";
@@ -2754,112 +396,6 @@
private string _shopEditCloseTime = "";
private string? _shopEditMessage;
private bool _shopEditSuccess;
- // Revenue report state
- private string _reportPeriod = "daily";
- private List _revenueReport = new();
- // Settings state
- private PosDataService.ShopSettingsInfo? _shopSettings;
- private string _settingsOpenTime = "";
- private string _settingsCloseTime = "";
- private List _settingsOpenDays = new();
- private bool _featHasInventory, _featHasBooking, _featHasTables;
- private bool _featHasKitchen, _featHasShipping, _featHasDelivery;
- private string? _settingsMessage;
- private bool _settingsSuccess;
- // Customer sub-tab state
- private string _customerSubTab = "members"; // members | levels | exp
- // Level CRUD state
- private bool _showLevelForm;
- private Guid? _editingLevelId;
- private int _newLevelNumber;
- private string _newLevelName = "";
- private int _newLevelRequiredExp;
- private string _newLevelDescription = "";
- private string _newLevelBadgeColor = "#CD7F32";
- private string? _levelFormMessage;
- private bool _levelFormSuccess;
- // EXP management state
- private Guid? _expMemberId;
- private int _expPoints = 100;
- private int _expSourceId = 7;
- private string? _expFormMessage;
- private bool _expFormSuccess;
- private PosDataService.MemberProgressInfo? _memberProgress;
- private List _expHistory = new();
- // Staff extended fields state
- private string _newStaffAddress = "";
- // Image upload state
- private Microsoft.AspNetCore.Components.Forms.IBrowserFile? _productImageFile;
- private string? _productImagePreview;
- private Microsoft.AspNetCore.Components.Forms.IBrowserFile? _categoryImageFile;
- private string? _categoryImagePreview;
- private string _newCategoryImageUrl = "";
- private Microsoft.AspNetCore.Components.Forms.IBrowserFile? _staffDocFrontFile;
- private string? _staffDocFrontPreview;
- private Microsoft.AspNetCore.Components.Forms.IBrowserFile? _staffDocBackFile;
- private string? _staffDocBackPreview;
- // Top products state
- private List _topProducts = new();
- // Tables CRUD state
- private bool _showTableForm;
- private Guid? _editingTableId;
- private string _newTableNumber = "";
- private int _newTableCapacity = 4;
- private string _newTableZone = "";
- private string? _tableFormMessage;
- private bool _tableFormSuccess;
- // Zones CRUD state
- private bool _showZoneForm;
- private string _newZoneName = "";
- private string? _editingZoneOriginalName;
- private string? _zoneFormMessage;
- private bool _zoneFormSuccess;
- private readonly List _customZones = new();
- private static readonly string[] _zoneColors = { "#3B82F6", "#A855F7", "#22C55E", "#F59E0B", "#EC4899", "#6366F1" };
- private static readonly string[] _zoneIcons = { "building", "crown", "trees", "wine", "coffee", "map-pin" };
- private List AllZoneNames => _tables.Select(t => t.Zone ?? "Chung").Distinct()
- .Union(_customZones).Distinct().OrderBy(z => z).ToList();
- // Kitchen state
- private List _kitchenTickets = new();
- private string _kitchenStatusFilter = "all";
- // Appointments form state
- private bool _showApptForm;
- private DateTime _newApptStart = DateTime.Today.AddHours(9);
- private DateTime _newApptEnd = DateTime.Today.AddHours(10);
- private string? _apptFormMessage;
- private bool _apptFormSuccess;
- // Resources CRUD state
- private bool _showResourceForm;
- private Guid? _editingResourceId;
- private string _newResourceName = "";
- private string _newResourceType = "Room";
- private int _newResourceCapacity = 1;
- private string? _resourceFormMessage;
- private bool _resourceFormSuccess;
- // Schedule form state
- private bool _showScheduleForm;
- private Guid _newSchedStaffId;
- private string _newSchedStaffIdStr = "";
- private int _newSchedDay = 1;
- private string _newSchedStart = "08:00";
- private string _newSchedEnd = "17:00";
- private string? _schedFormMessage;
- private bool _schedFormSuccess;
- // Voucher management state
- private string _promoSubTab = "campaigns"; // campaigns | vouchers
- private List _vouchers = new();
- private Guid? _voucherCampaignFilter;
- // Recipes state
- private List _recipes = new();
- private bool _showRecipeForm;
- private Guid? _editingRecipeId;
- private string _newRecipeName = "";
- private string _newRecipeInstructions = "";
- private int _newRecipePrepTime = 5;
- private List<(string Name, string Unit, string Qty, decimal Quantity, decimal Cost)> _recipeIngredients = new();
- private Guid? _expandedRecipeId;
- private string? _recipeFormMessage;
- private bool _recipeFormSuccess;
protected override async Task OnInitializedAsync()
{
@@ -2869,7 +405,6 @@
protected override async Task OnParametersSetAsync()
{
- // EN: Called when URL params change / VI: Gọi khi params URL thay đổi
if (_section != (Section?.ToLowerInvariant() ?? ""))
await LoadData();
}
@@ -2892,93 +427,26 @@
_shopName = shop.Name ?? "Cửa hàng";
_verticalLabel = ShopSidebarConfig.GetVerticalLabel(shop.Category);
_posVertical = MapCategoryToVertical(shop.Category);
- _merchantId = shop.MerchantId;
AdminLayoutRef?.SetShopContext(ShopId, _shopName, shop.Category);
}
}
- // EN: Load only data needed for current section / VI: Chỉ tải data cần cho section hiện tại
+ // Only load data needed for sections handled directly by shell
switch (_section)
{
case "overview":
_ovOrders = await DataService.GetOrdersAsync(_shopGuid);
_ovProducts = await DataService.GetAllProductsAsync(_shopGuid);
- _ovStaff = await DataService.GetStaffForShopAsync(_shopGuid.Value);
+ _ovStaff = await DataService.GetStaffForShopAsync(_shopGuid!.Value);
if (_shopGuid.HasValue && (_posVertical == "restaurant" || _posVertical == "karaoke"))
_ovTables = await DataService.GetTablesAsync(_shopGuid.Value);
if (_shopGuid.HasValue && (_posVertical == "spa" || _posVertical == "beauty"))
_ovAppts = await DataService.GetAppointmentsAsync(_shopGuid.Value);
break;
- case "menu":
- case "products":
- _products = await DataService.GetAllProductsAsync(_shopGuid);
- _categories = await DataService.GetAllCategoriesAsync(_shopGuid);
- break;
- case "inventory":
- _inventory = await DataService.GetInventoryAsync(_shopGuid);
- _invTxns = await DataService.GetInventoryTransactionsAsync(_shopGuid);
- break;
- case "finance":
- _orders = await DataService.GetOrdersAsync(_shopGuid);
- _wallets = await DataService.GetWalletsAsync();
- _walletTxns = await DataService.GetWalletTransactionsAsync();
- break;
- case "staff":
- _staff = await DataService.GetStaffForShopAsync(_shopGuid.Value);
- _staffSchedules = await DataService.GetStaffSchedulesAsync(_shopGuid);
- break;
- case "customers":
- var rawMembers = await DataService.GetMembersAsync();
- var rawLevels = await DataService.GetMembershipLevelsAsync();
- _memberLevels = PosDataService.EnrichLevelDefinitions(rawLevels, rawMembers);
- _members = PosDataService.ResolveMemberLevelNames(rawMembers, rawLevels);
- break;
- case "tables":
- case "rooms":
- case "zones":
- if (_shopGuid.HasValue)
- _tables = await DataService.GetTablesAsync(_shopGuid.Value);
- break;
- case "appointments":
- if (_shopGuid.HasValue)
- _appointments = await DataService.GetAppointmentsAsync(_shopGuid.Value);
- break;
- case "resources":
- if (_shopGuid.HasValue)
- _resources = await DataService.GetResourcesAsync(_shopGuid.Value);
- break;
case "services":
- _products = await DataService.GetAllProductsAsync(_shopGuid);
- break;
- case "reports":
- _reportOrders = await DataService.GetOrdersAsync(_shopGuid);
- _reportProducts = await DataService.GetAllProductsAsync(_shopGuid);
- _topProducts = await DataService.GetTopProductsAsync(_shopGuid);
- break;
- case "settings":
- if (_shopGuid.HasValue)
- await LoadShopSettings();
- break;
- case "promotions":
- _campaigns = await DataService.GetCampaignsAsync();
- break;
- case "shifts":
- case "schedule":
- _staffSchedules = await DataService.GetStaffSchedulesAsync(_shopGuid);
- _staff = await DataService.GetStaffForShopAsync(_shopGuid.Value);
- break;
- case "kitchen":
- if (_shopGuid.HasValue)
- _kitchenTickets = await DataService.GetKitchenTicketsAsync(_shopGuid);
- break;
- case "recipes":
- if (_shopGuid.HasValue)
- _recipes = await DataService.GetRecipesAsync(_shopGuid);
- break;
- case "drive":
- _storageFolders = await DataService.GetFoldersAsync(_currentFolderId);
- _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch);
+ _serviceProducts = await DataService.GetAllProductsAsync(_shopGuid);
break;
+ // All other sections are handled by child components with their own OnInitializedAsync
}
}
catch (Exception ex)
@@ -2989,58 +457,6 @@
finally { IsLoading = false; }
}
- private async Task LoadShopSettings()
- {
- if (!_shopGuid.HasValue) return;
- try
- {
- _shopSettings = await DataService.GetShopSettingsAsync(_shopGuid.Value);
- if (_shopSettings != null)
- {
- _settingsOpenTime = _shopSettings.OpenTime ?? "";
- _settingsCloseTime = _shopSettings.CloseTime ?? "";
- _settingsOpenDays = _shopSettings.OpenDays ?? new();
- if (_shopSettings.Features != null)
- {
- _featHasInventory = _shopSettings.Features.HasInventory;
- _featHasBooking = _shopSettings.Features.HasBooking;
- _featHasTables = _shopSettings.Features.HasTables;
- _featHasKitchen = _shopSettings.Features.HasKitchen;
- _featHasShipping = _shopSettings.Features.HasShipping;
- _featHasDelivery = _shopSettings.Features.HasDelivery;
- }
- }
- }
- catch { /* non-fatal */ }
- }
-
- private async Task SaveShopSettings()
- {
- if (!_shopGuid.HasValue) return;
- _settingsMessage = null;
- var features = new PosDataService.ShopFeaturesInfo
- {
- HasInventory = _featHasInventory, HasBooking = _featHasBooking, HasTables = _featHasTables,
- HasKitchen = _featHasKitchen, HasShipping = _featHasShipping, HasDelivery = _featHasDelivery
- };
- var req = new PosDataService.UpdateShopSettingsRequest(
- Features: features,
- OpenTime: string.IsNullOrWhiteSpace(_settingsOpenTime) ? null : _settingsOpenTime,
- CloseTime: string.IsNullOrWhiteSpace(_settingsCloseTime) ? null : _settingsCloseTime,
- OpenDays: _settingsOpenDays.Any() ? _settingsOpenDays : null);
- var ok = await DataService.UpdateShopSettingsAsync(_shopGuid.Value, req);
- _settingsSuccess = ok;
- _settingsMessage = ok ? "Đã lưu thiết lập thành công!" : "Lỗi khi lưu thiết lập.";
- StateHasChanged();
- }
-
- private void ToggleDay(string code)
- {
- if (_settingsOpenDays.Contains(code)) _settingsOpenDays.Remove(code);
- else _settingsOpenDays.Add(code);
- StateHasChanged();
- }
-
private void ConfigureSection()
{
switch (_section)
@@ -3079,47 +495,21 @@
}
}
- private static string FormatVND(decimal val) => val.ToString("N0") + " ₫";
-
- private async Task SwitchPromoTab(string tab)
+ private static string MapCategoryToVertical(string? category) => (category?.ToLowerInvariant()) switch
{
- _promoSubTab = tab;
- if (tab == "vouchers" && !_vouchers.Any())
- {
- _vouchers = await DataService.GetAdminVouchersAsync(_voucherCampaignFilter);
- StateHasChanged();
- }
- }
-
- private async Task LoadVouchers()
- {
- _vouchers = await DataService.GetAdminVouchersAsync(_voucherCampaignFilter);
- StateHasChanged();
- }
-
- private async Task RevokeVoucher(Guid voucherId)
- {
- var ok = await DataService.RevokeVoucherAsync(voucherId);
- if (ok) _vouchers = await DataService.GetAdminVouchersAsync(_voucherCampaignFilter);
- }
-
- private static string GetVoucherStatusLabel(string? status) => (status ?? "").ToLower() switch
- {
- "available" => "Chưa nhận", "claimed" => "Đã nhận", "redeemed" => "Đã dùng",
- "revoked" => "Thu hồi", "expired" => "Hết hạn", _ => status ?? "—"
+ "cafe" or "coffee" => "cafe",
+ "restaurant" or "nhahang" => "restaurant",
+ "karaoke" => "karaoke",
+ "spa" => "spa",
+ "beauty" or "salon" => "beauty",
+ "retail" or "banle" => "retail",
+ _ => "cafe"
};
- private static string GetVoucherStatusColor(string? status) => (status ?? "").ToLower() switch
- {
- "available" => "#F59E0B", "claimed" => "#3B82F6", "redeemed" => "#22C55E",
- "revoked" => "#EF4444", "expired" => "#888", _ => "#888"
- };
-
- // EN: Reusable empty state renderer with dynamic CTA href / VI: Renderer trạng thái trống với CTA href động
private RenderFragment RenderEmpty(string icon, string color, string title, string desc, string? ctaIcon = null, string? ctaLabel = null, string? ctaHref = null) => __builder =>
{
-
+
@title
@@ -3134,254 +524,25 @@
};
- private static string HexToRgb(string hex)
+ private RenderFragment RenderStubSection(string icon, string color, string title, string desc) => __builder =>
{
- hex = hex.TrimStart('#');
- if (hex.Length != 6) return "0,0,0";
- return $"{Convert.ToInt32(hex[..2], 16)},{Convert.ToInt32(hex[2..4], 16)},{Convert.ToInt32(hex[4..], 16)}";
- }
-
- ///
- /// EN: Map shop category to POS route segment.
- /// VI: Chuyển đổi danh mục cửa hàng thành segment POS route.
- ///
- private static string MapCategoryToVertical(string? category) => (category?.ToLowerInvariant()) switch
- {
- "cafe" or "coffee" => "cafe",
- "restaurant" or "nhahang" => "restaurant",
- "karaoke" => "karaoke",
- "spa" => "spa",
- "beauty" or "salon" => "beauty",
- "retail" or "banle" => "retail",
- _ => "cafe"
+
+
+
+
+
+
@title
+
@desc
+
+
+ Sẽ ra mắt trong phiên bản tiếp theo
+
+
+
};
- // ═══ CRUD ACTIONS ═══
- private async Task AddProduct()
- {
- _formMessage = null;
- if (string.IsNullOrWhiteSpace(_newProductName) || _newProductPrice <= 0 || !_shopGuid.HasValue)
- {
- _formMessage = "Vui lòng nhập tên và giá sản phẩm."; _formSuccess = false; return;
- }
- try
- {
- Guid? catId = Guid.TryParse(_newProductCategoryId, out var cid) ? cid : null;
- var imgUrl = await UploadFileIfNeeded(_productImageFile);
- await DataService.CreateProductAsync(new PosDataService.CreateProductRequest(
- _shopGuid.Value, _newProductName, _newProductDesc, _newProductPrice, _newProductType, imgUrl, null, catId));
- _formMessage = $"Đã thêm '{_newProductName}' thành công!"; _formSuccess = true;
- _newProductName = ""; _newProductPrice = 0; _newProductDesc = ""; _newProductCategoryId = ""; _productImageFile = null; _productImagePreview = null;
- _products = await DataService.GetAllProductsAsync(_shopGuid);
- }
- catch (Exception ex) { _formMessage = $"Lỗi: {ex.Message}"; _formSuccess = false; }
- }
-
- private async Task DeleteProduct(Guid productId)
- {
- try
- {
- await DataService.DeleteProductAsync(productId);
- _products = await DataService.GetAllProductsAsync(_shopGuid);
- }
- catch (Exception ex) { _errorMessage = $"Không thể xóa: {ex.Message}"; }
- }
-
- private void EditProduct(PosDataService.AdminProductInfo p)
- {
- _editingProductId = p.Id;
- _newProductName = p.Name;
- _newProductPrice = p.Price;
- _newProductType = p.Type ?? "PreparedFood";
- _newProductDesc = p.Description ?? "";
- _newProductCategoryId = p.CategoryId?.ToString() ?? "";
- _productImageFile = null;
- _productImagePreview = p.ImageUrl;
- _formMessage = null;
- _showProductForm = true;
- }
-
- private async Task SaveProduct()
- {
- _formMessage = null;
- if (string.IsNullOrWhiteSpace(_newProductName) || _newProductPrice <= 0 || !_shopGuid.HasValue || !_editingProductId.HasValue)
- {
- _formMessage = "Vui lòng nhập tên và giá sản phẩm."; _formSuccess = false; return;
- }
- try
- {
- Guid? catId = Guid.TryParse(_newProductCategoryId, out var cid2) ? cid2 : null;
- var imgUrl2 = await UploadFileIfNeeded(_productImageFile);
- await DataService.UpdateProductAsync(_editingProductId.Value, new PosDataService.CreateProductRequest(
- _shopGuid.Value, _newProductName, _newProductDesc, _newProductPrice, _newProductType, imgUrl2, null, catId));
- _formMessage = $"Đã cập nhật '{_newProductName}' thành công!"; _formSuccess = true;
- _editingProductId = null;
- _products = await DataService.GetAllProductsAsync(_shopGuid);
- }
- catch (Exception ex) { _formMessage = $"Lỗi: {ex.Message}"; _formSuccess = false; }
- }
-
- private async Task AddStaff()
- {
- _staffFormMessage = null;
- if (!_merchantId.HasValue)
- {
- _staffFormMessage = "Không tìm thấy thông tin merchant. Vui lòng tải lại trang."; _staffFormSuccess = false; return;
- }
- if (string.IsNullOrWhiteSpace(_newStaffCode))
- {
- _staffFormMessage = "Vui lòng nhập mã nhân viên."; _staffFormSuccess = false; return;
- }
- try
- {
- var docFrontUrl = await UploadFileIfNeeded(_staffDocFrontFile);
- var docBackUrl = await UploadFileIfNeeded(_staffDocBackFile);
- if (_createStaffAccount)
- {
- if (string.IsNullOrWhiteSpace(_newStaffEmail) || string.IsNullOrWhiteSpace(_newStaffPassword))
- {
- _staffFormMessage = "Vui lòng nhập đầy đủ email và mật khẩu."; _staffFormSuccess = false; return;
- }
- var (ok, err) = await DataService.InviteStaffWithAccountAsync(new PosDataService.InviteStaffWithAccountRequest(
- _newStaffEmail, _newStaffPassword, _newStaffFirstName, _newStaffLastName, _newStaffRole, _shopGuid));
- if (!ok) { _staffFormMessage = err ?? "Lỗi tạo tài khoản IAM. Kiểm tra email/mật khẩu."; _staffFormSuccess = false; return; }
- _staffFormMessage = $"Đã tạo tài khoản + mời NV '{_newStaffEmail}' thành công!"; _staffFormSuccess = true;
- }
- else
- {
- await DataService.CreateStaffAsync(new PosDataService.CreateStaffRequest(
- _merchantId.Value, _newStaffCode, _newStaffPhone, _newStaffEmail, _newStaffRole,
- _newStaffFirstName, _newStaffLastName, _newStaffAddress, null, docFrontUrl, docBackUrl));
- _staffFormMessage = $"Đã thêm NV '{_newStaffCode}' thành công!"; _staffFormSuccess = true;
- }
- _newStaffCode = ""; _newStaffPhone = ""; _newStaffEmail = ""; _newStaffFirstName = ""; _newStaffLastName = ""; _newStaffPassword = ""; _newStaffAddress = ""; _createStaffAccount = false;
- _staffDocFrontFile = null; _staffDocBackFile = null; _staffDocFrontPreview = null; _staffDocBackPreview = null;
- _staff = await DataService.GetStaffForShopAsync(_shopGuid.Value);
- }
- catch (Exception ex) { _staffFormMessage = $"Lỗi: {ex.Message}"; _staffFormSuccess = false; }
- }
-
- private void EditStaff(PosDataService.StaffInfo s)
- {
- _editingStaffId = s.Id;
- _newStaffCode = s.EmployeeCode ?? "";
- _newStaffRole = s.Role ?? "Cashier";
- _newStaffPhone = s.Phone ?? "";
- _newStaffEmail = s.Email ?? "";
- _newStaffFirstName = s.FirstName ?? "";
- _newStaffLastName = s.LastName ?? "";
- _newStaffAddress = s.Address ?? "";
- _staffDocFrontFile = null; _staffDocBackFile = null;
- _staffDocFrontPreview = s.DocumentFrontUrl;
- _staffDocBackPreview = s.DocumentBackUrl;
- _staffFormMessage = null;
- _showStaffForm = true;
- }
-
- private async Task SaveStaffEdit()
- {
- _staffFormMessage = null;
- if (string.IsNullOrWhiteSpace(_newStaffCode) || !_merchantId.HasValue || !_editingStaffId.HasValue)
- {
- _staffFormMessage = "Vui lòng nhập Mã NV."; _staffFormSuccess = false; return;
- }
- try
- {
- var docFrontUrl = await UploadFileIfNeeded(_staffDocFrontFile) ?? (_staffDocFrontPreview?.StartsWith("data:") == true ? null : _staffDocFrontPreview);
- var docBackUrl = await UploadFileIfNeeded(_staffDocBackFile) ?? (_staffDocBackPreview?.StartsWith("data:") == true ? null : _staffDocBackPreview);
- await DataService.UpdateStaffAsync(_editingStaffId.Value, new PosDataService.CreateStaffRequest(
- _merchantId.Value, _newStaffCode, _newStaffPhone, _newStaffEmail, _newStaffRole,
- _newStaffFirstName, _newStaffLastName, _newStaffAddress, null, docFrontUrl, docBackUrl));
- _staffFormMessage = $"Đã cập nhật NV '{_newStaffCode}' thành công!"; _staffFormSuccess = true;
- _editingStaffId = null;
- _staffDocFrontFile = null; _staffDocBackFile = null; _staffDocFrontPreview = null; _staffDocBackPreview = null;
- _staff = await DataService.GetStaffForShopAsync(_shopGuid.Value);
- }
- catch (Exception ex) { _staffFormMessage = $"Lỗi: {ex.Message}"; _staffFormSuccess = false; }
- }
-
- private async Task DeleteStaffMember(Guid staffId)
- {
- try
- {
- await DataService.DeleteStaffAsync(staffId);
- _staff = await DataService.GetStaffForShopAsync(_shopGuid.Value);
- }
- catch (Exception ex) { _errorMessage = $"Không thể xóa nhân viên: {ex.Message}"; }
- }
-
- // EN: Day-of-week label / VI: Nhãn ngày trong tuần
- private static string DayLabel(int dow) => dow switch
- {
- 0 => "CN", 1 => "T2", 2 => "T3", 3 => "T4",
- 4 => "T5", 5 => "T6", 6 => "T7", _ => $"#{dow}"
- };
-
- // ═══ CATEGORY CRUD ═══
- private async Task SaveCategory()
- {
- if (string.IsNullOrWhiteSpace(_newCategoryName)) { _categoryFormMessage = "Tên danh mục không được trống."; _categoryFormSuccess = false; return; }
- var catImgUrl = await UploadFileIfNeeded(_categoryImageFile) ?? _newCategoryImageUrl;
- var req = new PosDataService.AdminCreateCategoryRequest(_shopGuid ?? Guid.Empty, _newCategoryName, _newCategoryDesc, _newCategoryOrder, string.IsNullOrWhiteSpace(catImgUrl) ? null : catImgUrl);
- bool ok;
- if (_editingCategoryId.HasValue)
- ok = await DataService.UpdateCategoryAsync(_editingCategoryId.Value, req);
- else
- ok = await DataService.CreateCategoryAsync(req);
- _categoryFormMessage = ok ? (_editingCategoryId.HasValue ? "Đã cập nhật danh mục!" : "Đã thêm danh mục!") : "Lỗi khi lưu danh mục.";
- _categoryFormSuccess = ok;
- if (ok) { _showCategoryForm = false; _categoryImageFile = null; _categoryImagePreview = null; _newCategoryImageUrl = ""; _categories = await DataService.GetAllCategoriesAsync(_shopGuid); }
- }
- private void EditCategory(PosDataService.AdminCategoryInfo c) { _editingCategoryId = c.Id; _newCategoryName = c.Name ?? ""; _newCategoryDesc = c.Description ?? ""; _newCategoryOrder = c.DisplayOrder; _newCategoryImageUrl = c.ImageUrl ?? ""; _categoryImageFile = null; _categoryImagePreview = c.ImageUrl; _showCategoryForm = true; _categoryFormMessage = null; }
- private async Task DeleteCategoryItem(Guid id) { await DataService.DeleteCategoryAsync(id); _categories = await DataService.GetAllCategoriesAsync(_shopGuid); }
-
- // ═══ INVENTORY OPERATIONS ═══
- private async Task SwitchInvSubTab(string tab)
- {
- _invSubTab = tab;
- _invFormMessage = null;
- if (tab == "low-stock") await LoadLowStock();
- StateHasChanged();
- }
- private async Task LoadLowStock()
- {
- _lowStockItems = await DataService.GetLowStockAsync(_shopGuid);
- StateHasChanged();
- }
- private async Task DoStockIn()
- {
- _invFormMessage = null;
- if (_invSelectedProductId == Guid.Empty || _invAmount <= 0) { _invFormMessage = "Vui lòng chọn sản phẩm và nhập số lượng > 0."; _invFormSuccess = false; return; }
- var ok = await DataService.StockInAsync(new PosDataService.StockInRequest(_invSelectedProductId, _shopGuid!.Value, _invAmount, string.IsNullOrWhiteSpace(_invNotes) ? null : _invNotes));
- _invFormMessage = ok ? $"Đã nhập kho thành công +{_invAmount}!" : "Lỗi khi nhập kho.";
- _invFormSuccess = ok;
- if (ok) { _invAmount = 0; _invNotes = ""; _inventory = await DataService.GetInventoryAsync(_shopGuid); _invTxns = await DataService.GetInventoryTransactionsAsync(_shopGuid); }
- }
- private async Task DoStockOut()
- {
- _invFormMessage = null;
- if (_invSelectedProductId == Guid.Empty || _invAmount <= 0) { _invFormMessage = "Vui lòng chọn sản phẩm và nhập số lượng > 0."; _invFormSuccess = false; return; }
- var ok = await DataService.StockOutAsync(new PosDataService.StockOutRequest(_invSelectedProductId, _shopGuid!.Value, _invAmount, string.IsNullOrWhiteSpace(_invNotes) ? null : _invNotes));
- _invFormMessage = ok ? $"Đã xuất kho thành công -{_invAmount}!" : "Lỗi khi xuất kho. Kiểm tra số lượng tồn.";
- _invFormSuccess = ok;
- if (ok) { _invAmount = 0; _invNotes = ""; _inventory = await DataService.GetInventoryAsync(_shopGuid); _invTxns = await DataService.GetInventoryTransactionsAsync(_shopGuid); }
- }
- private async Task DoAdjustStock()
- {
- _invFormMessage = null;
- if (_invSelectedProductId == Guid.Empty || string.IsNullOrWhiteSpace(_invNotes)) { _invFormMessage = "Vui lòng chọn sản phẩm và nhập lý do điều chỉnh."; _invFormSuccess = false; return; }
- var ok = await DataService.AdjustStockAsync(new PosDataService.AdjustStockRequest(_invSelectedProductId, _shopGuid!.Value, _invNewQty, _invNotes));
- _invFormMessage = ok ? $"Đã điều chỉnh tồn kho = {_invNewQty}!" : "Lỗi khi điều chỉnh.";
- _invFormSuccess = ok;
- if (ok) { _invNewQty = 0; _invNotes = ""; _inventory = await DataService.GetInventoryAsync(_shopGuid); _invTxns = await DataService.GetInventoryTransactionsAsync(_shopGuid); }
- }
-
- // ═══ ORDER DETAIL ═══
- private async Task ViewOrderDetail(Guid orderId) { if (_selectedOrderId == orderId) { _selectedOrderId = null; _orderDetail = null; return; } _selectedOrderId = orderId; try { _orderDetail = await DataService.GetOrderDetailAsync(orderId, _shopGuid); } catch { _orderDetail = null; } }
- private async Task CancelOrderItem(Guid orderId) { var ok = await DataService.CancelOrderAsync(orderId); if (ok) { _selectedOrderId = null; _orderDetail = null; await LoadData(); } }
-
- // ═══ SHOP EDIT ═══
private void StartEditShop() { _editingShop = true; _shopEditName = _shopName ?? ""; _shopEditPhone = ""; _shopEditEmail = ""; _shopEditDesc = ""; _shopEditOpenTime = ""; _shopEditCloseTime = ""; _shopEditMessage = null; }
+
private async Task SaveShopEdit()
{
var req = new PosDataService.UpdateShopRequest(_shopEditName, _shopEditPhone, _shopEditEmail, _shopEditDesc, _shopEditOpenTime, _shopEditCloseTime, null);
@@ -3391,453 +552,6 @@
if (ok) { _editingShop = false; await LoadData(); }
}
- // ═══ REVENUE REPORT ═══
- private async Task LoadRevenueReport(string period) { _reportPeriod = period; _revenueReport = await DataService.GetRevenueReportAsync(period, _shopGuid); }
-
- // ═══ CAMPAIGN CRUD ═══
- private async Task SaveCampaign()
- {
- _campaignFormMessage = null;
- if (string.IsNullOrWhiteSpace(_newCampaignName) || _newCampaignValue <= 0 || _newCampaignVouchers <= 0)
- {
- _campaignFormMessage = "Vui lòng nhập đầy đủ tên, giá trị và số lượng voucher."; _campaignFormSuccess = false; return;
- }
- var desc = string.IsNullOrWhiteSpace(_newCampaignDesc) ? _newCampaignDiscountType : $"{_newCampaignDesc} [{_newCampaignDiscountType}]";
- if (_newCampaignDiscountType == "percentage") desc = $"Giảm {_newCampaignValue}% [{_newCampaignDiscountType}]";
- var req = new PosDataService.CreateCampaignRequest(_newCampaignName, desc, _newCampaignValue, _newCampaignVouchers, _newCampaignStart, _newCampaignEnd);
- bool ok;
- if (_editingCampaignId.HasValue)
- ok = await DataService.UpdateCampaignAsync(_editingCampaignId.Value, req);
- else
- ok = await DataService.CreateCampaignAsync(req);
- _campaignFormMessage = ok ? (_editingCampaignId.HasValue ? "Đã cập nhật chiến dịch!" : "Đã thêm chiến dịch!") : "Lỗi khi lưu chiến dịch.";
- _campaignFormSuccess = ok;
- if (ok) { _showCampaignForm = false; _editingCampaignId = null; _campaigns = await DataService.GetCampaignsAsync(); }
- }
-
- private void EditCampaign(PosDataService.CampaignInfo c)
- {
- _editingCampaignId = c.Id; _newCampaignName = c.Name; _newCampaignDesc = c.Description ?? "";
- _newCampaignValue = c.FaceValue; _newCampaignVouchers = c.TotalVouchers;
- _newCampaignStart = c.StartDate?.ToLocalTime().Date ?? DateTime.Today;
- _newCampaignEnd = c.EndDate?.ToLocalTime().Date ?? DateTime.Today.AddMonths(1);
- _showCampaignForm = true; _campaignFormMessage = null;
- }
-
- private async Task DeleteCampaignItem(Guid campaignId)
- {
- await DataService.DeleteCampaignAsync(campaignId);
- _campaigns = await DataService.GetCampaignsAsync();
- }
-
- // ═══ MEMBER CRUD ═══
- private string? _memberFormMessage;
- private bool _memberFormSuccess;
- private async Task SaveMember()
- {
- _memberFormMessage = null;
- if (_editingMemberId.HasValue)
- {
- var ok = await DataService.UpdateMemberAsync(_editingMemberId.Value, new PosDataService.UpdateMemberRequest(_newMemberGender, null));
- if (ok) { _showMemberForm = false; _editingMemberId = null; _newMemberName = ""; _newMemberPhone = ""; await RefreshMembersAndLevels(); }
- }
- else
- {
- var (ok, err) = await DataService.CreateMemberAsync(new PosDataService.CreateMemberRequest(_newMemberGender, _newMemberCountry,
- string.IsNullOrWhiteSpace(_newMemberName) ? null : _newMemberName,
- string.IsNullOrWhiteSpace(_newMemberPhone) ? null : _newMemberPhone));
- if (ok) { _showMemberForm = false; _editingMemberId = null; _newMemberName = ""; _newMemberPhone = ""; _memberFormMessage = null; await RefreshMembersAndLevels(); }
- else { _memberFormMessage = err ?? "Lỗi tạo khách hàng."; _memberFormSuccess = false; }
- }
- }
-
- private void EditMember(PosDataService.MemberInfo m)
- {
- _editingMemberId = m.Id; _newMemberGender = m.Gender ?? ""; _newMemberCountry = m.CountryCode ?? "VN";
- _newMemberName = m.DisplayName ?? ""; _newMemberPhone = m.Phone ?? "";
- _showMemberForm = true;
- }
-
- private async Task DeleteMemberItem(Guid memberId)
- {
- await DataService.DeleteMemberAsync(memberId);
- await RefreshMembersAndLevels();
- }
-
- // ═══ LEVEL CRUD ═══
- private async Task SaveLevel()
- {
- _levelFormMessage = null;
- if (string.IsNullOrWhiteSpace(_newLevelName) || _newLevelNumber <= 0)
- { _levelFormMessage = "Nhập tên và số level."; _levelFormSuccess = false; return; }
- var req = new PosDataService.CreateLevelRequest(_newLevelNumber, _newLevelName, _newLevelRequiredExp, _newLevelDescription, _newLevelBadgeColor);
- if (_editingLevelId.HasValue)
- {
- var (ok, err) = await DataService.UpdateLevelAsync(_editingLevelId.Value, req);
- _levelFormSuccess = ok; _levelFormMessage = ok ? "Đã cập nhật!" : err ?? "Lỗi.";
- }
- else
- {
- var (ok, err) = await DataService.CreateLevelAsync(req);
- _levelFormSuccess = ok; _levelFormMessage = ok ? "Đã thêm!" : err ?? "Lỗi.";
- }
- if (_levelFormSuccess) { _showLevelForm = false; await RefreshMembersAndLevels(); }
- }
- private void EditLevel(PosDataService.LevelDefinitionInfo lvl)
- {
- _editingLevelId = lvl.Id; _newLevelNumber = lvl.LevelNumber; _newLevelName = lvl.Name;
- _newLevelRequiredExp = lvl.RequiredExp; _newLevelDescription = lvl.Description ?? "";
- _newLevelBadgeColor = lvl.BadgeColor ?? "#CD7F32"; _showLevelForm = true;
- }
- private async Task DeleteLevel(Guid id)
- {
- await DataService.DeleteLevelAsync(id);
- await RefreshMembersAndLevels();
- }
-
- // ═══ EXP MANAGEMENT ═══
- private async Task AddExpToMember()
- {
- _expFormMessage = null;
- if (!_expMemberId.HasValue || _expPoints <= 0)
- { _expFormMessage = "Chọn thành viên và nhập số điểm > 0."; _expFormSuccess = false; return; }
- var result = await DataService.AddExperienceAsync(_expMemberId.Value, new PosDataService.AddExpRequest(_expPoints, _expSourceId, null));
- if (result != null)
- {
- _expFormSuccess = true;
- _expFormMessage = result.LeveledUp ? $"Đã cộng {result.PointsAdded} EXP! Lên level {result.CurrentLevel}!" : $"Đã cộng {result.PointsAdded} EXP! Tổng: {result.TotalExpEarned}";
- _memberProgress = await DataService.GetMemberProgressAsync(_expMemberId.Value);
- _expHistory = await DataService.GetExperienceHistoryAsync(_expMemberId.Value);
- await RefreshMembersAndLevels();
- }
- else { _expFormMessage = "Lỗi khi cộng EXP."; _expFormSuccess = false; }
- }
- private async Task LoadMemberProgress()
- {
- if (_expMemberId.HasValue)
- {
- _memberProgress = await DataService.GetMemberProgressAsync(_expMemberId.Value);
- _expHistory = await DataService.GetExperienceHistoryAsync(_expMemberId.Value);
- }
- else { _memberProgress = null; _expHistory = new(); }
- }
-
- // ═══ IMAGE UPLOAD HELPERS ═══
- private async Task OnProductImageSelected(Microsoft.AspNetCore.Components.Forms.InputFileChangeEventArgs e)
- {
- _productImageFile = e.File;
- var format = "image/png";
- var resized = await e.File.RequestImageFileAsync(format, 200, 200);
- var buffer = new byte[resized.Size];
- await resized.OpenReadStream().ReadAsync(buffer);
- _productImagePreview = $"data:{format};base64,{Convert.ToBase64String(buffer)}";
- StateHasChanged();
- }
- private async Task OnCategoryImageSelected(Microsoft.AspNetCore.Components.Forms.InputFileChangeEventArgs e)
- {
- _categoryImageFile = e.File;
- var format = "image/png";
- var resized = await e.File.RequestImageFileAsync(format, 200, 200);
- var buffer = new byte[resized.Size];
- await resized.OpenReadStream().ReadAsync(buffer);
- _categoryImagePreview = $"data:{format};base64,{Convert.ToBase64String(buffer)}";
- StateHasChanged();
- }
- private async Task OnStaffDocFrontSelected(Microsoft.AspNetCore.Components.Forms.InputFileChangeEventArgs e)
- {
- _staffDocFrontFile = e.File;
- var format = "image/png";
- var resized = await e.File.RequestImageFileAsync(format, 300, 200);
- var buffer = new byte[resized.Size];
- await resized.OpenReadStream().ReadAsync(buffer);
- _staffDocFrontPreview = $"data:{format};base64,{Convert.ToBase64String(buffer)}";
- StateHasChanged();
- }
- private async Task OnStaffDocBackSelected(Microsoft.AspNetCore.Components.Forms.InputFileChangeEventArgs e)
- {
- _staffDocBackFile = e.File;
- var format = "image/png";
- var resized = await e.File.RequestImageFileAsync(format, 300, 200);
- var buffer = new byte[resized.Size];
- await resized.OpenReadStream().ReadAsync(buffer);
- _staffDocBackPreview = $"data:{format};base64,{Convert.ToBase64String(buffer)}";
- StateHasChanged();
- }
- private async Task
UploadFileIfNeeded(Microsoft.AspNetCore.Components.Forms.IBrowserFile? file)
- {
- if (file == null) return null;
- using var stream = file.OpenReadStream(maxAllowedSize: 10_485_760);
- return await DataService.UploadImageAsync(stream, file.Name, file.ContentType);
- }
-
- // ═══ 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}"; }
- }
-
- // ═══ ZONE MANAGEMENT ═══
- private async Task SaveZone()
- {
- _zoneFormMessage = null;
- if (string.IsNullOrWhiteSpace(_newZoneName) || !_shopGuid.HasValue)
- {
- _zoneFormMessage = "Vui lòng nhập tên khu vực."; _zoneFormSuccess = false; return;
- }
- if (_editingZoneOriginalName != null)
- {
- // Rename: update all tables in old zone to new zone name
- var tablesInZone = _tables.Where(t => (t.Zone ?? "Chung") == _editingZoneOriginalName).ToList();
- try
- {
- foreach (var t in tablesInZone)
- await DataService.UpdateTableAsync(t.Id, new PosDataService.CreateTableRequest(_shopGuid.Value, t.TableNumber, t.Capacity, _newZoneName.Trim()));
- _tables = await DataService.GetTablesAsync(_shopGuid.Value);
- _zoneFormMessage = $"Đã đổi tên '{_editingZoneOriginalName}' → '{_newZoneName.Trim()}'"; _zoneFormSuccess = true;
- _editingZoneOriginalName = null; _newZoneName = ""; _showZoneForm = false;
- }
- catch (Exception ex) { _zoneFormMessage = $"Lỗi: {ex.Message}"; _zoneFormSuccess = false; }
- }
- else
- {
- var zoneName = _newZoneName.Trim();
- if (!_customZones.Contains(zoneName) && !_tables.Any(t => (t.Zone ?? "Chung") == zoneName))
- _customZones.Add(zoneName);
- _zoneFormMessage = $"Đã thêm khu vực '{zoneName}'."; _zoneFormSuccess = true;
- _newTableZone = zoneName;
- _newZoneName = "";
- _showZoneForm = false;
- }
- }
-
- // ═══ 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}"; }
- }
-
- private string? _toastMessage;
- private void ShowComingSoonPromo() => ShowComingSoon("Tặng ưu đãi");
- private void ShowComingSoonMessage() => ShowComingSoon("Gửi tin nhắn");
- private void GoToOrderHistory() => Nav.NavigateTo($"/admin/shop/{ShopId}/finance");
private async void ShowComingSoon(string feature)
{
_toastMessage = $"{feature} — tính năng sắp ra mắt!";
@@ -3846,31 +560,4 @@
_toastMessage = null;
StateHasChanged();
}
-
- private async Task RefreshMembersAndLevels()
- {
- var rm = await DataService.GetMembersAsync();
- var rl = await DataService.GetMembershipLevelsAsync();
- _memberLevels = PosDataService.EnrichLevelDefinitions(rl, rm);
- _members = PosDataService.ResolveMemberLevelNames(rm, rl);
- }
-
- private void ToggleFolderForm() => _showFolderForm = !_showFolderForm;
- private void HideFolderForm() => _showFolderForm = false;
- private List _storageFiles = new();
- private List _storageFolders = new();
- private Guid? _currentFolderId;
- private string _newFolderName = "";
- private bool _showFolderForm;
- private string _storageSearch = "";
- private async Task SearchStorageFiles() => _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch);
- private async Task NavigateToFolder(Guid id) { _currentFolderId = id; _storageFolders = await DataService.GetFoldersAsync(id); _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch); }
- private async Task NavigateToParentFolder() { _currentFolderId = null; _storageFolders = await DataService.GetFoldersAsync(null); _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch); }
- private async Task CreateFolder() { if (string.IsNullOrWhiteSpace(_newFolderName)) return; await DataService.CreateFolderAsync(new PosDataService.CreateFolderRequest(_newFolderName.Trim(), _currentFolderId)); _newFolderName = ""; _showFolderForm = false; _storageFolders = await DataService.GetFoldersAsync(_currentFolderId); }
- private async Task DeleteFolder(Guid id) { await DataService.DeleteFolderAsync(id); _storageFolders = await DataService.GetFoldersAsync(_currentFolderId); }
- private async Task DeleteStorageFile(Guid id) { await DataService.DeleteStorageFileAsync(id); _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch); }
- private async Task DownloadStorageFile(Guid id) { var url = await DataService.GetDownloadUrlAsync(id); if (url != null) Nav.NavigateTo(url, forceLoad: true); }
- private async Task HandleDriveUpload(InputFileChangeEventArgs e) { foreach (var f in e.GetMultipleFiles(20)) { using var s = f.OpenReadStream(10_485_760); await DataService.UploadImageAsync(s, f.Name, f.ContentType); } _storageFiles = await DataService.GetStorageFilesAsync(search: _storageSearch); }
- private static string FormatFileSize(long b) => b < 1024 ? $"{b} B" : b < 1048576 ? $"{b/1024.0:F1} KB" : b < 1073741824 ? $"{b/1048576.0:F1} MB" : $"{b/1073741824.0:F2} GB";
- private static string GetFileIcon(string? ct) => ct switch { string s when s.StartsWith("image/") => "image", string s when s.StartsWith("video/") => "video", string s when s.Contains("pdf") => "file-text", _ => "file" };
}
diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPromotions.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPromotions.razor
new file mode 100644
index 00000000..00840826
--- /dev/null
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopPromotions.razor
@@ -0,0 +1,268 @@
+@using WebClientTpos.Client.Services
+@using WebClientTpos.Client.Pages.Admin.Shop
+@inject PosDataService DataService
+
+
+
+ Chiến dịch khuyến mãi chung cho tất cả cửa hàng trong thương hiệu
+
+@* ─── Sub-tabs: Campaigns | Vouchers ─── *@
+
+ @{ var promoTabs = new[] { ("campaigns", "Chiến dịch", "tag"), ("vouchers", "Mã voucher", "ticket") }; }
+ @foreach (var (tab, label, icon) in promoTabs)
+ {
+ var t = tab;
+ var isActive = _promoSubTab == t;
+ SwitchPromoTab(t))"
+ class="@(isActive ? "admin-btn-primary" : "")"
+ style="padding:8px 16px;border-radius:8px;border:1px solid var(--admin-border-subtle);font-size:13px;display:inline-flex;align-items:center;gap:6px;cursor:pointer;@(isActive ? "background:var(--admin-orange-primary);color:#FFF;font-weight:700;border-color:var(--admin-orange-primary);" : "background:var(--admin-bg-elevated);color:var(--admin-text-secondary);font-weight:500;")">
+ @label
+
+ }
+
+@if (_promoSubTab == "campaigns")
+{
+
+
@_campaigns.Count chiến dịch
+ { _showCampaignForm = !_showCampaignForm; _editingCampaignId = null; _newCampaignName = ""; _newCampaignDesc = ""; _newCampaignValue = 0; _newCampaignVouchers = 0; _newCampaignDiscountType = "fixed"; _newCampaignStart = DateTime.Today; _newCampaignEnd = DateTime.Today.AddMonths(1); _campaignFormMessage = null; }'>
+ Thêm chiến dịch
+
+
+@if (_showCampaignForm)
+{
+
+
+
+
+ @if (_campaignFormMessage != null)
+ {
+
@_campaignFormMessage
+ }
+
+ Lưu
+ { _showCampaignForm = false; _campaignFormMessage = null; }'>Hủy
+
+
+
+}
+@if (!_campaigns.Any())
+{
+
+}
+else
+{
+
+
@_campaigns.Count Tổng chiến dịch
+
@_campaigns.Count(c => c.Status == "Active") Đang hoạt động
+
@_campaigns.Sum(c => c.TotalVouchers) Tổng voucher
+
@_campaigns.Sum(c => c.IssuedVouchers) Đã phát
+
+
+
+
+
+ Tên
+ Loại
+ Giá trị
+ Đã phát/Tổng
+ Bắt đầu
+ Kết thúc
+
+
+ @foreach (var c in _campaigns)
+ {
+
+ @c.Name
+ @(c.Description?.Contains("%") == true ? "%" : "₫")
+ @(c.Description?.Contains("%") == true ? $"{c.FaceValue}%" : ShopHelpers.FormatVND(c.FaceValue))
+ @c.IssuedVouchers / @c.TotalVouchers
+ @(c.StartDate?.ToString("dd/MM/yy") ?? "—")
+ @(c.EndDate?.ToString("dd/MM/yy") ?? "—")
+
+
+ EditCampaign(c))" style="background:rgba(59,130,246,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Sửa">
+ DeleteCampaignItem(c.Id))" style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Xóa">
+
+
+
+ }
+
+
+
+} @* end else *@
+} @* end campaigns sub-tab *@
+@if (_promoSubTab == "vouchers")
+{
+
+
@_vouchers.Count mã voucher
+
+ Làm mới
+
+
+ @if (!_vouchers.Any())
+ {
+
+ }
+ else
+ {
+
+
+
+
+ Mã
+ Chiến dịch
+ Mệnh giá
+ Còn lại
+ Trạng thái
+ Ngày tạo
+
+
+ @foreach (var v in _vouchers)
+ {
+
+ @v.Code
+ @(v.CampaignName ?? "—")
+ @ShopHelpers.FormatVND(v.FaceValue)
+ @ShopHelpers.FormatVND(v.RemainingValue)
+ @GetVoucherStatusLabel(v.Status)
+ @(v.CreatedAt?.ToLocalTime().ToString("dd/MM/yy") ?? "—")
+
+ @if (v.Status?.ToLower() == "available")
+ {
+ RevokeVoucher(v.Id))" style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;padding:4px 10px;font-size:11px;color:#EF4444;font-weight:600;cursor:pointer;" title="Thu hồi">Thu hồi
+ }
+
+
+ }
+
+
+
+ }
+}
+
+@code {
+ [Parameter] public Guid ShopId { get; set; }
+
+ private List _campaigns = new();
+ private bool _showCampaignForm;
+ private Guid? _editingCampaignId;
+ private string _newCampaignName = "";
+ private string _newCampaignDesc = "";
+ private decimal _newCampaignValue;
+ private int _newCampaignVouchers;
+ private DateTime _newCampaignStart = DateTime.Today;
+ private DateTime _newCampaignEnd = DateTime.Today.AddMonths(1);
+ private string _newCampaignDiscountType = "fixed";
+ private string? _campaignFormMessage;
+ private bool _campaignFormSuccess;
+ private string _promoSubTab = "campaigns";
+ private List _vouchers = new();
+ private Guid? _voucherCampaignFilter;
+
+ protected override async Task OnInitializedAsync()
+ {
+ _campaigns = await DataService.GetCampaignsAsync();
+ }
+
+ private async Task SwitchPromoTab(string tab)
+ {
+ _promoSubTab = tab;
+ if (tab == "vouchers" && !_vouchers.Any())
+ {
+ _vouchers = await DataService.GetAdminVouchersAsync(_voucherCampaignFilter);
+ StateHasChanged();
+ }
+ }
+
+ private async Task LoadVouchers()
+ {
+ _vouchers = await DataService.GetAdminVouchersAsync(_voucherCampaignFilter);
+ StateHasChanged();
+ }
+
+ private async Task RevokeVoucher(Guid voucherId)
+ {
+ var ok = await DataService.RevokeVoucherAsync(voucherId);
+ if (ok) _vouchers = await DataService.GetAdminVouchersAsync(_voucherCampaignFilter);
+ }
+
+ private static string GetVoucherStatusLabel(string? status) => (status ?? "").ToLower() switch
+ {
+ "available" => "Chưa nhận", "claimed" => "Đã nhận", "redeemed" => "Đã dùng",
+ "revoked" => "Thu hồi", "expired" => "Hết hạn", _ => status ?? "—"
+ };
+
+ private static string GetVoucherStatusColor(string? status) => (status ?? "").ToLower() switch
+ {
+ "available" => "#F59E0B", "claimed" => "#3B82F6", "redeemed" => "#22C55E",
+ "revoked" => "#EF4444", "expired" => "#888", _ => "#888"
+ };
+
+ private async Task SaveCampaign()
+ {
+ _campaignFormMessage = null;
+ if (string.IsNullOrWhiteSpace(_newCampaignName) || _newCampaignValue <= 0 || _newCampaignVouchers <= 0)
+ {
+ _campaignFormMessage = "Vui lòng nhập đầy đủ tên, giá trị và số lượng voucher."; _campaignFormSuccess = false; return;
+ }
+ var desc = string.IsNullOrWhiteSpace(_newCampaignDesc) ? _newCampaignDiscountType : $"{_newCampaignDesc} [{_newCampaignDiscountType}]";
+ if (_newCampaignDiscountType == "percentage") desc = $"Giảm {_newCampaignValue}% [{_newCampaignDiscountType}]";
+ var req = new PosDataService.CreateCampaignRequest(_newCampaignName, desc, _newCampaignValue, _newCampaignVouchers, _newCampaignStart, _newCampaignEnd);
+ bool ok;
+ if (_editingCampaignId.HasValue)
+ ok = await DataService.UpdateCampaignAsync(_editingCampaignId.Value, req);
+ else
+ ok = await DataService.CreateCampaignAsync(req);
+ _campaignFormMessage = ok ? (_editingCampaignId.HasValue ? "Đã cập nhật chiến dịch!" : "Đã thêm chiến dịch!") : "Lỗi khi lưu chiến dịch.";
+ _campaignFormSuccess = ok;
+ if (ok) { _showCampaignForm = false; _editingCampaignId = null; _campaigns = await DataService.GetCampaignsAsync(); }
+ }
+
+ private void EditCampaign(PosDataService.CampaignInfo c)
+ {
+ _editingCampaignId = c.Id; _newCampaignName = c.Name; _newCampaignDesc = c.Description ?? "";
+ _newCampaignValue = c.FaceValue; _newCampaignVouchers = c.TotalVouchers;
+ _newCampaignStart = c.StartDate?.ToLocalTime().Date ?? DateTime.Today;
+ _newCampaignEnd = c.EndDate?.ToLocalTime().Date ?? DateTime.Today.AddMonths(1);
+ _showCampaignForm = true; _campaignFormMessage = null;
+ }
+
+ private async Task DeleteCampaignItem(Guid campaignId)
+ {
+ await DataService.DeleteCampaignAsync(campaignId);
+ _campaigns = await DataService.GetCampaignsAsync();
+ }
+}
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
new file mode 100644
index 00000000..b02a8fcc
--- /dev/null
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopRecipes.razor
@@ -0,0 +1,155 @@
+@using WebClientTpos.Client.Services
+@using WebClientTpos.Client.Pages.Admin.Shop
+@inject PosDataService DataService
+
+
+
@_recipes.Count công thức
+ { _showRecipeForm = !_showRecipeForm; _editingRecipeId = null; _newRecipeName = ""; _newRecipeInstructions = ""; _newRecipePrepTime = 5; _recipeIngredients = new(); _recipeFormMessage = null; }'>
+ Thêm công thức
+
+
+@if (_showRecipeForm)
+{
+
+
+
+
+
Tên công thức *
+
Thời gian chuẩn bị (phút)
+
Hướng dẫn
+
+
+
Nguyên liệu _recipeIngredients.Add(new("","","",0,0))' style="padding:4px 10px;border-radius:6px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-primary);font-size:12px;cursor:pointer;">+ Thêm
+ @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;" />
+ _recipeIngredients.RemoveAt(i)' style="padding:6px;border-radius:6px;border:none;background:rgba(239,68,68,0.1);color:#EF4444;cursor:pointer;">✕
+
+ }
+
+
+ Lưu
+ _showRecipeForm = false' style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-secondary);cursor:pointer;"> Hủy
+
+ @if (_recipeFormMessage != null) {
@_recipeFormMessage
}
+
+
+}
+@if (!_recipes.Any())
+{
+ @RenderEmpty("flask-conical", "#FF5C00", "Chưa có công thức", "Thêm công thức và nguyên liệu pha chế")
+}
+else
+{
+
+ @foreach (var recipe in _recipes)
+ {
+ var isExpanded = _expandedRecipeId == recipe.Id;
+
{ _expandedRecipeId = isExpanded ? null : recipe.Id; StateHasChanged(); }'>
+
+
+
@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;">
+
+
+
@recipe.PrepTimeMinutes phút chuẩn bị
+ @if (isExpanded && !string.IsNullOrEmpty(recipe.Instructions))
+ {
+
@recipe.Instructions
+ }
+
+
+ }
+
+}
+
+@code {
+ [Parameter] public Guid ShopId { get; set; }
+
+ // Recipes state
+ private List _recipes = new();
+ private bool _showRecipeForm;
+ private Guid? _editingRecipeId;
+ private string _newRecipeName = "";
+ private string _newRecipeInstructions = "";
+ private int _newRecipePrepTime = 5;
+ private List<(string Name, string Unit, string Qty, decimal Quantity, decimal Cost)> _recipeIngredients = new();
+ private Guid? _expandedRecipeId;
+ private string? _recipeFormMessage;
+ private bool _recipeFormSuccess;
+ private string? _errorMessage;
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (ShopId != Guid.Empty)
+ _recipes = await DataService.GetRecipesAsync(ShopId);
+ }
+
+ // ═══ RECIPE CRUD ═══
+ private async Task SaveRecipe()
+ {
+ _recipeFormMessage = null;
+ if (string.IsNullOrWhiteSpace(_newRecipeName) || ShopId == Guid.Empty)
+ {
+ _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(ShopId, 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(ShopId); }
+ }
+ 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(ShopId);
+ }
+ catch (Exception ex) { _errorMessage = $"Không thể xóa công thức: {ex.Message}"; }
+ }
+
+ private RenderFragment RenderEmpty(string icon, string color, string title, string desc, string? ctaIcon = null, string? ctaLabel = null, string? ctaHref = null) => __builder =>
+ {
+
+
+
+
+
@title
+
@desc
+ @if (ctaIcon != null && ctaLabel != null)
+ {
+
+
+ @ctaLabel
+
+ }
+
+ };
+
+ private static string HexToRgb(string hex)
+ {
+ hex = hex.TrimStart('#');
+ if (hex.Length != 6) return "0,0,0";
+ return $"{Convert.ToInt32(hex[..2], 16)},{Convert.ToInt32(hex[2..4], 16)},{Convert.ToInt32(hex[4..], 16)}";
+ }
+}
diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopReports.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopReports.razor
new file mode 100644
index 00000000..51de72b7
--- /dev/null
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopReports.razor
@@ -0,0 +1,135 @@
+@using WebClientTpos.Client.Services
+@using WebClientTpos.Client.Pages.Admin.Shop
+@inject PosDataService DataService
+
+
+
@ShopHelpers.FormatVND(_reportOrders.Sum(o => o.TotalAmount)) Tổng doanh thu
+
@_reportOrders.Count Tổng đơn hàng
+
@ShopHelpers.FormatVND(_reportOrders.Any() ? _reportOrders.Average(o => o.TotalAmount) : 0) Giá trị TB / đơn
+
@_reportProducts.Count Sản phẩm
+
+@* Revenue Report *@
+
+
+
+ @if (_revenueReport.Any())
+ {
+
+
+ KỲ ĐƠN HÀNG DOANH THU
+
+
+ @foreach (var r in _revenueReport)
+ {
+
+ @r.Period.ToString("dd/MM/yyyy")
+ @r.OrderCount
+ @ShopHelpers.FormatVND(r.Revenue)
+
+ }
+
+
+ }
+ else
+ {
+
Nhấn Ngày / Tuần / Tháng để tải dữ liệu doanh thu.
+ }
+
+
+@* ─── Top products from real order_items data ─── *@
+@if (_topProducts.Any())
+{
+
+
+
+
+ #
+ Tên SP
+ Đã bán
+ Doanh thu
+
+ @{ var tpRank = 1; }
+ @foreach (var tp in _topProducts)
+ {
+
+ @(tpRank++)
+ @(tp.ProductName ?? "—")
+ @tp.TotalSold
+ @ShopHelpers.FormatVND(tp.TotalRevenue)
+
+ }
+
+
+
+}
+@if (_reportOrders.Any())
+{
+
+
+
+
+ Mã đơn
+ Giá trị
+ Trạng thái
+ Ngày tạo
+
+ @foreach (var o in _reportOrders.Take(20))
+ {
+
+ @o.Id.ToString()[..8]
+ @ShopHelpers.FormatVND(o.TotalAmount)
+ @(o.Status ?? "—")
+ @o.CreatedAt.ToString("dd/MM/yyyy HH:mm")
+
+ }
+
+
+
+}
+else
+{
+
+
+
+
+
Chưa có dữ liệu báo cáo
+
Dữ liệu sẽ hiển thị khi có đơn hàng và hoạt động kinh doanh
+
+}
+
+@code {
+ [Parameter] public Guid ShopId { get; set; }
+
+ private List _reportOrders = new();
+ private List _reportProducts = new();
+ private List _topProducts = new();
+ private List _revenueReport = new();
+ private string _reportPeriod = "daily";
+
+ protected override async Task OnInitializedAsync()
+ {
+ var shopGuid = ShopId != Guid.Empty ? ShopId : (Guid?)null;
+ _reportOrders = await DataService.GetOrdersAsync(shopGuid);
+ _reportProducts = await DataService.GetAllProductsAsync(shopGuid);
+ _topProducts = await DataService.GetTopProductsAsync(shopGuid);
+ }
+
+ private async Task LoadRevenueReport(string period)
+ {
+ _reportPeriod = period;
+ _revenueReport = await DataService.GetRevenueReportAsync(period, ShopId != Guid.Empty ? ShopId : null);
+ }
+}
diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopResources.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopResources.razor
new file mode 100644
index 00000000..9a9b0cec
--- /dev/null
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopResources.razor
@@ -0,0 +1,173 @@
+@using WebClientTpos.Client.Services
+@using WebClientTpos.Client.Pages.Admin.Shop
+@inject PosDataService DataService
+
+
+
+
+
@_resources.Count(r => r.IsActive) Hoạt động
+
+
{ _editingResourceId = null; _newResourceName = ""; _newResourceType = "Room"; _newResourceCapacity = 1; _resourceFormMessage = null; _showResourceForm = !_showResourceForm; }'>
+ Thêm tài nguyên
+
+
+@if (_showResourceForm)
+{
+
+
+
+
+
Tên *
+
Loại
+
+ Phòng
+ Giường
+ Ghế
+ Thiết bị
+
+
+
Sức chứa
+
+
+ @(_editingResourceId.HasValue ? "Cập nhật" : "Lưu")
+ _showResourceForm = false' style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-secondary);cursor:pointer;"> Hủy
+
+ @if (_resourceFormMessage != null) {
@_resourceFormMessage
}
+
+
+}
+@if (!_resources.Any())
+{
+ @RenderEmpty("door-open", "#EC4899", "Chưa có tài nguyên", "Thêm phòng, giường, thiết bị cho cửa hàng")
+}
+else
+{
+
+
+
+ Tên
+ Loại
+ Sức chứa
+ Trạng thái
+
+
+ @foreach (var r in _resources)
+ {
+
+ @r.Name
+ @(r.ResourceType ?? "—")
+ @r.Capacity
+ @(r.IsActive ? "Active" : "Inactive")
+
+
+ EditResource(r)' style="background:rgba(59,130,246,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;">
+ DeleteResourceItem(r.Id)' style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;">
+
+
+
+ }
+
+
+
+}
+
+@code {
+ [Parameter] public Guid ShopId { get; set; }
+
+ // Resources CRUD state
+ private List _resources = new();
+ private bool _showResourceForm;
+ private Guid? _editingResourceId;
+ private string _newResourceName = "";
+ private string _newResourceType = "Room";
+ private int _newResourceCapacity = 1;
+ private string? _resourceFormMessage;
+ private bool _resourceFormSuccess;
+ private string? _errorMessage;
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (ShopId != Guid.Empty)
+ _resources = await DataService.GetResourcesAsync(ShopId);
+ }
+
+ // ═══ 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) || ShopId == Guid.Empty)
+ {
+ _resourceFormMessage = "Vui lòng nhập tên tài nguyên."; _resourceFormSuccess = false; return;
+ }
+ try
+ {
+ await DataService.CreateResourceAsync(new PosDataService.CreateResourceRequest(ShopId, _newResourceName, _newResourceType, _newResourceCapacity));
+ _resourceFormMessage = $"Đã thêm '{_newResourceName}' thành công!"; _resourceFormSuccess = true;
+ _newResourceName = ""; _newResourceType = "Room"; _newResourceCapacity = 1;
+ _resources = await DataService.GetResourcesAsync(ShopId);
+ }
+ catch (Exception ex) { _resourceFormMessage = $"Lỗi: {ex.Message}"; _resourceFormSuccess = false; }
+ }
+
+ private async Task SaveResource()
+ {
+ _resourceFormMessage = null;
+ if (string.IsNullOrWhiteSpace(_newResourceName) || ShopId == Guid.Empty || !_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(ShopId, _newResourceName, _newResourceType, _newResourceCapacity));
+ _resourceFormMessage = $"Đã cập nhật '{_newResourceName}' thành công!"; _resourceFormSuccess = true;
+ _editingResourceId = null;
+ _resources = await DataService.GetResourcesAsync(ShopId);
+ }
+ catch (Exception ex) { _resourceFormMessage = $"Lỗi: {ex.Message}"; _resourceFormSuccess = false; }
+ }
+
+ private async Task DeleteResourceItem(Guid id)
+ {
+ try
+ {
+ await DataService.DeleteResourceAsync(id);
+ if (ShopId != Guid.Empty) _resources = await DataService.GetResourcesAsync(ShopId);
+ }
+ catch (Exception ex) { _errorMessage = $"Không thể xóa tài nguyên: {ex.Message}"; }
+ }
+
+ private RenderFragment RenderEmpty(string icon, string color, string title, string desc, string? ctaIcon = null, string? ctaLabel = null, string? ctaHref = null) => __builder =>
+ {
+
+
+
+
+
@title
+
@desc
+ @if (ctaIcon != null && ctaLabel != null)
+ {
+
+
+ @ctaLabel
+
+ }
+
+ };
+
+ private static string HexToRgb(string hex)
+ {
+ hex = hex.TrimStart('#');
+ if (hex.Length != 6) return "0,0,0";
+ return $"{Convert.ToInt32(hex[..2], 16)},{Convert.ToInt32(hex[2..4], 16)},{Convert.ToInt32(hex[4..], 16)}";
+ }
+}
diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopSchedule.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopSchedule.razor
new file mode 100644
index 00000000..b24ef791
--- /dev/null
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopSchedule.razor
@@ -0,0 +1,234 @@
+@using WebClientTpos.Client.Services
+@using WebClientTpos.Client.Pages.Admin.Shop
+@inject PosDataService DataService
+@inject NavigationManager Nav
+
+@if (SubSection == "schedule")
+{
+
+
+
@_staffSchedules.Select(s => s.StaffId).Distinct().Count() NV có lịch
+
@_staffSchedules.Count Ca làm việc
+
+
{ _showScheduleForm = !_showScheduleForm; _newSchedStaffId = Guid.Empty; _newSchedDay = 1; _newSchedStart = "08:00"; _newSchedEnd = "17:00"; _schedFormMessage = null; }'>
+ Thêm ca
+
+
+ @if (_showScheduleForm)
+ {
+
+
+
+
+
Nhân viên
+
+ -- Chọn NV --
+ @foreach (var st in _staff)
+ {
+ var stName = !string.IsNullOrWhiteSpace(st.LastName) || !string.IsNullOrWhiteSpace(st.FirstName) ? $"{st.LastName} {st.FirstName}".Trim() : (st.EmployeeCode ?? st.Id.ToString()[..8]);
+ @stName
+ }
+
+
+
Ngày
+
+ Thứ 2 Thứ 3 Thứ 4
+ Thứ 5 Thứ 6 Thứ 7 Chủ nhật
+
+
+
Bắt đầu
+
Kết thúc
+
+
+ Lưu
+ _showScheduleForm = false' style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-secondary);cursor:pointer;"> Hủy
+
+ @if (_schedFormMessage != null) {
@_schedFormMessage
}
+
+
+ }
+ @if (!_staffSchedules.Any())
+ {
+
+
+
+
+
Chưa có lịch làm việc
+
Thiết lập lịch ca cho nhân viên
+
+ }
+ else
+ {
+
+
+
+
+ Nhân viên
+ Vai trò
+ Thứ
+ Bắt đầu
+ Kết thúc
+
+
+ @foreach (var s in _staffSchedules.OrderBy(x => x.DayOfWeek).ThenBy(x => x.StartTime))
+ {
+ var schedStaff = _staff.FirstOrDefault(st => st.Id == s.StaffId);
+ var schedStaffName = schedStaff != null && (!string.IsNullOrWhiteSpace(schedStaff.LastName) || !string.IsNullOrWhiteSpace(schedStaff.FirstName)) ? $"{schedStaff.LastName} {schedStaff.FirstName}".Trim() : (s.EmployeeCode ?? s.StaffId.ToString()[..8]);
+
+ @schedStaffName
+ @(s.Role ?? "—")
+ @ShopHelpers.DayLabel(s.DayOfWeek)
+ @s.StartTime
+ @s.EndTime
+ DeleteScheduleItem(s.Id)' style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;">
+
+ }
+
+
+
+ }
+}
+else if (SubSection == "shifts")
+{
+
+
@_staffSchedules.Count Ca đã phân
+
+
@_staffSchedules.Select(s => s.DayOfWeek).Distinct().Count() Ngày có ca
+
+ @* ─── Add Schedule Form ─── *@
+
+
+ @if (_showScheduleForm)
+ {
+
+
+
Nhân viên
+
+ -- Chọn NV --
+ @foreach (var s in _staff) { @(s.EmployeeCode ?? s.Id.ToString()[..8]) }
+
+
+
Ngày
+
+ Thứ 2 Thứ 3 Thứ 4
+ Thứ 5 Thứ 6 Thứ 7 Chủ nhật
+
+
+
Giờ bắt đầu
+
Giờ kết thúc
+
+
+ Lưu
+ _showScheduleForm = false)">Hủy
+
+ @if (_schedFormMessage != null) {
@_schedFormMessage
}
+
+ }
+
+ @* ─── Weekly Grid ─── *@
+
+
+
+ @if (!_staff.Any())
+ {
+
Chưa có nhân viên. Thêm nhân viên trong mục
Nhân sự .
+ }
+ else
+ {
+
+ Nhân viên
+ @foreach (var d in new[] { "T2", "T3", "T4", "T5", "T6", "T7", "CN" })
+ {
+ @d
+ }
+
+ @foreach (var emp in _staff)
+ {
+
+ @(emp.EmployeeCode ?? emp.Id.ToString()[..8])
+ @foreach (var dow in new[] { 1, 2, 3, 4, 5, 6, 0 })
+ {
+ var sched = _staffSchedules.FirstOrDefault(s => s.StaffId == emp.Id && s.DayOfWeek == dow);
+ if (sched != null)
+ {
+ var isMorning = sched.StartTime?.CompareTo("12:00") < 0;
+ var bg = isMorning ? "rgba(59,130,246,0.12)" : "rgba(168,85,247,0.12)";
+ var fg = isMorning ? "#3B82F6" : "#A855F7";
+ var label = $"{sched.StartTime?[..5]}";
+
+ @label
+ DeleteScheduleItem(sched.Id))" style="background:none;border:none;cursor:pointer;margin-left:2px;" title="Xóa">
+
+ }
+ else
+ {
+
+ —
+
+ }
+ }
+
+ }
+
+ }
+
+
+
+
Sáng (<12:00)
+
Chiều (≥12:00)
+
— = Nghỉ
+
+}
+
+@code {
+ [Parameter] public Guid ShopId { get; set; }
+ [Parameter] public string SubSection { get; set; } = "schedule";
+
+ private List _staff = new();
+ private List _staffSchedules = new();
+ private bool _showScheduleForm;
+ private Guid _newSchedStaffId;
+ private string _newSchedStaffIdStr = "";
+ private int _newSchedDay = 1;
+ private string _newSchedStart = "08:00";
+ private string _newSchedEnd = "17:00";
+ private string? _schedFormMessage;
+ private bool _schedFormSuccess;
+
+ protected override async Task OnInitializedAsync()
+ {
+ _staffSchedules = await DataService.GetStaffSchedulesAsync(ShopId != Guid.Empty ? ShopId : null);
+ _staff = await DataService.GetStaffForShopAsync(ShopId);
+ }
+
+ private async Task AddSchedule()
+ {
+ _schedFormMessage = null;
+ if (!Guid.TryParse(_newSchedStaffIdStr, out var staffId) || ShopId == Guid.Empty)
+ {
+ _schedFormMessage = "Vui lòng nhập đúng Staff ID."; _schedFormSuccess = false; return;
+ }
+ try
+ {
+ _newSchedStaffId = staffId;
+ await DataService.CreateScheduleAsync(new PosDataService.CreateScheduleRequest(ShopId, _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(ShopId != Guid.Empty ? ShopId : null);
+ }
+ 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(ShopId != Guid.Empty ? ShopId : null);
+ }
+ catch (Exception ex) { Console.Error.WriteLine($"Không thể xóa lịch làm việc: {ex.Message}"); }
+ }
+}
diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopSettings.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopSettings.razor
new file mode 100644
index 00000000..9877b098
--- /dev/null
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopSettings.razor
@@ -0,0 +1,152 @@
+@using WebClientTpos.Client.Services
+@using WebClientTpos.Client.Pages.Admin.Shop
+@inject PosDataService DataService
+
+@* ─── Shop info (read-only) ─── *@
+
+
+
+
+
Tên cửa hàng @(_shopName ?? "—")
+
Ngành hàng @_verticalLabel
+
+
+
+@* ─── Opening hours + business days ─── *@
+
+
+
+
+
+
Ngày kinh doanh
+
+ @foreach (var (day, code) in new[] { ("T2","Monday"),("T3","Tuesday"),("T4","Wednesday"),("T5","Thursday"),("T6","Friday"),("T7","Saturday"),("CN","Sunday") })
+ {
+ var isOn = _settingsOpenDays.Contains(code);
+ ToggleDay(code))"
+ style="width:40px;height:40px;border-radius:10px;border:1px solid @(isOn ? "var(--admin-orange-primary)" : "var(--admin-border-subtle)");background:@(isOn ? "rgba(255,92,0,0.15)" : "var(--admin-bg-elevated)");color:@(isOn ? "var(--admin-orange-primary)" : "var(--admin-text-tertiary)");font-size:12px;font-weight:@(isOn ? "700" : "500");cursor:pointer;">
+ @day
+
+ }
+
+
+
+
+@* ─── Features config toggles ─── *@
+
+
+
+
+ @{ void RenderToggle(string label, bool isOn, Action
setter) {
+ { setter(!isOn); StateHasChanged(); })">
+
+
@label
+
;
+ } }
+ @{ RenderToggle("Quản lý tồn kho", _featHasInventory, v => _featHasInventory = v); }
+ @{ RenderToggle("Đặt lịch hẹn", _featHasBooking, v => _featHasBooking = v); }
+ @{ RenderToggle("Quản lý bàn", _featHasTables, v => _featHasTables = v); }
+ @{ RenderToggle("Hiển thị bếp", _featHasKitchen, v => _featHasKitchen = v); }
+ @{ RenderToggle("Vận chuyển", _featHasShipping, v => _featHasShipping = v); }
+ @{ RenderToggle("Giao hàng", _featHasDelivery, v => _featHasDelivery = v); }
+
+
+
+@* ─── Save button ─── *@
+
+
+ Lưu thiết lập
+
+ @if (_settingsMessage != null)
+ {
+ @_settingsMessage
+ }
+
+
+@code {
+ [Parameter] public Guid ShopId { get; set; }
+ [Parameter] public string? ShopName { get; set; }
+ [Parameter] public string? VerticalLabel { get; set; }
+
+ // Settings state
+ private PosDataService.ShopSettingsInfo? _shopSettings;
+ private string _settingsOpenTime = "";
+ private string _settingsCloseTime = "";
+ private List _settingsOpenDays = new();
+ private bool _featHasInventory, _featHasBooking, _featHasTables;
+ private bool _featHasKitchen, _featHasShipping, _featHasDelivery;
+ private string? _settingsMessage;
+ private bool _settingsSuccess;
+
+ private string? _shopName => ShopName;
+ private string _verticalLabel => VerticalLabel ?? "";
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (ShopId != Guid.Empty)
+ await LoadShopSettings();
+ }
+
+ private async Task LoadShopSettings()
+ {
+ if (ShopId == Guid.Empty) return;
+ try
+ {
+ _shopSettings = await DataService.GetShopSettingsAsync(ShopId);
+ if (_shopSettings != null)
+ {
+ _settingsOpenTime = _shopSettings.OpenTime ?? "";
+ _settingsCloseTime = _shopSettings.CloseTime ?? "";
+ _settingsOpenDays = _shopSettings.OpenDays ?? new();
+ if (_shopSettings.Features != null)
+ {
+ _featHasInventory = _shopSettings.Features.HasInventory;
+ _featHasBooking = _shopSettings.Features.HasBooking;
+ _featHasTables = _shopSettings.Features.HasTables;
+ _featHasKitchen = _shopSettings.Features.HasKitchen;
+ _featHasShipping = _shopSettings.Features.HasShipping;
+ _featHasDelivery = _shopSettings.Features.HasDelivery;
+ }
+ }
+ }
+ catch { /* non-fatal */ }
+ }
+
+ private async Task SaveShopSettings()
+ {
+ if (ShopId == Guid.Empty) return;
+ _settingsMessage = null;
+ var features = new PosDataService.ShopFeaturesInfo
+ {
+ HasInventory = _featHasInventory, HasBooking = _featHasBooking, HasTables = _featHasTables,
+ HasKitchen = _featHasKitchen, HasShipping = _featHasShipping, HasDelivery = _featHasDelivery
+ };
+ var req = new PosDataService.UpdateShopSettingsRequest(
+ Features: features,
+ OpenTime: string.IsNullOrWhiteSpace(_settingsOpenTime) ? null : _settingsOpenTime,
+ CloseTime: string.IsNullOrWhiteSpace(_settingsCloseTime) ? null : _settingsCloseTime,
+ OpenDays: _settingsOpenDays.Any() ? _settingsOpenDays : null);
+ var ok = await DataService.UpdateShopSettingsAsync(ShopId, req);
+ _settingsSuccess = ok;
+ _settingsMessage = ok ? "Đã lưu thiết lập thành công!" : "Lỗi khi lưu thiết lập.";
+ StateHasChanged();
+ }
+
+ private void ToggleDay(string code)
+ {
+ if (_settingsOpenDays.Contains(code)) _settingsOpenDays.Remove(code);
+ else _settingsOpenDays.Add(code);
+ StateHasChanged();
+ }
+}
diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopStaff.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopStaff.razor
new file mode 100644
index 00000000..1b0ff226
--- /dev/null
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopStaff.razor
@@ -0,0 +1,288 @@
+@using WebClientTpos.Client.Services
+@using WebClientTpos.Client.Pages.Admin.Shop
+@using Microsoft.AspNetCore.Components.Forms
+@inject PosDataService DataService
+
+
+
@(_staff.Count) nhân viên
+ { _editingStaffId = null; _newStaffCode = ""; _newStaffPhone = ""; _newStaffEmail = ""; _newStaffRole = "Cashier"; _staffFormMessage = null; _createStaffAccount = false; _newStaffFirstName = ""; _newStaffLastName = ""; _newStaffPassword = ""; _newStaffAddress = ""; _staffDocFrontFile = null; _staffDocBackFile = null; _staffDocFrontPreview = null; _staffDocBackPreview = null; _showStaffForm = !_showStaffForm; })">
+
+ Thêm nhân viên
+
+
+@if (_showStaffForm)
+{
+
+
+
+
+
+
CCCD / Giấy tờ tùy thân
+
+
+
Mặt trước
+ @if (_staffDocFrontPreview != null)
+ {
+
+ }
+
+
+
+
Mặt sau
+ @if (_staffDocBackPreview != null)
+ {
+
+ }
+
+
+
+
+ @if (!_editingStaffId.HasValue)
+ {
+
+
+ Tạo tài khoản đăng nhập (IAM)
+
+ @if (_createStaffAccount)
+ {
+
+ }
+
+ }
+
+ @(_editingStaffId.HasValue ? "Cập nhật" : "Lưu")
+ _showStaffForm = false)" style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-secondary);cursor:pointer;"> Hủy
+
+ @if (!string.IsNullOrEmpty(_staffFormMessage))
+ {
+
@_staffFormMessage
+ }
+
+
+}
+@if (!_staff.Any() && !_showStaffForm)
+{
+
+
+
+
+
Chưa có nhân viên
+
Thêm nhân viên để quản lý cửa hàng
+
{ _editingStaffId = null; _newStaffCode = ""; _newStaffPhone = ""; _newStaffEmail = ""; _newStaffRole = "Cashier"; _staffFormMessage = null; _createStaffAccount = false; _showStaffForm = true; })">
+ Thêm nhân viên
+
+
+}
+else if (_staff.Any())
+{
+
+
@_staff.Count(s => s.Status == "Active") Đang hoạt động
+
@_staff.Count Tổng nhân viên
+
+
+
+
+ Nhân viên
+ Mã NV
+ Vai trò
+ Trạng thái
+ SĐT
+ Hành động
+
+ @foreach (var s in _staff)
+ {
+ var staffDisplayName = !string.IsNullOrWhiteSpace(s.LastName) || !string.IsNullOrWhiteSpace(s.FirstName) ? $"{s.LastName} {s.FirstName}".Trim() : null;
+
+ @(staffDisplayName ?? s.EmployeeCode ?? s.Id.ToString()[..6])
+ @(s.EmployeeCode ?? "—")
+ @(s.Role ?? "—")
+ @(s.Status ?? "—")
+ @(s.Phone ?? s.Email ?? "—")
+
+
+ EditStaff(s))" style="background:rgba(59,130,246,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Sửa">
+ DeleteStaffMember(s.Id))" style="background:rgba(239,68,68,0.1);border:none;border-radius:6px;width:28px;height:28px;display:flex;align-items:center;justify-content:center;cursor:pointer;" title="Xóa">
+
+
+
+ }
+
+
+
+}
+
+@code {
+ [Parameter] public Guid ShopId { get; set; }
+
+ // Staff data
+ private List _staff = new();
+ // Staff form state
+ private bool _showStaffForm;
+ private Guid? _editingStaffId;
+ private string _newStaffCode = "";
+ private string _newStaffRole = "Cashier";
+ private string _newStaffPhone = "";
+ private string _newStaffEmail = "";
+ private string? _staffFormMessage;
+ private bool _staffFormSuccess;
+ private bool _createStaffAccount;
+ private string _newStaffFirstName = "";
+ private string _newStaffLastName = "";
+ private string _newStaffPassword = "";
+ // Staff extended fields state
+ private string _newStaffAddress = "";
+ // Image upload state for staff docs
+ private IBrowserFile? _staffDocFrontFile;
+ private string? _staffDocFrontPreview;
+ private IBrowserFile? _staffDocBackFile;
+ private string? _staffDocBackPreview;
+ // Merchant ID loaded from shop data
+ private Guid? _merchantId;
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (ShopId != Guid.Empty)
+ {
+ var shop = await DataService.GetShopByIdAsync(ShopId);
+ if (shop != null)
+ _merchantId = shop.MerchantId;
+ _staff = await DataService.GetStaffForShopAsync(ShopId);
+ }
+ }
+
+ private async Task AddStaff()
+ {
+ _staffFormMessage = null;
+ if (!_merchantId.HasValue)
+ {
+ _staffFormMessage = "Không tìm thấy thông tin merchant. Vui lòng tải lại trang."; _staffFormSuccess = false; return;
+ }
+ if (string.IsNullOrWhiteSpace(_newStaffCode))
+ {
+ _staffFormMessage = "Vui lòng nhập mã nhân viên."; _staffFormSuccess = false; return;
+ }
+ try
+ {
+ var docFrontUrl = await UploadFileIfNeeded(_staffDocFrontFile);
+ var docBackUrl = await UploadFileIfNeeded(_staffDocBackFile);
+ if (_createStaffAccount)
+ {
+ if (string.IsNullOrWhiteSpace(_newStaffEmail) || string.IsNullOrWhiteSpace(_newStaffPassword))
+ {
+ _staffFormMessage = "Vui lòng nhập đầy đủ email và mật khẩu."; _staffFormSuccess = false; return;
+ }
+ var (ok, err) = await DataService.InviteStaffWithAccountAsync(new PosDataService.InviteStaffWithAccountRequest(
+ _newStaffEmail, _newStaffPassword, _newStaffFirstName, _newStaffLastName, _newStaffRole, ShopId));
+ if (!ok) { _staffFormMessage = err ?? "Lỗi tạo tài khoản IAM. Kiểm tra email/mật khẩu."; _staffFormSuccess = false; return; }
+ _staffFormMessage = $"Đã tạo tài khoản + mời NV '{_newStaffEmail}' thành công!"; _staffFormSuccess = true;
+ }
+ else
+ {
+ await DataService.CreateStaffAsync(new PosDataService.CreateStaffRequest(
+ _merchantId.Value, _newStaffCode, _newStaffPhone, _newStaffEmail, _newStaffRole,
+ _newStaffFirstName, _newStaffLastName, _newStaffAddress, null, docFrontUrl, docBackUrl));
+ _staffFormMessage = $"Đã thêm NV '{_newStaffCode}' thành công!"; _staffFormSuccess = true;
+ }
+ _newStaffCode = ""; _newStaffPhone = ""; _newStaffEmail = ""; _newStaffFirstName = ""; _newStaffLastName = ""; _newStaffPassword = ""; _newStaffAddress = ""; _createStaffAccount = false;
+ _staffDocFrontFile = null; _staffDocBackFile = null; _staffDocFrontPreview = null; _staffDocBackPreview = null;
+ _staff = await DataService.GetStaffForShopAsync(ShopId);
+ }
+ catch (Exception ex) { _staffFormMessage = $"Lỗi: {ex.Message}"; _staffFormSuccess = false; }
+ }
+
+ private void EditStaff(PosDataService.StaffInfo s)
+ {
+ _editingStaffId = s.Id;
+ _newStaffCode = s.EmployeeCode ?? "";
+ _newStaffRole = s.Role ?? "Cashier";
+ _newStaffPhone = s.Phone ?? "";
+ _newStaffEmail = s.Email ?? "";
+ _newStaffFirstName = s.FirstName ?? "";
+ _newStaffLastName = s.LastName ?? "";
+ _newStaffAddress = s.Address ?? "";
+ _staffDocFrontFile = null; _staffDocBackFile = null;
+ _staffDocFrontPreview = s.DocumentFrontUrl;
+ _staffDocBackPreview = s.DocumentBackUrl;
+ _staffFormMessage = null;
+ _showStaffForm = true;
+ }
+
+ private async Task SaveStaffEdit()
+ {
+ _staffFormMessage = null;
+ if (string.IsNullOrWhiteSpace(_newStaffCode) || !_merchantId.HasValue || !_editingStaffId.HasValue)
+ {
+ _staffFormMessage = "Vui lòng nhập Mã NV."; _staffFormSuccess = false; return;
+ }
+ try
+ {
+ var docFrontUrl = await UploadFileIfNeeded(_staffDocFrontFile) ?? (_staffDocFrontPreview?.StartsWith("data:") == true ? null : _staffDocFrontPreview);
+ var docBackUrl = await UploadFileIfNeeded(_staffDocBackFile) ?? (_staffDocBackPreview?.StartsWith("data:") == true ? null : _staffDocBackPreview);
+ await DataService.UpdateStaffAsync(_editingStaffId.Value, new PosDataService.CreateStaffRequest(
+ _merchantId.Value, _newStaffCode, _newStaffPhone, _newStaffEmail, _newStaffRole,
+ _newStaffFirstName, _newStaffLastName, _newStaffAddress, null, docFrontUrl, docBackUrl));
+ _staffFormMessage = $"Đã cập nhật NV '{_newStaffCode}' thành công!"; _staffFormSuccess = true;
+ _editingStaffId = null;
+ _staffDocFrontFile = null; _staffDocBackFile = null; _staffDocFrontPreview = null; _staffDocBackPreview = null;
+ _staff = await DataService.GetStaffForShopAsync(ShopId);
+ }
+ catch (Exception ex) { _staffFormMessage = $"Lỗi: {ex.Message}"; _staffFormSuccess = false; }
+ }
+
+ private async Task DeleteStaffMember(Guid staffId)
+ {
+ try
+ {
+ await DataService.DeleteStaffAsync(staffId);
+ _staff = await DataService.GetStaffForShopAsync(ShopId);
+ }
+ catch (Exception ex) { Console.Error.WriteLine($"Không thể xóa nhân viên: {ex.Message}"); }
+ }
+
+ private async Task OnStaffDocFrontSelected(InputFileChangeEventArgs e)
+ {
+ _staffDocFrontFile = e.File;
+ var format = "image/png";
+ var resized = await e.File.RequestImageFileAsync(format, 300, 200);
+ var buffer = new byte[resized.Size];
+ await resized.OpenReadStream().ReadAsync(buffer);
+ _staffDocFrontPreview = $"data:{format};base64,{Convert.ToBase64String(buffer)}";
+ StateHasChanged();
+ }
+
+ private async Task OnStaffDocBackSelected(InputFileChangeEventArgs e)
+ {
+ _staffDocBackFile = e.File;
+ var format = "image/png";
+ var resized = await e.File.RequestImageFileAsync(format, 300, 200);
+ var buffer = new byte[resized.Size];
+ await resized.OpenReadStream().ReadAsync(buffer);
+ _staffDocBackPreview = $"data:{format};base64,{Convert.ToBase64String(buffer)}";
+ StateHasChanged();
+ }
+
+ private async Task UploadFileIfNeeded(IBrowserFile? file)
+ {
+ if (file == null) return null;
+ using var stream = file.OpenReadStream(maxAllowedSize: 10_485_760);
+ return await DataService.UploadImageAsync(stream, file.Name, file.ContentType);
+ }
+}
diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopTables.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopTables.razor
new file mode 100644
index 00000000..31ece1df
--- /dev/null
+++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopTables.razor
@@ -0,0 +1,323 @@
+@using WebClientTpos.Client.Services
+@using WebClientTpos.Client.Pages.Admin.Shop
+@inject PosDataService DataService
+
+@if (SubSection == "tables")
+{
+
+
+ Trống: @_tables.Count(t => t.Status == "available")
+ Đang dùng: @_tables.Count(t => t.Status == "occupied")
+ Đã đặt: @_tables.Count(t => t.Status == "reserved")
+
+
{ _editingTableId = null; _newTableNumber = ""; _newTableCapacity = 4; _newTableZone = ""; _tableFormMessage = null; _showTableForm = !_showTableForm; }'>
+ Thêm bàn
+
+
+ @if (_showTableForm)
+ {
+
+
+
+
+
Số bàn *
+
Sức chứa
+
Khu vực — Chưa chọn — @foreach (var z in AllZoneNames) { @z }
+
+
+ @(_editingTableId.HasValue ? "Cập nhật" : "Lưu")
+ _showTableForm = false' style="display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-secondary);cursor:pointer;"> Hủy
+
+ @if (_tableFormMessage != null) {
@_tableFormMessage
}
+
+
+ }
+ @if (!_tables.Any())
+ {
+ @RenderEmpty("grid-3x3", "#F59E0B", "Chưa có bàn nào", "Thêm bàn để quản lý sơ đồ phục vụ")
+ }
+ else
+ {
+
+ @foreach (var table in _tables)
+ {
+ var bgColor = table.Status switch { "available" => "rgba(34,197,94,0.08)", "occupied" => "rgba(239,68,68,0.08)", "reserved" => "rgba(245,158,11,0.08)", _ => "rgba(107,107,111,0.08)" };
+ var borderColor = table.Status switch { "available" => "rgba(34,197,94,0.3)", "occupied" => "rgba(239,68,68,0.3)", "reserved" => "rgba(245,158,11,0.3)", _ => "rgba(107,107,111,0.3)" };
+ var statusColor = table.Status switch { "available" => "#22C55E", "occupied" => "#EF4444", "reserved" => "#F59E0B", _ => "#6B6B6F" };
+ var statusText = table.Status switch { "available" => "Trống", "occupied" => "Đang dùng", "reserved" => "Đã đặt", "cleaning" => "Dọn dẹp", _ => table.Status };
+
+
+ EditTable(table)' style="background:rgba(59,130,246,0.1);border:none;border-radius:6px;width:26px;height:26px;display:flex;align-items:center;justify-content:center;cursor:pointer;">
+ DeleteTableItem(table.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;">
+
+
@table.TableNumber
+
@(table.Zone ?? "Chung") • @table.Capacity chỗ
+
+
+ @statusText
+
+ @if (table.SessionId.HasValue)
+ {
+
+ @table.GuestCount khách • @(table.StartedAt?.ToString("HH:mm") ?? "—")
+
+ }
+
+ }
+
+ }
+}
+else if (SubSection == "rooms")
+{
+ @if (!_tables.Any())
+ {
+ @RenderEmpty("door-open", "#8B5CF6", "Chưa có phòng nào", "Thêm phòng để quản lý Karaoke", "plus-circle", "Thêm phòng")
+ }
+ else
+ {
+
+
+ Trống: @_tables.Count(t => t.Status == "available")
+ Đang hát: @_tables.Count(t => t.Status == "occupied")
+ Đã đặt: @_tables.Count(t => t.Status == "reserved")
+
+
@_tables.Count phòng
+
+
+ @foreach (var room in _tables)
+ {
+ var bgColor = room.Status switch { "available" => "rgba(139,92,246,0.08)", "occupied" => "rgba(239,68,68,0.08)", "reserved" => "rgba(245,158,11,0.08)", _ => "rgba(107,107,111,0.08)" };
+ var borderColor = room.Status switch { "available" => "rgba(139,92,246,0.3)", "occupied" => "rgba(239,68,68,0.3)", "reserved" => "rgba(245,158,11,0.3)", _ => "rgba(107,107,111,0.3)" };
+ var statusColor = room.Status switch { "available" => "#8B5CF6", "occupied" => "#EF4444", "reserved" => "#F59E0B", _ => "#6B6B6F" };
+ var statusText = room.Status switch { "available" => "Trống", "occupied" => "Đang hát", "reserved" => "Đã đặt", "cleaning" => "Dọn dẹp", _ => room.Status };
+ var roomType = (room.Zone ?? "").ToLower() switch { var z when z.Contains("vip") => ("VIP", "#F59E0B", 200000m), var z when z.Contains("party") => ("Party", "#EC4899", 350000m), _ => ("Standard", "#8B5CF6", 120000m) };
+
+
+
+
+ Phòng @room.TableNumber
+
+
@roomType.Item1
+
+
@(room.Zone ?? "Chung") • @room.Capacity chỗ
+
@ShopHelpers.FormatVND(roomType.Item3)/giờ
+
+
+ @statusText
+
+ @if (room.SessionId.HasValue)
+ {
+ var elapsed = DateTime.UtcNow - (room.StartedAt ?? DateTime.UtcNow);
+ var hours = Math.Max(1, (int)Math.Ceiling(elapsed.TotalHours));
+ var bill = hours * roomType.Item3;
+
+
@room.GuestCount khách • Bắt đầu @(room.StartedAt?.ToString("HH:mm") ?? "—")
+
+ @hours giờ
+ @ShopHelpers.FormatVND(bill)
+
+
+ }
+
+ }
+
+ }
+}
+else if (SubSection == "zones")
+{
+ var tableZones = _tables.GroupBy(t => t.Zone ?? "Chung").Select(g => new { Name = g.Key, Count = g.Count() }).ToList();
+ var allZones = tableZones.Select(z => z.Name).Union(_customZones).Distinct().OrderBy(z => z).ToList();
+ var zoneGroups = allZones.Select((z, i) => new { Name = z, Count = tableZones.FirstOrDefault(tz => tz.Name == z)?.Count ?? 0, Color = _zoneColors[i % _zoneColors.Length], Icon = _zoneIcons[i % _zoneIcons.Length] }).ToList();
+
+
+
+ @if (_showZoneForm)
+ {
+
+
@(_editingZoneOriginalName != null ? "Đổi tên khu vực" : "Thêm khu vực mới")
+
+
+ Lưu
+ _showZoneForm = false' style="padding:8px 12px;border-radius:8px;border:1px solid var(--admin-border-subtle);background:transparent;color:var(--admin-text-secondary);cursor:pointer;font-size:12px;">
+
+ @if (_zoneFormMessage != null) {
@_zoneFormMessage
}
+
+ }
+ @if (!zoneGroups.Any())
+ {
+ @RenderEmpty("map-pin", "#F59E0B", "Chưa có khu vực nào", "Nhấn 'Thêm khu vực' ở trên để tạo khu vực đầu tiên", "", "", "")
+ }
+ else
+ {
+
+ @foreach (var zone in zoneGroups)
+ {
+ var rgbVal = zone.Color switch { "#3B82F6" => "59,130,246", "#A855F7" => "168,85,247", "#22C55E" => "34,197,94", "#F59E0B" => "245,158,11", "#EC4899" => "236,72,153", _ => "99,102,241" };
+
+
+
+
+
+
+
@zone.Name
+
@zone.Count bàn
+
+
+
+ Đang hoạt động
+
+
+ { _editingZoneOriginalName = zone.Name; _newZoneName = zone.Name; _showZoneForm = true; _zoneFormMessage = null; }' style="font-size:12px;color:var(--admin-orange-primary);background:none;border:none;cursor:pointer;display:flex;align-items:center;gap:4px;"> Đổi tên
+
+
+ }
+
+ }
+
+
+}
+
+@code {
+ [Parameter] public Guid ShopId { get; set; }
+ [Parameter] public string SubSection { get; set; } = "tables";
+
+ // Tables state
+ private List _tables = new();
+ // Table form state
+ private bool _showTableForm;
+ private Guid? _editingTableId;
+ private string _newTableNumber = "";
+ private int _newTableCapacity = 4;
+ private string _newTableZone = "";
+ private string? _tableFormMessage;
+ private bool _tableFormSuccess;
+ // Zone form state
+ private bool _showZoneForm;
+ private string _newZoneName = "";
+ private string? _editingZoneOriginalName;
+ private string? _zoneFormMessage;
+ private bool _zoneFormSuccess;
+ private readonly List _customZones = new();
+ private static readonly string[] _zoneColors = { "#3B82F6", "#A855F7", "#22C55E", "#F59E0B", "#EC4899", "#6366F1" };
+ private static readonly string[] _zoneIcons = { "building", "crown", "trees", "wine", "coffee", "map-pin" };
+ private List AllZoneNames => _tables.Select(t => t.Zone ?? "Chung").Distinct()
+ .Union(_customZones).Distinct().OrderBy(z => z).ToList();
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (ShopId != Guid.Empty)
+ _tables = await DataService.GetTablesAsync(ShopId);
+ }
+
+ // ═══ 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) || ShopId == Guid.Empty)
+ {
+ _tableFormMessage = "Vui lòng nhập số bàn."; _tableFormSuccess = false; return;
+ }
+ try
+ {
+ await DataService.CreateTableAsync(new PosDataService.CreateTableRequest(ShopId, _newTableNumber, _newTableCapacity, _newTableZone));
+ _tableFormMessage = $"Đã thêm bàn '{_newTableNumber}' thành công!"; _tableFormSuccess = true;
+ _newTableNumber = ""; _newTableCapacity = 4; _newTableZone = "";
+ if (ShopId != Guid.Empty) _tables = await DataService.GetTablesAsync(ShopId);
+ }
+ catch (Exception ex) { _tableFormMessage = $"Lỗi: {ex.Message}"; _tableFormSuccess = false; }
+ }
+
+ private async Task SaveTable()
+ {
+ _tableFormMessage = null;
+ if (string.IsNullOrWhiteSpace(_newTableNumber) || ShopId == Guid.Empty || !_editingTableId.HasValue)
+ {
+ _tableFormMessage = "Vui lòng nhập số bàn."; _tableFormSuccess = false; return;
+ }
+ try
+ {
+ await DataService.UpdateTableAsync(_editingTableId.Value, new PosDataService.CreateTableRequest(ShopId, _newTableNumber, _newTableCapacity, _newTableZone));
+ _tableFormMessage = $"Đã cập nhật bàn '{_newTableNumber}' thành công!"; _tableFormSuccess = true;
+ _editingTableId = null;
+ if (ShopId != Guid.Empty) _tables = await DataService.GetTablesAsync(ShopId);
+ }
+ catch (Exception ex) { _tableFormMessage = $"Lỗi: {ex.Message}"; _tableFormSuccess = false; }
+ }
+
+ private async Task DeleteTableItem(Guid id)
+ {
+ try
+ {
+ await DataService.DeleteTableAsync(id);
+ if (ShopId != Guid.Empty) _tables = await DataService.GetTablesAsync(ShopId);
+ }
+ catch (Exception ex) { Console.Error.WriteLine($"Không thể xóa bàn: {ex.Message}"); }
+ }
+
+ // ═══ ZONE MANAGEMENT ═══
+ private async Task SaveZone()
+ {
+ _zoneFormMessage = null;
+ if (string.IsNullOrWhiteSpace(_newZoneName) || ShopId == Guid.Empty)
+ {
+ _zoneFormMessage = "Vui lòng nhập tên khu vực."; _zoneFormSuccess = false; return;
+ }
+ if (_editingZoneOriginalName != null)
+ {
+ // Rename: update all tables in old zone to new zone name
+ var tablesInZone = _tables.Where(t => (t.Zone ?? "Chung") == _editingZoneOriginalName).ToList();
+ try
+ {
+ foreach (var t in tablesInZone)
+ await DataService.UpdateTableAsync(t.Id, new PosDataService.CreateTableRequest(ShopId, t.TableNumber, t.Capacity, _newZoneName.Trim()));
+ _tables = await DataService.GetTablesAsync(ShopId);
+ _zoneFormMessage = $"Đã đổi tên '{_editingZoneOriginalName}' → '{_newZoneName.Trim()}'"; _zoneFormSuccess = true;
+ _editingZoneOriginalName = null; _newZoneName = ""; _showZoneForm = false;
+ }
+ catch (Exception ex) { _zoneFormMessage = $"Lỗi: {ex.Message}"; _zoneFormSuccess = false; }
+ }
+ else
+ {
+ var zoneName = _newZoneName.Trim();
+ if (!_customZones.Contains(zoneName) && !_tables.Any(t => (t.Zone ?? "Chung") == zoneName))
+ _customZones.Add(zoneName);
+ _zoneFormMessage = $"Đã thêm khu vực '{zoneName}'."; _zoneFormSuccess = true;
+ _newTableZone = zoneName;
+ _newZoneName = "";
+ _showZoneForm = false;
+ }
+ }
+
+ private RenderFragment RenderEmpty(string icon, string color, string title, string desc, string? ctaIcon = null, string? ctaLabel = null, string? ctaHref = null) => __builder =>
+ {
+
+
+
+
+
@title
+
@desc
+ @if (ctaIcon != null && ctaLabel != null)
+ {
+
+
+ @ctaLabel
+
+ }
+
+ };
+}