Files
pos-system/apps/web-client-tpos-net/src/WebClientTpos.Server/Controllers/BffDataController.cs

234 lines
9.8 KiB
C#

using Microsoft.AspNetCore.Mvc;
using Npgsql;
using Dapper;
namespace WebClientTpos.Server.Controllers;
[ApiController]
[Route("api/bff")]
public class BffDataController : ControllerBase
{
// EN: DB host configurable via env var (Docker: "postgres", dev: "localhost")
// VI: DB host cấu hình qua env var (Docker: "postgres", dev: "localhost")
private static readonly string _dbHost = Environment.GetEnvironmentVariable("BFF_DB_HOST") ?? "localhost";
private static readonly string _dbPort = Environment.GetEnvironmentVariable("BFF_DB_PORT") ?? "5432";
private static readonly string _dbUser = Environment.GetEnvironmentVariable("BFF_DB_USER") ?? "goodgo";
private static readonly string _dbPass = Environment.GetEnvironmentVariable("BFF_DB_PASS") ?? "goodgo_dev_2024";
private static string ConnStr(string db) =>
$"Host={_dbHost};Port={_dbPort};Database={db};Username={_dbUser};Password={_dbPass}";
[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:guid}")]
public async Task<IActionResult> GetShopById(Guid shopId)
{
await using var conn = new NpgsqlConnection(ConnStr("merchant_service"));
var shop = await conn.QueryFirstOrDefaultAsync<dynamic>(
@"SELECT s.id, s.name, s.slug, s.description, s.phone, s.email,
s.open_time, s.close_time,
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.id = @ShopId AND s.is_deleted = false",
new { ShopId = shopId });
if (shop == null)
return NotFound(new { message = "Shop not found" });
return Ok(shop);
}
[HttpGet("staff")]
public async Task<IActionResult> GetStaff()
{
await using var conn = new NpgsqlConnection(ConnStr("merchant_service"));
var staff = await conn.QueryAsync<dynamic>(
@"SELECT ms.id, ms.user_id, ms.employee_code, ms.phone, ms.email,
ms.joined_at, ms.terminated_at,
sr.name as role, ss.name as status,
s.name as shop_name
FROM merchant_staff ms
JOIN staff_roles sr ON ms.role_id = sr.id
JOIN staff_statuses ss ON ms.status_id = ss.id
LEFT JOIN shop_members sm ON sm.staff_id = ms.id
LEFT JOIN shops s ON sm.shop_id = s.id
ORDER BY ms.joined_at DESC");
return Ok(staff);
}
[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);
}
// ═══ ADMIN-LEVEL PRODUCT ENDPOINTS ═══
/// <summary>
/// EN: Get all products across all shops (admin level).
/// VI: Lấy tất cả sản phẩm trên tất cả cửa hàng (cấp admin).
/// </summary>
[HttpGet("products")]
public async Task<IActionResult> GetAllProducts([FromQuery] Guid? shopId = null)
{
await using var conn = new NpgsqlConnection(ConnStr("catalog_service"));
var sql = @"SELECT p.id, p.name, p.price, p.sku, p.description, p.image_url,
p.is_active, pt.name as type, p.shop_id, p.created_at,
'' as category_name
FROM products p
JOIN product_types pt ON p.type_id = pt.id";
if (shopId.HasValue)
sql += " WHERE p.shop_id = @ShopId";
sql += " ORDER BY p.name";
var products = await conn.QueryAsync<dynamic>(sql, new { ShopId = shopId });
return Ok(products);
}
/// <summary>
/// EN: Get all categories across all shops (admin level).
/// VI: Lấy tất cả danh mục trên tất cả cửa hàng (cấp admin).
/// </summary>
[HttpGet("categories")]
public async Task<IActionResult> GetAllCategories([FromQuery] Guid? shopId = null)
{
await using var conn = new NpgsqlConnection(ConnStr("catalog_service"));
var sql = @"SELECT id, name, description, display_order, shop_id, parent_id, is_active
FROM categories WHERE is_active = true";
if (shopId.HasValue)
sql += " AND shop_id = @ShopId";
sql += " ORDER BY display_order, name";
var categories = await conn.QueryAsync<dynamic>(sql, new { ShopId = shopId });
return Ok(categories);
}
/// <summary>
/// EN: Create a product via BFF (writes directly to catalog DB).
/// VI: Tạo sản phẩm qua BFF (ghi trực tiếp vào catalog DB).
/// </summary>
[HttpPost("products")]
public async Task<IActionResult> CreateProduct([FromBody] CreateProductRequest req)
{
var id = Guid.NewGuid();
// EN: Map type string to type_id / VI: Chuyển type string sang type_id
var typeId = (req.Type ?? "PreparedFood") switch
{
"Physical" => 1,
"Service" => 2,
"PreparedFood" => 3,
_ => 3
};
await using var conn = new NpgsqlConnection(ConnStr("catalog_service"));
await conn.ExecuteAsync(
@"INSERT INTO products (id, shop_id, name, description, price, type_id, sku, image_url, is_active, created_at)
VALUES (@Id, @ShopId, @Name, @Description, @Price, @TypeId, @Sku, @ImageUrl, true, NOW())",
new { Id = id, req.ShopId, req.Name, req.Description, req.Price, TypeId = typeId, req.Sku, req.ImageUrl });
return CreatedAtAction(nameof(GetAllProducts), new { }, new { id });
}
/// <summary>
/// EN: Delete (deactivate) a product.
/// VI: Xóa (vô hiệu hóa) sản phẩm.
/// </summary>
[HttpDelete("products/{productId:guid}")]
public async Task<IActionResult> DeleteProduct(Guid productId)
{
await using var conn = new NpgsqlConnection(ConnStr("catalog_service"));
await conn.ExecuteAsync(
"UPDATE products SET is_active = false WHERE id = @Id",
new { Id = productId });
return NoContent();
}
// 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);
}