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 index e1016db4..165c45a4 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AdminLayout.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AdminLayout.razor @@ -191,18 +191,18 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { - if (firstRender) - { - // EN: Restore session from localStorage on first render (Blazor WASM ready) - // VI: Khôi phục session từ localStorage khi render lần đầu (Blazor WASM sẵn sàng) - await AuthSvc.TryRestoreSessionAsync(); - } + // EN: Session restore is handled by AdminBase.OnInitializedAsync() — no need to duplicate here. + // VI: Khôi phục session được xử lý bởi AdminBase.OnInitializedAsync() — không cần gọi lại ở đây. + // EN: Re-init Lucide icons after every render (Blazor navigation replaces DOM) // VI: Khởi tạo lại Lucide icons sau mỗi lần render (Blazor navigation thay đổi DOM) try { await JS.InvokeVoidAsync("lucide.createIcons"); } catch { } } - private void OnAuthStateChanged() => InvokeAsync(StateHasChanged); + private void OnAuthStateChanged() + { + try { InvokeAsync(StateHasChanged); } catch { } + } private void RecoverError() => _errorBoundary?.Recover(); diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Dashboard.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Dashboard.razor index 337e5df6..67a5cd86 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Dashboard.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Dashboard.razor @@ -115,6 +115,7 @@ { @foreach (var shop in _shops) { + var stats = _shopStats.FirstOrDefault(s => s.ShopId == shop.Id);
@@ -126,11 +127,43 @@
@ShopSidebarConfig.GetVerticalLabel(shop.Category) • @(shop.Slug)
-
- - @(shop.Status == "active" ? L["Dashboard_Status_Open"] : L["Dashboard_Status_Setup"]) +
+ @if (shop.Status != "active") + { + + } +
+ + @(shop.Status == "active" ? L["Dashboard_Status_Open"] : L["Dashboard_Status_Setup"]) +
+ @if (stats != null) + { +
+
+ + @FormatCurrency(stats.Revenue) +
+
+ + @stats.OrderCount @L["Dashboard_Stats_Orders"] +
+
+ + @stats.StaffCount @L["Dashboard_Stats_Staff"] +
+
+ + @stats.ProductCount @L["Dashboard_Stats_Products"] +
+
+ }
} } @@ -153,9 +186,9 @@ @L["Dashboard_QA_CreateStore"] - - - @L["Dashboard_QA_SystemSettings"] + + + @L["Dashboard_QA_ManageStaff"] @@ -163,6 +196,26 @@ @L["Dashboard_QA_Permissions"] + + + @L["Dashboard_QA_Devices"] + + + + + @L["Dashboard_QA_Integrations"] + + + + + @L["Dashboard_QA_AuditLog"] + + + + + @L["Dashboard_QA_SystemSettings"] + + @@ -173,26 +226,45 @@ @L["Dashboard_SystemStatus"] +
-
- API Gateway - - Online - -
-
- IAM Service - - Online - -
-
- Merchant Service - - Online - -
+ @if (_healthLoading && _serviceHealth.Count == 0) + { +
+ @L["Dashboard_CheckingServices"] +
+ } + else + { + @foreach (var svc in _serviceHealth) + { + var statusColor = svc.IsOnline ? "#22C55E" : "#EF4444"; + var statusText = svc.IsOnline ? "Online" : "Offline"; +
+ + + @svc.Name + + + @if (svc.IsOnline && svc.LatencyMs.HasValue) + { + @(svc.LatencyMs)ms + } + + @statusText + +
+ } + @if (_healthCheckedAt.HasValue) + { +
+ @L["Dashboard_LastChecked"] @_healthCheckedAt.Value.ToString("HH:mm:ss") +
+ } + }
@@ -201,6 +273,10 @@ @code { private List _shops = new(); + private List _shopStats = new(); + private List _serviceHealth = new(); + private bool _healthLoading = false; + private DateTime? _healthCheckedAt; protected override async Task OnInitializedAsync() { @@ -208,12 +284,17 @@ IsLoading = true; try { - _shops = await DataService.GetShopsAsync(); + var shopsTask = DataService.GetShopsAsync(); + var statsTask = DataService.GetShopStatsAsync(); + var healthTask = LoadHealthAsync(); + _shops = await shopsTask; + try { _shopStats = await statsTask; } catch { _shopStats = new(); } + await healthTask; } catch (Exception ex) { _shops = new(); - Console.Error.WriteLine($"[Dashboard] Error loading shops: {ex}"); + Console.Error.WriteLine($"[Dashboard] Error loading data: {ex}"); } finally { @@ -221,4 +302,37 @@ } } + private async Task PublishShopAsync(Guid shopId) + { + var ok = await DataService.PublishShopAsync(shopId); + if (ok) + { + _shops = await DataService.GetShopsAsync(); + StateHasChanged(); + } + } + + private async Task LoadHealthAsync() + { + _healthLoading = true; + try + { + _serviceHealth = await DataService.CheckServicesHealthAsync(); + _healthCheckedAt = DateTime.Now; + } + catch + { + _serviceHealth = new(); + } + finally + { + _healthLoading = false; + } + } + + private async Task RefreshHealthAsync() + { + await LoadHealthAsync(); + StateHasChanged(); + } } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopTables.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopTables.razor index 1c2d531f..c32f83ea 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopTables.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Admin/Shop/ShopTables.razor @@ -148,7 +148,7 @@ else if (SubSection == "rooms") Đang hát: @_tables.Count(t => t.Status == "occupied") Đã đặt: @_tables.Count(t => t.Status == "reserved") - @@ -157,10 +157,11 @@ else if (SubSection == "rooms")

@(_editingTableId.HasValue ? "Chỉnh sửa phòng" : "Thêm phòng mới")

-
+
+
@@ -183,7 +184,7 @@ else if (SubSection == "rooms") var borderColor = room.Status switch { "available" => "rgba(139,92,246,0.3)", "occupied" => "rgba(239,68,68,0.3)", "reserved" => "rgba(245,158,11,0.3)", _ => "rgba(107,107,111,0.3)" }; var statusColor = room.Status switch { "available" => "#8B5CF6", "occupied" => "#EF4444", "reserved" => "#F59E0B", _ => "#6B6B6F" }; var statusText = room.Status switch { "available" => "Trống", "occupied" => "Đang hát", "reserved" => "Đã đặt", "cleaning" => "Dọn dẹp", _ => room.Status }; - var roomType = (room.Zone ?? "").ToLower() switch { var z when z.Contains("vip") => ("VIP", "#F59E0B", 200000m), var z when z.Contains("party") => ("Party", "#EC4899", 350000m), _ => ("Standard", "#8B5CF6", 120000m) }; + var roomType = (room.Zone ?? "").ToLower() switch { var z when z.Contains("vip") => ("VIP", "#F59E0B"), var z when z.Contains("party") => ("Party", "#EC4899"), _ => ("Standard", "#8B5CF6") };
@@ -195,7 +196,7 @@ else if (SubSection == "rooms") Phòng @room.TableNumber
@(room.Zone ?? "Standard") • @room.Capacity chỗ
-
@ShopHelpers.FormatVND(roomType.Item3)/giờ
+
@ShopHelpers.FormatVND(room.HourlyRate)/giờ
@statusText @@ -204,7 +205,7 @@ else if (SubSection == "rooms") { var elapsed = DateTime.UtcNow - (room.StartedAt ?? DateTime.UtcNow); var hours = Math.Max(1, (int)Math.Ceiling(elapsed.TotalHours)); - var bill = hours * roomType.Item3; + var bill = hours * room.HourlyRate;
@room.GuestCount khách • Bắt đầu @(room.StartedAt?.ToString("HH:mm") ?? "—")
@@ -349,6 +350,7 @@ else if (SubSection == "zones") private string _newTableNumber = ""; private int _newTableCapacity = 4; private string _newTableZone = ""; + private decimal _newHourlyRate; private string? _tableFormMessage; private bool _tableFormSuccess; // Zone form state @@ -431,6 +433,7 @@ else if (SubSection == "zones") _newTableNumber = table.TableNumber; _newTableCapacity = table.Capacity; _newTableZone = table.Zone ?? ""; + _newHourlyRate = table.HourlyRate; _tableFormMessage = null; _showTableForm = true; } @@ -444,9 +447,9 @@ else if (SubSection == "zones") } try { - await DataService.CreateTableAsync(new PosDataService.CreateTableRequest(ShopId, _newTableNumber, _newTableCapacity, _newTableZone)); + await DataService.CreateTableAsync(new PosDataService.CreateTableRequest(ShopId, _newTableNumber, _newTableCapacity, _newTableZone, _newHourlyRate > 0 ? _newHourlyRate : null)); _tableFormMessage = $"Đã thêm bàn '{_newTableNumber}' thành công!"; _tableFormSuccess = true; - _newTableNumber = ""; _newTableCapacity = 4; _newTableZone = ""; + _newTableNumber = ""; _newTableCapacity = 4; _newTableZone = ""; _newHourlyRate = 0; if (ShopId != Guid.Empty) _tables = await DataService.GetTablesAsync(ShopId); } catch (Exception ex) { _tableFormMessage = $"Lỗi: {ex.Message}"; _tableFormSuccess = false; } @@ -461,7 +464,7 @@ else if (SubSection == "zones") } try { - await DataService.UpdateTableAsync(_editingTableId.Value, new PosDataService.CreateTableRequest(ShopId, _newTableNumber, _newTableCapacity, _newTableZone)); + await DataService.UpdateTableAsync(_editingTableId.Value, new PosDataService.CreateTableRequest(ShopId, _newTableNumber, _newTableCapacity, _newTableZone, _newHourlyRate > 0 ? _newHourlyRate : null)); _tableFormMessage = $"Đã cập nhật bàn '{_newTableNumber}' thành công!"; _tableFormSuccess = true; _editingTableId = null; if (ShopId != Guid.Empty) _tables = await DataService.GetTablesAsync(ShopId); diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/KaraokeDesktop.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/KaraokeDesktop.razor index 0522e08b..5b7c8781 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/KaraokeDesktop.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Karaoke/KaraokeDesktop.razor @@ -143,16 +143,28 @@
+
+ Giá/giờ + @FormatPrice(_room?.HourlyRate ?? 0) +
Thời gian @_elapsed.ToString(@"h\:mm") giờ
+
+ Tiền phòng + @FormatPrice(RoomCost) +
@* ═══ ACTION BUTTONS / NÚT HÀNH ĐỘNG ═══ *@ @@ -130,13 +138,17 @@
+ + diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/locales/en-US.json b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/locales/en-US.json index dd8d5ebc..0061b472 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/locales/en-US.json +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/locales/en-US.json @@ -420,9 +420,20 @@ "Dashboard_CreateStoreNow": "Create Store Now", "Dashboard_QuickActions": "Quick Actions", "Dashboard_QA_CreateStore": "Create new store", - "Dashboard_QA_SystemSettings": "System Settings", + "Dashboard_QA_ManageStaff": "Manage Staff", "Dashboard_QA_Permissions": "Permissions", + "Dashboard_QA_Devices": "Device Management", + "Dashboard_QA_Integrations": "Third-party Integrations", + "Dashboard_QA_AuditLog": "Activity Log", + "Dashboard_QA_SystemSettings": "System Settings", "Dashboard_SystemStatus": "System Status", + "Dashboard_RefreshStatus": "Refresh", + "Dashboard_CheckingServices": "Checking services...", + "Dashboard_LastChecked": "Last checked at", + "Dashboard_FinishSetup": "Finish Setup", + "Dashboard_Stats_Orders": "orders", + "Dashboard_Stats_Staff": "staff", + "Dashboard_Stats_Products": "products", "Dashboard_Status_Open": "Open", "Dashboard_Status_Setup": "Setting up" } \ No newline at end of file diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/locales/vi-VN.json b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/locales/vi-VN.json index ed42fc94..7c143558 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/locales/vi-VN.json +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/wwwroot/locales/vi-VN.json @@ -420,9 +420,20 @@ "Dashboard_CreateStoreNow": "Tạo cửa hàng ngay", "Dashboard_QuickActions": "Thao tác nhanh", "Dashboard_QA_CreateStore": "Tạo cửa hàng mới", - "Dashboard_QA_SystemSettings": "Cài đặt hệ thống", + "Dashboard_QA_ManageStaff": "Quản lý nhân viên", "Dashboard_QA_Permissions": "Phân quyền", + "Dashboard_QA_Devices": "Quản lý thiết bị", + "Dashboard_QA_Integrations": "Tích hợp bên thứ ba", + "Dashboard_QA_AuditLog": "Nhật ký hoạt động", + "Dashboard_QA_SystemSettings": "Cài đặt hệ thống", "Dashboard_SystemStatus": "Trạng thái hệ thống", + "Dashboard_RefreshStatus": "Làm mới", + "Dashboard_CheckingServices": "Đang kiểm tra dịch vụ...", + "Dashboard_LastChecked": "Cập nhật lúc", + "Dashboard_FinishSetup": "Hoàn thành thiết lập", + "Dashboard_Stats_Orders": "đơn", + "Dashboard_Stats_Staff": "NV", + "Dashboard_Stats_Products": "SP", "Dashboard_Status_Open": "Đang mở", "Dashboard_Status_Setup": "Thiết lập" } \ No newline at end of file diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/ShopController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/ShopController.cs index 277b68b9..047e709f 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/ShopController.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/ShopController.cs @@ -67,6 +67,14 @@ public class ShopController : ControllerBase public Task GetShopStats() => _merchant.GetAsync("/api/v1/shops/stats").ProxyAsync(); + /// + /// EN: Publish a shop (draft → active). + /// VI: Xuất bản cửa hàng (draft → active). + /// + [HttpPost("shops/{shopId:guid}/publish")] + public Task PublishShop(Guid shopId) => + _merchant.PostAsync($"/api/v1/shops/{shopId}/publish", null).ProxyAsync(); + /// /// EN: Get device tokens registered for this merchant's staff. /// VI: Lấy danh sách device token đã đăng ký cho nhân viên của merchant. diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateTableCommand.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateTableCommand.cs index 5462cf5a..bd897a89 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateTableCommand.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateTableCommand.cs @@ -13,7 +13,8 @@ public record CreateTableCommand( Guid ShopId, string TableNumber, int Capacity, - string? Zone = null + string? Zone = null, + decimal? HourlyRate = null ) : IRequest; /// diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateTableCommandHandler.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateTableCommandHandler.cs index 86cc8b70..5abbb7f4 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateTableCommandHandler.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/CreateTableCommandHandler.cs @@ -48,6 +48,9 @@ public class CreateTableCommandHandler : IRequestHandler 0) + table.SetHourlyRate(request.HourlyRate.Value); + await _tableRepository.AddAsync(table, cancellationToken); await _tableRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken); diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateTableCommand.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateTableCommand.cs index 8226d178..65d54d95 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateTableCommand.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateTableCommand.cs @@ -14,5 +14,6 @@ public record UpdateTableCommand( int? Capacity = null, string? Zone = null, int? PositionX = null, - int? PositionY = null + int? PositionY = null, + decimal? HourlyRate = null ) : IRequest; diff --git a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateTableCommandHandler.cs b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateTableCommandHandler.cs index 8c966def..7f7be78a 100644 --- a/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateTableCommandHandler.cs +++ b/services/fnb-engine-net/src/FnbEngine.API/Application/Commands/UpdateTableCommandHandler.cs @@ -34,6 +34,9 @@ public class UpdateTableCommandHandler : IRequestHandler { Success = true, Data = result }); @@ -158,7 +160,8 @@ public record CreateTableRequest( Guid ShopId, string TableNumber, int Capacity, - string? Zone = null); + string? Zone = null, + decimal? HourlyRate = null); /// /// EN: Request to update table details. @@ -168,7 +171,8 @@ public record UpdateTableRequest( int? Capacity = null, string? Zone = null, int? PositionX = null, - int? PositionY = null); + int? PositionY = null, + decimal? HourlyRate = null); /// /// EN: Request to change table status. diff --git a/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/TableAggregate/Table.cs b/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/TableAggregate/Table.cs index d2cb488e..9980d706 100644 --- a/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/TableAggregate/Table.cs +++ b/services/fnb-engine-net/src/FnbEngine.Domain/AggregatesModel/TableAggregate/Table.cs @@ -20,6 +20,7 @@ public class Table : Entity, IAggregateRoot private int? _positionX; private int? _positionY; private string? _qrToken; + private decimal _hourlyRate; private DateTime _createdAt; private DateTime? _updatedAt; @@ -27,11 +28,12 @@ public class Table : Entity, IAggregateRoot public string TableNumber => _tableNumber; public int Capacity => _capacity; public string? Zone => _zone; - public TableStatus Status => _status; + public TableStatus Status => _status ?? Enumeration.FromValue(StatusId); public int StatusId { get; private set; } public int? PositionX => _positionX; public int? PositionY => _positionY; public string? QrToken => _qrToken; + public decimal HourlyRate => _hourlyRate; public DateTime CreatedAt => _createdAt; public DateTime? UpdatedAt => _updatedAt; @@ -58,8 +60,14 @@ public class Table : Entity, IAggregateRoot _createdAt = DateTime.UtcNow; } + private void EnsureStatusLoaded() + { + _status ??= Enumeration.FromValue(StatusId); + } + public void MarkAsOccupied() { + EnsureStatusLoaded(); if (_status != TableStatus.Available && _status != TableStatus.Reserved) throw new DomainException($"Cannot occupy table with status {_status.Name}"); @@ -70,6 +78,7 @@ public class Table : Entity, IAggregateRoot public void MarkAsAvailable() { + EnsureStatusLoaded(); _status = TableStatus.Available; StatusId = TableStatus.Available.Id; _updatedAt = DateTime.UtcNow; @@ -77,11 +86,18 @@ public class Table : Entity, IAggregateRoot public void MarkAsCleaning() { + EnsureStatusLoaded(); _status = TableStatus.Cleaning; StatusId = TableStatus.Cleaning.Id; _updatedAt = DateTime.UtcNow; } + public void SetHourlyRate(decimal rate) + { + _hourlyRate = rate; + _updatedAt = DateTime.UtcNow; + } + public void SetPosition(int x, int y) { _positionX = x; diff --git a/services/fnb-engine-net/src/FnbEngine.Infrastructure/EntityConfigurations/TableEntityTypeConfiguration.cs b/services/fnb-engine-net/src/FnbEngine.Infrastructure/EntityConfigurations/TableEntityTypeConfiguration.cs index fab5d0f1..f6bbe104 100644 --- a/services/fnb-engine-net/src/FnbEngine.Infrastructure/EntityConfigurations/TableEntityTypeConfiguration.cs +++ b/services/fnb-engine-net/src/FnbEngine.Infrastructure/EntityConfigurations/TableEntityTypeConfiguration.cs @@ -58,6 +58,12 @@ public class TableEntityTypeConfiguration : IEntityTypeConfiguration .HasColumnName("qr_token") .HasMaxLength(64); + builder.Property(t => t.HourlyRate) + .HasField("_hourlyRate") + .HasColumnName("hourly_rate") + .HasPrecision(18, 2) + .HasDefaultValue(0m); + builder.Property(t => t.CreatedAt) .HasField("_createdAt") .HasColumnName("created_at")