Merge remote-tracking branch 'origin/feat/exportar-em-todas-as-paginas' into dev

This commit is contained in:
Leon 2026-03-06 14:15:51 -03:00
commit 64ffb9f2e5
12 changed files with 641 additions and 31 deletions

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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
// ==========================================================
@ -1424,6 +1585,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);
@ -1868,6 +2030,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:
@ -1882,13 +2170,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;
@ -1898,14 +2210,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);
@ -1929,21 +2245,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)
@ -1968,7 +2304,7 @@ namespace line_gestao_api.Controllers
var e = new MuregLine
{
Id = Guid.NewGuid(),
Item = TryInt(itemStr),
Item = item,
MobileLineId = mobileLineId,
LinhaAntiga = linhaAntigaSnapshot,
LinhaNova = linhaNova,
@ -4992,6 +5328,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);

View File

@ -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
};

View File

@ -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);

View File

@ -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;

View File

@ -19,6 +19,7 @@ public class UsersController : ControllerBase
{
AppRoles.SysAdmin,
AppRoles.Gestor,
AppRoles.Financeiro,
AppRoles.Cliente
};

View File

@ -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;
}
}

View File

@ -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];
}

View File

@ -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)
{