diff --git a/Controllers/LinesController.cs b/Controllers/LinesController.cs index ce0d9bf..af982ec 100644 --- a/Controllers/LinesController.cs +++ b/Controllers/LinesController.cs @@ -646,6 +646,11 @@ namespace line_gestao_api.Controllers // ========================= await ImportControleRecebidosFromWorkbook(wb); + // ========================= + // ✅ IMPORTA RESUMO + // ========================= + await ImportResumoFromWorkbook(wb); + await tx.CommitAsync(); return Ok(new ImportResultDto { Imported = imported }); } @@ -1474,6 +1479,398 @@ namespace line_gestao_api.Controllers } } + // ========================================================== + // ✅ IMPORTAÇÃO DA ABA RESUMO + // ========================================================== + private async Task ImportResumoFromWorkbook(XLWorkbook wb) + { + var ws = wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("RESUMO")); + if (ws == null) return; + + await _db.ResumoMacrophonyPlans.ExecuteDeleteAsync(); + await _db.ResumoMacrophonyTotals.ExecuteDeleteAsync(); + await _db.ResumoVivoLineResumos.ExecuteDeleteAsync(); + await _db.ResumoVivoLineTotals.ExecuteDeleteAsync(); + await _db.ResumoClienteEspeciais.ExecuteDeleteAsync(); + await _db.ResumoPlanoContratoResumos.ExecuteDeleteAsync(); + await _db.ResumoPlanoContratoTotals.ExecuteDeleteAsync(); + await _db.ResumoLineTotais.ExecuteDeleteAsync(); + await _db.ResumoReservaLines.ExecuteDeleteAsync(); + await _db.ResumoReservaTotals.ExecuteDeleteAsync(); + + var now = DateTime.UtcNow; + + await ImportResumoTabela1(ws, now); + await ImportResumoTabela2(ws, now); + await ImportResumoTabela3(ws, now); + await ImportResumoTabela4(ws, now); + await ImportResumoTabela5(ws, now); + await ImportResumoTabela6(ws, now); + } + + private async Task ImportResumoTabela1(IXLWorksheet ws, DateTime now) + { + const int headerRow = 5; + const int totalRow = 72; + var lastRow = Math.Min(totalRow - 1, ws.LastRowUsed()?.RowNumber() ?? totalRow - 1); + + var map = BuildHeaderMap(ws.Row(headerRow)); + var colPlano = GetCol(map, "PLANO CONTRATO"); + var colGb = GetCol(map, "GB"); + var colValorIndividual = GetColAny(map, "VALOR INDIVIDUAL C/ SVAs", "VALOR INDIVIDUAL C/ SVAS", "VALOR INDIVIDUAL"); + var colFranquiaGb = GetColAny(map, "FRANQUIA GB", "FRAQUIA GB"); + var colTotalLinhas = GetColAny(map, "TOTAL DE LINHAS", "TOTAL LINHAS"); + var colValorTotal = GetCol(map, "VALOR TOTAL"); + + var buffer = new List(200); + + for (int r = headerRow + 1; r <= lastRow; r++) + { + var plano = GetCellString(ws, r, colPlano); + var gb = GetCellString(ws, r, colGb); + var valorInd = GetCellString(ws, r, colValorIndividual); + var franquia = GetCellString(ws, r, colFranquiaGb); + var totalLinhas = GetCellString(ws, r, colTotalLinhas); + var valorTotal = GetCellString(ws, r, colValorTotal); + + if (string.IsNullOrWhiteSpace(plano) + && string.IsNullOrWhiteSpace(gb) + && string.IsNullOrWhiteSpace(valorInd) + && string.IsNullOrWhiteSpace(franquia) + && string.IsNullOrWhiteSpace(totalLinhas) + && string.IsNullOrWhiteSpace(valorTotal)) + { + continue; + } + + var vivoTravelCell = ws.Cell(r, 8).GetString(); + var vivoTravel = !string.IsNullOrWhiteSpace(vivoTravelCell) + && vivoTravelCell.Contains("VIVO TRAVEL", StringComparison.OrdinalIgnoreCase); + + buffer.Add(new ResumoMacrophonyPlan + { + PlanoContrato = string.IsNullOrWhiteSpace(plano) ? null : plano.Trim(), + Gb = TryDecimal(gb), + ValorIndividualComSvas = TryDecimal(valorInd), + FranquiaGb = TryDecimal(franquia), + TotalLinhas = TryNullableInt(totalLinhas), + ValorTotal = TryDecimal(valorTotal), + VivoTravel = vivoTravel, + CreatedAt = now, + UpdatedAt = now + }); + } + + if (buffer.Count > 0) + { + await _db.ResumoMacrophonyPlans.AddRangeAsync(buffer); + await _db.SaveChangesAsync(); + } + + var total = new ResumoMacrophonyTotal + { + FranquiaGbTotal = TryDecimal(GetCellString(ws, totalRow, colFranquiaGb)), + TotalLinhasTotal = TryNullableInt(GetCellString(ws, totalRow, colTotalLinhas)), + ValorTotal = TryDecimal(GetCellString(ws, totalRow, colValorTotal)), + CreatedAt = now, + UpdatedAt = now + }; + + await _db.ResumoMacrophonyTotals.AddAsync(total); + await _db.SaveChangesAsync(); + } + + private async Task ImportResumoTabela2(IXLWorksheet ws, DateTime now) + { + const int headerRow = 5; + const int totalRow = 219; + var lastRow = Math.Min(totalRow - 1, ws.LastRowUsed()?.RowNumber() ?? totalRow - 1); + + var map = BuildHeaderMap(ws.Row(headerRow)); + var colSkil = GetCol(map, "SKIL"); + var colCliente = GetCol(map, "CLIENTE"); + var colQtdLinhas = GetColAny(map, "QTD DE LINHAS", "QTD. DE LINHAS", "QTD LINHAS"); + var colFranquiaTotal = GetColAny(map, "FRANQUIA TOTAL", "FRAQUIA TOTAL"); + var colValorContratoVivo = GetColAny(map, "VALOR CONTRATO VIVO", "VALOR DO CONTRATO VIVO"); + var colFranquiaLine = GetColAny(map, "FRANQUIA LINE", "FRAQUIA LINE"); + var colValorContratoLine = GetColAny(map, "VALOR CONTRATO LINE", "VALOR DO CONTRATO LINE"); + var colLucro = GetCol(map, "LUCRO"); + + var buffer = new List(400); + + for (int r = headerRow + 1; r <= lastRow; r++) + { + var skil = GetCellString(ws, r, colSkil); + var cliente = GetCellString(ws, r, colCliente); + var qtdLinhas = GetCellString(ws, r, colQtdLinhas); + var franquiaTotal = GetCellString(ws, r, colFranquiaTotal); + var valorContratoVivo = GetCellString(ws, r, colValorContratoVivo); + var franquiaLine = GetCellString(ws, r, colFranquiaLine); + var valorContratoLine = GetCellString(ws, r, colValorContratoLine); + var lucro = GetCellString(ws, r, colLucro); + + if (string.IsNullOrWhiteSpace(skil) + && string.IsNullOrWhiteSpace(cliente) + && string.IsNullOrWhiteSpace(qtdLinhas) + && string.IsNullOrWhiteSpace(franquiaTotal) + && string.IsNullOrWhiteSpace(valorContratoVivo) + && string.IsNullOrWhiteSpace(franquiaLine) + && string.IsNullOrWhiteSpace(valorContratoLine) + && string.IsNullOrWhiteSpace(lucro)) + { + continue; + } + + buffer.Add(new ResumoVivoLineResumo + { + Skil = string.IsNullOrWhiteSpace(skil) ? null : skil.Trim(), + Cliente = string.IsNullOrWhiteSpace(cliente) ? null : cliente.Trim(), + QtdLinhas = TryNullableInt(qtdLinhas), + FranquiaTotal = TryDecimal(franquiaTotal), + ValorContratoVivo = TryDecimal(valorContratoVivo), + FranquiaLine = TryDecimal(franquiaLine), + ValorContratoLine = TryDecimal(valorContratoLine), + Lucro = TryDecimal(lucro), + CreatedAt = now, + UpdatedAt = now + }); + } + + if (buffer.Count > 0) + { + await _db.ResumoVivoLineResumos.AddRangeAsync(buffer); + await _db.SaveChangesAsync(); + } + + var total = new ResumoVivoLineTotal + { + QtdLinhasTotal = TryNullableInt(GetCellString(ws, totalRow, colQtdLinhas)), + FranquiaTotal = TryDecimal(GetCellString(ws, totalRow, colFranquiaTotal)), + ValorContratoVivo = TryDecimal(GetCellString(ws, totalRow, colValorContratoVivo)), + FranquiaLine = TryDecimal(GetCellString(ws, totalRow, colFranquiaLine)), + ValorContratoLine = TryDecimal(GetCellString(ws, totalRow, colValorContratoLine)), + Lucro = TryDecimal(GetCellString(ws, totalRow, colLucro)), + CreatedAt = now, + UpdatedAt = now + }; + + await _db.ResumoVivoLineTotals.AddAsync(total); + await _db.SaveChangesAsync(); + } + + private async Task ImportResumoTabela3(IXLWorksheet ws, DateTime now) + { + const int headerStartRow = 223; + const int headerEndRow = 225; + const int valuesRow = 227; + + var headerColumns = new Dictionary(); + for (int row = headerStartRow; row <= headerEndRow; row++) + { + var rowData = ws.Row(row); + var lastCol = rowData.LastCellUsed()?.Address.ColumnNumber ?? 1; + for (int col = 1; col <= lastCol; col++) + { + var name = rowData.Cell(col).GetString(); + if (string.IsNullOrWhiteSpace(name)) continue; + if (!headerColumns.ContainsKey(col)) + { + headerColumns[col] = name.Trim(); + } + } + } + + if (headerColumns.Count == 0) + { + return; + } + + var buffer = new List(headerColumns.Count); + foreach (var entry in headerColumns) + { + var valueStr = ws.Cell(valuesRow, entry.Key).GetString(); + buffer.Add(new ResumoClienteEspecial + { + Nome = entry.Value, + Valor = TryDecimal(valueStr), + CreatedAt = now, + UpdatedAt = now + }); + } + + await _db.ResumoClienteEspeciais.AddRangeAsync(buffer); + await _db.SaveChangesAsync(); + } + + private async Task ImportResumoTabela4(IXLWorksheet ws, DateTime now) + { + const int headerRow = 74; + const int totalRow = 81; + var lastRow = Math.Min(totalRow - 1, ws.LastRowUsed()?.RowNumber() ?? totalRow - 1); + + var map = BuildHeaderMap(ws.Row(headerRow)); + var colPlano = GetCol(map, "PLANO CONTRATO"); + var colGb = GetCol(map, "GB"); + var colValorIndividual = GetColAny(map, "VALOR INDIVIDUAL C/ SVAs", "VALOR INDIVIDUAL C/ SVAS", "VALOR INDIVIDUAL"); + var colFranquiaGb = GetColAny(map, "FRANQUIA GB", "FRAQUIA GB"); + var colTotalLinhas = GetColAny(map, "TOTAL DE LINHAS", "TOTAL LINHAS"); + var colValorTotal = GetCol(map, "VALOR TOTAL"); + + var buffer = new List(200); + + for (int r = headerRow + 1; r <= lastRow; r++) + { + var plano = GetCellString(ws, r, colPlano); + var gb = GetCellString(ws, r, colGb); + var valorInd = GetCellString(ws, r, colValorIndividual); + var franquia = GetCellString(ws, r, colFranquiaGb); + var totalLinhas = GetCellString(ws, r, colTotalLinhas); + var valorTotal = GetCellString(ws, r, colValorTotal); + + if (string.IsNullOrWhiteSpace(plano) + && string.IsNullOrWhiteSpace(gb) + && string.IsNullOrWhiteSpace(valorInd) + && string.IsNullOrWhiteSpace(franquia) + && string.IsNullOrWhiteSpace(totalLinhas) + && string.IsNullOrWhiteSpace(valorTotal)) + { + continue; + } + + buffer.Add(new ResumoPlanoContratoResumo + { + PlanoContrato = string.IsNullOrWhiteSpace(plano) ? null : plano.Trim(), + Gb = TryDecimal(gb), + ValorIndividualComSvas = TryDecimal(valorInd), + FranquiaGb = TryDecimal(franquia), + TotalLinhas = TryNullableInt(totalLinhas), + ValorTotal = TryDecimal(valorTotal), + CreatedAt = now, + UpdatedAt = now + }); + } + + if (buffer.Count > 0) + { + await _db.ResumoPlanoContratoResumos.AddRangeAsync(buffer); + await _db.SaveChangesAsync(); + } + + var total = new ResumoPlanoContratoTotal + { + ValorTotal = TryDecimal(ws.Cell(totalRow, 7).GetString()), + CreatedAt = now, + UpdatedAt = now + }; + + await _db.ResumoPlanoContratoTotals.AddAsync(total); + await _db.SaveChangesAsync(); + } + + private async Task ImportResumoTabela5(IXLWorksheet ws, DateTime now) + { + const int headerRow = 83; + var map = BuildHeaderMap(ws.Row(headerRow)); + var colValorTotalLine = GetColAny(map, "VALOR TOTAL LINE", "VALOR TOTAL LINE R$", "VALOR TOTAL LINE R$"); + var colLucroTotalLine = GetColAny(map, "LUCRO TOTAL LINE", "LUCRO TOTAL LINE R$", "LUCRO TOTAL LINE R$"); + var colQtdLinhas = GetColAny(map, "QTD. LINHAS", "QTD LINHAS", "QTD. DE LINHAS"); + + var buffer = new List(3); + for (int r = headerRow + 1; r <= headerRow + 3; r++) + { + var tipo = ws.Cell(r, 2).GetString(); + if (string.IsNullOrWhiteSpace(tipo)) + { + continue; + } + + buffer.Add(new ResumoLineTotais + { + Tipo = tipo.Trim(), + ValorTotalLine = TryDecimal(GetCellString(ws, r, colValorTotalLine)), + LucroTotalLine = TryDecimal(GetCellString(ws, r, colLucroTotalLine)), + QtdLinhas = TryNullableInt(GetCellString(ws, r, colQtdLinhas)), + CreatedAt = now, + UpdatedAt = now + }); + } + + if (buffer.Count > 0) + { + await _db.ResumoLineTotais.AddRangeAsync(buffer); + await _db.SaveChangesAsync(); + } + } + + private async Task ImportResumoTabela6(IXLWorksheet ws, DateTime now) + { + const int headerRow = 91; + const int totalRow = 139; + var lastRow = Math.Min(totalRow - 1, ws.LastRowUsed()?.RowNumber() ?? totalRow - 1); + + var map = BuildHeaderMap(ws.Row(headerRow)); + var colDdd = GetCol(map, "DDD"); + var colFranquiaGb = GetColAny(map, "FRANQUIA GB", "FRAQUIA GB"); + var colQtdLinhas = GetColAny(map, "QTD. DE LINHAS", "QTD DE LINHAS", "QTD. LINHAS"); + var colTotal = GetCol(map, "TOTAL"); + + var buffer = new List(200); + decimal? lastTotal = null; + + for (int r = headerRow + 1; r <= lastRow; r++) + { + var ddd = GetCellString(ws, r, colDdd); + var franquia = GetCellString(ws, r, colFranquiaGb); + var qtdLinhas = GetCellString(ws, r, colQtdLinhas); + var total = GetCellString(ws, r, colTotal); + + if (string.IsNullOrWhiteSpace(ddd) + && string.IsNullOrWhiteSpace(franquia) + && string.IsNullOrWhiteSpace(qtdLinhas) + && string.IsNullOrWhiteSpace(total)) + { + continue; + } + + var totalValue = TryDecimal(total); + if (!totalValue.HasValue && lastTotal.HasValue) + { + totalValue = lastTotal; + } + else if (totalValue.HasValue) + { + lastTotal = totalValue; + } + + buffer.Add(new ResumoReservaLine + { + Ddd = string.IsNullOrWhiteSpace(ddd) ? null : ddd.Trim(), + FranquiaGb = TryDecimal(franquia), + QtdLinhas = TryNullableInt(qtdLinhas), + Total = totalValue, + CreatedAt = now, + UpdatedAt = now + }); + } + + if (buffer.Count > 0) + { + await _db.ResumoReservaLines.AddRangeAsync(buffer); + await _db.SaveChangesAsync(); + } + + var totalEntity = new ResumoReservaTotal + { + QtdLinhasTotal = TryNullableInt(GetCellString(ws, totalRow, colQtdLinhas)), + Total = TryDecimal(GetCellString(ws, totalRow, colTotal)), + CreatedAt = now, + UpdatedAt = now + }; + + await _db.ResumoReservaTotals.AddAsync(totalEntity); + await _db.SaveChangesAsync(); + } + private async Task ImportControleRecebidosSheet(IXLWorksheet ws, int year) { var buffer = new List(500); diff --git a/Controllers/NotificationsController.cs b/Controllers/NotificationsController.cs index d842ae2..17536f0 100644 --- a/Controllers/NotificationsController.cs +++ b/Controllers/NotificationsController.cs @@ -1,5 +1,7 @@ +using ClosedXML.Excel; using line_gestao_api.Data; using line_gestao_api.Dtos; +using line_gestao_api.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -22,25 +24,46 @@ public class NotificationsController : ControllerBase [HttpGet("/notifications")] public async Task>> GetNotifications() { - var query = _db.Notifications.AsNoTracking(); - - var items = await query - .OrderByDescending(n => n.Data) - .Select(n => new NotificationDto - { - Id = n.Id, - Tipo = n.Tipo, - Titulo = n.Titulo, - Mensagem = n.Mensagem, - Data = n.Data, - ReferenciaData = n.ReferenciaData, - DiasParaVencer = n.DiasParaVencer, - Lida = n.Lida, - LidaEm = n.LidaEm, - VigenciaLineId = n.VigenciaLineId, - Cliente = n.Cliente, - Linha = n.Linha - }) + var items = await ( + from notification in _db.Notifications.AsNoTracking() + join vigencia in _db.VigenciaLines.AsNoTracking() + on notification.VigenciaLineId equals vigencia.Id into vigencias + from vigencia in vigencias.DefaultIfEmpty() + let vigenciaByLinha = _db.VigenciaLines.AsNoTracking() + .Where(v => notification.Linha != null && v.Linha == notification.Linha) + .OrderByDescending(v => v.UpdatedAt) + .FirstOrDefault() + orderby notification.Data descending + select new NotificationDto + { + Id = notification.Id, + Tipo = notification.Tipo, + Titulo = notification.Titulo, + Mensagem = notification.Mensagem, + Data = notification.Data, + ReferenciaData = notification.ReferenciaData, + DiasParaVencer = notification.DiasParaVencer, + Lida = notification.Lida, + LidaEm = notification.LidaEm, + VigenciaLineId = notification.VigenciaLineId, + Cliente = notification.Cliente + ?? (vigencia != null ? vigencia.Cliente : null) + ?? (vigenciaByLinha != null ? vigenciaByLinha.Cliente : null), + Linha = notification.Linha + ?? (vigencia != null ? vigencia.Linha : null) + ?? (vigenciaByLinha != null ? vigenciaByLinha.Linha : null), + Conta = (vigencia != null ? vigencia.Conta : null) + ?? (vigenciaByLinha != null ? vigenciaByLinha.Conta : null), + Usuario = notification.Usuario + ?? (vigencia != null ? vigencia.Usuario : null) + ?? (vigenciaByLinha != null ? vigenciaByLinha.Usuario : null), + PlanoContrato = (vigencia != null ? vigencia.PlanoContrato : null) + ?? (vigenciaByLinha != null ? vigenciaByLinha.PlanoContrato : null), + DtEfetivacaoServico = (vigencia != null ? vigencia.DtEfetivacaoServico : null) + ?? (vigenciaByLinha != null ? vigenciaByLinha.DtEfetivacaoServico : null), + DtTerminoFidelizacao = (vigencia != null ? vigencia.DtTerminoFidelizacao : null) + ?? (vigenciaByLinha != null ? vigenciaByLinha.DtTerminoFidelizacao : null) + }) .ToListAsync(); return Ok(items); @@ -68,4 +91,184 @@ public class NotificationsController : ControllerBase return NoContent(); } + [HttpPatch("read-all")] + [HttpPatch("/notifications/read-all")] + public async Task MarkAllAsRead( + [FromQuery] string? filter, + [FromBody] NotificationSelectionRequest? request) + { + var utcNow = DateTime.UtcNow; + var query = ApplySelectionAndFilter(_db.Notifications, filter, request?.NotificationIds) + .Where(n => !n.Lida); + + await query.ExecuteUpdateAsync(updates => updates + .SetProperty(n => n.Lida, true) + .SetProperty(n => n.LidaEm, utcNow)); + + return NoContent(); + } + + [HttpGet("export")] + [HttpGet("/notifications/export")] + public async Task ExportNotifications([FromQuery] string? filter) + { + var query = ApplySelectionAndFilter(_db.Notifications.AsNoTracking(), filter, null); + return await ExportNotificationsAsync(query, filter); + } + + [HttpPost("export")] + [HttpPost("/notifications/export")] + public async Task ExportNotifications( + [FromQuery] string? filter, + [FromBody] NotificationSelectionRequest? request) + { + var query = ApplySelectionAndFilter(_db.Notifications.AsNoTracking(), filter, request?.NotificationIds); + return await ExportNotificationsAsync(query, filter); + } + + private async Task ExportNotificationsAsync(IQueryable query, string? filter) + { + var rows = await ( + from notification in query + join vigencia in _db.VigenciaLines.AsNoTracking() + on notification.VigenciaLineId equals vigencia.Id into vigencias + from vigencia in vigencias.DefaultIfEmpty() + let vigenciaByLinha = _db.VigenciaLines.AsNoTracking() + .Where(v => notification.Linha != null && v.Linha == notification.Linha) + .OrderByDescending(v => v.UpdatedAt) + .FirstOrDefault() + orderby notification.ReferenciaData descending, notification.Data descending + select new NotificationExportRow( + (vigencia != null ? vigencia.Conta : null) + ?? (vigenciaByLinha != null ? vigenciaByLinha.Conta : null), + notification.Linha + ?? (vigencia != null ? vigencia.Linha : null) + ?? (vigenciaByLinha != null ? vigenciaByLinha.Linha : null), + notification.Cliente + ?? (vigencia != null ? vigencia.Cliente : null) + ?? (vigenciaByLinha != null ? vigenciaByLinha.Cliente : null), + notification.Usuario + ?? (vigencia != null ? vigencia.Usuario : null) + ?? (vigenciaByLinha != null ? vigenciaByLinha.Usuario : null), + (vigencia != null ? vigencia.PlanoContrato : null) + ?? (vigenciaByLinha != null ? vigenciaByLinha.PlanoContrato : null), + (vigencia != null ? vigencia.DtEfetivacaoServico : null) + ?? (vigenciaByLinha != null ? vigenciaByLinha.DtEfetivacaoServico : null), + notification.ReferenciaData + ?? (vigencia != null ? vigencia.DtTerminoFidelizacao : null) + ?? (vigenciaByLinha != null ? vigenciaByLinha.DtTerminoFidelizacao : null), + notification.Tipo)) + .ToListAsync(); + + using var workbook = new XLWorkbook(); + var worksheet = workbook.Worksheets.Add("Notificacoes"); + + var normalizedFilter = NormalizeFilter(filter); + var headers = new[] + { + "CONTA", + "LINHA", + "Cliente", + "Usuário", + "PLANO CONTRATO", + "DATA INICIO", + normalizedFilter is "vencidas" or "vencido" ? "DATA VENCIMENTO" : "DATA A VENCER", + "Status" + }; + + for (var i = 0; i < headers.Length; i++) + { + worksheet.Cell(1, i + 1).Value = headers[i]; + } + + var headerRange = worksheet.Range(1, 1, 1, headers.Length); + headerRange.Style.Font.Bold = true; + headerRange.Style.Fill.BackgroundColor = XLColor.LightGray; + headerRange.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; + + for (var i = 0; i < rows.Count; i++) + { + var row = rows[i]; + var rowIndex = i + 2; + worksheet.Cell(rowIndex, 1).Value = row.Conta ?? string.Empty; + worksheet.Cell(rowIndex, 2).Value = row.Linha ?? string.Empty; + worksheet.Cell(rowIndex, 3).Value = (row.Cliente ?? string.Empty).ToUpperInvariant(); + worksheet.Cell(rowIndex, 4).Value = (row.Usuario ?? string.Empty).ToUpperInvariant(); + worksheet.Cell(rowIndex, 5).Value = row.PlanoContrato ?? string.Empty; + worksheet.Cell(rowIndex, 6).Value = row.DataInicio; + worksheet.Cell(rowIndex, 7).Value = row.DataReferencia; + worksheet.Cell(rowIndex, 8).Value = row.Tipo.ToUpperInvariant(); + } + + worksheet.Column(1).Width = 18; + worksheet.Column(2).Width = 18; + worksheet.Column(3).Width = 26; + worksheet.Column(4).Width = 24; + worksheet.Column(5).Width = 22; + worksheet.Column(6).Width = 16; + worksheet.Column(7).Width = 18; + worksheet.Column(8).Width = 14; + + worksheet.Column(6).Style.DateFormat.Format = "dd/MM/yyyy"; + worksheet.Column(7).Style.DateFormat.Format = "dd/MM/yyyy"; + + worksheet.Columns().AdjustToContents(); + + using var stream = new MemoryStream(); + workbook.SaveAs(stream); + stream.Position = 0; + + var fileName = $"notificacoes-{DateTime.UtcNow:yyyyMMddHHmmss}.xlsx"; + return File( + stream.ToArray(), + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + fileName); + } + + private static IQueryable ApplySelectionAndFilter( + IQueryable query, + string? filter, + IReadOnlyCollection? notificationIds) + { + query = ApplyFilter(query, filter); + + if (notificationIds is { Count: > 0 }) + { + query = query.Where(n => notificationIds.Contains(n.Id)); + } + + return query; + } + + private static IQueryable ApplyFilter(IQueryable query, string? filter) + { + var normalized = NormalizeFilter(filter); + return normalized switch + { + "a-vencer" or "avencer" => query.Where(n => n.Tipo == "AVencer"), + "vencidas" or "vencido" => query.Where(n => n.Tipo == "Vencido"), + _ => query + }; + } + + private static string? NormalizeFilter(string? filter) + { + return filter?.Trim().ToLowerInvariant(); + } + + private sealed record NotificationExportRow( + string? Conta, + string? Linha, + string? Cliente, + string? Usuario, + string? PlanoContrato, + DateTime? DataInicio, + DateTime? DataReferencia, + string Tipo); + + public sealed class NotificationSelectionRequest + { + public List? NotificationIds { get; set; } + } + } diff --git a/Controllers/ResumoController.cs b/Controllers/ResumoController.cs new file mode 100644 index 0000000..51ef6d9 --- /dev/null +++ b/Controllers/ResumoController.cs @@ -0,0 +1,129 @@ +using line_gestao_api.Data; +using line_gestao_api.Dtos; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace line_gestao_api.Controllers; + +[ApiController] +[Route("api/resumo")] +[Authorize] +public class ResumoController : ControllerBase +{ + private readonly AppDbContext _db; + + public ResumoController(AppDbContext db) + { + _db = db; + } + + [HttpGet] + public async Task> GetResumo() + { + var response = new ResumoResponseDto + { + MacrophonyPlans = await _db.ResumoMacrophonyPlans.AsNoTracking() + .OrderBy(x => x.PlanoContrato) + .Select(x => new ResumoMacrophonyPlanDto + { + PlanoContrato = x.PlanoContrato, + Gb = x.Gb, + ValorIndividualComSvas = x.ValorIndividualComSvas, + FranquiaGb = x.FranquiaGb, + TotalLinhas = x.TotalLinhas, + ValorTotal = x.ValorTotal, + VivoTravel = x.VivoTravel + }) + .ToListAsync(), + MacrophonyTotals = await _db.ResumoMacrophonyTotals.AsNoTracking() + .Select(x => new ResumoMacrophonyTotalDto + { + FranquiaGbTotal = x.FranquiaGbTotal, + TotalLinhasTotal = x.TotalLinhasTotal, + ValorTotal = x.ValorTotal + }) + .FirstOrDefaultAsync(), + VivoLineResumos = await _db.ResumoVivoLineResumos.AsNoTracking() + .OrderBy(x => x.Cliente) + .Select(x => new ResumoVivoLineResumoDto + { + Skil = x.Skil, + Cliente = x.Cliente, + QtdLinhas = x.QtdLinhas, + FranquiaTotal = x.FranquiaTotal, + ValorContratoVivo = x.ValorContratoVivo, + FranquiaLine = x.FranquiaLine, + ValorContratoLine = x.ValorContratoLine, + Lucro = x.Lucro + }) + .ToListAsync(), + VivoLineTotals = await _db.ResumoVivoLineTotals.AsNoTracking() + .Select(x => new ResumoVivoLineTotalDto + { + QtdLinhasTotal = x.QtdLinhasTotal, + FranquiaTotal = x.FranquiaTotal, + ValorContratoVivo = x.ValorContratoVivo, + FranquiaLine = x.FranquiaLine, + ValorContratoLine = x.ValorContratoLine, + Lucro = x.Lucro + }) + .FirstOrDefaultAsync(), + ClienteEspeciais = await _db.ResumoClienteEspeciais.AsNoTracking() + .OrderBy(x => x.Nome) + .Select(x => new ResumoClienteEspecialDto + { + Nome = x.Nome, + Valor = x.Valor + }) + .ToListAsync(), + PlanoContratoResumos = await _db.ResumoPlanoContratoResumos.AsNoTracking() + .OrderBy(x => x.PlanoContrato) + .Select(x => new ResumoPlanoContratoResumoDto + { + PlanoContrato = x.PlanoContrato, + Gb = x.Gb, + ValorIndividualComSvas = x.ValorIndividualComSvas, + FranquiaGb = x.FranquiaGb, + TotalLinhas = x.TotalLinhas, + ValorTotal = x.ValorTotal + }) + .ToListAsync(), + PlanoContratoTotal = await _db.ResumoPlanoContratoTotals.AsNoTracking() + .Select(x => new ResumoPlanoContratoTotalDto + { + ValorTotal = x.ValorTotal + }) + .FirstOrDefaultAsync(), + LineTotais = await _db.ResumoLineTotais.AsNoTracking() + .OrderBy(x => x.Tipo) + .Select(x => new ResumoLineTotaisDto + { + Tipo = x.Tipo, + ValorTotalLine = x.ValorTotalLine, + LucroTotalLine = x.LucroTotalLine, + QtdLinhas = x.QtdLinhas + }) + .ToListAsync(), + ReservaLines = await _db.ResumoReservaLines.AsNoTracking() + .OrderBy(x => x.Ddd) + .Select(x => new ResumoReservaLineDto + { + Ddd = x.Ddd, + FranquiaGb = x.FranquiaGb, + QtdLinhas = x.QtdLinhas, + Total = x.Total + }) + .ToListAsync(), + ReservaTotal = await _db.ResumoReservaTotals.AsNoTracking() + .Select(x => new ResumoReservaTotalDto + { + QtdLinhasTotal = x.QtdLinhasTotal, + Total = x.Total + }) + .FirstOrDefaultAsync() + }; + + return Ok(response); + } +} diff --git a/Data/AppDbContext.cs b/Data/AppDbContext.cs index ddd837c..c3f9bee 100644 --- a/Data/AppDbContext.cs +++ b/Data/AppDbContext.cs @@ -44,6 +44,18 @@ public class AppDbContext : IdentityDbContext Notifications => Set(); + // ✅ tabela RESUMO + public DbSet ResumoMacrophonyPlans => Set(); + public DbSet ResumoMacrophonyTotals => Set(); + public DbSet ResumoVivoLineResumos => Set(); + public DbSet ResumoVivoLineTotals => Set(); + public DbSet ResumoClienteEspeciais => Set(); + public DbSet ResumoPlanoContratoResumos => Set(); + public DbSet ResumoPlanoContratoTotals => Set(); + public DbSet ResumoLineTotais => Set(); + public DbSet ResumoReservaLines => Set(); + public DbSet ResumoReservaTotals => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -212,6 +224,16 @@ public class AppDbContext : IdentityDbContext().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); + modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); modelBuilder.Entity().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); } diff --git a/Dtos/NotificationDto.cs b/Dtos/NotificationDto.cs index 0d7c7e7..6d63e34 100644 --- a/Dtos/NotificationDto.cs +++ b/Dtos/NotificationDto.cs @@ -14,4 +14,9 @@ public class NotificationDto public Guid? VigenciaLineId { get; set; } public string? Cliente { get; set; } public string? Linha { get; set; } + public string? Conta { get; set; } + public string? Usuario { get; set; } + public string? PlanoContrato { get; set; } + public DateTime? DtEfetivacaoServico { get; set; } + public DateTime? DtTerminoFidelizacao { get; set; } } diff --git a/Dtos/ResumoDtos.cs b/Dtos/ResumoDtos.cs new file mode 100644 index 0000000..3b735ed --- /dev/null +++ b/Dtos/ResumoDtos.cs @@ -0,0 +1,98 @@ +namespace line_gestao_api.Dtos; + +public sealed class ResumoResponseDto +{ + public List MacrophonyPlans { get; set; } = new(); + public ResumoMacrophonyTotalDto? MacrophonyTotals { get; set; } + public List VivoLineResumos { get; set; } = new(); + public ResumoVivoLineTotalDto? VivoLineTotals { get; set; } + public List ClienteEspeciais { get; set; } = new(); + public List PlanoContratoResumos { get; set; } = new(); + public ResumoPlanoContratoTotalDto? PlanoContratoTotal { get; set; } + public List LineTotais { get; set; } = new(); + public List ReservaLines { get; set; } = new(); + public ResumoReservaTotalDto? ReservaTotal { get; set; } +} + +public sealed class ResumoMacrophonyPlanDto +{ + public string? PlanoContrato { get; set; } + public decimal? Gb { get; set; } + public decimal? ValorIndividualComSvas { get; set; } + public decimal? FranquiaGb { get; set; } + public int? TotalLinhas { get; set; } + public decimal? ValorTotal { get; set; } + public bool VivoTravel { get; set; } +} + +public sealed class ResumoMacrophonyTotalDto +{ + public decimal? FranquiaGbTotal { get; set; } + public int? TotalLinhasTotal { get; set; } + public decimal? ValorTotal { get; set; } +} + +public sealed class ResumoVivoLineResumoDto +{ + public string? Skil { get; set; } + public string? Cliente { get; set; } + public int? QtdLinhas { get; set; } + public decimal? FranquiaTotal { get; set; } + public decimal? ValorContratoVivo { get; set; } + public decimal? FranquiaLine { get; set; } + public decimal? ValorContratoLine { get; set; } + public decimal? Lucro { get; set; } +} + +public sealed class ResumoVivoLineTotalDto +{ + public int? QtdLinhasTotal { get; set; } + public decimal? FranquiaTotal { get; set; } + public decimal? ValorContratoVivo { get; set; } + public decimal? FranquiaLine { get; set; } + public decimal? ValorContratoLine { get; set; } + public decimal? Lucro { get; set; } +} + +public sealed class ResumoClienteEspecialDto +{ + public string? Nome { get; set; } + public decimal? Valor { get; set; } +} + +public sealed class ResumoPlanoContratoResumoDto +{ + public string? PlanoContrato { get; set; } + public decimal? Gb { get; set; } + public decimal? ValorIndividualComSvas { get; set; } + public decimal? FranquiaGb { get; set; } + public int? TotalLinhas { get; set; } + public decimal? ValorTotal { get; set; } +} + +public sealed class ResumoPlanoContratoTotalDto +{ + public decimal? ValorTotal { get; set; } +} + +public sealed class ResumoLineTotaisDto +{ + public string? Tipo { get; set; } + public decimal? ValorTotalLine { get; set; } + public decimal? LucroTotalLine { get; set; } + public int? QtdLinhas { get; set; } +} + +public sealed class ResumoReservaLineDto +{ + public string? Ddd { get; set; } + public decimal? FranquiaGb { get; set; } + public int? QtdLinhas { get; set; } + public decimal? Total { get; set; } +} + +public sealed class ResumoReservaTotalDto +{ + public int? QtdLinhasTotal { get; set; } + public decimal? Total { get; set; } +} diff --git a/Models/ResumoClienteEspecial.cs b/Models/ResumoClienteEspecial.cs new file mode 100644 index 0000000..aa1d84c --- /dev/null +++ b/Models/ResumoClienteEspecial.cs @@ -0,0 +1,14 @@ +namespace line_gestao_api.Models; + +public class ResumoClienteEspecial : ITenantEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid TenantId { get; set; } + + public string? Nome { get; set; } + public decimal? Valor { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} diff --git a/Models/ResumoLineTotais.cs b/Models/ResumoLineTotais.cs new file mode 100644 index 0000000..b152058 --- /dev/null +++ b/Models/ResumoLineTotais.cs @@ -0,0 +1,16 @@ +namespace line_gestao_api.Models; + +public class ResumoLineTotais : ITenantEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid TenantId { get; set; } + + public string? Tipo { get; set; } + public decimal? ValorTotalLine { get; set; } + public decimal? LucroTotalLine { get; set; } + public int? QtdLinhas { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} diff --git a/Models/ResumoMacrophonyPlan.cs b/Models/ResumoMacrophonyPlan.cs new file mode 100644 index 0000000..3f13dff --- /dev/null +++ b/Models/ResumoMacrophonyPlan.cs @@ -0,0 +1,19 @@ +namespace line_gestao_api.Models; + +public class ResumoMacrophonyPlan : ITenantEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid TenantId { get; set; } + + public string? PlanoContrato { get; set; } + public decimal? Gb { get; set; } + public decimal? ValorIndividualComSvas { get; set; } + public decimal? FranquiaGb { get; set; } + public int? TotalLinhas { get; set; } + public decimal? ValorTotal { get; set; } + public bool VivoTravel { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} diff --git a/Models/ResumoMacrophonyTotal.cs b/Models/ResumoMacrophonyTotal.cs new file mode 100644 index 0000000..b24ad3b --- /dev/null +++ b/Models/ResumoMacrophonyTotal.cs @@ -0,0 +1,15 @@ +namespace line_gestao_api.Models; + +public class ResumoMacrophonyTotal : ITenantEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid TenantId { get; set; } + + public decimal? FranquiaGbTotal { get; set; } + public int? TotalLinhasTotal { get; set; } + public decimal? ValorTotal { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} diff --git a/Models/ResumoPlanoContratoResumo.cs b/Models/ResumoPlanoContratoResumo.cs new file mode 100644 index 0000000..6629b6c --- /dev/null +++ b/Models/ResumoPlanoContratoResumo.cs @@ -0,0 +1,18 @@ +namespace line_gestao_api.Models; + +public class ResumoPlanoContratoResumo : ITenantEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid TenantId { get; set; } + + public string? PlanoContrato { get; set; } + public decimal? Gb { get; set; } + public decimal? ValorIndividualComSvas { get; set; } + public decimal? FranquiaGb { get; set; } + public int? TotalLinhas { get; set; } + public decimal? ValorTotal { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} diff --git a/Models/ResumoPlanoContratoTotal.cs b/Models/ResumoPlanoContratoTotal.cs new file mode 100644 index 0000000..fa88609 --- /dev/null +++ b/Models/ResumoPlanoContratoTotal.cs @@ -0,0 +1,13 @@ +namespace line_gestao_api.Models; + +public class ResumoPlanoContratoTotal : ITenantEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid TenantId { get; set; } + + public decimal? ValorTotal { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} diff --git a/Models/ResumoReservaLine.cs b/Models/ResumoReservaLine.cs new file mode 100644 index 0000000..06e0c4b --- /dev/null +++ b/Models/ResumoReservaLine.cs @@ -0,0 +1,16 @@ +namespace line_gestao_api.Models; + +public class ResumoReservaLine : ITenantEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid TenantId { get; set; } + + public string? Ddd { get; set; } + public decimal? FranquiaGb { get; set; } + public int? QtdLinhas { get; set; } + public decimal? Total { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} diff --git a/Models/ResumoReservaTotal.cs b/Models/ResumoReservaTotal.cs new file mode 100644 index 0000000..a093ea5 --- /dev/null +++ b/Models/ResumoReservaTotal.cs @@ -0,0 +1,14 @@ +namespace line_gestao_api.Models; + +public class ResumoReservaTotal : ITenantEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid TenantId { get; set; } + + public int? QtdLinhasTotal { get; set; } + public decimal? Total { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} diff --git a/Models/ResumoVivoLineResumo.cs b/Models/ResumoVivoLineResumo.cs new file mode 100644 index 0000000..3d7f0fb --- /dev/null +++ b/Models/ResumoVivoLineResumo.cs @@ -0,0 +1,20 @@ +namespace line_gestao_api.Models; + +public class ResumoVivoLineResumo : ITenantEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid TenantId { get; set; } + + public string? Skil { get; set; } + public string? Cliente { get; set; } + public int? QtdLinhas { get; set; } + public decimal? FranquiaTotal { get; set; } + public decimal? ValorContratoVivo { get; set; } + public decimal? FranquiaLine { get; set; } + public decimal? ValorContratoLine { get; set; } + public decimal? Lucro { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} diff --git a/Models/ResumoVivoLineTotal.cs b/Models/ResumoVivoLineTotal.cs new file mode 100644 index 0000000..ec62bd4 --- /dev/null +++ b/Models/ResumoVivoLineTotal.cs @@ -0,0 +1,18 @@ +namespace line_gestao_api.Models; + +public class ResumoVivoLineTotal : ITenantEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid TenantId { get; set; } + + public int? QtdLinhasTotal { get; set; } + public decimal? FranquiaTotal { get; set; } + public decimal? ValorContratoVivo { get; set; } + public decimal? FranquiaLine { get; set; } + public decimal? ValorContratoLine { get; set; } + public decimal? Lucro { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} diff --git a/Services/VigenciaNotificationBackgroundService.cs b/Services/VigenciaNotificationBackgroundService.cs index d97ff91..69c281a 100644 --- a/Services/VigenciaNotificationBackgroundService.cs +++ b/Services/VigenciaNotificationBackgroundService.cs @@ -119,6 +119,11 @@ public class VigenciaNotificationBackgroundService : BackgroundService .Where(v => v.DtTerminoFidelizacao != null) .ToListAsync(stoppingToken); + if (vigencias.Count > 0) + { + await CleanupOutdatedNotificationsAsync(db, vigencias, reminderDays, today, stoppingToken); + } + var candidates = new List(); foreach (var vigencia in vigencias) { @@ -223,6 +228,92 @@ public class VigenciaNotificationBackgroundService : BackgroundService await db.SaveChangesAsync(stoppingToken); } + private static async Task CleanupOutdatedNotificationsAsync( + AppDbContext db, + IReadOnlyCollection vigencias, + IReadOnlyCollection reminderDays, + DateTime today, + CancellationToken stoppingToken) + { + var vigenciasById = vigencias.ToDictionary(v => v.Id, v => v); + var vigenciasByLinha = vigencias + .Where(v => !string.IsNullOrWhiteSpace(v.Linha)) + .GroupBy(v => v.Linha!) + .Select(g => g.OrderByDescending(v => v.UpdatedAt).First()) + .ToDictionary(v => v.Linha!, v => v); + + var existingNotifications = await db.Notifications.AsNoTracking() + .Where(n => n.Tipo == "Vencido" || n.Tipo == "AVencer") + .ToListAsync(stoppingToken); + + if (existingNotifications.Count == 0) + { + return; + } + + var idsToDelete = new List(); + foreach (var notification in existingNotifications) + { + var vigencia = ResolveVigencia(notification, vigenciasById, vigenciasByLinha); + if (vigencia?.DtTerminoFidelizacao is null) + { + continue; + } + + var endDate = vigencia.DtTerminoFidelizacao.Value.Date; + if (endDate < today) + { + if (notification.Tipo != "Vencido") + { + idsToDelete.Add(notification.Id); + } + + continue; + } + + var daysUntil = (endDate - today).Days; + if (notification.Tipo == "Vencido") + { + idsToDelete.Add(notification.Id); + continue; + } + + if (!reminderDays.Contains(daysUntil) || notification.DiasParaVencer != daysUntil) + { + idsToDelete.Add(notification.Id); + } + } + + if (idsToDelete.Count == 0) + { + return; + } + + await db.Notifications + .Where(n => idsToDelete.Contains(n.Id)) + .ExecuteDeleteAsync(stoppingToken); + } + + private static VigenciaLine? ResolveVigencia( + Notification notification, + IReadOnlyDictionary vigenciasById, + IReadOnlyDictionary vigenciasByLinha) + { + if (notification.VigenciaLineId.HasValue + && vigenciasById.TryGetValue(notification.VigenciaLineId.Value, out var byId)) + { + return byId; + } + + if (!string.IsNullOrWhiteSpace(notification.Linha) + && vigenciasByLinha.TryGetValue(notification.Linha, out var byLinha)) + { + return byLinha; + } + + return null; + } + private static Notification BuildNotification( string tipo, string titulo,