feat: Implement Blazor lifecycle improvements, enhance navigation with browser history, and update EF Core entity configurations for backing fields
This commit is contained in:
@@ -466,9 +466,7 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// EN: Restore auth session from localStorage so API calls have JWT token
|
||||
// VI: Khôi phục session auth từ localStorage để API calls có JWT token
|
||||
await AuthService.TryRestoreSessionAsync();
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -129,6 +129,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var productsTask = DataService.GetProductsAsync(ShopId);
|
||||
@@ -139,6 +141,7 @@
|
||||
var apiCategories = await categoriesTask;
|
||||
|
||||
_products = apiProducts.Select(p => new Product(
|
||||
p.Id,
|
||||
p.Name,
|
||||
p.Price,
|
||||
p.Category ?? "Khác"
|
||||
@@ -165,9 +168,9 @@
|
||||
|
||||
private void AddToCart(Product product)
|
||||
{
|
||||
var existing = _cartItems.FirstOrDefault(i => i.Name == product.Name);
|
||||
var existing = _cartItems.FirstOrDefault(i => i.ProductId == product.Id);
|
||||
if (existing != null) existing.Qty++;
|
||||
else _cartItems.Add(new CartItem(product.Name, product.Price));
|
||||
else _cartItems.Add(new CartItem(product.Id, product.Name, product.Price));
|
||||
}
|
||||
|
||||
private void ChangeQty(CartItem item, int delta)
|
||||
@@ -178,9 +181,10 @@
|
||||
|
||||
private void Checkout() => NavigateTo("cafe/order-customize");
|
||||
|
||||
private record Product(string Name, decimal Price, string Category);
|
||||
private class CartItem(string name, decimal price)
|
||||
private record Product(Guid Id, string Name, decimal Price, string Category);
|
||||
private class CartItem(Guid productId, string name, decimal price)
|
||||
{
|
||||
public Guid ProductId { get; set; } = productId;
|
||||
public string Name { get; set; } = name;
|
||||
public decimal Price { get; set; } = price;
|
||||
public int Qty { get; set; } = 1;
|
||||
|
||||
@@ -108,6 +108,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var productsTask = DataService.GetProductsAsync(ShopId);
|
||||
@@ -118,6 +120,7 @@
|
||||
var apiCategories = await categoriesTask;
|
||||
|
||||
_products = apiProducts.Select(p => new Product(
|
||||
p.Id,
|
||||
p.Name,
|
||||
p.Price,
|
||||
p.Category ?? "Khác"
|
||||
@@ -144,9 +147,9 @@
|
||||
|
||||
private void AddToCart(Product product)
|
||||
{
|
||||
var existing = _cartItems.FirstOrDefault(i => i.Name == product.Name);
|
||||
var existing = _cartItems.FirstOrDefault(i => i.ProductId == product.Id);
|
||||
if (existing != null) existing.Qty++;
|
||||
else _cartItems.Add(new CartItem(product.Name, product.Price));
|
||||
else _cartItems.Add(new CartItem(product.Id, product.Name, product.Price));
|
||||
}
|
||||
|
||||
private void ChangeQty(CartItem item, int delta)
|
||||
@@ -157,9 +160,10 @@
|
||||
|
||||
private void Checkout() => NavigateTo("cafe/order-customize");
|
||||
|
||||
private record Product(string Name, decimal Price, string Category);
|
||||
private class CartItem(string name, decimal price)
|
||||
private record Product(Guid Id, string Name, decimal Price, string Category);
|
||||
private class CartItem(Guid productId, string name, decimal price)
|
||||
{
|
||||
public Guid ProductId { get; set; } = productId;
|
||||
public string Name { get; set; } = name;
|
||||
public decimal Price { get; set; } = price;
|
||||
public int Qty { get; set; } = 1;
|
||||
|
||||
@@ -128,6 +128,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var productsTask = DataService.GetProductsAsync(ShopId);
|
||||
|
||||
@@ -177,6 +177,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var tables = await DataService.GetTablesAsync(ShopId);
|
||||
|
||||
@@ -115,6 +115,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var tables = await DataService.GetTablesAsync(ShopId);
|
||||
|
||||
@@ -164,6 +164,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var tables = await DataService.GetTablesAsync(ShopId);
|
||||
|
||||
@@ -133,6 +133,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var productsTask = DataService.GetProductsAsync(ShopId);
|
||||
|
||||
@@ -124,6 +124,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var tables = await DataService.GetTablesAsync(ShopId);
|
||||
|
||||
@@ -185,6 +185,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var tables = await DataService.GetTablesAsync(ShopId);
|
||||
|
||||
@@ -175,6 +175,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var products = await DataService.GetProductsAsync(ShopId);
|
||||
|
||||
@@ -166,6 +166,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var tables = await DataService.GetTablesAsync(ShopId);
|
||||
|
||||
@@ -129,6 +129,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var apiTables = await DataService.GetTablesAsync(ShopId);
|
||||
|
||||
@@ -109,6 +109,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var apiTables = await DataService.GetTablesAsync(ShopId);
|
||||
|
||||
@@ -128,6 +128,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var apiTables = await DataService.GetTablesAsync(ShopId);
|
||||
|
||||
@@ -133,6 +133,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// EN: Preload tables for reference — kitchen_tickets API not yet available
|
||||
|
||||
@@ -142,6 +142,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// EN: Preload tables for reference — orders API not yet available
|
||||
|
||||
@@ -138,6 +138,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var products = await DataService.GetProductsAsync(ShopId);
|
||||
|
||||
@@ -105,6 +105,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var apiTables = await DataService.GetTablesAsync(ShopId);
|
||||
|
||||
@@ -186,6 +186,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var apiTables = await DataService.GetTablesAsync(ShopId);
|
||||
|
||||
@@ -151,6 +151,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var apiTables = await DataService.GetTablesAsync(ShopId);
|
||||
|
||||
@@ -121,6 +121,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var products = await DataService.GetProductsAsync(ShopId);
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var apiProducts = await DataService.GetProductsAsync(ShopId);
|
||||
@@ -179,7 +181,7 @@
|
||||
_barcodeInput = "";
|
||||
}
|
||||
|
||||
private void Checkout() { }
|
||||
private void Checkout() => NavigateTo("payment-method-select");
|
||||
|
||||
private static string GetCategoryIcon(string category) => category switch
|
||||
{
|
||||
|
||||
@@ -138,6 +138,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var apiProducts = await DataService.GetProductsAsync(ShopId);
|
||||
@@ -176,7 +178,7 @@
|
||||
if (item.Qty <= 0) _cartItems.Remove(item);
|
||||
}
|
||||
|
||||
private void Checkout() { }
|
||||
private void Checkout() => NavigateTo("payment-method-select");
|
||||
|
||||
private static string GetCategoryIcon(string category) => category switch
|
||||
{
|
||||
|
||||
@@ -132,6 +132,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var apiProducts = await DataService.GetProductsAsync(ShopId);
|
||||
@@ -170,7 +172,7 @@
|
||||
if (item.Qty <= 0) _cartItems.Remove(item);
|
||||
}
|
||||
|
||||
private void Checkout() { }
|
||||
private void Checkout() => NavigateTo("payment-method-select");
|
||||
|
||||
private static string GetCategoryIcon(string category) => category switch
|
||||
{
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var apiProducts = await DataService.GetProductsAsync(ShopId);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="pos-dialog-overlay">
|
||||
<div style="width:100%;max-width:520px;background:var(--pos-bg-elevated);border-radius:16px;
|
||||
@@ -21,7 +22,7 @@
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);">Đơn #DH2024-0589</div>
|
||||
</div>
|
||||
<button style="background:none;border:none;color:var(--pos-text-tertiary);cursor:pointer;padding:8px;"
|
||||
@onclick="@(() => NavigateTo(""))">
|
||||
@onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
<i data-lucide="x" style="width:20px;height:20px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -91,7 +92,7 @@
|
||||
<div style="padding:16px 24px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:10px;">
|
||||
<button style="flex:1;padding:14px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
|
||||
background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:14px;font-weight:500;"
|
||||
@onclick="@(() => NavigateTo(""))">
|
||||
@onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
<i data-lucide="arrow-left" style="width:14px;height:14px;vertical-align:middle;margin-right:4px;"></i>
|
||||
Giữ đơn hàng
|
||||
</button>
|
||||
@@ -132,9 +133,9 @@
|
||||
new("Nước mía", 20_000, 3),
|
||||
};
|
||||
|
||||
private void CancelOrder()
|
||||
private async Task CancelOrder()
|
||||
{
|
||||
NavigateTo("");
|
||||
await JS.InvokeVoidAsync("history.back");
|
||||
}
|
||||
|
||||
private record CancelItem(string Name, decimal Price, int Qty);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="pos-dialog-overlay">
|
||||
<div style="width:100%;max-width:640px;background:var(--pos-bg-elevated);border-radius:16px;
|
||||
@@ -21,7 +22,7 @@
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);">Bàn 5 · 15:20 · Đang phục vụ</div>
|
||||
</div>
|
||||
<button style="background:none;border:none;color:var(--pos-text-tertiary);cursor:pointer;padding:8px;"
|
||||
@onclick="@(() => NavigateTo(""))">
|
||||
@onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
<i data-lucide="x" style="width:20px;height:20px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -137,7 +138,7 @@
|
||||
<div style="display:flex;gap:10px;">
|
||||
<button style="flex:1;padding:14px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
|
||||
background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:14px;"
|
||||
@onclick="@(() => NavigateTo(""))">
|
||||
@onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
Hủy
|
||||
</button>
|
||||
<button style="flex:2;padding:14px;border-radius:var(--pos-radius);border:none;
|
||||
@@ -197,7 +198,7 @@
|
||||
_items.Add(new EditableItem(quick.Name, quick.Price, 1));
|
||||
}
|
||||
|
||||
private void SaveChanges() => NavigateTo("");
|
||||
private async Task SaveChanges() => await JS.InvokeVoidAsync("history.back");
|
||||
|
||||
private class EditableItem(string name, decimal price, int qty)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="pos-dialog-overlay">
|
||||
<div style="width:100%;max-width:520px;background:var(--pos-bg-elevated);border-radius:16px;
|
||||
@@ -21,7 +22,7 @@
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);">Tra cứu giá sản phẩm</div>
|
||||
</div>
|
||||
<button style="background:none;border:none;color:var(--pos-text-tertiary);cursor:pointer;padding:8px;"
|
||||
@onclick="@(() => NavigateTo(""))">
|
||||
@onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
<i data-lucide="x" style="width:20px;height:20px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -125,7 +126,7 @@
|
||||
<div style="padding:16px 24px;border-top:1px solid var(--pos-border-subtle);">
|
||||
<button style="width:100%;padding:14px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
|
||||
background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:14px;font-weight:500;"
|
||||
@onclick="@(() => NavigateTo(""))">
|
||||
@onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
<i data-lucide="x" style="width:14px;height:14px;vertical-align:middle;margin-right:4px;"></i>
|
||||
Đóng
|
||||
</button>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="pos-dialog-overlay">
|
||||
<div style="width:100%;max-width:720px;background:var(--pos-bg-elevated);border-radius:16px;
|
||||
@@ -21,7 +22,7 @@
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);">Tổng: @FormatPrice(_billTotal) · Bàn 3 · 6 khách</div>
|
||||
</div>
|
||||
<button style="background:none;border:none;color:var(--pos-text-tertiary);cursor:pointer;padding:8px;"
|
||||
@onclick="@(() => NavigateTo(""))">
|
||||
@onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
<i data-lucide="x" style="width:20px;height:20px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -174,7 +175,7 @@
|
||||
<div style="padding:16px 24px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:10px;">
|
||||
<button style="flex:1;padding:14px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
|
||||
background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:14px;"
|
||||
@onclick="@(() => NavigateTo(""))">
|
||||
@onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
Hủy
|
||||
</button>
|
||||
<button style="flex:2;padding:14px;border-radius:var(--pos-radius);border:none;
|
||||
@@ -227,7 +228,7 @@
|
||||
private List<SplitItem> UnassignedItems => _billItems.Where(i => i.AssignedTo < 0).ToList();
|
||||
private decimal CustomRemaining => _billTotal - _customAmounts.Sum();
|
||||
|
||||
private void GenerateSplitBills() => NavigateTo("");
|
||||
private async Task GenerateSplitBills() => await JS.InvokeVoidAsync("history.back");
|
||||
|
||||
private record SplitMode(string Key, string Label);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="pos-dialog-overlay">
|
||||
<div style="width:100%;max-width:600px;background:var(--pos-bg-elevated);border-radius:16px;
|
||||
@@ -21,7 +22,7 @@
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);">Nhập hàng hóa vào kho</div>
|
||||
</div>
|
||||
<button style="background:none;border:none;color:var(--pos-text-tertiary);cursor:pointer;padding:8px;"
|
||||
@onclick="@(() => NavigateTo(""))">
|
||||
@onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
<i data-lucide="x" style="width:20px;height:20px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -138,7 +139,7 @@
|
||||
<div style="padding:16px 24px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:10px;">
|
||||
<button style="flex:1;padding:14px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
|
||||
background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:14px;"
|
||||
@onclick="@(() => NavigateTo(""))">
|
||||
@onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
Hủy
|
||||
</button>
|
||||
<button style="flex:2;padding:14px;border-radius:var(--pos-radius);border:none;
|
||||
@@ -187,7 +188,7 @@
|
||||
new("Ly giấy 16oz", 500, "cái", "25/02/2026"),
|
||||
};
|
||||
|
||||
private void ConfirmStockIn() => NavigateTo("");
|
||||
private async Task ConfirmStockIn() => await JS.InvokeVoidAsync("history.back");
|
||||
|
||||
private record StockLog(string Product, int Qty, string Unit, string Time);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="pos-dialog-overlay">
|
||||
<div style="width:100%;max-width:540px;background:var(--pos-bg-elevated);border-radius:16px;
|
||||
@@ -21,7 +22,7 @@
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);">Xuất hàng hóa khỏi kho</div>
|
||||
</div>
|
||||
<button style="background:none;border:none;color:var(--pos-text-tertiary);cursor:pointer;padding:8px;"
|
||||
@onclick="@(() => NavigateTo(""))">
|
||||
@onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
<i data-lucide="x" style="width:20px;height:20px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -118,7 +119,7 @@
|
||||
<div style="padding:16px 24px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:10px;">
|
||||
<button style="flex:1;padding:14px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
|
||||
background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:14px;"
|
||||
@onclick="@(() => NavigateTo(""))">
|
||||
@onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
Hủy
|
||||
</button>
|
||||
<button style="flex:2;padding:14px;border-radius:var(--pos-radius);border:none;
|
||||
@@ -159,5 +160,5 @@
|
||||
"Nguyễn Văn A", "Trần Văn C", "Lê Thị D", "Phạm Minh E"
|
||||
};
|
||||
|
||||
private void ConfirmStockOut() => NavigateTo("");
|
||||
private async Task ConfirmStockOut() => await JS.InvokeVoidAsync("history.back");
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="pos-dialog-overlay">
|
||||
<div style="width:100%;max-width:680px;background:var(--pos-bg-elevated);border-radius:16px;
|
||||
@@ -21,7 +22,7 @@
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);">Chuyển hàng giữa các chi nhánh</div>
|
||||
</div>
|
||||
<button style="background:none;border:none;color:var(--pos-text-tertiary);cursor:pointer;padding:8px;"
|
||||
@onclick="@(() => NavigateTo(""))">
|
||||
@onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
<i data-lucide="x" style="width:20px;height:20px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -139,7 +140,7 @@
|
||||
<div style="padding:16px 24px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:10px;">
|
||||
<button style="flex:1;padding:14px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
|
||||
background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:14px;"
|
||||
@onclick="@(() => NavigateTo(""))">
|
||||
@onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
Hủy
|
||||
</button>
|
||||
<button style="flex:2;padding:14px;border-radius:var(--pos-radius);border:none;
|
||||
@@ -189,7 +190,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateTransfer() => NavigateTo("");
|
||||
private async Task CreateTransfer() => await JS.InvokeVoidAsync("history.back");
|
||||
|
||||
private class TransferItem(string name, string sku, int stock, decimal unitPrice, int qty)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="pos-dialog-overlay">
|
||||
<div style="width:100%;max-width:560px;background:var(--pos-bg-elevated);border-radius:16px;
|
||||
@@ -21,7 +22,7 @@
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);">Void / Refund đơn hàng</div>
|
||||
</div>
|
||||
<button style="background:none;border:none;color:var(--pos-text-tertiary);cursor:pointer;padding:8px;"
|
||||
@onclick="@(() => NavigateTo(""))">
|
||||
@onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
<i data-lucide="x" style="width:20px;height:20px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -147,7 +148,7 @@
|
||||
<div style="padding:16px 24px;border-top:1px solid var(--pos-border-subtle);display:flex;gap:10px;">
|
||||
<button style="flex:1;padding:14px;border-radius:var(--pos-radius);border:1px solid var(--pos-border-default);
|
||||
background:transparent;color:var(--pos-text-primary);cursor:pointer;font-size:14px;font-weight:500;"
|
||||
@onclick="@(() => NavigateTo(""))">
|
||||
@onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
Hủy bỏ
|
||||
</button>
|
||||
<button style="flex:1;padding:14px;border-radius:var(--pos-radius);border:none;
|
||||
@@ -194,9 +195,9 @@
|
||||
_orderFound = true;
|
||||
}
|
||||
|
||||
private void ConfirmAction()
|
||||
private async Task ConfirmAction()
|
||||
{
|
||||
NavigateTo("");
|
||||
await JS.InvokeVoidAsync("history.back");
|
||||
}
|
||||
|
||||
private record OrderItem(string Name, decimal Price, int Qty);
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject IJSRuntime JS
|
||||
@implements IDisposable
|
||||
|
||||
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
||||
@* ═══ HEADER ═══ *@
|
||||
<div style="padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);display:flex;align-items:center;gap:12px;">
|
||||
<button class="pos-category-tab" @onclick="@(() => NavigateTo("operations"))">
|
||||
<button class="pos-category-tab" @onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
<i data-lucide="arrow-left" style="width:14px;"></i> Quay lại
|
||||
</button>
|
||||
<span style="font-size:16px;font-weight:700;">Chấm công</span>
|
||||
@@ -32,7 +34,7 @@
|
||||
<div style="width:64px;height:64px;border-radius:50%;background:var(--pos-bg-interactive);display:flex;align-items:center;justify-content:center;margin:0 auto 12px;">
|
||||
<i data-lucide="user" style="width:28px;height:28px;color:var(--pos-orange-primary);"></i>
|
||||
</div>
|
||||
<div style="font-size:18px;font-weight:700;color:var(--pos-text-primary);">@_staffName</div>
|
||||
<div style="font-size:18px;font-weight:700;color:var(--pos-text-primary);">@StaffName</div>
|
||||
<div style="font-size:13px;color:var(--pos-text-tertiary);margin-top:4px;">@_staffRole</div>
|
||||
<div style="margin-top:12px;padding:6px 16px;border-radius:20px;display:inline-block;font-size:12px;font-weight:600;
|
||||
background:@(_isClockedIn ? "rgba(34,197,94,.15)" : "rgba(239,68,68,.15)");
|
||||
@@ -91,11 +93,11 @@
|
||||
// TODO: Integrate with Merchant Service staff/shift endpoints when available.
|
||||
|
||||
// EN: Staff info / VI: Thông tin nhân viên
|
||||
private readonly string _staffName = "Nguyễn Văn A";
|
||||
private readonly string _staffRole = "Thu ngân — Cashier";
|
||||
private bool _isClockedIn = true;
|
||||
private DateTime _currentTime = DateTime.Now;
|
||||
private double _totalHours = 5.5;
|
||||
private Timer? _clockTimer;
|
||||
|
||||
// EN: Today's clock entries / VI: Bản ghi chấm công hôm nay
|
||||
private readonly List<ClockEntry> _clockEntries = new()
|
||||
@@ -105,11 +107,22 @@
|
||||
new("13:30", "in"),
|
||||
};
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_clockTimer = new Timer(_ =>
|
||||
{
|
||||
_currentTime = DateTime.Now;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
private void ToggleClock()
|
||||
{
|
||||
_isClockedIn = !_isClockedIn;
|
||||
_clockEntries.Add(new(DateTime.Now.ToString("HH:mm"), _isClockedIn ? "in" : "out"));
|
||||
}
|
||||
|
||||
public void Dispose() => _clockTimer?.Dispose();
|
||||
|
||||
private record ClockEntry(string Time, string Type);
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject PosDataService DataService
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
||||
@* ═══ HEADER ═══ *@
|
||||
<div style="padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);display:flex;align-items:center;gap:12px;">
|
||||
<button class="pos-category-tab" @onclick="@(() => NavigateTo("operations"))">
|
||||
<button class="pos-category-tab" @onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
<i data-lucide="arrow-left" style="width:14px;"></i> Quay lại
|
||||
</button>
|
||||
<span style="font-size:16px;font-weight:700;">Đơn hàng đang chờ</span>
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
||||
@* ═══ HEADER ═══ *@
|
||||
<div style="padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);display:flex;align-items:center;gap:12px;">
|
||||
<button class="pos-category-tab" @onclick="@(() => NavigateTo("operations"))">
|
||||
<button class="pos-category-tab" @onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
<i data-lucide="arrow-left" style="width:14px;"></i> Quay lại
|
||||
</button>
|
||||
<span style="font-size:16px;font-weight:700;">Bán nhanh</span>
|
||||
@@ -23,7 +24,7 @@
|
||||
<div style="width:100%;max-width:420px;background:var(--pos-bg-elevated);border-radius:var(--pos-radius);padding:24px;text-align:right;">
|
||||
<div style="font-size:12px;color:var(--pos-text-tertiary);margin-bottom:4px;">Số tiền</div>
|
||||
<div style="font-size:40px;font-weight:800;color:var(--pos-orange-primary);letter-spacing:1px;">
|
||||
@FormatPrice(decimal.Parse(_amountStr == "" ? "0" : _amountStr))
|
||||
@FormatPrice(ParsedAmount)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -101,7 +102,7 @@
|
||||
<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(decimal.Parse(_amountStr == "" ? "0" : _amountStr))</span>
|
||||
<span class="pos-cart-total__value">@FormatPrice(ParsedAmount)</span>
|
||||
</div>
|
||||
<button class="pos-btn-checkout" @onclick="Pay" disabled="@(_amountStr == "" || _amountStr == "0")">
|
||||
<i data-lucide="credit-card" style="width:18px;height:18px;"></i>
|
||||
@@ -119,6 +120,7 @@
|
||||
|
||||
// EN: Amount input state / VI: Trạng thái nhập số tiền
|
||||
private string _amountStr = "";
|
||||
private decimal ParsedAmount => decimal.TryParse(_amountStr, out var v) ? v : 0m;
|
||||
private string _selectedCategory = "Khác";
|
||||
private string _note = "";
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
|
||||
@* ═══ HEADER ═══ *@
|
||||
<div style="padding:12px 16px;border-bottom:1px solid var(--pos-border-subtle);display:flex;align-items:center;gap:12px;">
|
||||
<button class="pos-category-tab" @onclick="@(() => NavigateTo("operations"))">
|
||||
<button class="pos-category-tab" @onclick="@(async () => await JS.InvokeVoidAsync("history.back"))">
|
||||
<i data-lucide="arrow-left" style="width:14px;"></i> Quay lại
|
||||
</button>
|
||||
<span style="font-size:16px;font-weight:700;">Quản lý ca</span>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:32px;gap:32px;">
|
||||
@* ═══ ORDER TOTAL ═══ *@
|
||||
@@ -56,7 +57,7 @@
|
||||
};
|
||||
|
||||
private void SelectMethod(string route) => NavigateTo(route);
|
||||
private void GoBack() => NavigateTo("cafe");
|
||||
private async Task GoBack() => await JS.InvokeVoidAsync("history.back");
|
||||
|
||||
private record PaymentMethod(string Icon, string Label, string Description, string Route);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:32px;gap:24px;">
|
||||
@* ═══ SUCCESS ANIMATION ═══ *@
|
||||
@@ -85,5 +86,5 @@
|
||||
private string _transactionId = "TXN-20240215-001";
|
||||
|
||||
private void PrintReceipt() => NavigateTo("payment/receipt");
|
||||
private void NewOrder() => NavigateTo("cafe");
|
||||
private async Task NewOrder() => await JS.InvokeVoidAsync("history.back");
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@implements IDisposable
|
||||
|
||||
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:32px;gap:24px;">
|
||||
@* ═══ ORDER TOTAL ═══ *@
|
||||
@@ -41,10 +42,20 @@
|
||||
</div>
|
||||
|
||||
@* ═══ STATUS ═══ *@
|
||||
<div style="display:flex;align-items:center;gap:8px;color:var(--pos-text-tertiary);font-size:14px;">
|
||||
<div style="width:8px;height:8px;border-radius:50%;background:var(--pos-warning);animation:pulse 2s ease-in-out infinite;"></div>
|
||||
Chờ xác nhận thanh toán...
|
||||
</div>
|
||||
@if (_timerSeconds > 0)
|
||||
{
|
||||
<div style="display:flex;align-items:center;gap:8px;color:var(--pos-text-tertiary);font-size:14px;">
|
||||
<div style="width:8px;height:8px;border-radius:50%;background:var(--pos-warning);animation:pulse 2s ease-in-out infinite;"></div>
|
||||
Chờ xác nhận thanh toán...
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="display:flex;align-items:center;gap:8px;color:var(--pos-danger);font-size:14px;font-weight:600;">
|
||||
<div style="width:8px;height:8px;border-radius:50%;background:var(--pos-danger);"></div>
|
||||
Mã QR đã hết hạn. Nhấn "Làm mới" để tạo mã mới.
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ═══ ACTIONS ═══ *@
|
||||
<div style="display:flex;gap:12px;">
|
||||
@@ -75,10 +86,29 @@
|
||||
private string _selectedProvider = "VietQR";
|
||||
private int _timerSeconds = 300;
|
||||
private string _timerDisplay => $"{_timerSeconds / 60}:{(_timerSeconds % 60):D2}";
|
||||
private Timer? _countdownTimer;
|
||||
|
||||
// EN: QR providers / VI: Nhà cung cấp QR
|
||||
private readonly string[] _providers = { "VietQR", "MoMo", "ZaloPay" };
|
||||
|
||||
private void Refresh() => _timerSeconds = 300;
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_countdownTimer = new Timer(_ =>
|
||||
{
|
||||
if (_timerSeconds > 0)
|
||||
{
|
||||
_timerSeconds--;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
_timerSeconds = 300;
|
||||
}
|
||||
|
||||
private void Cancel() => NavigateTo("payment/method-select");
|
||||
|
||||
public void Dispose() => _countdownTimer?.Dispose();
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<span>Đơn #@_orderNumber</span>
|
||||
<span>@_orderDate</span>
|
||||
</div>
|
||||
<div style="font-size:11px;margin-bottom:4px;">NV: @_staffName</div>
|
||||
<div style="font-size:11px;margin-bottom:4px;">NV: @StaffName</div>
|
||||
|
||||
<div style="border-top:1px dashed #000;margin:12px 0;"></div>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
{
|
||||
<div style="display:flex;justify-content:space-between;font-size:12px;padding:3px 0;">
|
||||
<span>@item.Qty x @item.Name</span>
|
||||
<span>@FormatReceiptPrice(item.Price * item.Qty)</span>
|
||||
<span>@FormatPrice(item.Price * item.Qty)</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -44,15 +44,15 @@
|
||||
<div style="display:flex;flex-direction:column;gap:4px;font-size:12px;">
|
||||
<div style="display:flex;justify-content:space-between;">
|
||||
<span>Tạm tính</span>
|
||||
<span>@FormatReceiptPrice(_subtotal)</span>
|
||||
<span>@FormatPrice(_subtotal)</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;">
|
||||
<span>Phí dịch vụ (5%)</span>
|
||||
<span>@FormatReceiptPrice(_serviceCharge)</span>
|
||||
<span>@FormatPrice(_serviceCharge)</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;">
|
||||
<span>VAT (8%)</span>
|
||||
<span>@FormatReceiptPrice(_vat)</span>
|
||||
<span>@FormatPrice(_vat)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
<div style="display:flex;justify-content:space-between;font-size:16px;font-weight:700;">
|
||||
<span>TỔNG CỘNG</span>
|
||||
<span>@FormatReceiptPrice(_total)</span>
|
||||
<span>@FormatPrice(_total)</span>
|
||||
</div>
|
||||
|
||||
<div style="border-top:1px dashed #000;margin:12px 0;"></div>
|
||||
@@ -73,11 +73,11 @@
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;">
|
||||
<span>Khách đưa</span>
|
||||
<span>@FormatReceiptPrice(_amountPaid)</span>
|
||||
<span>@FormatPrice(_amountPaid)</span>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;">
|
||||
<span>Tiền thối</span>
|
||||
<span>@FormatReceiptPrice(_changeAmount)</span>
|
||||
<span>@FormatPrice(_changeAmount)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -125,7 +125,6 @@
|
||||
private string _orderNumber = "1042";
|
||||
private string _orderDate = "15/02/2024";
|
||||
private string _orderTime = "14:35:22";
|
||||
private string _staffName = "Nguyễn Thị Mai";
|
||||
|
||||
private readonly List<ReceiptItem> _items = new()
|
||||
{
|
||||
@@ -145,8 +144,6 @@
|
||||
private decimal _changeAmount => _amountPaid - _total;
|
||||
private string _transactionId = "TXN-20240215-001";
|
||||
|
||||
private static string FormatReceiptPrice(decimal price) => price.ToString("N0") + "₫";
|
||||
|
||||
private void Print()
|
||||
{
|
||||
// EN: Trigger browser print / VI: Kích hoạt in từ trình duyệt
|
||||
|
||||
@@ -151,6 +151,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var apiProducts = await DataService.GetProductsAsync(ShopId);
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var apiProducts = await DataService.GetProductsAsync(ShopId);
|
||||
|
||||
@@ -121,6 +121,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var apiProducts = await DataService.GetProductsAsync(ShopId);
|
||||
|
||||
@@ -188,6 +188,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var appointments = await DataService.GetAppointmentsAsync(ShopId);
|
||||
|
||||
@@ -145,6 +145,8 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var apiProducts = await DataService.GetProductsAsync(ShopId);
|
||||
|
||||
@@ -67,6 +67,6 @@ public class OrderController : ControllerBase
|
||||
public Task<IActionResult> GetPosDashboard([FromQuery] Guid? shopId = null)
|
||||
{
|
||||
var qs = shopId.HasValue ? $"?shopId={shopId}" : "";
|
||||
return _order.GetAsync($"/api/v1/pos/dashboard{qs}").ProxyAsync();
|
||||
return _order.GetAsync($"/api/v1/orders/dashboard{qs}").ProxyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,63 +22,60 @@ public class AppointmentEntityTypeConfiguration : IEntityTypeConfiguration<Appoi
|
||||
.HasColumnName("id")
|
||||
.ValueGeneratedNever();
|
||||
|
||||
builder.Property<Guid>("_shopId")
|
||||
builder.Property(a => a.ShopId)
|
||||
.HasField("_shopId")
|
||||
.HasColumnName("shop_id")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<Guid?>("_customerId")
|
||||
builder.Property(a => a.CustomerId)
|
||||
.HasField("_customerId")
|
||||
.HasColumnName("customer_id");
|
||||
|
||||
builder.Property<Guid?>("_staffId")
|
||||
builder.Property(a => a.StaffId)
|
||||
.HasField("_staffId")
|
||||
.HasColumnName("staff_id");
|
||||
|
||||
builder.Property<Guid?>("_resourceId")
|
||||
builder.Property(a => a.ResourceId)
|
||||
.HasField("_resourceId")
|
||||
.HasColumnName("resource_id");
|
||||
|
||||
builder.Property<Guid>("_serviceId")
|
||||
builder.Property(a => a.ServiceId)
|
||||
.HasField("_serviceId")
|
||||
.HasColumnName("service_id")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<DateTime>("_startTime")
|
||||
builder.Property(a => a.StartTime)
|
||||
.HasField("_startTime")
|
||||
.HasColumnName("start_time")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<DateTime>("_endTime")
|
||||
builder.Property(a => a.EndTime)
|
||||
.HasField("_endTime")
|
||||
.HasColumnName("end_time")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<string>("_status")
|
||||
builder.Property(a => a.Status)
|
||||
.HasField("_status")
|
||||
.HasColumnName("status")
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<DateTime>("_createdAt")
|
||||
builder.Property(a => a.CreatedAt)
|
||||
.HasField("_createdAt")
|
||||
.HasColumnName("created_at")
|
||||
.IsRequired();
|
||||
|
||||
// EN: Indexes / VI: Indexes
|
||||
builder.HasIndex("_shopId")
|
||||
builder.HasIndex(a => a.ShopId)
|
||||
.HasDatabaseName("ix_appointments_shop_id");
|
||||
|
||||
builder.HasIndex("_customerId")
|
||||
builder.HasIndex(a => a.CustomerId)
|
||||
.HasDatabaseName("ix_appointments_customer_id");
|
||||
|
||||
builder.HasIndex("_staffId")
|
||||
builder.HasIndex(a => a.StaffId)
|
||||
.HasDatabaseName("ix_appointments_staff_id");
|
||||
|
||||
builder.HasIndex("_startTime")
|
||||
builder.HasIndex(a => a.StartTime)
|
||||
.HasDatabaseName("ix_appointments_start_time");
|
||||
|
||||
// EN: Ignore public properties (mapped via backing fields)
|
||||
// VI: Bỏ qua các properties công khai (đã map qua backing fields)
|
||||
builder.Ignore(a => a.ShopId);
|
||||
builder.Ignore(a => a.CustomerId);
|
||||
builder.Ignore(a => a.StaffId);
|
||||
builder.Ignore(a => a.ResourceId);
|
||||
builder.Ignore(a => a.ServiceId);
|
||||
builder.Ignore(a => a.StartTime);
|
||||
builder.Ignore(a => a.EndTime);
|
||||
builder.Ignore(a => a.Status);
|
||||
builder.Ignore(a => a.CreatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,43 +22,40 @@ public class ResourceEntityTypeConfiguration : IEntityTypeConfiguration<Resource
|
||||
.HasColumnName("id")
|
||||
.ValueGeneratedNever();
|
||||
|
||||
builder.Property<Guid>("_shopId")
|
||||
builder.Property(r => r.ShopId)
|
||||
.HasField("_shopId")
|
||||
.HasColumnName("shop_id")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<string>("_name")
|
||||
builder.Property(r => r.Name)
|
||||
.HasField("_name")
|
||||
.HasColumnName("name")
|
||||
.HasMaxLength(200)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<string>("_resourceType")
|
||||
builder.Property(r => r.ResourceType)
|
||||
.HasField("_resourceType")
|
||||
.HasColumnName("resource_type")
|
||||
.HasMaxLength(50)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<int>("_capacity")
|
||||
builder.Property(r => r.Capacity)
|
||||
.HasField("_capacity")
|
||||
.HasColumnName("capacity")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<bool>("_isActive")
|
||||
builder.Property(r => r.IsActive)
|
||||
.HasField("_isActive")
|
||||
.HasColumnName("is_active")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<DateTime>("_createdAt")
|
||||
builder.Property(r => r.CreatedAt)
|
||||
.HasField("_createdAt")
|
||||
.HasColumnName("created_at")
|
||||
.IsRequired();
|
||||
|
||||
// EN: Indexes / VI: Indexes
|
||||
builder.HasIndex("_shopId")
|
||||
builder.HasIndex(r => r.ShopId)
|
||||
.HasDatabaseName("ix_resources_shop_id");
|
||||
|
||||
// EN: Ignore public properties (mapped via backing fields)
|
||||
// VI: Bỏ qua các properties công khai (đã map qua backing fields)
|
||||
builder.Ignore(r => r.ShopId);
|
||||
builder.Ignore(r => r.Name);
|
||||
builder.Ignore(r => r.ResourceType);
|
||||
builder.Ignore(r => r.Capacity);
|
||||
builder.Ignore(r => r.IsActive);
|
||||
builder.Ignore(r => r.CreatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,39 +22,36 @@ public class StaffScheduleEntityTypeConfiguration : IEntityTypeConfiguration<Sta
|
||||
.HasColumnName("id")
|
||||
.ValueGeneratedNever();
|
||||
|
||||
builder.Property<Guid>("_staffId")
|
||||
builder.Property(s => s.StaffId)
|
||||
.HasField("_staffId")
|
||||
.HasColumnName("staff_id")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<Guid>("_shopId")
|
||||
builder.Property(s => s.ShopId)
|
||||
.HasField("_shopId")
|
||||
.HasColumnName("shop_id")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<int>("_dayOfWeek")
|
||||
builder.Property(s => s.DayOfWeek)
|
||||
.HasField("_dayOfWeek")
|
||||
.HasColumnName("day_of_week")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<TimeOnly>("_startTime")
|
||||
builder.Property(s => s.StartTime)
|
||||
.HasField("_startTime")
|
||||
.HasColumnName("start_time")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<TimeOnly>("_endTime")
|
||||
builder.Property(s => s.EndTime)
|
||||
.HasField("_endTime")
|
||||
.HasColumnName("end_time")
|
||||
.IsRequired();
|
||||
|
||||
// EN: Indexes / VI: Indexes
|
||||
builder.HasIndex("_staffId", "_dayOfWeek")
|
||||
builder.HasIndex(s => new { s.StaffId, s.DayOfWeek })
|
||||
.HasDatabaseName("ix_staff_schedules_staff_day");
|
||||
|
||||
builder.HasIndex("_shopId")
|
||||
builder.HasIndex(s => s.ShopId)
|
||||
.HasDatabaseName("ix_staff_schedules_shop_id");
|
||||
|
||||
// EN: Ignore public properties (mapped via backing fields)
|
||||
// VI: Bỏ qua các properties công khai (đã map qua backing fields)
|
||||
builder.Ignore(s => s.StaffId);
|
||||
builder.Ignore(s => s.ShopId);
|
||||
builder.Ignore(s => s.DayOfWeek);
|
||||
builder.Ignore(s => s.StartTime);
|
||||
builder.Ignore(s => s.EndTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,27 @@ public class CategoriesController : ControllerBase
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get categories for a specific shop (RESTful path).
|
||||
/// VI: Lấy danh mục của một shop cụ thể (đường dẫn RESTful).
|
||||
/// </summary>
|
||||
[HttpGet("~/api/v{version:apiVersion}/shops/{shopId}/categories")]
|
||||
[ProducesResponseType(typeof(List<CategoryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<List<CategoryDto>>> GetShopCategories(
|
||||
Guid shopId,
|
||||
[FromQuery] Guid? parentId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new GetCategoriesQuery
|
||||
{
|
||||
ShopId = shopId,
|
||||
ParentId = parentId
|
||||
};
|
||||
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create a new category.
|
||||
/// VI: Tạo danh mục mới.
|
||||
|
||||
@@ -57,6 +57,33 @@ public class ProductsController : ControllerBase
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get products for a specific shop (RESTful path).
|
||||
/// VI: Lấy sản phẩm của một shop cụ thể (đường dẫn RESTful).
|
||||
/// </summary>
|
||||
[HttpGet("~/api/v{version:apiVersion}/shops/{shopId}/products")]
|
||||
[ProducesResponseType(typeof(PagedResult<ProductDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResult<ProductDto>>> GetShopProducts(
|
||||
Guid shopId,
|
||||
[FromQuery] bool? isActive = null,
|
||||
[FromQuery] string? type = null,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new GetProductsQuery
|
||||
{
|
||||
ShopId = shopId,
|
||||
IsActive = isActive,
|
||||
Type = type,
|
||||
Page = page,
|
||||
PageSize = pageSize
|
||||
};
|
||||
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get product by ID.
|
||||
/// VI: Lấy sản phẩm theo ID.
|
||||
|
||||
@@ -9,27 +9,35 @@ public class TableEntityTypeConfiguration : IEntityTypeConfiguration<Table>
|
||||
public void Configure(EntityTypeBuilder<Table> builder)
|
||||
{
|
||||
builder.ToTable("tables");
|
||||
|
||||
|
||||
builder.HasKey(t => t.Id);
|
||||
|
||||
|
||||
builder.Property(t => t.Id)
|
||||
.HasColumnName("id")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<Guid>("_shopId")
|
||||
// EN: Map read-only public properties to their backing fields
|
||||
// so EF Core can translate LINQ queries (e.g. t.ShopId == value).
|
||||
// VI: Map các property public chỉ đọc sang backing field tương ứng
|
||||
// để EF Core có thể dịch LINQ query (ví dụ: t.ShopId == value).
|
||||
builder.Property(t => t.ShopId)
|
||||
.HasField("_shopId")
|
||||
.HasColumnName("shop_id")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<string>("_tableNumber")
|
||||
builder.Property(t => t.TableNumber)
|
||||
.HasField("_tableNumber")
|
||||
.HasColumnName("table_number")
|
||||
.HasMaxLength(20)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<int>("_capacity")
|
||||
builder.Property(t => t.Capacity)
|
||||
.HasField("_capacity")
|
||||
.HasColumnName("capacity")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<string?>("_zone")
|
||||
builder.Property(t => t.Zone)
|
||||
.HasField("_zone")
|
||||
.HasColumnName("zone")
|
||||
.HasMaxLength(100);
|
||||
|
||||
@@ -37,28 +45,25 @@ public class TableEntityTypeConfiguration : IEntityTypeConfiguration<Table>
|
||||
.HasColumnName("status_id")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<DateTime>("_createdAt")
|
||||
builder.Property(t => t.CreatedAt)
|
||||
.HasField("_createdAt")
|
||||
.HasColumnName("created_at")
|
||||
.IsRequired();
|
||||
|
||||
builder.Property<DateTime?>("_updatedAt")
|
||||
builder.Property(t => t.UpdatedAt)
|
||||
.HasField("_updatedAt")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
// EN: Indexes / VI: Các index
|
||||
builder.HasIndex("_shopId")
|
||||
builder.HasIndex(t => t.ShopId)
|
||||
.HasDatabaseName("ix_tables_shop_id");
|
||||
|
||||
builder.HasIndex("_shopId", "_tableNumber")
|
||||
builder.HasIndex(t => new { t.ShopId, t.TableNumber })
|
||||
.HasDatabaseName("ix_tables_shop_table_number")
|
||||
.IsUnique();
|
||||
|
||||
// EN: Ignore navigation properties / VI: Bỏ qua các navigation properties
|
||||
builder.Ignore(t => t.ShopId);
|
||||
builder.Ignore(t => t.TableNumber);
|
||||
builder.Ignore(t => t.Capacity);
|
||||
builder.Ignore(t => t.Zone);
|
||||
// EN: Ignore the Status navigation (enumeration loaded via StatusId)
|
||||
// VI: Bỏ qua navigation Status (enumeration được load qua StatusId)
|
||||
builder.Ignore(t => t.Status);
|
||||
builder.Ignore(t => t.CreatedAt);
|
||||
builder.Ignore(t => t.UpdatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,18 +34,19 @@ public record OrderItemDto(
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Order summary for list views.
|
||||
/// VI: Tóm tắt order cho danh sách.
|
||||
/// EN: Order summary for list views (property-based for Dapper materialization).
|
||||
/// VI: Tóm tắt order cho danh sách (dựa trên property để Dapper materialize).
|
||||
/// </summary>
|
||||
public record OrderSummaryDto(
|
||||
Guid Id,
|
||||
Guid ShopId,
|
||||
Guid? CustomerId,
|
||||
string Status,
|
||||
decimal TotalAmount,
|
||||
int ItemCount,
|
||||
DateTime CreatedAt
|
||||
);
|
||||
public record OrderSummaryDto
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid ShopId { get; init; }
|
||||
public Guid? CustomerId { get; init; }
|
||||
public string Status { get; init; } = string.Empty;
|
||||
public decimal TotalAmount { get; init; }
|
||||
public long ItemCount { get; init; }
|
||||
public DateTime CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Paged result wrapper.
|
||||
|
||||
@@ -69,7 +69,7 @@ public class GetOrderStatsQueryHandler : IRequestHandler<GetOrderStatsQuery, Ord
|
||||
var byStatus = statusRows.ToDictionary(row => row.Status, row => row.Total, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return new OrderStatsDto(
|
||||
aggregate.TotalOrders,
|
||||
(int)aggregate.TotalOrders,
|
||||
aggregate.TotalRevenue,
|
||||
aggregate.AverageOrderValue,
|
||||
byStatus);
|
||||
@@ -108,6 +108,6 @@ public class GetOrderStatsQueryHandler : IRequestHandler<GetOrderStatsQuery, Ord
|
||||
return (whereClause, parameters);
|
||||
}
|
||||
|
||||
private record OrderStatsAggregateRow(int TotalOrders, decimal TotalRevenue, decimal AverageOrderValue);
|
||||
private record OrderStatsAggregateRow(long TotalOrders, decimal TotalRevenue, decimal AverageOrderValue);
|
||||
private record OrderStatusCountRow(string Status, int Total);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public record RecentOrderDto(
|
||||
Guid Id,
|
||||
decimal TotalAmount,
|
||||
string Status,
|
||||
int ItemCount,
|
||||
long ItemCount,
|
||||
DateTime CreatedAt
|
||||
);
|
||||
|
||||
@@ -169,8 +169,8 @@ public class GetPosDashboardQueryHandler : IRequestHandler<GetPosDashboardQuery,
|
||||
|
||||
return new PosDashboardDto(
|
||||
aggregate.Revenue,
|
||||
aggregate.OrderCount,
|
||||
aggregate.ItemsSold,
|
||||
(int)aggregate.OrderCount,
|
||||
(int)aggregate.ItemsSold,
|
||||
aggregate.AvgOrderValue,
|
||||
popularItems,
|
||||
paymentBreakdown,
|
||||
@@ -178,5 +178,5 @@ public class GetPosDashboardQueryHandler : IRequestHandler<GetPosDashboardQuery,
|
||||
recentOrders);
|
||||
}
|
||||
|
||||
private record AggregateRow(decimal Revenue, int OrderCount, int ItemsSold, decimal AvgOrderValue);
|
||||
private record AggregateRow(decimal Revenue, long OrderCount, long ItemsSold, decimal AvgOrderValue);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user