Merge: auth workflow fixes into master

Co-authored-by: Velik <hongochai10@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-02-27 08:36:41 +00:00
10 changed files with 124 additions and 125 deletions

View File

@@ -1,4 +1,4 @@
@inherits LayoutComponentBase
@inherits LayoutComponentBase
@inject IStringLocalizer<MainLayout> L
<MudThemeProvider IsDarkMode="true" Theme="_theme" />
@@ -21,8 +21,8 @@
<!-- Language Switcher -->
<LanguageSwitcher />
<a href="/login" class="tpos-nav-link">@L["Nav_Login"]</a>
<a href="#" class="btn-accent">@L["Nav_FreeTrial"]</a>
<a href="/auth/login" class="tpos-nav-link">@L["Nav_Login"]</a>
<a href="/register" class="btn-accent">@L["Nav_FreeTrial"]</a>
</div>
<!-- Mobile hamburger button -->
@@ -44,10 +44,10 @@
<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="/login" class="tpos-mobile-link" @onclick="CloseMobileMenu">@L["Nav_Login"]</a>
<a href="/auth/login" class="tpos-mobile-link" @onclick="CloseMobileMenu">@L["Nav_Login"]</a>
<div class="tpos-mobile-actions">
<LanguageSwitcher />
<a href="#" class="btn-accent btn-accent-lg" style="width:100%; text-align:center;">@L["Nav_FreeTrial"]</a>
<a href="/register" class="btn-accent btn-accent-lg" style="width:100%; text-align:center;">@L["Nav_FreeTrial"]</a>
</div>
</div>
}

View File

@@ -1,6 +1,8 @@
@page "/admin"
@layout AdminLayout
@inherits AdminBase
@inject PosDataService DataService
@using WebClientTpos.Client.Services
@*
EN: Admin Dashboard — overview of business metrics, stores, alerts, and recent activity.
@@ -107,123 +109,47 @@
<i data-lucide="store" style="color:var(--admin-orange-primary);"></i>
Cửa hàng của bạn
</h3>
<a href="/admin/stores" class="admin-panel__action">Quản lý tất cả →</a>
@if (_shops.Count > 0)
{
<a href="/admin/stores" class="admin-panel__action">Quản lý tất cả →</a>
}
</div>
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
@* Store 1: Coffee House Q1 *@
<div class="admin-store-card">
<div class="admin-store-card__top">
<div class="admin-store-card__info">
<div class="admin-store-card__avatar" style="background-color:rgba(255,92,0,0.125);">
<i data-lucide="coffee" style="color:var(--admin-orange-primary);"></i>
</div>
<div>
<div class="admin-store-card__name">Coffee House Q1</div>
<div class="admin-store-card__type">Café • 123 Nguyễn Huệ, Q1</div>
@if (_shops.Count == 0)
{
<div style="text-align:center;padding:40px 20px;">
<i data-lucide="store" style="width:48px;height:48px;color:var(--admin-orange-primary);margin-bottom:16px;"></i>
<h3 style="font-size:18px;font-weight:700;color:var(--pos-text-primary, #FFFFFF);margin:0 0 8px;">Welcome! Tạo cửa hàng đầu tiên</h3>
<p style="font-size:14px;color:var(--pos-text-tertiary, #ADADB0);margin:0 0 20px;">Bắt đầu bằng việc tạo cửa hàng để quản lý kinh doanh của bạn.</p>
<a href="/admin/onboarding/store" class="admin-btn-primary" style="display:inline-flex;align-items:center;gap:8px;">
<i data-lucide="plus" style="width:16px;height:16px;"></i>
Tạo cửa hàng ngay
</a>
</div>
}
else
{
@foreach (var shop in _shops)
{
<div class="admin-store-card">
<div class="admin-store-card__top">
<div class="admin-store-card__info">
<div class="admin-store-card__avatar" style="background-color:rgba(255,92,0,0.125);">
<i data-lucide="@GetShopIcon(shop.Category)" style="color:var(--admin-orange-primary);"></i>
</div>
<div>
<div class="admin-store-card__name">@shop.Name</div>
<div class="admin-store-card__type">@(shop.Category ?? "Shop") • @(shop.Description ?? shop.Slug)</div>
</div>
</div>
<div class="admin-status-badge admin-status-badge--@(shop.Status == "active" ? "online" : "setup")">
<span class="admin-status-badge__dot"></span>
@(shop.Status == "active" ? "Đang mở" : "Thiết lập")
</div>
</div>
</div>
<div class="admin-status-badge admin-status-badge--online">
<span class="admin-status-badge__dot"></span>
Đang mở
</div>
</div>
<div class="admin-store-card__stats">
<div class="admin-store-stat">
<div class="admin-store-stat__value">45.2M</div>
<div class="admin-store-stat__label">Doanh thu</div>
</div>
<div class="admin-store-stat">
<div class="admin-store-stat__value">342</div>
<div class="admin-store-stat__label">Đơn hàng</div>
</div>
<div class="admin-store-stat">
<div class="admin-store-stat__value">5</div>
<div class="admin-store-stat__label">Nhân viên</div>
</div>
<div class="admin-store-stat">
<div class="admin-store-stat__value">48</div>
<div class="admin-store-stat__label">Sản phẩm</div>
</div>
</div>
</div>
@* Store 2: Nhà hàng Q3 *@
<div class="admin-store-card">
<div class="admin-store-card__top">
<div class="admin-store-card__info">
<div class="admin-store-card__avatar" style="background-color:rgba(59,130,246,0.125);">
<i data-lucide="utensils" style="color:#3B82F6;"></i>
</div>
<div>
<div class="admin-store-card__name">Nhà hàng Q3</div>
<div class="admin-store-card__type">Restaurant • 456 Lê Văn Sỹ, Q3</div>
</div>
</div>
<div class="admin-status-badge admin-status-badge--online">
<span class="admin-status-badge__dot"></span>
Đang mở
</div>
</div>
<div class="admin-store-card__stats">
<div class="admin-store-stat">
<div class="admin-store-stat__value">62.8M</div>
<div class="admin-store-stat__label">Doanh thu</div>
</div>
<div class="admin-store-stat">
<div class="admin-store-stat__value">185</div>
<div class="admin-store-stat__label">Đơn hàng</div>
</div>
<div class="admin-store-stat">
<div class="admin-store-stat__value">8</div>
<div class="admin-store-stat__label">Nhân viên</div>
</div>
<div class="admin-store-stat">
<div class="admin-store-stat__value">72</div>
<div class="admin-store-stat__label">Sản phẩm</div>
</div>
</div>
</div>
@* Store 3: Karaoke Star Q7 *@
<div class="admin-store-card">
<div class="admin-store-card__top">
<div class="admin-store-card__info">
<div class="admin-store-card__avatar" style="background-color:rgba(139,92,246,0.125);">
<i data-lucide="mic" style="color:#8B5CF6;"></i>
</div>
<div>
<div class="admin-store-card__name">Karaoke Star Q7</div>
<div class="admin-store-card__type">Karaoke • 789 Nguyễn Thị Thập, Q7</div>
</div>
</div>
<div class="admin-status-badge admin-status-badge--setup">
<span class="admin-status-badge__dot"></span>
Thiết lập
</div>
</div>
<div class="admin-store-card__stats">
<div class="admin-store-stat">
<div class="admin-store-stat__value">--</div>
<div class="admin-store-stat__label">Doanh thu</div>
</div>
<div class="admin-store-stat">
<div class="admin-store-stat__value">--</div>
<div class="admin-store-stat__label">Đơn hàng</div>
</div>
<div class="admin-store-stat">
<div class="admin-store-stat__value">0</div>
<div class="admin-store-stat__label">Nhân viên</div>
</div>
<div class="admin-store-stat">
<div class="admin-store-stat__value">0</div>
<div class="admin-store-stat__label">Sản phẩm</div>
</div>
</div>
<button class="admin-store-card__cta admin-store-card__cta--warning">
<i data-lucide="settings" style="width:14px;height:14px;"></i>
Hoàn tất thiết lập
</button>
</div>
}
}
</div>
</div>
@@ -316,3 +242,34 @@
</div>
</div>
</div>
@code {
private List<PosDataService.ShopInfo> _shops = new();
protected override async Task OnInitializedAsync()
{
IsLoading = true;
try
{
_shops = await DataService.GetShopsAsync();
}
catch
{
_shops = new();
}
finally
{
IsLoading = false;
}
}
private static string GetShopIcon(string? category) => category?.ToLowerInvariant() switch
{
"cafe" or "café" or "coffee" => "coffee",
"restaurant" or "nhà hàng" => "utensils",
"karaoke" => "mic",
"spa" => "sparkles",
"retail" => "shopping-bag",
_ => "store"
};
}

View File

@@ -50,7 +50,7 @@
}
<div class="auth-footer">
<a href="/login" class="link-primary">@L["Auth_ForgotPassword_BackToLogin"]</a>
<a href="/auth/login" class="link-primary">@L["Auth_ForgotPassword_BackToLogin"]</a>
</div>
</section>
</div>

View File

@@ -1,5 +1,5 @@
@page "/auth/login/customer"
@page "/login"
@layout AuthLayout
@using WebClientTpos.Client.Components.Auth
@inherits AuthBase

View File

@@ -92,7 +92,7 @@
<div class="auth-footer">
<span>@L["Auth_Register_HaveAccount"]</span>
<a href="/login" class="link-primary">@L["Auth_Register_LoginLink"]</a>
<a href="/auth/login" class="link-primary">@L["Auth_Register_LoginLink"]</a>
</div>
</section>
</div>
@@ -127,7 +127,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("/login");
Navigation.NavigateTo("/auth/login");
}
else
{

View File

@@ -40,7 +40,7 @@
@message
</div>
<div class="mt-6">
<a href="/login" class="link-primary">@L["Auth_VerifyEmail_GoToLogin"]</a>
<a href="/auth/login" class="link-primary">@L["Auth_VerifyEmail_GoToLogin"]</a>
</div>
}
</section>

View File

@@ -20,6 +20,10 @@ builder.Services.AddSingleton(sp => new HttpClient { BaseAddress = new Uri(new U
// VI: Thêm POS data service cho BFF API calls
builder.Services.AddScoped<WebClientTpos.Client.Services.PosDataService>();
// EN: Add auth state service for role-based redirects
// VI: Thêm auth state service cho điều hướng theo vai trò
builder.Services.AddSingleton<WebClientTpos.Client.Services.AuthStateService>();
// EN: Add MudBlazor services
// VI: Thêm các services của MudBlazor
builder.Services.AddMudServices();

View File

@@ -0,0 +1,38 @@
namespace WebClientTpos.Client.Services;
public class AuthStateService
{
public bool IsAuthenticated { get; private set; }
public string? UserEmail { get; private set; }
public string? UserRole { get; private set; } // "owner", "staff", "customer", "branch"
public string? Token { get; private set; }
public event Action? OnChange;
public void Login(string email, string token, string role)
{
IsAuthenticated = true;
UserEmail = email;
Token = token;
UserRole = role;
OnChange?.Invoke();
}
public void Logout()
{
IsAuthenticated = false;
UserEmail = null;
Token = null;
UserRole = null;
OnChange?.Invoke();
}
public string GetPortalUrl() => UserRole switch
{
"owner" or "admin" => "/admin",
"staff" => "/pos/cafe",
"branch" => "/admin",
"customer" => "/app",
_ => "/auth/login"
};
}

View File

@@ -56,12 +56,11 @@ if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseDeveloperExceptionPage();
app.UseWebAssemblyDebugging();
}
app.UseHttpsRedirection();
// EN: Enable CORS
// VI: Kích hoạt CORS
// EN: Enable CORS
// VI: Kích hoạt CORS
app.UseCors("BlazorClient");

View File

@@ -0,0 +1 @@
{"Version":1,"Id":"40845B4B36348C1B2FD55311A93F4280","Created":"2026-02-26T17:40:10.6713835Z","Algorithm":"RS256","IsX509Certificate":false,"Data":"CfDJ8OE6IQaarJdMrFb-ELKfkmJdiuzUgIT8mztIqfJcZrKHdQw5UiPZvLOdgRvR4ffdcIvMXsaKsH-l0TKza8hmQVhE5RZX2CpQtd_EceQqb4xc7CMzvtCAr6f4HVbbIYtfGOfzzvLPQ2kLhHzoKRVnhFVWKkHswkDekog_H_a6MxDt9WbddI3xuM8c97YA8lsN4hojKfHhKSTE_05eHmXAir6NtMZEgNziU6h99r9EInp7acESdlQ4aUqKapVpDzYb1MTNxxFwggbU_gNlDNwhOZi2-ElWXNJ4pNBwF1ZUjRgydNwdu-qUsYejMkhDPpkX9nXRmU0Hofea-XPDGvcMu5u6YNXB7bRHaY6G1NB1-UGkGbFhM_CQMAuUM8Sp17Zxkgu5YYC9xcWaefdi82xaGl5SGEAV0nfFGZctvqG9YAYR7U2o2hXBOgN2pQuvSdfe4d4IP1KjGa726eVzEMwfzqy-wWpO_UKINHEKEeVHYfhjWvA3zofuCis154kVThw3KFVBOuLCcguF6AqiwXV_veLU-sMaq3sgLwHduxoaNeVZbGAWsqseJC3G1KPjibrQ6Dd1du0uzLMaOUctjPzsDwU4nFpXBUs-pGZuG03vltOJh0U_ankoGW4q5XjsyFQGDFKivdcrufst2DFx_pvD1PtkAawdAoLG9pZy5lrhCxwv_uzDw3pdIK4Q4C-eOpkC-nOsZE3PWfjdA0ouZ09USbj_qeA6mpF0TIafuJTRlz-KkSiy5X4qLWiznGQWO8JmvZGNLx2P8eX_IijhzZaZXknTDRLsmFi5bq1NaEPGvJRPnS_n2E5N2YMC_RBq6ct0pppTnsJaYOip-DPLcA2WUtw_j8QaLD9Y80a0Gv1ky_hU-iivKhUFsvWxvsuJqRFcbFBw5CO-wydUxvDR4ejznen1nAKxh3Jh9b1l26LfMllns9FBcMrTzYYHMkaZFSMXsY-KhaBkcw-46O8aunZlzioxP5oTsaAyI_Ru3jlCJLteCtXoP8c8gpnOsem1praCx6i8n418hmrw1HpK3gdNifrE4fW_M2h5UxzZLzzRZkhvsv_ZgKorpvYH5QrIGzOWUR2DF_wCCTcZbU6PFy0ztTgw_O8NGHi3kAveu_Dff566oWj0oAfNQ0g0lPXnwkK7ZxITHwL47iVhDvEt5OvSR6zTnX4J2J2P3MajQYPkojT3zk5yezEYVp0_hHjQ155f44Q-MoTVaDxJhvF3nN0JXCZ4Xr2wcGiFwGa8OPF0t1H66dapUNwYMfQdPQx8_KiOvSHQUrLtPh-fknasU-dD54TAZotts4zHKRw05W6_7kNaM7mMSCjUd8y1ub2Ae5t0qt1Vt123df-SyBI1vVKFtMPkCpYtTYzgtoR_HEMLUF3bqfoJqXoHvJDBM86DitJYYz-Zcdiukx5rMHSfz6XNQx1m9T8twbXwrowd5K7G3kaDZkXHKtv-Shbsb99gkyq2uHSPDELafyOJfrFQLcoapDuaXCHss8nIhFFZs_YTqs_eGzyUW_tu2fnech-3IO7dlaHmShXJhkE76oEW7eeJCT9WwSbsiSosdU38bCRZeJ05ATIgIdcRGhXF8zeOwEDK-TZvltb0x5JgzQr4BXlPAfi7cQuyyNVLIXz9tyoC0WuFnSS1C1-oJb0ZiW46HpafPzU7eh3is7zwBqv7UkiIZtPUHapJEvxSNNeGM95Mu7h5B1GeoYgurtYfNo4HBkmxsRqsbnEkGuLkfaK33k3ywZZcJx7-BqR_dtzOtdiX0vXcvQr-MnkpleCOKxUr2q0NMjRqOYHAGEdXhWHAqcDhIR5hpx6nFMnCFo2GJu-BpHAJSPQU9OuEWhOgnQmJXM_0NyuaXWQ5xSNGZExKr814ILSTopc56AYHU-AmZQ_Pt2paDcBDj7c3If9N2yo8H_8lxXO1hPBxS7IbJMfMGFDdJrvuHczF1xVzo1x6sYQbRNkezBpIglJXr0GSqwyJ1Ca6BYuCG3_G-jS-MTMkNYiIihZt7OhKtriwZ5YG582R9yyWRfw6YTv_7W-IL1lxdSGFNajXnrtGl513TvQmadqQA1lwjSUtCQYE9b-zXYaFwyn1JdOuJ4fMY2rGdlhODVPot2VGxqVVcjY4c_SN3WHzcxSCcRL-L49oN65OboVMFQ7slH4Gyx4J6gZBMsUFWl_iG5Y55OcfnOgEwbXzjl8yYy69z9NbzrElZ-1h5DlT2QETqWJb1Ujxhwpge-W5RIMxVptNRcBCKVpEhs4VqpKTtUX0v-7srkviEECgfpRAQrCqRZr7CDE6JS7bvOvYdrS0v39z86WhWsxrE7fkxMaoH0mzudU3g9yaD0f5HLjvHmA8VSRugAWi0OQqK7bVysc_q0wFBj6-MQLPD-CBW98X1FMsgVpBNIGeij5He9-iRt3Q9JZbMZakIB2BfnSrkvS9gJ68gfxX_rVhLJs8eF78Ftg1jXdNPZFXOQNqloVym4Oj","DataProtected":true}