using System.Data; using Dapper; using SIGCM.Application.DTOs; using SIGCM.Domain.Entities; using SIGCM.Domain.Interfaces; using SIGCM.Domain.Models; using SIGCM.Infrastructure.Data; namespace SIGCM.Infrastructure.Repositories; public class ListingRepository : IListingRepository { private readonly IDbConnectionFactory _connectionFactory; public ListingRepository(IDbConnectionFactory connectionFactory) { _connectionFactory = connectionFactory; } public async Task CreateAsync(Listing listing, Dictionary attributes) { using var conn = _connectionFactory.CreateConnection(); conn.Open(); using var transaction = conn.BeginTransaction(); try { var sqlListing = @" INSERT INTO Listings ( CategoryId, OperationId, Title, Description, Price, Currency, CreatedAt, Status, UserId, PrintText, PrintStartDate, PrintDaysCount, IsBold, IsFrame, PrintFontSize, PrintAlignment, AdFee ) VALUES ( @CategoryId, @OperationId, @Title, @Description, @Price, @Currency, @CreatedAt, @Status, @UserId, @PrintText, @PrintStartDate, @PrintDaysCount, @IsBold, @IsFrame, @PrintFontSize, @PrintAlignment, @AdFee ); SELECT CAST(SCOPE_IDENTITY() as int);"; var listingId = await conn.QuerySingleAsync(sqlListing, listing, transaction); if (attributes != null && attributes.Any()) { var sqlAttr = @" INSERT INTO ListingAttributeValues (ListingId, AttributeDefinitionId, Value) VALUES (@ListingId, @AttributeDefinitionId, @Value)"; foreach (var attr in attributes) { await conn.ExecuteAsync(sqlAttr, new { ListingId = listingId, AttributeDefinitionId = attr.Key, Value = attr.Value }, transaction); } } transaction.Commit(); return listingId; } catch { transaction.Rollback(); throw; } } public async Task GetByIdAsync(int id) { using var conn = _connectionFactory.CreateConnection(); return await conn.QuerySingleOrDefaultAsync("SELECT * FROM Listings WHERE Id = @Id", new { Id = id }); } public async Task GetDetailByIdAsync(int id) { using var conn = _connectionFactory.CreateConnection(); // Mejoramos el SQL para asegurar que los nulos se conviertan en 0 (false) desde el motor var sql = @" SELECT l.Id, l.CategoryId, l.OperationId, l.Title, l.Description, l.Price, l.AdFee, l.Currency, l.CreatedAt, l.Status, l.UserId, l.PrintText, l.PrintDaysCount, ISNULL(l.IsBold, 0) as IsBold, ISNULL(l.IsFrame, 0) as IsFrame, l.PrintFontSize, l.PrintAlignment, l.ClientId, c.Name as CategoryName, cl.Name as ClientName, cl.DniOrCuit as ClientDni FROM Listings l LEFT JOIN Categories c ON l.CategoryId = c.Id LEFT JOIN Clients cl ON l.ClientId = cl.Id WHERE l.Id = @Id; SELECT lav.*, ad.Name as AttributeName FROM ListingAttributeValues lav JOIN AttributeDefinitions ad ON lav.AttributeDefinitionId = ad.Id WHERE lav.ListingId = @Id; SELECT * FROM ListingImages WHERE ListingId = @Id ORDER BY DisplayOrder; "; using var multi = await conn.QueryMultipleAsync(sql, new { Id = id }); var listing = await multi.ReadSingleOrDefaultAsync(); if (listing == null) return null; var attributes = await multi.ReadAsync(); var images = await multi.ReadAsync(); return new ListingDetail { Listing = new Listing { Id = (int)listing.Id, Title = listing.Title, Description = listing.Description, Price = listing.Price, AdFee = listing.AdFee, Status = listing.Status, CreatedAt = listing.CreatedAt, PrintText = listing.PrintText, IsBold = Convert.ToBoolean(listing.IsBold), IsFrame = Convert.ToBoolean(listing.IsFrame), PrintDaysCount = listing.PrintDaysCount, CategoryName = listing.CategoryName }, Attributes = attributes, Images = images }; } public async Task> GetAllAsync() { using var conn = _connectionFactory.CreateConnection(); var sql = @" SELECT TOP 20 l.*, (SELECT TOP 1 Url FROM ListingImages li WHERE li.ListingId = l.Id ORDER BY IsMainInfo DESC, DisplayOrder ASC) as MainImageUrl FROM Listings l WHERE l.Status = 'Published' ORDER BY l.CreatedAt DESC"; return await conn.QueryAsync(sql); } public async Task> SearchAsync(string? query, int? categoryId) { return await SearchFacetedAsync(query, categoryId, null); } // Búsqueda Avanzada Facetada public async Task> SearchFacetedAsync(string? query, int? categoryId, Dictionary? attributes) { using var conn = _connectionFactory.CreateConnection(); var parameters = new DynamicParameters(); string sql; // Construcción Dinámica de la Query con CTE if (categoryId.HasValue && categoryId.Value > 0) { sql = @" WITH CategoryTree AS ( SELECT Id FROM Categories WHERE Id = @CategoryId UNION ALL SELECT c.Id FROM Categories c INNER JOIN CategoryTree ct ON c.ParentId = ct.Id ) SELECT l.*, (SELECT TOP 1 Url FROM ListingImages li WHERE li.ListingId = l.Id ORDER BY IsMainInfo DESC, DisplayOrder ASC) as MainImageUrl FROM Listings l WHERE l.Status = 'Published' AND l.CategoryId IN (SELECT Id FROM CategoryTree)"; parameters.Add("CategoryId", categoryId); } else { // Sin filtro de categoría (o todas) sql = @" SELECT l.*, (SELECT TOP 1 Url FROM ListingImages li WHERE li.ListingId = l.Id ORDER BY IsMainInfo DESC, DisplayOrder ASC) as MainImageUrl FROM Listings l WHERE l.Status = 'Published'"; } // Filtro de Texto if (!string.IsNullOrEmpty(query)) { sql += " AND (l.Title LIKE @Query OR l.Description LIKE @Query)"; parameters.Add("Query", $"%{query}%"); } // Filtros de Atributos (Igual que antes) if (attributes != null && attributes.Any()) { int i = 0; foreach (var attr in attributes) { if (string.IsNullOrWhiteSpace(attr.Value)) continue; string paramName = $"@Val{i}"; string paramKey = $"@Key{i}"; sql += $@" AND EXISTS ( SELECT 1 FROM ListingAttributeValues lav JOIN AttributeDefinitions ad ON lav.AttributeDefinitionId = ad.Id WHERE lav.ListingId = l.Id AND ad.Name = {paramKey} AND lav.Value LIKE {paramName} )"; parameters.Add($"Val{i}", $"%{attr.Value}%"); parameters.Add($"Key{i}", attr.Key); i++; } } sql += " ORDER BY l.CreatedAt DESC"; return await conn.QueryAsync(sql, parameters); } public async Task> GetListingsForPrintAsync(DateTime targetDate) { using var conn = _connectionFactory.CreateConnection(); var sql = @" SELECT l.*, c.Name as CategoryName FROM Listings l JOIN Categories c ON l.CategoryId = c.Id WHERE l.PrintStartDate IS NOT NULL AND @TargetDate >= CAST(l.PrintStartDate AS DATE) AND @TargetDate < DATEADD(day, l.PrintDaysCount, CAST(l.PrintStartDate AS DATE)) ORDER BY c.Name, l.Title"; return await conn.QueryAsync(sql, new { TargetDate = targetDate.Date }); } public async Task> GetPendingModerationAsync() { using var conn = _connectionFactory.CreateConnection(); // Avisos que NO están publicados ni rechazados ni borrados. // Asumimos 'Pending' o 'Draft' si vienen del Wizard y requieren revisión. // Para este ejemplo, buscamos 'Pending'. return await conn.QueryAsync("SELECT * FROM Listings WHERE Status = 'Pending' ORDER BY CreatedAt ASC"); } public async Task UpdateStatusAsync(int id, string status) { using var conn = _connectionFactory.CreateConnection(); await conn.ExecuteAsync("UPDATE Listings SET Status = @Status WHERE Id = @Id", new { Id = id, Status = status }); } public async Task CountByCategoryIdAsync(int categoryId) { using var conn = _connectionFactory.CreateConnection(); return await conn.ExecuteScalarAsync("SELECT COUNT(1) FROM Listings WHERE CategoryId = @Id", new { Id = categoryId }); } public async Task MoveListingsAsync(int sourceCategoryId, int targetCategoryId) { using var conn = _connectionFactory.CreateConnection(); // Solo movemos los avisos, no tocamos la estructura de categorías await conn.ExecuteAsync( "UPDATE Listings SET CategoryId = @TargetId WHERE CategoryId = @SourceId", new { SourceId = sourceCategoryId, TargetId = targetCategoryId }); } public async Task> GetSalesByRootCategoryAsync(DateTime startDate, DateTime endDate) { using var conn = _connectionFactory.CreateConnection(); // SQL con CTE Recursiva: // 1. Mapeamos TODA la jerarquía para saber cuál es el ID raíz de cada subcategoría. // 2. Unimos con Listings y sumamos. var sql = @" WITH CategoryHierarchy AS ( -- Caso base: Categorías Raíz (donde ParentId es NULL) SELECT Id, Id as RootId, Name FROM Categories WHERE ParentId IS NULL UNION ALL -- Caso recursivo: Hijos que se vinculan a su padre SELECT c.Id, ch.RootId, ch.Name FROM Categories c INNER JOIN CategoryHierarchy ch ON c.ParentId = ch.Id ) SELECT ch.RootId as CategoryId, ch.Name as CategoryName, SUM(l.Price) as TotalSales, COUNT(l.Id) as AdCount FROM Listings l INNER JOIN CategoryHierarchy ch ON l.CategoryId = ch.Id WHERE l.CreatedAt >= @StartDate AND l.CreatedAt <= @EndDate AND l.Status = 'Published' GROUP BY ch.RootId, ch.Name ORDER BY TotalSales DESC"; return await conn.QueryAsync(sql, new { StartDate = startDate, EndDate = endDate }); } public async Task GetPendingCountAsync() { using var conn = _connectionFactory.CreateConnection(); // Contamos solo avisos en estado 'Pending' return await conn.ExecuteScalarAsync("SELECT COUNT(1) FROM Listings WHERE Status = 'Pending'"); } public async Task GetDashboardStatsAsync(DateTime startDate, DateTime endDate) { using var conn = _connectionFactory.CreateConnection(); var stats = new DashboardStats(); // 1. KPIs del periodo seleccionado var kpisSql = @" SELECT CAST(ISNULL(SUM(AdFee), 0) AS DECIMAL(18,2)) as RevenueToday, COUNT(Id) as AdsToday FROM Listings WHERE CAST(CreatedAt AS DATE) >= @StartDate AND CAST(CreatedAt AS DATE) <= @EndDate AND Status = 'Published'"; var kpis = await conn.QueryFirstOrDefaultAsync(kpisSql, new { StartDate = startDate.Date, EndDate = endDate.Date }); stats.RevenueToday = kpis != null ? (decimal)kpis.RevenueToday : 0; stats.AdsToday = kpis != null ? (int)kpis.AdsToday : 0; stats.TicketAverage = stats.AdsToday > 0 ? stats.RevenueToday / stats.AdsToday : 0; // 2. Ocupación (basada en el último día del rango) stats.PaperOccupation = Math.Min(100, (stats.AdsToday * 100.0) / 100.0); // 3. Tendencia del periodo // Si el rango es mayor a 10 días, agrupamos diferente, pero por ahora mantenemos ddd var trendSql = @" SELECT FORMAT(CreatedAt, 'dd/MM') as Day, SUM(AdFee) as Amount FROM Listings WHERE CAST(CreatedAt AS DATE) >= @StartDate AND CAST(CreatedAt AS DATE) <= @EndDate AND Status = 'Published' GROUP BY FORMAT(CreatedAt, 'dd/MM'), CAST(CreatedAt AS DATE) ORDER BY CAST(CreatedAt AS DATE) ASC"; var trendResult = await conn.QueryAsync(trendSql, new { StartDate = startDate.Date, EndDate = endDate.Date }); stats.WeeklyTrend = trendResult.ToList(); // 4. Mix de Canales del periodo var channelsSql = @" SELECT CASE WHEN UserId IS NULL THEN 'Web' ELSE 'Mostrador' END as Name, COUNT(Id) as Value FROM Listings WHERE CAST(CreatedAt AS DATE) >= @StartDate AND CAST(CreatedAt AS DATE) <= @EndDate AND Status = 'Published' GROUP BY CASE WHEN UserId IS NULL THEN 'Web' ELSE 'Mostrador' END"; var channelResult = await conn.QueryAsync(channelsSql, new { StartDate = startDate.Date, EndDate = endDate.Date }); stats.ChannelMix = channelResult.ToList(); return stats; } public async Task GetCashierStatsAsync(int userId, DateTime startDate, DateTime endDate) { using var conn = _connectionFactory.CreateConnection(); // Filtramos tanto la recaudación como los pendientes por el rango seleccionado var sql = @" SELECT CAST(ISNULL(SUM(AdFee), 0) AS DECIMAL(18,2)) as MyRevenue, COUNT(Id) as MyAdsCount, (SELECT COUNT(1) FROM Listings WHERE UserId = @UserId AND Status = 'Pending' AND CAST(CreatedAt AS DATE) BETWEEN @Start AND @End) as MyPendingAds FROM Listings WHERE UserId = @UserId AND CAST(CreatedAt AS DATE) BETWEEN @Start AND @End AND Status = 'Published'"; return await conn.QueryFirstOrDefaultAsync(sql, new { UserId = userId, Start = startDate.Date, End = endDate.Date }); } public async Task GetDetailedReportAsync(DateTime start, DateTime end, int? userId = null) { using var conn = _connectionFactory.CreateConnection(); var report = new GlobalReportDto { FromDate = start, ToDate = end }; // Filtro inteligente: Si @UserId es NULL, devuelve todo. Si no, filtra por ese usuario. var sql = @" SELECT l.Id, l.CreatedAt as Date, l.Title, c.Name as Category, u.Username as Cashier, l.AdFee as Amount FROM Listings l JOIN Categories c ON l.CategoryId = c.Id LEFT JOIN Users u ON l.UserId = u.Id WHERE CAST(l.CreatedAt AS DATE) BETWEEN @Start AND @End AND l.Status = 'Published' AND (@UserId IS NULL OR l.UserId = @UserId) -- <--- FILTRO DINÁMICO ORDER BY l.CreatedAt ASC"; var items = await conn.QueryAsync(sql, new { Start = start.Date, End = end.Date, UserId = userId // Dapper pasará null si el parámetro es null }); report.Items = items.ToList(); report.TotalRevenue = report.Items.Sum(x => x.Amount); report.TotalAds = report.Items.Count; return report; } }