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 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:guid}")] public async Task GetShopById(Guid shopId) { await using var conn = new NpgsqlConnection(ConnStr("merchant_service")); var shop = await conn.QueryFirstOrDefaultAsync( @"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 GetStaff() { await using var conn = new NpgsqlConnection(ConnStr("merchant_service")); var staff = await conn.QueryAsync( @"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 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); } // ═══ ADMIN-LEVEL PRODUCT ENDPOINTS ═══ /// /// 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). /// [HttpGet("products")] public async Task 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(sql, new { ShopId = shopId }); return Ok(products); } /// /// 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). /// [HttpGet("categories")] public async Task 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(sql, new { ShopId = shopId }); return Ok(categories); } /// /// 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). /// [HttpPost("products")] public async Task 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 }); } /// /// EN: Delete (deactivate) a product. /// VI: Xóa (vô hiệu hóa) sản phẩm. /// [HttpDelete("products/{productId:guid}")] public async Task 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); }