refactor(web-client-tpos, order-service): improve API deserialization, update DTO types for Dapper compatibility, and refine API proxying for staff schedules and order cancellations.
This commit is contained in:
@@ -47,6 +47,68 @@ public class PosDataService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Robust list deserialization — handles plain arrays, PagedResult wrappers, and ApiResponse envelopes.
|
||||
/// VI: Deserialize list linh hoạt — xử lý array thuần, PagedResult wrapper, và ApiResponse envelope.
|
||||
/// </summary>
|
||||
private async Task<List<T>> GetListFromApiAsync<T>(string url)
|
||||
{
|
||||
AttachToken();
|
||||
var resp = await _http.GetAsync(url);
|
||||
if (!resp.IsSuccessStatusCode) return new();
|
||||
var json = await resp.Content.ReadAsStringAsync();
|
||||
if (string.IsNullOrWhiteSpace(json)) return new();
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// Case 1: plain array [...]
|
||||
if (root.ValueKind == JsonValueKind.Array)
|
||||
return JsonSerializer.Deserialize<List<T>>(json, _jsonOptions) ?? new();
|
||||
|
||||
// Case 2: { "items": [...] } (PagedResult)
|
||||
if (root.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Array)
|
||||
return JsonSerializer.Deserialize<List<T>>(items.GetRawText(), _jsonOptions) ?? new();
|
||||
|
||||
// Case 3: { "data": { "items": [...] } } (ApiResponse<PagedResult>)
|
||||
// Case 4: { "data": [...] } (ApiResponse<List>)
|
||||
if (root.TryGetProperty("data", out var data))
|
||||
{
|
||||
if (data.ValueKind == JsonValueKind.Object && data.TryGetProperty("items", out var dataItems) && dataItems.ValueKind == JsonValueKind.Array)
|
||||
return JsonSerializer.Deserialize<List<T>>(dataItems.GetRawText(), _jsonOptions) ?? new();
|
||||
if (data.ValueKind == JsonValueKind.Array)
|
||||
return JsonSerializer.Deserialize<List<T>>(data.GetRawText(), _jsonOptions) ?? new();
|
||||
}
|
||||
|
||||
return new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Robust single-object deserialization — handles plain objects and ApiResponse envelopes.
|
||||
/// VI: Deserialize đối tượng đơn linh hoạt — xử lý object thuần và ApiResponse envelope.
|
||||
/// </summary>
|
||||
private async Task<T?> GetObjectFromApiAsync<T>(string url) where T : class
|
||||
{
|
||||
AttachToken();
|
||||
var resp = await _http.GetAsync(url);
|
||||
if (!resp.IsSuccessStatusCode) return null;
|
||||
var json = await resp.Content.ReadAsStringAsync();
|
||||
if (string.IsNullOrWhiteSpace(json)) return null;
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// Case 1: { "data": {...} } (ApiResponse envelope)
|
||||
if (root.TryGetProperty("data", out var data) && data.ValueKind == JsonValueKind.Object)
|
||||
return JsonSerializer.Deserialize<T>(data.GetRawText(), _jsonOptions);
|
||||
|
||||
// Case 2: plain object
|
||||
if (root.ValueKind == JsonValueKind.Object)
|
||||
return JsonSerializer.Deserialize<T>(json, _jsonOptions);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -55,25 +117,25 @@ public class PosDataService
|
||||
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(); }
|
||||
=> await GetListFromApiAsync<ShopInfo>("api/bff/shops");
|
||||
|
||||
public async Task<ShopInfo?> GetShopByIdAsync(Guid shopId)
|
||||
{ AttachToken(); return await _http.GetFromJsonAsync<ShopInfo>($"api/bff/shops/{shopId}", _jsonOptions); }
|
||||
=> await GetObjectFromApiAsync<ShopInfo>($"api/bff/shops/{shopId}");
|
||||
|
||||
public async Task<List<ProductInfo>> GetProductsAsync(Guid shopId)
|
||||
{ AttachToken(); return await _http.GetFromJsonAsync<List<ProductInfo>>($"api/bff/shops/{shopId}/products", _jsonOptions) ?? new(); }
|
||||
=> await GetListFromApiAsync<ProductInfo>($"api/bff/shops/{shopId}/products");
|
||||
|
||||
public async Task<List<CategoryInfo>> GetCategoriesAsync(Guid shopId)
|
||||
{ AttachToken(); return await _http.GetFromJsonAsync<List<CategoryInfo>>($"api/bff/shops/{shopId}/categories", _jsonOptions) ?? new(); }
|
||||
=> await GetListFromApiAsync<CategoryInfo>($"api/bff/shops/{shopId}/categories");
|
||||
|
||||
public async Task<List<TableInfo>> GetTablesAsync(Guid shopId)
|
||||
{ AttachToken(); return await _http.GetFromJsonAsync<List<TableInfo>>($"api/bff/shops/{shopId}/tables", _jsonOptions) ?? new(); }
|
||||
=> await GetListFromApiAsync<TableInfo>($"api/bff/shops/{shopId}/tables");
|
||||
|
||||
public async Task<List<AppointmentInfo>> GetAppointmentsAsync(Guid shopId)
|
||||
{ AttachToken(); return await _http.GetFromJsonAsync<List<AppointmentInfo>>($"api/bff/shops/{shopId}/appointments", _jsonOptions) ?? new(); }
|
||||
=> await GetListFromApiAsync<AppointmentInfo>($"api/bff/shops/{shopId}/appointments");
|
||||
|
||||
public async Task<List<StaffInfo>> GetStaffAsync()
|
||||
{ AttachToken(); return await _http.GetFromJsonAsync<List<StaffInfo>>("api/bff/staff", _jsonOptions) ?? new(); }
|
||||
=> await GetListFromApiAsync<StaffInfo>("api/bff/staff");
|
||||
|
||||
// ═══ ADMIN-LEVEL PRODUCT/CATEGORY METHODS ═══
|
||||
|
||||
@@ -88,16 +150,14 @@ public class PosDataService
|
||||
|
||||
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();
|
||||
return await GetListFromApiAsync<AdminProductInfo>(url);
|
||||
}
|
||||
|
||||
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();
|
||||
return await GetListFromApiAsync<AdminCategoryInfo>(url);
|
||||
}
|
||||
|
||||
public async Task<bool> CreateProductAsync(CreateProductRequest req)
|
||||
@@ -128,9 +188,8 @@ public class PosDataService
|
||||
|
||||
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();
|
||||
return await GetListFromApiAsync<InventoryItemInfo>(url);
|
||||
}
|
||||
|
||||
// ═══ MEMBERSHIP/CUSTOMER METHODS ═══
|
||||
@@ -139,7 +198,7 @@ public class PosDataService
|
||||
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(); }
|
||||
=> await GetListFromApiAsync<MemberInfo>("api/bff/members");
|
||||
|
||||
// ═══ STAFF CREATE ═══
|
||||
|
||||
@@ -182,13 +241,12 @@ public class PosDataService
|
||||
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(); }
|
||||
=> await GetListFromApiAsync<StaffRoleInfo>("api/bff/staff/roles");
|
||||
|
||||
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();
|
||||
return await GetListFromApiAsync<ScheduleInfo>(url);
|
||||
}
|
||||
|
||||
// ═══ ORDERS ═══
|
||||
@@ -198,11 +256,10 @@ public class PosDataService
|
||||
|
||||
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();
|
||||
return await GetListFromApiAsync<OrderInfo>(url);
|
||||
}
|
||||
|
||||
// ═══ WALLETS / FINANCE ═══
|
||||
@@ -211,17 +268,17 @@ public class PosDataService
|
||||
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(); }
|
||||
=> await GetListFromApiAsync<WalletInfo>("api/bff/wallets");
|
||||
|
||||
public async Task<List<WalletTxnInfo>> GetWalletTransactionsAsync(int limit = 50)
|
||||
{ AttachToken(); return await _http.GetFromJsonAsync<List<WalletTxnInfo>>($"api/bff/wallet/transactions?limit={limit}", _jsonOptions) ?? new(); }
|
||||
=> await GetListFromApiAsync<WalletTxnInfo>($"api/bff/wallet/transactions?limit={limit}");
|
||||
|
||||
// ═══ 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(); }
|
||||
=> await GetListFromApiAsync<DeviceInfo>("api/bff/devices");
|
||||
|
||||
// ═══ PROMOTIONS ═══
|
||||
|
||||
@@ -229,7 +286,7 @@ public class PosDataService
|
||||
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(); }
|
||||
=> await GetListFromApiAsync<PromotionInfo>("api/bff/promotions");
|
||||
|
||||
// ═══ CAMPAIGNS CRUD ═══
|
||||
|
||||
@@ -240,7 +297,7 @@ 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/campaigns", _jsonOptions) ?? new(); }
|
||||
=> await GetListFromApiAsync<CampaignInfo>("api/bff/campaigns");
|
||||
|
||||
public async Task<bool> CreateCampaignAsync(CreateCampaignRequest req)
|
||||
{
|
||||
@@ -297,9 +354,8 @@ public class PosDataService
|
||||
|
||||
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();
|
||||
return await GetListFromApiAsync<InventoryTxnInfo>(url);
|
||||
}
|
||||
|
||||
// ═══ MEMBERSHIP LEVELS ═══
|
||||
@@ -307,21 +363,21 @@ public class PosDataService
|
||||
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(); }
|
||||
=> await GetListFromApiAsync<LevelDefinitionInfo>("api/bff/membership/levels");
|
||||
|
||||
// ═══ 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(); }
|
||||
=> await GetListFromApiAsync<ShopStatsInfo>("api/bff/shops/stats");
|
||||
|
||||
// ═══ 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(); }
|
||||
=> await GetListFromApiAsync<ResourceInfo>($"api/bff/shops/{shopId}/resources");
|
||||
|
||||
// ═══ POS DASHBOARD (real-time daily stats) ═══
|
||||
|
||||
@@ -351,7 +407,7 @@ public class PosDataService
|
||||
// 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 PosOrderItemRequest(Guid ProductId, string ProductName, int Quantity, decimal UnitPrice, string? ProductType = "Physical");
|
||||
public record CreatePosOrderResponse(Guid OrderId, string TransactionId, decimal TotalAmount, string Status);
|
||||
|
||||
public async Task<CreatePosOrderResponse?> CreatePosOrderAsync(CreatePosOrderRequest req)
|
||||
@@ -434,11 +490,10 @@ public class PosDataService
|
||||
|
||||
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();
|
||||
return await GetListFromApiAsync<RevenueReportItem>(url);
|
||||
}
|
||||
|
||||
// ═══ SHOP SETTINGS ═══
|
||||
@@ -447,10 +502,7 @@ public class PosDataService
|
||||
public record UpdateShopSettingsRequest(string? FeaturesConfig, string? OpenTime, string? CloseTime, string? OpenDays);
|
||||
|
||||
public async Task<ShopSettingsInfo?> GetShopSettingsAsync(Guid shopId)
|
||||
{
|
||||
AttachToken();
|
||||
return await _http.GetFromJsonAsync<ShopSettingsInfo>($"api/bff/shops/{shopId}/settings", _jsonOptions);
|
||||
}
|
||||
=> await GetObjectFromApiAsync<ShopSettingsInfo>($"api/bff/shops/{shopId}/settings");
|
||||
|
||||
public async Task<bool> UpdateShopSettingsAsync(Guid shopId, UpdateShopSettingsRequest req)
|
||||
{
|
||||
@@ -465,11 +517,10 @@ public class PosDataService
|
||||
|
||||
public async Task<List<TopProductInfo>> GetTopProductsAsync(Guid? shopId = null, int limit = 10)
|
||||
{
|
||||
AttachToken();
|
||||
var url = shopId.HasValue
|
||||
? $"api/bff/reports/top-products?shopId={shopId}&limit={limit}"
|
||||
: $"api/bff/reports/top-products?limit={limit}";
|
||||
return await _http.GetFromJsonAsync<List<TopProductInfo>>(url, _jsonOptions) ?? new();
|
||||
return await GetListFromApiAsync<TopProductInfo>(url);
|
||||
}
|
||||
|
||||
// ═══ TABLES CRUD ═══
|
||||
@@ -531,10 +582,9 @@ public class PosDataService
|
||||
|
||||
public async Task<List<KitchenTicketInfo>> GetKitchenTicketsAsync(Guid? shopId = null, string status = "pending")
|
||||
{
|
||||
AttachToken();
|
||||
if (!shopId.HasValue) return new();
|
||||
var url = $"api/bff/shops/{shopId}/kitchen-tickets?status={status}";
|
||||
return await _http.GetFromJsonAsync<List<KitchenTicketInfo>>(url, _jsonOptions) ?? new();
|
||||
return await GetListFromApiAsync<KitchenTicketInfo>(url);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateTicketStatusAsync(Guid ticketId, UpdateTicketStatusRequest req)
|
||||
@@ -549,10 +599,9 @@ public class PosDataService
|
||||
|
||||
public async Task<List<RecipeInfo>> GetRecipesAsync(Guid? shopId = null)
|
||||
{
|
||||
AttachToken();
|
||||
if (!shopId.HasValue) return new();
|
||||
var url = $"api/bff/shops/{shopId}/recipes";
|
||||
return await _http.GetFromJsonAsync<List<RecipeInfo>>(url, _jsonOptions) ?? new();
|
||||
return await GetListFromApiAsync<RecipeInfo>(url);
|
||||
}
|
||||
|
||||
public async Task<bool> CreateRecipeAsync(CreateRecipeRequest req)
|
||||
|
||||
@@ -48,8 +48,12 @@ public class BookingController : ControllerBase
|
||||
/// VI: Hủy lịch hẹn.
|
||||
/// </summary>
|
||||
[HttpDelete("appointments/{apptId:guid}/cancel")]
|
||||
public Task<IActionResult> CancelAppointment(Guid apptId) =>
|
||||
_booking.DeleteAsync($"/api/v1/appointments/{apptId}/cancel").ProxyAsync();
|
||||
public async Task<IActionResult> CancelAppointment(Guid apptId)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/appointments/{apptId}");
|
||||
request.Content = System.Net.Http.Json.JsonContent.Create(new { reason = "Cancelled from POS" });
|
||||
return await _booking.SendAsync(request).ProxyAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get resources for a specific shop.
|
||||
|
||||
@@ -30,8 +30,20 @@ public class OrderController : ControllerBase
|
||||
{
|
||||
var qs = new List<string>();
|
||||
if (shopId.HasValue) qs.Add($"shopId={shopId}");
|
||||
if (!string.IsNullOrEmpty(filter)) qs.Add($"filter={Uri.EscapeDataString(filter)}");
|
||||
var query = qs.Count > 0 ? "?" + string.Join("&", qs) : "";
|
||||
|
||||
// EN: Convert filter shorthand to fromDate/toDate for OrderService
|
||||
// VI: Chuyển đổi filter shorthand thành fromDate/toDate cho OrderService
|
||||
var now = DateTime.UtcNow;
|
||||
var (fromDate, toDate) = filter?.ToLower() switch
|
||||
{
|
||||
"week" => (now.Date.AddDays(-(int)now.DayOfWeek), now),
|
||||
"month" => (new DateTime(now.Year, now.Month, 1, 0, 0, 0, DateTimeKind.Utc), now),
|
||||
_ => (now.Date, now) // "today" or default
|
||||
};
|
||||
qs.Add($"fromDate={fromDate:O}");
|
||||
qs.Add($"toDate={toDate:O}");
|
||||
|
||||
var query = "?" + string.Join("&", qs);
|
||||
return _order.GetAsync($"/api/v1/orders{query}").ProxyAsync();
|
||||
}
|
||||
|
||||
@@ -48,8 +60,13 @@ public class OrderController : ControllerBase
|
||||
/// VI: Hủy đơn hàng.
|
||||
/// </summary>
|
||||
[HttpPut("orders/{orderId:guid}/cancel")]
|
||||
public Task<IActionResult> CancelOrder(Guid orderId) =>
|
||||
_order.PutAsync($"/api/v1/orders/{orderId}/cancel", null).ProxyAsync();
|
||||
public Task<IActionResult> CancelOrder(Guid orderId, [FromQuery] Guid? shopId = null)
|
||||
{
|
||||
var qs = shopId.HasValue ? $"?shopId={shopId}" : "";
|
||||
return _order.PostAsJsonAsync(
|
||||
$"/api/v1/orders/{orderId}/cancel{qs}",
|
||||
new { reason = "Cancelled from POS" }).ProxyAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create a POS order.
|
||||
|
||||
@@ -5,18 +5,20 @@ using WebClientTpos.Server.Infrastructure;
|
||||
namespace WebClientTpos.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Staff controller — proxies to MerchantService for staff/roles/schedules CRUD.
|
||||
/// VI: Controller nhân viên — proxy đến MerchantService cho CRUD nhân viên/vai trò/lịch.
|
||||
/// EN: Staff controller — proxies to MerchantService for staff/roles CRUD and BookingService for schedules.
|
||||
/// VI: Controller nhân viên — proxy đến MerchantService cho CRUD nhân viên/vai trò và BookingService cho lịch.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/bff")]
|
||||
public class StaffController : ControllerBase
|
||||
{
|
||||
private readonly HttpClient _merchant;
|
||||
private readonly HttpClient _booking;
|
||||
|
||||
public StaffController(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_merchant = httpClientFactory.CreateClient("MerchantService");
|
||||
_booking = httpClientFactory.CreateClient("BookingService");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -57,40 +59,40 @@ public class StaffController : ControllerBase
|
||||
/// </summary>
|
||||
[HttpGet("staff/roles")]
|
||||
public Task<IActionResult> GetStaffRoles() =>
|
||||
_merchant.GetAsync("/api/v1/merchants/me/staff/roles").ProxyAsync();
|
||||
_merchant.GetAsync("/api/v1/staff/roles").ProxyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get staff schedules — optionally filtered by shopId.
|
||||
/// VI: Lấy lịch làm việc nhân viên — tùy chọn lọc theo shopId.
|
||||
/// EN: Get staff schedules — proxies to BookingService.
|
||||
/// VI: Lấy lịch làm việc nhân viên — proxy đến BookingService.
|
||||
/// </summary>
|
||||
[HttpGet("staff/schedules")]
|
||||
public Task<IActionResult> GetStaffSchedules([FromQuery] Guid? shopId = null)
|
||||
{
|
||||
var qs = shopId.HasValue ? $"?shopId={shopId}" : "";
|
||||
return _merchant.GetAsync($"/api/v1/merchants/me/staff/schedules{qs}").ProxyAsync();
|
||||
return _booking.GetAsync($"/api/v1/schedules{qs}").ProxyAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create a staff schedule.
|
||||
/// VI: Tạo lịch làm việc nhân viên.
|
||||
/// EN: Create a staff schedule — proxies to BookingService.
|
||||
/// VI: Tạo lịch làm việc nhân viên — proxy đến BookingService.
|
||||
/// </summary>
|
||||
[HttpPost("staff/schedules")]
|
||||
public Task<IActionResult> CreateSchedule([FromBody] JsonElement body) =>
|
||||
_merchant.PostAsJsonAsync("/api/v1/merchants/me/staff/schedules", body).ProxyAsync();
|
||||
_booking.PostAsJsonAsync("/api/v1/schedules", body).ProxyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update a staff schedule.
|
||||
/// VI: Cập nhật lịch làm việc nhân viên.
|
||||
/// EN: Update a staff schedule — proxies to BookingService.
|
||||
/// VI: Cập nhật lịch làm việc nhân viên — proxy đến BookingService.
|
||||
/// </summary>
|
||||
[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();
|
||||
_booking.PutAsJsonAsync($"/api/v1/schedules/{scheduleId}", body).ProxyAsync();
|
||||
|
||||
/// <summary>
|
||||
/// EN: Delete a staff schedule.
|
||||
/// VI: Xóa lịch làm việc nhân viên.
|
||||
/// EN: Delete a staff schedule — proxies to BookingService.
|
||||
/// VI: Xóa lịch làm việc nhân viên — proxy đến BookingService.
|
||||
/// </summary>
|
||||
[HttpDelete("staff/schedules/{scheduleId:guid}")]
|
||||
public Task<IActionResult> DeleteSchedule(Guid scheduleId) =>
|
||||
_merchant.DeleteAsync($"/api/v1/merchants/me/staff/schedules/{scheduleId}").ProxyAsync();
|
||||
_booking.DeleteAsync($"/api/v1/schedules/{scheduleId}").ProxyAsync();
|
||||
}
|
||||
|
||||
@@ -26,20 +26,21 @@ public record RevenueReportDto(
|
||||
string Period,
|
||||
Guid ShopId,
|
||||
decimal TotalRevenue,
|
||||
int TotalOrders,
|
||||
long TotalOrders,
|
||||
List<RevenuePeriodDto> Data
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Revenue data for a single period.
|
||||
/// VI: Dữ liệu doanh thu cho một kỳ.
|
||||
/// EN: Revenue data for a single period (class for Dapper compatibility).
|
||||
/// VI: Dữ liệu doanh thu cho một kỳ (class cho tương thích Dapper).
|
||||
/// </summary>
|
||||
public record RevenuePeriodDto(
|
||||
DateTime PeriodStart,
|
||||
decimal Revenue,
|
||||
int OrderCount,
|
||||
decimal AvgOrderValue
|
||||
);
|
||||
public class RevenuePeriodDto
|
||||
{
|
||||
public DateTime PeriodStart { get; set; }
|
||||
public decimal Revenue { get; set; }
|
||||
public long OrderCount { get; set; }
|
||||
public decimal AvgOrderValue { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetRevenueReportQuery.
|
||||
|
||||
@@ -19,16 +19,17 @@ public record GetTopProductsQuery(
|
||||
) : IRequest<List<TopProductDto>>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Top product DTO.
|
||||
/// VI: DTO sản phẩm bán chạy.
|
||||
/// EN: Top product DTO (class for Dapper compatibility).
|
||||
/// VI: DTO sản phẩm bán chạy (class cho tương thích Dapper).
|
||||
/// </summary>
|
||||
public record TopProductDto(
|
||||
Guid ProductId,
|
||||
string ProductName,
|
||||
int TotalQuantity,
|
||||
decimal TotalRevenue,
|
||||
int OrderCount
|
||||
);
|
||||
public class TopProductDto
|
||||
{
|
||||
public Guid ProductId { get; set; }
|
||||
public string ProductName { get; set; } = string.Empty;
|
||||
public long TotalQuantity { get; set; }
|
||||
public decimal TotalRevenue { get; set; }
|
||||
public long OrderCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetTopProductsQuery.
|
||||
|
||||
Reference in New Issue
Block a user