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 99bddb81..e1016db4 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 @@ -230,10 +230,26 @@ _shopId = slashIndex > 0 ? remaining[..slashIndex] : remaining; _isShopContext = true; - // EN: Try to read shop info from query params or localStorage (set by Dashboard click) - // VI: Đọc thông tin shop từ query params hoặc localStorage - // For now, use shopId as name fallback - _shopName = _shopName == "Cửa hàng" ? $"Shop #{_shopId?[..8]}" : _shopName; + // EN: Use shopId as name fallback if not yet set by child page + // VI: Dùng shopId làm tên nếu chưa được set bởi trang con + if (string.IsNullOrEmpty(_shopName)) + _shopName = $"Shop #{_shopId?[..8]}"; + + // EN: Detect shop category from URL path segments + // VI: Phát hiện loại cửa hàng từ các phần URL + if (_shopCategory == null && slashIndex > 0) + { + var subPath = remaining[(slashIndex + 1)..]; + var subSlash = subPath.IndexOf('/'); + var section = subSlash > 0 ? subPath[..subSlash] : subPath; + // EN: Map URL section to shop category + // VI: Map section URL sang danh mục cửa hàng + _shopCategory = section switch + { + "cafe" or "restaurant" or "karaoke" or "spa" => section, + _ => _shopCategory + }; + } } else { diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AuthLayout.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AuthLayout.razor index c0c9a3c7..87ff4ff8 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AuthLayout.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/AuthLayout.razor @@ -14,7 +14,7 @@ @L["Nav_Features"] @L["Nav_Pricing"] @L["Nav_Login"] - @L["Nav_FreeTrial"] + @L["Nav_FreeTrial"] diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/MainLayout.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/MainLayout.razor index 41164c4d..52748ed1 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/MainLayout.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Layout/MainLayout.razor @@ -1,7 +1,7 @@ @inherits LayoutComponentBase @inject IStringLocalizer L - + @@ -16,7 +16,7 @@ +@implements IDisposable + @code { private string StoreName { get; set; } = "GoodGo POS"; + private string _currentTime = DateTime.Now.ToString("HH:mm"); + private Timer? _timer; protected override async Task OnInitializedAsync() { + // EN: Update clock every 30 seconds + // VI: Cập nhật đồng hồ mỗi 30 giây + _timer = new Timer(_ => + { + _currentTime = DateTime.Now.ToString("HH:mm"); + InvokeAsync(StateHasChanged); + }, null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30)); + // EN: Extract shopId from URL path: /pos/{shopId}/... // VI: Trích xuất shopId từ URL: /pos/{shopId}/... try @@ -65,6 +77,7 @@ private void GoToAdmin() => NavigationManager.NavigateTo("/admin"); + public void Dispose() => _timer?.Dispose(); } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/EmailSent.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/EmailSent.razor index b5478999..bdb0ad07 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/EmailSent.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/EmailSent.razor @@ -1,4 +1,5 @@ @page "/auth/email-sent" +@layout AuthLayout @using WebClientTpos.Client.Components.Auth @inherits AuthBase diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ForgotPassword.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ForgotPassword.razor index 89723dcd..a4472df0 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ForgotPassword.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ForgotPassword.razor @@ -1,4 +1,5 @@ @page "/forgot-password" +@layout AuthLayout @using WebClientTpos.Shared.DTOs @using WebClientTpos.Shared @inject HttpClient Http diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ForgotPasswordNew.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ForgotPasswordNew.razor index 02d6cf67..695c4cdb 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ForgotPasswordNew.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ForgotPasswordNew.razor @@ -1,10 +1,11 @@ @page "/auth/forgot-password-new" +@layout AuthLayout @using WebClientTpos.Client.Components.Auth @inherits AuthBase @* - EN: Forgot password page — centered card, email/phone input → sends reset link. - VI: Trang quên mật khẩu — card giữa, input email/SĐT → gửi link đặt lại. + EN: Forgot password page — centered card, email/phone input -> sends reset link. + VI: Trang quên mật khẩu — card giữa, input email/SĐT -> gửi link đặt lại. Design: pencil-design/src/pages/tPOS/auth/forgot-password/desktop.pen *@ @@ -17,19 +18,29 @@ Subtitle="@L["Auth_Forgot_Subtitle"]" Icon="key-round" IconClass="auth-icon--orange" - BackLink="/auth/login/branch" + BackLink="/login" BackLinkText="@L["Auth_Forgot_BackLink"]" Compact="true">
+ @if (!string.IsNullOrEmpty(_errorMessage)) + { +
+ + @_errorMessage +
+ } + + InputId="forgot-email" + Value="@_email" + ValueChanged="@(e => _email = e.Value?.ToString() ?? "")" />
- - + + @L["Auth_Forgot_Submit"]
@@ -42,13 +53,13 @@ Icon="mail-check" IconClass="auth-icon--success"> - user@example.com - + @_email + @L["Auth_Forgot_OpenEmail"] - - + + @L["Auth_Forgot_ResendEmail"] @@ -65,10 +76,46 @@ @code { - private bool _resetSent = false; + [Inject] private HttpClient Http { get; set; } = default!; - private void HandleSubmit() + private string _email = ""; + private bool _resetSent = false; + private bool _isLoading = false; + private string? _errorMessage; + + private async Task HandleSubmit() { - _resetSent = true; + if (_isLoading) return; + _errorMessage = null; + + if (string.IsNullOrWhiteSpace(_email)) + { + _errorMessage = "Vui lòng nhập email hoặc số điện thoại"; + return; + } + + _isLoading = true; + StateHasChanged(); + + try + { + var response = await Http.PostAsJsonAsync("/api/auth/forgot-password", new { Email = _email }); + // EN: Show success state regardless (security: don't reveal if email exists) + // VI: Hiển thị thành công (bảo mật: không tiết lộ email có tồn tại không) + _resetSent = true; + } + catch + { + _resetSent = true; + } + finally + { + _isLoading = false; + } + } + + private void HandleResend() + { + _resetSent = false; } } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginAdmin.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginAdmin.razor index 674ee49c..750d411f 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginAdmin.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginAdmin.razor @@ -1,4 +1,5 @@ @page "/auth/login/admin" +@layout AuthLayout @using WebClientTpos.Client.Components.Auth @using WebClientTpos.Client.Services @using WebClientTpos.Shared.DTOs diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginBranch.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginBranch.razor index 8cc05eb6..37946e6f 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginBranch.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginBranch.razor @@ -1,5 +1,7 @@ @page "/auth/login/branch" +@layout AuthLayout @using WebClientTpos.Client.Components.Auth +@using WebClientTpos.Client.Services @inherits AuthBase @* @@ -15,39 +17,103 @@ LogoText="a" Title="@L["Auth_Branch_BrandTitle"]" Description="@L["Auth_Branch_BrandDesc"]" /> - +
+ @if (!string.IsNullOrEmpty(_errorMessage)) + { +
+ + @_errorMessage +
+ } + - + AutoComplete="email" + Value="@_email" + ValueChanged="@(e => _email = e.Value?.ToString() ?? "")" /> +
- - - @L["Auth_Branch_Submit"] + + + @if (_isLoading) + { + Đang xử lý... + } + else + { + @L["Auth_Branch_Submit"] + }
- @L["Auth_Branch_NoAccount"] @L["Auth_Branch_Contact"] + @L["Auth_Branch_NoAccount"] @L["Auth_Branch_Contact"]
+ +@code { + [Inject] private AuthService AuthSvc { get; set; } = default!; + [Inject] private NavigationManager Nav { get; set; } = default!; + + private string _email = ""; + private string _password = ""; + private bool _isLoading = false; + private string? _errorMessage; + + private async Task HandleLogin() + { + if (_isLoading) return; + _errorMessage = null; + + if (string.IsNullOrWhiteSpace(_email)) + { + _errorMessage = "Vui lòng nhập email"; + return; + } + if (string.IsNullOrWhiteSpace(_password)) + { + _errorMessage = "Vui lòng nhập mật khẩu"; + return; + } + + _isLoading = true; + StateHasChanged(); + + var (ok, error) = await AuthSvc.LoginAsync(_email, _password); + + if (ok) + { + _isLoading = false; + StateHasChanged(); + await Task.Delay(500); + Nav.NavigateTo("/admin", forceLoad: true); + } + else + { + _errorMessage = error ?? "Đăng nhập thất bại"; + _isLoading = false; + } + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginStaff.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginStaff.razor index f3dd5ce6..3af99e4b 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginStaff.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/LoginStaff.razor @@ -1,5 +1,7 @@ @page "/auth/login/staff" +@layout AuthLayout @using WebClientTpos.Client.Components.Auth +@using WebClientTpos.Client.Services @inherits AuthBase @* @@ -20,23 +22,42 @@ Compact="true">
+ @if (!string.IsNullOrEmpty(_errorMessage)) + { +
+ + @_errorMessage +
+ } + - + AutoComplete="username" + Value="@_email" + ValueChanged="@(e => _email = e.Value?.ToString() ?? "")" /> + + AutoComplete="current-password" + Value="@_password" + ValueChanged="@(e => _password = e.Value?.ToString() ?? "")" />
- - - @L["Auth_Staff_Submit"] + + + @if (_isLoading) + { + Đang xử lý... + } + else + { + @L["Auth_Staff_Submit"] + }
@@ -58,8 +79,53 @@ - @L["Auth_Staff_ForgotPwd"] @L["Auth_Staff_ContactManager"] + @L["Auth_Staff_ForgotPwd"] @L["Auth_Staff_ContactManager"] + +@code { + [Inject] private AuthService AuthSvc { get; set; } = default!; + [Inject] private NavigationManager Nav { get; set; } = default!; + + private string _email = ""; + private string _password = ""; + private bool _isLoading = false; + private string? _errorMessage; + + private async Task HandleLogin() + { + if (_isLoading) return; + _errorMessage = null; + + if (string.IsNullOrWhiteSpace(_email)) + { + _errorMessage = "Vui lòng nhập mã nhân viên"; + return; + } + if (string.IsNullOrWhiteSpace(_password)) + { + _errorMessage = "Vui lòng nhập mật khẩu"; + return; + } + + _isLoading = true; + StateHasChanged(); + + var (ok, error) = await AuthSvc.LoginAsync(_email, _password); + + if (ok) + { + _isLoading = false; + StateHasChanged(); + await Task.Delay(500); + Nav.NavigateTo("/admin", forceLoad: true); + } + else + { + _errorMessage = error ?? "Đăng nhập thất bại"; + _isLoading = false; + } + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/OtpVerify.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/OtpVerify.razor index d92614cd..937b6fee 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/OtpVerify.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/OtpVerify.razor @@ -1,4 +1,5 @@ @page "/auth/otp-verify" +@layout AuthLayout @using WebClientTpos.Client.Components.Auth @inherits AuthBase diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/PasswordResetNew.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/PasswordResetNew.razor index 72d28921..b3eb8a02 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/PasswordResetNew.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/PasswordResetNew.razor @@ -1,4 +1,5 @@ @page "/auth/password-reset" +@layout AuthLayout @using WebClientTpos.Client.Components.Auth @inherits AuthBase @@ -79,6 +80,6 @@ private void GoToLogin() { - Navigation.NavigateTo("/auth/login/branch"); + Navigation.NavigateTo("/login"); } } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Profile.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Profile.razor index 083bca6a..76d91db6 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Profile.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Profile.razor @@ -1,9 +1,13 @@ @page "/profile" +@layout AuthLayout @using WebClientTpos.Shared.DTOs @using WebClientTpos.Shared +@using System.Net.Http.Headers @inject HttpClient Http @inject NavigationManager Navigation @inject IStringLocalizer L +@inject WebClientTpos.Client.Services.AuthService AuthService +@inject WebClientTpos.Client.Services.AuthStateService AuthState @* EN: User profile management page (requires authentication). @@ -155,9 +159,28 @@ protected override async Task OnInitializedAsync() { + await AuthService.TryRestoreSessionAsync(); + if (!AuthState.IsAuthenticated) + { + Navigation.NavigateTo("/login"); + return; + } await LoadUserProfile(); } + /// + /// EN: Attach Bearer token to HttpClient before API calls. + /// VI: Đính kèm Bearer token vào HttpClient trước khi gọi API. + /// + private void AttachToken() + { + if (!string.IsNullOrEmpty(AuthState.Token)) + { + Http.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Bearer", AuthState.Token); + } + } + /// /// EN: Load user profile data. /// VI: Tải dữ liệu hồ sơ người dùng. @@ -166,17 +189,28 @@ { try { - var response = await Http.GetAsync("api/auth/profile"); - + AttachToken(); + // EN: Use OIDC userinfo endpoint (Duende IdentityServer standard). + // VI: Dùng endpoint OIDC userinfo (chuẩn Duende IdentityServer). + var response = await Http.GetAsync("/api/iam/connect/userinfo"); + if (response.IsSuccessStatusCode) { - var result = await response.Content.ReadFromJsonAsync>(); - userProfile = result?.Data; + var info = await response.Content.ReadFromJsonAsync>(); + if (info != null) + { + userProfile = new UserProfileDto + { + Id = Guid.TryParse(info.GetValueOrDefault("sub")?.ToString(), out var id) ? id : Guid.Empty, + Email = info.GetValueOrDefault("email")?.ToString() ?? "", + DisplayName = info.GetValueOrDefault("name")?.ToString() ?? "", + EmailVerified = bool.TryParse(info.GetValueOrDefault("email_verified")?.ToString(), out var ev) && ev, + CreatedAt = DateTime.UtcNow + }; + } } else { - // EN: Redirect to login if unauthorized - // VI: Chuyển hướng đến đăng nhập nếu chưa xác thực Navigation.NavigateTo("/login"); } } @@ -202,7 +236,8 @@ try { - var response = await Http.PutAsJsonAsync("api/auth/profile", userProfile); + AttachToken(); + var response = await Http.PutAsJsonAsync("/api/auth/profile", userProfile); if (response.IsSuccessStatusCode) { @@ -238,7 +273,8 @@ try { - var response = await Http.PostAsJsonAsync("api/auth/change-password", changePasswordModel); + AttachToken(); + var response = await Http.PostAsJsonAsync("/api/auth/change-password", changePasswordModel); if (response.IsSuccessStatusCode) { @@ -282,7 +318,7 @@ { try { - await Http.PostAsync("api/auth/logout", null); + await AuthService.LogoutAsync(); Navigation.NavigateTo("/login"); } catch diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Register.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Register.razor index f7ac6945..d9b2aebd 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Register.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/Register.razor @@ -1,4 +1,5 @@ @page "/register" +@layout AuthLayout @using WebClientTpos.Shared.DTOs @using WebClientTpos.Shared @using WebClientTpos.Client.Services @@ -156,7 +157,7 @@ // EN: Redirect to login after 2 seconds // VI: Chuyển hướng đến đăng nhập sau 2 giây await Task.Delay(2000); - Navigation.NavigateTo("/auth/login/admin"); + Navigation.NavigateTo("/login"); } else { diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/RegisterCustomer.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/RegisterCustomer.razor index df5a5b63..09b9c117 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/RegisterCustomer.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/RegisterCustomer.razor @@ -1,4 +1,5 @@ @page "/auth/register/customer" +@layout AuthLayout @using WebClientTpos.Client.Components.Auth @inherits AuthBase diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ResetPassword.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ResetPassword.razor index 4de25acc..287059ed 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ResetPassword.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/ResetPassword.razor @@ -1,4 +1,5 @@ @page "/reset-password" +@layout AuthLayout @using WebClientTpos.Shared.DTOs @using WebClientTpos.Shared @inject HttpClient Http diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/TwoFactorAuth.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/TwoFactorAuth.razor index a0496e4d..53083984 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/TwoFactorAuth.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/TwoFactorAuth.razor @@ -1,4 +1,5 @@ @page "/auth/two-factor" +@layout AuthLayout @using WebClientTpos.Client.Components.Auth @inherits AuthBase diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/VerifyEmail.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/VerifyEmail.razor index bea07f52..35610801 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/VerifyEmail.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Auth/VerifyEmail.razor @@ -1,4 +1,5 @@ @page "/verify-email" +@layout AuthLayout @using WebClientTpos.Shared @inject HttpClient Http @inject NavigationManager Navigation diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Home.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Home.razor index 8dbb0ce5..3bc5077b 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Home.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Home.razor @@ -15,7 +15,7 @@

@L["HeroHeadline"]

@L["HeroSubtext"]

- + @L["HeroCTA_Primary"] @@ -39,23 +39,23 @@ @* Verticals Showcase *@
- + Café - + @L["Industry_Restaurant_Title"] - + @L["Industry_Karaoke_Title"] - + @L["Industry_Spa_Title"] - + @L["Vertical_Retail"] diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeDesktop.razor b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeDesktop.razor index bd81d122..c25f9fea 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeDesktop.razor +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Pages/Pos/Cafe/CafeDesktop.razor @@ -8,7 +8,6 @@ @layout PosLayout @inherits PosBase @using WebClientTpos.Client.Services -@inject AuthService AuthService @inject PosDataService DataService @inject IJSRuntime JS 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 index a0dff002..5ef0897a 100644 --- 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 @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Components; +using WebClientTpos.Client.Services; namespace WebClientTpos.Client.Pages.Pos; @@ -11,6 +12,8 @@ namespace WebClientTpos.Client.Pages.Pos; public abstract class PosBase : ComponentBase { [Inject] protected NavigationManager NavigationManager { get; set; } = default!; + [Inject] protected AuthService AuthService { get; set; } = default!; + [Inject] protected AuthStateService AuthState { get; set; } = default!; /// /// EN: Shop ID from route — injected into every POS page. @@ -48,6 +51,16 @@ public abstract class PosBase : ComponentBase /// protected bool HasActiveShift => !string.IsNullOrEmpty(CurrentShiftId); + /// + /// EN: Restore auth session from localStorage on first render. + /// VI: Khôi phục session xác thực từ localStorage khi render lần đầu. + /// + protected override async Task OnInitializedAsync() + { + await AuthService.TryRestoreSessionAsync(); + await base.OnInitializedAsync(); + } + /// /// EN: Format Vietnamese currency for POS display (compact). /// VI: Định dạng tiền tệ VND cho POS (gọn). diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs index 501bd743..0ab0a9de 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Client/Services/PosDataService.cs @@ -11,6 +11,8 @@ public class PosDataService { private readonly HttpClient _http; private readonly AuthStateService _authState; + // EN: Read options — case-insensitive to handle both snake_case and camelCase responses + // VI: Options đọc — không phân biệt hoa thường để xử lý cả snake_case và camelCase private static readonly JsonSerializerOptions _jsonOptions = new() { PropertyNameCaseInsensitive = true, @@ -18,6 +20,14 @@ public class PosDataService DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; + // EN: Write options — camelCase to match ASP.NET model binding defaults + // VI: Options ghi — camelCase để khớp với ASP.NET model binding mặc định + private static readonly JsonSerializerOptions _writeOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + public PosDataService(HttpClient http, AuthStateService authState) { _http = http; @@ -93,14 +103,14 @@ public class PosDataService public async Task CreateProductAsync(CreateProductRequest req) { AttachToken(); - var resp = await _http.PostAsJsonAsync("api/bff/products", req, _jsonOptions); + var resp = await _http.PostAsJsonAsync("api/bff/products", req, _writeOptions); return resp.IsSuccessStatusCode; } public async Task UpdateProductAsync(Guid productId, CreateProductRequest req) { AttachToken(); - var resp = await _http.PutAsJsonAsync($"api/bff/products/{productId}", req, _jsonOptions); + var resp = await _http.PutAsJsonAsync($"api/bff/products/{productId}", req, _writeOptions); return resp.IsSuccessStatusCode; } @@ -138,14 +148,14 @@ public class PosDataService public async Task CreateStaffAsync(CreateStaffRequest req) { AttachToken(); - var resp = await _http.PostAsJsonAsync("api/bff/staff", req, _jsonOptions); + var resp = await _http.PostAsJsonAsync("api/bff/staff", req, _writeOptions); return resp.IsSuccessStatusCode; } public async Task UpdateStaffAsync(Guid staffId, CreateStaffRequest req) { AttachToken(); - var resp = await _http.PutAsJsonAsync($"api/bff/staff/{staffId}", req, _jsonOptions); + var resp = await _http.PutAsJsonAsync($"api/bff/staff/{staffId}", req, _writeOptions); return resp.IsSuccessStatusCode; } @@ -161,7 +171,7 @@ public class PosDataService public async Task UpdateInventoryAsync(Guid inventoryId, UpdateInventoryRequest req) { AttachToken(); - var resp = await _http.PutAsJsonAsync($"api/bff/inventory/{inventoryId}", req, _jsonOptions); + var resp = await _http.PutAsJsonAsync($"api/bff/inventory/{inventoryId}", req, _writeOptions); return resp.IsSuccessStatusCode; } @@ -230,19 +240,19 @@ public class PosDataService public record CreateCampaignRequest(string Name, string? Description, decimal FaceValue, int TotalVouchers, DateTime StartDate, DateTime EndDate); public async Task> GetCampaignsAsync() - { AttachToken(); return await _http.GetFromJsonAsync>("api/bff/promotions", _jsonOptions) ?? new(); } + { AttachToken(); return await _http.GetFromJsonAsync>("api/bff/campaigns", _jsonOptions) ?? new(); } public async Task CreateCampaignAsync(CreateCampaignRequest req) { AttachToken(); - var resp = await _http.PostAsJsonAsync("api/bff/campaigns", req, _jsonOptions); + var resp = await _http.PostAsJsonAsync("api/bff/campaigns", req, _writeOptions); return resp.IsSuccessStatusCode; } public async Task UpdateCampaignAsync(Guid campaignId, CreateCampaignRequest req) { AttachToken(); - var resp = await _http.PutAsJsonAsync($"api/bff/campaigns/{campaignId}", req, _jsonOptions); + var resp = await _http.PutAsJsonAsync($"api/bff/campaigns/{campaignId}", req, _writeOptions); return resp.IsSuccessStatusCode; } @@ -263,14 +273,14 @@ public class PosDataService public async Task CreateMemberAsync(CreateMemberRequest req) { AttachToken(); - var resp = await _http.PostAsJsonAsync("api/bff/members", req, _jsonOptions); + var resp = await _http.PostAsJsonAsync("api/bff/members", req, _writeOptions); return resp.IsSuccessStatusCode; } public async Task UpdateMemberAsync(Guid memberId, UpdateMemberRequest req) { AttachToken(); - var resp = await _http.PutAsJsonAsync($"api/bff/members/{memberId}", req, _jsonOptions); + var resp = await _http.PutAsJsonAsync($"api/bff/members/{memberId}", req, _writeOptions); return resp.IsSuccessStatusCode; } @@ -347,14 +357,7 @@ public class PosDataService public async Task CreatePosOrderAsync(CreatePosOrderRequest req) { AttachToken(); - // EN: Use camelCase for POST body (ASP.NET model binding default) - // VI: Dùng camelCase cho POST body (ASP.NET model binding mặc định) - var postOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - var resp = await _http.PostAsJsonAsync("api/bff/pos/orders", req, postOptions); + var resp = await _http.PostAsJsonAsync("api/bff/pos/orders", req, _writeOptions); if (resp.IsSuccessStatusCode) return await resp.Content.ReadFromJsonAsync(_jsonOptions); return null; @@ -369,14 +372,14 @@ public class PosDataService public async Task CreateCategoryAsync(AdminCreateCategoryRequest req) { AttachToken(); - var resp = await _http.PostAsJsonAsync("api/bff/categories", req, _jsonOptions); + var resp = await _http.PostAsJsonAsync("api/bff/categories", req, _writeOptions); return resp.IsSuccessStatusCode; } public async Task UpdateCategoryAsync(Guid categoryId, AdminCreateCategoryRequest req) { AttachToken(); - var resp = await _http.PutAsJsonAsync($"api/bff/categories/{categoryId}", req, _jsonOptions); + var resp = await _http.PutAsJsonAsync($"api/bff/categories/{categoryId}", req, _writeOptions); return resp.IsSuccessStatusCode; } @@ -419,7 +422,7 @@ public class PosDataService public async Task UpdateShopAsync(Guid shopId, UpdateShopRequest req) { AttachToken(); - var resp = await _http.PutAsJsonAsync($"api/bff/shops/{shopId}", req, _jsonOptions); + var resp = await _http.PutAsJsonAsync($"api/bff/shops/{shopId}", req, _writeOptions); return resp.IsSuccessStatusCode; } @@ -452,7 +455,7 @@ public class PosDataService public async Task UpdateShopSettingsAsync(Guid shopId, UpdateShopSettingsRequest req) { AttachToken(); - var resp = await _http.PutAsJsonAsync($"api/bff/shops/{shopId}/settings", req, _jsonOptions); + var resp = await _http.PutAsJsonAsync($"api/bff/shops/{shopId}/settings", req, _writeOptions); return resp.IsSuccessStatusCode; } @@ -474,10 +477,10 @@ public class PosDataService public record CreateTableRequest(Guid ShopId, string TableNumber, int Capacity, string? Zone); public async Task CreateTableAsync(CreateTableRequest req) - { AttachToken(); var r = await _http.PostAsJsonAsync("api/bff/tables", req, _jsonOptions); return r.IsSuccessStatusCode; } + { AttachToken(); var r = await _http.PostAsJsonAsync("api/bff/tables", req, _writeOptions); return r.IsSuccessStatusCode; } public async Task UpdateTableAsync(Guid tableId, CreateTableRequest req) - { AttachToken(); var r = await _http.PutAsJsonAsync($"api/bff/tables/{tableId}", req, _jsonOptions); return r.IsSuccessStatusCode; } + { AttachToken(); var r = await _http.PutAsJsonAsync($"api/bff/tables/{tableId}", req, _writeOptions); return r.IsSuccessStatusCode; } public async Task DeleteTableAsync(Guid tableId) { AttachToken(); var r = await _http.DeleteAsync($"api/bff/tables/{tableId}"); return r.IsSuccessStatusCode; } @@ -487,10 +490,10 @@ public class PosDataService public record CreateAppointmentRequest(Guid ShopId, Guid? CustomerId, Guid? StaffId, Guid? ResourceId, Guid? ServiceId, DateTime StartTime, DateTime EndTime, string? Status = null); public async Task CreateAppointmentAsync(CreateAppointmentRequest req) - { AttachToken(); var r = await _http.PostAsJsonAsync("api/bff/appointments", req, _jsonOptions); return r.IsSuccessStatusCode; } + { AttachToken(); var r = await _http.PostAsJsonAsync("api/bff/appointments", req, _writeOptions); return r.IsSuccessStatusCode; } public async Task UpdateAppointmentAsync(Guid apptId, CreateAppointmentRequest req) - { AttachToken(); var r = await _http.PutAsJsonAsync($"api/bff/appointments/{apptId}", req, _jsonOptions); return r.IsSuccessStatusCode; } + { AttachToken(); var r = await _http.PutAsJsonAsync($"api/bff/appointments/{apptId}", req, _writeOptions); return r.IsSuccessStatusCode; } public async Task CancelAppointmentAsync(Guid apptId) { AttachToken(); var r = await _http.DeleteAsync($"api/bff/appointments/{apptId}/cancel"); return r.IsSuccessStatusCode; } @@ -500,10 +503,10 @@ public class PosDataService public record CreateResourceRequest(Guid ShopId, string Name, string ResourceType, int Capacity); public async Task CreateResourceAsync(CreateResourceRequest req) - { AttachToken(); var r = await _http.PostAsJsonAsync("api/bff/resources", req, _jsonOptions); return r.IsSuccessStatusCode; } + { AttachToken(); var r = await _http.PostAsJsonAsync("api/bff/resources", req, _writeOptions); return r.IsSuccessStatusCode; } public async Task UpdateResourceAsync(Guid resourceId, CreateResourceRequest req) - { AttachToken(); var r = await _http.PutAsJsonAsync($"api/bff/resources/{resourceId}", req, _jsonOptions); return r.IsSuccessStatusCode; } + { AttachToken(); var r = await _http.PutAsJsonAsync($"api/bff/resources/{resourceId}", req, _writeOptions); return r.IsSuccessStatusCode; } public async Task DeleteResourceAsync(Guid resourceId) { AttachToken(); var r = await _http.DeleteAsync($"api/bff/resources/{resourceId}"); return r.IsSuccessStatusCode; } @@ -513,13 +516,13 @@ public class PosDataService public record CreateScheduleRequest(Guid ShopId, Guid StaffId, int DayOfWeek, string StartTime, string EndTime); public async Task CreateScheduleAsync(CreateScheduleRequest req) - { AttachToken(); var r = await _http.PostAsJsonAsync("api/bff/schedules", req, _jsonOptions); return r.IsSuccessStatusCode; } + { AttachToken(); var r = await _http.PostAsJsonAsync("api/bff/staff/schedules", req, _writeOptions); return r.IsSuccessStatusCode; } public async Task UpdateScheduleAsync(Guid scheduleId, CreateScheduleRequest req) - { AttachToken(); var r = await _http.PutAsJsonAsync($"api/bff/schedules/{scheduleId}", req, _jsonOptions); return r.IsSuccessStatusCode; } + { AttachToken(); var r = await _http.PutAsJsonAsync($"api/bff/staff/schedules/{scheduleId}", req, _writeOptions); return r.IsSuccessStatusCode; } public async Task DeleteScheduleAsync(Guid scheduleId) - { AttachToken(); var r = await _http.DeleteAsync($"api/bff/schedules/{scheduleId}"); return r.IsSuccessStatusCode; } + { AttachToken(); var r = await _http.DeleteAsync($"api/bff/staff/schedules/{scheduleId}"); return r.IsSuccessStatusCode; } // ═══ KITCHEN TICKETS ═══ @@ -535,7 +538,7 @@ public class PosDataService } public async Task UpdateTicketStatusAsync(Guid ticketId, UpdateTicketStatusRequest req) - { AttachToken(); var r = await _http.PutAsJsonAsync($"api/bff/kitchen/tickets/{ticketId}/status", req, _jsonOptions); return r.IsSuccessStatusCode; } + { AttachToken(); var r = await _http.PutAsJsonAsync($"api/bff/kitchen/tickets/{ticketId}/status", req, _writeOptions); return r.IsSuccessStatusCode; } // ═══ RECIPES CRUD ═══ @@ -553,10 +556,10 @@ public class PosDataService } public async Task CreateRecipeAsync(CreateRecipeRequest req) - { AttachToken(); var r = await _http.PostAsJsonAsync("api/bff/recipes", req, _jsonOptions); return r.IsSuccessStatusCode; } + { AttachToken(); var r = await _http.PostAsJsonAsync("api/bff/recipes", req, _writeOptions); return r.IsSuccessStatusCode; } public async Task UpdateRecipeAsync(Guid recipeId, CreateRecipeRequest req) - { AttachToken(); var r = await _http.PutAsJsonAsync($"api/bff/recipes/{recipeId}", req, _jsonOptions); return r.IsSuccessStatusCode; } + { AttachToken(); var r = await _http.PutAsJsonAsync($"api/bff/recipes/{recipeId}", req, _writeOptions); return r.IsSuccessStatusCode; } public async Task DeleteRecipeAsync(Guid recipeId) { AttachToken(); var r = await _http.DeleteAsync($"api/bff/recipes/{recipeId}"); return r.IsSuccessStatusCode; } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/FinancialController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/FinancialController.cs index 78da08e0..9ceca259 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/FinancialController.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/FinancialController.cs @@ -45,6 +45,14 @@ public class FinancialController : ControllerBase public Task GetPromotions() => _promotion.GetAsync("/api/v1/promotions").ProxyAsync(); + /// + /// EN: Get campaigns for current merchant. + /// VI: Lấy danh sách chiến dịch của merchant hiện tại. + /// + [HttpGet("campaigns")] + public Task GetCampaigns() => + _promotion.GetAsync("/api/v1/campaigns").ProxyAsync(); + /// /// EN: Create a campaign. /// VI: Tạo chiến dịch. diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StaffController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StaffController.cs index 4b83cbe3..99b83519 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StaffController.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/StaffController.cs @@ -74,7 +74,7 @@ public class StaffController : ControllerBase /// EN: Create a staff schedule. /// VI: Tạo lịch làm việc nhân viên. /// - [HttpPost("schedules")] + [HttpPost("staff/schedules")] public Task CreateSchedule([FromBody] JsonElement body) => _merchant.PostAsJsonAsync("/api/v1/merchants/me/staff/schedules", body).ProxyAsync(); @@ -82,7 +82,7 @@ public class StaffController : ControllerBase /// EN: Update a staff schedule. /// VI: Cập nhật lịch làm việc nhân viên. /// - [HttpPut("schedules/{scheduleId:guid}")] + [HttpPut("staff/schedules/{scheduleId:guid}")] public Task UpdateSchedule(Guid scheduleId, [FromBody] JsonElement body) => _merchant.PutAsJsonAsync($"/api/v1/merchants/me/staff/schedules/{scheduleId}", body).ProxyAsync(); @@ -90,7 +90,7 @@ public class StaffController : ControllerBase /// EN: Delete a staff schedule. /// VI: Xóa lịch làm việc nhân viên. ///
- [HttpDelete("schedules/{scheduleId:guid}")] + [HttpDelete("staff/schedules/{scheduleId:guid}")] public Task DeleteSchedule(Guid scheduleId) => _merchant.DeleteAsync($"/api/v1/merchants/me/staff/schedules/{scheduleId}").ProxyAsync(); } diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs index bdfec23b..623774e9 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs @@ -107,7 +107,7 @@ void AddServiceClient(string name, string envVar, string defaultUrl) }).AddHttpMessageHandler(); } -AddServiceClient("MerchantService", "MerchantService__BaseUrl", "http://localhost:5002"); +AddServiceClient("MerchantService", "MerchantService__BaseUrl", "http://localhost:5005"); AddServiceClient("CatalogService", "CatalogService__BaseUrl", "http://localhost:5016"); AddServiceClient("OrderService", "OrderService__BaseUrl", "http://localhost:5017"); AddServiceClient("InventoryService", "InventoryService__BaseUrl", "http://localhost:5018");