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; namespace line_gestao_api.Controllers; [ApiController] [Route("api/notifications")] [Authorize] public class NotificationsController : ControllerBase { private readonly AppDbContext _db; public NotificationsController(AppDbContext db) { _db = db; } [HttpGet] [HttpGet("/notifications")] public async Task>> GetNotifications() { var items = await ( from notification in _db.Notifications.AsNoTracking() join vigencia in _db.VigenciaLines.AsNoTracking() on notification.VigenciaLineId equals vigencia.Id into vigencias from vigencia in vigencias.DefaultIfEmpty() let vigenciaByLinha = _db.VigenciaLines.AsNoTracking() .Where(v => notification.Linha != null && v.Linha == notification.Linha) .OrderByDescending(v => v.UpdatedAt) .FirstOrDefault() orderby notification.Data descending select new NotificationDto { Id = notification.Id, Tipo = notification.Tipo, Titulo = notification.Titulo, Mensagem = notification.Mensagem, Data = notification.Data, ReferenciaData = notification.ReferenciaData, DiasParaVencer = notification.DiasParaVencer, Lida = notification.Lida, LidaEm = notification.LidaEm, VigenciaLineId = notification.VigenciaLineId, Cliente = notification.Cliente ?? (vigencia != null ? vigencia.Cliente : null) ?? (vigenciaByLinha != null ? vigenciaByLinha.Cliente : null), Linha = notification.Linha ?? (vigencia != null ? vigencia.Linha : null) ?? (vigenciaByLinha != null ? vigenciaByLinha.Linha : null), Conta = (vigencia != null ? vigencia.Conta : null) ?? (vigenciaByLinha != null ? vigenciaByLinha.Conta : null), Usuario = notification.Usuario ?? (vigencia != null ? vigencia.Usuario : null) ?? (vigenciaByLinha != null ? vigenciaByLinha.Usuario : null), PlanoContrato = (vigencia != null ? vigencia.PlanoContrato : null) ?? (vigenciaByLinha != null ? vigenciaByLinha.PlanoContrato : null), DtEfetivacaoServico = (vigencia != null ? vigencia.DtEfetivacaoServico : null) ?? (vigenciaByLinha != null ? vigenciaByLinha.DtEfetivacaoServico : null), DtTerminoFidelizacao = (vigencia != null ? vigencia.DtTerminoFidelizacao : null) ?? (vigenciaByLinha != null ? vigenciaByLinha.DtTerminoFidelizacao : null) }) .ToListAsync(); var todayUtc = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc); foreach (var item in items) { ApplyEffectiveType(item, todayUtc); } return Ok(items); } [HttpPatch("{id:guid}/read")] [HttpPatch("/notifications/{id:guid}/read")] public async Task MarkAsRead(Guid id) { var notification = await _db.Notifications .FirstOrDefaultAsync(n => n.Id == id); if (notification is null) { return NotFound(); } if (!notification.Lida) { notification.Lida = true; notification.LidaEm = DateTime.UtcNow; await _db.SaveChangesAsync(); } return NoContent(); } [HttpPatch("read-all")] [HttpPatch("/notifications/read-all")] public async Task MarkAllAsRead( [FromQuery] string? filter, [FromBody] NotificationSelectionRequest? request) { var utcNow = DateTime.UtcNow; var query = ApplySelectionAndFilter(_db.Notifications, filter, request?.NotificationIds) .Where(n => !n.Lida); await query.ExecuteUpdateAsync(updates => updates .SetProperty(n => n.Lida, true) .SetProperty(n => n.LidaEm, utcNow)); return NoContent(); } [HttpGet("export")] [HttpGet("/notifications/export")] public async Task ExportNotifications([FromQuery] string? filter) { var query = ApplySelectionAndFilter(_db.Notifications.AsNoTracking(), filter, null); return await ExportNotificationsAsync(query, filter); } [HttpPost("export")] [HttpPost("/notifications/export")] public async Task ExportNotifications( [FromQuery] string? filter, [FromBody] NotificationSelectionRequest? request) { var query = ApplySelectionAndFilter(_db.Notifications.AsNoTracking(), filter, request?.NotificationIds); return await ExportNotificationsAsync(query, filter); } private async Task ExportNotificationsAsync(IQueryable query, string? filter) { var rows = await ( from notification in query join vigencia in _db.VigenciaLines.AsNoTracking() on notification.VigenciaLineId equals vigencia.Id into vigencias from vigencia in vigencias.DefaultIfEmpty() let vigenciaByLinha = _db.VigenciaLines.AsNoTracking() .Where(v => notification.Linha != null && v.Linha == notification.Linha) .OrderByDescending(v => v.UpdatedAt) .FirstOrDefault() orderby notification.ReferenciaData descending, notification.Data descending select new NotificationExportRow( (vigencia != null ? vigencia.Conta : null) ?? (vigenciaByLinha != null ? vigenciaByLinha.Conta : null), notification.Linha ?? (vigencia != null ? vigencia.Linha : null) ?? (vigenciaByLinha != null ? vigenciaByLinha.Linha : null), notification.Cliente ?? (vigencia != null ? vigencia.Cliente : null) ?? (vigenciaByLinha != null ? vigenciaByLinha.Cliente : null), notification.Usuario ?? (vigencia != null ? vigencia.Usuario : null) ?? (vigenciaByLinha != null ? vigenciaByLinha.Usuario : null), (vigencia != null ? vigencia.PlanoContrato : null) ?? (vigenciaByLinha != null ? vigenciaByLinha.PlanoContrato : null), (vigencia != null ? vigencia.DtEfetivacaoServico : null) ?? (vigenciaByLinha != null ? vigenciaByLinha.DtEfetivacaoServico : null), notification.ReferenciaData ?? (vigencia != null ? vigencia.DtTerminoFidelizacao : null) ?? (vigenciaByLinha != null ? vigenciaByLinha.DtTerminoFidelizacao : null), notification.Tipo)) .ToListAsync(); var todayUtc = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc); rows = rows .Select(r => r with { Tipo = ResolveEffectiveType(r.Tipo, r.DataReferencia, todayUtc) }) .ToList(); using var workbook = new XLWorkbook(); var worksheet = workbook.Worksheets.Add("Notificacoes"); var normalizedFilter = NormalizeFilter(filter); var headers = new[] { "CONTA", "LINHA", "Cliente", "Usuário", "PLANO CONTRATO", "DATA INICIO", normalizedFilter is "vencidas" or "vencido" ? "DATA VENCIMENTO" : "DATA A VENCER", "Status" }; for (var i = 0; i < headers.Length; i++) { worksheet.Cell(1, i + 1).Value = headers[i]; } var headerBackground = XLColor.FromHtml("#E8EEFC"); var headerText = XLColor.FromHtml("#1E3A8A"); var borderColor = XLColor.FromHtml("#DBE2EF"); var zebraBackground = XLColor.FromHtml("#F8FAFC"); var bodyText = XLColor.FromHtml("#0F172A"); var headerRange = worksheet.Range(1, 1, 1, headers.Length); headerRange.Style.Font.Bold = true; headerRange.Style.Font.FontName = "Segoe UI"; headerRange.Style.Font.FontSize = 11; headerRange.Style.Font.FontColor = headerText; headerRange.Style.Fill.BackgroundColor = headerBackground; headerRange.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; headerRange.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; for (var i = 0; i < rows.Count; i++) { var row = rows[i]; var rowIndex = i + 2; worksheet.Cell(rowIndex, 1).Value = row.Conta ?? string.Empty; worksheet.Cell(rowIndex, 2).Value = row.Linha ?? string.Empty; worksheet.Cell(rowIndex, 3).Value = (row.Cliente ?? string.Empty).ToUpperInvariant(); worksheet.Cell(rowIndex, 4).Value = (row.Usuario ?? string.Empty).ToUpperInvariant(); worksheet.Cell(rowIndex, 5).Value = row.PlanoContrato ?? string.Empty; worksheet.Cell(rowIndex, 6).Value = row.DataInicio; worksheet.Cell(rowIndex, 7).Value = row.DataReferencia; worksheet.Cell(rowIndex, 8).Value = row.Tipo.ToUpperInvariant(); var rowRange = worksheet.Range(rowIndex, 1, rowIndex, headers.Length); rowRange.Style.Fill.BackgroundColor = i % 2 == 0 ? XLColor.White : zebraBackground; rowRange.Style.Font.FontName = "Segoe UI"; rowRange.Style.Font.FontSize = 10.5; rowRange.Style.Font.FontColor = bodyText; rowRange.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center; worksheet.Cell(rowIndex, 8).Style.Font.Bold = true; worksheet.Cell(rowIndex, 8).Style.Font.FontColor = row.Tipo.Equals("Vencido", StringComparison.OrdinalIgnoreCase) ? XLColor.FromHtml("#B91C1C") : XLColor.FromHtml("#047857"); } var tableRange = worksheet.Range(1, 1, Math.Max(2, rows.Count + 1), headers.Length); tableRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin; tableRange.Style.Border.OutsideBorderColor = borderColor; tableRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin; tableRange.Style.Border.InsideBorderColor = borderColor; tableRange.SetAutoFilter(); worksheet.Column(6).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; worksheet.Column(7).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; worksheet.Column(8).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center; worksheet.Column(6).Style.DateFormat.Format = "dd/MM/yyyy"; worksheet.Column(7).Style.DateFormat.Format = "dd/MM/yyyy"; worksheet.SheetView.FreezeRows(1); worksheet.Columns(1, headers.Length).AdjustToContents(); var minWidths = new[] { 14d, 14d, 22d, 20d, 20d, 14d, 14d, 12d }; for (var i = 0; i < minWidths.Length; i++) { var col = worksheet.Column(i + 1); if (col.Width < minWidths[i]) { col.Width = minWidths[i]; } } using var stream = new MemoryStream(); workbook.SaveAs(stream); stream.Position = 0; var fileName = $"notificacoes-{DateTime.UtcNow:yyyyMMddHHmmss}.xlsx"; return File( stream.ToArray(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName); } private static IQueryable ApplySelectionAndFilter( IQueryable query, string? filter, IReadOnlyCollection? notificationIds) { query = ApplyFilter(query, filter); if (notificationIds is { Count: > 0 }) { query = query.Where(n => notificationIds.Contains(n.Id)); } return query; } private static IQueryable ApplyFilter(IQueryable query, string? filter) { var normalized = NormalizeFilter(filter); return normalized switch { "a-vencer" or "avencer" => query.Where(n => n.Tipo == "AVencer"), "vencidas" or "vencido" => query.Where(n => n.Tipo == "Vencido"), _ => query }; } private static string? NormalizeFilter(string? filter) { return filter?.Trim().ToLowerInvariant(); } private static void ApplyEffectiveType(NotificationDto item, DateTime todayUtc) { item.Tipo = ResolveEffectiveType(item.Tipo, item.DtTerminoFidelizacao ?? item.ReferenciaData, todayUtc); var effectiveDate = item.DtTerminoFidelizacao ?? item.ReferenciaData; if (!effectiveDate.HasValue) { return; } var endDateUtc = DateTime.SpecifyKind(effectiveDate.Value.Date, DateTimeKind.Utc); var daysUntil = (endDateUtc - todayUtc).Days; item.DiasParaVencer = daysUntil < 0 ? 0 : daysUntil; } private static string ResolveEffectiveType(string currentType, DateTime? referenceDate, DateTime todayUtc) { if (!referenceDate.HasValue) { return currentType; } var isKnownType = currentType.Equals("AVencer", StringComparison.OrdinalIgnoreCase) || currentType.Equals("Vencido", StringComparison.OrdinalIgnoreCase); if (!isKnownType) { return currentType; } var endDateUtc = DateTime.SpecifyKind(referenceDate.Value.Date, DateTimeKind.Utc); return endDateUtc < todayUtc ? "Vencido" : "AVencer"; } private sealed record NotificationExportRow( string? Conta, string? Linha, string? Cliente, string? Usuario, string? PlanoContrato, DateTime? DataInicio, DateTime? DataReferencia, string Tipo); public sealed class NotificationSelectionRequest { public List? NotificationIds { get; set; } } }