diff --git a/Controllers/LinesController.cs b/Controllers/LinesController.cs index ce0d9bf..ed8ce63 100644 --- a/Controllers/LinesController.cs +++ b/Controllers/LinesController.cs @@ -2,6 +2,7 @@ using line_gestao_api.Data; using line_gestao_api.Dtos; using line_gestao_api.Models; +using line_gestao_api.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -20,10 +21,12 @@ namespace line_gestao_api.Controllers public class LinesController : ControllerBase { private readonly AppDbContext _db; + private readonly ParcelamentosImportService _parcelamentosImportService; - public LinesController(AppDbContext db) + public LinesController(AppDbContext db, ParcelamentosImportService parcelamentosImportService) { _db = db; + _parcelamentosImportService = parcelamentosImportService; } public class ImportExcelForm @@ -646,8 +649,18 @@ namespace line_gestao_api.Controllers // ========================= await ImportControleRecebidosFromWorkbook(wb); + // ========================= + // ✅ IMPORTA RESUMO + // ========================= + await ImportResumoFromWorkbook(wb); + + // ========================= + // ✅ IMPORTA PARCELAMENTOS + // ========================= + var parcelamentosSummary = await _parcelamentosImportService.ImportFromWorkbookAsync(wb, replaceAll: true); + await tx.CommitAsync(); - return Ok(new ImportResultDto { Imported = imported }); + return Ok(new ImportResultDto { Imported = imported, Parcelamentos = parcelamentosSummary }); } catch (Exception ex) { @@ -1474,6 +1487,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/ParcelamentosController.cs b/Controllers/ParcelamentosController.cs new file mode 100644 index 0000000..989d143 --- /dev/null +++ b/Controllers/ParcelamentosController.cs @@ -0,0 +1,128 @@ +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/parcelamentos")] +[Authorize] +public class ParcelamentosController : ControllerBase +{ + private readonly AppDbContext _db; + + public ParcelamentosController(AppDbContext db) + { + _db = db; + } + + [HttpGet] + public async Task>> GetAll( + [FromQuery] int? anoRef, + [FromQuery] string? linha, + [FromQuery] string? cliente, + [FromQuery] int? competenciaAno, + [FromQuery] int? competenciaMes, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 20) + { + page = page < 1 ? 1 : page; + pageSize = pageSize < 1 ? 20 : pageSize; + + var query = _db.ParcelamentoLines.AsNoTracking(); + + if (anoRef.HasValue) + { + query = query.Where(x => x.AnoRef == anoRef.Value); + } + + if (!string.IsNullOrWhiteSpace(linha)) + { + var l = linha.Trim(); + query = query.Where(x => x.Linha == l); + } + + if (!string.IsNullOrWhiteSpace(cliente)) + { + var c = cliente.Trim(); + query = query.Where(x => x.Cliente != null && EF.Functions.ILike(x.Cliente, $"%{c}%")); + } + + if (competenciaAno.HasValue && competenciaMes.HasValue) + { + var competencia = new DateOnly(competenciaAno.Value, competenciaMes.Value, 1); + query = query.Where(x => x.MonthValues.Any(m => m.Competencia == competencia)); + } + + var total = await query.CountAsync(); + + var items = await query + .OrderBy(x => x.Item) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .Select(x => new ParcelamentoListDto + { + Id = x.Id, + AnoRef = x.AnoRef, + Item = x.Item, + Linha = x.Linha, + Cliente = x.Cliente, + QtParcelas = x.QtParcelas, + ParcelaAtual = x.ParcelaAtual, + TotalParcelas = x.TotalParcelas, + ValorCheio = x.ValorCheio, + Desconto = x.Desconto, + ValorComDesconto = x.ValorComDesconto + }) + .ToListAsync(); + + return Ok(new PagedResult + { + Page = page, + PageSize = pageSize, + Total = total, + Items = items + }); + } + + [HttpGet("{id:guid}")] + public async Task> GetById(Guid id) + { + var item = await _db.ParcelamentoLines + .AsNoTracking() + .Include(x => x.MonthValues) + .FirstOrDefaultAsync(x => x.Id == id); + + if (item == null) + { + return NotFound(); + } + + var dto = new ParcelamentoDetailDto + { + Id = item.Id, + AnoRef = item.AnoRef, + Item = item.Item, + Linha = item.Linha, + Cliente = item.Cliente, + QtParcelas = item.QtParcelas, + ParcelaAtual = item.ParcelaAtual, + TotalParcelas = item.TotalParcelas, + ValorCheio = item.ValorCheio, + Desconto = item.Desconto, + ValorComDesconto = item.ValorComDesconto, + MonthValues = item.MonthValues + .OrderBy(x => x.Competencia) + .Select(x => new ParcelamentoMonthDto + { + Competencia = x.Competencia, + Valor = x.Valor + }) + .ToList() + }; + + return Ok(dto); + } +} 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..6b81d02 100644 --- a/Data/AppDbContext.cs +++ b/Data/AppDbContext.cs @@ -44,6 +44,22 @@ 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(); + + // ✅ tabela PARCELAMENTOS + public DbSet ParcelamentoLines => Set(); + public DbSet ParcelamentoMonthValues => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -203,6 +219,35 @@ public class AppDbContext : IdentityDbContext(e => + { + e.Property(x => x.Linha).HasMaxLength(32); + e.Property(x => x.Cliente).HasMaxLength(120); + e.Property(x => x.QtParcelas).HasMaxLength(32); + e.Property(x => x.ValorCheio).HasPrecision(18, 2); + e.Property(x => x.Desconto).HasPrecision(18, 2); + e.Property(x => x.ValorComDesconto).HasPrecision(18, 2); + + e.HasIndex(x => new { x.AnoRef, x.Item }).IsUnique(); + e.HasIndex(x => x.Linha); + e.HasIndex(x => x.TenantId); + }); + + modelBuilder.Entity(e => + { + e.Property(x => x.Valor).HasPrecision(18, 2); + e.HasIndex(x => new { x.ParcelamentoLineId, x.Competencia }).IsUnique(); + e.HasIndex(x => x.TenantId); + + e.HasOne(x => x.ParcelamentoLine) + .WithMany(x => x.MonthValues) + .HasForeignKey(x => x.ParcelamentoLineId) + .OnDelete(DeleteBehavior.Cascade); + }); + 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); @@ -212,6 +257,18 @@ 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); + 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/MobileLineDtos.cs b/Dtos/MobileLineDtos.cs index 42d9e5c..16b550c 100644 --- a/Dtos/MobileLineDtos.cs +++ b/Dtos/MobileLineDtos.cs @@ -97,6 +97,7 @@ public class ImportResultDto { public int Imported { get; set; } + public ParcelamentosImportSummaryDto? Parcelamentos { get; set; } } public class LineOptionDto 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/ParcelamentosDtos.cs b/Dtos/ParcelamentosDtos.cs new file mode 100644 index 0000000..53d73b6 --- /dev/null +++ b/Dtos/ParcelamentosDtos.cs @@ -0,0 +1,43 @@ +namespace line_gestao_api.Dtos; + +public sealed class ParcelamentoListDto +{ + public Guid Id { get; set; } + public int? AnoRef { get; set; } + public int? Item { get; set; } + public string? Linha { get; set; } + public string? Cliente { get; set; } + public string? QtParcelas { get; set; } + public int? ParcelaAtual { get; set; } + public int? TotalParcelas { get; set; } + public decimal? ValorCheio { get; set; } + public decimal? Desconto { get; set; } + public decimal? ValorComDesconto { get; set; } +} + +public sealed class ParcelamentoMonthDto +{ + public DateOnly Competencia { get; set; } + public decimal? Valor { get; set; } +} + +public sealed class ParcelamentoDetailDto : ParcelamentoListDto +{ + public List MonthValues { get; set; } = new(); +} + +public sealed class ParcelamentosImportErrorDto +{ + public int LinhaExcel { get; set; } + public string Motivo { get; set; } = string.Empty; + public string? Valor { get; set; } +} + +public sealed class ParcelamentosImportSummaryDto +{ + public int Lidos { get; set; } + public int Inseridos { get; set; } + public int Atualizados { get; set; } + public int ParcelasInseridas { get; set; } + public List Erros { get; set; } = new(); +} 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/ParcelamentoLine.cs b/Models/ParcelamentoLine.cs new file mode 100644 index 0000000..b16e152 --- /dev/null +++ b/Models/ParcelamentoLine.cs @@ -0,0 +1,24 @@ +namespace line_gestao_api.Models; + +public class ParcelamentoLine : ITenantEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid TenantId { get; set; } + + public int? AnoRef { get; set; } + public int? Item { get; set; } + public string? Linha { get; set; } + public string? Cliente { get; set; } + public string? QtParcelas { get; set; } + public int? ParcelaAtual { get; set; } + public int? TotalParcelas { get; set; } + public decimal? ValorCheio { get; set; } + public decimal? Desconto { get; set; } + public decimal? ValorComDesconto { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + + public List MonthValues { get; set; } = new(); +} diff --git a/Models/ParcelamentoMonthValue.cs b/Models/ParcelamentoMonthValue.cs new file mode 100644 index 0000000..e8af6b6 --- /dev/null +++ b/Models/ParcelamentoMonthValue.cs @@ -0,0 +1,16 @@ +namespace line_gestao_api.Models; + +public class ParcelamentoMonthValue : ITenantEntity +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + public Guid TenantId { get; set; } + + public Guid ParcelamentoLineId { get; set; } + public ParcelamentoLine? ParcelamentoLine { get; set; } + + public DateOnly Competencia { get; set; } + public decimal? Valor { get; set; } + + public DateTime CreatedAt { 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/Program.cs b/Program.cs index 6376507..58b50b5 100644 --- a/Program.cs +++ b/Program.cs @@ -35,6 +35,7 @@ builder.Services.AddDbContext(options => builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddIdentityCore(options => { diff --git a/Services/ParcelamentosImportService.cs b/Services/ParcelamentosImportService.cs new file mode 100644 index 0000000..c1a5671 --- /dev/null +++ b/Services/ParcelamentosImportService.cs @@ -0,0 +1,447 @@ +using System.Globalization; +using System.Text; +using ClosedXML.Excel; +using line_gestao_api.Data; +using line_gestao_api.Dtos; +using line_gestao_api.Models; +using Microsoft.EntityFrameworkCore; + +namespace line_gestao_api.Services; + +public sealed class ParcelamentosImportService +{ + private readonly AppDbContext _db; + + public ParcelamentosImportService(AppDbContext db) + { + _db = db; + } + + public async Task ImportFromWorkbookAsync(XLWorkbook wb, bool replaceAll, CancellationToken cancellationToken = default) + { + var ws = FindWorksheet(wb); + if (ws == null) + { + return new ParcelamentosImportSummaryDto + { + Erros = + { + new ParcelamentosImportErrorDto + { + LinhaExcel = 0, + Motivo = "Aba 'PARCELAMENTOS DE APARELHOS' ou 'PARCELAMENTOS' não encontrada." + } + } + }; + } + + if (replaceAll) + { + await _db.ParcelamentoMonthValues.ExecuteDeleteAsync(cancellationToken); + await _db.ParcelamentoLines.ExecuteDeleteAsync(cancellationToken); + } + + var headerRowIndex = FindHeaderRow(ws); + if (headerRowIndex == 0) + { + return new ParcelamentosImportSummaryDto + { + Erros = + { + new ParcelamentosImportErrorDto + { + LinhaExcel = 0, + Motivo = "Cabeçalho 'LINHA' não encontrado na aba de parcelamentos." + } + } + }; + } + + var yearRowIndex = headerRowIndex - 1; + var headerRow = ws.Row(headerRowIndex); + var map = BuildHeaderMap(headerRow); + + var colLinha = GetCol(map, "LINHA"); + var colCliente = GetCol(map, "CLIENTE"); + var colQtParcelas = GetColAny(map, "QT PARCELAS", "QT. PARCELAS", "QT PARCELAS (NN/TT)", "QTDE PARCELAS"); + var colValorCheio = GetColAny(map, "VALOR CHEIO"); + var colDesconto = GetColAny(map, "DESCONTO"); + var colValorComDesconto = GetColAny(map, "VALOR C/ DESCONTO", "VALOR COM DESCONTO"); + + if (colLinha == 0 || colValorComDesconto == 0) + { + return new ParcelamentosImportSummaryDto + { + Erros = + { + new ParcelamentosImportErrorDto + { + LinhaExcel = headerRowIndex, + Motivo = "Colunas obrigatórias não encontradas (LINHA / VALOR C/ DESCONTO)." + } + } + }; + } + + var yearMap = BuildYearMap(ws, yearRowIndex, headerRow); + var monthColumns = BuildMonthColumns(headerRow, colValorComDesconto + 1); + + var existing = await _db.ParcelamentoLines + .AsNoTracking() + .ToListAsync(cancellationToken); + var existingByKey = existing + .Where(x => x.AnoRef.HasValue && x.Item.HasValue) + .ToDictionary(x => (x.AnoRef!.Value, x.Item!.Value), x => x.Id); + + var summary = new ParcelamentosImportSummaryDto(); + var lastRow = ws.LastRowUsed()?.RowNumber() ?? headerRowIndex; + + for (int row = headerRowIndex + 1; row <= lastRow; row++) + { + var linhaValue = GetCellString(ws, row, colLinha); + var itemStr = GetCellString(ws, row, 4); + if (string.IsNullOrWhiteSpace(itemStr) && string.IsNullOrWhiteSpace(linhaValue)) + { + break; + } + + summary.Lidos++; + + var anoRef = TryNullableInt(GetCellString(ws, row, 3)); + var item = TryNullableInt(itemStr); + + if (!item.HasValue) + { + summary.Erros.Add(new ParcelamentosImportErrorDto + { + LinhaExcel = row, + Motivo = "Item inválido ou vazio.", + Valor = itemStr + }); + continue; + } + + if (!anoRef.HasValue) + { + summary.Erros.Add(new ParcelamentosImportErrorDto + { + LinhaExcel = row, + Motivo = "AnoRef inválido ou vazio.", + Valor = GetCellString(ws, row, 3) + }); + continue; + } + + var qtParcelas = GetCellString(ws, row, colQtParcelas); + ParseParcelas(qtParcelas, out var parcelaAtual, out var totalParcelas); + + var parcelamento = new ParcelamentoLine + { + AnoRef = anoRef, + Item = item, + Linha = string.IsNullOrWhiteSpace(linhaValue) ? null : linhaValue.Trim(), + Cliente = NormalizeText(GetCellString(ws, row, colCliente)), + QtParcelas = string.IsNullOrWhiteSpace(qtParcelas) ? null : qtParcelas.Trim(), + ParcelaAtual = parcelaAtual, + TotalParcelas = totalParcelas, + ValorCheio = TryDecimal(GetCellString(ws, row, colValorCheio)), + Desconto = TryDecimal(GetCellString(ws, row, colDesconto)), + ValorComDesconto = TryDecimal(GetCellString(ws, row, colValorComDesconto)), + UpdatedAt = DateTime.UtcNow + }; + + if (existingByKey.TryGetValue((anoRef.Value, item.Value), out var existingId)) + { + var existingEntity = await _db.ParcelamentoLines + .FirstOrDefaultAsync(x => x.Id == existingId, cancellationToken); + if (existingEntity == null) + { + existingByKey.Remove((anoRef ?? 0, item.Value)); + } + else + { + existingEntity.AnoRef = parcelamento.AnoRef; + existingEntity.Item = parcelamento.Item; + existingEntity.Linha = parcelamento.Linha; + existingEntity.Cliente = parcelamento.Cliente; + existingEntity.QtParcelas = parcelamento.QtParcelas; + existingEntity.ParcelaAtual = parcelamento.ParcelaAtual; + existingEntity.TotalParcelas = parcelamento.TotalParcelas; + existingEntity.ValorCheio = parcelamento.ValorCheio; + existingEntity.Desconto = parcelamento.Desconto; + existingEntity.ValorComDesconto = parcelamento.ValorComDesconto; + existingEntity.UpdatedAt = parcelamento.UpdatedAt; + + await _db.ParcelamentoMonthValues + .Where(x => x.ParcelamentoLineId == existingEntity.Id) + .ExecuteDeleteAsync(cancellationToken); + + var monthValues = BuildMonthValues(ws, headerRowIndex, row, monthColumns, yearMap, existingEntity.Id, summary); + if (monthValues.Count > 0) + { + await _db.ParcelamentoMonthValues.AddRangeAsync(monthValues, cancellationToken); + } + + summary.Atualizados++; + continue; + } + } + + parcelamento.CreatedAt = DateTime.UtcNow; + parcelamento.MonthValues = BuildMonthValues(ws, headerRowIndex, row, monthColumns, yearMap, parcelamento.Id, summary); + summary.Inseridos++; + + await _db.ParcelamentoLines.AddAsync(parcelamento, cancellationToken); + } + + await _db.SaveChangesAsync(cancellationToken); + return summary; + } + + private static IXLWorksheet? FindWorksheet(XLWorkbook wb) + { + return wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("PARCELAMENTOS DE APARELHOS")) + ?? wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("PARCELAMENTOS")); + } + + private static int FindHeaderRow(IXLWorksheet ws) + { + var firstRow = ws.FirstRowUsed()?.RowNumber() ?? 1; + var lastRow = Math.Min(firstRow + 20, ws.LastRowUsed()?.RowNumber() ?? firstRow); + + for (int r = firstRow; r <= lastRow; r++) + { + var row = ws.Row(r); + foreach (var cell in row.CellsUsed()) + { + if (NormalizeHeader(cell.GetString()) == NormalizeHeader("LINHA")) + { + return r; + } + } + } + + return 0; + } + + private static Dictionary BuildYearMap(IXLWorksheet ws, int yearRowIndex, IXLRow headerRow) + { + var yearMap = new Dictionary(); + var lastCol = headerRow.LastCellUsed()?.Address.ColumnNumber ?? 1; + + if (yearRowIndex <= 0) + { + return yearMap; + } + + for (int col = 1; col <= lastCol; col++) + { + var yearCell = ws.Cell(yearRowIndex, col); + var yearText = yearCell.GetString(); + if (string.IsNullOrWhiteSpace(yearText)) + { + var merged = ws.MergedRanges.FirstOrDefault(r => + r.RangeAddress.FirstAddress.RowNumber == yearRowIndex && + r.RangeAddress.FirstAddress.ColumnNumber <= col && + r.RangeAddress.LastAddress.ColumnNumber >= col); + + if (merged != null) + { + yearText = merged.FirstCell().GetString(); + } + } + + if (int.TryParse(OnlyDigits(yearText), out var year)) + { + yearMap[col] = year; + } + } + + return yearMap; + } + + private static List BuildMonthColumns(IXLRow headerRow, int startCol) + { + var lastCol = headerRow.LastCellUsed()?.Address.ColumnNumber ?? startCol; + var months = new List(); + for (int col = startCol; col <= lastCol; col++) + { + var header = NormalizeHeader(headerRow.Cell(col).GetString()); + if (ToMonthNumber(header).HasValue) + { + months.Add(col); + } + } + + return months; + } + + private static List BuildMonthValues( + IXLWorksheet ws, + int headerRowIndex, + int row, + List monthColumns, + Dictionary yearMap, + Guid parcelamentoId, + ParcelamentosImportSummaryDto summary) + { + var monthValues = new List(); + foreach (var col in monthColumns) + { + if (!yearMap.TryGetValue(col, out var year)) + { + continue; + } + + var header = NormalizeHeader(ws.Cell(headerRowIndex, col).GetString()); + var monthNumber = ToMonthNumber(header); + if (!monthNumber.HasValue) + { + continue; + } + + var valueStr = ws.Cell(row, col).GetString(); + var value = TryDecimal(valueStr); + if (!value.HasValue) + { + continue; + } + + monthValues.Add(new ParcelamentoMonthValue + { + ParcelamentoLineId = parcelamentoId, + Competencia = new DateOnly(year, monthNumber.Value, 1), + Valor = value, + CreatedAt = DateTime.UtcNow + }); + summary.ParcelasInseridas++; + } + + return monthValues; + } + + private static void ParseParcelas(string? qtParcelas, out int? parcelaAtual, out int? totalParcelas) + { + parcelaAtual = null; + totalParcelas = null; + if (string.IsNullOrWhiteSpace(qtParcelas)) + { + return; + } + + var parts = qtParcelas.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (parts.Length >= 1 && int.TryParse(OnlyDigits(parts[0]), out var atual)) + { + parcelaAtual = atual; + } + + if (parts.Length >= 2 && int.TryParse(OnlyDigits(parts[1]), out var total)) + { + totalParcelas = total; + } + } + + private static Dictionary BuildHeaderMap(IXLRow headerRow) + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var cell in headerRow.CellsUsed()) + { + var k = NormalizeHeader(cell.GetString()); + if (!string.IsNullOrWhiteSpace(k) && !map.ContainsKey(k)) + map[k] = cell.Address.ColumnNumber; + } + return map; + } + + private static int GetCol(Dictionary map, string name) + => map.TryGetValue(NormalizeHeader(name), out var c) ? c : 0; + + private static int GetColAny(Dictionary map, params string[] headers) + { + foreach (var h in headers) + { + var k = NormalizeHeader(h); + if (map.TryGetValue(k, out var c)) return c; + } + return 0; + } + + private static string GetCellString(IXLWorksheet ws, int row, int col) + { + if (col <= 0) return ""; + return (ws.Cell(row, col).GetValue() ?? "").Trim(); + } + + private static string? NormalizeText(string value) + => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + + private static decimal? TryDecimal(string? s) + { + if (string.IsNullOrWhiteSpace(s)) return null; + + s = s.Replace("R$", "", StringComparison.OrdinalIgnoreCase).Trim(); + + if (decimal.TryParse(s, NumberStyles.Any, new CultureInfo("pt-BR"), out var d)) return d; + if (decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out d)) return d; + + var s2 = s.Replace(".", "").Replace(",", "."); + if (decimal.TryParse(s2, NumberStyles.Any, CultureInfo.InvariantCulture, out d)) return d; + + return null; + } + + private static int? TryNullableInt(string? s) + { + if (string.IsNullOrWhiteSpace(s)) return null; + var d = OnlyDigits(s); + if (string.IsNullOrWhiteSpace(d)) return null; + return int.TryParse(d, out var n) ? n : null; + } + + private static string OnlyDigits(string? s) + { + if (string.IsNullOrWhiteSpace(s)) return ""; + var sb = new StringBuilder(); + foreach (var c in s) if (char.IsDigit(c)) sb.Append(c); + return sb.ToString(); + } + + private static int? ToMonthNumber(string? month) + { + if (string.IsNullOrWhiteSpace(month)) return null; + return NormalizeHeader(month) switch + { + "JAN" => 1, + "FEV" => 2, + "MAR" => 3, + "ABR" => 4, + "MAI" => 5, + "JUN" => 6, + "JUL" => 7, + "AGO" => 8, + "SET" => 9, + "OUT" => 10, + "NOV" => 11, + "DEZ" => 12, + _ => null + }; + } + + private static string NormalizeHeader(string? s) + { + if (string.IsNullOrWhiteSpace(s)) return ""; + s = s.Trim().ToUpperInvariant().Normalize(NormalizationForm.FormD); + + var sb = new StringBuilder(); + foreach (var c in s) + if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark) + sb.Append(c); + + return sb.ToString() + .Normalize(NormalizationForm.FormC) + .Replace(" ", "") + .Replace("\t", "") + .Replace("\n", "") + .Replace("\r", ""); + } +} 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,