Feat: Cambios Varios
This commit is contained in:
@@ -3,6 +3,7 @@ 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;
|
||||
@@ -28,12 +29,12 @@ public class ListingRepository : IListingRepository
|
||||
INSERT INTO Listings (
|
||||
CategoryId, OperationId, Title, Description, Price, Currency,
|
||||
CreatedAt, Status, UserId, PrintText, PrintStartDate, PrintDaysCount,
|
||||
IsBold, IsFrame, PrintFontSize, PrintAlignment
|
||||
IsBold, IsFrame, PrintFontSize, PrintAlignment, AdFee
|
||||
)
|
||||
VALUES (
|
||||
@CategoryId, @OperationId, @Title, @Description, @Price, @Currency,
|
||||
@CreatedAt, @Status, @UserId, @PrintText, @PrintStartDate, @PrintDaysCount,
|
||||
@IsBold, @IsFrame, @PrintFontSize, @PrintAlignment
|
||||
@IsBold, @IsFrame, @PrintFontSize, @PrintAlignment, @AdFee
|
||||
);
|
||||
SELECT CAST(SCOPE_IDENTITY() as int);";
|
||||
|
||||
@@ -67,30 +68,56 @@ public class ListingRepository : IListingRepository
|
||||
return await conn.QuerySingleOrDefaultAsync<Listing>("SELECT * FROM Listings WHERE Id = @Id", new { Id = id });
|
||||
}
|
||||
|
||||
public async Task<SIGCM.Domain.Models.ListingDetail?> GetDetailByIdAsync(int id)
|
||||
public async Task<ListingDetail?> 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 * FROM Listings WHERE Id = @Id;
|
||||
|
||||
SELECT lav.*, ad.Name as AttributeName
|
||||
FROM ListingAttributeValues lav
|
||||
JOIN AttributeDefinitions ad ON lav.AttributeDefinitionId = ad.Id
|
||||
WHERE lav.ListingId = @Id;
|
||||
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;
|
||||
";
|
||||
SELECT * FROM ListingImages WHERE ListingId = @Id ORDER BY DisplayOrder;
|
||||
";
|
||||
|
||||
using var multi = await conn.QueryMultipleAsync(sql, new { Id = id });
|
||||
var listing = await multi.ReadSingleOrDefaultAsync<Listing>();
|
||||
var listing = await multi.ReadSingleOrDefaultAsync<dynamic>();
|
||||
if (listing == null) return null;
|
||||
|
||||
var attributes = await multi.ReadAsync<SIGCM.Domain.Models.ListingAttributeValueWithName>();
|
||||
var attributes = await multi.ReadAsync<ListingAttributeValueWithName>();
|
||||
var images = await multi.ReadAsync<ListingImage>();
|
||||
|
||||
return new SIGCM.Domain.Models.ListingDetail
|
||||
return new ListingDetail
|
||||
{
|
||||
Listing = listing,
|
||||
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
|
||||
};
|
||||
@@ -99,37 +126,86 @@ public class ListingRepository : IListingRepository
|
||||
public async Task<IEnumerable<Listing>> GetAllAsync()
|
||||
{
|
||||
using var conn = _connectionFactory.CreateConnection();
|
||||
// Subquery para obtener la imagen principal
|
||||
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<Listing>(sql);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Listing>> SearchAsync(string? query, int? categoryId)
|
||||
{
|
||||
return await SearchFacetedAsync(query, categoryId, null);
|
||||
}
|
||||
|
||||
// Búsqueda Avanzada Facetada
|
||||
public async Task<IEnumerable<Listing>> SearchFacetedAsync(string? query, int? categoryId, Dictionary<string, string>? attributes)
|
||||
{
|
||||
using var conn = _connectionFactory.CreateConnection();
|
||||
var 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 1=1";
|
||||
|
||||
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}%");
|
||||
}
|
||||
|
||||
if (categoryId.HasValue)
|
||||
// Filtros de Atributos (Igual que antes)
|
||||
if (attributes != null && attributes.Any())
|
||||
{
|
||||
sql += " AND l.CategoryId = @CategoryId";
|
||||
parameters.Add("CategoryId", categoryId);
|
||||
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";
|
||||
@@ -140,8 +216,6 @@ public class ListingRepository : IListingRepository
|
||||
public async Task<IEnumerable<Listing>> GetListingsForPrintAsync(DateTime targetDate)
|
||||
{
|
||||
using var conn = _connectionFactory.CreateConnection();
|
||||
// La lógica: El aviso debe haber empezado antes o en la fecha target
|
||||
// Y la fecha target debe ser menor a la fecha de inicio + duración
|
||||
var sql = @"
|
||||
SELECT l.*, c.Name as CategoryName
|
||||
FROM Listings l
|
||||
@@ -149,8 +223,190 @@ public class ListingRepository : IListingRepository
|
||||
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"; // Ordenado por Rubro y luego alfabético
|
||||
ORDER BY c.Name, l.Title";
|
||||
|
||||
return await conn.QueryAsync<Listing>(sql, new { TargetDate = targetDate.Date });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Listing>> 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<Listing>("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<int> CountByCategoryIdAsync(int categoryId)
|
||||
{
|
||||
using var conn = _connectionFactory.CreateConnection();
|
||||
return await conn.ExecuteScalarAsync<int>("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<IEnumerable<CategorySalesReportDto>> 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<CategorySalesReportDto>(sql, new { StartDate = startDate, EndDate = endDate });
|
||||
}
|
||||
|
||||
public async Task<int> GetPendingCountAsync()
|
||||
{
|
||||
using var conn = _connectionFactory.CreateConnection();
|
||||
// Contamos solo avisos en estado 'Pending'
|
||||
return await conn.ExecuteScalarAsync<int>("SELECT COUNT(1) FROM Listings WHERE Status = 'Pending'");
|
||||
}
|
||||
|
||||
public async Task<DashboardStats> 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<DailyRevenue>(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<ChannelStat>(channelsSql, new { StartDate = startDate.Date, EndDate = endDate.Date });
|
||||
stats.ChannelMix = channelResult.ToList();
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
public async Task<CashierDashboardDto?> 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<CashierDashboardDto>(sql,
|
||||
new { UserId = userId, Start = startDate.Date, End = endDate.Date });
|
||||
}
|
||||
|
||||
public async Task<GlobalReportDto> 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<ReportItemDto>(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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user