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 <hongochai10@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-02-26 20:15:08 +00:00
parent d586563c60
commit 26e13fc38f
3 changed files with 117 additions and 0 deletions

View File

@@ -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<IActionResult> GetShops()
{
await using var conn = new NpgsqlConnection(ConnStr("merchant_service"));
var shops = await conn.QueryAsync<dynamic>(
@"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<IActionResult> GetProducts(Guid shopId)
{
await using var conn = new NpgsqlConnection(ConnStr("catalog_service"));
var products = await conn.QueryAsync<dynamic>(
@"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<IActionResult> GetCategories(Guid shopId)
{
await using var conn = new NpgsqlConnection(ConnStr("catalog_service"));
var categories = await conn.QueryAsync<dynamic>(
@"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<IActionResult> GetTables(Guid shopId)
{
await using var conn = new NpgsqlConnection(ConnStr("fnb_engine"));
var tables = await conn.QueryAsync<dynamic>(
@"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<IActionResult> GetAppointments(Guid shopId)
{
await using var conn = new NpgsqlConnection(ConnStr("booking_service"));
var appointments = await conn.QueryAsync<dynamic>(
@"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<IActionResult> GetResources(Guid shopId)
{
await using var conn = new NpgsqlConnection(ConnStr("booking_service"));
var resources = await conn.QueryAsync<dynamic>(
@"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);
}
}

View File

@@ -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");

View File

@@ -7,8 +7,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1" />
<PackageReference Include="Npgsql" Version="9.0.3" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
</ItemGroup>