Files
pos-system/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/PosLayout.razor
Ho Ngoc Hai 659e8e05e5 fix: POS settings button navigates by role — staff→/staff, admin→/admin
PosLayout.razor hardcoded navigation to /admin for the settings button
and sidebar link, causing staff users to land on the admin page.
Now uses AuthStateService.GetPortalUrl() for role-aware routing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 07:24:08 +07:00

222 lines
9.7 KiB
Plaintext

@*
EN: POS terminal layout — Responsive full-screen layout with status bar + content.
Desktop: Full sidebar + content. Tablet: Collapsible order drawer. Mobile: Bottom nav + full-screen overlay.
VI: Layout POS — Layout toàn màn hình responsive với thanh trạng thái + nội dung.
Desktop: Sidebar đầy đủ + nội dung. Tablet: Drawer đơn hàng thu gọn. Mobile: Nav dưới + overlay toàn màn hình.
Design: pencil-design/src/pages/tPOS/pos/cafe/desktop.pen
*@
@inherits LayoutComponentBase
@implements IDisposable
@inject IStringLocalizer<PosLayout> L
@inject NavigationManager NavigationManager
@inject WebClientTpos.Client.Services.PosDataService DataService
@inject IJSRuntime JS
@inject WebClientTpos.Client.Services.AuthStateService AuthState
<MudThemeProvider IsDarkMode="true" Theme="AppTheme.DefaultDark" />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<div class="pos-layout">
@* ═══ STATUS BAR ═══ *@
<header class="pos-status-bar">
<div class="pos-status-bar__left">
@* EN: Hamburger menu — visible on tablet/mobile only / VI: Menu hamburger — chi hien thi tren tablet/mobile *@
<button class="pos-mobile-toggle" @onclick="ToggleSidebar" title="Menu">
<i data-lucide="@(_sidebarOpen ? "x" : "menu")" style="width:20px;height:20px;"></i>
</button>
<span class="pos-status-bar__logo">GoodGo POS</span>
<span class="pos-status-bar__store">@StoreName</span>
</div>
<div class="pos-status-bar__right">
<div class="pos-status-bar__indicator pos-status-bar__indicator--online">
<span style="width:6px;height:6px;border-radius:100px;background:currentColor;"></span>
<span>Online</span>
</div>
<span class="pos-status-bar__time">@_currentTime</span>
@* EN: Order panel toggle — visible on tablet/mobile when order panel is hidden
VI: Nút mở panel đơn hàng — hiện trên tablet/mobile khi panel đơn hàng ẩn *@
<button class="pos-order-toggle" @onclick="ToggleOrderPanel" title="Đơn hàng">
<i data-lucide="shopping-cart" style="width:18px;height:18px;"></i>
@if (_orderCount > 0)
{
<span class="pos-order-toggle__badge">@_orderCount</span>
}
</button>
<button class="admin-icon-btn pos-admin-btn" @onclick="GoToPortal" title="Quản lý">
<i data-lucide="settings"></i>
</button>
</div>
</header>
@* ═══ MAIN CONTENT ═══ *@
<div class="pos-main">
@* EN: Mobile sidebar overlay / VI: Overlay sidebar trên mobile *@
@if (_sidebarOpen)
{
<div class="pos-sidebar-overlay" @onclick="CloseSidebar"></div>
}
@* EN: Sidebar navigation — collapsible on tablet/mobile
VI: Sidebar điều hướng — thu gọn trên tablet/mobile *@
<nav class="pos-sidebar @(_sidebarOpen ? "pos-sidebar--open" : "")">
<div class="pos-sidebar__header">
<span style="font-size:15px;font-weight:700;color:var(--pos-orange-primary);">Menu</span>
<button class="pos-sidebar__close" @onclick="CloseSidebar">
<i data-lucide="x" style="width:18px;height:18px;"></i>
</button>
</div>
<div class="pos-sidebar__nav">
<a class="pos-sidebar__link" href="/pos/@_shopIdStr/karaoke" @onclick="CloseSidebar">
<i data-lucide="mic" style="width:18px;height:18px;"></i>
<span>Karaoke</span>
</a>
<a class="pos-sidebar__link" href="/pos/@_shopIdStr/restaurant" @onclick="CloseSidebar">
<i data-lucide="utensils" style="width:18px;height:18px;"></i>
<span>Nhà hàng</span>
</a>
<a class="pos-sidebar__link" href="/pos/@_shopIdStr/cafe" @onclick="CloseSidebar">
<i data-lucide="coffee" style="width:18px;height:18px;"></i>
<span>Café</span>
</a>
<a class="pos-sidebar__link" href="/pos/@_shopIdStr/spa" @onclick="CloseSidebar">
<i data-lucide="sparkles" style="width:18px;height:18px;"></i>
<span>Spa</span>
</a>
<a class="pos-sidebar__link" href="/pos/@_shopIdStr/retail" @onclick="CloseSidebar">
<i data-lucide="shopping-bag" style="width:18px;height:18px;"></i>
<span>Bán lẻ</span>
</a>
</div>
<div class="pos-sidebar__footer">
<a class="pos-sidebar__link" href="@(AuthState.GetPortalUrl())" @onclick="CloseSidebar">
<i data-lucide="settings" style="width:18px;height:18px;"></i>
<span>Quản lý</span>
</a>
</div>
</nav>
@* EN: Page content area / VI: Vùng nội dung trang *@
<div class="pos-page-content">
<CascadingValue Value="this">
@Body
</CascadingValue>
</div>
@* EN: Order panel drawer — slides in from right on tablet/mobile
VI: Drawer panel đơn hàng — trượt vào từ phải trên tablet/mobile *@
@if (_orderPanelOpen)
{
<div class="pos-order-overlay" @onclick="CloseOrderPanel"></div>
}
<div class="pos-order-drawer @(_orderPanelOpen ? "pos-order-drawer--open" : "")">
<div class="pos-order-drawer__header">
<span style="font-size:15px;font-weight:700;">Đơn hàng</span>
<button class="pos-order-drawer__close" @onclick="CloseOrderPanel">
<i data-lucide="x" style="width:18px;height:18px;"></i>
</button>
</div>
<div class="pos-order-drawer__content" id="pos-order-drawer-content">
@* EN: Order panel content is rendered by child pages via RenderFragment
VI: Nội dung panel đơn hàng được render bởi trang con qua RenderFragment *@
@if (OrderPanelContent is not null)
{
@OrderPanelContent
}
else
{
<div style="flex:1;display:flex;align-items:center;justify-content:center;color:var(--pos-text-tertiary);font-size:14px;padding:32px;">
Chưa có đơn hàng
</div>
}
</div>
</div>
</div>
</div>
@code {
private string StoreName { get; set; } = "GoodGo POS";
private string _currentTime = DateTime.Now.ToString("HH:mm");
private Timer? _timer;
private string _shopIdStr = "";
// EN: Responsive state / VI: Trạng thái responsive
private bool _sidebarOpen;
private bool _orderPanelOpen;
private int _orderCount;
/// <summary>
/// EN: Optional order panel content — set by child POS pages via CascadingValue.
/// VI: Nội dung panel đơn hàng tùy chọn — set bởi trang con POS qua CascadingValue.
/// </summary>
public RenderFragment? OrderPanelContent { get; set; }
/// <summary>
/// EN: Set order count badge on the order toggle button.
/// VI: Đặt badge số lượng đơn hàng trên nút toggle đơn hàng.
/// </summary>
public void SetOrderCount(int count)
{
_orderCount = count;
StateHasChanged();
}
/// <summary>
/// EN: Open order panel drawer (tablet/mobile).
/// VI: Mở drawer panel đơn hàng (tablet/mobile).
/// </summary>
public void OpenOrderPanel()
{
_orderPanelOpen = true;
StateHasChanged();
}
protected override async Task OnInitializedAsync()
{
// EN: Update clock every 30 seconds / VI: Cập nhật đồng hồ mỗi 30 giây
_timer = new Timer(_ =>
{
_currentTime = DateTime.Now.ToString("HH:mm");
InvokeAsync(StateHasChanged);
}, null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30));
// EN: Extract shopId from URL path: /pos/{shopId}/...
// VI: Trích xuất shopId từ URL: /pos/{shopId}/...
try
{
var uri = new Uri(NavigationManager.Uri);
var segments = uri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
// Expected: ["pos", "{shopId}", "cafe"|"restaurant"|...]
if (segments.Length >= 2 && Guid.TryParse(segments[1], out var shopId))
{
_shopIdStr = shopId.ToString();
var shop = await DataService.GetShopByIdAsync(shopId);
if (shop != null)
StoreName = shop.Name;
}
}
catch
{
// EN: Fallback to default name / VI: Dùng tên mặc định nếu lỗi
}
}
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
try { await JS.InvokeVoidAsync("lucide.createIcons"); } catch { }
}
private void GoToPortal() => NavigationManager.NavigateTo(AuthState.GetPortalUrl());
private void ToggleSidebar() => _sidebarOpen = !_sidebarOpen;
private void CloseSidebar() => _sidebarOpen = false;
private void ToggleOrderPanel() => _orderPanelOpen = !_orderPanelOpen;
private void CloseOrderPanel() => _orderPanelOpen = false;
public void Dispose() => _timer?.Dispose();
}