197 lines
8.6 KiB
Plaintext
197 lines
8.6 KiB
Plaintext
@*
|
||
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;
|
||
}
|
||
}
|