From e5194350188b907781448ca524213fba93ce00e7 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Thu, 12 Feb 2026 09:58:01 +0700 Subject: [PATCH] feat: Implement initial POS layout and base components, along with admin base components and agent prompts for admin modules. --- .../agent-1a-admin-dashboard-stores.md | 67 ++ .../prompts/agent-1b-admin-staff-products.md | 52 + .agent/prompts/agent-2-phase2-combined.md | 120 +++ .agent/prompts/agent-3-phase3-combined.md | 95 ++ .agent/prompts/agent-4-onboarding.md | 35 + .../Layout/AdminLayout.razor | 134 +++ .../Layout/PosLayout.razor | 61 ++ .../Pages/Admin/AdminBase.cs | 70 ++ .../WebClientTpos.Client/Pages/Pos/PosBase.cs | 60 ++ .../wwwroot/css/admin.css | 995 ++++++++++++++++++ .../WebClientTpos.Client/wwwroot/css/pos.css | 348 ++++++ .../WebClientTpos.Client/wwwroot/index.html | 2 + 12 files changed, 2039 insertions(+) create mode 100644 .agent/prompts/agent-1a-admin-dashboard-stores.md create mode 100644 .agent/prompts/agent-1b-admin-staff-products.md create mode 100644 .agent/prompts/agent-2-phase2-combined.md create mode 100644 .agent/prompts/agent-3-phase3-combined.md create mode 100644 .agent/prompts/agent-4-onboarding.md create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AdminLayout.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/PosLayout.razor create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/AdminBase.cs create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/PosBase.cs create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/admin.css create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/pos.css diff --git a/.agent/prompts/agent-1a-admin-dashboard-stores.md b/.agent/prompts/agent-1a-admin-dashboard-stores.md new file mode 100644 index 00000000..33e81f81 --- /dev/null +++ b/.agent/prompts/agent-1a-admin-dashboard-stores.md @@ -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 `` (add `lucide.createIcons()` in OnAfterRenderAsync) +- **i18n**: `IStringLocalizer` 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"` → `
` +- `type: "text"` with `content` → text node +- `type: "icon_font"` with `iconFontName` → `` +- `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 diff --git a/.agent/prompts/agent-1b-admin-staff-products.md b/.agent/prompts/agent-1b-admin-staff-products.md new file mode 100644 index 00000000..4bc763af --- /dev/null +++ b/.agent/prompts/agent-1b-admin-staff-products.md @@ -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` diff --git a/.agent/prompts/agent-2-phase2-combined.md b/.agent/prompts/agent-2-phase2-combined.md new file mode 100644 index 00000000..586d79f5 --- /dev/null +++ b/.agent/prompts/agent-2-phase2-combined.md @@ -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_* diff --git a/.agent/prompts/agent-3-phase3-combined.md b/.agent/prompts/agent-3-phase3-combined.md new file mode 100644 index 00000000..7b8deb2e --- /dev/null +++ b/.agent/prompts/agent-3-phase3-combined.md @@ -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 `` +- **i18n**: `IStringLocalizer` 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 *@ + + + +
+ + @L["Dialog_Title"] +
+
+ + @* Dialog content from .pen design *@ + + + @L["Dialog_Cancel"] + @L["Dialog_Submit"] + +
+ +@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. diff --git a/.agent/prompts/agent-4-onboarding.md b/.agent/prompts/agent-4-onboarding.md new file mode 100644 index 00000000..4b1c0e76 --- /dev/null +++ b/.agent/prompts/agent-4-onboarding.md @@ -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 diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AdminLayout.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AdminLayout.razor new file mode 100644 index 00000000..f21a5447 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AdminLayout.razor @@ -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 L +@inject NavigationManager NavigationManager + + + + + + +
+ @* ═══ SIDEBAR ═══ *@ + + + @* Mobile overlay *@ + @if (_sidebarOpen) + { +
+ } + + @* ═══ MAIN AREA ═══ *@ +
+ @Body +
+
+ +@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" + } + }; +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/PosLayout.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/PosLayout.razor new file mode 100644 index 00000000..2389eac8 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/PosLayout.razor @@ -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 L +@inject NavigationManager NavigationManager + + + + + + +
+ @* ═══ STATUS BAR ═══ *@ +
+
+ + @StoreName +
+
+
+ + Online +
+ @DateTime.Now.ToString("HH:mm") + +
+
+ + @* ═══ MAIN CONTENT ═══ *@ +
+ @Body +
+
+ +@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" + } + }; +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/AdminBase.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/AdminBase.cs new file mode 100644 index 00000000..75c9be8e --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/AdminBase.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Localization; + +namespace WebClientTpos.Client.Pages.Admin; + +/// +/// 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. +/// +public abstract class AdminBase : ComponentBase +{ + [Inject] protected NavigationManager NavigationManager { get; set; } = default!; + + /// + /// EN: Page title shown in topbar. + /// VI: Tiêu đề trang hiển thị trên topbar. + /// + protected string PageTitle { get; set; } = string.Empty; + + /// + /// EN: Page subtitle shown in topbar. + /// VI: Phụ đề trang hiển thị trên topbar. + /// + protected string PageSubtitle { get; set; } = string.Empty; + + /// + /// EN: Search query. + /// VI: Truy vấn tìm kiếm. + /// + protected string SearchQuery { get; set; } = string.Empty; + + /// + /// EN: Whether the page is loading data. + /// VI: Trang có đang tải dữ liệu không. + /// + protected bool IsLoading { get; set; } = false; + + /// + /// EN: Navigate to an admin sub-page. + /// VI: Điều hướng đến trang con admin. + /// + protected void NavigateTo(string path) + { + NavigationManager.NavigateTo($"/admin/{path}"); + } + + /// + /// EN: Format Vietnamese currency. + /// VI: Định dạng tiền tệ VND. + /// + 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"); + } + + /// + /// EN: Get today's date formatted in Vietnamese. + /// VI: Lấy ngày hôm nay định dạng tiếng Việt. + /// + protected static string GetTodayFormatted() + { + return DateTime.Now.ToString("dd/MM/yyyy"); + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/PosBase.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/PosBase.cs new file mode 100644 index 00000000..46ffa340 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/PosBase.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Components; + +namespace WebClientTpos.Client.Pages.Pos; + +/// +/// 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. +/// +public abstract class PosBase : ComponentBase +{ + [Inject] protected NavigationManager NavigationManager { get; set; } = default!; + + /// + /// EN: Whether POS is in offline mode. + /// VI: POS có đang ở chế độ offline không. + /// + protected bool IsOffline { get; set; } = false; + + /// + /// EN: Current shift ID. + /// VI: ID ca làm việc hiện tại. + /// + protected string? CurrentShiftId { get; set; } + + /// + /// EN: Staff name (logged-in user). + /// VI: Tên nhân viên (người đăng nhập). + /// + protected string StaffName { get; set; } = string.Empty; + + /// + /// EN: Store name. + /// VI: Tên cửa hàng. + /// + protected string StoreName { get; set; } = string.Empty; + + /// + /// EN: Whether a shift is currently active. + /// VI: Ca làm việc có đang hoạt động không. + /// + protected bool HasActiveShift => !string.IsNullOrEmpty(CurrentShiftId); + + /// + /// EN: Format Vietnamese currency for POS display (compact). + /// VI: Định dạng tiền tệ VND cho POS (gọn). + /// + protected static string FormatPrice(decimal price) + { + return price.ToString("N0") + "₫"; + } + + /// + /// EN: Navigate to POS sub-page. + /// VI: Điều hướng đến trang con POS. + /// + protected void NavigateTo(string path) + { + NavigationManager.NavigateTo($"/pos/{path}"); + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/admin.css b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/admin.css new file mode 100644 index 00000000..94d61ac0 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/admin.css @@ -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); + } +} \ No newline at end of file diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/pos.css b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/pos.css new file mode 100644 index 00000000..6615f375 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/css/pos.css @@ -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); +} \ No newline at end of file diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/index.html b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/index.html index 2aafffaf..ec69d35e 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/index.html +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/index.html @@ -31,6 +31,8 @@ + +