Feat: Novas Implementações
This commit is contained in:
parent
208c201156
commit
82dd0bf2d0
|
|
@ -10,7 +10,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "sysadmin,financeiro")]
|
||||
public class BillingController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
@ -197,7 +197,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateBillingClientRequest req)
|
||||
{
|
||||
var x = await _db.BillingClients.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
@ -230,7 +230,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var x = await _db.BillingClients.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/chips-virgens")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "sysadmin,gestor,financeiro")]
|
||||
public class ChipsVirgensController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/controle-recebidos")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "sysadmin,gestor,financeiro")]
|
||||
public class ControleRecebidosController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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;
|
||||
|
|
@ -11,9 +12,18 @@ namespace line_gestao_api.Controllers;
|
|||
|
||||
[ApiController]
|
||||
[Route("api/historico")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[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)
|
||||
|
|
@ -121,11 +131,118 @@ public class HistoricoController : ControllerBase
|
|||
Page = page,
|
||||
PageSize = pageSize,
|
||||
Total = total,
|
||||
Items = items.Select(ToDto).ToList()
|
||||
Items = items.Select(log => ToDto(log)).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
private static AuditLogDto ToDto(Models.AuditLog log)
|
||||
[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
|
||||
{
|
||||
|
|
@ -142,7 +259,7 @@ public class HistoricoController : ControllerBase
|
|||
RequestPath = log.RequestPath,
|
||||
RequestMethod = log.RequestMethod,
|
||||
IpAddress = log.IpAddress,
|
||||
Changes = ParseChanges(log.ChangesJson)
|
||||
Changes = parsedChanges ?? ParseChanges(log.ChangesJson)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -173,6 +290,102 @@ public class HistoricoController : ControllerBase
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -811,6 +811,7 @@ namespace line_gestao_api.Controllers
|
|||
|
||||
var previousLinha = lineToPersist!.Linha;
|
||||
ApplyCreateRequestToLine(lineToPersist, req, linhaLimpa, chipLimpo, franquiaVivo, valorPlanoVivo, now);
|
||||
ApplyBlockedLineToReservaContext(lineToPersist);
|
||||
ApplyReservaRule(lineToPersist);
|
||||
var ensuredTenant = await EnsureTenantForClientAsync(lineToPersist.Cliente);
|
||||
if (ensuredTenant != null)
|
||||
|
|
@ -1323,6 +1324,166 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// ✅ 5.5. BLOQUEIO / DESBLOQUEIO EM LOTE (GERAL)
|
||||
// ==========================================================
|
||||
[HttpPost("batch-status-update")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
public async Task<ActionResult<BatchLineStatusUpdateResultDto>> BatchStatusUpdate([FromBody] BatchLineStatusUpdateRequestDto req)
|
||||
{
|
||||
var action = (req?.Action ?? "").Trim().ToLowerInvariant();
|
||||
var isBlockAction = action is "block" or "bloquear";
|
||||
var isUnblockAction = action is "unblock" or "desbloquear";
|
||||
|
||||
if (!isBlockAction && !isUnblockAction)
|
||||
return BadRequest(new { message = "Ação inválida. Use 'block' ou 'unblock'." });
|
||||
|
||||
var blockStatus = NormalizeOptionalText(req?.BlockStatus);
|
||||
if (isBlockAction && string.IsNullOrWhiteSpace(blockStatus))
|
||||
return BadRequest(new { message = "Informe o tipo de bloqueio para bloqueio em lote." });
|
||||
|
||||
var applyToAllFiltered = req?.ApplyToAllFiltered ?? false;
|
||||
var ids = (req?.LineIds ?? new List<Guid>())
|
||||
.Where(x => x != Guid.Empty)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (!applyToAllFiltered)
|
||||
{
|
||||
if (ids.Count == 0)
|
||||
return BadRequest(new { message = "Selecione ao menos uma linha para processar." });
|
||||
if (ids.Count > 5000)
|
||||
return BadRequest(new { message = "Limite de 5000 linhas por operação em lote." });
|
||||
}
|
||||
|
||||
IQueryable<MobileLine> baseQuery;
|
||||
if (applyToAllFiltered)
|
||||
{
|
||||
baseQuery = BuildBatchStatusTargetQuery(req);
|
||||
const int filteredLimit = 20000;
|
||||
var overLimit = await baseQuery.OrderBy(x => x.Item).Take(filteredLimit + 1).CountAsync();
|
||||
if (overLimit > filteredLimit)
|
||||
return BadRequest(new { message = "Muitos registros filtrados. Refine os filtros (máximo 20000 por operação)." });
|
||||
}
|
||||
else
|
||||
{
|
||||
baseQuery = _db.MobileLines.Where(x => ids.Contains(x.Id));
|
||||
}
|
||||
|
||||
var userFilter = (req?.Usuario ?? "").Trim();
|
||||
if (!applyToAllFiltered && !string.IsNullOrWhiteSpace(userFilter))
|
||||
{
|
||||
baseQuery = baseQuery.Where(x => EF.Functions.ILike(x.Usuario ?? "", $"%{userFilter}%"));
|
||||
}
|
||||
|
||||
var targetLines = await baseQuery
|
||||
.OrderBy(x => x.Item)
|
||||
.ToListAsync();
|
||||
|
||||
var result = new BatchLineStatusUpdateResultDto
|
||||
{
|
||||
Requested = applyToAllFiltered ? targetLines.Count : ids.Count
|
||||
};
|
||||
|
||||
if (result.Requested <= 0)
|
||||
return Ok(result);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if (applyToAllFiltered)
|
||||
{
|
||||
foreach (var line in targetLines)
|
||||
{
|
||||
var previousStatus = NormalizeOptionalText(line.Status);
|
||||
var newStatus = isBlockAction ? blockStatus! : "ATIVO";
|
||||
|
||||
line.Status = newStatus;
|
||||
line.DataBloqueio = isBlockAction
|
||||
? (line.DataBloqueio ?? now)
|
||||
: null;
|
||||
if (isBlockAction)
|
||||
ApplyBlockedLineToReservaContext(line);
|
||||
ApplyReservaRule(line);
|
||||
line.UpdatedAt = now;
|
||||
|
||||
result.Items.Add(new BatchLineStatusUpdateItemResultDto
|
||||
{
|
||||
Id = line.Id,
|
||||
Item = line.Item,
|
||||
Linha = line.Linha,
|
||||
Usuario = line.Usuario,
|
||||
StatusAnterior = previousStatus,
|
||||
StatusNovo = newStatus,
|
||||
Success = true,
|
||||
Message = isBlockAction ? "Linha bloqueada com sucesso." : "Linha desbloqueada com sucesso."
|
||||
});
|
||||
|
||||
result.Updated++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var byId = targetLines.ToDictionary(x => x.Id, x => x);
|
||||
foreach (var id in ids)
|
||||
{
|
||||
if (!byId.TryGetValue(id, out var line))
|
||||
{
|
||||
result.Items.Add(new BatchLineStatusUpdateItemResultDto
|
||||
{
|
||||
Id = id,
|
||||
Success = false,
|
||||
Message = "Linha não encontrada para o contexto atual."
|
||||
});
|
||||
result.Failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var previousStatus = NormalizeOptionalText(line.Status);
|
||||
var newStatus = isBlockAction ? blockStatus! : "ATIVO";
|
||||
|
||||
line.Status = newStatus;
|
||||
line.DataBloqueio = isBlockAction
|
||||
? (line.DataBloqueio ?? now)
|
||||
: null;
|
||||
if (isBlockAction)
|
||||
ApplyBlockedLineToReservaContext(line);
|
||||
ApplyReservaRule(line);
|
||||
line.UpdatedAt = now;
|
||||
|
||||
result.Items.Add(new BatchLineStatusUpdateItemResultDto
|
||||
{
|
||||
Id = line.Id,
|
||||
Item = line.Item,
|
||||
Linha = line.Linha,
|
||||
Usuario = line.Usuario,
|
||||
StatusAnterior = previousStatus,
|
||||
StatusNovo = newStatus,
|
||||
Success = true,
|
||||
Message = isBlockAction ? "Linha bloqueada com sucesso." : "Linha desbloqueada com sucesso."
|
||||
});
|
||||
result.Updated++;
|
||||
}
|
||||
}
|
||||
|
||||
result.Failed = result.Requested - result.Updated;
|
||||
|
||||
if (result.Updated <= 0)
|
||||
return Ok(result);
|
||||
|
||||
await using var tx = await _db.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
await _db.SaveChangesAsync();
|
||||
await AddBatchStatusUpdateHistoryAsync(req, result, isBlockAction, blockStatus);
|
||||
await tx.CommitAsync();
|
||||
return Ok(result);
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
await tx.RollbackAsync();
|
||||
return StatusCode(500, new { message = "Erro ao processar bloqueio/desbloqueio em lote." });
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// ✅ 6. UPDATE
|
||||
// ==========================================================
|
||||
|
|
@ -1415,6 +1576,7 @@ namespace line_gestao_api.Controllers
|
|||
x.DataEntregaCliente = ToUtc(req.DataEntregaCliente);
|
||||
x.VencConta = NormalizeOptionalText(req.VencConta);
|
||||
x.TipoDeChip = NormalizeOptionalText(req.TipoDeChip);
|
||||
ApplyBlockedLineToReservaContext(x);
|
||||
|
||||
var previousClienteNormalized = string.IsNullOrWhiteSpace(previousCliente) ? null : previousCliente.Trim();
|
||||
var clienteAtualIsReserva = IsReservaValue(x.Cliente);
|
||||
|
|
@ -1859,6 +2021,132 @@ namespace line_gestao_api.Controllers
|
|||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private IQueryable<MobileLine> BuildBatchStatusTargetQuery(BatchLineStatusUpdateRequestDto? req)
|
||||
{
|
||||
var q = _db.MobileLines.AsQueryable();
|
||||
var reservaFilter = false;
|
||||
|
||||
var skil = (req?.Skil ?? "").Trim();
|
||||
if (!string.IsNullOrWhiteSpace(skil))
|
||||
{
|
||||
if (skil.Equals("RESERVA", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
reservaFilter = true;
|
||||
q = q.Where(x =>
|
||||
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
||||
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") ||
|
||||
EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
||||
}
|
||||
else
|
||||
{
|
||||
q = q.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{skil}%"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!reservaFilter)
|
||||
q = ExcludeReservaContext(q);
|
||||
|
||||
q = ApplyAdditionalFilters(q, req?.AdditionalMode, req?.AdditionalServices);
|
||||
|
||||
var clients = (req?.Clients ?? new List<string>())
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Select(x => x.Trim())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (clients.Count > 0)
|
||||
{
|
||||
var normalizedClients = clients
|
||||
.Select(x => x.ToUpperInvariant())
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
q = q.Where(x => normalizedClients.Contains((x.Cliente ?? "").Trim().ToUpper()));
|
||||
}
|
||||
|
||||
var search = (req?.Search ?? "").Trim();
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
q = q.Where(x =>
|
||||
EF.Functions.ILike(x.Linha ?? "", $"%{search}%") ||
|
||||
EF.Functions.ILike(x.Chip ?? "", $"%{search}%") ||
|
||||
EF.Functions.ILike(x.Cliente ?? "", $"%{search}%") ||
|
||||
EF.Functions.ILike(x.Usuario ?? "", $"%{search}%") ||
|
||||
EF.Functions.ILike(x.Conta ?? "", $"%{search}%") ||
|
||||
EF.Functions.ILike(x.Status ?? "", $"%{search}%"));
|
||||
}
|
||||
|
||||
var usuario = (req?.Usuario ?? "").Trim();
|
||||
if (!string.IsNullOrWhiteSpace(usuario))
|
||||
{
|
||||
q = q.Where(x => EF.Functions.ILike(x.Usuario ?? "", $"%{usuario}%"));
|
||||
}
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
private async Task AddBatchStatusUpdateHistoryAsync(
|
||||
BatchLineStatusUpdateRequestDto req,
|
||||
BatchLineStatusUpdateResultDto result,
|
||||
bool isBlockAction,
|
||||
string? blockStatus)
|
||||
{
|
||||
var tenantId = _tenantProvider.TenantId;
|
||||
if (!tenantId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var claimNameId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
var userId = Guid.TryParse(claimNameId, out var parsedUserId) ? parsedUserId : (Guid?)null;
|
||||
|
||||
var userName = User.FindFirst("name")?.Value
|
||||
?? User.FindFirst(ClaimTypes.Name)?.Value
|
||||
?? User.Identity?.Name;
|
||||
|
||||
var userEmail = User.FindFirst(ClaimTypes.Email)?.Value
|
||||
?? User.FindFirst("email")?.Value;
|
||||
|
||||
var clientFilter = string.Join(", ", (req.Clients ?? new List<string>())
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Select(x => x.Trim())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
var changes = new List<AuditFieldChangeDto>
|
||||
{
|
||||
new() { Field = "AcaoLote", ChangeType = "batch", NewValue = isBlockAction ? "BLOCK" : "UNBLOCK" },
|
||||
new() { Field = "Escopo", ChangeType = "batch", NewValue = req.ApplyToAllFiltered ? "TODOS_FILTRADOS" : "SELECAO_MANUAL" },
|
||||
new() { Field = "StatusAplicado", ChangeType = "batch", NewValue = isBlockAction ? (blockStatus ?? "-") : "ATIVO" },
|
||||
new() { Field = "QuantidadeSolicitada", ChangeType = "batch", NewValue = result.Requested.ToString(CultureInfo.InvariantCulture) },
|
||||
new() { Field = "QuantidadeAtualizada", ChangeType = "batch", NewValue = result.Updated.ToString(CultureInfo.InvariantCulture) },
|
||||
new() { Field = "QuantidadeFalha", ChangeType = "batch", NewValue = result.Failed.ToString(CultureInfo.InvariantCulture) },
|
||||
new() { Field = "FiltroSkil", ChangeType = "filter", NewValue = string.IsNullOrWhiteSpace(req.Skil) ? "-" : req.Skil!.Trim() },
|
||||
new() { Field = "FiltroCliente", ChangeType = "filter", NewValue = string.IsNullOrWhiteSpace(clientFilter) ? "-" : clientFilter },
|
||||
new() { Field = "FiltroUsuario", ChangeType = "filter", NewValue = string.IsNullOrWhiteSpace(req.Usuario) ? "-" : req.Usuario!.Trim() },
|
||||
new() { Field = "FiltroBusca", ChangeType = "filter", NewValue = string.IsNullOrWhiteSpace(req.Search) ? "-" : req.Search!.Trim() }
|
||||
};
|
||||
|
||||
_db.AuditLogs.Add(new AuditLog
|
||||
{
|
||||
TenantId = tenantId.Value,
|
||||
OccurredAtUtc = DateTime.UtcNow,
|
||||
UserId = userId,
|
||||
UserName = string.IsNullOrWhiteSpace(userName) ? "USUARIO" : userName,
|
||||
UserEmail = userEmail,
|
||||
Action = isBlockAction ? "BATCH_BLOCK" : "BATCH_UNBLOCK",
|
||||
Page = "Geral",
|
||||
EntityName = "MobileLineBatchStatus",
|
||||
EntityId = null,
|
||||
EntityLabel = "Bloqueio/Desbloqueio em Lote",
|
||||
ChangesJson = JsonSerializer.Serialize(changes),
|
||||
RequestPath = HttpContext.Request.Path.Value,
|
||||
RequestMethod = HttpContext.Request.Method,
|
||||
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// ✅ IMPORTAÇÃO DA ABA MUREG
|
||||
// ✅ NOVA REGRA:
|
||||
|
|
@ -1873,13 +2161,37 @@ namespace line_gestao_api.Controllers
|
|||
|
||||
if (wsM == null) return;
|
||||
|
||||
var headerRow = wsM.RowsUsed().FirstOrDefault(r => r.CellsUsed().Any(c => NormalizeHeader(c.GetString()) == "ITEM"));
|
||||
var headerRow = wsM.RowsUsed().FirstOrDefault(r =>
|
||||
{
|
||||
var keys = r.CellsUsed()
|
||||
.Select(c => NormalizeHeader(c.GetString()))
|
||||
.Where(k => !string.IsNullOrWhiteSpace(k))
|
||||
.ToArray();
|
||||
|
||||
if (keys.Length == 0) return false;
|
||||
|
||||
var hasItem = keys.Any(k => k == "ITEM" || k == "ITEMID" || k.Contains("ITEM"));
|
||||
var hasMuregColumns = keys.Any(k =>
|
||||
k.Contains("LINHAANTIGA") ||
|
||||
k.Contains("LINHANOVA") ||
|
||||
k.Contains("ICCID") ||
|
||||
k.Contains("DATAMUREG") ||
|
||||
k.Contains("DATADAMUREG"));
|
||||
|
||||
return hasItem && hasMuregColumns;
|
||||
});
|
||||
if (headerRow == null) return;
|
||||
|
||||
var map = BuildHeaderMap(headerRow);
|
||||
var lastCol = GetLastUsedColumn(wsM, headerRow.RowNumber());
|
||||
|
||||
int colItem = GetCol(map, "ITEM");
|
||||
if (colItem == 0) return;
|
||||
var colItem = FindColByAny(headerRow, lastCol, "ITEM", "ITEM ID", "ITEM(ID)", "ITEMID", "ITÉM", "ITÉM (ID)");
|
||||
var colLinhaAntiga = FindColByAny(headerRow, lastCol, "LINHA ANTIGA", "LINHA ANTERIOR", "LINHA ANT.", "LINHA ANT");
|
||||
var colLinhaNova = FindColByAny(headerRow, lastCol, "LINHA NOVA", "NOVA LINHA");
|
||||
var colIccid = FindColByAny(headerRow, lastCol, "ICCID", "CHIP");
|
||||
var colDataMureg = FindColByAny(headerRow, lastCol, "DATA DA MUREG", "DATA MUREG", "DT MUREG");
|
||||
|
||||
if (colLinhaAntiga == 0 && colLinhaNova == 0 && colIccid == 0)
|
||||
return;
|
||||
|
||||
var startRow = headerRow.RowNumber() + 1;
|
||||
|
||||
|
|
@ -1889,14 +2201,18 @@ namespace line_gestao_api.Controllers
|
|||
// ✅ dicionários para resolver MobileLineId por Linha/Chip
|
||||
var mobilePairs = await _db.MobileLines
|
||||
.AsNoTracking()
|
||||
.Select(x => new { x.Id, x.Linha, x.Chip })
|
||||
.Select(x => new { x.Id, x.Item, x.Linha, x.Chip })
|
||||
.ToListAsync();
|
||||
|
||||
var mobileByLinha = new Dictionary<string, Guid>(StringComparer.Ordinal);
|
||||
var mobileByChip = new Dictionary<string, Guid>(StringComparer.Ordinal);
|
||||
var mobileByItem = new Dictionary<int, Guid>();
|
||||
|
||||
foreach (var m in mobilePairs)
|
||||
{
|
||||
if (m.Item > 0 && !mobileByItem.ContainsKey(m.Item))
|
||||
mobileByItem[m.Item] = m.Id;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(m.Linha))
|
||||
{
|
||||
var k = OnlyDigits(m.Linha);
|
||||
|
|
@ -1920,21 +2236,41 @@ namespace line_gestao_api.Controllers
|
|||
|
||||
for (int r = startRow; r <= lastRow; r++)
|
||||
{
|
||||
var itemStr = GetCellString(wsM, r, colItem);
|
||||
if (string.IsNullOrWhiteSpace(itemStr)) break;
|
||||
var itemStr = colItem > 0 ? GetCellString(wsM, r, colItem) : "";
|
||||
var linhaAntigaRaw = colLinhaAntiga > 0 ? GetCellString(wsM, r, colLinhaAntiga) : "";
|
||||
var linhaNovaRaw = colLinhaNova > 0 ? GetCellString(wsM, r, colLinhaNova) : "";
|
||||
var iccidRaw = colIccid > 0 ? GetCellString(wsM, r, colIccid) : "";
|
||||
|
||||
var linhaAntiga = NullIfEmptyDigits(GetCellByHeader(wsM, r, map, "LINHA ANTIGA"));
|
||||
var linhaNova = NullIfEmptyDigits(GetCellByHeader(wsM, r, map, "LINHA NOVA"));
|
||||
var iccid = NullIfEmptyDigits(GetCellByHeader(wsM, r, map, "ICCID"));
|
||||
var dataMureg = TryDate(wsM, r, map, "DATA DA MUREG");
|
||||
if (string.IsNullOrWhiteSpace(itemStr)
|
||||
&& string.IsNullOrWhiteSpace(linhaAntigaRaw)
|
||||
&& string.IsNullOrWhiteSpace(linhaNovaRaw)
|
||||
&& string.IsNullOrWhiteSpace(iccidRaw))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// ✅ resolve MobileLineId (prioridade: LinhaAntiga, depois ICCID)
|
||||
var linhaAntiga = NullIfEmptyDigits(linhaAntigaRaw);
|
||||
var linhaNova = NullIfEmptyDigits(linhaNovaRaw);
|
||||
var iccid = NullIfEmptyDigits(iccidRaw);
|
||||
var dataMureg = colDataMureg > 0 ? TryDateCell(wsM, r, colDataMureg) : null;
|
||||
var item = TryInt(itemStr);
|
||||
var hasSourceItem = item > 0;
|
||||
if (!hasSourceItem)
|
||||
{
|
||||
item = (r - startRow) + 1;
|
||||
}
|
||||
|
||||
// ✅ resolve MobileLineId (prioridade: LinhaAntiga, ICCID, LinhaNova, Item)
|
||||
Guid mobileLineId = Guid.Empty;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(linhaAntiga) && mobileByLinha.TryGetValue(linhaAntiga, out var idPorLinha))
|
||||
mobileLineId = idPorLinha;
|
||||
else if (!string.IsNullOrWhiteSpace(iccid) && mobileByChip.TryGetValue(iccid, out var idPorChip))
|
||||
mobileLineId = idPorChip;
|
||||
else if (!string.IsNullOrWhiteSpace(linhaNova) && mobileByLinha.TryGetValue(linhaNova, out var idPorLinhaNova))
|
||||
mobileLineId = idPorLinhaNova;
|
||||
else if (hasSourceItem && mobileByItem.TryGetValue(item, out var idPorItem))
|
||||
mobileLineId = idPorItem;
|
||||
|
||||
// Se não encontrou correspondência na GERAL, não dá pra salvar (MobileLineId é obrigatório)
|
||||
if (mobileLineId == Guid.Empty)
|
||||
|
|
@ -1959,7 +2295,7 @@ namespace line_gestao_api.Controllers
|
|||
var e = new MuregLine
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Item = TryInt(itemStr),
|
||||
Item = item,
|
||||
MobileLineId = mobileLineId,
|
||||
LinhaAntiga = linhaAntigaSnapshot,
|
||||
LinhaNova = linhaNova,
|
||||
|
|
@ -4983,6 +5319,25 @@ namespace line_gestao_api.Controllers
|
|||
if (IsReservaValue(x.Skil)) x.Skil = "RESERVA";
|
||||
}
|
||||
|
||||
private static void ApplyBlockedLineToReservaContext(MobileLine line)
|
||||
{
|
||||
if (!ShouldAutoMoveBlockedLineToReserva(line?.Status)) return;
|
||||
line.Usuario = "RESERVA";
|
||||
line.Skil = "RESERVA";
|
||||
if (string.IsNullOrWhiteSpace(line.Cliente))
|
||||
line.Cliente = "RESERVA";
|
||||
}
|
||||
|
||||
private static bool ShouldAutoMoveBlockedLineToReserva(string? status)
|
||||
{
|
||||
var normalizedStatus = NormalizeHeader(status);
|
||||
if (string.IsNullOrWhiteSpace(normalizedStatus)) return false;
|
||||
|
||||
return normalizedStatus.Contains("PERDA")
|
||||
|| normalizedStatus.Contains("ROUBO")
|
||||
|| (normalizedStatus.Contains("BLOQUEIO") && normalizedStatus.Contains("120"));
|
||||
}
|
||||
|
||||
private static bool IsReservaValue(string? value)
|
||||
=> string.Equals(value?.Trim(), "RESERVA", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ namespace line_gestao_api.Controllers
|
|||
public string? LinhaAntiga { get; set; } // opcional (snapshot)
|
||||
public string? LinhaNova { get; set; } // opcional
|
||||
public string? ICCID { get; set; } // opcional
|
||||
public DateTime? DataDaMureg { get; set; } // opcional
|
||||
public DateTime? DataDaMureg { get; set; } // ignorado no create (sistema define automaticamente)
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
|
|
@ -234,7 +234,8 @@ namespace line_gestao_api.Controllers
|
|||
LinhaAntiga = linhaAntigaSnapshot,
|
||||
LinhaNova = linhaNova,
|
||||
ICCID = iccid,
|
||||
DataDaMureg = ToUtc(req.DataDaMureg),
|
||||
// Data automática no momento da criação da Mureg
|
||||
DataDaMureg = now,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace line_gestao_api.Controllers;
|
|||
|
||||
[ApiController]
|
||||
[Route("api/parcelamentos")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "sysadmin,financeiro")]
|
||||
public class ParcelamentosController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
@ -165,7 +165,7 @@ public class ParcelamentosController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
public async Task<ActionResult<ParcelamentoDetailDto>> Create([FromBody] ParcelamentoUpsertDto req)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
|
@ -202,7 +202,7 @@ public class ParcelamentosController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] ParcelamentoUpsertDto req)
|
||||
{
|
||||
var entity = await _db.ParcelamentoLines
|
||||
|
|
@ -239,7 +239,7 @@ public class ParcelamentosController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var entity = await _db.ParcelamentoLines.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/templates")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
[Authorize(Roles = "sysadmin,gestor,financeiro")]
|
||||
public class TemplatesController : ControllerBase
|
||||
{
|
||||
private readonly GeralSpreadsheetTemplateService _geralSpreadsheetTemplateService;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ public class UsersController : ControllerBase
|
|||
{
|
||||
AppRoles.SysAdmin,
|
||||
AppRoles.Gestor,
|
||||
AppRoles.Financeiro,
|
||||
AppRoles.Cliente
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -71,4 +71,42 @@ namespace line_gestao_api.Dtos
|
|||
public bool Success { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class BatchLineStatusUpdateRequestDto
|
||||
{
|
||||
// "block" | "unblock"
|
||||
public string? Action { get; set; }
|
||||
public string? BlockStatus { get; set; }
|
||||
public bool ApplyToAllFiltered { get; set; }
|
||||
|
||||
public List<Guid> LineIds { get; set; } = new();
|
||||
|
||||
// Filtros da tela Geral
|
||||
public string? Search { get; set; }
|
||||
public string? Skil { get; set; }
|
||||
public List<string> Clients { get; set; } = new();
|
||||
public string? AdditionalMode { get; set; }
|
||||
public string? AdditionalServices { get; set; }
|
||||
public string? Usuario { get; set; }
|
||||
}
|
||||
|
||||
public sealed class BatchLineStatusUpdateResultDto
|
||||
{
|
||||
public int Requested { get; set; }
|
||||
public int Updated { get; set; }
|
||||
public int Failed { get; set; }
|
||||
public List<BatchLineStatusUpdateItemResultDto> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class BatchLineStatusUpdateItemResultDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public int Item { get; set; }
|
||||
public string? Linha { get; set; }
|
||||
public string? Usuario { get; set; }
|
||||
public string? StatusAnterior { get; set; }
|
||||
public string? StatusNovo { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ public static class AppRoles
|
|||
{
|
||||
public const string SysAdmin = "sysadmin";
|
||||
public const string Gestor = "gestor";
|
||||
public const string Financeiro = "financeiro";
|
||||
public const string Cliente = "cliente";
|
||||
|
||||
public static readonly string[] All = [SysAdmin, Gestor, Cliente];
|
||||
public static readonly string[] All = [SysAdmin, Gestor, Financeiro, Cliente];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ public class TenantProvider : ITenantProvider
|
|||
|
||||
public bool HasGlobalViewAccess =>
|
||||
HasRole(AppRoles.SysAdmin) ||
|
||||
HasRole(AppRoles.Gestor);
|
||||
HasRole(AppRoles.Gestor) ||
|
||||
HasRole(AppRoles.Financeiro);
|
||||
|
||||
public void SetTenantId(Guid? tenantId)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue