using Microsoft.AspNetCore.Mvc; using Dapper; using WebClientTpos.Server.Infrastructure; using WebClientTpos.Server.Models; namespace WebClientTpos.Server.Controllers; /// /// EN: Shop management controller — CRUD for shops, settings, stats, and devices. /// All endpoints are scoped to the current merchant's shops. /// VI: Controller quản lý cửa hàng — CRUD cho shops, settings, stats và devices. /// Tất cả endpoints đều lọc theo shops của merchant hiện tại. /// [ApiController] [Route("api/bff")] public class ShopController : ControllerBase { private readonly TenantContext _tenant; private readonly BffDbConnectionFactory _dbFactory; public ShopController(TenantContext tenant, BffDbConnectionFactory dbFactory) { _tenant = tenant; _dbFactory = dbFactory; } /// /// EN: Get all shops belonging to the current merchant. /// VI: Lấy tất cả cửa hàng thuộc merchant hiện tại. /// [HttpGet("shops")] public async Task GetShops() { var merchantId = await _tenant.GetMerchantIdAsync(); if (merchantId == null) return Ok(Array.Empty()); await using var conn = _dbFactory.CreateConnection("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.merchant_id = @MerchantId AND s.is_deleted = false ORDER BY s.name", new { MerchantId = merchantId }); return Ok(shops); } /// /// EN: Get shop by ID — validates merchant ownership. /// VI: Lấy cửa hàng theo ID — kiểm tra quyền sở hữu merchant. /// [HttpGet("shops/{shopId:guid}")] public async Task GetShopById(Guid shopId) { var merchantId = await _tenant.GetMerchantIdAsync(); if (merchantId == null) return NotFound(new { message = "Shop not found" }); await using var conn = _dbFactory.CreateConnection("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.merchant_id = @MerchantId AND s.is_deleted = false", new { ShopId = shopId, MerchantId = merchantId }); if (shop == null) return NotFound(new { message = "Shop not found" }); return Ok(shop); } /// /// EN: Update shop info — validates ownership. /// VI: Cập nhật thông tin cửa hàng — kiểm tra quyền sở hữu. /// [HttpPut("shops/{shopId:guid}")] public async Task UpdateShop(Guid shopId, [FromBody] UpdateShopRequest req) { var merchantId = await _tenant.GetMerchantIdAsync(); if (merchantId == null) return Unauthorized(); var myShopIds = await _tenant.GetShopIdsAsync(); if (!myShopIds.Contains(shopId)) return Forbid(); await using var conn = _dbFactory.CreateConnection("merchant_service"); var rows = await conn.ExecuteAsync( @"UPDATE shops SET name=@Name, phone=@Phone, email=@Email, description=@Description, open_time=@OpenTime, close_time=@CloseTime, updated_at=NOW() WHERE id=@ShopId AND id = ANY(@ShopIds)", new { req.Name, req.Phone, req.Email, req.Description, req.OpenTime, req.CloseTime, ShopId = shopId, ShopIds = myShopIds.ToArray() }); return rows > 0 ? Ok(new { id = shopId }) : NotFound(); } /// /// EN: Get shop settings (features_config, open/close time, open days). /// VI: Lấy cài đặt cửa hàng (features_config, giờ mở/đóng cửa, ngày mở cửa). /// [HttpGet("shops/{shopId:guid}/settings")] public async Task GetShopSettings(Guid shopId) { var merchantId = await _tenant.GetMerchantIdAsync(); if (merchantId == null) return NotFound(); await using var conn = _dbFactory.CreateConnection("merchant_service"); var settings = await conn.QueryFirstOrDefaultAsync( @"SELECT features_config::text as features_config, open_time::text as open_time, close_time::text as close_time, open_days FROM shops WHERE id = @ShopId AND merchant_id = @MerchantId AND is_deleted = false", new { ShopId = shopId, MerchantId = merchantId }); if (settings == null) return NotFound(); return Ok(settings); } /// /// EN: Update shop settings — validates ownership. /// VI: Cập nhật cài đặt cửa hàng — kiểm tra quyền sở hữu. /// [HttpPut("shops/{shopId:guid}/settings")] public async Task UpdateShopSettings(Guid shopId, [FromBody] UpdateShopSettingsRequest req) { var merchantId = await _tenant.GetMerchantIdAsync(); if (merchantId == null) return Unauthorized(); var myShopIds = await _tenant.GetShopIdsAsync(); if (!myShopIds.Contains(shopId)) return Forbid(); await using var conn = _dbFactory.CreateConnection("merchant_service"); try { await conn.ExecuteAsync( @"UPDATE shops SET features_config = @FeaturesConfig::jsonb, open_time = @OpenTime::time, close_time = @CloseTime::time, open_days = @OpenDays, updated_at = NOW() WHERE id = @ShopId AND id = ANY(@ShopIds)", new { ShopId = shopId, ShopIds = myShopIds.ToArray(), FeaturesConfig = string.IsNullOrWhiteSpace(req.FeaturesConfig) ? "{}" : req.FeaturesConfig, OpenTime = string.IsNullOrWhiteSpace(req.OpenTime) ? (object)DBNull.Value : req.OpenTime, CloseTime= string.IsNullOrWhiteSpace(req.CloseTime) ? (object)DBNull.Value : req.CloseTime, OpenDays = req.OpenDays }); return Ok(new { success = true }); } catch (Exception ex) { return BadRequest(new { error = ex.Message }); } } /// /// EN: Get aggregated stats per shop — scoped to current merchant. /// VI: Lấy thống kê tổng hợp theo shop — lọc theo merchant hiện tại. /// [HttpGet("shops/stats")] public async Task GetShopStats() { var merchantId = await _tenant.GetMerchantIdAsync(); if (merchantId == null) return Ok(Array.Empty()); var myShopIds = await _tenant.GetShopIdsAsync(); if (!myShopIds.Any()) return Ok(Array.Empty()); var shopIdsArray = myShopIds.ToArray(); // EN: Products per shop (scoped) / VI: Số sản phẩm mỗi shop Dictionary productCounts = new(); try { await using var catConn = _dbFactory.CreateConnection("catalog_service"); var prodStats = await catConn.QueryAsync( "SELECT shop_id, COUNT(*) as cnt FROM products WHERE is_active = true AND shop_id = ANY(@ShopIds) GROUP BY shop_id", new { ShopIds = shopIdsArray }); foreach (var ps in prodStats) productCounts[(Guid)ps.shop_id] = (int)(long)ps.cnt; } catch { /* catalog_service may not have data yet */ } // EN: Orders per shop + revenue (scoped) / VI: Số đơn + doanh thu mỗi shop Dictionary orderCounts = new(); Dictionary revenues = new(); try { await using var orderConn = _dbFactory.CreateConnection("order_service"); var orderStats = await orderConn.QueryAsync( "SELECT shop_id, COUNT(*) as cnt, COALESCE(SUM(total_amount), 0) as revenue FROM orders WHERE shop_id = ANY(@ShopIds) GROUP BY shop_id", new { ShopIds = shopIdsArray }); foreach (var os in orderStats) { orderCounts[(Guid)os.shop_id] = (int)(long)os.cnt; revenues[(Guid)os.shop_id] = (decimal)os.revenue; } } catch { /* order_service may not have data yet */ } // EN: Staff per shop (scoped) / VI: Số nhân viên mỗi shop Dictionary staffCounts = new(); try { await using var mConn = _dbFactory.CreateConnection("merchant_service"); var staffStats = await mConn.QueryAsync( @"SELECT sm.shop_id, COUNT(DISTINCT sm.staff_id) as cnt FROM shop_members sm JOIN merchant_staff ms ON sm.staff_id = ms.id JOIN staff_statuses ss ON ms.status_id = ss.id WHERE ss.name = 'Active' AND sm.shop_id = ANY(@ShopIds) GROUP BY sm.shop_id", new { ShopIds = shopIdsArray }); foreach (var ss in staffStats) staffCounts[(Guid)ss.shop_id] = (int)(long)ss.cnt; } catch { /* merchant_service may not have data yet */ } var result = myShopIds.Select(shopId => new { shop_id = shopId, product_count = productCounts.GetValueOrDefault(shopId, 0), order_count = orderCounts.GetValueOrDefault(shopId, 0), staff_count = staffCounts.GetValueOrDefault(shopId, 0), revenue = revenues.GetValueOrDefault(shopId, 0m) }); return Ok(result); } /// /// EN: Get device tokens registered for this merchant's staff. /// VI: Lấy danh sách device token đã đăng ký cho nhân viên của merchant. /// [HttpGet("devices")] public async Task GetDevices() { await using var conn = _dbFactory.CreateConnection("merchant_service"); var devices = await conn.QueryAsync( @"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); } }