chore(web-client): delete 9 orphaned pages replaced by ShopPage sections

This commit is contained in:
Ho Ngoc Hai
2026-02-28 07:04:32 +07:00
parent abd709d31c
commit ceed3b7def
9 changed files with 0 additions and 1756 deletions

View File

@@ -1,137 +0,0 @@
@page "/admin/customers"
@layout AdminLayout
@inherits AdminBase
@inject PosDataService DataService
@using WebClientTpos.Client.Services
@*
EN: Customer database — real data from membership_service via BFF.
VI: Cơ sở dữ liệu khách hàng — dữ liệu thực từ membership_service qua BFF.
*@
<PageTitle>Khách hàng — GoodGo Admin</PageTitle>
<div class="admin-topbar">
<div class="admin-topbar__left">
<h1 class="admin-topbar__title">Khách hàng</h1>
<p class="admin-topbar__subtitle">@_members.Count thành viên</p>
</div>
<div class="admin-topbar__right">
<div class="admin-search" style="width:220px;">
<i data-lucide="search"></i>
<input type="text" placeholder="Tìm khách hàng..." @bind="_searchQuery" @bind:event="oninput" />
</div>
</div>
</div>
@* ═══ SUMMARY ═══ *@
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;">
<div class="admin-stat-card">
<div class="admin-stat-card__icon" style="background:rgba(139,92,246,0.1);">
<i data-lucide="users" style="color:#8B5CF6;"></i>
</div>
<div class="admin-stat-card__content">
<span class="admin-stat-card__value">@_members.Count</span>
<span class="admin-stat-card__label">Tổng thành viên</span>
</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-card__icon" style="background:rgba(255,92,0,0.1);">
<i data-lucide="crown" style="color:#FF5C00;"></i>
</div>
<div class="admin-stat-card__content">
<span class="admin-stat-card__value">@_members.Count(m => m.CurrentLevel >= 3)</span>
<span class="admin-stat-card__label">VIP</span>
</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-card__icon" style="background:rgba(34,197,94,0.1);">
<i data-lucide="trending-up" style="color:#22C55E;"></i>
</div>
<div class="admin-stat-card__content">
<span class="admin-stat-card__value">@_members.Where(m => m.CreatedAt >= DateTime.UtcNow.AddDays(-30)).Count()</span>
<span class="admin-stat-card__label">Mới (30 ngày)</span>
</div>
</div>
</div>
@* ═══ MEMBERS TABLE ═══ *@
@if (IsLoading)
{
<div style="text-align:center;padding:48px;">
<div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div>
<p style="color:var(--admin-text-tertiary);font-size:14px;">Đang tải dữ liệu...</p>
</div>
}
else if (!_members.Any())
{
<div style="text-align:center;padding:60px 20px;">
<div style="width:80px;height:80px;border-radius:24px;background:rgba(139,92,246,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">
<i data-lucide="users" style="width:36px;height:36px;color:#8B5CF6;"></i>
</div>
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFFFFF);">Chưa có thành viên</h2>
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Khách hàng sẽ tự động trở thành thành viên khi mua hàng</p>
</div>
}
else
{
<div class="admin-panel">
<div class="admin-panel__body" style="padding:0;">
<table class="admin-table" style="width:100%;">
<thead>
<tr>
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">ID</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Cấp bậc</th>
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">EXP</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Giới tính</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Quốc gia</th>
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngày tham gia</th>
</tr>
</thead>
<tbody>
@foreach (var m in _members)
{
<tr style="border-top:1px solid var(--admin-border-subtle);">
<td style="padding:12px 16px;font-size:12px;font-family:monospace;color:var(--admin-text-tertiary);">@m.Id.ToString()[..8]...</td>
<td style="padding:12px 16px;">
<span style="display:inline-flex;align-items:center;gap:6px;">
<span style="width:8px;height:8px;border-radius:50%;background:@GetLevelColor(m.CurrentLevel);"></span>
<span style="font-weight:600;font-size:14px;">@(m.LevelName ?? $"Level {m.CurrentLevel}")</span>
</span>
</td>
<td style="padding:12px 16px;text-align:right;font-size:14px;font-weight:600;color:var(--admin-orange-primary);">@m.TotalExpEarned.ToString("N0")</td>
<td style="padding:12px 16px;font-size:14px;">@(m.Gender ?? "—")</td>
<td style="padding:12px 16px;font-size:14px;">@(m.CountryCode ?? "—")</td>
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@m.CreatedAt.ToString("dd/MM/yyyy")</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
</div>
@code {
private string _searchQuery = "";
private List<PosDataService.MemberInfo> _members = new();
protected override async Task OnInitializedAsync()
{
IsLoading = true;
try { _members = await DataService.GetMembersAsync(); }
catch { }
finally { IsLoading = false; }
}
private static string GetLevelColor(int level) => level switch
{
1 => "#94A3B8",
2 => "#22C55E",
3 => "#3B82F6",
4 => "#F59E0B",
5 => "#FF5C00",
_ => "#8B5CF6"
};
}

View File

@@ -1,171 +0,0 @@
@page "/admin/inventory"
@layout AdminLayout
@inherits AdminBase
@inject PosDataService DataService
@using WebClientTpos.Client.Services
@*
EN: Inventory dashboard — real data from inventory_service via BFF.
VI: Bảng điều khiển tồn kho — dữ liệu thực từ inventory_service qua BFF.
*@
<PageTitle>Kho hàng — GoodGo Admin</PageTitle>
<div class="admin-topbar">
<div class="admin-topbar__left">
<h1 class="admin-topbar__title">Kho hàng</h1>
<p class="admin-topbar__subtitle">@_items.Count mặt hàng @(_selectedShopId.HasValue ? $"• {_shopName}" : "• Tất cả cửa hàng")</p>
</div>
<div class="admin-topbar__right">
<div class="admin-search" style="width:220px;">
<i data-lucide="search"></i>
<input type="text" placeholder="Tìm sản phẩm..." @bind="_searchQuery" @bind:event="oninput" />
</div>
<select class="admin-select" style="min-width:160px;" @onchange="OnShopFilterChanged">
<option value="">Tất cả cửa hàng</option>
@foreach (var shop in _shops)
{
<option value="@shop.Id" selected="@(_selectedShopId == shop.Id)">@shop.Name</option>
}
</select>
</div>
</div>
@* ═══ SUMMARY CARDS ═══ *@
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;">
<div class="admin-stat-card">
<div class="admin-stat-card__icon" style="background:rgba(59,130,246,0.1);">
<i data-lucide="boxes" style="color:#3B82F6;"></i>
</div>
<div class="admin-stat-card__content">
<span class="admin-stat-card__value">@_items.Count</span>
<span class="admin-stat-card__label">Tổng mặt hàng</span>
</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-card__icon" style="background:rgba(34,197,94,0.1);">
<i data-lucide="package-check" style="color:#22C55E;"></i>
</div>
<div class="admin-stat-card__content">
<span class="admin-stat-card__value">@_items.Count(i => i.Quantity > i.ReorderLevel)</span>
<span class="admin-stat-card__label">Đủ hàng</span>
</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-card__icon" style="background:rgba(245,158,11,0.1);">
<i data-lucide="alert-triangle" style="color:#F59E0B;"></i>
</div>
<div class="admin-stat-card__content">
<span class="admin-stat-card__value">@_items.Count(i => i.Quantity <= i.ReorderLevel && i.Quantity > 0)</span>
<span class="admin-stat-card__label">Sắp hết</span>
</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-card__icon" style="background:rgba(239,68,68,0.1);">
<i data-lucide="package-x" style="color:#EF4444;"></i>
</div>
<div class="admin-stat-card__content">
<span class="admin-stat-card__value">@_items.Count(i => i.Quantity <= 0)</span>
<span class="admin-stat-card__label">Hết hàng</span>
</div>
</div>
</div>
@* ═══ INVENTORY TABLE ═══ *@
@if (IsLoading)
{
<div style="text-align:center;padding:48px;">
<div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div>
<p style="color:var(--admin-text-tertiary);font-size:14px;">Đang tải tồn kho...</p>
</div>
}
else if (!FilteredItems.Any())
{
<div style="text-align:center;padding:60px 20px;">
<div style="width:80px;height:80px;border-radius:24px;background:rgba(59,130,246,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">
<i data-lucide="boxes" style="width:36px;height:36px;color:#3B82F6;"></i>
</div>
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFFFFF);">Chưa có dữ liệu tồn kho</h2>
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Tồn kho sẽ tự động cập nhật khi có giao dịch mua/bán</p>
</div>
}
else
{
<div class="admin-panel">
<div class="admin-panel__body" style="padding:0;">
<table class="admin-table" style="width:100%;">
<thead>
<tr>
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Sản phẩm</th>
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Tồn kho</th>
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Đặt trước</th>
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngưỡng</th>
<th style="padding:12px 16px;text-align:center;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Trạng thái</th>
</tr>
</thead>
<tbody>
@foreach (var item in FilteredItems)
{
<tr style="border-top:1px solid var(--admin-border-subtle);">
<td style="padding:12px 16px;font-weight:600;font-size:14px;">@(item.ProductName ?? "N/A")</td>
<td style="padding:12px 16px;text-align:right;font-size:14px;">@item.Quantity</td>
<td style="padding:12px 16px;text-align:right;font-size:14px;color:var(--admin-text-tertiary);">@item.ReservedQuantity</td>
<td style="padding:12px 16px;text-align:right;font-size:14px;color:var(--admin-text-tertiary);">@item.ReorderLevel</td>
<td style="padding:12px 16px;text-align:center;">
@{ var status = GetStockStatus(item); }
<span class="admin-status-badge @status.css" style="font-size:11px;padding:2px 10px;">
<span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>
@status.label
</span>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
</div>
@code {
private string _searchQuery = "";
private Guid? _selectedShopId;
private string _shopName = "";
private List<PosDataService.InventoryItemInfo> _items = new();
private List<PosDataService.ShopInfo> _shops = new();
private IEnumerable<PosDataService.InventoryItemInfo> FilteredItems => _items
.Where(i => string.IsNullOrEmpty(_searchQuery) ||
(i.ProductName ?? "").Contains(_searchQuery, StringComparison.OrdinalIgnoreCase));
protected override async Task OnInitializedAsync()
{
IsLoading = true;
try
{
_shops = await DataService.GetShopsAsync();
_items = await DataService.GetInventoryAsync(_selectedShopId);
}
catch { }
finally { IsLoading = false; }
}
private async Task OnShopFilterChanged(ChangeEventArgs e)
{
var val = e.Value?.ToString();
_selectedShopId = Guid.TryParse(val, out var id) ? id : null;
_shopName = _shops.FirstOrDefault(s => s.Id == _selectedShopId)?.Name ?? "";
IsLoading = true;
try { _items = await DataService.GetInventoryAsync(_selectedShopId); }
catch { }
finally { IsLoading = false; }
}
private static (string css, string label) GetStockStatus(PosDataService.InventoryItemInfo item)
{
if (item.Quantity <= 0) return ("admin-status-badge--offline", "Hết hàng");
if (item.Quantity <= item.ReorderLevel) return ("admin-status-badge--warning", "Sắp hết");
return ("admin-status-badge--online", "Đủ hàng");
}
}

View File

@@ -1,104 +0,0 @@
@page "/admin/products/menu-builder"
@layout AdminLayout
@inherits AdminBase
@inject PosDataService DataService
@using WebClientTpos.Client.Services
<PageTitle>Menu Builder — GoodGo Admin</PageTitle>
<div class="admin-topbar">
<div class="admin-topbar__left">
<h1 class="admin-topbar__title">Menu Builder</h1>
<p class="admin-topbar__subtitle">@_categories.Count danh mục • @_products.Count sản phẩm</p>
</div>
<div class="admin-topbar__right">
<select class="admin-select" style="min-width:160px;" @onchange="OnShopFilterChanged">
<option value="">Tất cả cửa hàng</option>
@foreach (var s in _shops) { <option value="@s.Id">@s.Name</option> }
</select>
</div>
</div>
<div class="admin-content" style="display:flex;gap:24px;">
@* Category sidebar *@
<div style="width:240px;flex-shrink:0;">
<div class="admin-panel">
<div class="admin-panel__header"><h3 class="admin-panel__title">Danh mục</h3></div>
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:4px;">
<button style="padding:8px 12px;border-radius:8px;border:none;text-align:left;font-weight:@(_selectedCategory == null ? "700" : "400");background:@(_selectedCategory == null ? "var(--admin-orange-primary)" : "transparent");color:@(_selectedCategory == null ? "#FFF" : "inherit");cursor:pointer;font-size:14px;" @onclick="@(() => SelectCategory(null))">Tất cả (@_products.Count)</button>
@foreach (var cat in _categories)
{
var catName = cat.Name;
var count = _products.Count(p => string.Equals(p.CategoryName, catName, StringComparison.OrdinalIgnoreCase));
<button style="padding:8px 12px;border-radius:8px;border:none;text-align:left;font-weight:@(_selectedCategory == catName ? "700" : "400");background:@(_selectedCategory == catName ? "var(--admin-orange-primary)" : "transparent");color:@(_selectedCategory == catName ? "#FFF" : "inherit");cursor:pointer;font-size:14px;" @onclick="@(() => SelectCategory(catName))">@catName (@count)</button>
}
</div>
</div>
</div>
@* Products grid *@
<div style="flex:1;">
@if (IsLoading)
{
<div style="text-align:center;padding:48px;"><div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div></div>
}
else if (!FilteredProducts.Any())
{
<div style="text-align:center;padding:60px 20px;"><h2 style="font-size:18px;font-weight:700;color:var(--pos-text-primary, #FFF);">Chưa có sản phẩm</h2></div>
}
else
{
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;">
@foreach (var p in FilteredProducts)
{
<div class="admin-panel" style="cursor:pointer;">
<div class="admin-panel__body" style="padding:16px;text-align:center;">
<div style="width:48px;height:48px;border-radius:12px;background:rgba(255,92,0,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 12px;"><i data-lucide="package" style="color:#FF5C00;width:24px;height:24px;"></i></div>
<div style="font-weight:600;font-size:14px;margin-bottom:4px;">@p.Name</div>
<div style="font-size:13px;color:var(--admin-text-tertiary);">@(p.CategoryName ?? "—")</div>
<div style="font-weight:700;color:var(--admin-orange-primary);margin-top:8px;">@p.Price.ToString("N0")₫</div>
</div>
</div>
}
</div>
}
</div>
</div>
@code {
private List<PosDataService.AdminProductInfo> _products = new();
private List<PosDataService.AdminCategoryInfo> _categories = new();
private List<PosDataService.ShopInfo> _shops = new();
private string? _selectedCategory;
private Guid? _selectedShopId;
private IEnumerable<PosDataService.AdminProductInfo> FilteredProducts => _selectedCategory == null
? _products
: _products.Where(p => string.Equals(p.CategoryName, _selectedCategory, StringComparison.OrdinalIgnoreCase));
protected override async Task OnInitializedAsync()
{
IsLoading = true;
try
{
_shops = await DataService.GetShopsAsync();
_products = await DataService.GetAllProductsAsync();
_categories = await DataService.GetAllCategoriesAsync();
}
catch { } finally { IsLoading = false; }
}
private async Task OnShopFilterChanged(ChangeEventArgs e)
{
_selectedShopId = Guid.TryParse(e.Value?.ToString(), out var id) ? id : null;
IsLoading = true;
try
{
_products = await DataService.GetAllProductsAsync(_selectedShopId);
_categories = await DataService.GetAllCategoriesAsync(_selectedShopId);
}
catch { } finally { IsLoading = false; }
}
private void SelectCategory(string? cat) { _selectedCategory = cat; }
}

View File

@@ -1,194 +0,0 @@
@page "/admin/products"
@layout AdminLayout
@inherits AdminBase
@inject PosDataService DataService
@using WebClientTpos.Client.Services
@*
EN: Product catalog — grid view with real data from catalog-service via BFF.
VI: Danh mục sản phẩm — grid view, dữ liệu thực từ catalog-service qua BFF.
*@
<PageTitle>Sản phẩm — GoodGo Admin</PageTitle>
@* ═══ TOP BAR ═══ *@
<div class="admin-topbar">
<div class="admin-topbar__left">
<h1 class="admin-topbar__title">Sản phẩm & Menu</h1>
<p class="admin-topbar__subtitle">@_products.Count sản phẩm @(_selectedShopId.HasValue ? $"• {_shopName}" : "• Tất cả cửa hàng")</p>
</div>
<div class="admin-topbar__right">
<div class="admin-search" style="width:220px;">
<i data-lucide="search"></i>
<input type="text" placeholder="Tìm sản phẩm..." @bind="_searchQuery" @bind:event="oninput" />
</div>
@* ── Shop filter dropdown ── *@
<select class="admin-select" style="min-width:160px;" @onchange="OnShopFilterChanged">
<option value="">Tất cả cửa hàng</option>
@foreach (var shop in _shops)
{
<option value="@shop.Id" selected="@(_selectedShopId == shop.Id)">@shop.Name</option>
}
</select>
<button class="admin-btn-primary" @onclick="@(() => NavigateTo("products/create"))">
<i data-lucide="plus"></i>
<span>Thêm sản phẩm</span>
</button>
</div>
</div>
@* ═══ CATEGORY TABS ═══ *@
<div class="admin-tabs">
<button class="admin-tab @(_category == "all" ? "admin-tab--active" : "")" @onclick="@(() => _category = "all")">
Tất cả <span class="admin-tab__badge admin-tab__badge--active">@FilteredProducts.Count()</span>
</button>
@foreach (var cat in _categoryNames)
{
<button class="admin-tab @(_category == cat ? "admin-tab--active" : "")" @onclick="@(() => _category = cat)">
@cat <span class="admin-tab__badge">@_products.Count(p => (p.CategoryName ?? "Khác") == cat)</span>
</button>
}
</div>
@* ═══ CONTENT ═══ *@
<div class="admin-content" style="display:flex;flex-direction:column;gap:16px;">
@if (IsLoading)
{
<div style="text-align:center;padding:48px 20px;">
<div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div>
<p style="color:var(--admin-text-tertiary);font-size:14px;">Đang tải sản phẩm...</p>
</div>
}
else if (!FilteredProducts.Any())
{
<div style="text-align:center;padding:60px 20px;">
<div style="width:80px;height:80px;border-radius:24px;background:rgba(255,92,0,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">
<i data-lucide="package" style="width:36px;height:36px;color:var(--admin-orange-primary);"></i>
</div>
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFFFFF);">Chưa có sản phẩm nào</h2>
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0 0 20px;">Thêm sản phẩm đầu tiên để bắt đầu bán hàng</p>
<button class="admin-btn-primary" @onclick="@(() => NavigateTo("products/create"))">
<i data-lucide="plus"></i>
<span>Thêm sản phẩm</span>
</button>
</div>
}
else
{
@* ── Product Grid ── *@
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:16px;">
@foreach (var prod in FilteredProducts)
{
<div class="admin-product-card">
<div class="admin-product-card__image" style="background-color:@(GetTypeBgColor(prod.Type));">
<i data-lucide="@GetTypeIcon(prod.Type)" style="color:@(GetTypeColor(prod.Type));width:32px;height:32px;"></i>
</div>
<div class="admin-product-card__body">
<div style="display:flex;justify-content:space-between;align-items:flex-start;">
<div>
<div style="font-size:14px;font-weight:600;">@prod.Name</div>
<div style="font-size:12px;color:var(--admin-text-tertiary);">@(prod.CategoryName ?? "Không phân loại")</div>
</div>
<div class="admin-status-badge @(prod.IsActive ? "admin-status-badge--online" : "admin-status-badge--offline")" style="font-size:10px;padding:2px 8px;">
<span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>
@(prod.IsActive ? "Có" : "Hết")
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px;">
<span style="font-size:16px;font-weight:700;color:var(--admin-orange-primary);">@FormatPrice(prod.Price)</span>
@if (!string.IsNullOrEmpty(prod.Sku))
{
<span style="font-size:11px;color:var(--admin-text-tertiary);">SKU: @prod.Sku</span>
}
</div>
</div>
</div>
}
</div>
}
</div>
@code {
private string _category = "all";
private string _searchQuery = "";
private Guid? _selectedShopId;
private string _shopName = "";
private List<PosDataService.AdminProductInfo> _products = new();
private List<PosDataService.ShopInfo> _shops = new();
private List<string> _categoryNames = new();
// EN: Filtered products by category and search query.
// VI: Sản phẩm đã lọc theo danh mục và từ khóa tìm kiếm.
private IEnumerable<PosDataService.AdminProductInfo> FilteredProducts => _products
.Where(p => _category == "all" || (p.CategoryName ?? "Khác") == _category)
.Where(p => string.IsNullOrEmpty(_searchQuery) ||
p.Name.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase) ||
(p.Sku ?? "").Contains(_searchQuery, StringComparison.OrdinalIgnoreCase));
protected override async Task OnInitializedAsync()
{
IsLoading = true;
try
{
_shops = await DataService.GetShopsAsync();
_products = await DataService.GetAllProductsAsync(_selectedShopId);
BuildCategoryTabs();
}
catch { }
finally { IsLoading = false; }
}
private async Task OnShopFilterChanged(ChangeEventArgs e)
{
var val = e.Value?.ToString();
_selectedShopId = Guid.TryParse(val, out var id) ? id : null;
_shopName = _shops.FirstOrDefault(s => s.Id == _selectedShopId)?.Name ?? "";
IsLoading = true;
try
{
_products = await DataService.GetAllProductsAsync(_selectedShopId);
BuildCategoryTabs();
_category = "all";
}
catch { }
finally { IsLoading = false; }
}
private void BuildCategoryTabs()
{
_categoryNames = _products
.Select(p => p.CategoryName ?? "Khác")
.Distinct()
.OrderBy(c => c)
.ToList();
}
// EN: Format price with K suffix / VI: Format giá với hậu tố K
private static string FormatPrice(decimal price) =>
price >= 1000 ? $"{price / 1000:0.#}K" : $"{price:N0}đ";
private static string GetTypeIcon(string? type) => type?.ToLowerInvariant() switch
{
"preparedfood" => "coffee",
"physical" => "package",
"service" => "sparkles",
_ => "package"
};
private static string GetTypeColor(string? type) => type?.ToLowerInvariant() switch
{
"preparedfood" => "#FF5C00",
"physical" => "#3B82F6",
"service" => "#8B5CF6",
_ => "#FF5C00"
};
private static string GetTypeBgColor(string? type) => type?.ToLowerInvariant() switch
{
"preparedfood" => "rgba(255,92,0,0.08)",
"physical" => "rgba(59,130,246,0.08)",
"service" => "rgba(139,92,246,0.08)",
_ => "rgba(255,92,0,0.08)"
};
}

View File

@@ -1,241 +0,0 @@
@page "/admin/products/create"
@layout AdminLayout
@inherits AdminBase
@inject PosDataService DataService
@using WebClientTpos.Client.Services
@*
EN: Create product — form with real API submission via BFF.
VI: Thêm sản phẩm — form gửi dữ liệu thực qua BFF.
*@
<PageTitle>Thêm sản phẩm — GoodGo Admin</PageTitle>
@* ═══ TOP BAR ═══ *@
<div class="admin-topbar">
<div class="admin-topbar__left" style="flex-direction:row;align-items:center;gap:12px;">
<button class="admin-icon-btn" @onclick="@(() => NavigateTo("products"))">
<i data-lucide="arrow-left"></i>
</button>
<div>
<h1 class="admin-topbar__title">Thêm sản phẩm mới</h1>
<p class="admin-topbar__subtitle">Điền thông tin sản phẩm</p>
</div>
</div>
<div class="admin-topbar__right">
<button class="admin-btn-secondary" @onclick="@(() => NavigateTo("products"))">Hủy</button>
<button class="admin-btn-primary" @onclick="HandleSubmit" disabled="@_isSaving">
@if (_isSaving)
{
<div class="spinner-small" style="width:16px;height:16px;"></div>
}
else
{
<i data-lucide="check"></i>
}
<span>@(_isSaving ? "Đang lưu..." : "Lưu sản phẩm")</span>
</button>
</div>
</div>
@* ═══ CONTENT ═══ *@
<div class="admin-content" style="display:flex;gap:24px;">
@* LEFT: Main form *@
<div style="flex:1;display:flex;flex-direction:column;gap:24px;">
@* Success/Error messages *@
@if (!string.IsNullOrEmpty(_message))
{
<div class="admin-panel" style="background:@(_isSuccess ? "rgba(34,197,94,0.1)" : "rgba(239,68,68,0.1)");border:1px solid @(_isSuccess ? "#22C55E" : "#EF4444");">
<div class="admin-panel__body" style="display:flex;align-items:center;gap:12px;">
<i data-lucide="@(_isSuccess ? "check-circle" : "alert-circle")" style="color:@(_isSuccess ? "#22C55E" : "#EF4444");width:20px;height:20px;"></i>
<span style="font-size:14px;">@_message</span>
</div>
</div>
}
@* Basic Info *@
<div class="admin-panel">
<div class="admin-panel__header">
<h3 class="admin-panel__title">
<i data-lucide="package" style="color:var(--admin-orange-primary);"></i>
Thông tin cơ bản
</h3>
</div>
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:20px;">
<div class="admin-form-group">
<label class="admin-form-label">Cửa hàng <span style="color:var(--admin-danger);">*</span></label>
<select class="admin-form-input" @bind="_shopId">
<option value="">Chọn cửa hàng...</option>
@foreach (var shop in _shops)
{
<option value="@shop.Id">@shop.Name</option>
}
</select>
</div>
<div class="admin-form-group">
<label class="admin-form-label">Tên sản phẩm <span style="color:var(--admin-danger);">*</span></label>
<input class="admin-form-input" type="text" placeholder="VD: Cappuccino" @bind="_name" />
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="admin-form-group">
<label class="admin-form-label">Loại sản phẩm</label>
<select class="admin-form-input" @bind="_type">
<option value="PreparedFood">Đồ ăn/uống pha chế</option>
<option value="Physical">Hàng hóa vật lý</option>
<option value="Service">Dịch vụ</option>
</select>
</div>
<div class="admin-form-group">
<label class="admin-form-label">SKU</label>
<input class="admin-form-input" type="text" placeholder="Tự động tạo" @bind="_sku" />
</div>
</div>
<div class="admin-form-group">
<label class="admin-form-label">Mô tả</label>
<textarea class="admin-form-input" rows="3" placeholder="Mô tả ngắn về sản phẩm..." @bind="_description"></textarea>
</div>
</div>
</div>
@* Pricing *@
<div class="admin-panel">
<div class="admin-panel__header">
<h3 class="admin-panel__title">
<i data-lucide="tag" style="color:#22C55E;"></i>
Giá bán
</h3>
</div>
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="admin-form-group">
<label class="admin-form-label">Giá bán (VND) <span style="color:var(--admin-danger);">*</span></label>
<input class="admin-form-input" type="number" placeholder="VD: 45000" @bind="_price" />
</div>
</div>
</div>
</div>
</div>
@* RIGHT: Image & Shop *@
<div style="width:320px;display:flex;flex-direction:column;gap:20px;">
@* Image Upload (placeholder) *@
<div class="admin-panel">
<div class="admin-panel__header">
<h3 class="admin-panel__title">Hình ảnh</h3>
</div>
<div class="admin-panel__body" style="display:flex;flex-direction:column;align-items:center;gap:12px;">
<div style="width:100%;height:200px;background:var(--admin-bg-interactive);border:2px dashed var(--admin-border-default);border-radius:12px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;cursor:pointer;">
<i data-lucide="image-plus" style="width:32px;height:32px;color:var(--admin-text-tertiary);"></i>
<span style="font-size:13px;color:var(--admin-text-tertiary);">Kéo thả hoặc click để upload</span>
<span style="font-size:11px;color:var(--admin-text-tertiary);">PNG, JPG tối đa 2MB</span>
</div>
</div>
</div>
@* Quick preview *@
@if (!string.IsNullOrEmpty(_name))
{
<div class="admin-panel">
<div class="admin-panel__header">
<h3 class="admin-panel__title">
<i data-lucide="eye" style="color:#3B82F6;"></i>
Xem trước
</h3>
</div>
<div class="admin-panel__body">
<div class="admin-product-card">
<div class="admin-product-card__image" style="background-color:rgba(255,92,0,0.08);">
<i data-lucide="@GetTypeIcon(_type)" style="color:var(--admin-orange-primary);width:32px;height:32px;"></i>
</div>
<div class="admin-product-card__body">
<div style="font-size:14px;font-weight:600;">@_name</div>
<div style="font-size:12px;color:var(--admin-text-tertiary);">@GetTypeLabel(_type)</div>
<div style="font-size:16px;font-weight:700;color:var(--admin-orange-primary);margin-top:8px;">@FormatPrice(_price)</div>
</div>
</div>
</div>
</div>
}
</div>
</div>
@code {
private string _shopId = "";
private string _name = "";
private string _description = "";
private decimal _price;
private string _type = "PreparedFood";
private string _sku = "";
private string _message = "";
private bool _isSuccess;
private bool _isSaving;
private List<PosDataService.ShopInfo> _shops = new();
protected override async Task OnInitializedAsync()
{
try { _shops = await DataService.GetShopsAsync(); }
catch { }
}
private async Task HandleSubmit()
{
_message = "";
// EN: Validate required fields / VI: Kiểm tra trường bắt buộc
if (string.IsNullOrWhiteSpace(_shopId) || !Guid.TryParse(_shopId, out var shopId))
{ _message = "Vui lòng chọn cửa hàng."; _isSuccess = false; return; }
if (string.IsNullOrWhiteSpace(_name))
{ _message = "Vui lòng nhập tên sản phẩm."; _isSuccess = false; return; }
if (_price <= 0)
{ _message = "Giá bán phải lớn hơn 0."; _isSuccess = false; return; }
_isSaving = true;
try
{
var req = new PosDataService.CreateProductRequest(
shopId, _name.Trim(), _description.Trim(), _price,
_type, string.IsNullOrWhiteSpace(_sku) ? null : _sku.Trim(), null);
var ok = await DataService.CreateProductAsync(req);
if (ok)
{
_message = $"Đã tạo sản phẩm \"{_name}\" thành công!";
_isSuccess = true;
// Reset form
_name = ""; _description = ""; _price = 0; _sku = "";
}
else
{
_message = "Không thể tạo sản phẩm. Vui lòng thử lại.";
_isSuccess = false;
}
}
catch (Exception ex)
{
_message = $"Lỗi: {ex.Message}";
_isSuccess = false;
}
finally { _isSaving = false; }
}
private static string FormatPrice(decimal price) =>
price >= 1000 ? $"{price / 1000:0.#}K" : price > 0 ? $"{price:N0}đ" : "--";
private static string GetTypeIcon(string? type) => type switch
{
"PreparedFood" => "coffee",
"Physical" => "package",
"Service" => "sparkles",
_ => "package"
};
private static string GetTypeLabel(string? type) => type switch
{
"PreparedFood" => "Đồ ăn/uống",
"Physical" => "Hàng hóa",
"Service" => "Dịch vụ",
_ => "Sản phẩm"
};
}

View File

@@ -1,101 +0,0 @@
@page "/admin/staff/create"
@layout AdminLayout
@inherits AdminBase
@inject PosDataService DataService
@using WebClientTpos.Client.Services
<PageTitle>Thêm nhân viên — GoodGo Admin</PageTitle>
<div class="admin-topbar">
<div class="admin-topbar__left" style="flex-direction:row;align-items:center;gap:12px;">
<button class="admin-icon-btn" @onclick="@(() => NavigateTo("staff"))"><i data-lucide="arrow-left"></i></button>
<div>
<h1 class="admin-topbar__title">Thêm nhân viên mới</h1>
<p class="admin-topbar__subtitle">Điền thông tin nhân viên</p>
</div>
</div>
<div class="admin-topbar__right">
<button class="admin-btn-secondary" @onclick="@(() => NavigateTo("staff"))">Hủy</button>
<button class="admin-btn-primary" @onclick="HandleSubmit" disabled="@_isSaving">
@if (_isSaving) { <div class="spinner-small" style="width:16px;height:16px;"></div> }
else { <i data-lucide="check"></i> }
<span>@(_isSaving ? "Đang lưu..." : "Lưu nhân viên")</span>
</button>
</div>
</div>
<div class="admin-content" style="display:flex;gap:24px;">
<div style="flex:1;display:flex;flex-direction:column;gap:24px;">
@if (!string.IsNullOrEmpty(_message))
{
<div class="admin-panel" style="background:@(_isSuccess ? "rgba(34,197,94,0.1)" : "rgba(239,68,68,0.1)");border:1px solid @(_isSuccess ? "#22C55E" : "#EF4444");">
<div class="admin-panel__body" style="display:flex;align-items:center;gap:12px;">
<i data-lucide="@(_isSuccess ? "check-circle" : "alert-circle")" style="color:@(_isSuccess ? "#22C55E" : "#EF4444");width:20px;height:20px;"></i>
<span style="font-size:14px;">@_message</span>
</div>
</div>
}
<div class="admin-panel">
<div class="admin-panel__header">
<h3 class="admin-panel__title"><i data-lucide="user-plus" style="color:var(--admin-orange-primary);"></i> Thông tin nhân viên</h3>
</div>
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="admin-form-group">
<label class="admin-form-label">Mã nhân viên</label>
<input class="admin-form-input" type="text" placeholder="VD: NV001" @bind="_empCode" />
</div>
<div class="admin-form-group">
<label class="admin-form-label">Vai trò <span style="color:var(--admin-danger);">*</span></label>
<select class="admin-form-input" @bind="_role">
@foreach (var r in _roles)
{
<option value="@r.Name">@r.Name</option>
}
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="admin-form-group">
<label class="admin-form-label">Số điện thoại</label>
<input class="admin-form-input" type="tel" placeholder="0901234567" @bind="_phone" />
</div>
<div class="admin-form-group">
<label class="admin-form-label">Email</label>
<input class="admin-form-input" type="email" placeholder="nv@goodgo.vn" @bind="_email" />
</div>
</div>
</div>
</div>
</div>
</div>
@code {
private string _empCode = "", _role = "Cashier", _phone = "", _email = "", _message = "";
private bool _isSuccess, _isSaving;
private List<PosDataService.StaffRoleInfo> _roles = new();
protected override async Task OnInitializedAsync()
{
try { _roles = await DataService.GetStaffRolesAsync(); if (_roles.Any()) _role = _roles[0].Name; } catch { }
}
private async Task HandleSubmit()
{
_message = "";
if (string.IsNullOrWhiteSpace(_role)) { _message = "Vui lòng chọn vai trò."; _isSuccess = false; return; }
_isSaving = true;
try
{
// EN: Use first merchant or Guid.Empty / VI: Dùng merchant đầu tiên hoặc Guid.Empty
var shops = await DataService.GetShopsAsync();
var merchantId = Guid.Empty; // Will be set correctly when merchant context is available
var ok = await DataService.CreateStaffAsync(new(merchantId, _empCode.Trim(), _phone.Trim(), _email.Trim(), _role));
if (ok) { _message = "Đã thêm nhân viên thành công!"; _isSuccess = true; _empCode = ""; _phone = ""; _email = ""; }
else { _message = "Không thể thêm nhân viên."; _isSuccess = false; }
}
catch (Exception ex) { _message = $"Lỗi: {ex.Message}"; _isSuccess = false; }
finally { _isSaving = false; }
}
}

View File

@@ -1,168 +0,0 @@
@page "/admin/staff"
@layout AdminLayout
@inherits AdminBase
@inject WebClientTpos.Client.Services.PosDataService DataService
@using WebClientTpos.Client.Services
@*
EN: Staff directory — real data from BFF, grid of staff cards with search & filter.
VI: Danh bạ nhân sự — dữ liệu thật từ BFF, grid thẻ nhân viên, tìm kiếm & lọc.
*@
<PageTitle>Quản lý nhân sự — GoodGo Admin</PageTitle>
@* ═══ TOP BAR ═══ *@
<div class="admin-topbar">
<div class="admin-topbar__left">
<h1 class="admin-topbar__title">Quản lý nhân sự</h1>
<p class="admin-topbar__subtitle">Tất cả cửa hàng • @_staffList.Count nhân viên</p>
</div>
<div class="admin-topbar__right">
<div class="admin-search" style="width:220px;">
<i data-lucide="search"></i>
<input type="text" placeholder="Tìm nhân viên..." @bind="SearchQuery" @bind:event="oninput" />
</div>
<button class="admin-btn-primary" @onclick="@(() => NavigateTo("staff/create"))">
<i data-lucide="user-plus"></i>
<span>Thêm nhân viên</span>
</button>
</div>
</div>
@* ═══ TABS ═══ *@
<div class="admin-tabs">
<button class="admin-tab @(_activeTab == "all" ? "admin-tab--active" : "")" @onclick="@(() => _activeTab = "all")">
Tất cả <span class="admin-tab__badge admin-tab__badge--active">@_staffList.Count</span>
</button>
<button class="admin-tab @(_activeTab == "active" ? "admin-tab--active" : "")" @onclick="@(() => _activeTab = "active")">
Đang làm <span class="admin-tab__badge">@_staffList.Count(s => s.Status == "Active")</span>
</button>
<button class="admin-tab @(_activeTab == "invited" ? "admin-tab--active" : "")" @onclick="@(() => _activeTab = "invited")">
Chờ xác nhận <span class="admin-tab__badge">@_staffList.Count(s => s.Status == "Invited")</span>
</button>
<button class="admin-tab @(_activeTab == "inactive" ? "admin-tab--active" : "")" @onclick="@(() => _activeTab = "inactive")">
Ngưng hoạt động <span class="admin-tab__badge">@_staffList.Count(s => s.Status == "Inactive" || s.Status == "Terminated")</span>
</button>
</div>
@* ═══ CONTENT ═══ *@
@if (_loading)
{
<div class="admin-content" style="display:flex;align-items:center;justify-content:center;min-height:300px;">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
</div>
}
else if (!FilteredStaff.Any())
{
<div class="admin-content" style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;gap:12px;">
<i data-lucide="users" style="width:48px;height:48px;color:var(--admin-text-tertiary);"></i>
<span style="color:var(--admin-text-tertiary);font-size:15px;">Chưa có nhân viên nào</span>
<button class="admin-btn-primary" @onclick="@(() => NavigateTo("staff/create"))">
<i data-lucide="user-plus"></i>
<span>Thêm nhân viên đầu tiên</span>
</button>
</div>
}
else
{
<div class="admin-content" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px;">
@foreach (var staff in FilteredStaff)
{
<div class="admin-staff-card">
<div class="admin-staff-card__header">
<div class="admin-user-avatar" style="width:48px;height:48px;font-size:16px;background-color:@GetAvatarColor(staff.Role ?? "");">
@GetInitials(staff.Email ?? staff.EmployeeCode ?? "?")
</div>
<div class="admin-status-badge @GetStatusCss(staff.Status)">
<span class="admin-status-badge__dot"></span>
@GetStatusLabel(staff.Status)
</div>
</div>
<div style="display:flex;flex-direction:column;gap:2px;">
<span style="font-size:16px;font-weight:600;">@(staff.Email ?? staff.EmployeeCode ?? "Nhân viên")</span>
<span style="font-size:12px;color:var(--admin-text-tertiary);">@(staff.Role ?? "—") • @(staff.ShopName ?? "Chưa gán")</span>
</div>
<div style="display:flex;gap:8px;">
<div class="admin-store-stat" style="flex:1;">
<div class="admin-store-stat__value">@(staff.EmployeeCode ?? "—")</div>
<div class="admin-store-stat__label">Mã NV</div>
</div>
<div class="admin-store-stat" style="flex:1;">
<div class="admin-store-stat__value">@(staff.Phone ?? "—")</div>
<div class="admin-store-stat__label">Điện thoại</div>
</div>
<div class="admin-store-stat" style="flex:1;">
<div class="admin-store-stat__value">@FormatDate(staff.JoinedAt)</div>
<div class="admin-store-stat__label">Ngày vào</div>
</div>
</div>
</div>
}
</div>
}
@code {
private bool _loading = true;
private string _activeTab = "all";
private List<PosDataService.StaffInfo> _staffList = new();
private IEnumerable<PosDataService.StaffInfo> FilteredStaff
{
get
{
var list = _activeTab switch
{
"active" => _staffList.Where(s => s.Status == "Active"),
"invited" => _staffList.Where(s => s.Status == "Invited"),
"inactive" => _staffList.Where(s => s.Status == "Inactive" || s.Status == "Terminated"),
_ => _staffList
};
if (!string.IsNullOrEmpty(SearchQuery))
list = list.Where(s => (s.Email ?? "").Contains(SearchQuery, StringComparison.OrdinalIgnoreCase)
|| (s.EmployeeCode ?? "").Contains(SearchQuery, StringComparison.OrdinalIgnoreCase)
|| (s.Phone ?? "").Contains(SearchQuery, StringComparison.OrdinalIgnoreCase)
|| (s.ShopName ?? "").Contains(SearchQuery, StringComparison.OrdinalIgnoreCase));
return list;
}
}
protected override async Task OnInitializedAsync()
{
try { _staffList = await DataService.GetStaffAsync(); }
catch { _staffList = new(); }
finally { _loading = false; }
}
private static string GetInitials(string value)
{
if (value.Contains('@')) value = value.Split('@')[0];
return value.Length >= 2 ? value[..2].ToUpper() : value.ToUpper();
}
private static string GetAvatarColor(string role) => role switch
{
"Cashier" => "#3B82F6",
"Waiter" => "#22C55E",
"Manager" => "#8B5CF6",
"Admin" => "#FF5C00",
_ => "#6B7280"
};
private static string GetStatusCss(string? status) => status switch
{
"Active" => "admin-status-badge--online",
"Invited" => "admin-status-badge--setup",
_ => "admin-status-badge--offline"
};
private static string GetStatusLabel(string? status) => status switch
{
"Active" => "Đang làm",
"Invited" => "Chờ xác nhận",
"Inactive" => "Tạm nghỉ",
"Terminated" => "Đã nghỉ",
_ => status ?? "—"
};
private static string FormatDate(DateTime? dt) => dt.HasValue ? dt.Value.ToString("dd/MM/yy") : "—";
}

View File

@@ -1,276 +0,0 @@
@page "/admin/stores/{Id}"
@layout AdminLayout
@inherits AdminBase
@inject PosDataService DataService
@using WebClientTpos.Client.Services
@*
EN: Store detail — overview with KPIs (real data from API).
VI: Chi tiết cửa hàng — tổng quan KPI (dữ liệu thực từ API).
*@
<PageTitle>@(_shop?.Name ?? "Cửa hàng") — GoodGo Admin</PageTitle>
@* ═══ TOP BAR ═══ *@
<div class="admin-topbar">
<div class="admin-topbar__left">
<button class="admin-icon-btn" style="margin-right:8px;" @onclick="@(() => NavigateTo("stores"))">
<i data-lucide="arrow-left"></i>
</button>
<div>
<h1 class="admin-topbar__title">@(_shop?.Name ?? "Đang tải...")</h1>
<p class="admin-topbar__subtitle">@(_shop?.Category ?? "—") • @(_shop?.Slug ?? "—")</p>
</div>
</div>
<div class="admin-topbar__right">
@if (_shop != null)
{
<div class="admin-status-badge admin-status-badge--@GetStatusBadgeClass(_shop.Status)">
<span class="admin-status-badge__dot"></span>
@GetStatusLabel(_shop.Status)
</div>
}
<button class="admin-btn-secondary" @onclick="@(() => NavigateTo($"stores/{Id}/settings"))">
<i data-lucide="settings"></i>
<span>Cài đặt</span>
</button>
<button class="admin-btn-primary">
<i data-lucide="monitor"></i>
<span>Mở POS</span>
</button>
</div>
</div>
@* ═══ CONTENT ═══ *@
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
@if (IsLoading)
{
<div style="text-align:center;padding:48px 20px;">
<div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div>
<p style="color:var(--admin-text-tertiary);font-size:14px;">Đang tải thông tin cửa hàng...</p>
</div>
}
else if (_shop == null)
{
<div style="text-align:center;padding:48px 20px;">
<i data-lucide="alert-circle" style="width:48px;height:48px;color:#EF4444;margin-bottom:16px;"></i>
<h3 style="font-size:18px;font-weight:700;color:var(--pos-text-primary, #FFFFFF);margin:0 0 8px;">Không tìm thấy cửa hàng</h3>
<p style="font-size:14px;color:var(--pos-text-tertiary, #ADADB0);margin:0 0 20px;">Cửa hàng không tồn tại hoặc đã bị xóa.</p>
<button class="admin-btn-primary" @onclick="@(() => NavigateTo("stores"))">
<i data-lucide="arrow-left"></i>
<span>Quay lại danh sách</span>
</button>
</div>
}
else
{
@* ── Store Info Card ── *@
<div class="admin-panel">
<div class="admin-panel__body" style="display:flex;gap:20px;align-items:center;">
<div class="admin-store-card__avatar" style="width:64px;height:64px;background-color:rgba(255,92,0,0.125);border-radius:16px;">
<i data-lucide="@GetShopIcon(_shop.Category)" style="color:var(--admin-orange-primary);width:28px;height:28px;"></i>
</div>
<div style="flex:1;">
<div style="font-size:18px;font-weight:700;">@_shop.Name</div>
<div style="font-size:13px;color:var(--admin-text-tertiary);">@_shop.Slug • @(_shop.Category ?? "—")</div>
@if (!string.IsNullOrEmpty(_shop.Description))
{
<div style="font-size:13px;color:var(--admin-text-tertiary);margin-top:4px;">@_shop.Description</div>
}
</div>
<div style="display:flex;flex-direction:column;gap:4px;align-items:flex-end;">
@if (!string.IsNullOrEmpty(_shop.Phone))
{
<div style="font-size:13px;color:var(--admin-text-tertiary);display:flex;align-items:center;gap:6px;">
<i data-lucide="phone" style="width:14px;height:14px;"></i> @_shop.Phone
</div>
}
@if (!string.IsNullOrEmpty(_shop.Email))
{
<div style="font-size:13px;color:var(--admin-text-tertiary);display:flex;align-items:center;gap:6px;">
<i data-lucide="mail" style="width:14px;height:14px;"></i> @_shop.Email
</div>
}
</div>
</div>
</div>
@* ── KPI ROW (placeholder — chưa có dữ liệu order/revenue) ── *@
<div class="admin-kpi-row">
<div class="admin-kpi-card">
<div class="admin-kpi-card__header">
<div class="admin-kpi-card__icon" style="background-color:rgba(34,197,94,0.125);">
<i data-lucide="trending-up" style="color:#22C55E;"></i>
</div>
</div>
<div class="admin-kpi-card__value">--</div>
<div class="admin-kpi-card__label">Doanh thu tháng</div>
</div>
<div class="admin-kpi-card">
<div class="admin-kpi-card__header">
<div class="admin-kpi-card__icon" style="background-color:rgba(59,130,246,0.125);">
<i data-lucide="shopping-bag" style="color:#3B82F6;"></i>
</div>
</div>
<div class="admin-kpi-card__value">--</div>
<div class="admin-kpi-card__label">Đơn hàng tháng</div>
</div>
<div class="admin-kpi-card">
<div class="admin-kpi-card__header">
<div class="admin-kpi-card__icon" style="background-color:rgba(139,92,246,0.125);">
<i data-lucide="banknote" style="color:#8B5CF6;"></i>
</div>
</div>
<div class="admin-kpi-card__value">--</div>
<div class="admin-kpi-card__label">Giá trị TB / đơn</div>
</div>
<div class="admin-kpi-card">
<div class="admin-kpi-card__header">
<div class="admin-kpi-card__icon" style="background-color:rgba(236,72,153,0.125);">
<i data-lucide="star" style="color:#EC4899;"></i>
</div>
</div>
<div class="admin-kpi-card__value">--</div>
<div class="admin-kpi-card__label">Đánh giá TB</div>
</div>
</div>
@* ── BOTTOM: Chart + Right Column ── *@
<div style="display:flex;gap:24px;flex:1;min-height:0;">
@* LEFT: Revenue Chart Placeholder *@
<div class="admin-panel" style="flex:1;">
<div class="admin-panel__header">
<h3 class="admin-panel__title">
<i data-lucide="bar-chart-2" style="color:var(--admin-orange-primary);"></i>
Doanh thu 7 ngày gần nhất
</h3>
<div style="display:flex;gap:8px;">
<button class="admin-tab admin-tab--active" style="padding:6px 12px;font-size:12px;">7 ngày</button>
<button class="admin-tab" style="padding:6px 12px;font-size:12px;">30 ngày</button>
</div>
</div>
<div class="admin-panel__body" style="display:flex;align-items:center;justify-content:center;min-height:200px;">
<div style="text-align:center;color:var(--admin-text-tertiary);">
<i data-lucide="bar-chart-2" style="width:40px;height:40px;margin-bottom:12px;opacity:0.5;"></i>
<p style="font-size:14px;margin:0;">Chưa có dữ liệu doanh thu</p>
<p style="font-size:12px;margin:4px 0 0;">Dữ liệu sẽ hiển thị khi có đơn hàng</p>
</div>
</div>
</div>
@* RIGHT COLUMN *@
<div style="width:380px;display:flex;flex-direction:column;gap:20px;">
@* Recent Orders Placeholder *@
<div class="admin-panel">
<div class="admin-panel__header">
<h3 class="admin-panel__title">
<i data-lucide="shopping-bag" style="color:#3B82F6;"></i>
Đơn gần nhất
</h3>
</div>
<div class="admin-panel__body" style="text-align:center;padding:20px;color:var(--admin-text-tertiary);font-size:14px;">
<i data-lucide="inbox" style="width:32px;height:32px;margin-bottom:8px;opacity:0.5;"></i>
<p style="margin:0;">Chưa có đơn hàng nào</p>
</div>
</div>
@* Shop Quick Info *@
<div class="admin-panel" style="flex:1;">
<div class="admin-panel__header">
<h3 class="admin-panel__title">
<i data-lucide="info" style="color:#22C55E;"></i>
Thông tin
</h3>
</div>
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="color:var(--admin-text-tertiary);font-size:14px;">Trạng thái</span>
<span style="font-size:13px;font-weight:600;">@GetStatusLabel(_shop.Status)</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="color:var(--admin-text-tertiary);font-size:14px;">Ngành hàng</span>
<span style="font-size:13px;font-weight:600;">@(_shop.Category ?? "—")</span>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="color:var(--admin-text-tertiary);font-size:14px;">Slug</span>
<span style="font-size:13px;font-weight:600;">@(_shop.Slug)</span>
</div>
@if (!string.IsNullOrEmpty(_shop.Phone))
{
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="color:var(--admin-text-tertiary);font-size:14px;">Điện thoại</span>
<span style="font-size:13px;font-weight:600;">@_shop.Phone</span>
</div>
}
@if (!string.IsNullOrEmpty(_shop.Email))
{
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="color:var(--admin-text-tertiary);font-size:14px;">Email</span>
<span style="font-size:13px;font-weight:600;">@_shop.Email</span>
</div>
}
</div>
</div>
</div>
</div>
}
</div>
@code {
[Parameter] public string Id { get; set; } = "";
private PosDataService.ShopInfo? _shop;
protected override async Task OnInitializedAsync()
{
IsLoading = true;
try
{
if (Guid.TryParse(Id, out var shopId))
{
_shop = await DataService.GetShopByIdAsync(shopId);
}
}
catch
{
_shop = null;
}
finally
{
IsLoading = false;
}
}
private static string GetStatusBadgeClass(string? status) => status?.ToLowerInvariant() switch
{
"published" or "active" => "online",
"draft" or "setup" => "setup",
"inactive" or "paused" => "paused",
_ => "setup"
};
private static string GetStatusLabel(string? status) => status?.ToLowerInvariant() switch
{
"published" or "active" => "Đang mở",
"draft" or "setup" => "Thiết lập",
"inactive" or "paused" => "Tạm dừng",
"closed" => "Đã đóng",
_ => status ?? "—"
};
private static string GetShopIcon(string? category) => category?.ToLowerInvariant() switch
{
"foodbeverage" or "café" or "cafe" or "coffee" => "coffee",
"restaurant" or "nhà hàng" => "utensils",
"entertainment" or "karaoke" => "mic",
"beauty" or "spa" => "sparkles",
"retail" => "shopping-bag",
_ => "store"
};
}

View File

@@ -1,364 +0,0 @@
@page "/admin/stores/{Id}/settings"
@layout AdminLayout
@inherits AdminBase
@inject PosDataService DataService
@using WebClientTpos.Client.Services
@using WebClientTpos.Shared.DTOs
@*
EN: Store settings — tabs for general, payment, receipts, integrations (real data from API).
VI: Cài đặt cửa hàng — tabs cho chung, thanh toán, hóa đơn, tích hợp (dữ liệu thực từ API).
*@
<PageTitle>Cài đặt cửa hàng — GoodGo Admin</PageTitle>
@* ═══ TOP BAR ═══ *@
<div class="admin-topbar">
<div class="admin-topbar__left">
<button class="admin-icon-btn" style="margin-right:8px;" @onclick="@(() => NavigateTo($"stores/{Id}"))">
<i data-lucide="arrow-left"></i>
</button>
<div>
<h1 class="admin-topbar__title">Cài đặt cửa hàng</h1>
<p class="admin-topbar__subtitle">@(_shop?.Name ?? "Đang tải...") — Quản lý cài đặt</p>
</div>
</div>
<div class="admin-topbar__right">
@if (!string.IsNullOrEmpty(_successMessage))
{
<span style="font-size:13px;color:#22C55E;display:flex;align-items:center;gap:4px;">
<i data-lucide="check-circle" style="width:14px;height:14px;"></i>
@_successMessage
</span>
}
@if (!string.IsNullOrEmpty(_errorMessage))
{
<span style="font-size:13px;color:#EF4444;">@_errorMessage</span>
}
<button class="admin-btn-primary" @onclick="SaveSettings" disabled="@_isSaving">
@if (_isSaving)
{
<span class="spinner-small" style="margin-right:6px;"></span>
<span>Đang lưu...</span>
}
else
{
<i data-lucide="check"></i>
<span>Lưu thay đổi</span>
}
</button>
</div>
</div>
@* ═══ TABS ═══ *@
<div class="admin-tabs">
<button class="admin-tab @(_tab == "general" ? "admin-tab--active" : "")" @onclick="@(() => _tab = "general")">
<i data-lucide="settings" style="width:16px;height:16px;"></i>
Chung
</button>
<button class="admin-tab @(_tab == "payment" ? "admin-tab--active" : "")" @onclick="@(() => _tab = "payment")">
<i data-lucide="credit-card" style="width:16px;height:16px;"></i>
Thanh toán
</button>
<button class="admin-tab @(_tab == "receipt" ? "admin-tab--active" : "")" @onclick="@(() => _tab = "receipt")">
<i data-lucide="receipt" style="width:16px;height:16px;"></i>
Hóa đơn
</button>
<button class="admin-tab @(_tab == "integrations" ? "admin-tab--active" : "")" @onclick="@(() => _tab = "integrations")">
<i data-lucide="plug" style="width:16px;height:16px;"></i>
Tích hợp
</button>
</div>
@* ═══ CONTENT ═══ *@
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
@if (IsLoading)
{
<div style="text-align:center;padding:48px 20px;">
<div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div>
<p style="color:var(--admin-text-tertiary);font-size:14px;">Đang tải cài đặt cửa hàng...</p>
</div>
}
else if (_shop == null)
{
<div style="text-align:center;padding:48px 20px;">
<i data-lucide="alert-circle" style="width:48px;height:48px;color:#EF4444;margin-bottom:16px;"></i>
<h3 style="font-size:18px;font-weight:700;color:var(--pos-text-primary, #FFFFFF);margin:0 0 8px;">Không tìm thấy cửa hàng</h3>
<button class="admin-btn-primary" @onclick="@(() => NavigateTo("stores"))">Quay lại</button>
</div>
}
else
{
@if (_tab == "general")
{
@* General Settings *@
<div class="admin-panel">
<div class="admin-panel__header">
<h3 class="admin-panel__title">Thông tin cửa hàng</h3>
</div>
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="admin-form-group">
<label class="admin-form-label">Tên cửa hàng</label>
<input class="admin-form-input" type="text" @bind="_editName" />
</div>
<div class="admin-form-group">
<label class="admin-form-label">Slug (URL)</label>
<input class="admin-form-input" type="text" value="@_shop.Slug" disabled />
<small style="color:var(--admin-text-tertiary);font-size:12px;">Không thể thay đổi slug sau khi tạo</small>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="admin-form-group">
<label class="admin-form-label">Ngành hàng</label>
<input class="admin-form-input" type="text" value="@(_shop.Category ?? "—")" disabled />
</div>
<div class="admin-form-group">
<label class="admin-form-label">Trạng thái</label>
<input class="admin-form-input" type="text" value="@GetStatusLabel(_shop.Status)" disabled />
</div>
</div>
<div class="admin-form-group">
<label class="admin-form-label">Mô tả</label>
<textarea class="admin-form-input" rows="3" @bind="_editDescription"></textarea>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="admin-form-group">
<label class="admin-form-label">Điện thoại</label>
<input class="admin-form-input" type="tel" @bind="_editPhone" />
</div>
<div class="admin-form-group">
<label class="admin-form-label">Email</label>
<input class="admin-form-input" type="email" @bind="_editEmail" />
</div>
</div>
</div>
</div>
@* Danger Zone *@
<div class="admin-panel" style="border:1px solid rgba(239,68,68,0.3);">
<div class="admin-panel__header">
<h3 class="admin-panel__title" style="color:#EF4444;">
<i data-lucide="alert-triangle" style="color:#EF4444;"></i>
Vùng nguy hiểm
</h3>
</div>
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:16px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div>
<div style="font-weight:600;">Tạm dừng cửa hàng</div>
<div style="font-size:12px;color:var(--admin-text-tertiary);">Cửa hàng sẽ ngừng hoạt động tạm thời</div>
</div>
<button class="admin-btn-secondary" style="color:#F59E0B;border-color:rgba(245,158,11,0.3);">
<i data-lucide="pause"></i>
Tạm dừng
</button>
</div>
<div style="height:1px;background:var(--admin-border-subtle);"></div>
<div style="display:flex;justify-content:space-between;align-items:center;">
<div>
<div style="font-weight:600;color:#EF4444;">Đóng cửa hàng</div>
<div style="font-size:12px;color:var(--admin-text-tertiary);">Không thể hoàn tác, cửa hàng sẽ bị đóng vĩnh viễn</div>
</div>
<button class="admin-btn-secondary" style="color:#EF4444;border-color:rgba(239,68,68,0.3);">
<i data-lucide="trash-2"></i>
Đóng
</button>
</div>
</div>
</div>
}
@if (_tab == "payment")
{
@* Payment Methods *@
<div class="admin-panel">
<div class="admin-panel__header">
<h3 class="admin-panel__title">Phương thức thanh toán</h3>
</div>
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
@foreach (var pm in _paymentMethods)
{
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px;background:var(--admin-bg-interactive);border-radius:12px;">
<div style="display:flex;align-items:center;gap:12px;">
<i data-lucide="@pm.Icon" style="color:@pm.Color;"></i>
<div>
<div style="font-weight:600;font-size:14px;">@pm.Name</div>
<div style="font-size:12px;color:var(--admin-text-tertiary);">@pm.Desc</div>
</div>
</div>
<MudSwitch T="bool" Value="pm.Enabled" Color="Color.Primary" />
</div>
}
</div>
</div>
}
@if (_tab == "receipt")
{
<div class="admin-panel">
<div class="admin-panel__header">
<h3 class="admin-panel__title">Mẫu hóa đơn</h3>
</div>
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:20px;">
<div class="admin-form-group">
<label class="admin-form-label">Header hóa đơn</label>
<input class="admin-form-input" type="text" value="@(_shop.Name)" />
</div>
<div class="admin-form-group">
<label class="admin-form-label">Footer</label>
<textarea class="admin-form-input" rows="2">Cảm ơn quý khách! Hẹn gặp lại!</textarea>
</div>
<div style="display:flex;gap:16px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<MudCheckBox T="bool" Value="true" Color="Color.Primary" />
<span style="font-size:13px;">Hiện logo</span>
</label>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<MudCheckBox T="bool" Value="true" Color="Color.Primary" />
<span style="font-size:13px;">Hiện QR code</span>
</label>
</div>
</div>
</div>
}
@if (_tab == "integrations")
{
<div class="admin-panel">
<div class="admin-panel__header">
<h3 class="admin-panel__title">Tích hợp bên thứ ba</h3>
</div>
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;justify-content:space-between;align-items:center;padding:14px;background:var(--admin-bg-interactive);border-radius:12px;">
<div style="display:flex;align-items:center;gap:12px;">
<i data-lucide="truck" style="color:var(--admin-text-tertiary);"></i>
<div>
<div style="font-weight:600;">GrabFood</div>
<div style="font-size:12px;color:var(--admin-text-tertiary);">Đồng bộ đơn hàng delivery</div>
</div>
</div>
<button class="admin-btn-secondary" style="font-size:12px;padding:4px 12px;">Kết nối</button>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;padding:14px;background:var(--admin-bg-interactive);border-radius:12px;">
<div style="display:flex;align-items:center;gap:12px;">
<i data-lucide="truck" style="color:var(--admin-text-tertiary);"></i>
<div>
<div style="font-weight:600;">ShopeeFood</div>
<div style="font-size:12px;color:var(--admin-text-tertiary);">Đồng bộ đơn hàng delivery</div>
</div>
</div>
<button class="admin-btn-secondary" style="font-size:12px;padding:4px 12px;">Kết nối</button>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;padding:14px;background:var(--admin-bg-interactive);border-radius:12px;">
<div style="display:flex;align-items:center;gap:12px;">
<i data-lucide="file-text" style="color:var(--admin-text-tertiary);"></i>
<div>
<div style="font-weight:600;">Kế toán (Misa)</div>
<div style="font-size:12px;color:var(--admin-text-tertiary);">Xuất hóa đơn điện tử</div>
</div>
</div>
<button class="admin-btn-secondary" style="font-size:12px;padding:4px 12px;">Kết nối</button>
</div>
</div>
</div>
}
}
</div>
@code {
[Inject] private MerchantApiService MerchantApi { get; set; } = default!;
[Parameter] public string Id { get; set; } = "";
private PosDataService.ShopInfo? _shop;
private string _tab = "general";
private bool _isSaving = false;
private string? _successMessage;
private string? _errorMessage;
// EN: Editable fields pre-filled from shop data
// VI: Các trường có thể sửa, điền sẵn từ dữ liệu shop
private string _editName = "";
private string _editDescription = "";
private string _editPhone = "";
private string _editEmail = "";
protected override async Task OnInitializedAsync()
{
IsLoading = true;
try
{
if (Guid.TryParse(Id, out var shopId))
{
_shop = await DataService.GetShopByIdAsync(shopId);
if (_shop != null)
{
_editName = _shop.Name;
_editDescription = _shop.Description ?? "";
_editPhone = _shop.Phone ?? "";
_editEmail = _shop.Email ?? "";
}
}
}
catch
{
_shop = null;
}
finally
{
IsLoading = false;
}
}
private async Task SaveSettings()
{
if (_isSaving || _shop == null) return;
_isSaving = true;
_successMessage = null;
_errorMessage = null;
StateHasChanged();
var dto = new ShopUpdateDto
{
Name = _editName,
Description = _editDescription,
Phone = _editPhone,
Email = _editEmail
};
var (success, error) = await MerchantApi.UpdateShopAsync(_shop.Id, dto);
if (success)
{
_successMessage = "Đã lưu thành công!";
// EN: Reload shop data to reflect changes
// VI: Tải lại dữ liệu shop để phản ánh thay đổi
_shop = await DataService.GetShopByIdAsync(_shop.Id);
}
else
{
_errorMessage = error ?? "Không thể lưu thay đổi";
}
_isSaving = false;
}
private static string GetStatusLabel(string? status) => status?.ToLowerInvariant() switch
{
"published" or "active" => "Đang mở",
"draft" or "setup" => "Thiết lập",
"inactive" or "paused" => "Tạm dừng",
"closed" => "Đã đóng",
_ => status ?? "—"
};
private record PaymentMethod(string Name, string Icon, string Color, string Desc, bool Enabled);
private readonly PaymentMethod[] _paymentMethods = new[]
{
new PaymentMethod("Tiền mặt", "banknote", "#22C55E", "Thanh toán trực tiếp", true),
new PaymentMethod("Thẻ ngân hàng", "credit-card", "#3B82F6", "Visa, Mastercard, JCB", true),
new PaymentMethod("QR Code", "qr-code", "#8B5CF6", "VNPay, MoMo, ZaloPay", true),
new PaymentMethod("Chuyển khoản", "send", "#F59E0B", "Chuyển khoản ngân hàng", false),
new PaymentMethod("Thẻ quà tặng", "gift", "#EC4899", "Gift card & voucher", false),
};
}