using System.Text.Json; using line_gestao_api.Data; using line_gestao_api.Dtos; using line_gestao_api.Models; using line_gestao_api.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Globalization; namespace line_gestao_api.Controllers; [ApiController] [Route("api/historico")] [Authorize(Roles = "sysadmin,gestor,financeiro")] public class HistoricoController : ControllerBase { private static readonly HashSet LineRelatedEntities = new(StringComparer.OrdinalIgnoreCase) { nameof(MobileLine), nameof(MuregLine), nameof(TrocaNumeroLine), nameof(VigenciaLine), nameof(ParcelamentoLine) }; private readonly AppDbContext _db; public HistoricoController(AppDbContext db) { _db = db; } [HttpGet] public async Task>> GetAll( [FromQuery] string? pageName, [FromQuery] string? action, [FromQuery] string? entity, [FromQuery] string? user, [FromQuery] string? search, [FromQuery] DateTime? dateFrom, [FromQuery] DateTime? dateTo, [FromQuery] int page = 1, [FromQuery] int pageSize = 20) { page = page < 1 ? 1 : page; pageSize = pageSize < 1 ? 20 : pageSize; var q = _db.AuditLogs .AsNoTracking() .Where(x => !EF.Functions.ILike(x.RequestPath ?? "", "%import-excel%") || x.Page == AuditLogBuilder.SpreadsheetImportPageName); if (!string.IsNullOrWhiteSpace(pageName)) { var p = pageName.Trim(); q = q.Where(x => EF.Functions.ILike(x.Page, $"%{p}%")); } if (!string.IsNullOrWhiteSpace(action)) { var a = action.Trim().ToUpperInvariant(); q = q.Where(x => x.Action == a); } if (!string.IsNullOrWhiteSpace(entity)) { var e = entity.Trim(); q = q.Where(x => EF.Functions.ILike(x.EntityName, $"%{e}%")); } if (!string.IsNullOrWhiteSpace(user)) { var u = user.Trim(); q = q.Where(x => EF.Functions.ILike(x.UserName ?? "", $"%{u}%") || EF.Functions.ILike(x.UserEmail ?? "", $"%{u}%")); } if (!string.IsNullOrWhiteSpace(search)) { var s = search.Trim(); var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc); var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue; q = q.Where(x => EF.Functions.ILike(x.UserName ?? "", $"%{s}%") || EF.Functions.ILike(x.UserEmail ?? "", $"%{s}%") || EF.Functions.ILike(x.Action ?? "", $"%{s}%") || EF.Functions.ILike(x.EntityName ?? "", $"%{s}%") || EF.Functions.ILike(x.EntityLabel ?? "", $"%{s}%") || EF.Functions.ILike(x.EntityId ?? "", $"%{s}%") || EF.Functions.ILike(x.Page ?? "", $"%{s}%") || EF.Functions.ILike(x.RequestPath ?? "", $"%{s}%") || EF.Functions.ILike(x.RequestMethod ?? "", $"%{s}%") || EF.Functions.ILike(x.IpAddress ?? "", $"%{s}%") || // ChangesJson is stored as jsonb; applying ILIKE directly causes PostgreSQL 42883. (hasDateSearch && x.OccurredAtUtc >= searchDateStartUtc && x.OccurredAtUtc < searchDateEndUtc)); } if (dateFrom.HasValue) { var fromUtc = ToUtc(dateFrom.Value); q = q.Where(x => x.OccurredAtUtc >= fromUtc); } if (dateTo.HasValue) { var toUtc = ToUtc(dateTo.Value); if (dateTo.Value.TimeOfDay == TimeSpan.Zero) { toUtc = toUtc.Date.AddDays(1).AddTicks(-1); } q = q.Where(x => x.OccurredAtUtc <= toUtc); } var total = await q.CountAsync(); var items = await q .OrderByDescending(x => x.OccurredAtUtc) .ThenByDescending(x => x.Id) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return Ok(new PagedResult { Page = page, PageSize = pageSize, Total = total, Items = items.Select(log => ToDto(log)).ToList() }); } [HttpGet("linhas")] public async Task>> GetLineHistory( [FromQuery] string? line, [FromQuery] string? pageName, [FromQuery] string? action, [FromQuery] string? user, [FromQuery] string? search, [FromQuery] DateTime? dateFrom, [FromQuery] DateTime? dateTo, [FromQuery] int page = 1, [FromQuery] int pageSize = 20) { page = page < 1 ? 1 : page; pageSize = pageSize < 1 ? 20 : pageSize; var lineTerm = (line ?? string.Empty).Trim(); var normalizedLineDigits = DigitsOnly(lineTerm); if (string.IsNullOrWhiteSpace(lineTerm) && string.IsNullOrWhiteSpace(normalizedLineDigits)) { return BadRequest(new { message = "Informe uma linha para consultar o histórico." }); } var q = _db.AuditLogs .AsNoTracking() .Where(x => LineRelatedEntities.Contains(x.EntityName)) .Where(x => !EF.Functions.ILike(x.RequestPath ?? "", "%import-excel%") || x.Page == AuditLogBuilder.SpreadsheetImportPageName); if (!string.IsNullOrWhiteSpace(pageName)) { var p = pageName.Trim(); q = q.Where(x => EF.Functions.ILike(x.Page, $"%{p}%")); } if (!string.IsNullOrWhiteSpace(action)) { var a = action.Trim().ToUpperInvariant(); q = q.Where(x => x.Action == a); } if (!string.IsNullOrWhiteSpace(user)) { var u = user.Trim(); q = q.Where(x => EF.Functions.ILike(x.UserName ?? "", $"%{u}%") || EF.Functions.ILike(x.UserEmail ?? "", $"%{u}%")); } if (dateFrom.HasValue) { var fromUtc = ToUtc(dateFrom.Value); q = q.Where(x => x.OccurredAtUtc >= fromUtc); } if (dateTo.HasValue) { var toUtc = ToUtc(dateTo.Value); if (dateTo.Value.TimeOfDay == TimeSpan.Zero) { toUtc = toUtc.Date.AddDays(1).AddTicks(-1); } q = q.Where(x => x.OccurredAtUtc <= toUtc); } var candidateLogs = await q .OrderByDescending(x => x.OccurredAtUtc) .ThenByDescending(x => x.Id) .ToListAsync(); var searchTerm = (search ?? string.Empty).Trim(); var searchDigits = DigitsOnly(searchTerm); var matchedLogs = new List<(AuditLog Log, List Changes)>(); foreach (var log in candidateLogs) { var changes = ParseChanges(log.ChangesJson); if (!MatchesLine(log, changes, lineTerm, normalizedLineDigits)) { continue; } if (!string.IsNullOrWhiteSpace(searchTerm) && !MatchesSearch(log, changes, searchTerm, searchDigits)) { continue; } matchedLogs.Add((log, changes)); } var total = matchedLogs.Count; var items = matchedLogs .Skip((page - 1) * pageSize) .Take(pageSize) .Select(x => ToDto(x.Log, x.Changes)) .ToList(); return Ok(new PagedResult { Page = page, PageSize = pageSize, Total = total, Items = items }); } private static AuditLogDto ToDto(AuditLog log, List? parsedChanges = null) { return new AuditLogDto { Id = log.Id, OccurredAtUtc = log.OccurredAtUtc, Action = log.Action, Page = log.Page, EntityName = log.EntityName, EntityId = log.EntityId, EntityLabel = log.EntityLabel, UserId = log.UserId, UserName = log.UserName, UserEmail = log.UserEmail, RequestPath = log.RequestPath, RequestMethod = log.RequestMethod, IpAddress = log.IpAddress, Changes = parsedChanges ?? ParseChanges(log.ChangesJson) }; } private static List ParseChanges(string? json) { if (string.IsNullOrWhiteSpace(json)) { return new List(); } try { return JsonSerializer.Deserialize>(json) ?? new List(); } catch { return new List(); } } private static DateTime ToUtc(DateTime value) { if (value.Kind == DateTimeKind.Utc) return value; if (value.Kind == DateTimeKind.Local) return value.ToUniversalTime(); return DateTime.SpecifyKind(value, DateTimeKind.Utc); } private static bool MatchesLine( AuditLog log, List changes, string lineTerm, string normalizedLineDigits) { if (MatchesTerm(log.EntityLabel, lineTerm, normalizedLineDigits) || MatchesTerm(log.EntityId, lineTerm, normalizedLineDigits)) { return true; } foreach (var change in changes) { if (MatchesTerm(change.Field, lineTerm, normalizedLineDigits) || MatchesTerm(change.OldValue, lineTerm, normalizedLineDigits) || MatchesTerm(change.NewValue, lineTerm, normalizedLineDigits)) { return true; } } return false; } private static bool MatchesSearch( AuditLog log, List changes, string searchTerm, string searchDigits) { if (MatchesTerm(log.UserName, searchTerm, searchDigits) || MatchesTerm(log.UserEmail, searchTerm, searchDigits) || MatchesTerm(log.Action, searchTerm, searchDigits) || MatchesTerm(log.Page, searchTerm, searchDigits) || MatchesTerm(log.EntityName, searchTerm, searchDigits) || MatchesTerm(log.EntityId, searchTerm, searchDigits) || MatchesTerm(log.EntityLabel, searchTerm, searchDigits) || MatchesTerm(log.RequestMethod, searchTerm, searchDigits) || MatchesTerm(log.RequestPath, searchTerm, searchDigits) || MatchesTerm(log.IpAddress, searchTerm, searchDigits)) { return true; } foreach (var change in changes) { if (MatchesTerm(change.Field, searchTerm, searchDigits) || MatchesTerm(change.OldValue, searchTerm, searchDigits) || MatchesTerm(change.NewValue, searchTerm, searchDigits)) { return true; } } return false; } private static bool MatchesTerm(string? source, string term, string digitsTerm) { if (string.IsNullOrWhiteSpace(source)) { return false; } if (!string.IsNullOrWhiteSpace(term) && source.Contains(term, StringComparison.OrdinalIgnoreCase)) { return true; } if (string.IsNullOrWhiteSpace(digitsTerm)) { return false; } var sourceDigits = DigitsOnly(source); if (string.IsNullOrWhiteSpace(sourceDigits)) { return false; } return sourceDigits.Contains(digitsTerm, StringComparison.Ordinal); } private static string DigitsOnly(string? value) { if (string.IsNullOrWhiteSpace(value)) { return string.Empty; } var chars = value.Where(char.IsDigit).ToArray(); return chars.Length == 0 ? string.Empty : new string(chars); } private static bool TryParseSearchDateUtcStart(string value, out DateTime utcStart) { utcStart = default; if (string.IsNullOrWhiteSpace(value)) return false; var s = value.Trim(); DateTime parsed; if (DateTime.TryParse(s, new CultureInfo("pt-BR"), DateTimeStyles.None, out parsed) || DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsed)) { utcStart = DateTime.SpecifyKind(parsed.Date, DateTimeKind.Utc); return true; } return false; } }