refactor(web-client-tpos): dashboard data-driven, 2-level sidebar, fix YARP 502 in Docker

This commit is contained in:
Ho Ngoc Hai
2026-02-28 03:51:51 +07:00
parent 07dc82ad49
commit a1e27aca46
7 changed files with 462 additions and 184 deletions

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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})");
}
}

View File

@@ -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"
};
}

View File

@@ -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;

View File

@@ -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()

View File

@@ -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: