From a3f308c8772cf2b3ecd170d1d0dd76501a4b8690 Mon Sep 17 00:00:00 2001 From: Eduardo Lopes <155753879+eduardolopesx03@users.noreply.github.com> Date: Mon, 2 Feb 2026 08:47:52 -0300 Subject: [PATCH 1/8] Add notification export and bulk read endpoints --- Controllers/NotificationsController.cs | 136 +++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/Controllers/NotificationsController.cs b/Controllers/NotificationsController.cs index d842ae2..baa4f1c 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; @@ -68,4 +70,138 @@ public class NotificationsController : ControllerBase return NoContent(); } + [HttpPatch("read-all")] + [HttpPatch("/notifications/read-all")] + public async Task MarkAllAsRead([FromQuery] string? filter) + { + var utcNow = DateTime.UtcNow; + var query = ApplyFilter(_db.Notifications, filter) + .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 = ApplyFilter(_db.Notifications.AsNoTracking(), 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() + orderby notification.ReferenciaData descending, notification.Data descending + select new NotificationExportRow( + vigencia == null ? null : vigencia.Item, + vigencia == null ? null : vigencia.Conta, + vigencia == null ? null : vigencia.Linha, + vigencia == null ? null : vigencia.Cliente, + vigencia == null ? null : vigencia.Usuario, + vigencia == null ? null : vigencia.PlanoContrato, + vigencia == null ? null : vigencia.DtEfetivacaoServico, + vigencia == null ? null : vigencia.DtTerminoFidelizacao, + vigencia == null ? null : vigencia.Total, + notification.Tipo)) + .ToListAsync(); + + using var workbook = new XLWorkbook(); + var worksheet = workbook.Worksheets.Add("Notificacoes"); + + var headers = new[] + { + "Item (ID)", + "Conta", + "Linha", + "Cliente", + "Usuário", + "Plano Contrato", + "DT. DE EFETIVAÇÃO DO SERVIÇO", + "DT. DE TÉRMINO DA FIDELIZAÇÃO", + "TOTAL", + "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.Item; + worksheet.Cell(rowIndex, 2).Value = row.Conta ?? string.Empty; + worksheet.Cell(rowIndex, 3).Value = row.Linha ?? string.Empty; + worksheet.Cell(rowIndex, 4).Value = row.Cliente ?? string.Empty; + worksheet.Cell(rowIndex, 5).Value = row.Usuario ?? string.Empty; + worksheet.Cell(rowIndex, 6).Value = row.PlanoContrato ?? string.Empty; + worksheet.Cell(rowIndex, 7).Value = row.DtEfetivacaoServico; + worksheet.Cell(rowIndex, 8).Value = row.DtTerminoFidelizacao; + worksheet.Cell(rowIndex, 9).Value = row.Total; + worksheet.Cell(rowIndex, 10).Value = row.Tipo; + } + + worksheet.Column(1).Width = 12; + worksheet.Column(2).Width = 18; + worksheet.Column(3).Width = 18; + worksheet.Column(4).Width = 22; + worksheet.Column(5).Width = 22; + worksheet.Column(6).Width = 20; + worksheet.Column(7).Width = 22; + worksheet.Column(8).Width = 24; + worksheet.Column(9).Width = 14; + worksheet.Column(10).Width = 14; + + worksheet.Column(7).Style.DateFormat.Format = "dd/MM/yyyy"; + worksheet.Column(8).Style.DateFormat.Format = "dd/MM/yyyy"; + worksheet.Column(9).Style.NumberFormat.Format = "#,##0.00"; + + 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 ApplyFilter(IQueryable query, string? filter) + { + var normalized = filter?.Trim().ToLowerInvariant(); + return normalized switch + { + "a-vencer" or "avencer" => query.Where(n => n.Tipo == "AVencer"), + "vencidas" or "vencido" => query.Where(n => n.Tipo == "Vencido"), + _ => query + }; + } + + private sealed record NotificationExportRow( + int? Item, + string? Conta, + string? Linha, + string? Cliente, + string? Usuario, + string? PlanoContrato, + DateTime? DtEfetivacaoServico, + DateTime? DtTerminoFidelizacao, + decimal? Total, + string Tipo); + } From b5c410b9cfc5faa4084b3bdb3d065bd9dc3a2432 Mon Sep 17 00:00:00 2001 From: Eduardo Lopes <155753879+eduardolopesx03@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:28:53 -0300 Subject: [PATCH 2/8] Fix notification export data and selection support --- Controllers/NotificationsController.cs | 134 ++++++++++++++----------- 1 file changed, 78 insertions(+), 56 deletions(-) diff --git a/Controllers/NotificationsController.cs b/Controllers/NotificationsController.cs index baa4f1c..ccdff7d 100644 --- a/Controllers/NotificationsController.cs +++ b/Controllers/NotificationsController.cs @@ -72,10 +72,12 @@ public class NotificationsController : ControllerBase [HttpPatch("read-all")] [HttpPatch("/notifications/read-all")] - public async Task MarkAllAsRead([FromQuery] string? filter) + public async Task MarkAllAsRead( + [FromQuery] string? filter, + [FromBody] NotificationSelectionRequest? request) { var utcNow = DateTime.UtcNow; - var query = ApplyFilter(_db.Notifications, filter) + var query = ApplySelectionAndFilter(_db.Notifications, filter, request?.NotificationIds) .Where(n => !n.Lida); await query.ExecuteUpdateAsync(updates => updates @@ -89,41 +91,53 @@ public class NotificationsController : ControllerBase [HttpGet("/notifications/export")] public async Task ExportNotifications([FromQuery] string? filter) { - var query = ApplyFilter(_db.Notifications.AsNoTracking(), 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() - orderby notification.ReferenciaData descending, notification.Data descending - select new NotificationExportRow( - vigencia == null ? null : vigencia.Item, - vigencia == null ? null : vigencia.Conta, - vigencia == null ? null : vigencia.Linha, - vigencia == null ? null : vigencia.Cliente, - vigencia == null ? null : vigencia.Usuario, - vigencia == null ? null : vigencia.PlanoContrato, - vigencia == null ? null : vigencia.DtEfetivacaoServico, - vigencia == null ? null : vigencia.DtTerminoFidelizacao, - vigencia == null ? null : vigencia.Total, - notification.Tipo)) + from notification in query + join vigencia in _db.VigenciaLines.AsNoTracking() + on notification.VigenciaLineId equals vigencia.Id into vigencias + from vigencia in vigencias.DefaultIfEmpty() + orderby notification.ReferenciaData descending, notification.Data descending + select new NotificationExportRow( + notification.Linha ?? vigencia.Linha, + notification.Cliente ?? vigencia.Cliente, + notification.Usuario ?? vigencia.Usuario, + notification.ReferenciaData ?? vigencia.DtTerminoFidelizacao, + notification.Tipo)) .ToListAsync(); using var workbook = new XLWorkbook(); var worksheet = workbook.Worksheets.Add("Notificacoes"); + var normalizedFilter = NormalizeFilter(filter); + var dateHeader = normalizedFilter switch + { + "vencidas" or "vencido" => "Data da Expiração", + "a-vencer" or "avencer" => "Data a Vencer", + _ => "Data de Referência" + }; + var headers = new[] { - "Item (ID)", - "Conta", - "Linha", + "Número da Linha", "Cliente", "Usuário", - "Plano Contrato", - "DT. DE EFETIVAÇÃO DO SERVIÇO", - "DT. DE TÉRMINO DA FIDELIZAÇÃO", - "TOTAL", + dateHeader, "Status" }; @@ -141,32 +155,20 @@ public class NotificationsController : ControllerBase { var row = rows[i]; var rowIndex = i + 2; - worksheet.Cell(rowIndex, 1).Value = row.Item; - worksheet.Cell(rowIndex, 2).Value = row.Conta ?? string.Empty; - worksheet.Cell(rowIndex, 3).Value = row.Linha ?? string.Empty; - worksheet.Cell(rowIndex, 4).Value = row.Cliente ?? string.Empty; - worksheet.Cell(rowIndex, 5).Value = row.Usuario ?? string.Empty; - worksheet.Cell(rowIndex, 6).Value = row.PlanoContrato ?? string.Empty; - worksheet.Cell(rowIndex, 7).Value = row.DtEfetivacaoServico; - worksheet.Cell(rowIndex, 8).Value = row.DtTerminoFidelizacao; - worksheet.Cell(rowIndex, 9).Value = row.Total; - worksheet.Cell(rowIndex, 10).Value = row.Tipo; + worksheet.Cell(rowIndex, 1).Value = row.Linha ?? string.Empty; + worksheet.Cell(rowIndex, 2).Value = row.Cliente ?? string.Empty; + worksheet.Cell(rowIndex, 3).Value = row.Usuario ?? string.Empty; + worksheet.Cell(rowIndex, 4).Value = row.DataReferencia; + worksheet.Cell(rowIndex, 5).Value = row.Tipo; } - worksheet.Column(1).Width = 12; - worksheet.Column(2).Width = 18; - worksheet.Column(3).Width = 18; - worksheet.Column(4).Width = 22; - worksheet.Column(5).Width = 22; - worksheet.Column(6).Width = 20; - worksheet.Column(7).Width = 22; - worksheet.Column(8).Width = 24; - worksheet.Column(9).Width = 14; - worksheet.Column(10).Width = 14; + worksheet.Column(1).Width = 18; + worksheet.Column(2).Width = 26; + worksheet.Column(3).Width = 24; + worksheet.Column(4).Width = 20; + worksheet.Column(5).Width = 14; - worksheet.Column(7).Style.DateFormat.Format = "dd/MM/yyyy"; - worksheet.Column(8).Style.DateFormat.Format = "dd/MM/yyyy"; - worksheet.Column(9).Style.NumberFormat.Format = "#,##0.00"; + worksheet.Column(4).Style.DateFormat.Format = "dd/MM/yyyy"; worksheet.Columns().AdjustToContents(); @@ -181,9 +183,24 @@ public class NotificationsController : ControllerBase 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 = filter?.Trim().ToLowerInvariant(); + var normalized = NormalizeFilter(filter); return normalized switch { "a-vencer" or "avencer" => query.Where(n => n.Tipo == "AVencer"), @@ -192,16 +209,21 @@ public class NotificationsController : ControllerBase }; } + private static string? NormalizeFilter(string? filter) + { + return filter?.Trim().ToLowerInvariant(); + } + private sealed record NotificationExportRow( - int? Item, - string? Conta, string? Linha, string? Cliente, string? Usuario, - string? PlanoContrato, - DateTime? DtEfetivacaoServico, - DateTime? DtTerminoFidelizacao, - decimal? Total, + DateTime? DataReferencia, string Tipo); + public sealed class NotificationSelectionRequest + { + public List? NotificationIds { get; set; } + } + } From 02acc181a53db8f64c2e2efc4f0e63f82e080ea6 Mon Sep 17 00:00:00 2001 From: Eduardo Lopes <155753879+eduardolopesx03@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:10:30 -0300 Subject: [PATCH 3/8] Expand notification export columns --- Controllers/NotificationsController.cs | 47 +++++++++++++++----------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/Controllers/NotificationsController.cs b/Controllers/NotificationsController.cs index ccdff7d..8acb913 100644 --- a/Controllers/NotificationsController.cs +++ b/Controllers/NotificationsController.cs @@ -114,9 +114,12 @@ public class NotificationsController : ControllerBase from vigencia in vigencias.DefaultIfEmpty() orderby notification.ReferenciaData descending, notification.Data descending select new NotificationExportRow( + vigencia.Conta, notification.Linha ?? vigencia.Linha, notification.Cliente ?? vigencia.Cliente, notification.Usuario ?? vigencia.Usuario, + vigencia.PlanoContrato, + vigencia.DtEfetivacaoServico, notification.ReferenciaData ?? vigencia.DtTerminoFidelizacao, notification.Tipo)) .ToListAsync(); @@ -125,19 +128,15 @@ public class NotificationsController : ControllerBase var worksheet = workbook.Worksheets.Add("Notificacoes"); var normalizedFilter = NormalizeFilter(filter); - var dateHeader = normalizedFilter switch - { - "vencidas" or "vencido" => "Data da Expiração", - "a-vencer" or "avencer" => "Data a Vencer", - _ => "Data de Referência" - }; - var headers = new[] { - "Número da Linha", + "CONTA", + "LINHA", "Cliente", "Usuário", - dateHeader, + "PLANO CONTRATO", + "DATA INICIO", + normalizedFilter is "vencidas" or "vencido" ? "DATA VENCIMENTO" : "DATA A VENCER", "Status" }; @@ -155,20 +154,27 @@ public class NotificationsController : ControllerBase { var row = rows[i]; var rowIndex = i + 2; - worksheet.Cell(rowIndex, 1).Value = row.Linha ?? string.Empty; - worksheet.Cell(rowIndex, 2).Value = row.Cliente ?? string.Empty; - worksheet.Cell(rowIndex, 3).Value = row.Usuario ?? string.Empty; - worksheet.Cell(rowIndex, 4).Value = row.DataReferencia; - worksheet.Cell(rowIndex, 5).Value = row.Tipo; + 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; + worksheet.Cell(rowIndex, 4).Value = row.Usuario ?? string.Empty; + 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; } worksheet.Column(1).Width = 18; - worksheet.Column(2).Width = 26; - worksheet.Column(3).Width = 24; - worksheet.Column(4).Width = 20; - worksheet.Column(5).Width = 14; + 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(4).Style.DateFormat.Format = "dd/MM/yyyy"; + worksheet.Column(6).Style.DateFormat.Format = "dd/MM/yyyy"; + worksheet.Column(7).Style.DateFormat.Format = "dd/MM/yyyy"; worksheet.Columns().AdjustToContents(); @@ -215,9 +221,12 @@ public class NotificationsController : ControllerBase } private sealed record NotificationExportRow( + string? Conta, string? Linha, string? Cliente, string? Usuario, + string? PlanoContrato, + DateTime? DataInicio, DateTime? DataReferencia, string Tipo); From 6c88c3ebfd14a7ea54937eff9f16006e7c2170ed Mon Sep 17 00:00:00 2001 From: Eduardo Lopes <155753879+eduardolopesx03@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:20:21 -0300 Subject: [PATCH 4/8] Uppercase export fields and ensure vigencia columns --- Controllers/NotificationsController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Controllers/NotificationsController.cs b/Controllers/NotificationsController.cs index 8acb913..21d6c85 100644 --- a/Controllers/NotificationsController.cs +++ b/Controllers/NotificationsController.cs @@ -156,12 +156,12 @@ public class NotificationsController : ControllerBase 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; - worksheet.Cell(rowIndex, 4).Value = row.Usuario ?? 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; + worksheet.Cell(rowIndex, 8).Value = row.Tipo.ToUpperInvariant(); } worksheet.Column(1).Width = 18; From 71ab348bc39a4806ba9468c5eae892625269e566 Mon Sep 17 00:00:00 2001 From: Eduardo Lopes <155753879+eduardolopesx03@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:37:39 -0300 Subject: [PATCH 5/8] Expose vigencia details in notifications --- Controllers/NotificationsController.cs | 45 +++++++++++++++----------- Dtos/NotificationDto.cs | 5 +++ 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/Controllers/NotificationsController.cs b/Controllers/NotificationsController.cs index 21d6c85..c211428 100644 --- a/Controllers/NotificationsController.cs +++ b/Controllers/NotificationsController.cs @@ -24,25 +24,32 @@ 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() + 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.Cliente, + Linha = notification.Linha ?? vigencia.Linha, + Conta = vigencia.Conta, + Usuario = notification.Usuario ?? vigencia.Usuario, + PlanoContrato = vigencia.PlanoContrato, + DtEfetivacaoServico = vigencia.DtEfetivacaoServico, + DtTerminoFidelizacao = vigencia.DtTerminoFidelizacao + }) .ToListAsync(); return Ok(items); 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; } } From aca7f4e74a59b2824b7d230bf377e8ec38a0f527 Mon Sep 17 00:00:00 2001 From: Eduardo Lopes <155753879+eduardolopesx03@users.noreply.github.com> Date: Mon, 2 Feb 2026 10:51:03 -0300 Subject: [PATCH 6/8] =?UTF-8?q?Corrigir=20dados=20de=20vig=C3=AAncia=20em?= =?UTF-8?q?=20notifica=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Controllers/NotificationsController.cs | 57 +++++++++++++++++++------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/Controllers/NotificationsController.cs b/Controllers/NotificationsController.cs index c211428..17536f0 100644 --- a/Controllers/NotificationsController.cs +++ b/Controllers/NotificationsController.cs @@ -29,6 +29,10 @@ public class NotificationsController : ControllerBase 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 { @@ -42,13 +46,23 @@ public class NotificationsController : ControllerBase Lida = notification.Lida, LidaEm = notification.LidaEm, VigenciaLineId = notification.VigenciaLineId, - Cliente = notification.Cliente ?? vigencia.Cliente, - Linha = notification.Linha ?? vigencia.Linha, - Conta = vigencia.Conta, - Usuario = notification.Usuario ?? vigencia.Usuario, - PlanoContrato = vigencia.PlanoContrato, - DtEfetivacaoServico = vigencia.DtEfetivacaoServico, - DtTerminoFidelizacao = vigencia.DtTerminoFidelizacao + 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(); @@ -119,15 +133,30 @@ public class NotificationsController : ControllerBase 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.Conta, - notification.Linha ?? vigencia.Linha, - notification.Cliente ?? vigencia.Cliente, - notification.Usuario ?? vigencia.Usuario, - vigencia.PlanoContrato, - vigencia.DtEfetivacaoServico, - notification.ReferenciaData ?? vigencia.DtTerminoFidelizacao, + (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(); From 2371d0fff8c6afe55248e86616ef3ee93eb43514 Mon Sep 17 00:00:00 2001 From: Eduardo Lopes <155753879+eduardolopesx03@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:06:44 -0300 Subject: [PATCH 7/8] =?UTF-8?q?Corrigir=20classifica=C3=A7=C3=A3o=20de=20n?= =?UTF-8?q?otifica=C3=A7=C3=B5es=20vencidas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../VigenciaNotificationBackgroundService.cs | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) 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, From 255f63f5467de8bdf5f919d58610c9509465301f Mon Sep 17 00:00:00 2001 From: Eduardo Lopes <155753879+eduardolopesx03@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:27:10 -0300 Subject: [PATCH 8/8] =?UTF-8?q?Adicionar=20importa=C3=A7=C3=A3o=20e=20API?= =?UTF-8?q?=20de=20resumo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Controllers/LinesController.cs | 397 ++++++++++++++++++++++++++++ Controllers/ResumoController.cs | 129 +++++++++ Data/AppDbContext.cs | 22 ++ Dtos/ResumoDtos.cs | 98 +++++++ Models/ResumoClienteEspecial.cs | 14 + Models/ResumoLineTotais.cs | 16 ++ Models/ResumoMacrophonyPlan.cs | 19 ++ Models/ResumoMacrophonyTotal.cs | 15 ++ Models/ResumoPlanoContratoResumo.cs | 18 ++ Models/ResumoPlanoContratoTotal.cs | 13 + Models/ResumoReservaLine.cs | 16 ++ Models/ResumoReservaTotal.cs | 14 + Models/ResumoVivoLineResumo.cs | 20 ++ Models/ResumoVivoLineTotal.cs | 18 ++ 14 files changed, 809 insertions(+) create mode 100644 Controllers/ResumoController.cs create mode 100644 Dtos/ResumoDtos.cs create mode 100644 Models/ResumoClienteEspecial.cs create mode 100644 Models/ResumoLineTotais.cs create mode 100644 Models/ResumoMacrophonyPlan.cs create mode 100644 Models/ResumoMacrophonyTotal.cs create mode 100644 Models/ResumoPlanoContratoResumo.cs create mode 100644 Models/ResumoPlanoContratoTotal.cs create mode 100644 Models/ResumoReservaLine.cs create mode 100644 Models/ResumoReservaTotal.cs create mode 100644 Models/ResumoVivoLineResumo.cs create mode 100644 Models/ResumoVivoLineTotal.cs 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/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/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; } +}