feat: Implement initial POS layout and base components, along with admin base components and agent prompts for admin modules.

This commit is contained in:
Ho Ngoc Hai
2026-02-12 09:58:01 +07:00
parent 65ced2c512
commit e519435018
12 changed files with 2039 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
# Sub-Agent 1A: Admin Dashboard + Store Management
## Objective
Convert 5 Pencil design files into Blazor Server pages for the Admin module.
## Tech Stack
- **Framework**: Blazor Server (.NET 8) with MudBlazor
- **Styling**: Vanilla CSS using existing `admin.css` tokens (BEM: `.admin-{component}--{variant}`)
- **Icons**: Lucide via `<i data-lucide="icon-name"></i>` (add `lucide.createIcons()` in OnAfterRenderAsync)
- **i18n**: `IStringLocalizer<T>` with locale files at `wwwroot/locales/{vi-VN,en-US}.json`
- **Base class**: Inherit from `AdminBase` at `Pages/Admin/AdminBase.cs`
- **Layout**: Use `@layout AdminLayout` (already created at `Layout/AdminLayout.razor`)
## Design Files (Input)
Read and convert these `.pen` files from `pencil-design/src/pages/tPOS/admin/`:
1. `admin-dashboard.pen``Pages/Admin/Dashboard.razor`
2. `store-list.pen``Pages/Admin/Store/StoreList.razor`
3. `store-detail.pen``Pages/Admin/Store/StoreDetail.razor`
4. `store-create.pen``Pages/Admin/Store/StoreCreate.razor`
5. `store-settings.pen``Pages/Admin/Store/StoreSettings.razor`
## Route Definitions
```
@page "/admin" → Dashboard.razor
@page "/admin/stores" → StoreList.razor
@page "/admin/stores/{Id}" → StoreDetail.razor
@page "/admin/stores/create" → StoreCreate.razor
@page "/admin/stores/{Id}/settings" → StoreSettings.razor
```
## Pattern Reference
Follow the same pattern as `Pages/Auth/LoginCustomer.razor`:
- `@page` directive, `@inherits AdminBase`, `@layout AdminLayout`
- Vietnamese/English bilingual comments with `@* EN: ... VI: ... *@`
- All user-visible text uses `@L["Key"]`
- Add locale keys to both `vi-VN.json` and `en-US.json`
## How to Read .pen Files
Each `.pen` file is JSON with structure:
```json
{
"children": [{ "type": "frame", "children": [...] }],
"variables": { "bg-page": { "type": "color", "value": "#0A0A0B" } }
}
```
- `type: "frame"``<div>`
- `type: "text"` with `content` → text node
- `type: "icon_font"` with `iconFontName``<i data-lucide="icon-name">`
- `fill: "$variable-name"` → use CSS var `--admin-variable-name`
- `layout: "vertical"``flex-direction: column`
- `gap`, `padding`, `cornerRadius` → standard CSS properties
## CSS Guidelines
- Use existing classes from `admin.css` wherever possible
- Add new classes to admin.css only if needed, following BEM naming
- Topbar page title/subtitle: use `admin-topbar__title` and `admin-topbar__subtitle`
- For the Dashboard page, include the topbar directly in the page content area
## Output Files
Create these files:
1. `Pages/Admin/Dashboard.razor` (with KPI cards, store overview, alerts, activity feed)
2. `Pages/Admin/Store/StoreList.razor` (data table with search, filters)
3. `Pages/Admin/Store/StoreDetail.razor` (detail view with stats)
4. `Pages/Admin/Store/StoreCreate.razor` (form with validation)
5. `Pages/Admin/Store/StoreSettings.razor` (settings form)
6. Update `wwwroot/locales/vi-VN.json` — add Admin_Dashboard_*, Admin_Store_* keys
7. Update `wwwroot/locales/en-US.json` — matching English translations

View File

@@ -0,0 +1,52 @@
# Sub-Agent 1B: Admin Staff + Products
## Objective
Convert 11 Pencil design files into Blazor Server pages for Staff and Products modules.
## Tech Stack
Same as Agent 1A — Blazor Server, MudBlazor, admin.css tokens, Lucide icons, IStringLocalizer.
- Inherit from `AdminBase`, use `@layout AdminLayout`
## Design Files (Input)
Read from `pencil-design/src/pages/tPOS/admin/`:
### Staff (6 files)
1. `staff-directory.pen``Pages/Admin/Staff/StaffDirectory.razor`
2. `staff-create.pen``Pages/Admin/Staff/StaffCreate.razor`
3. `staff-schedule.pen``Pages/Admin/Staff/StaffSchedule.razor`
4. `attendance-dashboard.pen``Pages/Admin/Staff/AttendanceDashboard.razor`
5. `payroll-commission.pen``Pages/Admin/Staff/PayrollCommission.razor`
6. `role-permissions.pen``Pages/Admin/Staff/RolePermissions.razor`
### Products (5 files)
7. `product-catalog.pen``Pages/Admin/Products/ProductCatalog.razor`
8. `product-create.pen``Pages/Admin/Products/ProductCreate.razor`
9. `menu-builder.pen``Pages/Admin/Products/MenuBuilder.razor`
10. `modifier-groups.pen``Pages/Admin/Products/ModifierGroups.razor`
11. `pricing-rules.pen``Pages/Admin/Products/PricingRules.razor`
## Route Definitions
```
@page "/admin/staff" → StaffDirectory
@page "/admin/staff/create" → StaffCreate
@page "/admin/staff/schedule" → StaffSchedule
@page "/admin/staff/attendance" → AttendanceDashboard
@page "/admin/staff/payroll" → PayrollCommission
@page "/admin/roles" → RolePermissions
@page "/admin/products" → ProductCatalog
@page "/admin/products/create" → ProductCreate
@page "/admin/menu" → MenuBuilder
@page "/admin/products/modifiers" → ModifierGroups
@page "/admin/products/pricing" → PricingRules
```
## Pattern Reference
Same as Agent 1A — see `Pages/Auth/LoginCustomer.razor` for pattern.
## How to Read .pen Files
Same as Agent 1A — JSON with frames, text, icon_font, variables.
## Output Files
- 11 `.razor` files in respective folders
- Update locale files with Admin_Staff_* and Admin_Products_* keys
- Any new CSS classes added to `admin.css`

View File

@@ -0,0 +1,120 @@
# Sub-Agent 2A: Admin Finance + Inventory
## Objective
Convert 8 Pencil design files into Blazor Server pages for Finance and Inventory modules.
## Tech Stack
Same as Agent 1A — Blazor Server, MudBlazor, admin.css tokens, Lucide icons, IStringLocalizer.
- Inherit from `AdminBase`, use `@layout AdminLayout`
## Design Files (Input)
Read from `pencil-design/src/pages/tPOS/admin/`:
### Inventory (4 files)
1. `inventory-dashboard.pen``Pages/Admin/Inventory/InventoryDashboard.razor`
2. `purchase-orders.pen``Pages/Admin/Inventory/PurchaseOrders.razor`
3. `stock-transfer.pen``Pages/Admin/Inventory/StockTransfer.razor`
4. `supplier-management.pen``Pages/Admin/Inventory/SupplierManagement.razor`
### Finance (4 files)
5. `financial-overview.pen``Pages/Admin/Finance/FinancialOverview.razor`
6. `revenue-analytics.pen``Pages/Admin/Finance/RevenueAnalytics.razor`
7. `expense-management.pen``Pages/Admin/Finance/ExpenseManagement.razor`
8. `tax-configuration.pen``Pages/Admin/Finance/TaxConfiguration.razor`
## Route Definitions
```
@page "/admin/inventory" → InventoryDashboard
@page "/admin/inventory/orders" → PurchaseOrders
@page "/admin/inventory/transfers" → StockTransfer
@page "/admin/inventory/suppliers" → SupplierManagement
@page "/admin/finance" → FinancialOverview
@page "/admin/finance/revenue" → RevenueAnalytics
@page "/admin/finance/expenses" → ExpenseManagement
@page "/admin/finance/tax" → TaxConfiguration
```
## Pattern Reference & .pen Reading
Same as Agent 1A/1B.
## Output Files
- 8 `.razor` files
- Update locale files with Admin_Inventory_* and Admin_Finance_* keys
- New CSS classes in `admin.css` if needed
---
# Sub-Agent 2B: Admin Customer + System
## Objective
Convert 7 Pencil design files into Blazor Server pages.
## Design Files (Input)
Read from `pencil-design/src/pages/tPOS/admin/`:
### Customer (3 files)
1. `customer-database.pen``Pages/Admin/Customers/CustomerDatabase.razor`
2. `customer-feedback.pen``Pages/Admin/Customers/CustomerFeedback.razor`
3. `loyalty-program.pen``Pages/Admin/Customers/LoyaltyProgram.razor`
### System (4 files)
4. `device-management.pen``Pages/Admin/System/DeviceManagement.razor`
5. `integration-hub.pen``Pages/Admin/System/IntegrationHub.razor`
6. `notification-center.pen``Pages/Admin/System/NotificationCenter.razor`
7. `audit-log.pen``Pages/Admin/System/AuditLog.razor`
## Route Definitions
```
@page "/admin/customers" → CustomerDatabase
@page "/admin/customers/feedback" → CustomerFeedback
@page "/admin/loyalty" → LoyaltyProgram
@page "/admin/devices" → DeviceManagement
@page "/admin/integrations" → IntegrationHub
@page "/admin/notifications" → NotificationCenter
@page "/admin/audit" → AuditLog
```
---
# Sub-Agent 2C: POS Screens + Payment
## Objective
Convert 33 Pencil design files into Blazor Server pages for POS module.
## Tech Stack
- Inherit from `PosBase` at `Pages/Pos/PosBase.cs`
- Use `@layout PosLayout` (at `Layout/PosLayout.razor`)
- Styling: `pos.css` tokens with BEM `.pos-{component}--{variant}`
- All other conventions same as Admin agents
## Design Files (Input)
Read from `pencil-design/src/pages/tPOS/pos/shared/`:
### Screens (22 files)
`screens/login.pen`, `screens/quick-sale.pen`, `screens/settings.pen`,
`screens/shift-management.pen`, `screens/clock-in-out.pen`,
`screens/device-setup.pen`, `screens/offline-mode.pen`,
`screens/password-reset.pen`, `screens/pin-entry.pen`,
`screens/pending-orders.pen`, `screens/promo-active.pen`,
`screens/staff-list.pen`, `screens/staff-schedule.pen`,
`screens/stock-count.pen`, `screens/theme-customization.pen`,
`screens/training-mode.pen`, `screens/accessibility.pen`,
`screens/backup-restore.pen`, `screens/biometric-setup.pen`,
`screens/cash-drawer.pen`, `screens/commission-setup.pen`,
`screens/customer-group.pen`
### Payment (11 files)
`payment/method-select.pen`, `payment/cash.pen`, `payment/card.pen`,
`payment/qr.pen`, `payment/bank-transfer.pen`, `payment/gift-card.pen`,
`payment/partial-payment.pen`, `payment/tip-entry.pen`,
`payment/payment-pending.pen`, `payment/receipt.pen`, `payment/success.pen`
## Route Definitions
POS screens: `/pos/{screen-name}` (e.g., `/pos/quick-sale`, `/pos/settings`)
Payment: `/pos/payment/{method}` (e.g., `/pos/payment/cash`, `/pos/payment/card`)
## Output
- 22 POS screen `.razor` files in `Pages/Pos/`
- 11 Payment `.razor` files in `Pages/Pos/Payment/`
- Shared POS components in `Components/Pos/` if reusable patterns emerge
- Locale keys: Pos_*, Pos_Payment_*

View File

@@ -0,0 +1,95 @@
# Sub-Agent 3A: POS Dialogs
## Objective
Convert 47 POS dialog design files into MudDialog components.
## Tech Stack
- **Component type**: MudBlazor `MudDialog` (not full pages)
- **Styling**: `pos.css` tokens
- **Icons**: Lucide via `<i data-lucide="icon-name"></i>`
- **i18n**: `IStringLocalizer<T>` with locale files
- **Location**: `Components/Pos/Dialogs/{DialogName}.razor`
## Design Files (Input)
Read ALL `.pen` files from `pencil-design/src/pages/tPOS/pos/shared/dialogs/`:
barcode-scan, change-calculator, confirmation, coupon-redeem,
customer-add, customer-edit, customer-history, customer-note, customer-search,
data-export, deposit-withdrawal, discount-apply, expense-entry, expiry-warning,
feedback-form, help-support, hold-recall, keyboard-shortcuts,
low-stock-alert, loyalty-reward, loyalty-scan, manager-override,
modifier-select, multi-discount, network-error, open-price,
order-cancel, order-edit, order-reprint, permission-denied, petty-cash,
price-check, printer-error, product-search, quantity-adjust,
role-switch, session-timeout, split-bill, stock-in, stock-out,
stock-transfer, sync-conflict, sync-status, two-factor,
vip-benefits, void-refund, weight-entry
## MudDialog Pattern
```razor
@* EN: Dialog description / VI: Mô tả dialog *@
<MudDialog>
<TitleContent>
<div style="display:flex;align-items:center;gap:10px;">
<i data-lucide="icon-name"></i>
<span>@L["Dialog_Title"]</span>
</div>
</TitleContent>
<DialogContent>
@* Dialog content from .pen design *@
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">@L["Dialog_Cancel"]</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">@L["Dialog_Submit"]</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
private void Cancel() => MudDialog.Cancel();
private void Submit() => MudDialog.Close(DialogResult.Ok(true));
}
```
## Output
- 47 dialog `.razor` files in `Components/Pos/Dialogs/`
- Locale keys: Pos_Dialog_{DialogName}_*
---
# Sub-Agent 3B: POS Reports + Verticals
## Objective
Convert 22 Pencil design files for POS reports and vertical-specific screens.
## Tech Stack
Same POS conventions — inherit `PosBase`, use `@layout PosLayout`, `pos.css`.
## Design Files (Input)
### Reports (8 files) from `pos/shared/reports/`:
`sales-dashboard.pen`, `shift-report.pen`, `tax-report.pen`,
`cash-reconciliation.pen`, `inventory-alert.pen`, `payment-report.pen`,
`staff-performance.pen`, `top-sellers.pen`
### Café (5+1 files) from `pos/cafe/`:
`desktop.pen`, `tablet.pen`, `mobile.pen`,
`barista-queue.pen`, `customer-display.pen`
### Restaurant (3 files) from `pos/restaurant/`:
`desktop.pen`, `tablet.pen`, `mobile.pen`
### Karaoke (3 files) from `pos/karaoke/`:
`desktop.pen`, `tablet.pen`, `mobile.pen`
### Spa (3 files) from `pos/spa/`:
`desktop.pen`, `tablet.pen`, `mobile.pen`
## Route Definitions
Reports: `/pos/reports/{report-type}`
Verticals: `/pos/{vertical}/{view}` (e.g., `/pos/cafe/desktop`)
## Output
- 8 Report pages in `Pages/Pos/Reports/`
- Vertical pages in `Pages/Pos/Cafe/`, `Pages/Pos/Restaurant/`, etc.
- Locale keys: Pos_Report_*, Pos_Cafe_*, Pos_Restaurant_*, etc.

View File

@@ -0,0 +1,35 @@
# Sub-Agent 4: Admin Onboarding Wizard
## Objective
Convert 6 Pencil onboarding files into a multi-step wizard flow.
## Tech Stack
Same Admin conventions — inherit `AdminBase`, use `@layout AdminLayout`, `admin.css`.
## Design Files (Input)
Read from `pencil-design/src/pages/tPOS/admin/`:
1. `onboarding-business.pen` → Step 1: Business info
2. `onboarding-store.pen` → Step 2: Store setup
3. `onboarding-products.pen` → Step 3: Product catalog
4. `onboarding-staff.pen` → Step 4: Staff setup
5. `onboarding-device.pen` → Step 5: Device pairing
6. `onboarding-ready.pen` → Step 6: Ready to go!
## Route Definitions
```
@page "/admin/onboarding" → OnboardingWizard.razor (all steps)
@page "/admin/onboarding/{Step}" → Same component, step parameter
```
## Implementation Notes
- Create a SINGLE `OnboardingWizard.razor` with step navigation
- Use MudStepper or custom stepper component
- Each step renders content based on `{Step}` parameter
- Include progress indicator, back/next buttons
- Final step: celebration + redirect to `/admin`
## Output
- `Pages/Admin/Onboarding/OnboardingWizard.razor`
- `Components/Admin/OnboardingStep.razor` (reusable step wrapper)
- Locale keys: Admin_Onboarding_*
- Wire up the route from the admin layout's sidebar

View File

@@ -0,0 +1,134 @@
@*
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
*@
@inherits LayoutComponentBase
@inject IStringLocalizer<AdminLayout> L
@inject NavigationManager NavigationManager
<MudThemeProvider IsDarkMode="true" Theme="_theme" />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<div class="admin-layout">
@* ═══ SIDEBAR ═══ *@
<aside class="admin-sidebar @(_sidebarOpen ? "admin-sidebar--open" : "")">
@* Logo *@
<div class="admin-sidebar__logo">
<div class="admin-sidebar__logo-icon">G</div>
<div class="admin-sidebar__logo-text">
<span class="admin-sidebar__logo-name">GoodGo Admin</span>
<span class="admin-sidebar__logo-sub">Management Console</span>
</div>
</div>
@* Navigation *@
<nav class="admin-sidebar__nav">
@* TỔNG QUAN *@
<span class="admin-nav-label">@L["Nav_Overview"]</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>
@* CỬA HÀNG *@
<span class="admin-nav-label">@L["Nav_Store"]</span>
<NavLink href="/admin/stores" class="admin-nav-item" ActiveClass="admin-nav-item--active">
<i data-lucide="store"></i>
<span>@L["Nav_StoreManagement"]</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>@L["Nav_Products"]</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>@L["Nav_Staff"]</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>@L["Nav_Inventory"]</span>
</NavLink>
<NavLink href="/admin/devices" class="admin-nav-item admin-nav-item--sub" ActiveClass="admin-nav-item--active">
<i data-lucide="monitor"></i>
<span>@L["Nav_Devices"]</span>
</NavLink>
@* KINH DOANH *@
<span class="admin-nav-label">@L["Nav_Business"]</span>
<NavLink href="/admin/customers" class="admin-nav-item" ActiveClass="admin-nav-item--active">
<i data-lucide="heart"></i>
<span>@L["Nav_Customers"]</span>
</NavLink>
<NavLink href="/admin/reports" class="admin-nav-item" ActiveClass="admin-nav-item--active">
<i data-lucide="bar-chart-2"></i>
<span>@L["Nav_Reports"]</span>
</NavLink>
<NavLink href="/admin/finance" class="admin-nav-item" ActiveClass="admin-nav-item--active">
<i data-lucide="wallet"></i>
<span>@L["Nav_Finance"]</span>
</NavLink>
@* HỆ THỐNG *@
<span class="admin-nav-label">@L["Nav_System"]</span>
<NavLink href="/admin/roles" class="admin-nav-item" ActiveClass="admin-nav-item--active">
<i data-lucide="shield"></i>
<span>@L["Nav_Roles"]</span>
</NavLink>
<NavLink href="/admin/settings" class="admin-nav-item" ActiveClass="admin-nav-item--active">
<i data-lucide="settings"></i>
<span>@L["Nav_Settings"]</span>
</NavLink>
</nav>
@* User profile *@
<div class="admin-sidebar__user">
<div class="admin-user-avatar">VH</div>
<div class="admin-user-info">
<span class="admin-user-name">Velik Ho</span>
<span class="admin-user-role">Owner</span>
</div>
<button @onclick="Logout" title="@L["Nav_Logout"]">
<i data-lucide="log-out"></i>
</button>
</div>
</aside>
@* Mobile overlay *@
@if (_sidebarOpen)
{
<div class="admin-sidebar-overlay" @onclick="CloseSidebar"></div>
}
@* ═══ MAIN AREA ═══ *@
<main class="admin-main">
@Body
</main>
</div>
@code {
private bool _sidebarOpen = false;
private void ToggleSidebar() => _sidebarOpen = !_sidebarOpen;
private void CloseSidebar() => _sidebarOpen = false;
private void Logout() => NavigationManager.NavigateTo("/login");
private MudTheme _theme = new()
{
PaletteDark = new PaletteDark()
{
Primary = "#FF5C00",
PrimaryContrastText = "#FFFFFF",
AppbarBackground = "#1A1A1D",
AppbarText = "#FFFFFF",
Background = "#0A0A0B",
Surface = "#1A1A1D",
TextPrimary = "#FFFFFF",
TextSecondary = "#ADADB0",
ActionDefault = "#FFFFFF",
LinesDefault = "#1F1F23"
}
};
}

View File

@@ -0,0 +1,61 @@
@*
EN: POS terminal layout — Full-screen, status bar + content, touch-friendly.
VI: Layout POS — Toàn màn hình, thanh trạng thái + nội dung, thân thiện cảm ứng.
Design: pencil-design/src/pages/tPOS/pos/cafe/desktop.pen
*@
@inherits LayoutComponentBase
@inject IStringLocalizer<PosLayout> L
@inject NavigationManager NavigationManager
<MudThemeProvider IsDarkMode="true" Theme="_theme" />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<div class="pos-layout">
@* ═══ STATUS BAR ═══ *@
<header class="pos-status-bar">
<div class="pos-status-bar__left">
<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 style="font-size:13px;color:var(--pos-text-secondary);">@DateTime.Now.ToString("HH:mm")</span>
<button class="admin-icon-btn" @onclick="GoToAdmin" title="Admin">
<i data-lucide="settings"></i>
</button>
</div>
</header>
@* ═══ MAIN CONTENT ═══ *@
<div class="pos-main">
@Body
</div>
</div>
@code {
private string StoreName { get; set; } = "Coffee House Q1";
private void GoToAdmin() => NavigationManager.NavigateTo("/admin");
private MudTheme _theme = new()
{
PaletteDark = new PaletteDark()
{
Primary = "#FF5C00",
PrimaryContrastText = "#FFFFFF",
AppbarBackground = "#1A1A1D",
AppbarText = "#FFFFFF",
Background = "#0A0A0B",
Surface = "#1A1A1D",
TextPrimary = "#FFFFFF",
TextSecondary = "#ADADB0",
ActionDefault = "#FFFFFF",
LinesDefault = "#1F1F23"
}
};
}

View File

@@ -0,0 +1,70 @@
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization;
namespace WebClientTpos.Client.Pages.Admin;
/// <summary>
/// EN: Base class for all Admin pages — provides common sidebar state, page title, search.
/// VI: Lớp cơ sở cho tất cả trang Admin — cung cấp trạng thái sidebar, tiêu đề, tìm kiếm.
/// </summary>
public abstract class AdminBase : ComponentBase
{
[Inject] protected NavigationManager NavigationManager { get; set; } = default!;
/// <summary>
/// EN: Page title shown in topbar.
/// VI: Tiêu đề trang hiển thị trên topbar.
/// </summary>
protected string PageTitle { get; set; } = string.Empty;
/// <summary>
/// EN: Page subtitle shown in topbar.
/// VI: Phụ đề trang hiển thị trên topbar.
/// </summary>
protected string PageSubtitle { get; set; } = string.Empty;
/// <summary>
/// EN: Search query.
/// VI: Truy vấn tìm kiếm.
/// </summary>
protected string SearchQuery { get; set; } = string.Empty;
/// <summary>
/// EN: Whether the page is loading data.
/// VI: Trang có đang tải dữ liệu không.
/// </summary>
protected bool IsLoading { get; set; } = false;
/// <summary>
/// EN: Navigate to an admin sub-page.
/// VI: Điều hướng đến trang con admin.
/// </summary>
protected void NavigateTo(string path)
{
NavigationManager.NavigateTo($"/admin/{path}");
}
/// <summary>
/// EN: Format Vietnamese currency.
/// VI: Định dạng tiền tệ VND.
/// </summary>
protected static string FormatCurrency(decimal amount)
{
if (amount >= 1_000_000_000)
return $"{amount / 1_000_000_000:0.#}B";
if (amount >= 1_000_000)
return $"{amount / 1_000_000:0.#}M";
if (amount >= 1_000)
return $"{amount / 1_000:0.#}K";
return amount.ToString("N0");
}
/// <summary>
/// EN: Get today's date formatted in Vietnamese.
/// VI: Lấy ngày hôm nay định dạng tiếng Việt.
/// </summary>
protected static string GetTodayFormatted()
{
return DateTime.Now.ToString("dd/MM/yyyy");
}
}

View File

@@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Components;
namespace WebClientTpos.Client.Pages.Pos;
/// <summary>
/// EN: Base class for all POS pages — shift state, offline detection, cart management.
/// VI: Lớp cơ sở cho tất cả trang POS — trạng thái ca, phát hiện offline, quản lý giỏ hàng.
/// </summary>
public abstract class PosBase : ComponentBase
{
[Inject] protected NavigationManager NavigationManager { get; set; } = default!;
/// <summary>
/// EN: Whether POS is in offline mode.
/// VI: POS có đang ở chế độ offline không.
/// </summary>
protected bool IsOffline { get; set; } = false;
/// <summary>
/// EN: Current shift ID.
/// VI: ID ca làm việc hiện tại.
/// </summary>
protected string? CurrentShiftId { get; set; }
/// <summary>
/// EN: Staff name (logged-in user).
/// VI: Tên nhân viên (người đăng nhập).
/// </summary>
protected string StaffName { get; set; } = string.Empty;
/// <summary>
/// EN: Store name.
/// VI: Tên cửa hàng.
/// </summary>
protected string StoreName { get; set; } = string.Empty;
/// <summary>
/// EN: Whether a shift is currently active.
/// VI: Ca làm việc có đang hoạt động không.
/// </summary>
protected bool HasActiveShift => !string.IsNullOrEmpty(CurrentShiftId);
/// <summary>
/// EN: Format Vietnamese currency for POS display (compact).
/// VI: Định dạng tiền tệ VND cho POS (gọn).
/// </summary>
protected static string FormatPrice(decimal price)
{
return price.ToString("N0") + "₫";
}
/// <summary>
/// EN: Navigate to POS sub-page.
/// VI: Điều hướng đến trang con POS.
/// </summary>
protected void NavigateTo(string path)
{
NavigationManager.NavigateTo($"/pos/{path}");
}
}

View File

@@ -0,0 +1,995 @@
/* ═══════════════════════════════════════════════════════════════════════════════
Admin Pages — CSS Foundation
EN: Styles for all admin back-office pages (Dashboard, Store, Staff, etc.)
VI: Styles cho tất cả trang quản trị (Dashboard, Cửa hàng, Nhân sự, v.v.)
Based on: pencil-design/src/pages/tPOS/admin/ tokens
═══════════════════════════════════════════════════════════════════════════════ */
/* ═════════════════════════════════════════════════════════════════════════
1. ADMIN DESIGN TOKENS
═════════════════════════════════════════════════════════════════════════ */
:root {
/* EN: Core background colors / VI: Màu nền chính */
--admin-bg-page: #0A0A0B;
--admin-bg-elevated: #1A1A1D;
--admin-bg-interactive: #2A2A2E;
/* EN: Text colors / VI: Màu chữ */
--admin-text-primary: #FFFFFF;
--admin-text-secondary: #ADADB0;
--admin-text-tertiary: #8B8B90;
/* EN: Border colors / VI: Màu viền */
--admin-border-default: #2A2A2E;
--admin-border-subtle: #1F1F23;
/* EN: Brand colors / VI: Màu thương hiệu */
--admin-orange-primary: #FF5C00;
--admin-orange-gradient: linear-gradient(135deg, #FF5C00 0%, #FF8A4C 100%);
/* EN: Status colors / VI: Màu trạng thái */
--admin-success: #22C55E;
--admin-warning: #F59E0B;
--admin-danger: #EF4444;
--admin-info: #3B82F6;
--admin-purple: #8B5CF6;
--admin-pink: #EC4899;
/* EN: Sidebar dimensions / VI: Kích thước sidebar */
--admin-sidebar-width: 260px;
--admin-sidebar-collapsed: 72px;
--admin-topbar-height: 64px;
/* EN: Spacing & radius / VI: Khoảng cách & bo góc */
--admin-radius-sm: 8px;
--admin-radius-md: 10px;
--admin-radius-lg: 14px;
--admin-radius-xl: 16px;
--admin-content-padding: 28px;
--admin-gap-sm: 8px;
--admin-gap-md: 12px;
--admin-gap-lg: 20px;
--admin-gap-xl: 24px;
/* EN: Typography / VI: Font chữ */
--admin-font: 'Roboto', 'Inter', sans-serif;
}
/* ═════════════════════════════════════════════════════════════════════════
2. ADMIN LAYOUT — Sidebar + TopBar + Content
═════════════════════════════════════════════════════════════════════════ */
/* EN: Full-page admin wrapper / VI: Container toàn trang admin */
.admin-layout {
display: flex;
width: 100%;
min-height: 100vh;
background-color: var(--admin-bg-page);
font-family: var(--admin-font);
color: var(--admin-text-primary);
}
/* ═════════════════════════════════════════
2a. SIDEBAR
═════════════════════════════════════════ */
/* EN: Left sidebar / VI: Sidebar bên trái */
.admin-sidebar {
width: var(--admin-sidebar-width);
min-width: var(--admin-sidebar-width);
height: 100vh;
position: sticky;
top: 0;
display: flex;
flex-direction: column;
background-color: var(--admin-bg-elevated);
border-right: 1px solid var(--admin-border-subtle);
overflow-y: auto;
z-index: 50;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* EN: Collapsed sidebar / VI: Sidebar thu gọn */
.admin-sidebar--collapsed {
width: var(--admin-sidebar-collapsed);
min-width: var(--admin-sidebar-collapsed);
}
/* EN: Logo area / VI: Vùng logo */
.admin-sidebar__logo {
display: flex;
align-items: center;
gap: 12px;
padding: 24px;
border-bottom: 1px solid var(--admin-border-subtle);
}
.admin-sidebar__logo-icon {
width: 40px;
height: 40px;
min-width: 40px;
background: var(--admin-orange-gradient);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #FFFFFF;
font-size: 20px;
font-weight: 800;
}
.admin-sidebar__logo-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.admin-sidebar__logo-name {
font-size: 16px;
font-weight: 700;
color: var(--admin-text-primary);
}
.admin-sidebar__logo-sub {
font-size: 11px;
color: var(--admin-text-tertiary);
}
/* EN: Navigation area / VI: Vùng điều hướng */
.admin-sidebar__nav {
flex: 1;
padding: 16px 12px;
display: flex;
flex-direction: column;
gap: 4px;
overflow-y: auto;
}
/* EN: Navigation section label / VI: Nhãn phần điều hướng */
.admin-nav-label {
font-size: 10px;
font-weight: 700;
color: var(--admin-text-tertiary);
padding: 16px 12px 8px 12px;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.admin-nav-label:first-child {
padding-top: 0;
}
/* EN: Navigation item / VI: Mục điều hướng */
.admin-nav-item {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
height: 44px;
padding: 0 12px;
border-radius: var(--admin-radius-md);
border: none;
background: transparent;
cursor: pointer;
text-decoration: none;
transition: all 0.2s ease;
}
.admin-nav-item i,
.admin-nav-item svg {
width: 20px;
height: 20px;
color: var(--admin-text-secondary);
flex-shrink: 0;
}
.admin-nav-item span {
font-size: 14px;
font-weight: 500;
color: var(--admin-text-secondary);
white-space: nowrap;
}
.admin-nav-item:hover {
background-color: var(--admin-bg-interactive);
}
.admin-nav-item:hover i,
.admin-nav-item:hover span {
color: var(--admin-text-primary);
}
/* EN: Active nav item / VI: Mục điều hướng đang chọn */
.admin-nav-item--active {
background-color: var(--admin-orange-primary);
}
.admin-nav-item--active i,
.admin-nav-item--active svg {
color: #FFFFFF;
}
.admin-nav-item--active span {
color: #FFFFFF;
font-weight: 600;
}
.admin-nav-item--active:hover {
background-color: var(--admin-orange-primary);
opacity: 0.9;
}
/* EN: Sub-nav item (indented) / VI: Mục con (thụt vào) */
.admin-nav-item--sub {
padding-left: 36px;
}
.admin-nav-item--sub i,
.admin-nav-item--sub svg {
width: 18px;
height: 18px;
color: var(--admin-text-tertiary);
}
.admin-nav-item--sub span {
font-size: 13px;
color: var(--admin-text-tertiary);
}
/* EN: Nav badge / VI: Huy hiệu điều hướng */
.admin-nav-badge {
width: 22px;
height: 22px;
border-radius: 100px;
background-color: var(--admin-bg-interactive);
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
color: var(--admin-text-tertiary);
margin-left: auto;
}
/* EN: Sidebar user profile / VI: Profile người dùng sidebar */
.admin-sidebar__user {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-top: 1px solid var(--admin-border-subtle);
}
.admin-user-avatar {
width: 36px;
height: 36px;
min-width: 36px;
border-radius: 100px;
background-color: var(--admin-info);
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
color: #FFFFFF;
}
.admin-user-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.admin-user-name {
font-size: 13px;
font-weight: 600;
color: var(--admin-text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.admin-user-role {
font-size: 11px;
color: var(--admin-text-tertiary);
}
.admin-sidebar__user button {
background: none;
border: none;
cursor: pointer;
color: var(--admin-text-tertiary);
padding: 4px;
display: flex;
align-items: center;
transition: color 0.2s ease;
}
.admin-sidebar__user button:hover {
color: var(--admin-text-primary);
}
/* ═════════════════════════════════════════
2b. MAIN AREA (TopBar + Content)
═════════════════════════════════════════ */
/* EN: Main content area / VI: Vùng nội dung chính */
.admin-main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
/* EN: Top bar / VI: Thanh trên */
.admin-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 32px;
background-color: var(--admin-bg-elevated);
border-bottom: 1px solid var(--admin-border-subtle);
min-height: var(--admin-topbar-height);
}
.admin-topbar__left {
display: flex;
flex-direction: column;
gap: 4px;
}
.admin-topbar__title {
font-size: 22px;
font-weight: 700;
color: var(--admin-text-primary);
margin: 0;
}
.admin-topbar__subtitle {
font-size: 13px;
color: var(--admin-text-tertiary);
margin: 0;
}
.admin-topbar__right {
display: flex;
align-items: center;
gap: 12px;
}
/* EN: Search box in topbar / VI: Ô tìm kiếm */
.admin-search {
width: 260px;
height: 40px;
background-color: var(--admin-bg-interactive);
border-radius: var(--admin-radius-md);
border: none;
padding: 0 14px;
display: flex;
align-items: center;
gap: 10px;
}
.admin-search input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--admin-text-primary);
font-size: 14px;
font-family: var(--admin-font);
}
.admin-search input::placeholder {
color: var(--admin-text-tertiary);
}
.admin-search i {
width: 18px;
height: 18px;
color: var(--admin-text-tertiary);
}
/* EN: Icon button (notifications, etc.) / VI: Nút icon */
.admin-icon-btn {
width: 40px;
height: 40px;
background-color: var(--admin-bg-interactive);
border-radius: var(--admin-radius-md);
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
transition: background-color 0.2s ease;
}
.admin-icon-btn i {
width: 20px;
height: 20px;
color: var(--admin-text-secondary);
}
.admin-icon-btn:hover {
background-color: var(--admin-border-default);
}
/* EN: Notification dot / VI: Chấm thông báo */
.admin-icon-btn__dot {
position: absolute;
top: 8px;
right: 8px;
width: 8px;
height: 8px;
background-color: var(--admin-danger);
border-radius: 100px;
}
/* EN: Primary CTA button / VI: Nút CTA chính */
.admin-btn-primary {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background-color: var(--admin-orange-primary);
color: #FFFFFF;
border: none;
border-radius: var(--admin-radius-md);
font-size: 13px;
font-weight: 600;
font-family: var(--admin-font);
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.admin-btn-primary i {
width: 18px;
height: 18px;
}
.admin-btn-primary:hover {
background-color: #E05200;
transform: translateY(-1px);
}
/* EN: Content area / VI: Vùng nội dung */
.admin-content {
flex: 1;
padding: var(--admin-content-padding);
overflow-y: auto;
}
/* ═════════════════════════════════════════════════════════════════════════
3. SHARED ADMIN COMPONENTS
═════════════════════════════════════════════════════════════════════════ */
/* EN: KPI stat card / VI: Thẻ KPI thống kê */
.admin-kpi-row {
display: flex;
gap: var(--admin-gap-lg);
width: 100%;
}
.admin-kpi-card {
flex: 1;
background-color: var(--admin-bg-elevated);
border-radius: var(--admin-radius-xl);
padding: 20px;
display: flex;
flex-direction: column;
gap: 14px;
}
.admin-kpi-card__header {
display: flex;
align-items: center;
justify-content: space-between;
}
.admin-kpi-card__icon {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.admin-kpi-card__icon i {
width: 22px;
height: 22px;
}
.admin-kpi-card__badge {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
}
.admin-kpi-card__badge i {
width: 12px;
height: 12px;
}
/* EN: KPI badge variants / VI: Biến thể badge KPI */
.admin-kpi-card__badge--up {
background-color: rgba(34, 197, 94, 0.125);
color: var(--admin-success);
}
.admin-kpi-card__badge--down {
background-color: rgba(239, 68, 68, 0.125);
color: var(--admin-danger);
}
.admin-kpi-card__value {
font-size: 28px;
font-weight: 700;
color: var(--admin-text-primary);
}
.admin-kpi-card__label {
font-size: 13px;
color: var(--admin-text-tertiary);
}
/* EN: Panel/card with header / VI: Panel/thẻ có header */
.admin-panel {
background-color: var(--admin-bg-elevated);
border-radius: var(--admin-radius-xl);
display: flex;
flex-direction: column;
overflow: hidden;
}
.admin-panel__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--admin-border-subtle);
}
.admin-panel__title {
display: flex;
align-items: center;
gap: 10px;
font-size: 15px;
font-weight: 600;
color: var(--admin-text-primary);
margin: 0;
}
.admin-panel__title i {
width: 20px;
height: 20px;
}
.admin-panel__action {
font-size: 12px;
font-weight: 600;
color: var(--admin-orange-primary);
background: none;
border: none;
cursor: pointer;
text-decoration: none;
transition: opacity 0.2s ease;
}
.admin-panel__action:hover {
opacity: 0.8;
}
.admin-panel__body {
padding: 16px;
flex: 1;
overflow-y: auto;
}
/* EN: Alert items / VI: Mục cảnh báo */
.admin-alert-list {
display: flex;
flex-direction: column;
gap: var(--admin-gap-sm);
}
.admin-alert-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: var(--admin-radius-md);
}
.admin-alert-item--danger {
background-color: rgba(239, 68, 68, 0.08);
}
.admin-alert-item--warning {
background-color: rgba(245, 158, 11, 0.08);
}
.admin-alert-item--info {
background-color: rgba(59, 130, 246, 0.08);
}
.admin-alert-item i {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.admin-alert-item__text {
display: flex;
flex-direction: column;
gap: 2px;
}
.admin-alert-item__title {
font-size: 12px;
font-weight: 600;
color: var(--admin-text-primary);
}
.admin-alert-item__sub {
font-size: 10px;
color: var(--admin-text-tertiary);
}
/* EN: Activity feed / VI: Feed hoạt động */
.admin-activity-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.admin-activity-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
}
.admin-activity-dot {
width: 8px;
height: 8px;
min-width: 8px;
border-radius: 100px;
}
.admin-activity-item__text {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.admin-activity-item__title {
font-size: 12px;
color: var(--admin-text-primary);
}
.admin-activity-item__time {
font-size: 10px;
color: var(--admin-text-tertiary);
}
/* EN: Store card / VI: Thẻ cửa hàng */
.admin-store-card {
background-color: var(--admin-bg-interactive);
border-radius: var(--admin-radius-lg);
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.admin-store-card__top {
display: flex;
align-items: center;
justify-content: space-between;
}
.admin-store-card__info {
display: flex;
align-items: center;
gap: 12px;
}
.admin-store-card__avatar {
width: 40px;
height: 40px;
border-radius: var(--admin-radius-md);
display: flex;
align-items: center;
justify-content: center;
}
.admin-store-card__avatar i {
width: 20px;
height: 20px;
}
.admin-store-card__name {
font-size: 14px;
font-weight: 600;
color: var(--admin-text-primary);
}
.admin-store-card__type {
font-size: 11px;
color: var(--admin-text-tertiary);
}
/* EN: Status badge / VI: Badge trạng thái */
.admin-status-badge {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
}
.admin-status-badge__dot {
width: 6px;
height: 6px;
border-radius: 100px;
}
.admin-status-badge--online {
background-color: rgba(34, 197, 94, 0.125);
color: var(--admin-success);
}
.admin-status-badge--online .admin-status-badge__dot {
background-color: var(--admin-success);
}
.admin-status-badge--setup {
background-color: rgba(245, 158, 11, 0.125);
color: var(--admin-warning);
}
.admin-status-badge--setup .admin-status-badge__dot {
background-color: var(--admin-warning);
}
.admin-status-badge--offline {
background-color: rgba(239, 68, 68, 0.125);
color: var(--admin-danger);
}
.admin-status-badge--offline .admin-status-badge__dot {
background-color: var(--admin-danger);
}
/* EN: Store stats row / VI: Hàng thống kê cửa hàng */
.admin-store-card__stats {
display: flex;
gap: 16px;
}
.admin-store-stat {
flex: 1;
background-color: var(--admin-bg-page);
border-radius: var(--admin-radius-sm);
padding: 8px 12px;
display: flex;
flex-direction: column;
gap: 4px;
align-items: center;
text-align: center;
}
.admin-store-stat__value {
font-size: 14px;
font-weight: 700;
color: var(--admin-text-primary);
}
.admin-store-stat__label {
font-size: 10px;
color: var(--admin-text-tertiary);
}
/* EN: CTA button in store card / VI: Nút CTA trong thẻ cửa hàng */
.admin-store-card__cta {
width: 100%;
height: 36px;
border-radius: var(--admin-radius-sm);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.2s ease;
}
.admin-store-card__cta--warning {
background-color: rgba(245, 158, 11, 0.125);
color: var(--admin-warning);
border: 1px solid rgba(245, 158, 11, 0.25);
}
.admin-store-card__cta--warning:hover {
background-color: rgba(245, 158, 11, 0.2);
}
/* EN: Label/count badge / VI: Badge số */
.admin-badge-count {
width: 22px;
height: 22px;
border-radius: 100px;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
}
.admin-badge-count--danger {
background-color: var(--admin-danger);
color: #FFFFFF;
}
/* EN: General secondary button / VI: Nút phụ chung */
.admin-btn-secondary {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background-color: var(--admin-bg-interactive);
color: var(--admin-text-secondary);
border: 1px solid var(--admin-border-default);
border-radius: var(--admin-radius-md);
font-size: 13px;
font-weight: 500;
font-family: var(--admin-font);
cursor: pointer;
transition: all 0.2s ease;
}
.admin-btn-secondary:hover {
background-color: var(--admin-border-default);
color: var(--admin-text-primary);
}
/* ═════════════════════════════════════════════════════════════════════════
4. DATA TABLE COMPONENTS
═════════════════════════════════════════════════════════════════════════ */
/* EN: Admin data table / VI: Bảng dữ liệu admin */
.admin-table {
width: 100%;
border-collapse: collapse;
}
.admin-table th {
padding: 12px 16px;
text-align: left;
font-size: 11px;
font-weight: 600;
color: var(--admin-text-tertiary);
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid var(--admin-border-subtle);
}
.admin-table td {
padding: 14px 16px;
font-size: 13px;
color: var(--admin-text-primary);
border-bottom: 1px solid var(--admin-border-subtle);
}
.admin-table tr:hover td {
background-color: var(--admin-bg-interactive);
}
/* EN: Page header / VI: Header trang */
.admin-page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--admin-gap-xl);
}
.admin-page-header__left {
display: flex;
flex-direction: column;
gap: 4px;
}
.admin-page-header__title {
font-size: 22px;
font-weight: 700;
color: var(--admin-text-primary);
margin: 0;
}
.admin-page-header__subtitle {
font-size: 13px;
color: var(--admin-text-tertiary);
margin: 0;
}
.admin-page-header__actions {
display: flex;
align-items: center;
gap: 12px;
}
/* ═════════════════════════════════════════════════════════════════════════
5. RESPONSIVE (Tablet / Mobile)
═════════════════════════════════════════════════════════════════════════ */
@media (max-width: 1024px) {
.admin-sidebar {
position: fixed;
left: -260px;
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 200;
}
.admin-sidebar--open {
left: 0;
}
.admin-topbar {
padding: 12px 20px;
}
.admin-content {
padding: 20px;
}
.admin-kpi-row {
flex-wrap: wrap;
}
.admin-kpi-card {
min-width: calc(50% - 10px);
}
}
@media (max-width: 640px) {
.admin-kpi-card {
min-width: 100%;
}
.admin-content {
padding: 16px;
}
.admin-topbar {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.admin-search {
width: 100%;
}
.admin-store-card__stats {
flex-wrap: wrap;
}
.admin-store-stat {
min-width: calc(50% - 8px);
}
}

View File

@@ -0,0 +1,348 @@
/* ═══════════════════════════════════════════════════════════════════════════════
POS Terminal — CSS Foundation
EN: Styles for POS front-of-house pages (Quick Sale, Payment, Reports)
VI: Styles cho trang POS tuyến trước (Bán nhanh, Thanh toán, Báo cáo)
Based on: pencil-design/src/pages/tPOS/pos/ tokens
═══════════════════════════════════════════════════════════════════════════════ */
/* ═════════════════════════════════════════════════════════════════════════
1. POS DESIGN TOKENS
═════════════════════════════════════════════════════════════════════════ */
:root {
/* EN: POS background colors / VI: Màu nền POS */
--pos-bg-page: #0A0A0B;
--pos-bg-elevated: #1A1A1D;
--pos-bg-interactive: #2A2A2E;
/* EN: POS text colors / VI: Màu chữ POS */
--pos-text-primary: #FFFFFF;
--pos-text-secondary: #ADADB0;
--pos-text-tertiary: #8B8B90;
/* EN: POS border colors / VI: Màu viền POS */
--pos-border-default: #2A2A2E;
--pos-border-subtle: #1F1F23;
/* EN: POS brand & status colors / VI: Màu thương hiệu & trạng thái POS */
--pos-orange-primary: #FF5C00;
--pos-success: #22C55E;
--pos-warning: #F59E0B;
--pos-danger: #EF4444;
--pos-info: #3B82F6;
/* EN: POS spacing / VI: Khoảng cách POS */
--pos-status-bar-height: 48px;
--pos-footer-height: 64px;
--pos-product-grid-gap: 12px;
--pos-radius: 12px;
/* EN: POS font / VI: Font POS */
--pos-font: 'Roboto', 'Inter', sans-serif;
}
/* ═════════════════════════════════════════════════════════════════════════
2. POS LAYOUT — Full-screen terminal
═════════════════════════════════════════════════════════════════════════ */
.pos-layout {
display: flex;
flex-direction: column;
width: 100%;
height: 100vh;
background-color: var(--pos-bg-page);
font-family: var(--pos-font);
color: var(--pos-text-primary);
overflow: hidden;
}
/* EN: POS status bar / VI: Thanh trạng thái POS */
.pos-status-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: var(--pos-status-bar-height);
padding: 0 16px;
background-color: var(--pos-bg-elevated);
border-bottom: 1px solid var(--pos-border-subtle);
flex-shrink: 0;
}
.pos-status-bar__left {
display: flex;
align-items: center;
gap: 12px;
}
.pos-status-bar__right {
display: flex;
align-items: center;
gap: 8px;
}
.pos-status-bar__logo {
font-size: 15px;
font-weight: 700;
color: var(--pos-orange-primary);
}
.pos-status-bar__store {
font-size: 13px;
color: var(--pos-text-secondary);
}
.pos-status-bar__indicator {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
}
.pos-status-bar__indicator--online {
background-color: rgba(34, 197, 94, 0.125);
color: var(--pos-success);
}
.pos-status-bar__indicator--offline {
background-color: rgba(239, 68, 68, 0.125);
color: var(--pos-danger);
}
/* EN: POS main content area / VI: Vùng nội dung chính POS */
.pos-main {
flex: 1;
display: flex;
overflow: hidden;
}
/* EN: POS product panel (left) / VI: Panel sản phẩm (trái) */
.pos-product-panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* EN: POS cart panel (right) / VI: Panel giỏ hàng (phải) */
.pos-cart-panel {
width: 380px;
min-width: 380px;
display: flex;
flex-direction: column;
background-color: var(--pos-bg-elevated);
border-left: 1px solid var(--pos-border-subtle);
}
/* EN: Category tabs / VI: Tab danh mục */
.pos-category-tabs {
display: flex;
gap: 8px;
padding: 12px 16px;
overflow-x: auto;
flex-shrink: 0;
}
.pos-category-tab {
padding: 8px 16px;
border-radius: var(--pos-radius);
font-size: 13px;
font-weight: 500;
white-space: nowrap;
border: none;
cursor: pointer;
transition: all 0.2s ease;
background-color: var(--pos-bg-interactive);
color: var(--pos-text-secondary);
}
.pos-category-tab--active {
background-color: var(--pos-orange-primary);
color: #FFFFFF;
font-weight: 600;
}
/* EN: Product grid / VI: Lưới sản phẩm */
.pos-product-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: var(--pos-product-grid-gap);
padding: 16px;
overflow-y: auto;
align-content: start;
}
.pos-product-card {
background-color: var(--pos-bg-elevated);
border-radius: var(--pos-radius);
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.pos-product-card:hover {
border-color: var(--pos-orange-primary);
transform: translateY(-2px);
}
.pos-product-card__image {
width: 100%;
aspect-ratio: 1;
border-radius: 8px;
background-color: var(--pos-bg-interactive);
overflow: hidden;
}
.pos-product-card__name {
font-size: 13px;
font-weight: 600;
color: var(--pos-text-primary);
}
.pos-product-card__price {
font-size: 14px;
font-weight: 700;
color: var(--pos-orange-primary);
}
/* EN: Cart items / VI: Mục giỏ hàng */
.pos-cart-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid var(--pos-border-subtle);
}
.pos-cart-header__title {
font-size: 15px;
font-weight: 600;
}
.pos-cart-items {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.pos-cart-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
border-radius: var(--pos-radius);
transition: background-color 0.2s ease;
}
.pos-cart-item:hover {
background-color: var(--pos-bg-interactive);
}
.pos-cart-item__info {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.pos-cart-item__name {
font-size: 13px;
font-weight: 500;
}
.pos-cart-item__price {
font-size: 12px;
color: var(--pos-text-tertiary);
}
.pos-cart-item__qty {
display: flex;
align-items: center;
gap: 8px;
}
.pos-cart-item__qty button {
width: 28px;
height: 28px;
border-radius: 8px;
border: 1px solid var(--pos-border-default);
background: transparent;
color: var(--pos-text-primary);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
/* EN: Cart footer / VI: Footer giỏ hàng */
.pos-cart-footer {
padding: 16px;
border-top: 1px solid var(--pos-border-subtle);
}
.pos-cart-total {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.pos-cart-total__label {
font-size: 14px;
color: var(--pos-text-secondary);
}
.pos-cart-total__value {
font-size: 22px;
font-weight: 700;
color: var(--pos-orange-primary);
}
.pos-btn-checkout {
width: 100%;
height: 48px;
background-color: var(--pos-orange-primary);
color: #FFFFFF;
border: none;
border-radius: var(--pos-radius);
font-size: 15px;
font-weight: 600;
font-family: var(--pos-font);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s ease;
}
.pos-btn-checkout:hover {
background-color: #E05200;
}
/* EN: POS dialog overlay / VI: Overlay dialog POS */
.pos-dialog-overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 300;
}
.pos-dialog {
background-color: var(--pos-bg-elevated);
border-radius: 16px;
width: 90%;
max-width: 480px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.4);
}

View File

@@ -31,6 +31,8 @@
<!-- VI: CSS tùy chỉnh -->
<link rel="stylesheet" href="/css/app.css" />
<link rel="stylesheet" href="/css/auth.css" />
<link rel="stylesheet" href="/css/admin.css" />
<link rel="stylesheet" href="/css/pos.css" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link href="/WebClientTpos.Client.styles.css" rel="stylesheet" />
</head>