diff --git a/Controllers/BillingController.cs b/Controllers/BillingController.cs index 82e3077..6154858 100644 --- a/Controllers/BillingController.cs +++ b/Controllers/BillingController.cs @@ -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 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 Delete(Guid id) { var x = await _db.BillingClients.FirstOrDefaultAsync(a => a.Id == id); diff --git a/Controllers/ChipsVirgensController.cs b/Controllers/ChipsVirgensController.cs index 4c1a3f1..ddc3979 100644 --- a/Controllers/ChipsVirgensController.cs +++ b/Controllers/ChipsVirgensController.cs @@ -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; diff --git a/Controllers/ControleRecebidosController.cs b/Controllers/ControleRecebidosController.cs index 1457b72..9e8df1a 100644 --- a/Controllers/ControleRecebidosController.cs +++ b/Controllers/ControleRecebidosController.cs @@ -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; diff --git a/Controllers/HistoricoController.cs b/Controllers/HistoricoController.cs index 8ec234b..c6676d5 100644 --- a/Controllers/HistoricoController.cs +++ b/Controllers/HistoricoController.cs @@ -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 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>> 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 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 + { + Page = page, + PageSize = pageSize, + Total = total, + Items = items + }); + } + + private static AuditLogDto ToDto(AuditLog log, List? 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 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 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; diff --git a/Controllers/LinesController.cs b/Controllers/LinesController.cs index fcfb02f..2cfc246 100644 --- a/Controllers/LinesController.cs +++ b/Controllers/LinesController.cs @@ -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> 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()) + .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 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 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()) + .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()) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase)); + + var changes = new List + { + 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(StringComparer.Ordinal); var mobileByChip = new Dictionary(StringComparer.Ordinal); + var mobileByItem = new Dictionary(); 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); diff --git a/Controllers/MuregController.cs b/Controllers/MuregController.cs index 4ec5909..8c0bd90 100644 --- a/Controllers/MuregController.cs +++ b/Controllers/MuregController.cs @@ -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 }; diff --git a/Controllers/ParcelamentosController.cs b/Controllers/ParcelamentosController.cs index 71b9f75..2d10494 100644 --- a/Controllers/ParcelamentosController.cs +++ b/Controllers/ParcelamentosController.cs @@ -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> 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 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 Delete(Guid id) { var entity = await _db.ParcelamentoLines.FirstOrDefaultAsync(x => x.Id == id); diff --git a/Controllers/TemplatesController.cs b/Controllers/TemplatesController.cs index adb254a..31c74e5 100644 --- a/Controllers/TemplatesController.cs +++ b/Controllers/TemplatesController.cs @@ -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; diff --git a/Controllers/UsersController.cs b/Controllers/UsersController.cs index b65f733..e752f28 100644 --- a/Controllers/UsersController.cs +++ b/Controllers/UsersController.cs @@ -19,6 +19,7 @@ public class UsersController : ControllerBase { AppRoles.SysAdmin, AppRoles.Gestor, + AppRoles.Financeiro, AppRoles.Cliente }; diff --git a/Dtos/LinesBatchExcelPreviewDtos.cs b/Dtos/LinesBatchExcelPreviewDtos.cs index 7356198..4eb9db5 100644 --- a/Dtos/LinesBatchExcelPreviewDtos.cs +++ b/Dtos/LinesBatchExcelPreviewDtos.cs @@ -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 LineIds { get; set; } = new(); + + // Filtros da tela Geral + public string? Search { get; set; } + public string? Skil { get; set; } + public List 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 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; + } } diff --git a/Services/AppRoles.cs b/Services/AppRoles.cs index 6bc0ff5..3e11b90 100644 --- a/Services/AppRoles.cs +++ b/Services/AppRoles.cs @@ -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]; } diff --git a/Services/TenantProvider.cs b/Services/TenantProvider.cs index 2e030f4..87181b9 100644 --- a/Services/TenantProvider.cs +++ b/Services/TenantProvider.cs @@ -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) {