chore(web-client): delete 9 orphaned pages replaced by ShopPage sections
This commit is contained in:
@@ -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"
|
||||
};
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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)"
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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") : "—";
|
||||
}
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user