Files
pos-system/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Retail/RetailMobile.razor

197 lines
8.6 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@*
EN: Retail POS Mobile — Single column, floating cart button, bottom sheet cart.
VI: POS Bán lẻ Mobile — Một cột, nút giỏ hàng nổi, giỏ hàng dạng sheet dưới.
*@
@page "/pos/{ShopId:guid}/retail/mobile"
@layout PosLayout
@inherits PosBase
@inject WebClientTpos.Client.Services.PosDataService DataService
<div style="display:flex;flex-direction:column;width:100%;height:100%;overflow:hidden;">
@* EN: Barcode input / VI: Ô nhập mã vạch *@
<div style="padding:8px 12px;border-bottom:1px solid var(--pos-border-subtle);">
<div style="display:flex;align-items:center;gap:8px;background:var(--pos-bg-interactive);border-radius:var(--pos-radius);padding:0 10px;">
<i data-lucide="scan-barcode" style="width:14px;height:14px;color:var(--pos-text-tertiary);"></i>
<input type="text" @bind="_barcodeInput" @bind:event="oninput" placeholder="Quét mã vạch..."
style="flex:1;background:transparent;border:none;color:var(--pos-text-primary);font-size:13px;
padding:8px 0;outline:none;font-family:inherit;" />
</div>
</div>
@if (_isLoading)
{
<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
Đang tải...
</div>
}
else if (_loadError)
{
<div style="display:flex;align-items:center;justify-content:center;flex:1;color:var(--pos-text-tertiary);">
Không thể tải dữ liệu
</div>
}
else
{
@* EN: Category tabs / VI: Tab danh mục *@
<div class="pos-category-tabs" style="padding:8px 12px;">
@foreach (var cat in _categories)
{
<button class="pos-category-tab @(cat == _selectedCategory ? "pos-category-tab--active" : "")"
style="padding:10px 16px;font-size:14px;"
@onclick="() => _selectedCategory = cat">
@cat
</button>
}
</div>
@* EN: Product grid / VI: Lưới sản phẩm *@
<div class="pos-product-grid" style="grid-template-columns:repeat(2, 1fr);gap:10px;padding:12px;">
@foreach (var product in FilteredProducts)
{
<div class="pos-product-card" style="padding:10px;" @onclick="() => AddToCart(product)">
<div class="pos-product-card__image" style="aspect-ratio:1.2;display:flex;align-items:center;justify-content:center;">
<i data-lucide="@product.Icon" style="width:28px;height:28px;color:var(--pos-text-tertiary);"></i>
</div>
<span class="pos-product-card__name" style="font-size:12px;">@product.Name</span>
<span class="pos-product-card__price" style="font-size:13px;">@FormatPrice(product.Price)</span>
</div>
}
</div>
}
@* EN: Floating cart button / VI: Nút giỏ hàng nổi *@
@if (_cartItems.Any())
{
<button style="position:fixed;bottom:20px;right:20px;width:64px;height:64px;border-radius:50%;background:var(--pos-orange-primary);border:none;color:#fff;font-size:20px;cursor:pointer;box-shadow:0 4px 20px rgba(255,92,0,0.4);display:flex;align-items:center;justify-content:center;z-index:100;"
@onclick="() => _showCart = !_showCart">
<i data-lucide="shopping-cart" style="width:24px;height:24px;"></i>
<span style="position:absolute;top:-4px;right:-4px;background:var(--pos-danger);color:#fff;font-size:11px;font-weight:700;width:22px;height:22px;border-radius:50%;display:flex;align-items:center;justify-content:center;">
@_cartItems.Sum(i => i.Qty)
</span>
</button>
}
@* EN: Bottom sheet cart / VI: Giỏ hàng dạng sheet dưới *@
@if (_showCart)
{
<div class="pos-dialog-overlay" @onclick="() => _showCart = false">
<div style="position:fixed;bottom:0;left:0;right:0;max-height:70vh;background:var(--pos-bg-elevated);border-radius:20px 20px 0 0;display:flex;flex-direction:column;overflow:hidden;"
@onclick:stopPropagation="true">
@* EN: Handle bar / VI: Thanh kéo *@
<div style="padding:12px;display:flex;justify-content:center;">
<div style="width:40px;height:4px;border-radius:2px;background:var(--pos-border-default);"></div>
</div>
<div class="pos-cart-header">
<span class="pos-cart-header__title">Giỏ hàng</span>
<button style="background:none;border:none;color:var(--pos-danger);font-size:13px;cursor:pointer;"
@onclick="() => { _cartItems.Clear(); _showCart = false; }">Xóa</button>
</div>
<div class="pos-cart-items" style="max-height:40vh;">
@foreach (var item in _cartItems)
{
<div class="pos-cart-item">
<div class="pos-cart-item__info">
<span class="pos-cart-item__name">@item.Name</span>
<span class="pos-cart-item__price">@FormatPrice(item.Price)</span>
</div>
<div class="pos-cart-item__qty">
<button @onclick="() => ChangeQty(item, -1)"></button>
<span style="font-size:14px;font-weight:600;">@item.Qty</span>
<button @onclick="() => ChangeQty(item, 1)">+</button>
</div>
</div>
}
</div>
<div class="pos-cart-footer">
<div class="pos-cart-total">
<span class="pos-cart-total__label">Tổng cộng</span>
<span class="pos-cart-total__value">@FormatPrice(CartTotal * 1.1m)</span>
</div>
<button class="pos-btn-checkout" @onclick="Checkout">Thanh toán</button>
</div>
</div>
</div>
}
</div>
@code {
// EN: Loading state / VI: Trạng thái tải
private bool _isLoading = true;
private bool _loadError;
// EN: Categories / VI: Danh mục
private string[] _categories = { "Tất cả" };
private string _selectedCategory = "Tất cả";
private string _barcodeInput = "";
private bool _showCart;
// EN: Product list from API / VI: Danh sách sản phẩm từ API
private List<Product> _products = new();
private readonly List<CartItem> _cartItems = new();
private IEnumerable<Product> FilteredProducts =>
_selectedCategory == "Tất cả" ? _products : _products.Where(p => p.Category == _selectedCategory);
private decimal CartTotal => _cartItems.Sum(i => i.Price * i.Qty);
protected override async Task OnInitializedAsync()
{
try
{
var apiProducts = await DataService.GetProductsAsync(ShopId);
_products = apiProducts.Select(p => new Product(
p.Name,
p.Sku ?? "",
p.Price,
p.Category ?? "Khác",
GetCategoryIcon(p.Category ?? "Khác")
)).ToList();
var cats = _products.Select(p => p.Category).Distinct().ToList();
_categories = new[] { "Tất cả" }.Concat(cats).ToArray();
}
catch
{
_loadError = true;
}
finally
{
_isLoading = false;
}
}
private void AddToCart(Product product)
{
var existing = _cartItems.FirstOrDefault(i => i.Sku == product.Sku);
if (existing != null) existing.Qty++;
else _cartItems.Add(new CartItem(product.Name, product.Sku, product.Price));
}
private void ChangeQty(CartItem item, int delta)
{
item.Qty += delta;
if (item.Qty <= 0) _cartItems.Remove(item);
}
private void Checkout() { }
private static string GetCategoryIcon(string category) => category switch
{
"Thời trang" => "shirt", "Phụ kiện" => "shopping-bag", "Điện tử" => "headphones",
"Gia dụng" => "cooking-pot", "Mỹ phẩm" => "sparkles", _ => "package"
};
// EN: Models / VI: Mô hình dữ liệu
private record Product(string Name, string Sku, decimal Price, string Category, string Icon);
private class CartItem(string name, string sku, decimal price)
{
public string Name { get; set; } = name;
public string Sku { get; set; } = sku;
public decimal Price { get; set; } = price;
public int Qty { get; set; } = 1;
}
}