refactor(web-client-tpos): dashboard data-driven, 2-level sidebar, fix YARP 502 in Docker
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
@*
|
||||
EN: Admin back-office layout — Sidebar + TopBar + Content area.
|
||||
VI: Layout quản trị — Sidebar + TopBar + Vùng nội dung.
|
||||
Design: pencil-design/src/pages/tPOS/admin/admin-dashboard.pen
|
||||
EN: Admin back-office layout — 2-level sidebar (Admin vs Shop) + Content area.
|
||||
VI: Layout quản trị — Sidebar 2 cấp (Admin vs Cửa hàng) + Vùng nội dung.
|
||||
|
||||
Admin Level: Dashboard, Quản lý Cửa hàng, Hệ thống, Phân quyền, Cài đặt
|
||||
Shop Level: Detected via URL /admin/shop/{shopId}/**, shows vertical-specific menu
|
||||
*@
|
||||
@inherits LayoutComponentBase
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IJSRuntime JS
|
||||
@inject WebClientTpos.Client.Services.AuthStateService AuthState
|
||||
@inject WebClientTpos.Client.Services.AuthService AuthSvc
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
<MudThemeProvider IsDarkMode="true" Theme="_theme" />
|
||||
<MudPopoverProvider />
|
||||
@@ -28,61 +31,74 @@
|
||||
|
||||
@* Navigation *@
|
||||
<nav class="admin-sidebar__nav">
|
||||
@* TỔNG QUAN *@
|
||||
<span class="admin-nav-label">Tổng quan</span>
|
||||
<NavLink href="/admin" class="admin-nav-item" Match="NavLinkMatch.All" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="layout-dashboard"></i>
|
||||
<span>Dashboard</span>
|
||||
</NavLink>
|
||||
@if (_isShopContext)
|
||||
{
|
||||
@* ═══ SHOP LEVEL SIDEBAR ═══ *@
|
||||
|
||||
@* Back to Admin *@
|
||||
<a href="/admin" class="admin-nav-item admin-nav-item--back" style="margin-bottom:8px;">
|
||||
<i data-lucide="arrow-left"></i>
|
||||
<span>Quay lại Admin</span>
|
||||
</a>
|
||||
|
||||
@* CỬA HÀNG *@
|
||||
<span class="admin-nav-label">Cửa hàng</span>
|
||||
<NavLink href="/admin/stores" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="store"></i>
|
||||
<span>Quản lý cửa hàng</span>
|
||||
</NavLink>
|
||||
<NavLink href="/admin/products" class="admin-nav-item admin-nav-item--sub" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="package"></i>
|
||||
<span>Sản phẩm & Menu</span>
|
||||
</NavLink>
|
||||
<NavLink href="/admin/staff" class="admin-nav-item admin-nav-item--sub" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="users"></i>
|
||||
<span>Nhân sự</span>
|
||||
</NavLink>
|
||||
<NavLink href="/admin/inventory" class="admin-nav-item admin-nav-item--sub" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="warehouse"></i>
|
||||
<span>Kho hàng</span>
|
||||
</NavLink>
|
||||
<NavLink href="/admin/system/devices" class="admin-nav-item admin-nav-item--sub" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="monitor"></i>
|
||||
<span>Thiết bị</span>
|
||||
</NavLink>
|
||||
@* Shop Info Header *@
|
||||
<div style="padding:12px 16px;margin-bottom:4px;">
|
||||
<div style="display:flex;align-items:center;gap:10px;">
|
||||
<div style="width:32px;height:32px;border-radius:8px;background:rgba(255,92,0,0.15);display:flex;align-items:center;justify-content:center;">
|
||||
<i data-lucide="@ShopSidebarConfig.GetVerticalIcon(_shopCategory)" style="width:16px;height:16px;color:var(--admin-orange-primary);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:14px;color:var(--admin-text-primary);">@_shopName</div>
|
||||
<div style="font-size:11px;color:var(--admin-text-tertiary);">@ShopSidebarConfig.GetVerticalLabel(_shopCategory)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* KINH DOANH *@
|
||||
<span class="admin-nav-label">Kinh doanh</span>
|
||||
<NavLink href="/admin/customers" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="heart"></i>
|
||||
<span>Khách hàng & Loyalty</span>
|
||||
</NavLink>
|
||||
<NavLink href="/admin/finance/revenue" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="bar-chart-2"></i>
|
||||
<span>Báo cáo & Phân tích</span>
|
||||
</NavLink>
|
||||
<NavLink href="/admin/finance" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="wallet"></i>
|
||||
<span>Tài chính</span>
|
||||
</NavLink>
|
||||
@* Vertical-specific menu *@
|
||||
<span class="admin-nav-label">Quản lý</span>
|
||||
@foreach (var item in ShopSidebarConfig.GetMenuItems(_shopCategory))
|
||||
{
|
||||
var route = $"/admin/shop/{_shopId}/{item.Route}";
|
||||
var cssClass = item.IsSub ? "admin-nav-item admin-nav-item--sub" : "admin-nav-item";
|
||||
<NavLink href="@route" class="@cssClass" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="@item.Icon"></i>
|
||||
<span>@item.Label</span>
|
||||
</NavLink>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@* ═══ ADMIN LEVEL SIDEBAR ═══ *@
|
||||
|
||||
@* TỔNG QUAN *@
|
||||
<span class="admin-nav-label">Tổng quan</span>
|
||||
<NavLink href="/admin" class="admin-nav-item" Match="NavLinkMatch.All" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="layout-dashboard"></i>
|
||||
<span>Dashboard</span>
|
||||
</NavLink>
|
||||
|
||||
@* HỆ THỐNG *@
|
||||
<span class="admin-nav-label">Hệ thống</span>
|
||||
<NavLink href="/admin/roles" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="shield"></i>
|
||||
<span>Phân quyền</span>
|
||||
</NavLink>
|
||||
<NavLink href="/admin/system/audit" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="settings"></i>
|
||||
<span>Cài đặt</span>
|
||||
</NavLink>
|
||||
@* CỬA HÀNG *@
|
||||
<span class="admin-nav-label">Cửa hàng</span>
|
||||
<NavLink href="/admin/stores" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="store"></i>
|
||||
<span>Quản lý Cửa hàng</span>
|
||||
</NavLink>
|
||||
|
||||
@* HỆ THỐNG *@
|
||||
<span class="admin-nav-label">Hệ thống</span>
|
||||
<NavLink href="/admin/system/audit" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="settings"></i>
|
||||
<span>Hệ thống</span>
|
||||
</NavLink>
|
||||
<NavLink href="/admin/roles" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="shield"></i>
|
||||
<span>Phân quyền</span>
|
||||
</NavLink>
|
||||
<NavLink href="/admin/settings" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="sliders-horizontal"></i>
|
||||
<span>Cài đặt</span>
|
||||
</NavLink>
|
||||
}
|
||||
</nav>
|
||||
|
||||
@* User profile *@
|
||||
@@ -113,11 +129,73 @@
|
||||
@code {
|
||||
private bool _sidebarOpen = false;
|
||||
|
||||
// EN: Shop context detection — parsed from URL
|
||||
// VI: Phát hiện context cửa hàng — parse từ URL
|
||||
private bool _isShopContext = false;
|
||||
private string? _shopId;
|
||||
private string _shopName = "Cửa hàng";
|
||||
private string? _shopCategory;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
NavigationManager.LocationChanged += OnLocationChanged;
|
||||
DetectShopContext();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
// EN: Re-init Lucide icons after every render (Blazor navigation replaces DOM)
|
||||
// VI: Khởi tạo lại Lucide icons sau mỗi lần render (Blazor navigation thay đổi DOM)
|
||||
try { await JS.InvokeVoidAsync("lucide.createIcons"); } catch { /* ignore if lucide not loaded */ }
|
||||
try { await JS.InvokeVoidAsync("lucide.createIcons"); } catch { }
|
||||
}
|
||||
|
||||
private void OnLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
|
||||
{
|
||||
DetectShopContext();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Detect if current URL is in shop context (/admin/shop/{shopId}/**).
|
||||
/// VI: Phát hiện URL hiện tại có đang trong context cửa hàng không.
|
||||
/// </summary>
|
||||
private void DetectShopContext()
|
||||
{
|
||||
var uri = new Uri(NavigationManager.Uri);
|
||||
var path = uri.AbsolutePath;
|
||||
|
||||
// EN: Match pattern /admin/shop/{shopId}/**
|
||||
// VI: Match pattern /admin/shop/{shopId}/**
|
||||
if (path.StartsWith("/admin/shop/") && path.Length > "/admin/shop/".Length)
|
||||
{
|
||||
var remaining = path["/admin/shop/".Length..];
|
||||
var slashIndex = remaining.IndexOf('/');
|
||||
_shopId = slashIndex > 0 ? remaining[..slashIndex] : remaining;
|
||||
_isShopContext = true;
|
||||
|
||||
// EN: Try to read shop info from query params or localStorage (set by Dashboard click)
|
||||
// VI: Đọc thông tin shop từ query params hoặc localStorage
|
||||
// For now, use shopId as name fallback
|
||||
_shopName = _shopName == "Cửa hàng" ? $"Shop #{_shopId?[..8]}" : _shopName;
|
||||
}
|
||||
else
|
||||
{
|
||||
_isShopContext = false;
|
||||
_shopId = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Set shop context info (called from pages that know shop details).
|
||||
/// VI: Đặt thông tin context cửa hàng (gọi từ pages biết chi tiết shop).
|
||||
/// </summary>
|
||||
public void SetShopContext(string shopId, string name, string? category)
|
||||
{
|
||||
_shopId = shopId;
|
||||
_shopName = name;
|
||||
_shopCategory = category;
|
||||
_isShopContext = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void ToggleSidebar() => _sidebarOpen = !_sidebarOpen;
|
||||
@@ -154,4 +232,9 @@
|
||||
LinesDefault = "#1F1F23"
|
||||
}
|
||||
};
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
NavigationManager.LocationChanged -= OnLocationChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
@using WebClientTpos.Client.Services
|
||||
|
||||
@*
|
||||
EN: Admin Dashboard — overview of business metrics, stores, alerts, and recent activity.
|
||||
VI: Dashboard Admin — tổng quan chỉ số kinh doanh, cửa hàng, cảnh báo, hoạt động gần đây.
|
||||
Design: pencil-design/src/pages/tPOS/admin/admin-dashboard.pen
|
||||
EN: Admin Dashboard — overview with real data from shops list.
|
||||
VI: Dashboard Admin — tổng quan với dữ liệu thực từ danh sách shops.
|
||||
*@
|
||||
|
||||
<PageTitle>Dashboard — GoodGo Admin</PageTitle>
|
||||
@@ -25,7 +24,6 @@
|
||||
</div>
|
||||
<button class="admin-icon-btn" title="Thông báo">
|
||||
<i data-lucide="bell"></i>
|
||||
<span class="admin-icon-btn__dot"></span>
|
||||
</button>
|
||||
<button class="admin-btn-primary" @onclick="@(() => NavigateTo("stores/create"))">
|
||||
<i data-lucide="plus"></i>
|
||||
@@ -37,69 +35,54 @@
|
||||
@* ═══ CONTENT ═══ *@
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;gap:24px;">
|
||||
|
||||
@* ── KPI ROW ── *@
|
||||
@* ── KPI ROW (data-driven) ── *@
|
||||
<div class="admin-kpi-row">
|
||||
@* KPI 1: Tổng doanh thu *@
|
||||
<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 class="admin-kpi-card__badge admin-kpi-card__badge--up">
|
||||
<i data-lucide="arrow-up"></i>
|
||||
<span>+18.2%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">128.5M</div>
|
||||
<div class="admin-kpi-card__label">Tổng doanh thu</div>
|
||||
</div>
|
||||
|
||||
@* KPI 2: Tổng đơn hàng *@
|
||||
<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 class="admin-kpi-card__badge admin-kpi-card__badge--up">
|
||||
<i data-lucide="arrow-up"></i>
|
||||
<span>+12.4%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">1,247</div>
|
||||
<div class="admin-kpi-card__label">Tổng đơn hàng</div>
|
||||
</div>
|
||||
|
||||
@* KPI 3: Cửa hàng hoạt động *@
|
||||
@* KPI 1: Cửa hàng *@
|
||||
<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="store" style="color:#8B5CF6;"></i>
|
||||
</div>
|
||||
<div class="admin-kpi-card__badge admin-kpi-card__badge--up">
|
||||
<span>3 online</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">3</div>
|
||||
<div class="admin-kpi-card__label">Cửa hàng hoạt động</div>
|
||||
<div class="admin-kpi-card__value">@_shops.Count</div>
|
||||
<div class="admin-kpi-card__label">Cửa hàng</div>
|
||||
</div>
|
||||
|
||||
@* KPI 4: Nhân viên online *@
|
||||
@* KPI 2: Đang hoạt động *@
|
||||
<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="check-circle" style="color:#22C55E;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">@_shops.Count(s => s.Status == "active")</div>
|
||||
<div class="admin-kpi-card__label">Đang hoạt động</div>
|
||||
</div>
|
||||
|
||||
@* KPI 3: Đang thiết lập *@
|
||||
<div class="admin-kpi-card">
|
||||
<div class="admin-kpi-card__header">
|
||||
<div class="admin-kpi-card__icon" style="background-color:rgba(245,158,11,0.125);">
|
||||
<i data-lucide="settings" style="color:#F59E0B;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">@_shops.Count(s => s.Status != "active")</div>
|
||||
<div class="admin-kpi-card__label">Đang thiết lập</div>
|
||||
</div>
|
||||
|
||||
@* KPI 4: Ngành hàng *@
|
||||
<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="users" style="color:#EC4899;"></i>
|
||||
</div>
|
||||
<div class="admin-kpi-card__badge admin-kpi-card__badge--up">
|
||||
<i data-lucide="arrow-up"></i>
|
||||
<span>+22.7%</span>
|
||||
<i data-lucide="layers" style="color:#EC4899;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-kpi-card__value">12</div>
|
||||
<div class="admin-kpi-card__label">Nhân viên online</div>
|
||||
<div class="admin-kpi-card__value">@_shops.Select(s => s.Category).Distinct().Count()</div>
|
||||
<div class="admin-kpi-card__label">Ngành hàng</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── BOTTOM ROW: Store Overview + Right Column ── *@
|
||||
@* ── BOTTOM ROW ── *@
|
||||
<div style="display:flex;gap:24px;flex:1;min-height:0;">
|
||||
|
||||
@* ── LEFT: Store Overview ── *@
|
||||
@@ -121,7 +104,7 @@
|
||||
<i data-lucide="store" style="width:48px;height:48px;color:var(--admin-orange-primary);margin-bottom:16px;"></i>
|
||||
<h3 style="font-size:18px;font-weight:700;color:var(--pos-text-primary, #FFFFFF);margin:0 0 8px;">Welcome! Tạo cửa hàng đầu tiên</h3>
|
||||
<p style="font-size:14px;color:var(--pos-text-tertiary, #ADADB0);margin:0 0 20px;">Bắt đầu bằng việc tạo cửa hàng để quản lý kinh doanh của bạn.</p>
|
||||
<a href="/admin/onboarding/store" class="admin-btn-primary" style="display:inline-flex;align-items:center;gap:8px;">
|
||||
<a href="/admin/stores/create" class="admin-btn-primary" style="display:inline-flex;align-items:center;gap:8px;">
|
||||
<i data-lucide="plus" style="width:16px;height:16px;"></i>
|
||||
Tạo cửa hàng ngay
|
||||
</a>
|
||||
@@ -131,7 +114,7 @@
|
||||
{
|
||||
@foreach (var shop in _shops)
|
||||
{
|
||||
<div class="admin-store-card">
|
||||
<a href="/admin/shop/@shop.Id/overview" class="admin-store-card" style="text-decoration:none;color:inherit;cursor:pointer;">
|
||||
<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);">
|
||||
@@ -139,7 +122,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="admin-store-card__name">@shop.Name</div>
|
||||
<div class="admin-store-card__type">@(shop.Category ?? "Shop") • @(shop.Description ?? shop.Slug)</div>
|
||||
<div class="admin-store-card__type">@ShopSidebarConfig.GetVerticalLabel(shop.Category) • @(shop.Slug)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-status-badge admin-status-badge--@(shop.Status == "active" ? "online" : "setup")">
|
||||
@@ -147,95 +130,67 @@
|
||||
@(shop.Status == "active" ? "Đang mở" : "Thiết lập")
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ── RIGHT COLUMN: Alerts + Activity ── *@
|
||||
@* ── RIGHT COLUMN ── *@
|
||||
<div style="width:380px;display:flex;flex-direction:column;gap:20px;">
|
||||
@* Alerts Panel *@
|
||||
@* Quick Actions Panel *@
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="alert-triangle" style="color:#F59E0B;"></i>
|
||||
Cảnh báo
|
||||
<span class="admin-badge-count admin-badge-count--danger">4</span>
|
||||
<i data-lucide="zap" style="color:var(--admin-orange-primary);"></i>
|
||||
Thao tác nhanh
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body">
|
||||
<div class="admin-alert-list">
|
||||
<div class="admin-alert-item admin-alert-item--danger">
|
||||
<i data-lucide="package-x" style="color:#EF4444;"></i>
|
||||
<div class="admin-alert-item__text">
|
||||
<div class="admin-alert-item__title">5 sản phẩm sắp hết hàng</div>
|
||||
<div class="admin-alert-item__sub">Coffee House Q1</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-alert-item admin-alert-item--warning">
|
||||
<i data-lucide="clock" style="color:#F59E0B;"></i>
|
||||
<div class="admin-alert-item__text">
|
||||
<div class="admin-alert-item__title">Ca tối thiếu 1 nhân viên</div>
|
||||
<div class="admin-alert-item__sub">Nhà hàng Q3 • Ngày mai</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-alert-item admin-alert-item--info">
|
||||
<i data-lucide="printer" style="color:#3B82F6;"></i>
|
||||
<div class="admin-alert-item__text">
|
||||
<div class="admin-alert-item__title">Máy in mất kết nối</div>
|
||||
<div class="admin-alert-item__sub">Coffee House Q1 • Kitchen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:8px;">
|
||||
<a href="/admin/stores/create" class="admin-quick-action">
|
||||
<i data-lucide="plus-circle" style="color:#22C55E;width:18px;height:18px;"></i>
|
||||
<span>Tạo cửa hàng mới</span>
|
||||
<i data-lucide="chevron-right" style="margin-left:auto;width:16px;height:16px;color:var(--admin-text-tertiary);"></i>
|
||||
</a>
|
||||
<a href="/admin/system/audit" class="admin-quick-action">
|
||||
<i data-lucide="settings" style="color:#3B82F6;width:18px;height:18px;"></i>
|
||||
<span>Cài đặt hệ thống</span>
|
||||
<i data-lucide="chevron-right" style="margin-left:auto;width:16px;height:16px;color:var(--admin-text-tertiary);"></i>
|
||||
</a>
|
||||
<a href="/admin/roles" class="admin-quick-action">
|
||||
<i data-lucide="shield" style="color:#8B5CF6;width:18px;height:18px;"></i>
|
||||
<span>Phân quyền</span>
|
||||
<i data-lucide="chevron-right" style="margin-left:auto;width:16px;height:16px;color:var(--admin-text-tertiary);"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Recent Activity Panel *@
|
||||
@* Status Panel *@
|
||||
<div class="admin-panel" style="flex:1;">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="activity" style="color:#22C55E;"></i>
|
||||
Hoạt động gần đây
|
||||
Trạng thái hệ thống
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body">
|
||||
<div class="admin-activity-list">
|
||||
<div class="admin-activity-item">
|
||||
<span class="admin-activity-dot" style="background-color:#22C55E;"></span>
|
||||
<div class="admin-activity-item__text">
|
||||
<div class="admin-activity-item__title">Đơn #2847 hoàn thành</div>
|
||||
<div class="admin-activity-item__time">Coffee House Q1 • 2 phút trước</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-activity-item">
|
||||
<span class="admin-activity-dot" style="background-color:#3B82F6;"></span>
|
||||
<div class="admin-activity-item__text">
|
||||
<div class="admin-activity-item__title">Nguyễn Văn A clock-in</div>
|
||||
<div class="admin-activity-item__time">Nhà hàng Q3 • 5 phút trước</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-activity-item">
|
||||
<span class="admin-activity-dot" style="background-color:#F59E0B;"></span>
|
||||
<div class="admin-activity-item__text">
|
||||
<div class="admin-activity-item__title">Nhập kho 15 sản phẩm</div>
|
||||
<div class="admin-activity-item__time">Coffee House Q1 • 12 phút trước</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-activity-item">
|
||||
<span class="admin-activity-dot" style="background-color:#8B5CF6;"></span>
|
||||
<div class="admin-activity-item__text">
|
||||
<div class="admin-activity-item__title">Cập nhật menu buổi tối</div>
|
||||
<div class="admin-activity-item__time">Nhà hàng Q3 • 28 phút trước</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-activity-item">
|
||||
<span class="admin-activity-dot" style="background-color:#EC4899;"></span>
|
||||
<div class="admin-activity-item__text">
|
||||
<div class="admin-activity-item__title">Khách VIP mới: Trần Thị B</div>
|
||||
<div class="admin-activity-item__time">Hệ thống • 45 phút trước</div>
|
||||
</div>
|
||||
</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;">API Gateway</span>
|
||||
<span style="font-size:13px;color:#22C55E;display:flex;align-items:center;gap:4px;">
|
||||
<span style="width:6px;height:6px;border-radius:50%;background:#22C55E;display:inline-block;"></span> Online
|
||||
</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="color:var(--admin-text-tertiary);font-size:14px;">IAM Service</span>
|
||||
<span style="font-size:13px;color:#22C55E;display:flex;align-items:center;gap:4px;">
|
||||
<span style="width:6px;height:6px;border-radius:50%;background:#22C55E;display:inline-block;"></span> Online
|
||||
</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<span style="color:var(--admin-text-tertiary);font-size:14px;">Merchant Service</span>
|
||||
<span style="font-size:13px;color:#22C55E;display:flex;align-items:center;gap:4px;">
|
||||
<span style="width:6px;height:6px;border-radius:50%;background:#22C55E;display:inline-block;"></span> Online
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,19 @@ public class AuthService
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync("/api/auth/register", dto);
|
||||
// EN: Build payload with FirstName/LastName from DisplayName (IAM requires these)
|
||||
// VI: Tạo payload với FirstName/LastName từ DisplayName (IAM yêu cầu)
|
||||
var parts = (dto.DisplayName ?? "User").Trim().Split(' ', 2);
|
||||
var payload = new
|
||||
{
|
||||
dto.Email,
|
||||
dto.Password,
|
||||
FirstName = parts[0],
|
||||
LastName = parts.Length > 1 ? parts[1] : parts[0],
|
||||
DisplayName = dto.DisplayName
|
||||
};
|
||||
|
||||
var response = await _http.PostAsJsonAsync("/api/auth/register", payload);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return (true, null);
|
||||
@@ -59,18 +71,32 @@ public class AuthService
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
try
|
||||
{
|
||||
var error = JsonSerializer.Deserialize<ApiResponse<object>>(content,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
return (false, error?.Error ?? "Đăng ký thất bại");
|
||||
// EN: Try to parse structured validation errors from IAM
|
||||
// VI: Parse lỗi validation có cấu trúc từ IAM
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
if (doc.RootElement.TryGetProperty("errors", out var errors))
|
||||
{
|
||||
var msgs = new List<string>();
|
||||
foreach (var prop in errors.EnumerateObject())
|
||||
{
|
||||
foreach (var err in prop.Value.EnumerateArray())
|
||||
msgs.Add(err.GetString() ?? prop.Name);
|
||||
}
|
||||
return (false, string.Join("; ", msgs));
|
||||
}
|
||||
if (doc.RootElement.TryGetProperty("title", out var title))
|
||||
return (false, title.GetString());
|
||||
|
||||
return (false, content.Length > 200 ? "Đăng ký thất bại" : content);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (false, content);
|
||||
return (false, "Đăng ký thất bại");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, $"Lỗi kết nối: {ex.Message}");
|
||||
return (false, $"Lỗi ({ex.GetType().Name})");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// EN: Sidebar menu configuration per shop vertical (Café, Restaurant, Karaoke, Spa).
|
||||
// VI: Cấu hình menu sidebar theo ngành hàng (Café, Nhà hàng, Karaoke, Spa).
|
||||
|
||||
namespace WebClientTpos.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Static config for shop-level sidebar menus per vertical type.
|
||||
/// VI: Cấu hình tĩnh cho menu sidebar cấp cửa hàng theo loại ngành hàng.
|
||||
/// </summary>
|
||||
public static class ShopSidebarConfig
|
||||
{
|
||||
public record MenuItem(string Label, string Icon, string Route, bool IsSub = false);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get sidebar menu items for a specific shop vertical.
|
||||
/// VI: Lấy danh sách menu sidebar cho ngành hàng cụ thể.
|
||||
/// </summary>
|
||||
public static List<MenuItem> GetMenuItems(string? category)
|
||||
{
|
||||
var vertical = (category ?? "").ToLowerInvariant() switch
|
||||
{
|
||||
"cafe" or "café" or "coffee" or "foodbeverage" => "cafe",
|
||||
"restaurant" or "nhà hàng" => "restaurant",
|
||||
"karaoke" or "entertainment" => "karaoke",
|
||||
"spa" or "beauty" => "spa",
|
||||
_ => "cafe" // EN: Default to café / VI: Mặc định là cafe
|
||||
};
|
||||
|
||||
return vertical switch
|
||||
{
|
||||
"cafe" => new()
|
||||
{
|
||||
new("Tổng quan", "layout-dashboard", "overview"),
|
||||
new("POS Bán hàng", "monitor", "pos"),
|
||||
new("Menu & Đồ uống", "coffee", "menu", true),
|
||||
new("Tồn kho", "warehouse", "inventory", true),
|
||||
new("Nhân sự", "users", "staff", true),
|
||||
new("Khách hàng", "heart", "customers"),
|
||||
new("Báo cáo", "bar-chart-2", "reports"),
|
||||
},
|
||||
"restaurant" => new()
|
||||
{
|
||||
new("Tổng quan", "layout-dashboard", "overview"),
|
||||
new("POS Bán hàng", "monitor", "pos"),
|
||||
new("Menu & Món ăn", "utensils", "menu", true),
|
||||
new("Bàn / Table", "grid-3x3", "tables", true),
|
||||
new("Bếp (Kitchen)", "flame", "kitchen", true),
|
||||
new("Tồn kho", "warehouse", "inventory", true),
|
||||
new("Nhân sự", "users", "staff", true),
|
||||
new("Khách hàng", "heart", "customers"),
|
||||
new("Báo cáo", "bar-chart-2", "reports"),
|
||||
},
|
||||
"karaoke" => new()
|
||||
{
|
||||
new("Tổng quan", "layout-dashboard", "overview"),
|
||||
new("POS Bán hàng", "monitor", "pos"),
|
||||
new("Phòng", "door-open", "rooms", true),
|
||||
new("Menu / Bar", "wine", "menu", true),
|
||||
new("Tồn kho", "warehouse", "inventory", true),
|
||||
new("Nhân sự", "users", "staff", true),
|
||||
new("Khách hàng", "heart", "customers"),
|
||||
new("Báo cáo", "bar-chart-2", "reports"),
|
||||
},
|
||||
"spa" => new()
|
||||
{
|
||||
new("Tổng quan", "layout-dashboard", "overview"),
|
||||
new("POS Bán hàng", "monitor", "pos"),
|
||||
new("Lịch hẹn", "calendar", "appointments", true),
|
||||
new("Dịch vụ", "sparkles", "services", true),
|
||||
new("Nhân sự", "users", "staff", true),
|
||||
new("Khách hàng", "heart", "customers"),
|
||||
new("Báo cáo", "bar-chart-2", "reports"),
|
||||
},
|
||||
_ => new()
|
||||
{
|
||||
new("Tổng quan", "layout-dashboard", "overview"),
|
||||
new("POS Bán hàng", "monitor", "pos"),
|
||||
new("Sản phẩm", "package", "products", true),
|
||||
new("Nhân sự", "users", "staff", true),
|
||||
new("Báo cáo", "bar-chart-2", "reports"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get vertical display name.
|
||||
/// VI: Lấy tên hiển thị của ngành hàng.
|
||||
/// </summary>
|
||||
public static string GetVerticalLabel(string? category) => (category ?? "").ToLowerInvariant() switch
|
||||
{
|
||||
"cafe" or "café" or "coffee" or "foodbeverage" => "Café",
|
||||
"restaurant" or "nhà hàng" => "Nhà hàng",
|
||||
"karaoke" or "entertainment" => "Karaoke",
|
||||
"spa" or "beauty" => "Spa",
|
||||
_ => "Cửa hàng"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get vertical icon.
|
||||
/// VI: Lấy icon ngành hàng.
|
||||
/// </summary>
|
||||
public static string GetVerticalIcon(string? category) => (category ?? "").ToLowerInvariant() switch
|
||||
{
|
||||
"cafe" or "café" or "coffee" or "foodbeverage" => "coffee",
|
||||
"restaurant" or "nhà hàng" => "utensils",
|
||||
"karaoke" or "entertainment" => "mic",
|
||||
"spa" or "beauty" => "sparkles",
|
||||
_ => "store"
|
||||
};
|
||||
}
|
||||
@@ -236,6 +236,60 @@
|
||||
color: var(--admin-text-tertiary);
|
||||
}
|
||||
|
||||
/* EN: Back nav item (← Quay lại Admin) / VI: Mục quay lại Admin */
|
||||
.admin-nav-item--back {
|
||||
color: var(--admin-text-tertiary);
|
||||
border-bottom: 1px solid var(--admin-border-subtle);
|
||||
border-radius: 0;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.admin-nav-item--back:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.admin-nav-item--back i,
|
||||
.admin-nav-item--back svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--admin-text-tertiary);
|
||||
}
|
||||
|
||||
.admin-nav-item--back span {
|
||||
font-size: 13px;
|
||||
color: var(--admin-text-tertiary);
|
||||
}
|
||||
|
||||
/* EN: Store card as clickable link / VI: Thẻ cửa hàng là link bấm được */
|
||||
a.admin-store-card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
a.admin-store-card:hover {
|
||||
background-color: rgba(255, 92, 0, 0.08);
|
||||
border-color: var(--admin-orange-primary);
|
||||
}
|
||||
|
||||
/* EN: Quick action row / VI: Hàng thao tác nhanh */
|
||||
.admin-quick-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--admin-radius-md);
|
||||
background: var(--admin-bg-interactive);
|
||||
text-decoration: none;
|
||||
color: var(--admin-text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.admin-quick-action:hover {
|
||||
background-color: rgba(255, 92, 0, 0.08);
|
||||
color: var(--admin-orange-primary);
|
||||
}
|
||||
|
||||
/* EN: Nav badge / VI: Huy hiệu điều hướng */
|
||||
.admin-nav-badge {
|
||||
width: 22px;
|
||||
|
||||
@@ -16,6 +16,50 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
// VI: Load cấu hình YARP từ yarp.json
|
||||
builder.Configuration.AddJsonFile("yarp.json", optional: false, reloadOnChange: true);
|
||||
|
||||
// EN: Override YARP cluster addresses from Docker environment variables
|
||||
// VI: Override địa chỉ YARP cluster từ biến môi trường Docker
|
||||
var iamBaseUrl = Environment.GetEnvironmentVariable("IamService__BaseUrl");
|
||||
var gatewayUrl = Environment.GetEnvironmentVariable("ApiSettings__GatewayUrl");
|
||||
|
||||
if (!string.IsNullOrEmpty(iamBaseUrl))
|
||||
{
|
||||
builder.Configuration["ReverseProxy:Clusters:iam-cluster:Destinations:destination1:Address"] = iamBaseUrl;
|
||||
}
|
||||
|
||||
// EN: Merchant service — discover via env or construct from gateway naming convention
|
||||
// VI: Merchant service — phát hiện qua env hoặc tạo từ naming convention gateway
|
||||
var merchantBaseUrl = Environment.GetEnvironmentVariable("MerchantService__BaseUrl");
|
||||
if (string.IsNullOrEmpty(merchantBaseUrl) && !string.IsNullOrEmpty(iamBaseUrl))
|
||||
{
|
||||
// EN: If no explicit merchant URL, try to construct from Docker network naming
|
||||
// VI: Nếu không có URL merchant rõ ràng, thử tạo từ Docker network naming
|
||||
merchantBaseUrl = iamBaseUrl.Replace("iam-service-net", "merchant-service-net");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(merchantBaseUrl))
|
||||
{
|
||||
builder.Configuration["ReverseProxy:Clusters:merchant-cluster:Destinations:destination1:Address"] = merchantBaseUrl;
|
||||
}
|
||||
|
||||
var catalogBaseUrl = Environment.GetEnvironmentVariable("CatalogService__BaseUrl");
|
||||
if (string.IsNullOrEmpty(catalogBaseUrl) && !string.IsNullOrEmpty(iamBaseUrl))
|
||||
{
|
||||
catalogBaseUrl = iamBaseUrl.Replace("iam-service-net", "catalog-service-net");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(catalogBaseUrl))
|
||||
{
|
||||
builder.Configuration["ReverseProxy:Clusters:catalog-cluster:Destinations:destination1:Address"] = catalogBaseUrl;
|
||||
}
|
||||
|
||||
var orderBaseUrl = Environment.GetEnvironmentVariable("OrderService__BaseUrl");
|
||||
if (string.IsNullOrEmpty(orderBaseUrl) && !string.IsNullOrEmpty(iamBaseUrl))
|
||||
{
|
||||
orderBaseUrl = iamBaseUrl.Replace("iam-service-net", "order-service-net");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(orderBaseUrl))
|
||||
{
|
||||
builder.Configuration["ReverseProxy:Clusters:order-cluster:Destinations:destination1:Address"] = orderBaseUrl;
|
||||
}
|
||||
|
||||
// EN: Add YARP Reverse Proxy
|
||||
// VI: Thêm YARP Reverse Proxy
|
||||
builder.Services.AddReverseProxy()
|
||||
|
||||
@@ -1282,6 +1282,12 @@ services:
|
||||
# EN: IAM Service Communication
|
||||
# VI: Giao tiếp IAM Service
|
||||
- IamService__BaseUrl=http://iam-service-net:8080
|
||||
# EN: YARP Reverse Proxy — override cluster addresses for Docker network
|
||||
# VI: YARP Reverse Proxy — override địa chỉ cluster cho Docker network
|
||||
- ReverseProxy__Clusters__iam-cluster__Destinations__destination1__Address=http://iam-service-net:8080
|
||||
- ReverseProxy__Clusters__merchant-cluster__Destinations__destination1__Address=http://merchant-service-net:8080
|
||||
- ReverseProxy__Clusters__catalog-cluster__Destinations__destination1__Address=http://catalog-service-net:8080
|
||||
- ReverseProxy__Clusters__order-cluster__Destinations__destination1__Address=http://order-service-net:8080
|
||||
ports:
|
||||
- "3001:8080"
|
||||
depends_on:
|
||||
|
||||
Reference in New Issue
Block a user