1168 lines
53 KiB
C#
1168 lines
53 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;
|
|
// EN: Read options — case-insensitive to handle camelCase responses from BFF/microservices.
|
|
// VI: Options đọc — không phân biệt hoa thường để xử lý camelCase responses từ BFF/microservices.
|
|
private static readonly JsonSerializerOptions _jsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
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;
|
|
_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);
|
|
}
|
|
}
|
|
|
|
private static async Task<string> TryExtractError(HttpResponseMessage resp)
|
|
{
|
|
if ((int)resp.StatusCode == 401)
|
|
return "Phiên đăng nhập hết hạn. Vui lòng đăng nhập lại.";
|
|
try
|
|
{
|
|
var body = await resp.Content.ReadAsStringAsync();
|
|
using var doc = JsonDocument.Parse(body);
|
|
if (doc.RootElement.TryGetProperty("message", out var msg))
|
|
return msg.GetString() ?? resp.StatusCode.ToString();
|
|
if (doc.RootElement.TryGetProperty("detail", out var detail))
|
|
return detail.GetString() ?? resp.StatusCode.ToString();
|
|
if (doc.RootElement.TryGetProperty("title", out var title))
|
|
return title.GetString() ?? resp.StatusCode.ToString();
|
|
return body.Length > 200 ? $"Lỗi ({resp.StatusCode})" : body;
|
|
}
|
|
catch { return $"Lỗi ({resp.StatusCode})"; }
|
|
}
|
|
|
|
/// <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();
|
|
}
|
|
|
|
// Case 5: { "<anyKey>": [...] } — generic single-array-property wrapper (e.g. { "members": [...] })
|
|
foreach (var prop in root.EnumerateObject())
|
|
{
|
|
if (prop.Value.ValueKind == JsonValueKind.Array)
|
|
return JsonSerializer.Deserialize<List<T>>(prop.Value.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? CategoryName, int? DurationMinutes, Guid? CategoryId = null)
|
|
{
|
|
public string? Category => CategoryName;
|
|
}
|
|
public record CategoryInfo(Guid Id, string Name, string? Description, int DisplayOrder, string? ImageUrl = null);
|
|
public record TableInfo(Guid Id, string TableNumber, int Capacity, string? Zone, string Status, Guid? SessionId, int? GuestCount, DateTime? StartedAt, decimal HourlyRate = 0, int? PositionX = null, int? PositionY = null, string? QrToken = null);
|
|
public record AppointmentInfo(Guid Id, Guid? CustomerId, Guid? StaffId, Guid? ResourceId, Guid ServiceId, DateTime StartTime, DateTime EndTime, string Status, string? ResourceName);
|
|
public record ShopAssignmentInfo(Guid ShopId, string? ShopRole, Guid? BranchId);
|
|
public record StaffInfo(Guid Id, Guid? UserId, string? EmployeeCode, string? Phone, string? Email, DateTime? JoinedAt, DateTime? TerminatedAt, string? Role, string? Status, string? ShopName,
|
|
string? FirstName = null, string? LastName = null, string? Address = null, string? ProfilePhotoUrl = null, string? DocumentFrontUrl = null, string? DocumentBackUrl = null,
|
|
List<ShopAssignmentInfo>? ShopAssignments = null);
|
|
|
|
public async Task<List<StaffInfo>> GetStaffForShopAsync(Guid shopId)
|
|
{
|
|
var all = await GetStaffAsync();
|
|
return all.Where(s =>
|
|
s.ShopAssignments == null || s.ShopAssignments.Count == 0 ||
|
|
s.ShopAssignments.Any(a => a.ShopId == shopId)).ToList();
|
|
}
|
|
|
|
public async Task<List<ShopInfo>> GetShopsAsync()
|
|
=> await GetListFromApiAsync<ShopInfo>("api/bff/shops");
|
|
|
|
public async Task<ShopInfo?> GetShopByIdAsync(Guid shopId)
|
|
=> await GetObjectFromApiAsync<ShopInfo>($"api/bff/shops/{shopId}");
|
|
|
|
public async Task<List<ProductInfo>> GetProductsAsync(Guid shopId)
|
|
=> await GetListFromApiAsync<ProductInfo>($"api/bff/shops/{shopId}/products");
|
|
|
|
public async Task<List<CategoryInfo>> GetCategoriesAsync(Guid shopId)
|
|
=> await GetListFromApiAsync<CategoryInfo>($"api/bff/shops/{shopId}/categories");
|
|
|
|
public async Task<List<TableInfo>> GetTablesAsync(Guid shopId)
|
|
=> await GetListFromApiAsync<TableInfo>($"api/bff/shops/{shopId}/tables");
|
|
|
|
public async Task<List<AppointmentInfo>> GetAppointmentsAsync(Guid shopId)
|
|
=> await GetListFromApiAsync<AppointmentInfo>($"api/bff/shops/{shopId}/appointments");
|
|
|
|
public async Task<List<StaffInfo>> GetStaffAsync()
|
|
=> await GetListFromApiAsync<StaffInfo>("api/bff/staff");
|
|
|
|
// ═══ 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, Guid? CategoryId = null);
|
|
public record AdminCategoryInfo(Guid Id, string Name, string? Description, int DisplayOrder,
|
|
Guid ShopId, Guid? ParentId, bool IsActive, string? ImageUrl = null);
|
|
public record CreateProductRequest(Guid ShopId, string Name, string? Description, decimal Price,
|
|
string? Type, string? Sku, string? ImageUrl, Guid? CategoryId = null);
|
|
|
|
public async Task<List<AdminProductInfo>> GetAllProductsAsync(Guid? shopId = null)
|
|
{
|
|
var url = shopId.HasValue ? $"api/bff/products?shopId={shopId}" : "api/bff/products";
|
|
return await GetListFromApiAsync<AdminProductInfo>(url);
|
|
}
|
|
|
|
public async Task<List<AdminCategoryInfo>> GetAllCategoriesAsync(Guid? shopId = null)
|
|
{
|
|
var url = shopId.HasValue ? $"api/bff/categories?shopId={shopId}" : "api/bff/categories";
|
|
return await GetListFromApiAsync<AdminCategoryInfo>(url);
|
|
}
|
|
|
|
public async Task<bool> CreateProductAsync(CreateProductRequest req)
|
|
{
|
|
AttachToken();
|
|
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, _writeOptions);
|
|
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)
|
|
{
|
|
var url = shopId.HasValue ? $"api/bff/inventory?shopId={shopId}" : "api/bff/inventory";
|
|
return await GetListFromApiAsync<InventoryItemInfo>(url);
|
|
}
|
|
|
|
// ═══ MEMBERSHIP/CUSTOMER METHODS ═══
|
|
|
|
public record MemberInfo(Guid Id, string? CountryCode, string? Gender, int CurrentExp,
|
|
int CurrentLevel, int TotalExpEarned, DateTime CreatedAt, string? LevelName,
|
|
string? DisplayName = null, string? Phone = null);
|
|
|
|
public async Task<List<MemberInfo>> GetMembersAsync(string? search = null)
|
|
{
|
|
var url = string.IsNullOrWhiteSpace(search) ? "api/bff/members" : $"api/bff/members?search={Uri.EscapeDataString(search)}";
|
|
return await GetListFromApiAsync<MemberInfo>(url);
|
|
}
|
|
|
|
// ═══ STAFF CREATE ═══
|
|
|
|
public record CreateStaffRequest(Guid MerchantId, string? EmployeeCode, string? Phone, string? Email, string? Role,
|
|
string? FirstName = null, string? LastName = null, string? Address = null,
|
|
string? ProfilePhotoUrl = null, string? DocumentFrontUrl = null, string? DocumentBackUrl = null);
|
|
|
|
public async Task<bool> CreateStaffAsync(CreateStaffRequest req)
|
|
{
|
|
AttachToken();
|
|
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, _writeOptions);
|
|
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, _writeOptions);
|
|
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()
|
|
=> await GetListFromApiAsync<StaffRoleInfo>("api/bff/staff/roles");
|
|
|
|
public async Task<List<ScheduleInfo>> GetStaffSchedulesAsync(Guid? shopId = null)
|
|
{
|
|
var url = shopId.HasValue ? $"api/bff/staff/schedules?shopId={shopId}" : "api/bff/staff/schedules";
|
|
return await GetListFromApiAsync<ScheduleInfo>(url);
|
|
}
|
|
|
|
// ═══ ORDERS ═══
|
|
|
|
public record OrderInfo(Guid Id, Guid ShopId, decimal TotalAmount, int StatusId, DateTime CreatedAt,
|
|
string? Status, string? PaymentMethod, string? Notes, int ItemCount = 0);
|
|
|
|
public async Task<List<OrderInfo>> GetOrdersAsync(Guid? shopId = null, string filter = "today")
|
|
{
|
|
var url = shopId.HasValue
|
|
? $"api/bff/orders?shopId={shopId}&filter={filter}"
|
|
: $"api/bff/orders?filter={filter}";
|
|
return await GetListFromApiAsync<OrderInfo>(url);
|
|
}
|
|
|
|
// ═══ 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()
|
|
=> await GetListFromApiAsync<WalletInfo>("api/bff/wallets");
|
|
|
|
public async Task<List<WalletTxnInfo>> GetWalletTransactionsAsync(int limit = 50)
|
|
=> 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()
|
|
=> await GetListFromApiAsync<DeviceInfo>("api/bff/devices");
|
|
|
|
// ═══ 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()
|
|
=> await GetListFromApiAsync<PromotionInfo>("api/bff/promotions");
|
|
|
|
// ═══ CAMPAIGNS CRUD ═══
|
|
|
|
// EN: Campaign record and request DTOs for CRUD operations
|
|
// VI: Record chiến dịch và DTO yêu cầu cho CRUD
|
|
public record CampaignInfo(Guid Id, string Name, string? Description, decimal FaceValue,
|
|
int TotalVouchers, int IssuedVouchers, DateTime? StartDate, DateTime? EndDate, string? Status, DateTime CreatedAt);
|
|
public record PaginatedCampaignResponse(List<CampaignInfo>? Items, int TotalCount, int PageNumber, int PageSize, int TotalPages);
|
|
public record CreateCampaignRequest(string Name, string? Description, decimal FaceValue, int TotalVouchers, DateTime StartDate, DateTime EndDate);
|
|
|
|
public async Task<List<CampaignInfo>> GetCampaignsAsync()
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.GetFromJsonAsync<PaginatedCampaignResponse>("api/bff/campaigns", _jsonOptions);
|
|
return resp?.Items ?? new();
|
|
}
|
|
|
|
public async Task<bool> CreateCampaignAsync(CreateCampaignRequest req)
|
|
{
|
|
AttachToken();
|
|
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, _writeOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
public async Task<bool> DeleteCampaignAsync(Guid campaignId)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.DeleteAsync($"api/bff/campaigns/{campaignId}");
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ MEMBER CRUD ═══
|
|
|
|
// EN: Member create/update request DTOs
|
|
// VI: DTO tạo/cập nhật thành viên
|
|
public record CreateMemberRequest(string? Gender, string? CountryCode, string? Name = null, string? Phone = null);
|
|
public record UpdateMemberRequest(string? Gender, string? Preferences);
|
|
|
|
public async Task<(bool Ok, string? Error)> CreateMemberAsync(CreateMemberRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync("api/bff/members", req, _writeOptions);
|
|
if (resp.IsSuccessStatusCode) return (true, null);
|
|
var err = await TryExtractError(resp);
|
|
return (false, err);
|
|
}
|
|
|
|
public async Task<bool> UpdateMemberAsync(Guid memberId, UpdateMemberRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PutAsJsonAsync($"api/bff/members/{memberId}", req, _writeOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
public async Task<bool> DeleteMemberAsync(Guid memberId)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.DeleteAsync($"api/bff/members/{memberId}");
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ 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)
|
|
{
|
|
var url = shopId.HasValue ? $"api/bff/inventory/transactions?shopId={shopId}" : "api/bff/inventory/transactions";
|
|
return await GetListFromApiAsync<InventoryTxnInfo>(url);
|
|
}
|
|
|
|
// ═══ INVENTORY OPERATIONS ═══
|
|
|
|
public record StockInRequest(Guid ProductId, Guid ShopId, int Amount, string? Notes);
|
|
|
|
public async Task<bool> StockInAsync(StockInRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync("api/bff/inventory/stock-in", req, _writeOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
public record StockOutRequest(Guid ProductId, Guid ShopId, int Amount, string? Notes);
|
|
|
|
public async Task<bool> StockOutAsync(StockOutRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync("api/bff/inventory/stock-out", req, _writeOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
public record AdjustStockRequest(Guid ProductId, Guid ShopId, int NewQuantity, string Notes);
|
|
|
|
public async Task<bool> AdjustStockAsync(AdjustStockRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync("api/bff/inventory/adjust", req, _writeOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
public record LowStockItemInfo(Guid Id, Guid ProductId, string? ProductName, int Quantity, int LowStockThreshold, Guid ShopId);
|
|
|
|
public async Task<List<LowStockItemInfo>> GetLowStockAsync(Guid? shopId = null)
|
|
{
|
|
var url = shopId.HasValue ? $"api/bff/inventory/low-stock?shopId={shopId}" : "api/bff/inventory/low-stock";
|
|
return await GetListFromApiAsync<LowStockItemInfo>(url);
|
|
}
|
|
|
|
// ═══ MEMBERSHIP LEVELS ═══
|
|
|
|
public record LevelDefinitionInfo(Guid Id, int LevelNumber, string Name, int RequiredExp,
|
|
string? Description = null, string? BadgeColor = null, bool IsActive = true, int MemberCount = 0,
|
|
int MinExp = 0, int MaxExp = 0)
|
|
{
|
|
public int Level => LevelNumber;
|
|
};
|
|
|
|
public async Task<List<LevelDefinitionInfo>> GetMembershipLevelsAsync()
|
|
=> await GetListFromApiAsync<LevelDefinitionInfo>("api/bff/membership/levels");
|
|
|
|
public static List<LevelDefinitionInfo> EnrichLevelDefinitions(List<LevelDefinitionInfo> levels, List<MemberInfo> members)
|
|
{
|
|
if (!levels.Any()) return levels;
|
|
var sorted = levels.OrderBy(l => l.LevelNumber).ToList();
|
|
var enriched = new List<LevelDefinitionInfo>();
|
|
for (int i = 0; i < sorted.Count; i++)
|
|
{
|
|
var lvl = sorted[i];
|
|
int minExp = lvl.RequiredExp;
|
|
int maxExp = (i + 1 < sorted.Count) ? sorted[i + 1].RequiredExp - 1 : int.MaxValue;
|
|
int memberCount = members.Count(m => m.CurrentLevel == lvl.LevelNumber);
|
|
enriched.Add(lvl with { MinExp = minExp, MaxExp = maxExp == int.MaxValue ? 999999 : maxExp, MemberCount = memberCount });
|
|
}
|
|
return enriched;
|
|
}
|
|
|
|
public static List<MemberInfo> ResolveMemberLevelNames(List<MemberInfo> members, List<LevelDefinitionInfo> levels)
|
|
{
|
|
var levelMap = levels.ToDictionary(l => l.LevelNumber, l => l.Name);
|
|
return members.Select(m => m with { LevelName = levelMap.GetValueOrDefault(m.CurrentLevel) ?? m.LevelName }).ToList();
|
|
}
|
|
|
|
// ═══ LEVEL DEFINITION CRUD ═══
|
|
|
|
public record CreateLevelRequest(int LevelNumber, string Name, int RequiredExp, string? Description, string? BadgeColor);
|
|
|
|
public async Task<(bool Ok, string? Error)> CreateLevelAsync(CreateLevelRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync("api/bff/membership/levels", req, _writeOptions);
|
|
if (resp.IsSuccessStatusCode) return (true, null);
|
|
return (false, await TryExtractError(resp));
|
|
}
|
|
|
|
public async Task<(bool Ok, string? Error)> UpdateLevelAsync(Guid levelId, CreateLevelRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PutAsJsonAsync($"api/bff/membership/levels/{levelId}", req, _writeOptions);
|
|
if (resp.IsSuccessStatusCode) return (true, null);
|
|
return (false, await TryExtractError(resp));
|
|
}
|
|
|
|
public async Task<bool> DeleteLevelAsync(Guid levelId)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.DeleteAsync($"api/bff/membership/levels/{levelId}");
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ EXP MANAGEMENT ═══
|
|
|
|
public record AddExpRequest(int Points, int SourceId, string? ReferenceId);
|
|
public record AddExpResult(Guid MemberId, int PointsAdded, int CurrentExp, int TotalExpEarned,
|
|
int PreviousLevel, int CurrentLevel, bool LeveledUp);
|
|
public record MemberProgressInfo(Guid MemberId, int CurrentLevel, string? CurrentLevelName,
|
|
int CurrentExp, int TotalExpEarned, int ExpToNextLevel, int ProgressPercent,
|
|
int? NextLevel, string? NextLevelName, string? BadgeColor);
|
|
public record ExpTransactionInfo(Guid Id, int Points, string? Source, int SourceId,
|
|
string? ReferenceId, int LevelAtTime, DateTime CreatedAt);
|
|
|
|
public async Task<AddExpResult?> AddExperienceAsync(Guid memberId, AddExpRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync($"api/bff/members/{memberId}/experience", req, _writeOptions);
|
|
if (resp.IsSuccessStatusCode)
|
|
return await resp.Content.ReadFromJsonAsync<AddExpResult>(_jsonOptions);
|
|
return null;
|
|
}
|
|
|
|
public async Task<MemberProgressInfo?> GetMemberProgressAsync(Guid memberId)
|
|
=> await GetObjectFromApiAsync<MemberProgressInfo>($"api/bff/members/{memberId}/progress");
|
|
|
|
public async Task<List<ExpTransactionInfo>> GetExperienceHistoryAsync(Guid memberId)
|
|
=> await GetListFromApiAsync<ExpTransactionInfo>($"api/bff/members/{memberId}/experience");
|
|
|
|
// ═══ FILE UPLOAD (Storage Service) ═══
|
|
|
|
public async Task<string?> UploadImageAsync(Stream fileStream, string fileName, string contentType)
|
|
{
|
|
AttachToken();
|
|
using var content = new MultipartFormDataContent();
|
|
var streamContent = new StreamContent(fileStream);
|
|
streamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
|
|
content.Add(streamContent, "file", fileName);
|
|
var resp = await _http.PostAsync("api/bff/files/upload?accessLevel=public", content);
|
|
if (!resp.IsSuccessStatusCode) return null;
|
|
var json = await resp.Content.ReadAsStringAsync();
|
|
using var doc = JsonDocument.Parse(json);
|
|
// EN: Try to extract download URL or file ID from response
|
|
if (doc.RootElement.TryGetProperty("downloadUrl", out var url))
|
|
return url.GetString();
|
|
if (doc.RootElement.TryGetProperty("data", out var data))
|
|
{
|
|
if (data.TryGetProperty("downloadUrl", out var dUrl)) return dUrl.GetString();
|
|
if (data.TryGetProperty("fileId", out var fId)) return $"api/bff/files/{fId.GetString()}/download";
|
|
}
|
|
if (doc.RootElement.TryGetProperty("fileId", out var fileId))
|
|
return $"api/bff/files/{fileId.GetString()}/download";
|
|
return null;
|
|
}
|
|
|
|
// ═══ SHOP STATS (aggregated per-shop) ═══
|
|
|
|
public record ShopStatsInfo(Guid ShopId, int ProductCount, int OrderCount, int StaffCount, decimal Revenue);
|
|
|
|
public async Task<List<ShopStatsInfo>> GetShopStatsAsync()
|
|
=> 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)
|
|
=> await GetListFromApiAsync<ResourceInfo>($"api/bff/shops/{shopId}/resources");
|
|
|
|
// ═══ POS DASHBOARD (real-time daily stats) ═══
|
|
|
|
// EN: POS dashboard response DTOs — matching camelCase API response from OrderService
|
|
// VI: DTOs cho response dashboard POS — khớp camelCase API response từ OrderService
|
|
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(Guid ProductId, string ProductName, int QuantitySold, decimal Revenue)
|
|
{
|
|
// EN: Aliases for Razor display / VI: Alias cho hiển thị Razor
|
|
public string Name => ProductName;
|
|
public int Qty => QuantitySold;
|
|
}
|
|
public record PaymentBreakdownInfo(string Method, int Count, decimal Amount)
|
|
{
|
|
public int Pct { get; set; }
|
|
}
|
|
public record HourlyRevenueInfo(int Hour, decimal Revenue, int OrderCount)
|
|
{
|
|
public string HourLabel => $"{Hour}h";
|
|
public int Pct { get; set; }
|
|
}
|
|
public record RecentOrderInfo(Guid Id, decimal TotalAmount, string Status, int ItemCount, DateTime CreatedAt);
|
|
|
|
public async Task<PosDashboardInfo> GetPosDashboardAsync(Guid shopId, string? period = "today")
|
|
{
|
|
AttachToken();
|
|
return await _http.GetFromJsonAsync<PosDashboardInfo>(
|
|
$"api/bff/pos/dashboard?shopId={shopId}&period={period ?? "today"}", _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,
|
|
decimal? DiscountAmount = null, string? DiscountType = null, string? DiscountReference = null, Guid? TableId = null);
|
|
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)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync("api/bff/pos/orders", req, _writeOptions);
|
|
if (resp.IsSuccessStatusCode)
|
|
return await resp.Content.ReadFromJsonAsync<CreatePosOrderResponse>(_jsonOptions);
|
|
return null;
|
|
}
|
|
|
|
// ═══ PAY ORDER ═══
|
|
|
|
public async Task<bool> PayOrderAsync(Guid orderId, Guid shopId)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync($"api/bff/orders/{orderId}/pay?shopId={shopId}", new { }, _writeOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ ACTIVE TABLE ORDERS ═══
|
|
|
|
// EN: DTOs for active table orders (orders with table_id, status=Validated)
|
|
// VI: DTOs cho active table orders (orders có table_id, status=Validated)
|
|
public record ActiveTableOrderDto
|
|
{
|
|
public Guid OrderId { get; init; }
|
|
public Guid? TableId { get; init; }
|
|
public decimal TotalAmount { get; init; }
|
|
public DateTime CreatedAt { get; init; }
|
|
public List<ActiveTableOrderItemDto> Items { get; init; } = new();
|
|
}
|
|
|
|
public record ActiveTableOrderItemDto
|
|
{
|
|
public Guid ProductId { get; init; }
|
|
public string ProductName { get; init; } = string.Empty;
|
|
public int Quantity { get; init; }
|
|
public decimal UnitPrice { get; init; }
|
|
}
|
|
|
|
public async Task<List<ActiveTableOrderDto>> GetActiveTableOrdersAsync(Guid shopId)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.GetAsync($"api/bff/orders/active-by-table?shopId={shopId}");
|
|
if (resp.IsSuccessStatusCode)
|
|
return await resp.Content.ReadFromJsonAsync<List<ActiveTableOrderDto>>(_jsonOptions) ?? new();
|
|
return new();
|
|
}
|
|
|
|
// ═══ 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, string? ImageUrl = null);
|
|
|
|
public async Task<bool> CreateCategoryAsync(AdminCreateCategoryRequest req)
|
|
{
|
|
AttachToken();
|
|
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, _writeOptions);
|
|
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, Guid? shopId = null)
|
|
{
|
|
AttachToken();
|
|
var qs = shopId.HasValue ? $"?shopId={shopId}" : "";
|
|
return await _http.GetFromJsonAsync<OrderDetailResponse>($"api/bff/orders/{orderId}{qs}", _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, _writeOptions);
|
|
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)
|
|
{
|
|
var url = shopId.HasValue
|
|
? $"api/bff/reports/revenue?period={period}&shopId={shopId}"
|
|
: $"api/bff/reports/revenue?period={period}";
|
|
return await GetListFromApiAsync<RevenueReportItem>(url);
|
|
}
|
|
|
|
// ═══ SHOP SETTINGS ═══
|
|
|
|
public record ShopFeaturesInfo
|
|
{
|
|
public bool HasInventory { get; init; }
|
|
public bool HasBooking { get; init; }
|
|
public bool HasTables { get; init; }
|
|
public bool HasKitchen { get; init; }
|
|
public bool HasShipping { get; init; }
|
|
public bool HasDelivery { get; init; }
|
|
}
|
|
public record ShopSettingsInfo
|
|
{
|
|
public Guid ShopId { get; init; }
|
|
public ShopFeaturesInfo? Features { get; init; }
|
|
public string? OpenTime { get; init; }
|
|
public string? CloseTime { get; init; }
|
|
public List<string>? OpenDays { get; init; }
|
|
}
|
|
public record UpdateShopSettingsRequest(ShopFeaturesInfo? Features, string? OpenTime, string? CloseTime, List<string>? OpenDays);
|
|
|
|
public async Task<ShopSettingsInfo?> GetShopSettingsAsync(Guid shopId)
|
|
=> await GetObjectFromApiAsync<ShopSettingsInfo>($"api/bff/shops/{shopId}/settings");
|
|
|
|
public async Task<bool> UpdateShopSettingsAsync(Guid shopId, UpdateShopSettingsRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PutAsJsonAsync($"api/bff/shops/{shopId}/settings", req, _writeOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ TOP PRODUCTS ═══
|
|
|
|
public record TopProductInfo(string? ProductName, long TotalSold, decimal TotalRevenue);
|
|
|
|
public async Task<List<TopProductInfo>> GetTopProductsAsync(Guid? shopId = null, int limit = 10)
|
|
{
|
|
var url = shopId.HasValue
|
|
? $"api/bff/reports/top-products?shopId={shopId}&limit={limit}"
|
|
: $"api/bff/reports/top-products?limit={limit}";
|
|
return await GetListFromApiAsync<TopProductInfo>(url);
|
|
}
|
|
|
|
// ═══ TABLES CRUD ═══
|
|
|
|
public record CreateTableRequest(Guid ShopId, string TableNumber, int Capacity, string? Zone, decimal? HourlyRate = null);
|
|
|
|
public async Task<bool> CreateTableAsync(CreateTableRequest req)
|
|
{ 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, _writeOptions); return r.IsSuccessStatusCode; }
|
|
|
|
public async Task<bool> UpdateTablePositionAsync(Guid tableId, int x, int y)
|
|
{ AttachToken(); var r = await _http.PutAsJsonAsync($"api/bff/tables/{tableId}", new { positionX = x, positionY = y }, _writeOptions); return r.IsSuccessStatusCode; }
|
|
|
|
public async Task<bool> DeleteTableAsync(Guid tableId)
|
|
{ AttachToken(); var r = await _http.DeleteAsync($"api/bff/tables/{tableId}"); return r.IsSuccessStatusCode; }
|
|
|
|
// ═══ APPOINTMENTS CRUD ═══
|
|
|
|
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, _writeOptions); return r.IsSuccessStatusCode; }
|
|
|
|
public async Task<bool> UpdateAppointmentAsync(Guid apptId, CreateAppointmentRequest req)
|
|
{ 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; }
|
|
|
|
// ═══ RESOURCES CRUD ═══
|
|
|
|
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, _writeOptions); return r.IsSuccessStatusCode; }
|
|
|
|
public async Task<bool> UpdateResourceAsync(Guid resourceId, CreateResourceRequest req)
|
|
{ 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; }
|
|
|
|
// ═══ STAFF SCHEDULES CRUD ═══
|
|
|
|
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/staff/schedules", req, _writeOptions); return r.IsSuccessStatusCode; }
|
|
|
|
public async Task<bool> UpdateScheduleAsync(Guid scheduleId, CreateScheduleRequest req)
|
|
{ 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/staff/schedules/{scheduleId}"); return r.IsSuccessStatusCode; }
|
|
|
|
// ═══ KITCHEN TICKETS ═══
|
|
|
|
public record KitchenTicketInfo(Guid Id, Guid SessionId, Guid OrderItemId, string ItemName, string? Station, int Priority, string Status, DateTime CreatedAt, DateTime? CompletedAt);
|
|
public record UpdateTicketStatusRequest(string Status);
|
|
|
|
public async Task<List<KitchenTicketInfo>> GetKitchenTicketsAsync(Guid? shopId = null, string status = "Pending")
|
|
{
|
|
if (!shopId.HasValue) return new();
|
|
var url = $"api/bff/shops/{shopId}/kitchen-tickets?status={status}";
|
|
return await GetListFromApiAsync<KitchenTicketInfo>(url);
|
|
}
|
|
|
|
public async Task<bool> UpdateTicketStatusAsync(Guid ticketId, UpdateTicketStatusRequest req)
|
|
{
|
|
AttachToken();
|
|
var request = new HttpRequestMessage(HttpMethod.Patch, $"api/bff/kitchen/tickets/{ticketId}/status")
|
|
{
|
|
Content = JsonContent.Create(req, options: _writeOptions)
|
|
};
|
|
var r = await _http.SendAsync(request);
|
|
return r.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ RECIPES CRUD ═══
|
|
|
|
public record RecipeIngredientInfo(Guid Id, Guid RecipeId, string IngredientName, decimal Quantity, string Unit, decimal CostPerUnit);
|
|
public record RecipeInfo(Guid Id, Guid ProductId, Guid ShopId, string Name, string? Instructions, int PrepTimeMinutes, bool IsActive, DateTime CreatedAt);
|
|
public record CreateRecipeRequest(Guid ShopId, Guid ProductId, string Name, string? Instructions, int PrepTimeMinutes, List<RecipeIngredientRequest>? Ingredients);
|
|
public record RecipeIngredientRequest(string IngredientName, decimal Quantity, string Unit, decimal CostPerUnit);
|
|
|
|
public async Task<List<RecipeInfo>> GetRecipesAsync(Guid? shopId = null)
|
|
{
|
|
if (!shopId.HasValue) return new();
|
|
var url = $"api/bff/shops/{shopId}/recipes";
|
|
return await GetListFromApiAsync<RecipeInfo>(url);
|
|
}
|
|
|
|
public async Task<bool> CreateRecipeAsync(CreateRecipeRequest req)
|
|
{ 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, _writeOptions); return r.IsSuccessStatusCode; }
|
|
|
|
public async Task<bool> DeleteRecipeAsync(Guid recipeId)
|
|
{ AttachToken(); var r = await _http.DeleteAsync($"api/bff/recipes/{recipeId}"); return r.IsSuccessStatusCode; }
|
|
|
|
// ═══ VOUCHER VALIDATION ═══
|
|
|
|
public record VoucherValidationInfo(bool IsValid, string? ErrorMessage, Guid? VoucherId,
|
|
string? VoucherCode, decimal? RemainingValue, DateTime? ExpiresAt, string? CampaignName);
|
|
|
|
public async Task<VoucherValidationInfo?> ValidateVoucherAsync(string code)
|
|
=> await GetObjectFromApiAsync<VoucherValidationInfo>($"api/bff/vouchers/validate/{Uri.EscapeDataString(code)}");
|
|
|
|
public async Task<bool> RedeemVoucherAsync(Guid voucherId, decimal amount)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync("api/bff/vouchers/redeem", new { voucherId, amount }, _writeOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ ADMIN VOUCHER MANAGEMENT ═══
|
|
|
|
public record AdminVoucherInfo(Guid Id, Guid CampaignId, string? CampaignName, string? Code,
|
|
Guid? OwnerId, string? OwnerEmail, decimal FaceValue, decimal RemainingValue, string? Status,
|
|
DateTime? ClaimedAt, DateTime? ExpiresAt, DateTime? RedeemedAt, DateTime? CreatedAt);
|
|
|
|
public record PaginatedVoucherResponse(List<AdminVoucherInfo>? Items, int TotalCount, int PageNumber, int PageSize, int TotalPages);
|
|
|
|
public async Task<List<AdminVoucherInfo>> GetAdminVouchersAsync(Guid? campaignId = null, string? status = null, int pageSize = 50)
|
|
{
|
|
AttachToken();
|
|
var qs = $"pageSize={pageSize}";
|
|
if (campaignId.HasValue) qs += $"&campaignId={campaignId}";
|
|
if (!string.IsNullOrEmpty(status)) qs += $"&status={status}";
|
|
var resp = await _http.GetFromJsonAsync<PaginatedVoucherResponse>($"api/bff/vouchers/list?{qs}", _jsonOptions);
|
|
return resp?.Items ?? new();
|
|
}
|
|
|
|
public async Task<bool> RevokeVoucherAsync(Guid voucherId)
|
|
{ AttachToken(); var r = await _http.PostAsync($"api/bff/vouchers/{voucherId}/revoke", null); return r.IsSuccessStatusCode; }
|
|
|
|
// ═══ CAMPAIGN ACTIONS ═══
|
|
|
|
public async Task<bool> ActivateCampaignAsync(Guid campaignId)
|
|
{ AttachToken(); var r = await _http.PostAsync($"api/bff/campaigns/{campaignId}/activate", null); return r.IsSuccessStatusCode; }
|
|
|
|
public async Task<bool> PauseCampaignAsync(Guid campaignId)
|
|
{ AttachToken(); var r = await _http.PostAsync($"api/bff/campaigns/{campaignId}/pause", null); return r.IsSuccessStatusCode; }
|
|
|
|
// ═══ STAFF IAM ═══
|
|
|
|
public record InviteStaffWithAccountRequest(string Email, string Password, string FirstName, string LastName, string Role, Guid? ShopId);
|
|
|
|
public async Task<(bool Ok, string? Error)> InviteStaffWithAccountAsync(InviteStaffWithAccountRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync("api/bff/staff/invite-with-account", req, _writeOptions);
|
|
if (resp.IsSuccessStatusCode) return (true, null);
|
|
var err = await TryExtractError(resp);
|
|
return (false, err);
|
|
}
|
|
|
|
// ═══ STORAGE / DRIVE ═══
|
|
|
|
public record StorageFileInfo(Guid Id, string FileName, string? ContentType, long FileSizeBytes, string? AccessLevel, DateTime UploadedAt);
|
|
public record StorageFolderInfo(Guid Id, Guid? ParentId, string Name, string? Path, DateTime CreatedAt);
|
|
public record CreateFolderRequest(string Name, Guid? ParentId);
|
|
|
|
// Storage API wraps responses in { success, data, error }
|
|
private record StorageApiResponse<T>(bool Success, T? Data, string? Error);
|
|
private record UserFilesResult(List<StorageFileInfo> Files, int TotalCount);
|
|
|
|
public async Task<List<StorageFileInfo>> GetStorageFilesAsync(int skip = 0, int take = 50, string? search = null)
|
|
{
|
|
AttachToken();
|
|
var qs = $"?skip={skip}&take={take}";
|
|
if (!string.IsNullOrEmpty(search)) qs += $"&search={Uri.EscapeDataString(search)}";
|
|
var wrapper = await _http.GetFromJsonAsync<StorageApiResponse<UserFilesResult>>($"api/bff/files{qs}", _jsonOptions);
|
|
return wrapper?.Data?.Files ?? new();
|
|
}
|
|
|
|
public async Task<bool> DeleteStorageFileAsync(Guid fileId)
|
|
{ AttachToken(); var r = await _http.DeleteAsync($"api/bff/files/{fileId}"); return r.IsSuccessStatusCode; }
|
|
|
|
public async Task<string?> GetDownloadUrlAsync(Guid fileId)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.GetAsync($"api/bff/files/{fileId}/download-url");
|
|
if (!resp.IsSuccessStatusCode) return null;
|
|
var json = await resp.Content.ReadFromJsonAsync<JsonElement>(_jsonOptions);
|
|
if (json.TryGetProperty("data", out var data))
|
|
{
|
|
if (data.TryGetProperty("url", out var url)) return url.GetString();
|
|
if (data.TryGetProperty("downloadUrl", out var dl)) return dl.GetString();
|
|
}
|
|
if (json.TryGetProperty("url", out var directUrl)) return directUrl.GetString();
|
|
return null;
|
|
}
|
|
|
|
public async Task<List<StorageFolderInfo>> GetFoldersAsync(Guid? parentId = null)
|
|
{
|
|
AttachToken();
|
|
var qs = parentId.HasValue ? $"?parentId={parentId}" : "";
|
|
var wrapper = await _http.GetFromJsonAsync<StorageApiResponse<IEnumerable<StorageFolderInfo>>>($"api/bff/folders{qs}", _jsonOptions);
|
|
return wrapper?.Data?.ToList() ?? new();
|
|
}
|
|
|
|
public async Task<(bool Ok, string? Error)> CreateFolderAsync(CreateFolderRequest req)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync("api/bff/folders", req, _writeOptions);
|
|
if (resp.IsSuccessStatusCode) return (true, null);
|
|
var err = await TryExtractError(resp);
|
|
return (false, err);
|
|
}
|
|
|
|
public async Task<bool> DeleteFolderAsync(Guid folderId)
|
|
{ AttachToken(); var r = await _http.DeleteAsync($"api/bff/folders/{folderId}"); return r.IsSuccessStatusCode; }
|
|
|
|
public async Task<bool> UploadFileRawAsync(MultipartFormDataContent content)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsync("api/bff/files/upload?accessLevel=public", content);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ TABLE QR ═══
|
|
|
|
public async Task<string?> GenerateTableQrTokenAsync(Guid tableId)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync($"api/bff/tables/{tableId}/generate-qr", new { }, _writeOptions);
|
|
if (resp.IsSuccessStatusCode)
|
|
{
|
|
var json = await resp.Content.ReadFromJsonAsync<System.Text.Json.JsonElement>(_jsonOptions);
|
|
if (json.TryGetProperty("data", out var data) && data.TryGetProperty("qrToken", out var token))
|
|
return token.GetString();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public record TableByTokenInfo(Guid Id, Guid ShopId, string TableNumber, int Capacity, string? Zone);
|
|
|
|
public async Task<TableByTokenInfo?> GetTableByTokenAsync(string token)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.GetAsync($"api/bff/tables/by-token/{token}");
|
|
if (resp.IsSuccessStatusCode)
|
|
{
|
|
var json = await resp.Content.ReadFromJsonAsync<System.Text.Json.JsonElement>(_jsonOptions);
|
|
if (json.TryGetProperty("data", out var data))
|
|
return System.Text.Json.JsonSerializer.Deserialize<TableByTokenInfo>(data.GetRawText(), _jsonOptions);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ═══ RESERVATIONS ═══
|
|
|
|
public record ReservationInfo(Guid Id, Guid ShopId, Guid? TableId, string GuestName, string? Phone,
|
|
int PartySize, DateTime ReservationTime, string Status, string? Note, DateTime CreatedAt);
|
|
public record CreateReservationRequest2(Guid ShopId, string GuestName, int PartySize, DateTime ReservationTime,
|
|
string? Phone = null, Guid? TableId = null, string? Note = null);
|
|
|
|
public async Task<List<ReservationInfo>> GetReservationsAsync(Guid shopId, string? date = null)
|
|
{
|
|
var url = $"api/bff/shops/{shopId}/reservations";
|
|
if (!string.IsNullOrEmpty(date)) url += $"?date={Uri.EscapeDataString(date)}";
|
|
return await GetListFromApiAsync<ReservationInfo>(url);
|
|
}
|
|
|
|
public async Task<bool> CreateReservationAsync(CreateReservationRequest2 req)
|
|
{ AttachToken(); var r = await _http.PostAsJsonAsync("api/bff/reservations", req, _writeOptions); return r.IsSuccessStatusCode; }
|
|
|
|
public async Task<bool> UpdateReservationStatusAsync(Guid reservationId, string status)
|
|
{
|
|
AttachToken();
|
|
var request = new HttpRequestMessage(HttpMethod.Patch, $"api/bff/reservations/{reservationId}/status")
|
|
{
|
|
Content = JsonContent.Create(new { status }, options: _writeOptions)
|
|
};
|
|
var r = await _http.SendAsync(request);
|
|
return r.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ TABLE STATUS ═══
|
|
|
|
public async Task<bool> UpdateTableStatusAsync(Guid tableId, string status)
|
|
{
|
|
AttachToken();
|
|
var request = new HttpRequestMessage(HttpMethod.Patch, $"api/bff/tables/{tableId}/status")
|
|
{
|
|
Content = JsonContent.Create(new { status }, options: _writeOptions)
|
|
};
|
|
var r = await _http.SendAsync(request);
|
|
return r.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ SESSIONS ═══
|
|
|
|
public record SessionInfo(Guid Id, Guid TableId, Guid ShopId, int GuestCount, DateTime StartedAt, DateTime? ClosedAt, string Status);
|
|
|
|
public async Task<SessionInfo?> OpenSessionAsync(Guid tableId, Guid shopId, int guestCount = 1)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync("api/bff/sessions",
|
|
new { tableId, shopId, guestCount }, _writeOptions);
|
|
if (resp.IsSuccessStatusCode)
|
|
{
|
|
var json = await resp.Content.ReadFromJsonAsync<System.Text.Json.JsonElement>(_jsonOptions);
|
|
if (json.TryGetProperty("data", out var data))
|
|
{
|
|
var sessionId = data.GetProperty("sessionId").GetGuid();
|
|
return new SessionInfo(sessionId, tableId, shopId, guestCount, DateTime.UtcNow, null, "Open");
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public async Task<SessionInfo?> GetSessionAsync(Guid sessionId)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.GetAsync($"api/bff/sessions/{sessionId}");
|
|
if (resp.IsSuccessStatusCode)
|
|
{
|
|
var json = await resp.Content.ReadFromJsonAsync<System.Text.Json.JsonElement>(_jsonOptions);
|
|
if (json.TryGetProperty("data", out var data))
|
|
return System.Text.Json.JsonSerializer.Deserialize<SessionInfo>(data.GetRawText(), _jsonOptions);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public async Task<bool> CloseSessionAsync(Guid sessionId)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsJsonAsync($"api/bff/sessions/{sessionId}/close", new { }, _writeOptions);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ SHOP PUBLISH (draft → active) ═══
|
|
|
|
public async Task<bool> PublishShopAsync(Guid shopId)
|
|
{
|
|
AttachToken();
|
|
var resp = await _http.PostAsync($"api/bff/shops/{shopId}/publish", null);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
|
|
// ═══ SERVICE HEALTH CHECK ═══
|
|
|
|
public record ServiceHealthInfo(string Name, string Icon, bool IsOnline, int? LatencyMs);
|
|
|
|
private static readonly (string Name, string Icon, string HealthPath)[] _healthEndpoints = new[]
|
|
{
|
|
("IAM Service", "shield", "api/iam/health"),
|
|
("Merchant Service", "store", "api/merchants/health"),
|
|
("Catalog Service", "package", "api/catalog/health"),
|
|
("Order Service", "shopping-bag", "api/orders/health"),
|
|
};
|
|
|
|
public async Task<List<ServiceHealthInfo>> CheckServicesHealthAsync()
|
|
{
|
|
var results = new List<ServiceHealthInfo>();
|
|
var tasks = _healthEndpoints.Select(async svc =>
|
|
{
|
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
try
|
|
{
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
var resp = await _http.GetAsync(svc.HealthPath, cts.Token);
|
|
sw.Stop();
|
|
return new ServiceHealthInfo(svc.Name, svc.Icon, resp.IsSuccessStatusCode, (int)sw.ElapsedMilliseconds);
|
|
}
|
|
catch
|
|
{
|
|
sw.Stop();
|
|
return new ServiceHealthInfo(svc.Name, svc.Icon, false, null);
|
|
}
|
|
}).ToArray();
|
|
|
|
return (await Task.WhenAll(tasks)).ToList();
|
|
}
|
|
}
|