All checks were successful
Build & Deploy to K8s / build-and-deploy (push) Successful in 9m29s
OnboardingBusiness.razor was only navigating to the next step without calling the merchant registration API, so no merchant record was ever created in the database. This caused settings page updates to fail with "Merchant not found" and the SuperAdmin panel to show zero merchants. - Inject MerchantApiService and call RegisterMerchantAsync on "Tiếp tục" - Remove hardcoded demo data from onboarding form fields - Add fallback in AdminSettings SaveMerchant to auto-register if PUT fails - Add BFF POST /api/bff/account/register-merchant endpoint Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
230 lines
11 KiB
Plaintext
230 lines
11 KiB
Plaintext
@page "/admin/onboarding/business"
|
|
@layout AdminLayout
|
|
@inherits AdminBase
|
|
@inject WebClientTpos.Client.Services.MerchantApiService MerchantApi
|
|
|
|
@*
|
|
EN: Onboarding Step 1 — Business info (name, type, tax ID, address, logo upload)
|
|
VI: Bước 1 thiết lập — Thông tin doanh nghiệp (tên, loại hình, MST, địa chỉ, logo)
|
|
Design: pencil-design/src/pages/tPOS/admin/onboarding-business.pen
|
|
*@
|
|
|
|
<PageTitle>Thiết lập doanh nghiệp — aPOS Admin</PageTitle>
|
|
|
|
<div class="admin-content" style="padding:40px;">
|
|
<div class="admin-panel" style="max-width:980px;margin:0 auto;display:flex;flex-direction:column;gap:32px;">
|
|
|
|
<section>
|
|
<div style="display:flex;gap:8px;align-items:center;margin-bottom:12px;">
|
|
@for (int i = 1; i <= 6; i++)
|
|
{
|
|
<div style="width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:14px;
|
|
background-color:@(i == currentStep ? "var(--admin-orange-primary)" : i < currentStep ? "rgba(34,197,94,0.2)" : "var(--admin-bg-interactive)");
|
|
color:@(i == currentStep ? "#FFF" : i < currentStep ? "#22C55E" : "var(--admin-text-tertiary)");">
|
|
@(i < currentStep ? "✓" : i)
|
|
</div>
|
|
@if (i < 6)
|
|
{
|
|
<div style="flex:1;height:2px;background-color:@(i < currentStep ? "#22C55E" : "var(--admin-border-subtle)");"></div>
|
|
}
|
|
}
|
|
</div>
|
|
<div style="display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:6px;margin-bottom:6px;">
|
|
@for (int i = 1; i <= 6; i++)
|
|
{
|
|
<span style="text-align:center;font-size:12px;font-weight:@(i == currentStep ? "600" : "500");color:@(i == currentStep ? "var(--admin-orange-primary)" : "var(--admin-text-tertiary)");">
|
|
@_steps[i - 1].Label
|
|
</span>
|
|
}
|
|
</div>
|
|
<p style="font-size:12px;color:var(--admin-text-tertiary);margin:0;">Bước 1/6 — Nhập thông tin sơ bộ để aPOS hiểu doanh nghiệp của bạn</p>
|
|
</section>
|
|
|
|
<section class="admin-panel" style="padding:32px;display:flex;flex-direction:column;gap:24px;">
|
|
<div style="display:flex;flex-direction:column;gap:6px;">
|
|
<h2 style="font-size:24px;margin:0;font-weight:700;">Thông tin doanh nghiệp</h2>
|
|
<p style="margin:0;font-size:14px;color:var(--admin-text-tertiary);">Giúp chúng tôi biết bạn bán gì và ở đâu.</p>
|
|
</div>
|
|
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;">
|
|
<div class="admin-form-group">
|
|
<label class="admin-form-label">Tên doanh nghiệp <span style="color:var(--admin-danger);">*</span></label>
|
|
<input class="admin-form-input" type="text" @bind="_businessName" />
|
|
</div>
|
|
<div class="admin-form-group">
|
|
<label class="admin-form-label">Mã số thuế</label>
|
|
<input class="admin-form-input" type="text" @bind="_taxId" />
|
|
</div>
|
|
<div class="admin-form-group">
|
|
<label class="admin-form-label">Địa chỉ trụ sở</label>
|
|
<input class="admin-form-input" type="text" @bind="_address" />
|
|
</div>
|
|
<div class="admin-form-group">
|
|
<label class="admin-form-label">Số điện thoại</label>
|
|
<input class="admin-form-input" type="tel" @bind="_phone" />
|
|
</div>
|
|
<div class="admin-form-group">
|
|
<label class="admin-form-label">Email liên hệ</label>
|
|
<input class="admin-form-input" type="email" @bind="_email" />
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="admin-form-label">Loại hình kinh doanh</label>
|
|
<div style="display:flex;flex-wrap:wrap;gap:12px;padding-top:8px;">
|
|
@foreach (var type in _businessTypes)
|
|
{
|
|
<button type="button" class="admin-btn-secondary" style="background-color:@(type.Key == _businessType ? "var(--admin-orange-primary)" : "var(--admin-bg-interactive)");
|
|
color:@(type.Key == _businessType ? "#FFF" : "var(--admin-text-primary)");border:1px solid var(--admin-border-default);min-width:140px;display:flex;flex-direction:column;gap:4px;padding:14px;"
|
|
@onclick="() => _businessType = type.Key">
|
|
<span style="font-size:14px;font-weight:600;">@type.Label</span>
|
|
<span style="font-size:12px;color:@(type.Key == _businessType ? "rgba(255,255,255,0.8)" : "var(--admin-text-tertiary)");">@type.Description</span>
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:flex;flex-wrap:wrap;gap:20px;">
|
|
<div style="flex:1;min-width:220px;">
|
|
<label class="admin-form-label">Logo doanh nghiệp</label>
|
|
<div style="height:120px;border:2px dashed var(--admin-border-default);border-radius:14px;background-color:var(--admin-bg-interactive);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;">
|
|
<i data-lucide="upload" style="width:28px;height:28px;color:var(--admin-text-tertiary);"></i>
|
|
<span style="font-size:13px;color:var(--admin-text-tertiary);">Kéo thả hoặc nhấn để chọn logo</span>
|
|
<span style="font-size:12px;color:var(--admin-text-secondary);">@_logoPlaceholder</span>
|
|
</div>
|
|
</div>
|
|
<div style="flex:1;min-width:220px;border-radius:14px;background-color:var(--admin-bg-interactive);padding:20px;display:flex;flex-direction:column;gap:8px;">
|
|
<span style="font-size:14px;font-weight:600;">Tóm tắt nhanh</span>
|
|
<div style="display:flex;justify-content:space-between;font-size:13px;color:var(--admin-text-tertiary);">
|
|
<span>Loại hình</span>
|
|
<span>@GetBusinessTypeLabel()</span>
|
|
</div>
|
|
<div style="display:flex;justify-content:space-between;font-size:13px;color:var(--admin-text-tertiary);">
|
|
<span>Địa chỉ</span>
|
|
<span>@_address</span>
|
|
</div>
|
|
<div style="display:flex;justify-content:space-between;font-size:13px;color:var(--admin-text-tertiary);">
|
|
<span>Liên hệ</span>
|
|
<span>@_phone</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (!string.IsNullOrEmpty(_errorMsg))
|
|
{
|
|
<div style="padding:10px 14px;border-radius:8px;font-size:13px;background:rgba(239,68,68,0.12);color:#EF4444;">
|
|
@_errorMsg
|
|
</div>
|
|
}
|
|
|
|
<div style="display:flex;justify-content:flex-end;">
|
|
<button class="admin-btn-primary" @onclick="SaveAndContinue" disabled="@_saving" style="display:flex;align-items:center;gap:8px;">
|
|
@if (_saving)
|
|
{
|
|
<MudProgressCircular Indeterminate="true" Size="Size.Small" Color="Color.Surface" Style="width:16px;height:16px;" />
|
|
<span>Đang lưu...</span>
|
|
}
|
|
else
|
|
{
|
|
<span>Tiếp tục</span>
|
|
<i data-lucide="arrow-right"></i>
|
|
}
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
private int currentStep = 1;
|
|
private string _businessName = "";
|
|
private string _taxId = "";
|
|
private string _address = "";
|
|
private string _phone = "";
|
|
private string _email = "";
|
|
private string _businessType = "fnb";
|
|
private string _logoPlaceholder = "Chưa chọn tệp";
|
|
private bool _saving;
|
|
private string? _errorMsg;
|
|
|
|
private record StepInfo(int Index, string Label);
|
|
private readonly StepInfo[] _steps =
|
|
{
|
|
new StepInfo(1, "Doanh nghiệp"),
|
|
new StepInfo(2, "Cửa hàng"),
|
|
new StepInfo(3, "Sản phẩm"),
|
|
new StepInfo(4, "Nhân viên"),
|
|
new StepInfo(5, "Thiết bị"),
|
|
new StepInfo(6, "Hoàn tất"),
|
|
};
|
|
|
|
private record BusinessType(string Key, string Label, string Description);
|
|
private readonly BusinessType[] _businessTypes =
|
|
{
|
|
new BusinessType("fnb", "F&B", "Quán cà phê, nhà hàng"),
|
|
new BusinessType("retail", "Bán lẻ", "Cửa hàng, showroom"),
|
|
new BusinessType("services", "Dịch vụ", "Spa, salon, gym"),
|
|
};
|
|
|
|
private string GetBusinessTypeLabel()
|
|
=> Array.Find(_businessTypes, t => t.Key == _businessType)?.Label ?? "F&B";
|
|
|
|
/// <summary>
|
|
/// EN: Register merchant via API, then navigate to next step.
|
|
/// VI: Đăng ký merchant qua API, sau đó chuyển sang bước tiếp theo.
|
|
/// </summary>
|
|
private async Task SaveAndContinue()
|
|
{
|
|
_errorMsg = null;
|
|
|
|
// EN: Validate required fields
|
|
// VI: Kiểm tra trường bắt buộc
|
|
if (string.IsNullOrWhiteSpace(_businessName))
|
|
{
|
|
_errorMsg = "Vui lòng nhập tên doanh nghiệp";
|
|
return;
|
|
}
|
|
|
|
_saving = true;
|
|
StateHasChanged();
|
|
|
|
try
|
|
{
|
|
var dto = new WebClientTpos.Shared.DTOs.MerchantRegisterDto
|
|
{
|
|
BusinessName = _businessName.Trim(),
|
|
Type = "Individual",
|
|
TaxId = string.IsNullOrWhiteSpace(_taxId) ? null : _taxId.Trim()
|
|
};
|
|
|
|
var (result, error) = await MerchantApi.RegisterMerchantAsync(dto);
|
|
|
|
if (result != null)
|
|
{
|
|
// EN: Registration successful, proceed to next step
|
|
// VI: Đăng ký thành công, chuyển sang bước tiếp theo
|
|
NavigateTo("onboarding/store");
|
|
}
|
|
else if (error != null && error.Contains("already has", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// EN: User already has a merchant account, just continue to next step
|
|
// VI: User đã có tài khoản merchant, tiếp tục bước tiếp theo
|
|
NavigateTo("onboarding/store");
|
|
}
|
|
else
|
|
{
|
|
_errorMsg = error ?? "Không thể đăng ký doanh nghiệp. Vui lòng thử lại.";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_errorMsg = $"Lỗi: {ex.Message}";
|
|
}
|
|
finally
|
|
{
|
|
_saving = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
}
|