Compare commits
2 Commits
d37ea0c377
...
ae378ba9cc
| Author | SHA1 | Date |
|---|---|---|
|
|
ae378ba9cc | |
|
|
6af6674468 |
|
|
@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace line_gestao_api.Controllers;
|
namespace line_gestao_api.Controllers;
|
||||||
|
|
||||||
|
|
@ -24,6 +25,14 @@ public class HistoricoController : ControllerBase
|
||||||
nameof(ParcelamentoLine)
|
nameof(ParcelamentoLine)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly HashSet<string> ChipRelatedEntities = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
nameof(MobileLine),
|
||||||
|
nameof(TrocaNumeroLine),
|
||||||
|
nameof(ChipVirgemLine),
|
||||||
|
nameof(ControleRecebidoLine)
|
||||||
|
};
|
||||||
|
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
|
|
||||||
public HistoricoController(AppDbContext db)
|
public HistoricoController(AppDbContext db)
|
||||||
|
|
@ -152,10 +161,7 @@ public class HistoricoController : ControllerBase
|
||||||
|
|
||||||
var lineTerm = (line ?? string.Empty).Trim();
|
var lineTerm = (line ?? string.Empty).Trim();
|
||||||
var normalizedLineDigits = DigitsOnly(lineTerm);
|
var normalizedLineDigits = DigitsOnly(lineTerm);
|
||||||
if (string.IsNullOrWhiteSpace(lineTerm) && string.IsNullOrWhiteSpace(normalizedLineDigits))
|
var hasLineFilter = !string.IsNullOrWhiteSpace(lineTerm) || !string.IsNullOrWhiteSpace(normalizedLineDigits);
|
||||||
{
|
|
||||||
return BadRequest(new { message = "Informe uma linha para consultar o histórico." });
|
|
||||||
}
|
|
||||||
|
|
||||||
var q = _db.AuditLogs
|
var q = _db.AuditLogs
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
|
|
@ -213,7 +219,116 @@ public class HistoricoController : ControllerBase
|
||||||
foreach (var log in candidateLogs)
|
foreach (var log in candidateLogs)
|
||||||
{
|
{
|
||||||
var changes = ParseChanges(log.ChangesJson);
|
var changes = ParseChanges(log.ChangesJson);
|
||||||
if (!MatchesLine(log, changes, lineTerm, normalizedLineDigits))
|
if (hasLineFilter && !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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("chips")]
|
||||||
|
public async Task<ActionResult<PagedResult<AuditLogDto>>> GetChipHistory(
|
||||||
|
[FromQuery] string? chip,
|
||||||
|
[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 chipTerm = (chip ?? string.Empty).Trim();
|
||||||
|
var normalizedChipDigits = DigitsOnly(chipTerm);
|
||||||
|
var hasChipFilter = !string.IsNullOrWhiteSpace(chipTerm) || !string.IsNullOrWhiteSpace(normalizedChipDigits);
|
||||||
|
|
||||||
|
var q = _db.AuditLogs
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => ChipRelatedEntities.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 (!HasChipContext(log, changes))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChipFilter && !MatchesChip(log, changes, chipTerm, normalizedChipDigits))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -348,6 +463,51 @@ public class HistoricoController : ControllerBase
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool MatchesChip(
|
||||||
|
AuditLog log,
|
||||||
|
List<AuditFieldChangeDto> changes,
|
||||||
|
string chipTerm,
|
||||||
|
string normalizedChipDigits)
|
||||||
|
{
|
||||||
|
if (MatchesTerm(log.EntityLabel, chipTerm, normalizedChipDigits) ||
|
||||||
|
MatchesTerm(log.EntityId, chipTerm, normalizedChipDigits))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var change in changes)
|
||||||
|
{
|
||||||
|
if (MatchesTerm(change.Field, chipTerm, normalizedChipDigits) ||
|
||||||
|
MatchesTerm(change.OldValue, chipTerm, normalizedChipDigits) ||
|
||||||
|
MatchesTerm(change.NewValue, chipTerm, normalizedChipDigits))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasChipContext(AuditLog log, List<AuditFieldChangeDto> changes)
|
||||||
|
{
|
||||||
|
if ((log.EntityName ?? string.Empty).Equals(nameof(ChipVirgemLine), StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
(log.Page ?? string.Empty).Contains("chip", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var change in changes)
|
||||||
|
{
|
||||||
|
var normalizedField = NormalizeField(change.Field);
|
||||||
|
if (normalizedField is "chip" or "iccid" or "numerodochip")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool MatchesTerm(string? source, string term, string digitsTerm)
|
private static bool MatchesTerm(string? source, string term, string digitsTerm)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(source))
|
if (string.IsNullOrWhiteSpace(source))
|
||||||
|
|
@ -375,6 +535,21 @@ public class HistoricoController : ControllerBase
|
||||||
return sourceDigits.Contains(digitsTerm, StringComparison.Ordinal);
|
return sourceDigits.Contains(digitsTerm, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string NormalizeField(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new string(value
|
||||||
|
.Normalize(NormalizationForm.FormD)
|
||||||
|
.Where(ch => CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark)
|
||||||
|
.Where(char.IsLetterOrDigit)
|
||||||
|
.Select(char.ToLowerInvariant)
|
||||||
|
.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
private static string DigitsOnly(string? value)
|
private static string DigitsOnly(string? value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ namespace line_gestao_api.Controllers
|
||||||
private readonly IVigenciaNotificationSyncService _vigenciaNotificationSyncService;
|
private readonly IVigenciaNotificationSyncService _vigenciaNotificationSyncService;
|
||||||
private readonly ParcelamentosImportService _parcelamentosImportService;
|
private readonly ParcelamentosImportService _parcelamentosImportService;
|
||||||
private readonly SpreadsheetImportAuditService _spreadsheetImportAuditService;
|
private readonly SpreadsheetImportAuditService _spreadsheetImportAuditService;
|
||||||
|
private readonly DashboardKpiSnapshotService _dashboardKpiSnapshotService;
|
||||||
private readonly string _aparelhoAttachmentsRootPath;
|
private readonly string _aparelhoAttachmentsRootPath;
|
||||||
private static readonly FileExtensionContentTypeProvider FileContentTypeProvider = new();
|
private static readonly FileExtensionContentTypeProvider FileContentTypeProvider = new();
|
||||||
|
|
||||||
|
|
@ -71,6 +72,7 @@ namespace line_gestao_api.Controllers
|
||||||
IVigenciaNotificationSyncService vigenciaNotificationSyncService,
|
IVigenciaNotificationSyncService vigenciaNotificationSyncService,
|
||||||
ParcelamentosImportService parcelamentosImportService,
|
ParcelamentosImportService parcelamentosImportService,
|
||||||
SpreadsheetImportAuditService spreadsheetImportAuditService,
|
SpreadsheetImportAuditService spreadsheetImportAuditService,
|
||||||
|
DashboardKpiSnapshotService dashboardKpiSnapshotService,
|
||||||
IWebHostEnvironment webHostEnvironment)
|
IWebHostEnvironment webHostEnvironment)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
|
|
@ -78,6 +80,7 @@ namespace line_gestao_api.Controllers
|
||||||
_vigenciaNotificationSyncService = vigenciaNotificationSyncService;
|
_vigenciaNotificationSyncService = vigenciaNotificationSyncService;
|
||||||
_parcelamentosImportService = parcelamentosImportService;
|
_parcelamentosImportService = parcelamentosImportService;
|
||||||
_spreadsheetImportAuditService = spreadsheetImportAuditService;
|
_spreadsheetImportAuditService = spreadsheetImportAuditService;
|
||||||
|
_dashboardKpiSnapshotService = dashboardKpiSnapshotService;
|
||||||
_aparelhoAttachmentsRootPath = Path.Combine(webHostEnvironment.ContentRootPath, "uploads", "aparelhos");
|
_aparelhoAttachmentsRootPath = Path.Combine(webHostEnvironment.ContentRootPath, "uploads", "aparelhos");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,6 +101,7 @@ namespace line_gestao_api.Controllers
|
||||||
[HttpGet("groups")]
|
[HttpGet("groups")]
|
||||||
public async Task<ActionResult<PagedResult<ClientGroupDto>>> GetClientGroups(
|
public async Task<ActionResult<PagedResult<ClientGroupDto>>> GetClientGroups(
|
||||||
[FromQuery] string? skil,
|
[FromQuery] string? skil,
|
||||||
|
[FromQuery] string? reservaMode,
|
||||||
[FromQuery] string? search,
|
[FromQuery] string? search,
|
||||||
[FromQuery] string? additionalMode,
|
[FromQuery] string? additionalMode,
|
||||||
[FromQuery] string? additionalServices,
|
[FromQuery] string? additionalServices,
|
||||||
|
|
@ -117,10 +121,7 @@ namespace line_gestao_api.Controllers
|
||||||
if (sSkil.Equals("RESERVA", StringComparison.OrdinalIgnoreCase))
|
if (sSkil.Equals("RESERVA", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
reservaFilter = true;
|
reservaFilter = true;
|
||||||
query = query.Where(x =>
|
query = ApplyReservaContextFilter(query);
|
||||||
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
|
||||||
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") ||
|
|
||||||
EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
query = query.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%"));
|
query = query.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%"));
|
||||||
|
|
@ -135,37 +136,7 @@ namespace line_gestao_api.Controllers
|
||||||
|
|
||||||
if (reservaFilter)
|
if (reservaFilter)
|
||||||
{
|
{
|
||||||
var userDataClientByLine = BuildUserDataClientByLineQuery();
|
var reservaRows = ApplyReservaMode(BuildReservaLineProjection(query), reservaMode);
|
||||||
var userDataClientByItem = BuildUserDataClientByItemQuery();
|
|
||||||
var reservaRows =
|
|
||||||
from line in query
|
|
||||||
join udLine in userDataClientByLine on line.Linha equals udLine.Linha into udLineJoin
|
|
||||||
from udLine in udLineJoin.DefaultIfEmpty()
|
|
||||||
join udItem in userDataClientByItem on line.Item equals udItem.Item into udItemJoin
|
|
||||||
from udItem in udItemJoin.DefaultIfEmpty()
|
|
||||||
let clienteOriginal = (line.Cliente ?? "").Trim()
|
|
||||||
let skilOriginal = (line.Skil ?? "").Trim()
|
|
||||||
let clientePorLinha = (udLine.Cliente ?? "").Trim()
|
|
||||||
let clientePorItem = (udItem.Cliente ?? "").Trim()
|
|
||||||
let reservaEstrita = EF.Functions.ILike(clienteOriginal, "RESERVA") &&
|
|
||||||
EF.Functions.ILike(skilOriginal, "RESERVA")
|
|
||||||
let clienteEfetivo = reservaEstrita
|
|
||||||
? "RESERVA"
|
|
||||||
: (!string.IsNullOrEmpty(clienteOriginal) &&
|
|
||||||
!EF.Functions.ILike(clienteOriginal, "RESERVA"))
|
|
||||||
? clienteOriginal
|
|
||||||
: (!string.IsNullOrEmpty(clientePorLinha) &&
|
|
||||||
!EF.Functions.ILike(clientePorLinha, "RESERVA"))
|
|
||||||
? clientePorLinha
|
|
||||||
: (!string.IsNullOrEmpty(clientePorItem) &&
|
|
||||||
!EF.Functions.ILike(clientePorItem, "RESERVA"))
|
|
||||||
? clientePorItem
|
|
||||||
: ""
|
|
||||||
select new
|
|
||||||
{
|
|
||||||
Cliente = string.IsNullOrEmpty(clienteEfetivo) ? "RESERVA" : clienteEfetivo,
|
|
||||||
line.Status
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
if (!string.IsNullOrWhiteSpace(search))
|
||||||
{
|
{
|
||||||
|
|
@ -211,7 +182,8 @@ namespace line_gestao_api.Controllers
|
||||||
|
|
||||||
var orderedGroupedQuery = reservaFilter
|
var orderedGroupedQuery = reservaFilter
|
||||||
? groupedQuery
|
? groupedQuery
|
||||||
.OrderByDescending(x => EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"))
|
.OrderByDescending(x => EF.Functions.ILike((x.Cliente ?? "").Trim(), "ESTOQUE"))
|
||||||
|
.ThenByDescending(x => EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"))
|
||||||
.ThenBy(x => x.Cliente)
|
.ThenBy(x => x.Cliente)
|
||||||
: groupedQuery.OrderBy(x => x.Cliente);
|
: groupedQuery.OrderBy(x => x.Cliente);
|
||||||
|
|
||||||
|
|
@ -235,6 +207,7 @@ namespace line_gestao_api.Controllers
|
||||||
[HttpGet("clients")]
|
[HttpGet("clients")]
|
||||||
public async Task<ActionResult<List<string>>> GetClients(
|
public async Task<ActionResult<List<string>>> GetClients(
|
||||||
[FromQuery] string? skil,
|
[FromQuery] string? skil,
|
||||||
|
[FromQuery] string? reservaMode,
|
||||||
[FromQuery] string? additionalMode,
|
[FromQuery] string? additionalMode,
|
||||||
[FromQuery] string? additionalServices)
|
[FromQuery] string? additionalServices)
|
||||||
{
|
{
|
||||||
|
|
@ -247,10 +220,7 @@ namespace line_gestao_api.Controllers
|
||||||
if (sSkil.Equals("RESERVA", StringComparison.OrdinalIgnoreCase))
|
if (sSkil.Equals("RESERVA", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
reservaFilter = true;
|
reservaFilter = true;
|
||||||
query = query.Where(x =>
|
query = ApplyReservaContextFilter(query);
|
||||||
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
|
||||||
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") ||
|
|
||||||
EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
query = query.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%"));
|
query = query.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%"));
|
||||||
|
|
@ -264,37 +234,11 @@ namespace line_gestao_api.Controllers
|
||||||
List<string> clients;
|
List<string> clients;
|
||||||
if (reservaFilter)
|
if (reservaFilter)
|
||||||
{
|
{
|
||||||
var userDataClientByLine = BuildUserDataClientByLineQuery();
|
clients = await ApplyReservaMode(BuildReservaLineProjection(query), reservaMode)
|
||||||
var userDataClientByItem = BuildUserDataClientByItemQuery();
|
.Select(x => x.Cliente)
|
||||||
clients = await (
|
|
||||||
from line in query
|
|
||||||
join udLine in userDataClientByLine on line.Linha equals udLine.Linha into udLineJoin
|
|
||||||
from udLine in udLineJoin.DefaultIfEmpty()
|
|
||||||
join udItem in userDataClientByItem on line.Item equals udItem.Item into udItemJoin
|
|
||||||
from udItem in udItemJoin.DefaultIfEmpty()
|
|
||||||
let clienteOriginal = (line.Cliente ?? "").Trim()
|
|
||||||
let skilOriginal = (line.Skil ?? "").Trim()
|
|
||||||
let clientePorLinha = (udLine.Cliente ?? "").Trim()
|
|
||||||
let clientePorItem = (udItem.Cliente ?? "").Trim()
|
|
||||||
let reservaEstrita = EF.Functions.ILike(clienteOriginal, "RESERVA") &&
|
|
||||||
EF.Functions.ILike(skilOriginal, "RESERVA")
|
|
||||||
let clienteEfetivo = reservaEstrita
|
|
||||||
? "RESERVA"
|
|
||||||
: (!string.IsNullOrEmpty(clienteOriginal) &&
|
|
||||||
!EF.Functions.ILike(clienteOriginal, "RESERVA"))
|
|
||||||
? clienteOriginal
|
|
||||||
: (!string.IsNullOrEmpty(clientePorLinha) &&
|
|
||||||
!EF.Functions.ILike(clientePorLinha, "RESERVA"))
|
|
||||||
? clientePorLinha
|
|
||||||
: (!string.IsNullOrEmpty(clientePorItem) &&
|
|
||||||
!EF.Functions.ILike(clientePorItem, "RESERVA"))
|
|
||||||
? clientePorItem
|
|
||||||
: ""
|
|
||||||
let clienteExibicao = string.IsNullOrEmpty(clienteEfetivo) ? "RESERVA" : clienteEfetivo
|
|
||||||
select clienteExibicao
|
|
||||||
)
|
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.OrderByDescending(x => EF.Functions.ILike((x ?? "").Trim(), "RESERVA"))
|
.OrderByDescending(x => EF.Functions.ILike((x ?? "").Trim(), "ESTOQUE"))
|
||||||
|
.ThenByDescending(x => EF.Functions.ILike((x ?? "").Trim(), "RESERVA"))
|
||||||
.ThenBy(x => x)
|
.ThenBy(x => x)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
@ -449,6 +393,7 @@ namespace line_gestao_api.Controllers
|
||||||
public async Task<ActionResult<PagedResult<MobileLineListDto>>> GetAll(
|
public async Task<ActionResult<PagedResult<MobileLineListDto>>> GetAll(
|
||||||
[FromQuery] string? search,
|
[FromQuery] string? search,
|
||||||
[FromQuery] string? skil,
|
[FromQuery] string? skil,
|
||||||
|
[FromQuery] string? reservaMode,
|
||||||
[FromQuery] string? client,
|
[FromQuery] string? client,
|
||||||
[FromQuery] string? operadora,
|
[FromQuery] string? operadora,
|
||||||
[FromQuery] string? additionalMode,
|
[FromQuery] string? additionalMode,
|
||||||
|
|
@ -470,10 +415,7 @@ namespace line_gestao_api.Controllers
|
||||||
if (sSkil.Equals("RESERVA", StringComparison.OrdinalIgnoreCase))
|
if (sSkil.Equals("RESERVA", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
reservaFilter = true;
|
reservaFilter = true;
|
||||||
q = q.Where(x =>
|
q = ApplyReservaContextFilter(q);
|
||||||
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
|
||||||
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") ||
|
|
||||||
EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
q = q.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%"));
|
q = q.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%"));
|
||||||
|
|
@ -493,61 +435,7 @@ namespace line_gestao_api.Controllers
|
||||||
|
|
||||||
if (reservaFilter)
|
if (reservaFilter)
|
||||||
{
|
{
|
||||||
var userDataClientByLine = BuildUserDataClientByLineQuery();
|
var rq = ApplyReservaMode(BuildReservaLineProjection(q), reservaMode);
|
||||||
var userDataClientByItem = BuildUserDataClientByItemQuery();
|
|
||||||
var rq =
|
|
||||||
from line in q
|
|
||||||
join udLine in userDataClientByLine on line.Linha equals udLine.Linha into udLineJoin
|
|
||||||
from udLine in udLineJoin.DefaultIfEmpty()
|
|
||||||
join udItem in userDataClientByItem on line.Item equals udItem.Item into udItemJoin
|
|
||||||
from udItem in udItemJoin.DefaultIfEmpty()
|
|
||||||
let clienteOriginal = (line.Cliente ?? "").Trim()
|
|
||||||
let skilOriginal = (line.Skil ?? "").Trim()
|
|
||||||
let clientePorLinha = (udLine.Cliente ?? "").Trim()
|
|
||||||
let clientePorItem = (udItem.Cliente ?? "").Trim()
|
|
||||||
let reservaEstrita = EF.Functions.ILike(clienteOriginal, "RESERVA") &&
|
|
||||||
EF.Functions.ILike(skilOriginal, "RESERVA")
|
|
||||||
let clienteEfetivo = reservaEstrita
|
|
||||||
? "RESERVA"
|
|
||||||
: (!string.IsNullOrEmpty(clienteOriginal) &&
|
|
||||||
!EF.Functions.ILike(clienteOriginal, "RESERVA"))
|
|
||||||
? clienteOriginal
|
|
||||||
: (!string.IsNullOrEmpty(clientePorLinha) &&
|
|
||||||
!EF.Functions.ILike(clientePorLinha, "RESERVA"))
|
|
||||||
? clientePorLinha
|
|
||||||
: (!string.IsNullOrEmpty(clientePorItem) &&
|
|
||||||
!EF.Functions.ILike(clientePorItem, "RESERVA"))
|
|
||||||
? clientePorItem
|
|
||||||
: ""
|
|
||||||
let clienteExibicao = string.IsNullOrEmpty(clienteEfetivo) ? "RESERVA" : clienteEfetivo
|
|
||||||
select new
|
|
||||||
{
|
|
||||||
line.Id,
|
|
||||||
line.Item,
|
|
||||||
line.Conta,
|
|
||||||
line.Linha,
|
|
||||||
line.Chip,
|
|
||||||
Cliente = clienteExibicao,
|
|
||||||
line.Usuario,
|
|
||||||
line.CentroDeCustos,
|
|
||||||
SetorNome = line.Setor != null ? line.Setor!.Nome : null,
|
|
||||||
AparelhoNome = line.Aparelho != null ? line.Aparelho!.Nome : null,
|
|
||||||
AparelhoCor = line.Aparelho != null ? line.Aparelho!.Cor : null,
|
|
||||||
line.PlanoContrato,
|
|
||||||
line.Status,
|
|
||||||
line.Skil,
|
|
||||||
line.Modalidade,
|
|
||||||
line.VencConta,
|
|
||||||
line.FranquiaVivo,
|
|
||||||
line.FranquiaLine,
|
|
||||||
line.GestaoVozDados,
|
|
||||||
line.Skeelo,
|
|
||||||
line.VivoNewsPlus,
|
|
||||||
line.VivoTravelMundo,
|
|
||||||
line.VivoSync,
|
|
||||||
line.VivoGestaoDispositivo,
|
|
||||||
line.TipoDeChip
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(client))
|
if (!string.IsNullOrWhiteSpace(client))
|
||||||
rq = rq.Where(x => EF.Functions.ILike(x.Cliente ?? "", client.Trim()));
|
rq = rq.Where(x => EF.Functions.ILike(x.Cliente ?? "", client.Trim()));
|
||||||
|
|
@ -572,10 +460,12 @@ namespace line_gestao_api.Controllers
|
||||||
"linha" => desc ? rq.OrderByDescending(x => x.Linha ?? "").ThenBy(x => x.Item) : rq.OrderBy(x => x.Linha ?? "").ThenBy(x => x.Item),
|
"linha" => desc ? rq.OrderByDescending(x => x.Linha ?? "").ThenBy(x => x.Item) : rq.OrderBy(x => x.Linha ?? "").ThenBy(x => x.Item),
|
||||||
"chip" => desc ? rq.OrderByDescending(x => x.Chip ?? "").ThenBy(x => x.Item) : rq.OrderBy(x => x.Chip ?? "").ThenBy(x => x.Item),
|
"chip" => desc ? rq.OrderByDescending(x => x.Chip ?? "").ThenBy(x => x.Item) : rq.OrderBy(x => x.Chip ?? "").ThenBy(x => x.Item),
|
||||||
"cliente" => desc
|
"cliente" => desc
|
||||||
? rq.OrderByDescending(x => EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"))
|
? rq.OrderByDescending(x => EF.Functions.ILike((x.Cliente ?? "").Trim(), "ESTOQUE"))
|
||||||
|
.ThenByDescending(x => EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"))
|
||||||
.ThenByDescending(x => x.Cliente ?? "")
|
.ThenByDescending(x => x.Cliente ?? "")
|
||||||
.ThenBy(x => x.Item)
|
.ThenBy(x => x.Item)
|
||||||
: rq.OrderByDescending(x => EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"))
|
: rq.OrderByDescending(x => EF.Functions.ILike((x.Cliente ?? "").Trim(), "ESTOQUE"))
|
||||||
|
.ThenByDescending(x => EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"))
|
||||||
.ThenBy(x => x.Cliente ?? "")
|
.ThenBy(x => x.Cliente ?? "")
|
||||||
.ThenBy(x => x.Item),
|
.ThenBy(x => x.Item),
|
||||||
"usuario" => desc ? rq.OrderByDescending(x => x.Usuario ?? "").ThenBy(x => x.Item) : rq.OrderBy(x => x.Usuario ?? "").ThenBy(x => x.Item),
|
"usuario" => desc ? rq.OrderByDescending(x => x.Usuario ?? "").ThenBy(x => x.Item) : rq.OrderBy(x => x.Usuario ?? "").ThenBy(x => x.Item),
|
||||||
|
|
@ -1984,6 +1874,7 @@ namespace line_gestao_api.Controllers
|
||||||
}
|
}
|
||||||
|
|
||||||
await AddSpreadsheetImportHistoryAsync(file.FileName, imported, parcelamentosSummary);
|
await AddSpreadsheetImportHistoryAsync(file.FileName, imported, parcelamentosSummary);
|
||||||
|
await _dashboardKpiSnapshotService.CaptureAfterSpreadsheetImportAsync(User, HttpContext);
|
||||||
await tx.CommitAsync();
|
await tx.CommitAsync();
|
||||||
return Ok(new ImportResultDto { Imported = imported, Parcelamentos = parcelamentosSummary });
|
return Ok(new ImportResultDto { Imported = imported, Parcelamentos = parcelamentosSummary });
|
||||||
}
|
}
|
||||||
|
|
@ -5443,6 +5334,36 @@ namespace line_gestao_api.Controllers
|
||||||
public string Cliente { get; set; } = "";
|
public string Cliente { get; set; } = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class ReservaLineProjection
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public int Item { get; set; }
|
||||||
|
public string? Conta { get; set; }
|
||||||
|
public string? Linha { get; set; }
|
||||||
|
public string? Chip { get; set; }
|
||||||
|
public string Cliente { get; set; } = "";
|
||||||
|
public string? Usuario { get; set; }
|
||||||
|
public string? CentroDeCustos { get; set; }
|
||||||
|
public string? SetorNome { get; set; }
|
||||||
|
public string? AparelhoNome { get; set; }
|
||||||
|
public string? AparelhoCor { get; set; }
|
||||||
|
public string? PlanoContrato { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public string? Skil { get; set; }
|
||||||
|
public string? Modalidade { get; set; }
|
||||||
|
public string? VencConta { get; set; }
|
||||||
|
public decimal? FranquiaVivo { get; set; }
|
||||||
|
public decimal? FranquiaLine { get; set; }
|
||||||
|
public decimal? GestaoVozDados { get; set; }
|
||||||
|
public decimal? Skeelo { get; set; }
|
||||||
|
public decimal? VivoNewsPlus { get; set; }
|
||||||
|
public decimal? VivoTravelMundo { get; set; }
|
||||||
|
public decimal? VivoSync { get; set; }
|
||||||
|
public decimal? VivoGestaoDispositivo { get; set; }
|
||||||
|
public string? TipoDeChip { get; set; }
|
||||||
|
public bool IsStock { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
private IQueryable<UserDataClientByLine> BuildUserDataClientByLineQuery()
|
private IQueryable<UserDataClientByLine> BuildUserDataClientByLineQuery()
|
||||||
{
|
{
|
||||||
return _db.UserDatas
|
return _db.UserDatas
|
||||||
|
|
@ -5473,12 +5394,104 @@ namespace line_gestao_api.Controllers
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IQueryable<ReservaLineProjection> BuildReservaLineProjection(IQueryable<MobileLine> query)
|
||||||
|
{
|
||||||
|
var userDataClientByLine = BuildUserDataClientByLineQuery();
|
||||||
|
var userDataClientByItem = BuildUserDataClientByItemQuery();
|
||||||
|
|
||||||
|
return
|
||||||
|
from line in query
|
||||||
|
join udLine in userDataClientByLine on line.Linha equals udLine.Linha into udLineJoin
|
||||||
|
from udLine in udLineJoin.DefaultIfEmpty()
|
||||||
|
join udItem in userDataClientByItem on line.Item equals udItem.Item into udItemJoin
|
||||||
|
from udItem in udItemJoin.DefaultIfEmpty()
|
||||||
|
let clienteOriginal = (line.Cliente ?? "").Trim()
|
||||||
|
let clientePorLinha = (udLine.Cliente ?? "").Trim()
|
||||||
|
let clientePorItem = (udItem.Cliente ?? "").Trim()
|
||||||
|
let isStock = clienteOriginal.ToUpper() == "RESERVA"
|
||||||
|
let clienteEfetivo = isStock
|
||||||
|
? "ESTOQUE"
|
||||||
|
: (!string.IsNullOrEmpty(clienteOriginal) &&
|
||||||
|
clienteOriginal.ToUpper() != "RESERVA")
|
||||||
|
? clienteOriginal
|
||||||
|
: (!string.IsNullOrEmpty(clientePorLinha) &&
|
||||||
|
clientePorLinha.ToUpper() != "RESERVA")
|
||||||
|
? clientePorLinha
|
||||||
|
: (!string.IsNullOrEmpty(clientePorItem) &&
|
||||||
|
clientePorItem.ToUpper() != "RESERVA")
|
||||||
|
? clientePorItem
|
||||||
|
: ""
|
||||||
|
select new ReservaLineProjection
|
||||||
|
{
|
||||||
|
Id = line.Id,
|
||||||
|
Item = line.Item,
|
||||||
|
Conta = line.Conta,
|
||||||
|
Linha = line.Linha,
|
||||||
|
Chip = line.Chip,
|
||||||
|
Cliente = string.IsNullOrEmpty(clienteEfetivo) ? (isStock ? "ESTOQUE" : "RESERVA") : clienteEfetivo,
|
||||||
|
Usuario = line.Usuario,
|
||||||
|
CentroDeCustos = line.CentroDeCustos,
|
||||||
|
SetorNome = line.Setor != null ? line.Setor!.Nome : null,
|
||||||
|
AparelhoNome = line.Aparelho != null ? line.Aparelho!.Nome : null,
|
||||||
|
AparelhoCor = line.Aparelho != null ? line.Aparelho!.Cor : null,
|
||||||
|
PlanoContrato = line.PlanoContrato,
|
||||||
|
Status = line.Status,
|
||||||
|
Skil = line.Skil,
|
||||||
|
Modalidade = line.Modalidade,
|
||||||
|
VencConta = line.VencConta,
|
||||||
|
FranquiaVivo = line.FranquiaVivo,
|
||||||
|
FranquiaLine = line.FranquiaLine,
|
||||||
|
GestaoVozDados = line.GestaoVozDados,
|
||||||
|
Skeelo = line.Skeelo,
|
||||||
|
VivoNewsPlus = line.VivoNewsPlus,
|
||||||
|
VivoTravelMundo = line.VivoTravelMundo,
|
||||||
|
VivoSync = line.VivoSync,
|
||||||
|
VivoGestaoDispositivo = line.VivoGestaoDispositivo,
|
||||||
|
TipoDeChip = line.TipoDeChip,
|
||||||
|
IsStock = isStock
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<MobileLine> ApplyReservaContextFilter(IQueryable<MobileLine> query)
|
||||||
|
{
|
||||||
|
return query.Where(x =>
|
||||||
|
(x.Usuario ?? "").Trim().ToUpper() == "RESERVA" ||
|
||||||
|
(x.Skil ?? "").Trim().ToUpper() == "RESERVA" ||
|
||||||
|
(x.Cliente ?? "").Trim().ToUpper() == "RESERVA");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<ReservaLineProjection> ApplyReservaMode(
|
||||||
|
IQueryable<ReservaLineProjection> query,
|
||||||
|
string? reservaMode)
|
||||||
|
{
|
||||||
|
var mode = NormalizeReservaMode(reservaMode);
|
||||||
|
return mode switch
|
||||||
|
{
|
||||||
|
"stock" => query.Where(x => x.IsStock),
|
||||||
|
"assigned" => query.Where(x =>
|
||||||
|
!x.IsStock &&
|
||||||
|
(x.Cliente ?? "").Trim().ToUpper() != "RESERVA"),
|
||||||
|
_ => query
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeReservaMode(string? reservaMode)
|
||||||
|
{
|
||||||
|
var token = (reservaMode ?? "").Trim().ToLowerInvariant();
|
||||||
|
return token switch
|
||||||
|
{
|
||||||
|
"stock" or "estoque" => "stock",
|
||||||
|
"assigned" or "reservas" or "reserva" => "assigned",
|
||||||
|
_ => "all"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static IQueryable<MobileLine> ExcludeReservaContext(IQueryable<MobileLine> query)
|
private static IQueryable<MobileLine> ExcludeReservaContext(IQueryable<MobileLine> query)
|
||||||
{
|
{
|
||||||
return query.Where(x =>
|
return query.Where(x =>
|
||||||
!EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") &&
|
(x.Usuario ?? "").Trim().ToUpper() != "RESERVA" &&
|
||||||
!EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") &&
|
(x.Skil ?? "").Trim().ToUpper() != "RESERVA" &&
|
||||||
!EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
(x.Cliente ?? "").Trim().ToUpper() != "RESERVA");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IQueryable<MobileLine> ApplyAdditionalFilters(
|
private static IQueryable<MobileLine> ApplyAdditionalFilters(
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,14 @@ namespace line_gestao_api.Controllers
|
||||||
public class RelatoriosController : ControllerBase
|
public class RelatoriosController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
|
private readonly DashboardKpiSnapshotService _dashboardKpiSnapshotService;
|
||||||
|
|
||||||
public RelatoriosController(AppDbContext db)
|
public RelatoriosController(
|
||||||
|
AppDbContext db,
|
||||||
|
DashboardKpiSnapshotService dashboardKpiSnapshotService)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
|
_dashboardKpiSnapshotService = dashboardKpiSnapshotService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("dashboard")]
|
[HttpGet("dashboard")]
|
||||||
|
|
@ -226,6 +230,7 @@ namespace line_gestao_api.Controllers
|
||||||
UserDataComCpf = userDataComCpf,
|
UserDataComCpf = userDataComCpf,
|
||||||
UserDataComEmail = userDataComEmail
|
UserDataComEmail = userDataComEmail
|
||||||
},
|
},
|
||||||
|
KpiTrends = await _dashboardKpiSnapshotService.GetTrendMapAsync(operadora),
|
||||||
TopClientes = topClientes,
|
TopClientes = topClientes,
|
||||||
SerieMuregUltimos12Meses = serieMureg12,
|
SerieMuregUltimos12Meses = serieMureg12,
|
||||||
SerieTrocaUltimos12Meses = serieTroca12,
|
SerieTrocaUltimos12Meses = serieTroca12,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ namespace line_gestao_api.Dtos
|
||||||
public class RelatoriosDashboardDto
|
public class RelatoriosDashboardDto
|
||||||
{
|
{
|
||||||
public DashboardKpisDto Kpis { get; set; } = new();
|
public DashboardKpisDto Kpis { get; set; } = new();
|
||||||
|
public Dictionary<string, string> KpiTrends { get; set; } = new();
|
||||||
public List<TopClienteDto> TopClientes { get; set; } = new();
|
public List<TopClienteDto> TopClientes { get; set; } = new();
|
||||||
public List<SerieMesDto> SerieMuregUltimos12Meses { get; set; } = new();
|
public List<SerieMesDto> SerieMuregUltimos12Meses { get; set; } = new();
|
||||||
public List<SerieMesDto> SerieTrocaUltimos12Meses { get; set; } = new();
|
public List<SerieMesDto> SerieTrocaUltimos12Meses { get; set; } = new();
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ builder.Services.AddScoped<ISystemAuditService, SystemAuditService>();
|
||||||
builder.Services.AddScoped<IVigenciaNotificationSyncService, VigenciaNotificationSyncService>();
|
builder.Services.AddScoped<IVigenciaNotificationSyncService, VigenciaNotificationSyncService>();
|
||||||
builder.Services.AddScoped<ParcelamentosImportService>();
|
builder.Services.AddScoped<ParcelamentosImportService>();
|
||||||
builder.Services.AddScoped<GeralDashboardInsightsService>();
|
builder.Services.AddScoped<GeralDashboardInsightsService>();
|
||||||
|
builder.Services.AddScoped<DashboardKpiSnapshotService>();
|
||||||
builder.Services.AddScoped<GeralSpreadsheetTemplateService>();
|
builder.Services.AddScoped<GeralSpreadsheetTemplateService>();
|
||||||
builder.Services.AddScoped<SpreadsheetImportAuditService>();
|
builder.Services.AddScoped<SpreadsheetImportAuditService>();
|
||||||
builder.Services.AddScoped<MveCsvParserService>();
|
builder.Services.AddScoped<MveCsvParserService>();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,380 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using line_gestao_api.Data;
|
||||||
|
using line_gestao_api.Models;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Services;
|
||||||
|
|
||||||
|
public sealed class DashboardKpiSnapshotService
|
||||||
|
{
|
||||||
|
private const string SnapshotEntityName = "DashboardKpiSnapshot";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
private readonly ITenantProvider _tenantProvider;
|
||||||
|
|
||||||
|
public DashboardKpiSnapshotService(AppDbContext db, ITenantProvider tenantProvider)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_tenantProvider = tenantProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CaptureAfterSpreadsheetImportAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
HttpContext httpContext,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var tenantId = _tenantProvider.TenantId;
|
||||||
|
if (!tenantId.HasValue)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = TryParseUserId(user);
|
||||||
|
var userName = user.FindFirst("name")?.Value
|
||||||
|
?? user.FindFirst(ClaimTypes.Name)?.Value
|
||||||
|
?? user.Identity?.Name
|
||||||
|
?? "USUARIO";
|
||||||
|
var userEmail = user.FindFirst(ClaimTypes.Email)?.Value
|
||||||
|
?? user.FindFirst("email")?.Value;
|
||||||
|
|
||||||
|
foreach (var scope in EnumerateScopes())
|
||||||
|
{
|
||||||
|
var snapshot = await BuildSnapshotAsync(scope.Operadora, cancellationToken);
|
||||||
|
|
||||||
|
_db.AuditLogs.Add(new AuditLog
|
||||||
|
{
|
||||||
|
TenantId = tenantId.Value,
|
||||||
|
ActorUserId = userId,
|
||||||
|
ActorTenantId = tenantId.Value,
|
||||||
|
TargetTenantId = tenantId.Value,
|
||||||
|
OccurredAtUtc = DateTime.UtcNow,
|
||||||
|
UserId = userId,
|
||||||
|
UserName = userName,
|
||||||
|
UserEmail = userEmail,
|
||||||
|
Action = "SNAPSHOT",
|
||||||
|
Page = AuditLogBuilder.SpreadsheetImportPageName,
|
||||||
|
EntityName = SnapshotEntityName,
|
||||||
|
EntityId = snapshot.Scope,
|
||||||
|
EntityLabel = $"Snapshot Dashboard {snapshot.Scope}",
|
||||||
|
ChangesJson = "[]",
|
||||||
|
MetadataJson = JsonSerializer.Serialize(snapshot, JsonOptions),
|
||||||
|
RequestPath = httpContext.Request.Path.Value,
|
||||||
|
RequestMethod = httpContext.Request.Method,
|
||||||
|
IpAddress = httpContext.Connection.RemoteIpAddress?.ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<string, string>> GetTrendMapAsync(
|
||||||
|
string? operadora = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var scope = NormalizeScope(operadora);
|
||||||
|
var logs = await _db.AuditLogs
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
x.Page == AuditLogBuilder.SpreadsheetImportPageName &&
|
||||||
|
x.EntityName == SnapshotEntityName &&
|
||||||
|
x.EntityId == scope)
|
||||||
|
.OrderByDescending(x => x.OccurredAtUtc)
|
||||||
|
.Take(8)
|
||||||
|
.Select(x => new { x.OccurredAtUtc, x.MetadataJson })
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (logs.Count < 2)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
var latest = DeserializeSnapshot(logs[0].MetadataJson);
|
||||||
|
if (latest == null)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
var baselineThreshold = logs[0].OccurredAtUtc.AddHours(-24);
|
||||||
|
var previous = logs
|
||||||
|
.Skip(1)
|
||||||
|
.Select(x => DeserializeSnapshot(x.MetadataJson))
|
||||||
|
.FirstOrDefault(x => x != null && x.CapturedAtUtc >= baselineThreshold);
|
||||||
|
|
||||||
|
if (previous == null)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys = latest.Metrics.Keys
|
||||||
|
.Concat(previous.Metrics.Keys)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var key in keys)
|
||||||
|
{
|
||||||
|
latest.Metrics.TryGetValue(key, out var currentValue);
|
||||||
|
previous.Metrics.TryGetValue(key, out var previousValue);
|
||||||
|
result[key] = currentValue > previousValue
|
||||||
|
? "up"
|
||||||
|
: currentValue < previousValue
|
||||||
|
? "down"
|
||||||
|
: "stable";
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<DashboardKpiSnapshotPayload> BuildSnapshotAsync(
|
||||||
|
string? operadora,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var todayUtcStart = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc);
|
||||||
|
var tomorrowUtcStart = todayUtcStart.AddDays(1);
|
||||||
|
var last30UtcStart = todayUtcStart.AddDays(-30);
|
||||||
|
var limit30ExclusiveUtcStart = todayUtcStart.AddDays(31);
|
||||||
|
|
||||||
|
var qLines = OperadoraContaResolver.ApplyOperadoraFilter(_db.MobileLines.AsNoTracking(), operadora);
|
||||||
|
var qReserva = ApplyReservaContextFilter(qLines);
|
||||||
|
var qOperacionais = ExcludeReservaContext(qLines);
|
||||||
|
var qOperacionaisWithClient = qOperacionais.Where(x => x.Cliente != null && x.Cliente != "");
|
||||||
|
var qNonReserva = ExcludeReservaContext(qLines);
|
||||||
|
|
||||||
|
var totalLinhas = await qLines.CountAsync(cancellationToken);
|
||||||
|
var linhasAtivas = await qOperacionais.CountAsync(x =>
|
||||||
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%ativo%"), cancellationToken);
|
||||||
|
var linhasBloqueadas = await qOperacionaisWithClient.CountAsync(x =>
|
||||||
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") ||
|
||||||
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") ||
|
||||||
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%"), cancellationToken);
|
||||||
|
var linhasReserva = await qReserva.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var franquiaVivoTotal = await qLines.SumAsync(x => x.FranquiaVivo ?? 0m, cancellationToken);
|
||||||
|
var franquiaLineTotal = await qLines.SumAsync(x => x.FranquiaLine ?? 0m, cancellationToken);
|
||||||
|
|
||||||
|
var vigVencidos = await _db.VigenciaLines.AsNoTracking()
|
||||||
|
.CountAsync(x =>
|
||||||
|
x.DtTerminoFidelizacao != null &&
|
||||||
|
x.DtTerminoFidelizacao.Value < todayUtcStart, cancellationToken);
|
||||||
|
|
||||||
|
var vigencia30 = await _db.VigenciaLines.AsNoTracking()
|
||||||
|
.CountAsync(x =>
|
||||||
|
x.DtTerminoFidelizacao != null &&
|
||||||
|
x.DtTerminoFidelizacao.Value >= todayUtcStart &&
|
||||||
|
x.DtTerminoFidelizacao.Value < limit30ExclusiveUtcStart, cancellationToken);
|
||||||
|
|
||||||
|
var mureg30 = await _db.MuregLines.AsNoTracking()
|
||||||
|
.CountAsync(x =>
|
||||||
|
x.DataDaMureg != null &&
|
||||||
|
x.DataDaMureg.Value >= last30UtcStart &&
|
||||||
|
x.DataDaMureg.Value < tomorrowUtcStart, cancellationToken);
|
||||||
|
|
||||||
|
var troca30 = await _db.TrocaNumeroLines.AsNoTracking()
|
||||||
|
.CountAsync(x =>
|
||||||
|
x.DataTroca != null &&
|
||||||
|
x.DataTroca.Value >= last30UtcStart &&
|
||||||
|
x.DataTroca.Value < tomorrowUtcStart, cancellationToken);
|
||||||
|
|
||||||
|
var qLineItems = qLines
|
||||||
|
.Where(x => x.Item > 0)
|
||||||
|
.Select(x => x.Item);
|
||||||
|
var qLineNumbers = qLines
|
||||||
|
.Where(x => x.Linha != null && x.Linha != "")
|
||||||
|
.Select(x => x.Linha!);
|
||||||
|
|
||||||
|
var qUserData = _db.UserDatas.AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
(x.Item > 0 && qLineItems.Contains(x.Item)) ||
|
||||||
|
(x.Linha != null && x.Linha != "" && qLineNumbers.Contains(x.Linha)));
|
||||||
|
|
||||||
|
var cadastrosTotal = await qUserData.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var travelCom = await qLines.CountAsync(x => (x.VivoTravelMundo ?? 0m) > 0m, cancellationToken);
|
||||||
|
var adicionalPago = await qLines.CountAsync(x =>
|
||||||
|
(x.GestaoVozDados ?? 0m) > 0m ||
|
||||||
|
(x.Skeelo ?? 0m) > 0m ||
|
||||||
|
(x.VivoNewsPlus ?? 0m) > 0m ||
|
||||||
|
(x.VivoTravelMundo ?? 0m) > 0m ||
|
||||||
|
(x.VivoSync ?? 0m) > 0m ||
|
||||||
|
(x.VivoGestaoDispositivo ?? 0m) > 0m, cancellationToken);
|
||||||
|
|
||||||
|
var planosContratados = await qNonReserva
|
||||||
|
.Where(x => x.PlanoContrato != null && x.PlanoContrato != "")
|
||||||
|
.Select(x => (x.PlanoContrato ?? "").Trim())
|
||||||
|
.Distinct()
|
||||||
|
.CountAsync(cancellationToken);
|
||||||
|
|
||||||
|
var usuariosRaw = await qNonReserva
|
||||||
|
.Where(x => x.Usuario != null && x.Usuario != "")
|
||||||
|
.Select(x => new
|
||||||
|
{
|
||||||
|
Usuario = (x.Usuario ?? "").Trim(),
|
||||||
|
Status = (x.Status ?? "").Trim()
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var usuariosComLinha = usuariosRaw
|
||||||
|
.Where(x => ShouldIncludeUsuario(x.Usuario, x.Status))
|
||||||
|
.Select(x => x.Usuario)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Count();
|
||||||
|
|
||||||
|
return new DashboardKpiSnapshotPayload
|
||||||
|
{
|
||||||
|
Scope = NormalizeScope(operadora),
|
||||||
|
CapturedAtUtc = DateTime.UtcNow,
|
||||||
|
Metrics = new Dictionary<string, decimal>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["linhas_total"] = totalLinhas,
|
||||||
|
["linhas_ativas"] = linhasAtivas,
|
||||||
|
["linhas_bloqueadas"] = linhasBloqueadas,
|
||||||
|
["linhas_reserva"] = linhasReserva,
|
||||||
|
["franquia_vivo_total"] = franquiaVivoTotal,
|
||||||
|
["franquia_line_total"] = franquiaLineTotal,
|
||||||
|
["vig_vencidos"] = vigVencidos,
|
||||||
|
["vig_30"] = vigencia30,
|
||||||
|
["mureg_30"] = mureg30,
|
||||||
|
["troca_30"] = troca30,
|
||||||
|
["cadastros_total"] = cadastrosTotal,
|
||||||
|
["travel_com"] = travelCom,
|
||||||
|
["adicional_pago"] = adicionalPago,
|
||||||
|
["planos_contratados"] = planosContratados,
|
||||||
|
["usuarios_com_linha"] = usuariosComLinha
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Guid? TryParseUserId(ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
var claim = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
return Guid.TryParse(claim, out var userId) ? userId : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<(string Scope, string? Operadora)> EnumerateScopes()
|
||||||
|
{
|
||||||
|
yield return ("TODOS", null);
|
||||||
|
yield return ("VIVO", "VIVO");
|
||||||
|
yield return ("CLARO", "CLARO");
|
||||||
|
yield return ("TIM", "TIM");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeScope(string? operadora)
|
||||||
|
{
|
||||||
|
var token = NormalizeToken(operadora);
|
||||||
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
{
|
||||||
|
return "TODOS";
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DashboardKpiSnapshotPayload? DeserializeSnapshot(string? metadataJson)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(metadataJson))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<DashboardKpiSnapshotPayload>(metadataJson, JsonOptions);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<MobileLine> ApplyReservaContextFilter(IQueryable<MobileLine> query)
|
||||||
|
{
|
||||||
|
return query.Where(x =>
|
||||||
|
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
||||||
|
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") ||
|
||||||
|
EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<MobileLine> ExcludeReservaContext(IQueryable<MobileLine> query)
|
||||||
|
{
|
||||||
|
return query.Where(x =>
|
||||||
|
!EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") &&
|
||||||
|
!EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") &&
|
||||||
|
!EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldIncludeUsuario(string usuario, string status)
|
||||||
|
{
|
||||||
|
var usuarioKey = NormalizeToken(usuario);
|
||||||
|
if (string.IsNullOrWhiteSpace(usuarioKey))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalidUserTokens = new[]
|
||||||
|
{
|
||||||
|
"SEMUSUARIO",
|
||||||
|
"AGUARDANDOUSUARIO",
|
||||||
|
"AGUARDANDO",
|
||||||
|
"BLOQUEAR",
|
||||||
|
"BLOQUEAD",
|
||||||
|
"BLOQUEADO",
|
||||||
|
"BLOQUEIO",
|
||||||
|
"BLOQ120",
|
||||||
|
"RESERVA",
|
||||||
|
"NAOATRIBUIDO",
|
||||||
|
"PENDENTE",
|
||||||
|
"COBRANCA",
|
||||||
|
"FATURAMENTO",
|
||||||
|
"FINANCEIRO",
|
||||||
|
"BACKOFFICE",
|
||||||
|
"ADMINISTRATIVO",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (invalidUserTokens.Any(usuarioKey.Contains))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusKey = NormalizeToken(status);
|
||||||
|
var blockedStatusTokens = new[] { "BLOQUE", "PERDA", "ROUBO", "SUSPEN", "CANCEL", "AGUARD" };
|
||||||
|
return !blockedStatusTokens.Any(statusKey.Contains);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeToken(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = value.Normalize(NormalizationForm.FormD);
|
||||||
|
var builder = new StringBuilder(normalized.Length);
|
||||||
|
foreach (var ch in normalized)
|
||||||
|
{
|
||||||
|
var category = CharUnicodeInfo.GetUnicodeCategory(ch);
|
||||||
|
if (category == UnicodeCategory.NonSpacingMark)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char.IsLetterOrDigit(ch))
|
||||||
|
{
|
||||||
|
builder.Append(char.ToUpperInvariant(ch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DashboardKpiSnapshotPayload
|
||||||
|
{
|
||||||
|
public string Scope { get; set; } = "TODOS";
|
||||||
|
public DateTime CapturedAtUtc { get; set; }
|
||||||
|
public Dictionary<string, decimal> Metrics { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -79,6 +79,34 @@ internal static class MveAuditNormalization
|
||||||
return string.IsNullOrWhiteSpace(digits) ? null : digits;
|
return string.IsNullOrWhiteSpace(digits) ? null : digits;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string NormalizePhoneDigits(string? value)
|
||||||
|
{
|
||||||
|
var digits = OnlyDigits(value);
|
||||||
|
if (string.IsNullOrWhiteSpace(digits))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (digits.StartsWith("55", StringComparison.Ordinal) && digits.Length is 12 or 13)
|
||||||
|
{
|
||||||
|
digits = digits[2..];
|
||||||
|
}
|
||||||
|
|
||||||
|
return digits.Length > 11 ? digits[^11..] : digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ExtractPhoneDdd(string? value)
|
||||||
|
{
|
||||||
|
var digits = NormalizePhoneDigits(value);
|
||||||
|
return digits.Length >= 10 ? digits[..2] : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ExtractPhoneLocalNumber(string? value)
|
||||||
|
{
|
||||||
|
var digits = NormalizePhoneDigits(value);
|
||||||
|
return digits.Length >= 10 ? digits[2..] : digits;
|
||||||
|
}
|
||||||
|
|
||||||
public static string NormalizeComparableText(string? value)
|
public static string NormalizeComparableText(string? value)
|
||||||
{
|
{
|
||||||
var cleaned = CleanTextValue(value);
|
var cleaned = CleanTextValue(value);
|
||||||
|
|
@ -207,6 +235,7 @@ internal static class MveAuditNormalization
|
||||||
{
|
{
|
||||||
var text when text.Contains("ATIVO", StringComparison.Ordinal) || text == "ATIVA" => "ATIVO",
|
var text when text.Contains("ATIVO", StringComparison.Ordinal) || text == "ATIVA" => "ATIVO",
|
||||||
var text when text.Contains("BLOQUEIO PARCIAL", StringComparison.Ordinal) => "BLOQUEIO_PERDA_ROUBO",
|
var text when text.Contains("BLOQUEIO PARCIAL", StringComparison.Ordinal) => "BLOQUEIO_PERDA_ROUBO",
|
||||||
|
var text when text.Contains("PRE ATIVACAO", StringComparison.Ordinal) => "BLOQUEIO_PERDA_ROUBO",
|
||||||
var text when text.Contains("CANCEL", StringComparison.Ordinal) => "BLOQUEIO_120_DIAS",
|
var text when text.Contains("CANCEL", StringComparison.Ordinal) => "BLOQUEIO_120_DIAS",
|
||||||
var text when text.Contains("SUSPENS", StringComparison.Ordinal) => "SUSPENSO",
|
var text when text.Contains("SUSPENS", StringComparison.Ordinal) => "SUSPENSO",
|
||||||
var text when text.Contains("PENDENTE", StringComparison.Ordinal) &&
|
var text when text.Contains("PENDENTE", StringComparison.Ordinal) &&
|
||||||
|
|
@ -250,6 +279,7 @@ internal static class MveAuditNormalization
|
||||||
var text when text.Contains("ATIVO", StringComparison.Ordinal) || text == "ATIVA" => "ATIVO",
|
var text when text.Contains("ATIVO", StringComparison.Ordinal) || text == "ATIVA" => "ATIVO",
|
||||||
var text when text.Contains("PERDA", StringComparison.Ordinal) || text.Contains("ROUBO", StringComparison.Ordinal) => "BLOQUEIO_PERDA_ROUBO",
|
var text when text.Contains("PERDA", StringComparison.Ordinal) || text.Contains("ROUBO", StringComparison.Ordinal) => "BLOQUEIO_PERDA_ROUBO",
|
||||||
var text when text.Contains("BLOQUEIO PARCIAL", StringComparison.Ordinal) => "BLOQUEIO_PERDA_ROUBO",
|
var text when text.Contains("BLOQUEIO PARCIAL", StringComparison.Ordinal) => "BLOQUEIO_PERDA_ROUBO",
|
||||||
|
var text when text.Contains("PRE ATIVACAO", StringComparison.Ordinal) => "BLOQUEIO_PERDA_ROUBO",
|
||||||
var text when text.Contains("BLOQUEIO", StringComparison.Ordinal) && text.Contains("120", StringComparison.Ordinal) => "BLOQUEIO_120_DIAS",
|
var text when text.Contains("BLOQUEIO", StringComparison.Ordinal) && text.Contains("120", StringComparison.Ordinal) => "BLOQUEIO_120_DIAS",
|
||||||
var text when text.Contains("CANCEL", StringComparison.Ordinal) => "CANCELADO",
|
var text when text.Contains("CANCEL", StringComparison.Ordinal) => "CANCELADO",
|
||||||
var text when text.Contains("SUSPENS", StringComparison.Ordinal) => "SUSPENSO",
|
var text when text.Contains("SUSPENS", StringComparison.Ordinal) => "SUSPENSO",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@ public sealed class MveAuditSchemaBootstrapper
|
||||||
|
|
||||||
public async Task EnsureSchemaAsync(CancellationToken cancellationToken = default)
|
public async Task EnsureSchemaAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (!_db.Database.IsRelational())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await _db.Database.ExecuteSqlRawAsync(
|
await _db.Database.ExecuteSqlRawAsync(
|
||||||
"""
|
"""
|
||||||
ALTER TABLE "Aparelhos"
|
ALTER TABLE "Aparelhos"
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,64 @@ public sealed class MveAuditService
|
||||||
.Where(x => lineIds.Contains(x.Id))
|
.Where(x => lineIds.Contains(x.Id))
|
||||||
.ToDictionaryAsync(x => x.Id, cancellationToken);
|
.ToDictionaryAsync(x => x.Id, cancellationToken);
|
||||||
|
|
||||||
|
var reportSnapshotsByIssueId = selectedIssues
|
||||||
|
.ToDictionary(x => x.Id, x => DeserializeSnapshot(x.ReportSnapshotJson));
|
||||||
|
|
||||||
|
var lineNumbers = linesById.Values
|
||||||
|
.Select(x => MveAuditNormalization.NullIfEmptyDigits(x.Linha))
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.Cast<string>()
|
||||||
|
.Concat(reportSnapshotsByIssueId.Values
|
||||||
|
.Select(x => MveAuditNormalization.NullIfEmptyDigits(x?.NumeroLinha))
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.Cast<string>())
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var items = linesById.Values
|
||||||
|
.Where(x => x.Item > 0)
|
||||||
|
.Select(x => x.Item)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var vigencias = await _db.VigenciaLines
|
||||||
|
.Where(x => (x.Item > 0 && items.Contains(x.Item)) || (x.Linha != null && lineNumbers.Contains(x.Linha)))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var userDatas = await _db.UserDatas
|
||||||
|
.Where(x => (x.Item > 0 && items.Contains(x.Item)) || (x.Linha != null && lineNumbers.Contains(x.Linha)))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var vigenciaByLine = vigencias
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.Linha))
|
||||||
|
.GroupBy(x => x.Linha!, StringComparer.Ordinal)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.OrderByDescending(x => x.UpdatedAt).ThenByDescending(x => x.CreatedAt).First(),
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var vigenciaByItem = vigencias
|
||||||
|
.Where(x => x.Item > 0)
|
||||||
|
.GroupBy(x => x.Item)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.OrderByDescending(x => x.UpdatedAt).ThenByDescending(x => x.CreatedAt).First());
|
||||||
|
|
||||||
|
var userDataByLine = userDatas
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.Linha))
|
||||||
|
.GroupBy(x => x.Linha!, StringComparer.Ordinal)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.OrderByDescending(x => x.UpdatedAt).ThenByDescending(x => x.CreatedAt).First(),
|
||||||
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var userDataByItem = userDatas
|
||||||
|
.Where(x => x.Item > 0)
|
||||||
|
.GroupBy(x => x.Item)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.OrderByDescending(x => x.UpdatedAt).ThenByDescending(x => x.CreatedAt).First());
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var updatedLineIds = new HashSet<Guid>();
|
var updatedLineIds = new HashSet<Guid>();
|
||||||
var updatedFields = 0;
|
var updatedFields = 0;
|
||||||
|
|
@ -210,7 +268,12 @@ public sealed class MveAuditService
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var reportSnapshot = DeserializeSnapshot(issue.ReportSnapshotJson);
|
if (!reportSnapshotsByIssueId.TryGetValue(issue.Id, out var reportSnapshot))
|
||||||
|
{
|
||||||
|
skippedIssues++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (reportSnapshot == null)
|
if (reportSnapshot == null)
|
||||||
{
|
{
|
||||||
skippedIssues++;
|
skippedIssues++;
|
||||||
|
|
@ -220,7 +283,69 @@ public sealed class MveAuditService
|
||||||
var differences = DeserializeDifferences(issue.DifferencesJson);
|
var differences = DeserializeDifferences(issue.DifferencesJson);
|
||||||
var lineChanged = false;
|
var lineChanged = false;
|
||||||
|
|
||||||
foreach (var difference in differences.Where(x => x.Syncable && x.FieldKey == "status"))
|
var hasLineDifference = differences.Any(x => x.Syncable && x.FieldKey == "line");
|
||||||
|
var hasChipDifference = differences.Any(x => x.Syncable && x.FieldKey == "chip");
|
||||||
|
var hasStatusDifference = differences.Any(x => x.Syncable && x.FieldKey == "status");
|
||||||
|
|
||||||
|
var previousLine = MveAuditNormalization.NullIfEmptyDigits(line.Linha);
|
||||||
|
var nextLine = hasLineDifference ? MveAuditNormalization.NullIfEmptyDigits(reportSnapshot.NumeroLinha) : previousLine;
|
||||||
|
if (hasLineDifference)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(nextLine))
|
||||||
|
{
|
||||||
|
skippedIssues++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasConflict = await _db.MobileLines
|
||||||
|
.AsNoTracking()
|
||||||
|
.AnyAsync(
|
||||||
|
x => x.TenantId == line.TenantId &&
|
||||||
|
x.Id != line.Id &&
|
||||||
|
x.Linha == nextLine,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (hasConflict)
|
||||||
|
{
|
||||||
|
skippedIssues++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasLineDifference && SetString(line.Linha, nextLine, value => line.Linha = value))
|
||||||
|
{
|
||||||
|
lineChanged = true;
|
||||||
|
updatedFields++;
|
||||||
|
|
||||||
|
SyncLinkedLineRecords(
|
||||||
|
line,
|
||||||
|
previousLine,
|
||||||
|
nextLine,
|
||||||
|
vigenciaByLine,
|
||||||
|
vigenciaByItem,
|
||||||
|
userDataByLine,
|
||||||
|
userDataByItem,
|
||||||
|
now);
|
||||||
|
|
||||||
|
AddTrocaNumeroHistory(
|
||||||
|
line,
|
||||||
|
previousLine,
|
||||||
|
nextLine,
|
||||||
|
MveAuditNormalization.NullIfEmptyDigits(reportSnapshot.Chip) ?? MveAuditNormalization.NullIfEmptyDigits(line.Chip),
|
||||||
|
now);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChipDifference)
|
||||||
|
{
|
||||||
|
var nextChip = MveAuditNormalization.NullIfEmptyDigits(reportSnapshot.Chip);
|
||||||
|
if (SetString(line.Chip, nextChip, value => line.Chip = value))
|
||||||
|
{
|
||||||
|
lineChanged = true;
|
||||||
|
updatedFields++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasStatusDifference)
|
||||||
{
|
{
|
||||||
var systemStatus = MveAuditNormalization.NormalizeStatusForSystem(reportSnapshot.StatusLinha);
|
var systemStatus = MveAuditNormalization.NormalizeStatusForSystem(reportSnapshot.StatusLinha);
|
||||||
if (SetString(line.Status, systemStatus, value => line.Status = value))
|
if (SetString(line.Status, systemStatus, value => line.Status = value))
|
||||||
|
|
@ -441,9 +566,89 @@ public sealed class MveAuditService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SyncLinkedLineRecords(
|
||||||
|
MobileLine line,
|
||||||
|
string? previousLine,
|
||||||
|
string? nextLine,
|
||||||
|
IDictionary<string, VigenciaLine> vigenciaByLine,
|
||||||
|
IDictionary<int, VigenciaLine> vigenciaByItem,
|
||||||
|
IDictionary<string, UserData> userDataByLine,
|
||||||
|
IDictionary<int, UserData> userDataByItem,
|
||||||
|
DateTime now)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(nextLine))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lookupLine = !string.IsNullOrWhiteSpace(previousLine) ? previousLine : nextLine;
|
||||||
|
|
||||||
|
var vigencia = ResolveVigencia(line, lookupLine, vigenciaByLine, vigenciaByItem);
|
||||||
|
if (vigencia != null && SetString(vigencia.Linha, nextLine, value => vigencia.Linha = value))
|
||||||
|
{
|
||||||
|
vigencia.UpdatedAt = now;
|
||||||
|
RefreshLineLookup(vigenciaByLine, previousLine, nextLine, vigencia);
|
||||||
|
}
|
||||||
|
|
||||||
|
var userData = ResolveUserData(line, lookupLine, userDataByLine, userDataByItem);
|
||||||
|
if (userData != null && SetString(userData.Linha, nextLine, value => userData.Linha = value))
|
||||||
|
{
|
||||||
|
userData.UpdatedAt = now;
|
||||||
|
RefreshLineLookup(userDataByLine, previousLine, nextLine, userData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddTrocaNumeroHistory(
|
||||||
|
MobileLine line,
|
||||||
|
string? previousLine,
|
||||||
|
string? nextLine,
|
||||||
|
string? chip,
|
||||||
|
DateTime now)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(previousLine) ||
|
||||||
|
string.IsNullOrWhiteSpace(nextLine) ||
|
||||||
|
string.Equals(previousLine, nextLine, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_db.TrocaNumeroLines.Add(new TrocaNumeroLine
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
TenantId = line.TenantId,
|
||||||
|
Item = line.Item,
|
||||||
|
LinhaAntiga = previousLine,
|
||||||
|
LinhaNova = nextLine,
|
||||||
|
ICCID = MveAuditNormalization.NullIfEmptyDigits(chip),
|
||||||
|
DataTroca = now,
|
||||||
|
Motivo = "Auditoria MVE",
|
||||||
|
Observacao = "Linha atualizada automaticamente a partir do relatório MVE.",
|
||||||
|
CreatedAt = now,
|
||||||
|
UpdatedAt = now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RefreshLineLookup<T>(
|
||||||
|
IDictionary<string, T> lookup,
|
||||||
|
string? previousLine,
|
||||||
|
string? nextLine,
|
||||||
|
T entity)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(previousLine))
|
||||||
|
{
|
||||||
|
lookup.Remove(previousLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(nextLine))
|
||||||
|
{
|
||||||
|
lookup[nextLine] = entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private VigenciaLine? ResolveVigencia(
|
private VigenciaLine? ResolveVigencia(
|
||||||
MobileLine line,
|
MobileLine line,
|
||||||
string numeroLinha,
|
string? numeroLinha,
|
||||||
IDictionary<string, VigenciaLine> vigenciaByLine,
|
IDictionary<string, VigenciaLine> vigenciaByLine,
|
||||||
IDictionary<int, VigenciaLine> vigenciaByItem)
|
IDictionary<int, VigenciaLine> vigenciaByItem)
|
||||||
{
|
{
|
||||||
|
|
@ -462,7 +667,7 @@ public sealed class MveAuditService
|
||||||
|
|
||||||
private UserData? ResolveUserData(
|
private UserData? ResolveUserData(
|
||||||
MobileLine line,
|
MobileLine line,
|
||||||
string numeroLinha,
|
string? numeroLinha,
|
||||||
IDictionary<string, UserData> userDataByLine,
|
IDictionary<string, UserData> userDataByLine,
|
||||||
IDictionary<int, UserData> userDataByItem)
|
IDictionary<int, UserData> userDataByItem)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -119,8 +119,10 @@ public sealed class MveReconciliationService
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var blockedKeys = new HashSet<string>(duplicateReportKeys, StringComparer.Ordinal);
|
var blockedSystemKeys = new HashSet<string>(duplicateSystemKeys, StringComparer.Ordinal);
|
||||||
blockedKeys.UnionWith(duplicateSystemKeys);
|
var blockedReportKeys = new HashSet<string>(duplicateReportKeys, StringComparer.Ordinal);
|
||||||
|
var matchedSystemLineIds = new HashSet<Guid>();
|
||||||
|
var matchedReportRows = new HashSet<int>();
|
||||||
|
|
||||||
var allKeys = reportByNumber.Keys
|
var allKeys = reportByNumber.Keys
|
||||||
.Concat(systemByNumber.Keys)
|
.Concat(systemByNumber.Keys)
|
||||||
|
|
@ -131,7 +133,7 @@ public sealed class MveReconciliationService
|
||||||
|
|
||||||
foreach (var key in allKeys)
|
foreach (var key in allKeys)
|
||||||
{
|
{
|
||||||
if (blockedKeys.Contains(key))
|
if (blockedSystemKeys.Contains(key) || blockedReportKeys.Contains(key))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -141,74 +143,102 @@ public sealed class MveReconciliationService
|
||||||
var reportLine = hasReport ? reportLines![0] : null;
|
var reportLine = hasReport ? reportLines![0] : null;
|
||||||
var systemLine = hasSystem ? systemLines![0] : null;
|
var systemLine = hasSystem ? systemLines![0] : null;
|
||||||
|
|
||||||
if (reportLine == null && systemLine != null)
|
|
||||||
{
|
|
||||||
result.TotalOnlyInSystem++;
|
|
||||||
result.Issues.Add(new MveReconciliationIssueResult
|
|
||||||
{
|
|
||||||
NumeroLinha = key,
|
|
||||||
MobileLineId = systemLine.MobileLine.Id,
|
|
||||||
SystemItem = systemLine.MobileLine.Item,
|
|
||||||
IssueType = "ONLY_IN_SYSTEM",
|
|
||||||
Situation = "ausente no relatório",
|
|
||||||
Severity = "WARNING",
|
|
||||||
Syncable = false,
|
|
||||||
ActionSuggestion = "Validar com a Vivo antes de alterar o cadastro",
|
|
||||||
Notes = "A linha existe no sistema, mas não foi encontrada no relatório MVE.",
|
|
||||||
SystemStatus = systemLine.MobileLine.Status,
|
|
||||||
SystemPlan = systemLine.MobileLine.PlanoContrato,
|
|
||||||
SystemSnapshot = BuildSystemSnapshot(systemLine)
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reportLine != null && systemLine == null)
|
|
||||||
{
|
|
||||||
result.TotalOnlyInReport++;
|
|
||||||
result.Issues.Add(new MveReconciliationIssueResult
|
|
||||||
{
|
|
||||||
SourceRowNumber = reportLine.SourceRowNumber,
|
|
||||||
NumeroLinha = key,
|
|
||||||
IssueType = "ONLY_IN_REPORT",
|
|
||||||
Situation = "ausente no sistema",
|
|
||||||
Severity = "WARNING",
|
|
||||||
Syncable = false,
|
|
||||||
ActionSuggestion = "Avaliar cadastro manual dessa linha",
|
|
||||||
Notes = "A linha existe no relatório MVE, mas não foi encontrada na página Geral.",
|
|
||||||
ReportStatus = reportLine.StatusLinha,
|
|
||||||
ReportPlan = reportLine.PlanoLinha,
|
|
||||||
ReportSnapshot = BuildReportSnapshot(reportLine)
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reportLine == null || systemLine == null)
|
if (reportLine == null || systemLine == null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
matchedSystemLineIds.Add(systemLine.MobileLine.Id);
|
||||||
|
matchedReportRows.Add(reportLine.SourceRowNumber);
|
||||||
|
|
||||||
var comparison = CompareMatchedLine(systemLine, reportLine);
|
var comparison = CompareMatchedLine(systemLine, reportLine);
|
||||||
if (comparison == null)
|
RegisterComparisonResult(result, comparison);
|
||||||
|
}
|
||||||
|
|
||||||
|
var unmatchedSystemLines = systemAggregates
|
||||||
|
.Where(x => !blockedSystemKeys.Contains(x.NumeroNormalizado))
|
||||||
|
.Where(x => !matchedSystemLineIds.Contains(x.MobileLine.Id))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var unmatchedReportLines = parsedFile.Lines
|
||||||
|
.Where(x => !blockedReportKeys.Contains(x.NumeroNormalizado))
|
||||||
|
.Where(x => !matchedReportRows.Contains(x.SourceRowNumber))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var chipMatchedSystemLineIds = new HashSet<Guid>();
|
||||||
|
var chipMatchedReportRows = new HashSet<int>();
|
||||||
|
|
||||||
|
var systemByChip = unmatchedSystemLines
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(MveAuditNormalization.NullIfEmptyDigits(x.MobileLine.Chip)))
|
||||||
|
.GroupBy(x => MveAuditNormalization.OnlyDigits(x.MobileLine.Chip), StringComparer.Ordinal)
|
||||||
|
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var reportByChip = unmatchedReportLines
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(MveAuditNormalization.NullIfEmptyDigits(x.Chip)))
|
||||||
|
.GroupBy(x => MveAuditNormalization.OnlyDigits(x.Chip), StringComparer.Ordinal)
|
||||||
|
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var chipKeys = systemByChip.Keys
|
||||||
|
.Intersect(reportByChip.Keys, StringComparer.Ordinal)
|
||||||
|
.OrderBy(key => key, StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var chipKey in chipKeys)
|
||||||
|
{
|
||||||
|
var systemCandidates = systemByChip[chipKey];
|
||||||
|
var reportCandidates = reportByChip[chipKey];
|
||||||
|
if (systemCandidates.Count != 1 || reportCandidates.Count != 1)
|
||||||
{
|
{
|
||||||
result.TotalConciliated++;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Issues.Add(comparison);
|
var systemLine = systemCandidates[0];
|
||||||
if (comparison.Differences.Any(x => x.FieldKey == "status"))
|
var reportLine = reportCandidates[0];
|
||||||
{
|
|
||||||
result.TotalStatusDivergences++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (comparison.Differences.Any(x => x.FieldKey != "status" && x.Syncable))
|
chipMatchedSystemLineIds.Add(systemLine.MobileLine.Id);
|
||||||
{
|
chipMatchedReportRows.Add(reportLine.SourceRowNumber);
|
||||||
result.TotalDataDivergences++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (comparison.Syncable)
|
var comparison = CompareMatchedByChip(systemLine, reportLine);
|
||||||
|
RegisterComparisonResult(result, comparison);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var systemLine in unmatchedSystemLines.Where(x => !chipMatchedSystemLineIds.Contains(x.MobileLine.Id)))
|
||||||
|
{
|
||||||
|
result.TotalOnlyInSystem++;
|
||||||
|
result.Issues.Add(new MveReconciliationIssueResult
|
||||||
{
|
{
|
||||||
result.TotalSyncableIssues++;
|
NumeroLinha = systemLine.NumeroNormalizado,
|
||||||
}
|
MobileLineId = systemLine.MobileLine.Id,
|
||||||
|
SystemItem = systemLine.MobileLine.Item,
|
||||||
|
IssueType = "ONLY_IN_SYSTEM",
|
||||||
|
Situation = "ausente no relatório",
|
||||||
|
Severity = "WARNING",
|
||||||
|
Syncable = false,
|
||||||
|
ActionSuggestion = "Validar com a Vivo antes de alterar o cadastro",
|
||||||
|
Notes = "A linha existe no sistema, mas não foi encontrada no relatório MVE.",
|
||||||
|
SystemStatus = systemLine.MobileLine.Status,
|
||||||
|
SystemPlan = systemLine.MobileLine.PlanoContrato,
|
||||||
|
SystemSnapshot = BuildSystemSnapshot(systemLine)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var reportLine in unmatchedReportLines.Where(x => !chipMatchedReportRows.Contains(x.SourceRowNumber)))
|
||||||
|
{
|
||||||
|
result.TotalOnlyInReport++;
|
||||||
|
result.Issues.Add(new MveReconciliationIssueResult
|
||||||
|
{
|
||||||
|
SourceRowNumber = reportLine.SourceRowNumber,
|
||||||
|
NumeroLinha = reportLine.NumeroNormalizado,
|
||||||
|
IssueType = "ONLY_IN_REPORT",
|
||||||
|
Situation = "ausente no sistema",
|
||||||
|
Severity = "WARNING",
|
||||||
|
Syncable = false,
|
||||||
|
ActionSuggestion = "Avaliar cadastro manual dessa linha",
|
||||||
|
Notes = "A linha existe no relatório MVE, mas não foi encontrada na página Geral.",
|
||||||
|
ReportStatus = reportLine.StatusLinha,
|
||||||
|
ReportPlan = reportLine.PlanoLinha,
|
||||||
|
ReportSnapshot = BuildReportSnapshot(reportLine)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -291,12 +321,104 @@ public sealed class MveReconciliationService
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AddDifference(
|
||||||
|
differences,
|
||||||
|
"chip",
|
||||||
|
"Chip da linha",
|
||||||
|
systemSnapshot.Chip,
|
||||||
|
reportSnapshot.Chip,
|
||||||
|
syncable: true,
|
||||||
|
comparer: MveAuditNormalization.OnlyDigits);
|
||||||
|
|
||||||
var hasUnknownStatus = !reportLine.StatusLinhaRecognized;
|
var hasUnknownStatus = !reportLine.StatusLinhaRecognized;
|
||||||
if (differences.Count == 0 && !hasUnknownStatus)
|
if (differences.Count == 0 && !hasUnknownStatus)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return BuildIssue(systemLine, reportLine, systemSnapshot, reportSnapshot, differences, hasUnknownStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MveReconciliationIssueResult? CompareMatchedByChip(
|
||||||
|
MveSystemLineAggregate systemLine,
|
||||||
|
MveParsedLine reportLine)
|
||||||
|
{
|
||||||
|
var systemSnapshot = BuildSystemSnapshot(systemLine);
|
||||||
|
var reportSnapshot = BuildReportSnapshot(reportLine);
|
||||||
|
var systemLocalNumber = MveAuditNormalization.ExtractPhoneLocalNumber(systemSnapshot.NumeroLinha);
|
||||||
|
var reportLocalNumber = MveAuditNormalization.ExtractPhoneLocalNumber(reportSnapshot.NumeroLinha);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(systemLocalNumber) &&
|
||||||
|
string.Equals(systemLocalNumber, reportLocalNumber, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return BuildDddReviewIssue(systemLine, reportLine, systemSnapshot, reportSnapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
var differences = new List<MveAuditDifferenceDto>();
|
||||||
|
AddDifference(
|
||||||
|
differences,
|
||||||
|
"line",
|
||||||
|
"Número da linha",
|
||||||
|
systemSnapshot.NumeroLinha,
|
||||||
|
reportSnapshot.NumeroLinha,
|
||||||
|
syncable: true,
|
||||||
|
comparer: MveAuditNormalization.OnlyDigits);
|
||||||
|
|
||||||
|
var systemStatus = MveAuditNormalization.NormalizeSystemStatus(systemSnapshot.StatusLinha);
|
||||||
|
if (!string.Equals(systemStatus.Key, reportLine.StatusLinhaKey, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
differences.Add(new MveAuditDifferenceDto
|
||||||
|
{
|
||||||
|
FieldKey = "status",
|
||||||
|
Label = "Status da linha",
|
||||||
|
SystemValue = NullIfEmpty(systemSnapshot.StatusLinha),
|
||||||
|
ReportValue = NullIfEmpty(reportSnapshot.StatusLinha),
|
||||||
|
Syncable = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasUnknownStatus = !reportLine.StatusLinhaRecognized;
|
||||||
|
if (differences.Count == 0 && !hasUnknownStatus)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BuildIssue(systemLine, reportLine, systemSnapshot, reportSnapshot, differences, hasUnknownStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterComparisonResult(MveReconciliationResult result, MveReconciliationIssueResult? comparison)
|
||||||
|
{
|
||||||
|
if (comparison == null)
|
||||||
|
{
|
||||||
|
result.TotalConciliated++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Issues.Add(comparison);
|
||||||
|
if (comparison.Differences.Any(x => x.FieldKey == "status"))
|
||||||
|
{
|
||||||
|
result.TotalStatusDivergences++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comparison.Differences.Any(x => x.FieldKey != "status" && x.Syncable))
|
||||||
|
{
|
||||||
|
result.TotalDataDivergences++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comparison.Syncable)
|
||||||
|
{
|
||||||
|
result.TotalSyncableIssues++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MveReconciliationIssueResult BuildIssue(
|
||||||
|
MveSystemLineAggregate systemLine,
|
||||||
|
MveParsedLine reportLine,
|
||||||
|
MveAuditSnapshotDto systemSnapshot,
|
||||||
|
MveAuditSnapshotDto reportSnapshot,
|
||||||
|
List<MveAuditDifferenceDto> differences,
|
||||||
|
bool hasUnknownStatus)
|
||||||
|
{
|
||||||
var notes = new List<string>();
|
var notes = new List<string>();
|
||||||
if (hasUnknownStatus)
|
if (hasUnknownStatus)
|
||||||
{
|
{
|
||||||
|
|
@ -304,8 +426,9 @@ public sealed class MveReconciliationService
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasStatusDifference = differences.Any(x => x.FieldKey == "status");
|
var hasStatusDifference = differences.Any(x => x.FieldKey == "status");
|
||||||
var hasDataDifference = false;
|
var hasLineDifference = differences.Any(x => x.FieldKey == "line");
|
||||||
var issueType = hasStatusDifference ? "STATUS_DIVERGENCE" : "UNKNOWN_STATUS";
|
var hasChipDifference = differences.Any(x => x.FieldKey == "chip");
|
||||||
|
var hasDataDifference = differences.Any(x => x.FieldKey != "status" && x.Syncable);
|
||||||
|
|
||||||
return new MveReconciliationIssueResult
|
return new MveReconciliationIssueResult
|
||||||
{
|
{
|
||||||
|
|
@ -313,11 +436,11 @@ public sealed class MveReconciliationService
|
||||||
NumeroLinha = reportLine.NumeroNormalizado,
|
NumeroLinha = reportLine.NumeroNormalizado,
|
||||||
MobileLineId = systemLine.MobileLine.Id,
|
MobileLineId = systemLine.MobileLine.Id,
|
||||||
SystemItem = systemLine.MobileLine.Item,
|
SystemItem = systemLine.MobileLine.Item,
|
||||||
IssueType = issueType,
|
IssueType = ResolveIssueType(hasStatusDifference, hasLineDifference, hasChipDifference, hasUnknownStatus),
|
||||||
Situation = ResolveSituation(hasStatusDifference, hasDataDifference, hasUnknownStatus),
|
Situation = ResolveSituation(hasStatusDifference, hasLineDifference, hasChipDifference, hasUnknownStatus),
|
||||||
Severity = ResolveSeverity(hasStatusDifference, hasDataDifference, hasUnknownStatus),
|
Severity = ResolveSeverity(hasStatusDifference, hasDataDifference, hasUnknownStatus),
|
||||||
Syncable = differences.Any(x => x.Syncable),
|
Syncable = differences.Any(x => x.Syncable),
|
||||||
ActionSuggestion = ResolveActionSuggestion(hasStatusDifference, hasDataDifference, hasUnknownStatus),
|
ActionSuggestion = ResolveActionSuggestion(hasStatusDifference, hasLineDifference, hasChipDifference, hasUnknownStatus),
|
||||||
Notes = notes.Count == 0 ? null : string.Join(" ", notes),
|
Notes = notes.Count == 0 ? null : string.Join(" ", notes),
|
||||||
SystemStatus = systemSnapshot.StatusLinha,
|
SystemStatus = systemSnapshot.StatusLinha,
|
||||||
ReportStatus = reportSnapshot.StatusLinha,
|
ReportStatus = reportSnapshot.StatusLinha,
|
||||||
|
|
@ -329,11 +452,92 @@ public sealed class MveReconciliationService
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolveSituation(bool hasStatusDifference, bool hasDataDifference, bool hasUnknownStatus)
|
private static MveReconciliationIssueResult BuildDddReviewIssue(
|
||||||
|
MveSystemLineAggregate systemLine,
|
||||||
|
MveParsedLine reportLine,
|
||||||
|
MveAuditSnapshotDto systemSnapshot,
|
||||||
|
MveAuditSnapshotDto reportSnapshot)
|
||||||
{
|
{
|
||||||
if (hasStatusDifference && hasDataDifference)
|
return new MveReconciliationIssueResult
|
||||||
{
|
{
|
||||||
return "divergência de status e cadastro";
|
SourceRowNumber = reportLine.SourceRowNumber,
|
||||||
|
NumeroLinha = reportLine.NumeroNormalizado,
|
||||||
|
MobileLineId = systemLine.MobileLine.Id,
|
||||||
|
SystemItem = systemLine.MobileLine.Item,
|
||||||
|
IssueType = "DDD_CHANGE_REVIEW",
|
||||||
|
Situation = "mudança de DDD detectada",
|
||||||
|
Severity = "WARNING",
|
||||||
|
Syncable = false,
|
||||||
|
ActionSuggestion = "Revisar manualmente na página Mureg antes de aplicar alterações",
|
||||||
|
Notes = "O mesmo chip foi encontrado com o mesmo número base, mas com DDD diferente. Esse cenário ainda não é atualizado automaticamente pelo MVE.",
|
||||||
|
SystemStatus = systemSnapshot.StatusLinha,
|
||||||
|
ReportStatus = reportSnapshot.StatusLinha,
|
||||||
|
SystemPlan = systemSnapshot.PlanoLinha,
|
||||||
|
ReportPlan = reportSnapshot.PlanoLinha,
|
||||||
|
SystemSnapshot = systemSnapshot,
|
||||||
|
ReportSnapshot = reportSnapshot,
|
||||||
|
Differences = new List<MveAuditDifferenceDto>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
FieldKey = "ddd",
|
||||||
|
Label = "DDD da linha",
|
||||||
|
SystemValue = NullIfEmpty(MveAuditNormalization.ExtractPhoneDdd(systemSnapshot.NumeroLinha)),
|
||||||
|
ReportValue = NullIfEmpty(MveAuditNormalization.ExtractPhoneDdd(reportSnapshot.NumeroLinha)),
|
||||||
|
Syncable = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveIssueType(
|
||||||
|
bool hasStatusDifference,
|
||||||
|
bool hasLineDifference,
|
||||||
|
bool hasChipDifference,
|
||||||
|
bool hasUnknownStatus)
|
||||||
|
{
|
||||||
|
if (hasLineDifference)
|
||||||
|
{
|
||||||
|
return "LINE_CHANGE_DETECTED";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChipDifference)
|
||||||
|
{
|
||||||
|
return "CHIP_CHANGE_DETECTED";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasStatusDifference)
|
||||||
|
{
|
||||||
|
return "STATUS_DIVERGENCE";
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasUnknownStatus ? "UNKNOWN_STATUS" : "ALIGNED";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveSituation(
|
||||||
|
bool hasStatusDifference,
|
||||||
|
bool hasLineDifference,
|
||||||
|
bool hasChipDifference,
|
||||||
|
bool hasUnknownStatus)
|
||||||
|
{
|
||||||
|
if (hasLineDifference && hasStatusDifference)
|
||||||
|
{
|
||||||
|
return "troca de número e status diferente";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasLineDifference)
|
||||||
|
{
|
||||||
|
return "troca de número detectada";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChipDifference && hasStatusDifference)
|
||||||
|
{
|
||||||
|
return "troca de chip e status diferente";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChipDifference)
|
||||||
|
{
|
||||||
|
return "troca de chip detectada";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasStatusDifference)
|
if (hasStatusDifference)
|
||||||
|
|
@ -341,11 +545,6 @@ public sealed class MveReconciliationService
|
||||||
return "divergência de status";
|
return "divergência de status";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasDataDifference)
|
|
||||||
{
|
|
||||||
return "divergência de cadastro";
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasUnknownStatus ? "status desconhecido no relatório" : "alinhada";
|
return hasUnknownStatus ? "status desconhecido no relatório" : "alinhada";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -364,8 +563,32 @@ public sealed class MveReconciliationService
|
||||||
return hasUnknownStatus ? "WARNING" : "INFO";
|
return hasUnknownStatus ? "WARNING" : "INFO";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolveActionSuggestion(bool hasStatusDifference, bool hasDataDifference, bool hasUnknownStatus)
|
private static string ResolveActionSuggestion(
|
||||||
|
bool hasStatusDifference,
|
||||||
|
bool hasLineDifference,
|
||||||
|
bool hasChipDifference,
|
||||||
|
bool hasUnknownStatus)
|
||||||
{
|
{
|
||||||
|
if (hasLineDifference && hasStatusDifference)
|
||||||
|
{
|
||||||
|
return "Atualizar linha e status da linha com base no MVE";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasLineDifference)
|
||||||
|
{
|
||||||
|
return "Atualizar a linha cadastrada com base no chip informado no MVE";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChipDifference && hasStatusDifference)
|
||||||
|
{
|
||||||
|
return "Atualizar chip e status da linha com base no MVE";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChipDifference)
|
||||||
|
{
|
||||||
|
return "Atualizar o chip da linha com base no MVE";
|
||||||
|
}
|
||||||
|
|
||||||
if (hasStatusDifference)
|
if (hasStatusDifference)
|
||||||
{
|
{
|
||||||
return "Atualizar status da linha com base no MVE";
|
return "Atualizar status da linha com base no MVE";
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@ using line_gestao_api.Data;
|
||||||
using line_gestao_api.Dtos;
|
using line_gestao_api.Dtos;
|
||||||
using line_gestao_api.Models;
|
using line_gestao_api.Models;
|
||||||
using line_gestao_api.Services;
|
using line_gestao_api.Services;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc.Testing;
|
using Microsoft.AspNetCore.Mvc.Testing;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
@ -324,6 +326,7 @@ public class SystemTenantIntegrationTests
|
||||||
|
|
||||||
services.RemoveAll<AppDbContext>();
|
services.RemoveAll<AppDbContext>();
|
||||||
services.RemoveAll<DbContextOptions<AppDbContext>>();
|
services.RemoveAll<DbContextOptions<AppDbContext>>();
|
||||||
|
services.RemoveAll<IDbContextOptionsConfiguration<AppDbContext>>();
|
||||||
|
|
||||||
services.AddDbContext<AppDbContext>(options =>
|
services.AddDbContext<AppDbContext>(options =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue