407 lines
13 KiB
C#
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;
|
|
}
|
|
}
|