BFF Endpoints (6 new):
- POST/PUT/DELETE categories — full CRUD with shop ownership validation
- GET orders/{id} — order detail with items
- PUT orders/{id}/cancel — cancel non-completed orders (status=6)
- PUT shops/{id} — update name, phone, email, hours
- GET reports/revenue — daily/weekly/monthly revenue aggregation
PosDataService (8 new methods):
- CreateCategory, UpdateCategory, DeleteCategory
- GetOrderDetail, CancelOrder
- UpdateShop
- GetRevenueReport
ShopPage UI (222 lines):
- Menu tab: categories table with add/edit/delete
- Finance tab: expandable order rows with items + cancel button
- Overview tab: shop info edit form
- Reports tab: period selector (Ngày/Tuần/Tháng) + revenue table
381 lines
18 KiB
C#
381 lines
18 KiB
C#
using System.Net.Http.Headers;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace WebClientTpos.Client.Services;
|
|
|
|
// EN: POS data service — attaches auth token for multi-tenant data isolation.
|
|
// VI: POS data service — đính kèm auth token để cách ly dữ liệu multi-tenant.
|
|
public class PosDataService
|
|
{
|
|
private readonly HttpClient _http;
|
|
private readonly AuthStateService _authState;
|
|
private static readonly JsonSerializerOptions _jsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
|
};
|
|
|
|
public PosDataService(HttpClient http, AuthStateService authState)
|
|
{
|
|
_http = http;
|
|
_authState = authState;
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Ensure Authorization header is set before each BFF call.
|
|
/// VI: Đảm bảo header Authorization được đặt trước mỗi BFF call.
|
|
/// </summary>
|
|
private void AttachToken()
|
|
{
|
|
if (!string.IsNullOrEmpty(_authState.Token))
|
|
{
|
|
_http.DefaultRequestHeaders.Authorization =
|
|
new AuthenticationHeaderValue("Bearer", _authState.Token);
|
|
}
|
|
}
|
|
|
|
public record ShopInfo(Guid Id, string Name, string Slug, string? Description, string? Phone, string? Email, string? Category, string? Status, Guid? MerchantId = null);
|
|
public record ProductInfo(Guid Id, string Name, decimal Price, string? Sku, string? Description, string? Category, int? DurationMinutes);
|
|
public record CategoryInfo(Guid Id, string Name, string? Description, int DisplayOrder);
|
|
public record TableInfo(Guid Id, string TableNumber, int Capacity, string? Zone, string Status, Guid? SessionId, int? GuestCount, DateTime? StartedAt);
|
|
public record AppointmentInfo(Guid Id, Guid? CustomerId, Guid? StaffId, Guid? ResourceId, Guid ServiceId, DateTime StartTime, DateTime EndTime, string Status, string? ResourceName);
|
|
public record StaffInfo(Guid Id, Guid? UserId, string? EmployeeCode, string? Phone, string? Email, DateTime? JoinedAt, DateTime? TerminatedAt, string? Role, string? Status, string? ShopName);
|
|
|
|
public async Task<List<ShopInfo>> GetShopsAsync()
|
|
{ AttachToken(); return await _http.GetFromJsonAsync<List<ShopInfo>>("api/bff/shops", _jsonOptions) ?? new(); }
|
|
|
|
public async Task<ShopInfo?> GetShopByIdAsync(Guid shopId)
|
|
{ AttachToken(); return await _http.GetFromJsonAsync<ShopInfo>($"api/bff/shops/{shopId}", _jsonOptions); }
|
|
|
|
public async Task<List<ProductInfo>> GetProductsAsync(Guid shopId)
|
|
{ AttachToken(); return await _http.GetFromJsonAsync<List<ProductInfo>>($"api/bff/shops/{shopId}/products", _jsonOptions) ?? new(); }
|
|
|
|
public async Task<List<CategoryInfo>> GetCategoriesAsync(Guid shopId)
|
|
{ AttachToken(); return await _http.GetFromJsonAsync<List<CategoryInfo>>($"api/bff/shops/{shopId}/categories", _jsonOptions) ?? new(); }
|
|
|
|
public async Task<List<TableInfo>> GetTablesAsync(Guid shopId)
|
|
{ AttachToken(); return await _http.GetFromJsonAsync<List<TableInfo>>($"api/bff/shops/{shopId}/tables", _jsonOptions) ?? new(); }
|
|
|
|
public async Task<List<AppointmentInfo>> GetAppointmentsAsync(Guid shopId)
|
|
{ AttachToken(); return await _http.GetFromJsonAsync<List<AppointmentInfo>>($"api/bff/shops/{shopId}/appointments", _jsonOptions) ?? new(); }
|
|
|
|
public async Task<List<StaffInfo>> GetStaffAsync()
|
|
{ AttachToken(); return await _http.GetFromJsonAsync<List<StaffInfo>>("api/bff/staff", _jsonOptions) ?? new(); }
|
|
|
|
// ═══ ADMIN-LEVEL PRODUCT/CATEGORY METHODS ═══
|
|
|
|
// EN: Admin-level records with shop_id and category info
|
|
// VI: Record cấp admin với shop_id và thông tin danh mục
|
|
public record AdminProductInfo(Guid Id, string Name, decimal Price, string? Sku, string? Description,
|
|
string? ImageUrl, bool IsActive, string? Type, Guid ShopId, DateTime CreatedAt, string? CategoryName);
|
|
public record AdminCategoryInfo(Guid Id, string Name, string? Description, int DisplayOrder,
|
|
Guid ShopId, Guid? ParentId, bool IsActive);
|
|
public record CreateProductRequest(Guid ShopId, string Name, string? Description, decimal Price,
|
|
string? Type, string? Sku, string? ImageUrl);
|
|
|
|
public async Task<List<AdminProductInfo>> GetAllProductsAsync(Guid? shopId = null)
|
|
{
|
|
AttachToken();
|
|
var url = shopId.HasValue ? $"api/bff/products?shopId={shopId}" : "api/bff/products";
|
|
return await _http.GetFromJsonAsync<List<AdminProductInfo>>(url, _jsonOptions) ?? new();
|
|
}
|
|
|
|
public async Task<List<AdminCategoryInfo>> GetAllCategoriesAsync(Guid? shopId = null)
|
|
{
|
|
AttachToken();
|
|
var url = shopId.HasValue ? $"api/bff/categories?shopId={shopId}" : "api/bff/categories";
|
|
return await _http.GetFromJsonAsync<List<AdminCategoryInfo>>(url, _jsonOptions) ?? new();
|
|
}
|
|
|
|
public async Task<bool> CreateProductAsync(CreateProductRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync("api/bff/products", req, _jsonOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
public async Task<bool> UpdateProductAsync(Guid productId, CreateProductRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PutAsJsonAsync($"api/bff/products/{productId}", req, _jsonOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
public async Task<bool> DeleteProductAsync(Guid productId)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.DeleteAsync($"api/bff/products/{productId}");
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ INVENTORY METHODS ═══
|
|
|
|
public record InventoryItemInfo(Guid Id, Guid ProductId, Guid ShopId, int Quantity,
|
|
int ReorderLevel, int ReservedQuantity, DateTime? UpdatedAt, string? ProductName);
|
|
|
|
public async Task<List<InventoryItemInfo>> GetInventoryAsync(Guid? shopId = null)
|
|
{
|
|
AttachToken();
|
|
var url = shopId.HasValue ? $"api/bff/inventory?shopId={shopId}" : "api/bff/inventory";
|
|
return await _http.GetFromJsonAsync<List<InventoryItemInfo>>(url, _jsonOptions) ?? new();
|
|
}
|
|
|
|
// ═══ MEMBERSHIP/CUSTOMER METHODS ═══
|
|
|
|
public record MemberInfo(Guid Id, string? CountryCode, string? Gender, int CurrentExp,
|
|
int CurrentLevel, int TotalExpEarned, DateTime CreatedAt, string? LevelName);
|
|
|
|
public async Task<List<MemberInfo>> GetMembersAsync()
|
|
{ AttachToken(); return await _http.GetFromJsonAsync<List<MemberInfo>>("api/bff/members", _jsonOptions) ?? new(); }
|
|
|
|
// ═══ STAFF CREATE ═══
|
|
|
|
public record CreateStaffRequest(Guid MerchantId, string? EmployeeCode, string? Phone, string? Email, string? Role);
|
|
|
|
public async Task<bool> CreateStaffAsync(CreateStaffRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync("api/bff/staff", req, _jsonOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
public async Task<bool> UpdateStaffAsync(Guid staffId, CreateStaffRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PutAsJsonAsync($"api/bff/staff/{staffId}", req, _jsonOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
public async Task<bool> DeleteStaffAsync(Guid staffId)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.DeleteAsync($"api/bff/staff/{staffId}");
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
public record UpdateInventoryRequest(int Quantity, int ReorderLevel);
|
|
|
|
public async Task<bool> UpdateInventoryAsync(Guid inventoryId, UpdateInventoryRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PutAsJsonAsync($"api/bff/inventory/{inventoryId}", req, _jsonOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ STAFF ROLES & SCHEDULES ═══
|
|
|
|
public record StaffRoleInfo(int Id, string Name);
|
|
public record ScheduleInfo(Guid Id, Guid StaffId, Guid ShopId, int DayOfWeek, string StartTime, string EndTime,
|
|
string? EmployeeCode, string? Role, string? Phone);
|
|
|
|
public async Task<List<StaffRoleInfo>> GetStaffRolesAsync()
|
|
{ AttachToken(); return await _http.GetFromJsonAsync<List<StaffRoleInfo>>("api/bff/staff/roles", _jsonOptions) ?? new(); }
|
|
|
|
public async Task<List<ScheduleInfo>> GetStaffSchedulesAsync(Guid? shopId = null)
|
|
{
|
|
AttachToken();
|
|
var url = shopId.HasValue ? $"api/bff/staff/schedules?shopId={shopId}" : "api/bff/staff/schedules";
|
|
return await _http.GetFromJsonAsync<List<ScheduleInfo>>(url, _jsonOptions) ?? new();
|
|
}
|
|
|
|
// ═══ ORDERS ═══
|
|
|
|
public record OrderInfo(Guid Id, Guid ShopId, decimal TotalAmount, int StatusId, DateTime CreatedAt,
|
|
string? Status, string? PaymentMethod, string? Notes);
|
|
|
|
public async Task<List<OrderInfo>> GetOrdersAsync(Guid? shopId = null, string filter = "today")
|
|
{
|
|
AttachToken();
|
|
var url = shopId.HasValue
|
|
? $"api/bff/orders?shopId={shopId}&filter={filter}"
|
|
: $"api/bff/orders?filter={filter}";
|
|
return await _http.GetFromJsonAsync<List<OrderInfo>>(url, _jsonOptions) ?? new();
|
|
}
|
|
|
|
// ═══ WALLETS / FINANCE ═══
|
|
|
|
public record WalletInfo(Guid Id, decimal Balance, string? Currency, Guid OwnerId, DateTime CreatedAt, decimal TotalIncome, decimal TotalExpense);
|
|
public record WalletTxnInfo(Guid Id, Guid WalletId, decimal Amount, string? Description, DateTime CreatedAt, string? ItemName);
|
|
|
|
public async Task<List<WalletInfo>> GetWalletsAsync()
|
|
{ AttachToken(); return await _http.GetFromJsonAsync<List<WalletInfo>>("api/bff/wallets", _jsonOptions) ?? new(); }
|
|
|
|
public async Task<List<WalletTxnInfo>> GetWalletTransactionsAsync(int limit = 50)
|
|
{ AttachToken(); return await _http.GetFromJsonAsync<List<WalletTxnInfo>>($"api/bff/wallet/transactions?limit={limit}", _jsonOptions) ?? new(); }
|
|
|
|
// ═══ DEVICES ═══
|
|
|
|
public record DeviceInfo(Guid Id, string? DeviceToken, string? Platform, bool IsActive, DateTime CreatedAt, string? StaffCode);
|
|
|
|
public async Task<List<DeviceInfo>> GetDevicesAsync()
|
|
{ AttachToken(); return await _http.GetFromJsonAsync<List<DeviceInfo>>("api/bff/devices", _jsonOptions) ?? new(); }
|
|
|
|
// ═══ PROMOTIONS ═══
|
|
|
|
public record PromotionInfo(Guid Id, string Name, string? Description, DateTime? StartDate, DateTime? EndDate,
|
|
bool IsActive, string? DiscountType, decimal? DiscountValue, int VoucherCount, int RedemptionCount);
|
|
|
|
public async Task<List<PromotionInfo>> GetPromotionsAsync()
|
|
{ AttachToken(); return await _http.GetFromJsonAsync<List<PromotionInfo>>("api/bff/promotions", _jsonOptions) ?? new(); }
|
|
|
|
// ═══ INVENTORY TRANSACTIONS ═══
|
|
|
|
public record InventoryTxnInfo(Guid Id, Guid InventoryItemId, int QuantityChange, string? Reason, DateTime CreatedAt, string? TransactionType);
|
|
|
|
public async Task<List<InventoryTxnInfo>> GetInventoryTransactionsAsync(Guid? shopId = null)
|
|
{
|
|
AttachToken();
|
|
var url = shopId.HasValue ? $"api/bff/inventory/transactions?shopId={shopId}" : "api/bff/inventory/transactions";
|
|
return await _http.GetFromJsonAsync<List<InventoryTxnInfo>>(url, _jsonOptions) ?? new();
|
|
}
|
|
|
|
// ═══ MEMBERSHIP LEVELS ═══
|
|
|
|
public record LevelDefinitionInfo(Guid Id, int Level, string Name, int MinExp, int MaxExp, int MemberCount);
|
|
|
|
public async Task<List<LevelDefinitionInfo>> GetMembershipLevelsAsync()
|
|
{ AttachToken(); return await _http.GetFromJsonAsync<List<LevelDefinitionInfo>>("api/bff/membership/levels", _jsonOptions) ?? new(); }
|
|
|
|
// ═══ SHOP STATS (aggregated per-shop) ═══
|
|
|
|
public record ShopStatsInfo(Guid ShopId, int ProductCount, int OrderCount, int StaffCount, decimal Revenue);
|
|
|
|
public async Task<List<ShopStatsInfo>> GetShopStatsAsync()
|
|
{ AttachToken(); return await _http.GetFromJsonAsync<List<ShopStatsInfo>>("api/bff/shops/stats", _jsonOptions) ?? new(); }
|
|
|
|
// ═══ BOOKING RESOURCES ═══
|
|
|
|
public record ResourceInfo(Guid Id, string Name, string? ResourceType, int Capacity, bool IsActive);
|
|
|
|
public async Task<List<ResourceInfo>> GetResourcesAsync(Guid shopId)
|
|
{ AttachToken(); return await _http.GetFromJsonAsync<List<ResourceInfo>>($"api/bff/shops/{shopId}/resources", _jsonOptions) ?? new(); }
|
|
|
|
// ═══ POS DASHBOARD (real-time daily stats) ═══
|
|
|
|
// EN: POS dashboard response DTOs
|
|
// VI: DTOs cho response dashboard POS
|
|
public record PosDashboardInfo(
|
|
decimal Revenue, int OrderCount, int ItemsSold, decimal AvgOrderValue,
|
|
List<PopularItemInfo> PopularItems,
|
|
List<PaymentBreakdownInfo> PaymentBreakdown,
|
|
List<HourlyRevenueInfo> HourlyRevenue,
|
|
List<RecentOrderInfo> RecentOrders);
|
|
public record PopularItemInfo(string Name, int Qty, decimal Revenue);
|
|
public record PaymentBreakdownInfo(string Method, decimal Amount, int Pct);
|
|
public record HourlyRevenueInfo(string Hour, decimal Revenue, int Pct);
|
|
public record RecentOrderInfo(string Id, decimal Total, string Time, string Status, string Method);
|
|
|
|
public async Task<PosDashboardInfo> GetPosDashboardAsync(Guid shopId)
|
|
{
|
|
AttachToken();
|
|
return await _http.GetFromJsonAsync<PosDashboardInfo>(
|
|
$"api/bff/pos/dashboard?shopId={shopId}", _jsonOptions)
|
|
?? new(0, 0, 0, 0, new(), new(), new(), new());
|
|
}
|
|
|
|
// ═══ POS ORDER CREATE ═══
|
|
|
|
// EN: POS order creation DTOs
|
|
// VI: DTOs cho tạo đơn POS
|
|
public record CreatePosOrderRequest(Guid ShopId, string? PaymentMethod, List<PosOrderItemRequest> Items);
|
|
public record PosOrderItemRequest(Guid ProductId, string ProductName, int Quantity, decimal UnitPrice);
|
|
public record CreatePosOrderResponse(Guid OrderId, string TransactionId, decimal TotalAmount, string Status);
|
|
|
|
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);
|
|
if (resp.IsSuccessStatusCode)
|
|
return await resp.Content.ReadFromJsonAsync<CreatePosOrderResponse>(_jsonOptions);
|
|
return null;
|
|
}
|
|
|
|
// ═══ CATEGORIES CRUD ═══
|
|
|
|
// EN: Category create/update request DTO
|
|
// VI: DTO tạo/cập nhật danh mục
|
|
public record AdminCreateCategoryRequest(Guid ShopId, string Name, string? Description, int DisplayOrder);
|
|
|
|
public async Task<bool> CreateCategoryAsync(AdminCreateCategoryRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync("api/bff/categories", req, _jsonOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
public async Task<bool> UpdateCategoryAsync(Guid categoryId, AdminCreateCategoryRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PutAsJsonAsync($"api/bff/categories/{categoryId}", req, _jsonOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
public async Task<bool> DeleteCategoryAsync(Guid categoryId)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.DeleteAsync($"api/bff/categories/{categoryId}");
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ ORDER DETAIL & CANCEL ═══
|
|
|
|
// EN: Order detail DTOs
|
|
// VI: DTOs cho chi tiết đơn hàng
|
|
public record OrderDetailInfo(Guid Id, Guid ShopId, decimal TotalAmount, string? Status, int StatusId, string? PaymentMethod, string? Notes, DateTime CreatedAt);
|
|
public record OrderItemInfo(Guid Id, string? ProductName, int Quantity, decimal UnitPrice, decimal Subtotal);
|
|
public record OrderDetailResponse(OrderDetailInfo? Order, List<OrderItemInfo>? Items);
|
|
|
|
public async Task<OrderDetailResponse?> GetOrderDetailAsync(Guid orderId)
|
|
{
|
|
AttachToken();
|
|
return await _http.GetFromJsonAsync<OrderDetailResponse>($"api/bff/orders/{orderId}", _jsonOptions);
|
|
}
|
|
|
|
public async Task<bool> CancelOrderAsync(Guid orderId)
|
|
{
|
|
AttachToken();
|
|
using var req = new HttpRequestMessage(HttpMethod.Put, $"api/bff/orders/{orderId}/cancel");
|
|
req.Headers.Authorization = _http.DefaultRequestHeaders.Authorization;
|
|
var resp = await _http.SendAsync(req);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ SHOP UPDATE ═══
|
|
|
|
// EN: Shop update DTO
|
|
// VI: DTO cập nhật thông tin cửa hàng
|
|
public record UpdateShopRequest(string? Name, string? Phone, string? Email, string? Description, string? OpenTime, string? CloseTime, string? OpenDays);
|
|
|
|
public async Task<bool> UpdateShopAsync(Guid shopId, UpdateShopRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PutAsJsonAsync($"api/bff/shops/{shopId}", req, _jsonOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ REVENUE REPORT ═══
|
|
|
|
// EN: Revenue report item DTO
|
|
// VI: DTO cho từng dòng báo cáo doanh thu
|
|
public record RevenueReportItem(DateTime Period, long OrderCount, decimal Revenue);
|
|
|
|
public async Task<List<RevenueReportItem>> GetRevenueReportAsync(string period = "daily", Guid? shopId = null)
|
|
{
|
|
AttachToken();
|
|
var url = shopId.HasValue
|
|
? $"api/bff/reports/revenue?period={period}&shopId={shopId}"
|
|
: $"api/bff/reports/revenue?period={period}";
|
|
return await _http.GetFromJsonAsync<List<RevenueReportItem>>(url, _jsonOptions) ?? new();
|
|
}
|
|
}
|