line-gestao-api/Controllers/HistoricoController.cs

407 lines
13 KiB
C#

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<string> 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<ActionResult<PagedResult<AuditLogDto>>> 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<AuditLogDto>
{
Page = page,
PageSize = pageSize,
Total = total,
Items = items.Select(log => ToDto(log)).ToList()
});
}
[HttpGet("linhas")]
public async Task<ActionResult<PagedResult<AuditLogDto>>> 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<AuditFieldChangeDto> 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<AuditLogDto>
{
Page = page,
PageSize = pageSize,
Total = total,
Items = items
});
}
private static AuditLogDto ToDto(AuditLog log, List<AuditFieldChangeDto>? 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<AuditFieldChangeDto> ParseChanges(string? json)
{
if (string.IsNullOrWhiteSpace(json))
{
return new List<AuditFieldChangeDto>();
}
try
{
return JsonSerializer.Deserialize<List<AuditFieldChangeDto>>(json) ?? new List<AuditFieldChangeDto>();
}
catch
{
return new List<AuditFieldChangeDto>();
}
}
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<AuditFieldChangeDto> 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<AuditFieldChangeDto> 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;
}
}