From 26e13fc38ff2f767b9af570f33da46e7b9717f4a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 26 Feb 2026 20:15:08 +0000 Subject: [PATCH] feat(tpos): add BFF data endpoints with Npgsql + Dapper - Add Npgsql 9.0.3 and Dapper 2.1.66 packages to Server project - Create BffDataController with read-only endpoints: GET /api/bff/shops GET /api/bff/shops/{shopId}/products GET /api/bff/shops/{shopId}/categories GET /api/bff/shops/{shopId}/tables GET /api/bff/shops/{shopId}/appointments GET /api/bff/shops/{shopId}/resources - Register MVC controllers in Program.cs (AddControllers + MapControllers) Co-authored-by: Velik --- .../Controllers/BffDataController.cs | 107 ++++++++++++++++++ .../src/WebClientTpos.Server/Program.cs | 8 ++ .../WebClientTpos.Server.csproj | 2 + 3 files changed, 117 insertions(+) create mode 100644 apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs new file mode 100644 index 00000000..b4f93124 --- /dev/null +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs @@ -0,0 +1,107 @@ +using Microsoft.AspNetCore.Mvc; +using Npgsql; +using Dapper; + +namespace WebClientTpos.Server.Controllers; + +[ApiController] +[Route("api/bff")] +public class BffDataController : ControllerBase +{ + private static string ConnStr(string db) => + $"Host=localhost;Port=5432;Database={db};Username=goodgo;Password=goodgo_dev_2024"; + + [HttpGet("shops")] + public async Task GetShops() + { + await using var conn = new NpgsqlConnection(ConnStr("merchant_service")); + var shops = await conn.QueryAsync( + @"SELECT s.id, s.name, s.slug, s.description, s.phone, s.email, + s.open_time, s.close_time, s.features_config, + bc.name as category, st.name as status + FROM shops s + JOIN business_categories bc ON s.category_id = bc.id + JOIN shop_statuses st ON s.status_id = st.id + WHERE s.is_deleted = false + ORDER BY s.name"); + return Ok(shops); + } + + [HttpGet("shops/{shopId}/products")] + public async Task GetProducts(Guid shopId) + { + await using var conn = new NpgsqlConnection(ConnStr("catalog_service")); + var products = await conn.QueryAsync( + @"SELECT id, name, price, sku, description, image_url, is_active, + attributes->>'category' as category, + (attributes->>'duration')::int as duration_minutes + FROM products + WHERE shop_id = @ShopId AND is_active = true + ORDER BY name", + new { ShopId = shopId }); + return Ok(products); + } + + [HttpGet("shops/{shopId}/categories")] + public async Task GetCategories(Guid shopId) + { + await using var conn = new NpgsqlConnection(ConnStr("catalog_service")); + var categories = await conn.QueryAsync( + @"SELECT id, name, description, display_order + FROM categories + WHERE shop_id = @ShopId AND is_active = true + ORDER BY display_order", + new { ShopId = shopId }); + return Ok(categories); + } + + [HttpGet("shops/{shopId}/tables")] + public async Task GetTables(Guid shopId) + { + await using var conn = new NpgsqlConnection(ConnStr("fnb_engine")); + var tables = await conn.QueryAsync( + @"SELECT t.id, t.table_number, t.capacity, t.zone, + CASE t.status_id + WHEN 1 THEN 'available' + WHEN 2 THEN 'occupied' + WHEN 3 THEN 'reserved' + WHEN 4 THEN 'cleaning' + END as status, + s.id as session_id, s.guest_count, s.started_at + FROM tables t + LEFT JOIN sessions s ON s.table_id = t.id AND s.status = 'Active' + WHERE t.shop_id = @ShopId + ORDER BY t.table_number", + new { ShopId = shopId }); + return Ok(tables); + } + + [HttpGet("shops/{shopId}/appointments")] + public async Task GetAppointments(Guid shopId) + { + await using var conn = new NpgsqlConnection(ConnStr("booking_service")); + var appointments = await conn.QueryAsync( + @"SELECT a.id, a.customer_id, a.staff_id, a.resource_id, + a.service_id, a.start_time, a.end_time, a.status, + r.name as resource_name + FROM appointments a + LEFT JOIN resources r ON a.resource_id = r.id + WHERE a.shop_id = @ShopId + ORDER BY a.start_time", + new { ShopId = shopId }); + return Ok(appointments); + } + + [HttpGet("shops/{shopId}/resources")] + public async Task GetResources(Guid shopId) + { + await using var conn = new NpgsqlConnection(ConnStr("booking_service")); + var resources = await conn.QueryAsync( + @"SELECT id, name, resource_type, capacity, is_active + FROM resources + WHERE shop_id = @ShopId AND is_active = true + ORDER BY name", + new { ShopId = shopId }); + return Ok(resources); + } +} diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs b/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs index c4d43548..0e4ffd2d 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/Program.cs @@ -41,6 +41,10 @@ builder.Services.AddCors(options => // VI: Thêm health checks builder.Services.AddHealthChecks(); +// EN: Add MVC controllers for BFF data endpoints +// VI: Thêm MVC controllers cho BFF data endpoints +builder.Services.AddControllers(); + var app = builder.Build(); // ═══════════════════════════════════════════════════════════════════════════════ @@ -116,6 +120,10 @@ app.Map("{culture:regex(^(en-US|vi-VN)$)}/{**slug}", async (string culture, Http return Results.Content(modifiedHtml, "text/html"); }); +// EN: Map BFF API controllers +// VI: Map BFF API controllers +app.MapControllers(); + // EN: Fallback to index.html for SPA routing (default culture) // VI: Fallback đến index.html cho SPA routing (ngôn ngữ mặc định) app.MapFallbackToFile("index.html"); diff --git a/apps/web-client-tpos-net/src/WebClientTpos.Server/WebClientTpos.Server.csproj b/apps/web-client-tpos-net/src/WebClientTpos.Server/WebClientTpos.Server.csproj index 75077d36..d89c0221 100644 --- a/apps/web-client-tpos-net/src/WebClientTpos.Server/WebClientTpos.Server.csproj +++ b/apps/web-client-tpos-net/src/WebClientTpos.Server/WebClientTpos.Server.csproj @@ -7,8 +7,10 @@ + +