refactor(web-client): audit shop sidebar — fix bugs, DRY code, enhance UX
P1 Bug Fixes: - Fix Finance stat spacing (0₫ → 0 ₫) - Replace 3 empty catch blocks with error state UI + logging - Make POS button functional (navigate to /pos) - Add @implements IDisposable to AdminLayout P2 Code Quality: - Create ShopVerticalHelper.cs (DRY icon/label/normalize) - Remove dead overview case from ShopPage.razor - Delete 15 orphaned admin pages (moved to shop-scope) - ShopSidebarConfig delegates to ShopVerticalHelper P3 UX Enhancement: - Add CTA buttons to all empty states - Real KPIs in ShopOverview (orders, products, revenue) - User role from auth token (Admin/Khách) - Responsive CSS for stat cards, tables, mobile sidebar
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
Shop Level: Detected via URL /admin/shop/{shopId}/**, shows vertical-specific menu
|
||||
*@
|
||||
@inherits LayoutComponentBase
|
||||
@implements IDisposable
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IJSRuntime JS
|
||||
@inject WebClientTpos.Client.Services.AuthStateService AuthState
|
||||
@@ -117,7 +118,7 @@
|
||||
<div class="admin-user-avatar">@_userInitials</div>
|
||||
<div class="admin-user-info">
|
||||
<span class="admin-user-name">@_userName</span>
|
||||
<span class="admin-user-role">Owner</span>
|
||||
<span class="admin-user-role">@_userRole</span>
|
||||
</div>
|
||||
<button @onclick="Logout" title="Đăng xuất">
|
||||
<i data-lucide="log-out"></i>
|
||||
@@ -222,6 +223,7 @@
|
||||
private string _userInitials => _userName.Length >= 2
|
||||
? _userName[..2].ToUpper()
|
||||
: _userName.ToUpper();
|
||||
private string _userRole => AuthState.IsAuthenticated ? "Admin" : "Khách";
|
||||
|
||||
private async Task Logout()
|
||||
{
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
@page "/admin/customers/feedback"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
|
||||
<PageTitle>Phản hồi — GoodGo Admin</PageTitle>
|
||||
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Phản hồi khách hàng</h1>
|
||||
<p class="admin-topbar__subtitle">Thu thập đánh giá và phản hồi</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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(245,158,11,0.1);"><i data-lucide="star" style="color:#F59E0B;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">—</span><span class="admin-stat-card__label">Đánh giá TB</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(59,130,246,0.1);"><i data-lucide="message-square" style="color:#3B82F6;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">0</span><span class="admin-stat-card__label">Tổng phản hồi</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="thumbs-up" style="color:#22C55E;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">0</span><span class="admin-stat-card__label">Tích cực</span></div></div>
|
||||
</div>
|
||||
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(245,158,11,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;"><i data-lucide="message-circle" style="width:36px;height:36px;color:#F59E0B;"></i></div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chưa có phản hồi</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Phản hồi sẽ hiển thị khi khách hàng đánh giá dịch vụ</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,73 +0,0 @@
|
||||
@page "/admin/customers/loyalty"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Chương trình thành viên — GoodGo Admin</PageTitle>
|
||||
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Chương trình thành viên</h1>
|
||||
<p class="admin-topbar__subtitle">Quản lý cấp bậc & ưu đãi thành viên</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
@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 (!_levels.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="crown" style="width:36px;height:36px;color:#FF5C00;"></i></div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chưa cấu hình cấp bậc</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Thiết lập cấp bậc thành viên để xây dựng chương trình loyalty</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;">
|
||||
@foreach (var level in _levels)
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__body" style="text-align:center;padding:24px;">
|
||||
<div style="width:56px;height:56px;border-radius:16px;background:@GetLevelBg(level.Level);display:flex;align-items:center;justify-content:center;margin:0 auto 12px;">
|
||||
<i data-lucide="@GetLevelIcon(level.Level)" style="color:@GetLevelColor(level.Level);width:28px;height:28px;"></i>
|
||||
</div>
|
||||
<h3 style="font-size:18px;font-weight:700;margin:0 0 4px;">@level.Name</h3>
|
||||
<p style="font-size:13px;color:var(--admin-text-tertiary);margin:0 0 16px;">Level @level.Level</p>
|
||||
<div style="display:flex;justify-content:center;gap:24px;">
|
||||
<div><span style="font-weight:700;font-size:16px;color:var(--admin-orange-primary);">@level.MinExp.ToString("N0")</span><br/><span style="font-size:11px;color:var(--admin-text-tertiary);">Min EXP</span></div>
|
||||
<div><span style="font-weight:700;font-size:16px;">@level.MemberCount</span><br/><span style="font-size:11px;color:var(--admin-text-tertiary);">Thành viên</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Tổng quan</h3></div>
|
||||
<div class="admin-panel__body" style="display:flex;gap:24px;">
|
||||
<div><span style="font-weight:700;font-size:24px;">@_levels.Sum(l => l.MemberCount)</span><br/><span style="font-size:13px;color:var(--admin-text-tertiary);">Tổng thành viên</span></div>
|
||||
<div><span style="font-weight:700;font-size:24px;">@_levels.Count</span><br/><span style="font-size:13px;color:var(--admin-text-tertiary);">Cấp bậc</span></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<PosDataService.LevelDefinitionInfo> _levels = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
try { _levels = await DataService.GetMembershipLevelsAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private static string GetLevelColor(int l) => l switch { 1 => "#94A3B8", 2 => "#22C55E", 3 => "#3B82F6", 4 => "#F59E0B", _ => "#FF5C00" };
|
||||
private static string GetLevelBg(int l) => l switch { 1 => "rgba(148,163,184,0.1)", 2 => "rgba(34,197,94,0.1)", 3 => "rgba(59,130,246,0.1)", 4 => "rgba(245,158,11,0.1)", _ => "rgba(255,92,0,0.1)" };
|
||||
private static string GetLevelIcon(int l) => l switch { 1 => "user", 2 => "star", 3 => "award", 4 => "gem", _ => "crown" };
|
||||
}
|
||||
@@ -118,7 +118,7 @@
|
||||
<div class="admin-store-card__top">
|
||||
<div class="admin-store-card__info">
|
||||
<div class="admin-store-card__avatar" style="background-color:rgba(255,92,0,0.125);">
|
||||
<i data-lucide="@GetShopIcon(shop.Category)" style="color:var(--admin-orange-primary);"></i>
|
||||
<i data-lucide="@ShopVerticalHelper.GetIcon(shop.Category)" style="color:var(--admin-orange-primary);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="admin-store-card__name">@shop.Name</div>
|
||||
@@ -208,9 +208,10 @@
|
||||
{
|
||||
_shops = await DataService.GetShopsAsync();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
_shops = new();
|
||||
Console.Error.WriteLine($"[Dashboard] Error loading shops: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -218,13 +219,4 @@
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetShopIcon(string? category) => category?.ToLowerInvariant() switch
|
||||
{
|
||||
"cafe" or "café" or "coffee" => "coffee",
|
||||
"restaurant" or "nhà hàng" => "utensils",
|
||||
"karaoke" => "mic",
|
||||
"spa" => "sparkles",
|
||||
"retail" => "shopping-bag",
|
||||
_ => "store"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
@page "/admin/finance/expenses"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Chi phí — GoodGo Admin</PageTitle>
|
||||
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Quản lý chi phí</h1>
|
||||
<p class="admin-topbar__subtitle">@_txns.Count giao dịch</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
@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 (!_txns.Any())
|
||||
{
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(239,68,68,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;"><i data-lucide="receipt" style="width:36px;height:36px;color:#EF4444;"></i></div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chưa có giao dịch chi phí</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Giao dịch sẽ hiển thị khi có hoạt động ví</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Giao dịch gần đây</h3></div>
|
||||
<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);">Mô tả</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Số tiền</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngày</th>
|
||||
</tr></thead><tbody>
|
||||
@foreach (var t in _txns)
|
||||
{
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;font-size:14px;">@(t.Description ?? t.ItemName ?? "—")</td>
|
||||
<td style="padding:12px 16px;text-align:right;font-weight:600;color:@(t.Amount < 0 ? "#EF4444" : "#22C55E");">@(t.Amount < 0 ? "-" : "+")@Math.Abs(t.Amount).ToString("N0")₫</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@t.CreatedAt.ToString("dd/MM HH:mm")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<PosDataService.WalletTxnInfo> _txns = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
try { _txns = await DataService.GetWalletTransactionsAsync(100); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
@page "/admin/finance"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Tài chính — GoodGo Admin</PageTitle>
|
||||
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Tổng quan tài chính</h1>
|
||||
<p class="admin-topbar__subtitle">@_orders.Count đơn hàng • @_wallets.Count ví</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px;">
|
||||
<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">@FormatVND(_totalRevenue)</span>
|
||||
<span class="admin-stat-card__label">Tổng doanh thu</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="trending-down" style="color:#EF4444;"></i></div>
|
||||
<div class="admin-stat-card__content">
|
||||
<span class="admin-stat-card__value">@FormatVND(_totalExpense)</span>
|
||||
<span class="admin-stat-card__label">Tổng chi phí</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-stat-card">
|
||||
<div class="admin-stat-card__icon" style="background:rgba(59,130,246,0.1);"><i data-lucide="receipt" style="color:#3B82F6;"></i></div>
|
||||
<div class="admin-stat-card__content">
|
||||
<span class="admin-stat-card__value">@_orders.Count</span>
|
||||
<span class="admin-stat-card__label">Tổng đơn hàng</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="wallet" style="color:#FF5C00;"></i></div>
|
||||
<div class="admin-stat-card__content">
|
||||
<span class="admin-stat-card__value">@FormatVND(_wallets.Sum(w => w.Balance))</span>
|
||||
<span class="admin-stat-card__label">Số dư ví</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@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 (!_orders.Any())
|
||||
{
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(34,197,94,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;"><i data-lucide="bar-chart-3" style="width:36px;height:36px;color:#22C55E;"></i></div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chưa có dữ liệu tài chính</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Dữ liệu sẽ tự động cập nhật khi có đơn hàng</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Đơn hàng gần đây</h3></div>
|
||||
<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:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Số tiền</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>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngày</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@foreach (var o in _orders.Take(20))
|
||||
{
|
||||
<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);">@o.Id.ToString()[..8]</td>
|
||||
<td style="padding:12px 16px;text-align:right;font-weight:600;">@FormatVND(o.TotalAmount)</td>
|
||||
<td style="padding:12px 16px;text-align:center;"><span class="admin-status-badge admin-status-badge--online" style="font-size:11px;padding:2px 10px;"><span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>@(o.Status ?? "—")</span></td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@o.CreatedAt.ToString("dd/MM HH:mm")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<PosDataService.OrderInfo> _orders = new();
|
||||
private List<PosDataService.WalletInfo> _wallets = new();
|
||||
private decimal _totalRevenue, _totalExpense;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
try
|
||||
{
|
||||
_orders = await DataService.GetOrdersAsync();
|
||||
_wallets = await DataService.GetWalletsAsync();
|
||||
_totalRevenue = _wallets.Sum(w => w.TotalIncome);
|
||||
_totalExpense = _wallets.Sum(w => w.TotalExpense);
|
||||
}
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private static string FormatVND(decimal val) => val.ToString("N0") + "₫";
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
@page "/admin/finance/revenue"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Phân tích doanh thu — GoodGo Admin</PageTitle>
|
||||
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Phân tích doanh thu</h1>
|
||||
<p class="admin-topbar__subtitle">@_orders.Count đơn hàng • @FormatVND(_orders.Sum(o => o.TotalAmount))</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;flex-direction:column;gap:20px;">
|
||||
@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 (!_orders.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="bar-chart" 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, #FFF);">Chưa có đơn hàng</h2>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:16px;">
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(34,197,94,0.1);"><i data-lucide="dollar-sign" style="color:#22C55E;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@FormatVND(_orders.Sum(o => o.TotalAmount))</span><span class="admin-stat-card__label">Tổng doanh thu</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(59,130,246,0.1);"><i data-lucide="shopping-bag" style="color:#3B82F6;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_orders.Count</span><span class="admin-stat-card__label">Số đơn hàng</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="calculator" style="color:#FF5C00;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@FormatVND(_orders.Any() ? _orders.Average(o => o.TotalAmount) : 0)</span><span class="admin-stat-card__label">Trung bình/đơn</span></div></div>
|
||||
</div>
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Chi tiết đơn hàng</h3></div>
|
||||
<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:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Số tiền</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>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngày</th>
|
||||
</tr></thead><tbody>
|
||||
@foreach (var o in _orders.Take(50))
|
||||
{
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;font-size:12px;font-family:monospace;">@o.Id.ToString()[..8]</td>
|
||||
<td style="padding:12px 16px;text-align:right;font-weight:600;">@FormatVND(o.TotalAmount)</td>
|
||||
<td style="padding:12px 16px;text-align:center;"><span class="admin-status-badge admin-status-badge--online" style="font-size:11px;padding:2px 10px;">@(o.Status ?? "—")</span></td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@o.CreatedAt.ToString("dd/MM HH:mm")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<PosDataService.OrderInfo> _orders = new();
|
||||
private List<PosDataService.ShopInfo> _shops = new();
|
||||
private Guid? _selectedShopId;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
try { _shops = await DataService.GetShopsAsync(); _orders = await DataService.GetOrdersAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private async Task OnShopFilterChanged(ChangeEventArgs e)
|
||||
{
|
||||
_selectedShopId = Guid.TryParse(e.Value?.ToString(), out var id) ? id : null;
|
||||
IsLoading = true;
|
||||
try { _orders = await DataService.GetOrdersAsync(_selectedShopId); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private static string FormatVND(decimal val) => val.ToString("N0") + "₫";
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
@page "/admin/finance/tax"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Cấu hình thuế — GoodGo Admin</PageTitle>
|
||||
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Cấu hình thuế</h1>
|
||||
<p class="admin-topbar__subtitle">Quản lý thuế theo cửa hàng</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
@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
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Cửa hàng</h3></div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
|
||||
@if (!_shops.Any())
|
||||
{
|
||||
<p style="color:var(--admin-text-tertiary);font-size:14px;text-align:center;padding:20px;">Chưa có cửa hàng</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var shop in _shops)
|
||||
{
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-radius:8px;background:var(--admin-bg-interactive);">
|
||||
<div>
|
||||
<span style="font-weight:600;font-size:14px;">@shop.Name</span>
|
||||
<span style="font-size:12px;color:var(--admin-text-tertiary);margin-left:8px;">@(shop.Category ?? "")</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<span style="font-size:14px;color:var(--admin-text-tertiary);">VAT: 10%</span>
|
||||
<i data-lucide="chevron-right" style="width:16px;height:16px;color:var(--admin-text-tertiary);"></i>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Loại thuế</h3></div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
|
||||
@foreach (var tax in new[] { ("VAT", "10%", "Thuế giá trị gia tăng"), ("TNCN", "Tự động", "Thuế thu nhập cá nhân"), ("Phí dịch vụ", "5%", "Service charge") })
|
||||
{
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-radius:8px;background:var(--admin-bg-interactive);">
|
||||
<div>
|
||||
<span style="font-weight:600;font-size:14px;">@tax.Item1</span>
|
||||
<span style="font-size:12px;color:var(--admin-text-tertiary);margin-left:8px;">@tax.Item3</span>
|
||||
</div>
|
||||
<span style="font-weight:600;color:var(--admin-orange-primary);">@tax.Item2</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<PosDataService.ShopInfo> _shops = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
try { _shops = await DataService.GetShopsAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
@page "/admin/inventory/purchase-orders"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Đơn nhập hàng — GoodGo Admin</PageTitle>
|
||||
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Đơn nhập hàng</h1>
|
||||
<p class="admin-topbar__subtitle">@_txns.Count giao dịch nhập kho</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;flex-direction:column;gap:20px;">
|
||||
@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 (!_txns.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="truck" 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, #FFF);">Chưa có đơn nhập hàng</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Tạo đơn nhập hàng để quản lý tồn kho</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);">Loại</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Số lượng</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ghi chú</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngày</th>
|
||||
</tr></thead><tbody>
|
||||
@foreach (var t in _txns)
|
||||
{
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;font-weight:600;">@(t.TransactionType ?? "—")</td>
|
||||
<td style="padding:12px 16px;text-align:right;font-weight:600;color:@(t.QuantityChange > 0 ? "#22C55E" : "#EF4444");">@(t.QuantityChange > 0 ? "+" : "")@t.QuantityChange</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@(t.Reason ?? "—")</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@t.CreatedAt.ToString("dd/MM HH:mm")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<PosDataService.InventoryTxnInfo> _txns = new();
|
||||
private List<PosDataService.ShopInfo> _shops = new();
|
||||
private Guid? _selectedShopId;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
try { _shops = await DataService.GetShopsAsync(); _txns = await DataService.GetInventoryTransactionsAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private async Task OnShopFilterChanged(ChangeEventArgs e)
|
||||
{
|
||||
_selectedShopId = Guid.TryParse(e.Value?.ToString(), out var id) ? id : null;
|
||||
IsLoading = true;
|
||||
try { _txns = await DataService.GetInventoryTransactionsAsync(_selectedShopId); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
@page "/admin/inventory/transfers"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Chuyển kho — GoodGo Admin</PageTitle>
|
||||
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Chuyển kho</h1>
|
||||
<p class="admin-topbar__subtitle">Quản lý chuyển hàng giữa các cửa hàng</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
@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
|
||||
{
|
||||
<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="arrow-right-left" 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, #FFF);">Chuyển kho</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0 0 20px;">Tạo yêu cầu chuyển hàng giữa các cửa hàng</p>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;max-width:400px;margin:0 auto;">
|
||||
@foreach (var shop in _shops)
|
||||
{
|
||||
<div style="padding:16px;border-radius:12px;background:var(--admin-bg-interactive);text-align:center;">
|
||||
<div style="font-weight:600;font-size:14px;margin-bottom:4px;">@shop.Name</div>
|
||||
<div style="font-size:12px;color:var(--admin-text-tertiary);">@(shop.Category ?? "—")</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<PosDataService.ShopInfo> _shops = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
try { _shops = await DataService.GetShopsAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
@page "/admin/inventory/suppliers"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Nhà cung cấp — GoodGo Admin</PageTitle>
|
||||
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Nhà cung cấp</h1>
|
||||
<p class="admin-topbar__subtitle">Quản lý nhà cung cấp</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<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="building-2" 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, #FFF);">Quản lý nhà cung cấp</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0 0 16px;">Tính năng đang phát triển — sẽ sớm ra mắt</p>
|
||||
<span class="admin-status-badge admin-status-badge--warning" style="font-size:12px;padding:4px 12px;"><span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>Coming Soon</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
@page "/admin/products/modifiers"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
|
||||
<PageTitle>Nhóm tùy chọn — GoodGo Admin</PageTitle>
|
||||
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Nhóm tùy chỉnh</h1>
|
||||
<p class="admin-topbar__subtitle">Quản lý topping, size, đường/đá</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<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="sliders-horizontal" 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, #FFF);">Nhóm tùy chỉnh</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0 0 16px;">Tính năng đang phát triển — sẽ sớm ra mắt</p>
|
||||
<span class="admin-status-badge admin-status-badge--warning" style="font-size:12px;padding:4px 12px;"><span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>Coming Soon</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,71 +0,0 @@
|
||||
@page "/admin/products/pricing"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Chiến lược giá — GoodGo Admin</PageTitle>
|
||||
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Chiến lược giá & Khuyến mãi</h1>
|
||||
<p class="admin-topbar__subtitle">@_promos.Count chương trình</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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(34,197,94,0.1);"><i data-lucide="tag" style="color:#22C55E;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_promos.Count(p => p.IsActive)</span><span class="admin-stat-card__label">Đang hoạt động</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(59,130,246,0.1);"><i data-lucide="ticket" style="color:#3B82F6;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_promos.Sum(p => p.VoucherCount)</span><span class="admin-stat-card__label">Tổng voucher</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="check-circle" style="color:#FF5C00;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_promos.Sum(p => p.RedemptionCount)</span><span class="admin-stat-card__label">Đã sử dụng</span></div></div>
|
||||
</div>
|
||||
|
||||
@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 (!_promos.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="percent" style="width:36px;height:36px;color:#FF5C00;"></i></div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chưa có chương trình khuyến mãi</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Tạo chiến dịch mới để thu hút khách 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);">Tên</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Giảm giá</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Voucher</th>
|
||||
<th style="padding:12px 16px;text-align:right;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Đã dù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 p in _promos)
|
||||
{
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;"><div style="font-weight:600;font-size:14px;">@p.Name</div><div style="font-size:12px;color:var(--admin-text-tertiary);">@(p.Description ?? "")</div></td>
|
||||
<td style="padding:12px 16px;text-align:right;font-weight:600;color:var(--admin-orange-primary);">@(p.DiscountType == "Percentage" ? $"{p.DiscountValue}%" : $"{p.DiscountValue?.ToString("N0")}₫")</td>
|
||||
<td style="padding:12px 16px;text-align:right;">@p.VoucherCount</td>
|
||||
<td style="padding:12px 16px;text-align:right;">@p.RedemptionCount</td>
|
||||
<td style="padding:12px 16px;text-align:center;"><span class="admin-status-badge @(p.IsActive ? "admin-status-badge--online" : "admin-status-badge--offline")" style="font-size:11px;padding:2px 10px;"><span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>@(p.IsActive ? "Active" : "Inactive")</span></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody></table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<PosDataService.PromotionInfo> _promos = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
try { _promos = await DataService.GetPromotionsAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
}
|
||||
@@ -27,10 +27,10 @@
|
||||
@GetStatusLabel(_shop.Status)
|
||||
</div>
|
||||
}
|
||||
<button class="admin-btn-primary">
|
||||
<a href="/pos" class="admin-btn-primary" style="text-decoration:none;">
|
||||
<i data-lucide="monitor"></i>
|
||||
<span>Mở POS</span>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<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>
|
||||
<i data-lucide="@ShopVerticalHelper.GetIcon(_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>
|
||||
@@ -97,7 +97,7 @@
|
||||
<i data-lucide="trending-up" style="color:#22C55E;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">--</div>
|
||||
<div class="admin-kpi-card__value">@FormatVND(_orders.Sum(o => o.TotalAmount))</div>
|
||||
<div class="admin-kpi-card__label">Doanh thu tháng</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
@@ -106,7 +106,7 @@
|
||||
<i data-lucide="shopping-bag" style="color:#3B82F6;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">--</div>
|
||||
<div class="admin-kpi-card__value">@_orders.Count</div>
|
||||
<div class="admin-kpi-card__label">Đơn hàng tháng</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card">
|
||||
@@ -115,17 +115,17 @@
|
||||
<i data-lucide="banknote" style="color:#8B5CF6;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">--</div>
|
||||
<div class="admin-kpi-card__value">@FormatVND(_orders.Any() ? _orders.Average(o => o.TotalAmount) : 0)</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>
|
||||
<i data-lucide="package" style="color:#EC4899;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">--</div>
|
||||
<div class="admin-kpi-card__label">Đánh giá TB</div>
|
||||
<div class="admin-kpi-card__value">@_products.Count</div>
|
||||
<div class="admin-kpi-card__label">Sản phẩm</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -199,6 +199,8 @@
|
||||
[Parameter] public string ShopId { get; set; } = "";
|
||||
|
||||
private PosDataService.ShopInfo? _shop;
|
||||
private List<PosDataService.OrderInfo> _orders = new();
|
||||
private List<PosDataService.AdminProductInfo> _products = new();
|
||||
|
||||
// EN: Cascade layout reference to set shop context for sidebar switching.
|
||||
// VI: Cascade layout để set shop context cho sidebar chuyển đổi.
|
||||
@@ -216,12 +218,23 @@
|
||||
{
|
||||
Layout?.SetShopContext(ShopId, _shop.Name ?? "Cửa hàng", _shop.Category);
|
||||
}
|
||||
// EN: Load KPI data in parallel / VI: Tải dữ liệu KPI song song
|
||||
var ordersTask = DataService.GetOrdersAsync(id);
|
||||
var productsTask = DataService.GetAllProductsAsync(id);
|
||||
_orders = await ordersTask;
|
||||
_products = await productsTask;
|
||||
}
|
||||
}
|
||||
catch { _shop = null; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_shop = null;
|
||||
Console.Error.WriteLine($"[ShopOverview] Error loading shop {ShopId}: {ex}");
|
||||
}
|
||||
finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private static string FormatVND(decimal val) => val.ToString("N0") + " ₫";
|
||||
|
||||
private static string GetStatusBadgeClass(string? status) => status?.ToLowerInvariant() switch
|
||||
{
|
||||
"published" or "active" => "online",
|
||||
@@ -239,13 +252,4 @@
|
||||
_ => 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"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,7 +22,23 @@
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
|
||||
|
||||
@if (IsLoading)
|
||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<div class="admin-panel" style="border:1px solid rgba(239,68,68,0.3);">
|
||||
<div class="admin-panel__body" style="text-align:center;padding:40px 20px;">
|
||||
<div style="width:64px;height:64px;border-radius:20px;background:rgba(239,68,68,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 16px;">
|
||||
<i data-lucide="alert-circle" style="width:28px;height:28px;color:#EF4444;"></i>
|
||||
</div>
|
||||
<h3 style="font-size:16px;font-weight:700;margin:0 0 8px;color:#EF4444;">Lỗi tải dữ liệu</h3>
|
||||
<p style="font-size:13px;color:var(--admin-text-tertiary);margin:0 0 16px;max-width:400px;margin-left:auto;margin-right:auto;">@_errorMessage</p>
|
||||
<button class="admin-btn-primary" @onclick="@(() => LoadData())" style="display:inline-flex;align-items:center;gap:8px;">
|
||||
<i data-lucide="refresh-cw" style="width:14px;height:14px;"></i>
|
||||
Thử lại
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (IsLoading)
|
||||
{
|
||||
<div style="text-align:center;padding:48px 20px;">
|
||||
<div class="spinner-small" style="width:32px;height:32px;margin:0 auto 16px;"></div>
|
||||
@@ -33,22 +49,12 @@
|
||||
{
|
||||
@switch (_section)
|
||||
{
|
||||
// ═══ OVERVIEW ═══
|
||||
case "overview":
|
||||
<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(34,197,94,0.1);"><i data-lucide="package" style="color:#22C55E;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_products.Count</span><span class="admin-stat-card__label">Sản phẩm</span></div></div>
|
||||
<div class="admin-stat-card"><div class="admin-stat-card__icon" style="background:rgba(59,130,246,0.1);"><i data-lucide="warehouse" style="color:#3B82F6;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_inventory.Count</span><span class="admin-stat-card__label">Tồn kho</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="receipt" style="color:#FF5C00;"></i></div><div class="admin-stat-card__content"><span class="admin-stat-card__value">@_orders.Count</span><span class="admin-stat-card__label">Đơn hàng</span></div></div>
|
||||
<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">@_staff.Count</span><span class="admin-stat-card__label">Nhân viên</span></div></div>
|
||||
</div>
|
||||
break;
|
||||
|
||||
// ═══ MENU / PRODUCTS ═══
|
||||
case "menu":
|
||||
case "products":
|
||||
@if (!_products.Any())
|
||||
{
|
||||
@RenderEmpty("coffee", "#F59E0B", "Chưa có sản phẩm", "Thêm sản phẩm để bắt đầu bán hàng")
|
||||
@RenderEmpty("coffee", "#F59E0B", "Chưa có sản phẩm", "Thêm sản phẩm để bắt đầu bán hàng", "plus-circle", "Thêm sản phẩm")
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -72,7 +78,7 @@
|
||||
case "inventory":
|
||||
@if (!_inventory.Any())
|
||||
{
|
||||
@RenderEmpty("warehouse", "#3B82F6", "Chưa có tồn kho", "Tồn kho sẽ hiển thị khi có sản phẩm")
|
||||
@RenderEmpty("warehouse", "#3B82F6", "Chưa có tồn kho", "Tồn kho sẽ hiển thị khi có sản phẩm", "package", "Thêm sản phẩm trước")
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -111,7 +117,7 @@
|
||||
</div>
|
||||
@if (!_orders.Any())
|
||||
{
|
||||
@RenderEmpty("bar-chart-3", "#22C55E", "Chưa có dữ liệu tài chính", "Dữ liệu sẽ tự động cập nhật khi có đơn hàng")
|
||||
@RenderEmpty("bar-chart-3", "#22C55E", "Chưa có dữ liệu tài chính", "Dữ liệu sẽ tự động cập nhật khi có đơn hàng", "monitor", "Mở POS bán hàng")
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -143,7 +149,7 @@
|
||||
case "staff":
|
||||
@if (!_staff.Any())
|
||||
{
|
||||
@RenderEmpty("users", "#8B5CF6", "Chưa có nhân viên", "Thêm nhân viên để quản lý cửa hàng")
|
||||
@RenderEmpty("users", "#8B5CF6", "Chưa có nhân viên", "Thêm nhân viên để quản lý cửa hàng", "user-plus", "Thêm nhân viên")
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -178,7 +184,7 @@
|
||||
case "customers":
|
||||
@if (!_members.Any())
|
||||
{
|
||||
@RenderEmpty("heart", "#EF4444", "Chưa có khách hàng", "Khách hàng sẽ hiển thị khi có giao dịch")
|
||||
@RenderEmpty("heart", "#EF4444", "Chưa có khách hàng", "Khách hàng sẽ hiển thị khi có giao dịch", "monitor", "Mở POS bán hàng")
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -240,6 +246,7 @@
|
||||
private string _sectionTitle = "";
|
||||
private string _sectionIcon = "layout-dashboard";
|
||||
private string _sectionDescription = "";
|
||||
private string? _errorMessage;
|
||||
private Guid? _shopGuid;
|
||||
|
||||
// ═══ DATA ═══
|
||||
@@ -261,6 +268,7 @@
|
||||
private async Task LoadData()
|
||||
{
|
||||
IsLoading = true;
|
||||
_errorMessage = null;
|
||||
_section = Section?.ToLowerInvariant() ?? "";
|
||||
ConfigureSection();
|
||||
|
||||
@@ -281,12 +289,6 @@
|
||||
// EN: Load only data needed for current section / VI: Chỉ tải data cần cho section hiện tại
|
||||
switch (_section)
|
||||
{
|
||||
case "overview":
|
||||
_products = await DataService.GetAllProductsAsync(_shopGuid);
|
||||
_inventory = await DataService.GetInventoryAsync(_shopGuid);
|
||||
_orders = await DataService.GetOrdersAsync(_shopGuid);
|
||||
_staff = await DataService.GetStaffAsync();
|
||||
break;
|
||||
case "menu":
|
||||
case "products":
|
||||
_products = await DataService.GetAllProductsAsync(_shopGuid);
|
||||
@@ -305,7 +307,11 @@
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorMessage = $"Không thể tải dữ liệu: {ex.Message}";
|
||||
Console.Error.WriteLine($"[ShopPage] Error loading {_section}: {ex}");
|
||||
}
|
||||
finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
@@ -331,17 +337,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatVND(decimal val) => val.ToString("N0") + "₫";
|
||||
private static string FormatVND(decimal val) => val.ToString("N0") + " ₫";
|
||||
|
||||
// EN: Reusable empty state renderer / VI: Renderer trạng thái trống tái sử dụng
|
||||
private RenderFragment RenderEmpty(string icon, string color, string title, string desc) => __builder =>
|
||||
// EN: Reusable empty state renderer with optional CTA / VI: Renderer trạng thái trống tái sử dụng với CTA tùy chọn
|
||||
private RenderFragment RenderEmpty(string icon, string color, string title, string desc, string? ctaIcon = null, string? ctaLabel = null) => __builder =>
|
||||
{
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:@($"rgba({HexToRgb(color)},0.1)");display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">
|
||||
<i data-lucide="@icon" style="width:36px;height:36px;color:@color;"></i>
|
||||
</div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">@title</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">@desc</p>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0 0 20px;">@desc</p>
|
||||
@if (ctaIcon != null && ctaLabel != null)
|
||||
{
|
||||
<a href="/admin/shop/@ShopId/menu" class="admin-btn-primary" style="display:inline-flex;align-items:center;gap:8px;text-decoration:none;">
|
||||
<i data-lucide="@ctaIcon" style="width:16px;height:16px;"></i>
|
||||
@ctaLabel
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
};
|
||||
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
@page "/admin/staff/attendance"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Chấm công — GoodGo Admin</PageTitle>
|
||||
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Chấm công</h1>
|
||||
<p class="admin-topbar__subtitle">Theo dõi giờ làm việc nhân viên</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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(34,197,94,0.1);"><i data-lucide="user-check" style="color:#22C55E;"></i></div>
|
||||
<div class="admin-stat-card__content"><span class="admin-stat-card__value">@_staff.Count(s => s.Status == "Active")</span><span class="admin-stat-card__label">Đang hoạt động</span></div>
|
||||
</div>
|
||||
<div class="admin-stat-card">
|
||||
<div class="admin-stat-card__icon" style="background:rgba(59,130,246,0.1);"><i data-lucide="users" style="color:#3B82F6;"></i></div>
|
||||
<div class="admin-stat-card__content"><span class="admin-stat-card__value">@_staff.Count</span><span class="admin-stat-card__label">Tổng nhân viên</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="clock" style="color:#F59E0B;"></i></div>
|
||||
<div class="admin-stat-card__content"><span class="admin-stat-card__value">@_schedules.Count</span><span class="admin-stat-card__label">Tổng ca hôm nay</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@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
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Nhân viên hôm nay</h3></div>
|
||||
<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);">Nhân viên</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Vai trò</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>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Cửa hàng</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@foreach (var s in _staff)
|
||||
{
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;font-weight:600;">@(s.EmployeeCode ?? s.Id.ToString()[..6])</td>
|
||||
<td style="padding:12px 16px;">@(s.Role ?? "—")</td>
|
||||
<td style="padding:12px 16px;text-align:center;">
|
||||
<span class="admin-status-badge @(s.Status == "Active" ? "admin-status-badge--online" : "admin-status-badge--offline")" style="font-size:11px;padding:2px 10px;">
|
||||
<span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>@(s.Status ?? "—")
|
||||
</span>
|
||||
</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@(s.ShopName ?? "—")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<PosDataService.StaffInfo> _staff = new();
|
||||
private List<PosDataService.ScheduleInfo> _schedules = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
try
|
||||
{
|
||||
_staff = await DataService.GetStaffAsync();
|
||||
_schedules = await DataService.GetStaffSchedulesAsync();
|
||||
}
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
@page "/admin/staff/payroll"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Bảng lương — GoodGo Admin</PageTitle>
|
||||
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Bảng lương</h1>
|
||||
<p class="admin-topbar__subtitle">@_staff.Count nhân viên • @DateTime.Now.ToString("MM/yyyy")</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
@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 (!_staff.Any())
|
||||
{
|
||||
<div style="text-align:center;padding:60px 20px;">
|
||||
<div style="width:80px;height:80px;border-radius:24px;background:rgba(34,197,94,0.1);display:flex;align-items:center;justify-content:center;margin:0 auto 20px;">
|
||||
<i data-lucide="banknote" style="width:36px;height:36px;color:#22C55E;"></i>
|
||||
</div>
|
||||
<h2 style="font-size:20px;font-weight:700;margin:0 0 8px;color:var(--pos-text-primary, #FFF);">Chưa có dữ liệu lương</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Thêm nhân viên để quản lý bảng lương</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Danh sách nhân viên</h3></div>
|
||||
<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);">Mã NV</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Vai trò</th>
|
||||
<th style="padding:12px 16px;text-align:left;font-size:12px;text-transform:uppercase;color:var(--admin-text-tertiary);">Ngày vào</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 s in _staff)
|
||||
{
|
||||
<tr style="border-top:1px solid var(--admin-border-subtle);">
|
||||
<td style="padding:12px 16px;font-weight:600;">@(s.EmployeeCode ?? "—")</td>
|
||||
<td style="padding:12px 16px;">@(s.Role ?? "—")</td>
|
||||
<td style="padding:12px 16px;font-size:13px;color:var(--admin-text-tertiary);">@(s.JoinedAt?.ToString("dd/MM/yyyy") ?? "—")</td>
|
||||
<td style="padding:12px 16px;text-align:center;">
|
||||
<span class="admin-status-badge @(s.Status == "Active" ? "admin-status-badge--online" : "admin-status-badge--offline")" style="font-size:11px;padding:2px 10px;">
|
||||
<span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>@(s.Status ?? "—")
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<PosDataService.StaffInfo> _staff = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
try { _staff = await DataService.GetStaffAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
@page "/admin/staff/schedule"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject PosDataService DataService
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<PageTitle>Lịch làm việc — GoodGo Admin</PageTitle>
|
||||
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Lịch làm việc</h1>
|
||||
<p class="admin-topbar__subtitle">@_schedules.Count ca làm việc</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 shop in _shops) { <option value="@shop.Id">@shop.Name</option> }
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
@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 lịch...</p></div>
|
||||
}
|
||||
else if (!_schedules.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="calendar" 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, #FFF);">Chưa có lịch làm việc</h2>
|
||||
<p style="font-size:14px;color:var(--admin-text-tertiary);margin:0;">Thêm lịch cho nhân viên để quản lý ca làm việc</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var dayGroup in _schedules.GroupBy(s => s.DayOfWeek).OrderBy(g => g.Key))
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">@DayName(dayGroup.Key)</h3></div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:8px;">
|
||||
@foreach (var s in dayGroup)
|
||||
{
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-radius:8px;background:var(--admin-bg-interactive);">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<span style="font-weight:600;font-size:14px;">@(s.EmployeeCode ?? s.StaffId.ToString()[..6])</span>
|
||||
<span style="font-size:12px;color:var(--admin-text-tertiary);">@(s.Role ?? "")</span>
|
||||
</div>
|
||||
<span style="font-size:14px;font-weight:600;color:var(--admin-orange-primary);">@s.StartTime — @s.EndTime</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<PosDataService.ScheduleInfo> _schedules = new();
|
||||
private List<PosDataService.ShopInfo> _shops = new();
|
||||
private Guid? _selectedShopId;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
try { _shops = await DataService.GetShopsAsync(); _schedules = await DataService.GetStaffSchedulesAsync(); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private async Task OnShopFilterChanged(ChangeEventArgs e)
|
||||
{
|
||||
_selectedShopId = Guid.TryParse(e.Value?.ToString(), out var id) ? id : null;
|
||||
IsLoading = true;
|
||||
try { _schedules = await DataService.GetStaffSchedulesAsync(_selectedShopId); }
|
||||
catch { } finally { IsLoading = false; }
|
||||
}
|
||||
|
||||
private static string DayName(int day) => day switch {
|
||||
0 => "Chủ nhật", 1 => "Thứ 2", 2 => "Thứ 3", 3 => "Thứ 4", 4 => "Thứ 5", 5 => "Thứ 6", 6 => "Thứ 7", _ => $"Ngày {day}"
|
||||
};
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
@page "/admin/system/notifications"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
|
||||
<PageTitle>Thông báo — GoodGo Admin</PageTitle>
|
||||
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Trung tâm thông báo</h1>
|
||||
<p class="admin-topbar__subtitle">Quản lý thông báo đẩy</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:20px;">
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header"><h3 class="admin-panel__title">Mẫu thông báo</h3></div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
|
||||
@foreach (var tmpl in _templates)
|
||||
{
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-radius:8px;background:var(--admin-bg-interactive);">
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:14px;">@tmpl.name</div>
|
||||
<div style="font-size:12px;color:var(--admin-text-tertiary);margin-top:2px;">@tmpl.desc</div>
|
||||
</div>
|
||||
<span class="admin-status-badge @(tmpl.active ? "admin-status-badge--online" : "admin-status-badge--offline")" style="font-size:11px;padding:2px 10px;"><span class="admin-status-badge__dot" style="width:5px;height:5px;"></span>@(tmpl.active ? "Bật" : "Tắt")</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private readonly (string name, string desc, bool active)[] _templates = new[]
|
||||
{
|
||||
("Đơn hàng mới", "Thông báo khi có đơn hàng mới", true),
|
||||
("Hết hàng", "Cảnh báo khi sản phẩm hết hàng", true),
|
||||
("Khuyến mãi", "Thông báo chương trình khuyến mãi", false),
|
||||
("Đặt lịch", "Nhắc nhở lịch hẹn", true),
|
||||
("Chấm công", "Thông báo chấm công", false),
|
||||
("Hệ thống", "Thông báo bảo trì hệ thống", true),
|
||||
};
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public static class ShopSidebarConfig
|
||||
/// </summary>
|
||||
public static List<MenuItem> GetMenuItems(string? category)
|
||||
{
|
||||
var vertical = NormalizeVertical(category);
|
||||
var vertical = ShopVerticalHelper.NormalizeVertical(category);
|
||||
|
||||
return vertical switch
|
||||
{
|
||||
@@ -91,41 +91,14 @@ public static class ShopSidebarConfig
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Normalize category string to internal vertical key.
|
||||
/// VI: Chuẩn hóa chuỗi category thành key nội bộ.
|
||||
/// EN: Get vertical display name (delegates to ShopVerticalHelper).
|
||||
/// VI: Lấy tên hiển thị của ngành hàng (ủy quyền cho ShopVerticalHelper).
|
||||
/// </summary>
|
||||
private static string NormalizeVertical(string? category) => (category ?? "").ToLowerInvariant() switch
|
||||
{
|
||||
"cafe" or "café" or "coffee" or "foodbeverage" => "cafe",
|
||||
"restaurant" or "nhà hàng" or "bar" => "restaurant",
|
||||
"karaoke" or "entertainment" => "karaoke",
|
||||
"spa" or "beauty" or "salon" => "spa",
|
||||
_ => "default"
|
||||
};
|
||||
public static string GetVerticalLabel(string? category) => ShopVerticalHelper.GetLabel(category);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get vertical display name.
|
||||
/// VI: Lấy tên hiển thị của ngành hàng.
|
||||
/// EN: Get vertical icon (delegates to ShopVerticalHelper).
|
||||
/// VI: Lấy icon ngành hàng (ủy quyền cho ShopVerticalHelper).
|
||||
/// </summary>
|
||||
public static string GetVerticalLabel(string? category) => NormalizeVertical(category) switch
|
||||
{
|
||||
"cafe" => "Café",
|
||||
"restaurant" => "Nhà hàng / Bar",
|
||||
"karaoke" => "Karaoke",
|
||||
"spa" => "Spa / Thẩm mỹ",
|
||||
_ => "Cửa hàng"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get vertical icon.
|
||||
/// VI: Lấy icon ngành hàng.
|
||||
/// </summary>
|
||||
public static string GetVerticalIcon(string? category) => NormalizeVertical(category) switch
|
||||
{
|
||||
"cafe" => "coffee",
|
||||
"restaurant" => "utensils",
|
||||
"karaoke" => "mic",
|
||||
"spa" => "sparkles",
|
||||
_ => "store"
|
||||
};
|
||||
public static string GetVerticalIcon(string? category) => ShopVerticalHelper.GetIcon(category);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// EN: Centralized helper for shop vertical/category utilities.
|
||||
// VI: Helper tập trung cho các tiện ích ngành hàng cửa hàng.
|
||||
//
|
||||
// Consolidates duplicate GetShopIcon, GetVerticalLabel, NormalizeVertical
|
||||
// logic that was scattered across Dashboard.razor, ShopOverview.razor,
|
||||
// and ShopSidebarConfig.cs.
|
||||
|
||||
namespace WebClientTpos.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Centralized utilities for shop vertical display — icon, label, normalize.
|
||||
/// VI: Tiện ích tập trung cho hiển thị ngành hàng — icon, label, chuẩn hóa.
|
||||
/// </summary>
|
||||
public static class ShopVerticalHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// EN: Normalize category string to internal vertical key.
|
||||
/// VI: Chuẩn hóa chuỗi category thành key nội bộ.
|
||||
/// </summary>
|
||||
public static string NormalizeVertical(string? category) => (category ?? "").ToLowerInvariant() switch
|
||||
{
|
||||
"cafe" or "café" or "coffee" or "foodbeverage" => "cafe",
|
||||
"restaurant" or "nhà hàng" or "bar" => "restaurant",
|
||||
"karaoke" or "entertainment" => "karaoke",
|
||||
"spa" or "beauty" or "salon" => "spa",
|
||||
"retail" => "retail",
|
||||
_ => "default"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get Lucide icon name for a shop vertical/category.
|
||||
/// VI: Lấy tên icon Lucide cho ngành hàng.
|
||||
/// </summary>
|
||||
public static string GetIcon(string? category) => NormalizeVertical(category) switch
|
||||
{
|
||||
"cafe" => "coffee",
|
||||
"restaurant" => "utensils",
|
||||
"karaoke" => "mic",
|
||||
"spa" => "sparkles",
|
||||
"retail" => "shopping-bag",
|
||||
_ => "store"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get display label for a vertical.
|
||||
/// VI: Lấy tên hiển thị cho ngành hàng.
|
||||
/// </summary>
|
||||
public static string GetLabel(string? category) => NormalizeVertical(category) switch
|
||||
{
|
||||
"cafe" => "Café",
|
||||
"restaurant" => "Nhà hàng / Bar",
|
||||
"karaoke" => "Karaoke",
|
||||
"spa" => "Spa / Thẩm mỹ",
|
||||
"retail" => "Bán lẻ",
|
||||
_ => "Cửa hàng"
|
||||
};
|
||||
}
|
||||
@@ -1003,6 +1003,20 @@ a.admin-store-card:hover {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* EN: Mobile sidebar overlay / VI: Overlay sidebar trên mobile */
|
||||
.admin-sidebar-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 199;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
/* EN: Mobile hamburger button / VI: Nút hamburger trên mobile */
|
||||
.admin-topbar__menu-btn {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.admin-topbar {
|
||||
padding: 12px 20px;
|
||||
}
|
||||
@@ -1018,6 +1032,18 @@ a.admin-store-card:hover {
|
||||
.admin-kpi-card {
|
||||
min-width: calc(50% - 10px);
|
||||
}
|
||||
|
||||
/* EN: Stat card grid responsive / VI: Lưới stat card responsive */
|
||||
.admin-stat-card {
|
||||
min-width: calc(50% - 8px);
|
||||
}
|
||||
|
||||
/* EN: Tables scroll horizontally / VI: Bảng scroll ngang */
|
||||
.admin-table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@@ -1025,6 +1051,10 @@ a.admin-store-card:hover {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.admin-stat-card {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
padding: 16px;
|
||||
}
|
||||
@@ -1035,6 +1065,10 @@ a.admin-store-card:hover {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.admin-topbar__title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.admin-search {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1046,6 +1080,11 @@ a.admin-store-card:hover {
|
||||
.admin-store-stat {
|
||||
min-width: calc(50% - 8px);
|
||||
}
|
||||
|
||||
.admin-btn-primary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* ═════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user