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/9] 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/9] 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/9] 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/9] 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/9] 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/9] =?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/9] =?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/9] =?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; } +} From 8cb0b72474034bd6c5615094b3e0367df1306b42 Mon Sep 17 00:00:00 2001 From: Eduardo Lopes <155753879+eduardolopesx03@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:41:43 -0300 Subject: [PATCH 9/9] =?UTF-8?q?Adicionar=20importa=C3=A7=C3=A3o=20e=20API?= =?UTF-8?q?=20de=20parcelamentos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Controllers/LinesController.cs | 12 +- Controllers/ParcelamentosController.cs | 128 +++++++ Data/AppDbContext.cs | 35 ++ Dtos/MobileLineDtos.cs | 1 + Dtos/ParcelamentosDtos.cs | 43 +++ Models/ParcelamentoLine.cs | 24 ++ Models/ParcelamentoMonthValue.cs | 16 + Program.cs | 1 + Services/ParcelamentosImportService.cs | 447 +++++++++++++++++++++++++ 9 files changed, 705 insertions(+), 2 deletions(-) create mode 100644 Controllers/ParcelamentosController.cs create mode 100644 Dtos/ParcelamentosDtos.cs create mode 100644 Models/ParcelamentoLine.cs create mode 100644 Models/ParcelamentoMonthValue.cs create mode 100644 Services/ParcelamentosImportService.cs diff --git a/Controllers/LinesController.cs b/Controllers/LinesController.cs index af982ec..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 @@ -651,8 +654,13 @@ namespace line_gestao_api.Controllers // ========================= 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) { 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/Data/AppDbContext.cs b/Data/AppDbContext.cs index c3f9bee..6b81d02 100644 --- a/Data/AppDbContext.cs +++ b/Data/AppDbContext.cs @@ -56,6 +56,10 @@ public class AppDbContext : IdentityDbContext 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); @@ -215,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); @@ -234,6 +267,8 @@ 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); } 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/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/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/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", ""); + } +}