feat(web-client-tpos): connect all remaining admin pages to real backend APIs

- BFF: Added 10 new endpoints (staff roles/schedules, orders, wallets, devices, promotions, inventory transactions, membership levels)
- PosDataService: Added 14 new client methods with DTOs
- Rewrote 19 admin pages from hardcoded to real API:
  Staff: Create, Schedule, Attendance, Payroll
  Finance: Overview, Revenue, Expenses, Tax
  Inventory: PurchaseOrders, StockTransfer, SupplierMgmt
  Product: MenuBuilder, ModifierGroups, PricingRules
  Customer: Feedback, LoyaltyProgram
  System: DeviceManagement, NotificationCenter, IntegrationHub
This commit is contained in:
Ho Ngoc Hai
2026-02-28 06:05:50 +07:00
parent e0d7567cf0
commit 545bc1f519
21 changed files with 1039 additions and 2296 deletions

View File

@@ -307,6 +307,136 @@ public class BffDataController : ControllerBase
return CreatedAtAction(nameof(GetStaff), new { }, new { id });
}
// ═══ STAFF ROLES ═══
[HttpGet("staff/roles")]
public async Task<IActionResult> GetStaffRoles()
{
await using var conn = new NpgsqlConnection(ConnStr("merchant_service"));
var roles = await conn.QueryAsync<dynamic>("SELECT id, name FROM staff_roles ORDER BY id");
return Ok(roles);
}
// ═══ STAFF SCHEDULES ═══
[HttpGet("staff/schedules")]
public async Task<IActionResult> GetStaffSchedules([FromQuery] Guid? shopId = null)
{
await using var conn = new NpgsqlConnection(ConnStr("booking_service"));
var sql = @"SELECT id, staff_id, shop_id, day_of_week, start_time, end_time FROM staff_schedules";
if (shopId.HasValue) sql += " WHERE shop_id = @ShopId";
sql += " ORDER BY day_of_week, start_time";
var schedules = await conn.QueryAsync<dynamic>(sql, new { ShopId = shopId });
// EN: Enrich with staff names / VI: Bổ sung tên nhân viên
await using var mConn = new NpgsqlConnection(ConnStr("merchant_service"));
var staffList = (await mConn.QueryAsync<dynamic>(
"SELECT ms.id, ms.employee_code, ms.phone, sr.name as role FROM merchant_staff ms JOIN staff_roles sr ON ms.role_id = sr.id")).ToList();
var staffMap = staffList.ToDictionary(s => (Guid)s.id, s => new { code = (string?)s.employee_code, role = (string)s.role, phone = (string?)s.phone });
var result = schedules.Select(s => new {
s.id, s.staff_id, s.shop_id, s.day_of_week, s.start_time, s.end_time,
employee_code = staffMap.TryGetValue((Guid)s.staff_id, out var info) ? info.code : null,
role = info?.role, phone = info?.phone
});
return Ok(result);
}
// ═══ ORDERS SUMMARY ═══
[HttpGet("orders")]
public async Task<IActionResult> GetOrders([FromQuery] Guid? shopId = null)
{
await using var conn = new NpgsqlConnection(ConnStr("order_service"));
var sql = @"SELECT o.id, o.shop_id, o.total_amount, o.status_id, o.created_at,
os.name as status
FROM orders o
JOIN order_statuses os ON o.status_id = os.id";
if (shopId.HasValue) sql += " WHERE o.shop_id = @ShopId";
sql += " ORDER BY o.created_at DESC LIMIT 200";
var orders = await conn.QueryAsync<dynamic>(sql, new { ShopId = shopId });
return Ok(orders);
}
// ═══ WALLET/FINANCE ═══
[HttpGet("wallets")]
public async Task<IActionResult> GetWallets()
{
await using var conn = new NpgsqlConnection(ConnStr("wallet_service"));
var wallets = await conn.QueryAsync<dynamic>(
@"SELECT w.id, w.balance, w.currency, w.owner_id, w.created_at,
(SELECT COALESCE(SUM(amount),0) FROM wallet_transactions wt WHERE wt.wallet_id = w.id AND wt.amount > 0) as total_income,
(SELECT COALESCE(SUM(ABS(amount)),0) FROM wallet_transactions wt WHERE wt.wallet_id = w.id AND wt.amount < 0) as total_expense
FROM wallets w ORDER BY w.created_at DESC");
return Ok(wallets);
}
[HttpGet("wallet/transactions")]
public async Task<IActionResult> GetWalletTransactions([FromQuery] int limit = 50)
{
await using var conn = new NpgsqlConnection(ConnStr("wallet_service"));
var txns = await conn.QueryAsync<dynamic>(
@"SELECT wt.id, wt.wallet_id, wt.amount, wt.description, wt.created_at,
wi.name as item_name
FROM wallet_transactions wt
LEFT JOIN wallet_items wi ON wt.reference_id = wi.id
ORDER BY wt.created_at DESC LIMIT @Limit",
new { Limit = limit });
return Ok(txns);
}
// ═══ DEVICES ═══
[HttpGet("devices")]
public async Task<IActionResult> GetDevices()
{
await using var conn = new NpgsqlConnection(ConnStr("merchant_service"));
var devices = await conn.QueryAsync<dynamic>(
@"SELECT dt.id, dt.device_token, dt.platform, dt.is_active, dt.created_at,
ms.employee_code as staff_code
FROM device_tokens dt
LEFT JOIN merchant_staff ms ON dt.staff_id = ms.id
ORDER BY dt.created_at DESC");
return Ok(devices);
}
// ═══ PROMOTIONS ═══
[HttpGet("promotions")]
public async Task<IActionResult> GetPromotions()
{
await using var conn = new NpgsqlConnection(ConnStr("promotion_service"));
var promos = await conn.QueryAsync<dynamic>(
@"SELECT c.id, c.name, c.description, c.start_date, c.end_date, c.is_active, c.discount_type, c.discount_value,
(SELECT COUNT(*) FROM vouchers v WHERE v.campaign_id = c.id) as voucher_count,
(SELECT COUNT(*) FROM redemptions r WHERE r.campaign_id = c.id) as redemption_count
FROM campaigns c ORDER BY c.created_at DESC");
return Ok(promos);
}
// ═══ INVENTORY TRANSACTIONS ═══
[HttpGet("inventory/transactions")]
public async Task<IActionResult> GetInventoryTransactions([FromQuery] Guid? shopId = null)
{
await using var conn = new NpgsqlConnection(ConnStr("inventory_service"));
var sql = @"SELECT it.id, it.inventory_item_id, it.quantity_change, it.reason, it.created_at,
tt.name as transaction_type
FROM inventory_transactions it
JOIN transaction_types tt ON it.type_id = tt.id";
if (shopId.HasValue)
sql += @" JOIN inventory_items ii ON it.inventory_item_id = ii.id WHERE ii.shop_id = @ShopId";
sql += " ORDER BY it.created_at DESC LIMIT 100";
var txns = await conn.QueryAsync<dynamic>(sql, new { ShopId = shopId });
return Ok(txns);
}
// ═══ MEMBERSHIP LEVELS ═══
[HttpGet("membership/levels")]
public async Task<IActionResult> GetMembershipLevels()
{
await using var conn = new NpgsqlConnection(ConnStr("membership_service"));
var levels = await conn.QueryAsync<dynamic>(
@"SELECT ld.id, ld.level, ld.name, ld.min_exp, ld.max_exp,
(SELECT COUNT(*) FROM members m WHERE m.current_level = ld.level) as member_count
FROM level_definitions ld ORDER BY ld.level");
return Ok(levels);
}
// EN: Request DTOs / VI: DTO yêu cầu
public record CreateProductRequest(Guid ShopId, string Name, string? Description, decimal Price, string? Type, string? Sku, string? ImageUrl);
public record CreateStaffRequest(Guid MerchantId, string? EmployeeCode, string? Phone, string? Email, string? Role);