refactor(web-client-tpos): unify JSON serialization for API client, streamline authentication flows, and update UI layouts and navigation.
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<a href="/#features" class="tpos-nav-link">@L["Nav_Features"]</a>
|
||||
<a href="/#pricing" class="tpos-nav-link">@L["Nav_Pricing"]</a>
|
||||
<a href="/auth/login" class="tpos-nav-link">@L["Nav_Login"]</a>
|
||||
<a href="/auth/register" class="btn-accent">@L["Nav_FreeTrial"]</a>
|
||||
<a href="/register" class="btn-accent">@L["Nav_FreeTrial"]</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@inherits LayoutComponentBase
|
||||
@inject IStringLocalizer<MainLayout> L
|
||||
|
||||
<MudThemeProvider IsDarkMode="true" Theme="_theme" />
|
||||
<MudThemeProvider IsDarkMode="true" Theme="AppTheme.DefaultDark" />
|
||||
<MudPopoverProvider />
|
||||
<MudDialogProvider />
|
||||
<MudSnackbarProvider />
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="tpos-nav-links">
|
||||
<a href="#features" class="tpos-nav-link">@L["Nav_Features"]</a>
|
||||
<a href="#pricing" class="tpos-nav-link">@L["Nav_Pricing"]</a>
|
||||
<a href="#" class="tpos-nav-link">@L["Nav_Contact"]</a>
|
||||
<a href="/home#contact" class="tpos-nav-link">@L["Nav_Contact"]</a>
|
||||
|
||||
<!-- Language Switcher -->
|
||||
<LanguageSwitcher />
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="tpos-mobile-drawer">
|
||||
<a href="#features" class="tpos-mobile-link" @onclick="CloseMobileMenu">@L["Nav_Features"]</a>
|
||||
<a href="#pricing" class="tpos-mobile-link" @onclick="CloseMobileMenu">@L["Nav_Pricing"]</a>
|
||||
<a href="#" class="tpos-mobile-link" @onclick="CloseMobileMenu">@L["Nav_Contact"]</a>
|
||||
<a href="/home#contact" class="tpos-mobile-link" @onclick="CloseMobileMenu">@L["Nav_Contact"]</a>
|
||||
<a href="/login" class="tpos-mobile-link" @onclick="CloseMobileMenu">@L["Nav_Login"]</a>
|
||||
<div class="tpos-mobile-actions">
|
||||
<LanguageSwitcher />
|
||||
@@ -64,21 +64,4 @@
|
||||
|
||||
private void ToggleMobileMenu() => _mobileMenuOpen = !_mobileMenuOpen;
|
||||
private void CloseMobileMenu() => _mobileMenuOpen = false;
|
||||
|
||||
private MudTheme _theme = new()
|
||||
{
|
||||
PaletteDark = new PaletteDark()
|
||||
{
|
||||
Primary = "#FF5C00",
|
||||
PrimaryContrastText = "#FFFFFF",
|
||||
AppbarBackground = "rgba(10,10,11,0.85)",
|
||||
AppbarText = "#FFFFFF",
|
||||
Background = "#0A0A0B",
|
||||
Surface = "#111113",
|
||||
TextPrimary = "#FFFFFF",
|
||||
TextSecondary = "#ADADB0",
|
||||
ActionDefault = "#FFFFFF",
|
||||
LinesDefault = "#1F1F23"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<MudNavMenu>
|
||||
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Rounded.Home">
|
||||
Home
|
||||
</MudNavLink>
|
||||
<MudNavLink Href="/products" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Rounded.ShoppingCart">
|
||||
Products
|
||||
</MudNavLink>
|
||||
<MudNavLink Href="/auth" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Rounded.Person">
|
||||
Auth
|
||||
</MudNavLink>
|
||||
<MudNavLink Href="/counter" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Rounded.Add">
|
||||
Counter
|
||||
</MudNavLink>
|
||||
<MudNavLink Href="/weather" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Rounded.Cloud">
|
||||
Weather
|
||||
</MudNavLink>
|
||||
</MudNavMenu>
|
||||
@@ -1,83 +0,0 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
min-height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
<span style="width:6px;height:6px;border-radius:100px;background:currentColor;"></span>
|
||||
<span>Online</span>
|
||||
</div>
|
||||
<span style="font-size:13px;color:var(--pos-text-secondary);">@DateTime.Now.ToString("HH:mm")</span>
|
||||
<span style="font-size:13px;color:var(--pos-text-secondary);">@_currentTime</span>
|
||||
<button class="admin-icon-btn" @onclick="GoToAdmin" title="Admin">
|
||||
<i data-lucide="settings"></i>
|
||||
</button>
|
||||
@@ -38,11 +38,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/auth/email-sent"
|
||||
@layout AuthLayout
|
||||
@using WebClientTpos.Client.Components.Auth
|
||||
@inherits AuthBase
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/forgot-password"
|
||||
@layout AuthLayout
|
||||
@using WebClientTpos.Shared.DTOs
|
||||
@using WebClientTpos.Shared
|
||||
@inject HttpClient Http
|
||||
|
||||
@@ -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">
|
||||
<ChildContent>
|
||||
<div class="auth-form">
|
||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<div style="margin-bottom:16px;padding:12px 16px;border-radius:8px;background:rgba(239,68,68,0.12);color:#EF4444;font-size:14px;display:flex;align-items:center;gap:8px;">
|
||||
<i data-lucide="alert-circle" style="width:16px;height:16px;flex-shrink:0;"></i>
|
||||
<span>@_errorMessage</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<AuthInput Label="@L["Auth_Forgot_InputLabel"]"
|
||||
InputType="text"
|
||||
Placeholder="@L["Auth_Forgot_InputPlaceholder"]"
|
||||
PrefixIcon="mail"
|
||||
InputId="forgot-email" />
|
||||
InputId="forgot-email"
|
||||
Value="@_email"
|
||||
ValueChanged="@(e => _email = e.Value?.ToString() ?? "")" />
|
||||
</div>
|
||||
|
||||
<AuthButton Variant="orange" OnClick="HandleSubmit">
|
||||
|
||||
<AuthButton Variant="orange" Disabled="@_isLoading" Loading="@_isLoading" OnClick="HandleSubmit">
|
||||
@L["Auth_Forgot_Submit"]
|
||||
</AuthButton>
|
||||
</ChildContent>
|
||||
@@ -42,13 +53,13 @@
|
||||
Icon="mail-check"
|
||||
IconClass="auth-icon--success">
|
||||
<ChildContent>
|
||||
<span class="auth-highlight">user@example.com</span>
|
||||
|
||||
<span class="auth-highlight">@_email</span>
|
||||
|
||||
<AuthButton Variant="orange" IconName="external-link">
|
||||
@L["Auth_Forgot_OpenEmail"]
|
||||
</AuthButton>
|
||||
|
||||
<AuthButton Variant="outline">
|
||||
|
||||
<AuthButton Variant="outline" OnClick="HandleResend">
|
||||
@L["Auth_Forgot_ResendEmail"]
|
||||
</AuthButton>
|
||||
</ChildContent>
|
||||
@@ -65,10 +76,46 @@
|
||||
</div>
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/auth/login/admin"
|
||||
@layout AuthLayout
|
||||
@using WebClientTpos.Client.Components.Auth
|
||||
@using WebClientTpos.Client.Services
|
||||
@using WebClientTpos.Shared.DTOs
|
||||
|
||||
@@ -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"]" />
|
||||
|
||||
|
||||
<div class="auth-form-panel">
|
||||
<AuthCard Title="@L["Auth_Branch_Title"]"
|
||||
Subtitle="@L["Auth_Branch_Subtitle"]"
|
||||
Transparent="true">
|
||||
<ChildContent>
|
||||
<div class="auth-form">
|
||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<div style="margin-bottom:16px;padding:12px 16px;border-radius:8px;background:rgba(239,68,68,0.12);color:#EF4444;font-size:14px;display:flex;align-items:center;gap:8px;">
|
||||
<i data-lucide="alert-circle" style="width:16px;height:16px;flex-shrink:0;"></i>
|
||||
<span>@_errorMessage</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<AuthInput Label="@L["Auth_Branch_EmailLabel"]"
|
||||
InputType="text"
|
||||
Placeholder="@L["Auth_Branch_EmailPlaceholder"]"
|
||||
PrefixIcon="mail"
|
||||
InputId="branch-email"
|
||||
AutoComplete="email" />
|
||||
|
||||
AutoComplete="email"
|
||||
Value="@_email"
|
||||
ValueChanged="@(e => _email = e.Value?.ToString() ?? "")" />
|
||||
|
||||
<AuthInput Label="@L["Auth_Branch_PasswordLabel"]"
|
||||
InputType="password"
|
||||
Placeholder="••••••••"
|
||||
PrefixIcon="lock"
|
||||
InputId="branch-password"
|
||||
AutoComplete="current-password"
|
||||
ForgotPasswordLink="/auth/forgot-password"
|
||||
Value="@_password"
|
||||
ValueChanged="@(e => _password = e.Value?.ToString() ?? "")"
|
||||
ForgotPasswordLink="/forgot-password"
|
||||
ForgotPasswordText="@L["Auth_Branch_ForgotPwd"]" />
|
||||
</div>
|
||||
|
||||
<AuthButton Variant="orange">
|
||||
@L["Auth_Branch_Submit"]
|
||||
|
||||
<AuthButton Variant="orange" Disabled="@_isLoading" Loading="@_isLoading" OnClick="HandleLogin">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<span>Đang xử lý...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@L["Auth_Branch_Submit"]
|
||||
}
|
||||
</AuthButton>
|
||||
</ChildContent>
|
||||
<FooterContent>
|
||||
<span class="auth-footer-text">
|
||||
@L["Auth_Branch_NoAccount"] <a href="/auth/register">@L["Auth_Branch_Contact"]</a>
|
||||
@L["Auth_Branch_NoAccount"] <a href="/register">@L["Auth_Branch_Contact"]</a>
|
||||
</span>
|
||||
</FooterContent>
|
||||
</AuthCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
<ChildContent>
|
||||
<div class="auth-form">
|
||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<div style="margin-bottom:16px;padding:12px 16px;border-radius:8px;background:rgba(239,68,68,0.12);color:#EF4444;font-size:14px;display:flex;align-items:center;gap:8px;">
|
||||
<i data-lucide="alert-circle" style="width:16px;height:16px;flex-shrink:0;"></i>
|
||||
<span>@_errorMessage</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<AuthInput Label="@L["Auth_Staff_IdLabel"]"
|
||||
InputType="text"
|
||||
Placeholder="@L["Auth_Staff_IdPlaceholder"]"
|
||||
PrefixIcon="user"
|
||||
InputId="staff-id"
|
||||
AutoComplete="username" />
|
||||
|
||||
AutoComplete="username"
|
||||
Value="@_email"
|
||||
ValueChanged="@(e => _email = e.Value?.ToString() ?? "")" />
|
||||
|
||||
<AuthInput Label="@L["Auth_Staff_PasswordLabel"]"
|
||||
InputType="password"
|
||||
Placeholder="••••••••"
|
||||
PrefixIcon="lock"
|
||||
InputId="staff-password"
|
||||
AutoComplete="current-password" />
|
||||
AutoComplete="current-password"
|
||||
Value="@_password"
|
||||
ValueChanged="@(e => _password = e.Value?.ToString() ?? "")" />
|
||||
</div>
|
||||
|
||||
<AuthButton Variant="green" IconName="log-in">
|
||||
@L["Auth_Staff_Submit"]
|
||||
|
||||
<AuthButton Variant="green" IconName="log-in" Disabled="@_isLoading" Loading="@_isLoading" OnClick="HandleLogin">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<span>Đang xử lý...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@L["Auth_Staff_Submit"]
|
||||
}
|
||||
</AuthButton>
|
||||
</ChildContent>
|
||||
<FooterContent>
|
||||
@@ -58,8 +79,53 @@
|
||||
</div>
|
||||
</div>
|
||||
<span class="auth-footer-text">
|
||||
@L["Auth_Staff_ForgotPwd"] <a href="/auth/forgot-password">@L["Auth_Staff_ContactManager"]</a>
|
||||
@L["Auth_Staff_ForgotPwd"] <a href="/forgot-password">@L["Auth_Staff_ContactManager"]</a>
|
||||
</span>
|
||||
</FooterContent>
|
||||
</AuthCard>
|
||||
</div>
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/auth/otp-verify"
|
||||
@layout AuthLayout
|
||||
@using WebClientTpos.Client.Components.Auth
|
||||
@inherits AuthBase
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Profile> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Attach Bearer token to HttpClient before API calls.
|
||||
/// VI: Đính kèm Bearer token vào HttpClient trước khi gọi API.
|
||||
/// </summary>
|
||||
private void AttachToken()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(AuthState.Token))
|
||||
{
|
||||
Http.DefaultRequestHeaders.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", AuthState.Token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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<ApiResponse<UserProfileDto>>();
|
||||
userProfile = result?.Data;
|
||||
var info = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
|
||||
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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/auth/register/customer"
|
||||
@layout AuthLayout
|
||||
@using WebClientTpos.Client.Components.Auth
|
||||
@inherits AuthBase
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/reset-password"
|
||||
@layout AuthLayout
|
||||
@using WebClientTpos.Shared.DTOs
|
||||
@using WebClientTpos.Shared
|
||||
@inject HttpClient Http
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/auth/two-factor"
|
||||
@layout AuthLayout
|
||||
@using WebClientTpos.Client.Components.Auth
|
||||
@inherits AuthBase
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/verify-email"
|
||||
@layout AuthLayout
|
||||
@using WebClientTpos.Shared
|
||||
@inject HttpClient Http
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<h1 class="home-hero__title">@L["HeroHeadline"]</h1>
|
||||
<p class="home-hero__subtitle">@L["HeroSubtext"]</p>
|
||||
<div class="home-hero__actions">
|
||||
<a href="/auth/register" class="home-hero__btn home-hero__btn--primary">
|
||||
<a href="/register" class="home-hero__btn home-hero__btn--primary">
|
||||
<i data-lucide="zap"></i>
|
||||
@L["HeroCTA_Primary"]
|
||||
</a>
|
||||
@@ -39,23 +39,23 @@
|
||||
@* Verticals Showcase *@
|
||||
<div class="home-verticals">
|
||||
<div class="home-verticals__grid">
|
||||
<a href="/auth/register" class="home-vertical-card">
|
||||
<a href="/register" class="home-vertical-card">
|
||||
<i data-lucide="coffee"></i>
|
||||
<span>Café</span>
|
||||
</a>
|
||||
<a href="/auth/register" class="home-vertical-card">
|
||||
<a href="/register" class="home-vertical-card">
|
||||
<i data-lucide="utensils-crossed"></i>
|
||||
<span>@L["Industry_Restaurant_Title"]</span>
|
||||
</a>
|
||||
<a href="/auth/register" class="home-vertical-card">
|
||||
<a href="/register" class="home-vertical-card">
|
||||
<i data-lucide="mic"></i>
|
||||
<span>@L["Industry_Karaoke_Title"]</span>
|
||||
</a>
|
||||
<a href="/auth/register" class="home-vertical-card">
|
||||
<a href="/register" class="home-vertical-card">
|
||||
<i data-lucide="sparkles"></i>
|
||||
<span>@L["Industry_Spa_Title"]</span>
|
||||
</a>
|
||||
<a href="/auth/register" class="home-vertical-card">
|
||||
<a href="/register" class="home-vertical-card">
|
||||
<i data-lucide="shopping-bag"></i>
|
||||
<span>@L["Vertical_Retail"]</span>
|
||||
</a>
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
@layout PosLayout
|
||||
@inherits PosBase
|
||||
@using WebClientTpos.Client.Services
|
||||
@inject AuthService AuthService
|
||||
@inject PosDataService DataService
|
||||
@inject IJSRuntime JS
|
||||
|
||||
|
||||
@@ -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!;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Shop ID from route — injected into every POS page.
|
||||
@@ -48,6 +51,16 @@ public abstract class PosBase : ComponentBase
|
||||
/// </summary>
|
||||
protected bool HasActiveShift => !string.IsNullOrEmpty(CurrentShiftId);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await AuthService.TryRestoreSessionAsync();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Format Vietnamese currency for POS display (compact).
|
||||
/// VI: Định dạng tiền tệ VND cho POS (gọn).
|
||||
|
||||
@@ -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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<List<CampaignInfo>> GetCampaignsAsync()
|
||||
{ AttachToken(); return await _http.GetFromJsonAsync<List<CampaignInfo>>("api/bff/promotions", _jsonOptions) ?? new(); }
|
||||
{ AttachToken(); return await _http.GetFromJsonAsync<List<CampaignInfo>>("api/bff/campaigns", _jsonOptions) ?? new(); }
|
||||
|
||||
public async Task<bool> 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<bool> 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<bool> 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<bool> 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<CreatePosOrderResponse?> 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<CreatePosOrderResponse>(_jsonOptions);
|
||||
return null;
|
||||
@@ -369,14 +372,14 @@ public class PosDataService
|
||||
public async Task<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> DeleteRecipeAsync(Guid recipeId)
|
||||
{ AttachToken(); var r = await _http.DeleteAsync($"api/bff/recipes/{recipeId}"); return r.IsSuccessStatusCode; }
|
||||
|
||||
@@ -45,6 +45,14 @@ public class FinancialController : ControllerBase
|
||||
public Task<IActionResult> GetPromotions() =>
|
||||
_promotion.GetAsync("/api/v1/promotions").ProxyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get campaigns for current merchant.
|
||||
/// VI: Lấy danh sách chiến dịch của merchant hiện tại.
|
||||
/// </summary>
|
||||
[HttpGet("campaigns")]
|
||||
public Task<IActionResult> GetCampaigns() =>
|
||||
_promotion.GetAsync("/api/v1/campaigns").ProxyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create a campaign.
|
||||
/// VI: Tạo chiến dịch.
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
[HttpPost("schedules")]
|
||||
[HttpPost("staff/schedules")]
|
||||
public Task<IActionResult> 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.
|
||||
/// </summary>
|
||||
[HttpPut("schedules/{scheduleId:guid}")]
|
||||
[HttpPut("staff/schedules/{scheduleId:guid}")]
|
||||
public Task<IActionResult> 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.
|
||||
/// </summary>
|
||||
[HttpDelete("schedules/{scheduleId:guid}")]
|
||||
[HttpDelete("staff/schedules/{scheduleId:guid}")]
|
||||
public Task<IActionResult> DeleteSchedule(Guid scheduleId) =>
|
||||
_merchant.DeleteAsync($"/api/v1/merchants/me/staff/schedules/{scheduleId}").ProxyAsync();
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ void AddServiceClient(string name, string envVar, string defaultUrl)
|
||||
}).AddHttpMessageHandler<WebClientTpos.Server.Infrastructure.AuthForwardingHandler>();
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user