Compare commits
No commits in common. "64ffb9f2e58ae7c641bd6714b674e2fe3e16c69f" and "208c201156af8b18a14a41a81056024a8e1b0169" have entirely different histories.
64ffb9f2e5
...
208c201156
|
|
@ -10,7 +10,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Authorize(Roles = "sysadmin,financeiro")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
public class BillingController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
@ -197,7 +197,7 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
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")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
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,financeiro")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
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,financeiro")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
public class ControleRecebidosController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
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;
|
||||
|
|
@ -12,18 +11,9 @@ namespace line_gestao_api.Controllers;
|
|||
|
||||
[ApiController]
|
||||
[Route("api/historico")]
|
||||
[Authorize(Roles = "sysadmin,gestor,financeiro")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
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)
|
||||
|
|
@ -131,118 +121,11 @@ public class HistoricoController : ControllerBase
|
|||
Page = page,
|
||||
PageSize = pageSize,
|
||||
Total = total,
|
||||
Items = items.Select(log => ToDto(log)).ToList()
|
||||
Items = items.Select(ToDto).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("linhas")]
|
||||
public async Task<ActionResult<PagedResult<AuditLogDto>>> GetLineHistory(
|
||||
[FromQuery] string? line,
|
||||
[FromQuery] string? pageName,
|
||||
[FromQuery] string? action,
|
||||
[FromQuery] string? user,
|
||||
[FromQuery] string? search,
|
||||
[FromQuery] DateTime? dateFrom,
|
||||
[FromQuery] DateTime? dateTo,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20)
|
||||
{
|
||||
page = page < 1 ? 1 : page;
|
||||
pageSize = pageSize < 1 ? 20 : pageSize;
|
||||
|
||||
var lineTerm = (line ?? string.Empty).Trim();
|
||||
var normalizedLineDigits = DigitsOnly(lineTerm);
|
||||
if (string.IsNullOrWhiteSpace(lineTerm) && string.IsNullOrWhiteSpace(normalizedLineDigits))
|
||||
{
|
||||
return BadRequest(new { message = "Informe uma linha para consultar o histórico." });
|
||||
}
|
||||
|
||||
var q = _db.AuditLogs
|
||||
.AsNoTracking()
|
||||
.Where(x => LineRelatedEntities.Contains(x.EntityName))
|
||||
.Where(x =>
|
||||
!EF.Functions.ILike(x.RequestPath ?? "", "%import-excel%") ||
|
||||
x.Page == AuditLogBuilder.SpreadsheetImportPageName);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(pageName))
|
||||
{
|
||||
var p = pageName.Trim();
|
||||
q = q.Where(x => EF.Functions.ILike(x.Page, $"%{p}%"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(action))
|
||||
{
|
||||
var a = action.Trim().ToUpperInvariant();
|
||||
q = q.Where(x => x.Action == a);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(user))
|
||||
{
|
||||
var u = user.Trim();
|
||||
q = q.Where(x =>
|
||||
EF.Functions.ILike(x.UserName ?? "", $"%{u}%") ||
|
||||
EF.Functions.ILike(x.UserEmail ?? "", $"%{u}%"));
|
||||
}
|
||||
|
||||
if (dateFrom.HasValue)
|
||||
{
|
||||
var fromUtc = ToUtc(dateFrom.Value);
|
||||
q = q.Where(x => x.OccurredAtUtc >= fromUtc);
|
||||
}
|
||||
|
||||
if (dateTo.HasValue)
|
||||
{
|
||||
var toUtc = ToUtc(dateTo.Value);
|
||||
if (dateTo.Value.TimeOfDay == TimeSpan.Zero)
|
||||
{
|
||||
toUtc = toUtc.Date.AddDays(1).AddTicks(-1);
|
||||
}
|
||||
|
||||
q = q.Where(x => x.OccurredAtUtc <= toUtc);
|
||||
}
|
||||
|
||||
var candidateLogs = await q
|
||||
.OrderByDescending(x => x.OccurredAtUtc)
|
||||
.ThenByDescending(x => x.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var searchTerm = (search ?? string.Empty).Trim();
|
||||
var searchDigits = DigitsOnly(searchTerm);
|
||||
var matchedLogs = new List<(AuditLog Log, List<AuditFieldChangeDto> Changes)>();
|
||||
|
||||
foreach (var log in candidateLogs)
|
||||
{
|
||||
var changes = ParseChanges(log.ChangesJson);
|
||||
if (!MatchesLine(log, changes, lineTerm, normalizedLineDigits))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchTerm) && !MatchesSearch(log, changes, searchTerm, searchDigits))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
matchedLogs.Add((log, changes));
|
||||
}
|
||||
|
||||
var total = matchedLogs.Count;
|
||||
var items = matchedLogs
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(x => ToDto(x.Log, x.Changes))
|
||||
.ToList();
|
||||
|
||||
return Ok(new PagedResult<AuditLogDto>
|
||||
{
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
Total = total,
|
||||
Items = items
|
||||
});
|
||||
}
|
||||
|
||||
private static AuditLogDto ToDto(AuditLog log, List<AuditFieldChangeDto>? parsedChanges = null)
|
||||
private static AuditLogDto ToDto(Models.AuditLog log)
|
||||
{
|
||||
return new AuditLogDto
|
||||
{
|
||||
|
|
@ -259,7 +142,7 @@ public class HistoricoController : ControllerBase
|
|||
RequestPath = log.RequestPath,
|
||||
RequestMethod = log.RequestMethod,
|
||||
IpAddress = log.IpAddress,
|
||||
Changes = parsedChanges ?? ParseChanges(log.ChangesJson)
|
||||
Changes = ParseChanges(log.ChangesJson)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -290,102 +173,6 @@ 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,7 +811,6 @@ 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)
|
||||
|
|
@ -1324,166 +1323,6 @@ 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
|
||||
// ==========================================================
|
||||
|
|
@ -1500,31 +1339,22 @@ namespace line_gestao_api.Controllers
|
|||
var canManageFullLine = User.IsInRole(AppRoles.SysAdmin) || User.IsInRole(AppRoles.Gestor);
|
||||
if (!canManageFullLine)
|
||||
{
|
||||
var tenantId = x.TenantId != Guid.Empty
|
||||
? x.TenantId
|
||||
: (_tenantProvider.ActorTenantId ?? Guid.Empty);
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
return BadRequest(new { message = "Tenant inválido para atualizar linha." });
|
||||
}
|
||||
|
||||
if (x.TenantId == Guid.Empty)
|
||||
{
|
||||
x.TenantId = tenantId;
|
||||
}
|
||||
|
||||
x.Usuario = NormalizeOptionalText(req.Usuario);
|
||||
x.CentroDeCustos = NormalizeOptionalText(req.CentroDeCustos);
|
||||
|
||||
await ApplySetorToLineAsync(x, tenantId, req.SetorId, req.SetorNome);
|
||||
await ApplyAparelhoToLineAsync(
|
||||
x,
|
||||
tenantId,
|
||||
x.TenantId,
|
||||
req.AparelhoId,
|
||||
req.AparelhoNome,
|
||||
req.AparelhoCor,
|
||||
req.AparelhoImei);
|
||||
|
||||
await UpsertVigenciaFromMobileLineAsync(
|
||||
x,
|
||||
dtEfetivacaoServico: null,
|
||||
dtTerminoFidelizacao: null,
|
||||
overrideDates: false,
|
||||
previousLinha: null);
|
||||
x.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
try
|
||||
|
|
@ -1585,7 +1415,6 @@ 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);
|
||||
|
|
@ -2030,132 +1859,6 @@ 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:
|
||||
|
|
@ -2170,37 +1873,13 @@ namespace line_gestao_api.Controllers
|
|||
|
||||
if (wsM == null) return;
|
||||
|
||||
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;
|
||||
});
|
||||
var headerRow = wsM.RowsUsed().FirstOrDefault(r => r.CellsUsed().Any(c => NormalizeHeader(c.GetString()) == "ITEM"));
|
||||
if (headerRow == null) return;
|
||||
|
||||
var lastCol = GetLastUsedColumn(wsM, headerRow.RowNumber());
|
||||
var map = BuildHeaderMap(headerRow);
|
||||
|
||||
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;
|
||||
int colItem = GetCol(map, "ITEM");
|
||||
if (colItem == 0) return;
|
||||
|
||||
var startRow = headerRow.RowNumber() + 1;
|
||||
|
||||
|
|
@ -2210,18 +1889,14 @@ 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.Item, x.Linha, x.Chip })
|
||||
.Select(x => new { x.Id, 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);
|
||||
|
|
@ -2245,41 +1920,21 @@ namespace line_gestao_api.Controllers
|
|||
|
||||
for (int r = startRow; r <= lastRow; r++)
|
||||
{
|
||||
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 itemStr = GetCellString(wsM, r, colItem);
|
||||
if (string.IsNullOrWhiteSpace(itemStr)) break;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(itemStr)
|
||||
&& string.IsNullOrWhiteSpace(linhaAntigaRaw)
|
||||
&& string.IsNullOrWhiteSpace(linhaNovaRaw)
|
||||
&& string.IsNullOrWhiteSpace(iccidRaw))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
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");
|
||||
|
||||
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)
|
||||
// ✅ resolve MobileLineId (prioridade: LinhaAntiga, depois ICCID)
|
||||
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)
|
||||
|
|
@ -2304,7 +1959,7 @@ namespace line_gestao_api.Controllers
|
|||
var e = new MuregLine
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Item = item,
|
||||
Item = TryInt(itemStr),
|
||||
MobileLineId = mobileLineId,
|
||||
LinhaAntiga = linhaAntigaSnapshot,
|
||||
LinhaNova = linhaNova,
|
||||
|
|
@ -5328,25 +4983,6 @@ 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; } // ignorado no create (sistema define automaticamente)
|
||||
public DateTime? DataDaMureg { get; set; } // opcional
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
|
|
@ -234,8 +234,7 @@ namespace line_gestao_api.Controllers
|
|||
LinhaAntiga = linhaAntigaSnapshot,
|
||||
LinhaNova = linhaNova,
|
||||
ICCID = iccid,
|
||||
// Data automática no momento da criação da Mureg
|
||||
DataDaMureg = now,
|
||||
DataDaMureg = ToUtc(req.DataDaMureg),
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ namespace line_gestao_api.Controllers;
|
|||
|
||||
[ApiController]
|
||||
[Route("api/parcelamentos")]
|
||||
[Authorize(Roles = "sysadmin,financeiro")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
public class ParcelamentosController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
|
@ -165,7 +165,7 @@ public class ParcelamentosController : ControllerBase
|
|||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "sysadmin")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
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")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
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")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var entity = await _db.ParcelamentoLines.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
|
|
|||
|
|
@ -1,240 +0,0 @@
|
|||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using line_gestao_api.Data;
|
||||
using line_gestao_api.Dtos;
|
||||
using line_gestao_api.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace line_gestao_api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/solicitacoes-linhas")]
|
||||
[Authorize]
|
||||
public class SolicitacoesLinhasController : ControllerBase
|
||||
{
|
||||
private const string TipoAlteracaoFranquia = "ALTERACAO_FRANQUIA";
|
||||
private const string TipoBloqueio = "BLOQUEIO";
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public SolicitacoesLinhasController(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Roles = "sysadmin,gestor,cliente")]
|
||||
public async Task<ActionResult<SolicitacaoLinhaListDto>> Create([FromBody] CreateSolicitacaoLinhaRequestDto req)
|
||||
{
|
||||
if (req.LineId == Guid.Empty)
|
||||
{
|
||||
return BadRequest(new { message = "Linha inválida para solicitação." });
|
||||
}
|
||||
|
||||
var line = await _db.MobileLines
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == req.LineId);
|
||||
if (line == null)
|
||||
{
|
||||
return NotFound(new { message = "Linha não encontrada." });
|
||||
}
|
||||
|
||||
var tipoSolicitacao = NormalizeTipoSolicitacao(req.TipoSolicitacao);
|
||||
if (tipoSolicitacao == null)
|
||||
{
|
||||
return BadRequest(new { message = "Tipo de solicitação inválido. Use 'alteracao-franquia' ou 'bloqueio'." });
|
||||
}
|
||||
|
||||
decimal? franquiaLineNova = null;
|
||||
if (tipoSolicitacao == TipoAlteracaoFranquia)
|
||||
{
|
||||
if (!req.FranquiaLineNova.HasValue)
|
||||
{
|
||||
return BadRequest(new { message = "Informe a nova franquia para solicitar alteração." });
|
||||
}
|
||||
|
||||
franquiaLineNova = decimal.Round(req.FranquiaLineNova.Value, 2, MidpointRounding.AwayFromZero);
|
||||
if (franquiaLineNova < 0)
|
||||
{
|
||||
return BadRequest(new { message = "A nova franquia não pode ser negativa." });
|
||||
}
|
||||
}
|
||||
|
||||
var solicitanteNome = ResolveSolicitanteNome();
|
||||
var usuarioLinha = NormalizeOptionalText(line.Usuario) ?? solicitanteNome;
|
||||
var linha = NormalizeOptionalText(line.Linha) ?? "-";
|
||||
var mensagem = tipoSolicitacao == TipoAlteracaoFranquia
|
||||
? $"O Usuário \"{usuarioLinha}\" solicitou alteração da linha \"{linha}\" \"{FormatFranquia(line.FranquiaLine)}\" -> \"{FormatFranquia(franquiaLineNova)}\""
|
||||
: $"O Usuário \"{usuarioLinha}\" solicitou bloqueio da linha \"{linha}\"";
|
||||
|
||||
var solicitacao = new SolicitacaoLinha
|
||||
{
|
||||
TenantId = line.TenantId,
|
||||
MobileLineId = line.Id,
|
||||
Linha = NormalizeOptionalText(line.Linha),
|
||||
UsuarioLinha = NormalizeOptionalText(line.Usuario),
|
||||
TipoSolicitacao = tipoSolicitacao,
|
||||
FranquiaLineAtual = line.FranquiaLine,
|
||||
FranquiaLineNova = franquiaLineNova,
|
||||
SolicitanteUserId = ResolveSolicitanteUserId(),
|
||||
SolicitanteNome = solicitanteNome,
|
||||
Mensagem = mensagem,
|
||||
Status = "PENDENTE",
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_db.SolicitacaoLinhas.Add(solicitacao);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var tenantNome = await _db.Tenants
|
||||
.AsNoTracking()
|
||||
.Where(t => t.Id == solicitacao.TenantId)
|
||||
.Select(t => t.NomeOficial)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return Ok(ToDto(solicitacao, tenantNome));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
public async Task<ActionResult<PagedResult<SolicitacaoLinhaListDto>>> List(
|
||||
[FromQuery] string? search,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20)
|
||||
{
|
||||
page = page < 1 ? 1 : page;
|
||||
pageSize = pageSize < 1 ? 20 : Math.Min(pageSize, 200);
|
||||
|
||||
var query =
|
||||
from solicitacao in _db.SolicitacaoLinhas.AsNoTracking()
|
||||
join tenant in _db.Tenants.AsNoTracking()
|
||||
on solicitacao.TenantId equals tenant.Id into tenantJoin
|
||||
from tenant in tenantJoin.DefaultIfEmpty()
|
||||
select new
|
||||
{
|
||||
Solicitacao = solicitacao,
|
||||
TenantNome = tenant != null ? tenant.NomeOficial : null
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
var term = search.Trim();
|
||||
query = query.Where(x =>
|
||||
EF.Functions.ILike(x.Solicitacao.Linha ?? "", $"%{term}%") ||
|
||||
EF.Functions.ILike(x.Solicitacao.UsuarioLinha ?? "", $"%{term}%") ||
|
||||
EF.Functions.ILike(x.Solicitacao.SolicitanteNome ?? "", $"%{term}%") ||
|
||||
EF.Functions.ILike(x.Solicitacao.Mensagem ?? "", $"%{term}%"));
|
||||
}
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var items = await query
|
||||
.OrderByDescending(x => x.Solicitacao.CreatedAt)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(x => new SolicitacaoLinhaListDto
|
||||
{
|
||||
Id = x.Solicitacao.Id,
|
||||
TenantId = x.Solicitacao.TenantId,
|
||||
TenantNome = x.TenantNome,
|
||||
MobileLineId = x.Solicitacao.MobileLineId,
|
||||
Linha = x.Solicitacao.Linha,
|
||||
UsuarioLinha = x.Solicitacao.UsuarioLinha,
|
||||
TipoSolicitacao = x.Solicitacao.TipoSolicitacao,
|
||||
FranquiaLineAtual = x.Solicitacao.FranquiaLineAtual,
|
||||
FranquiaLineNova = x.Solicitacao.FranquiaLineNova,
|
||||
SolicitanteNome = x.Solicitacao.SolicitanteNome,
|
||||
Mensagem = x.Solicitacao.Mensagem,
|
||||
Status = x.Solicitacao.Status,
|
||||
CreatedAt = x.Solicitacao.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(new PagedResult<SolicitacaoLinhaListDto>
|
||||
{
|
||||
Page = page,
|
||||
PageSize = pageSize,
|
||||
Total = total,
|
||||
Items = items
|
||||
});
|
||||
}
|
||||
|
||||
private static string? NormalizeTipoSolicitacao(string? tipoSolicitacao)
|
||||
{
|
||||
var value = (tipoSolicitacao ?? string.Empty).Trim().ToLowerInvariant();
|
||||
return value switch
|
||||
{
|
||||
"alteracao-franquia" => TipoAlteracaoFranquia,
|
||||
"alteracao_franquia" => TipoAlteracaoFranquia,
|
||||
"alteracaofranquia" => TipoAlteracaoFranquia,
|
||||
"franquia" => TipoAlteracaoFranquia,
|
||||
"bloqueio" => TipoBloqueio,
|
||||
"solicitar-bloqueio" => TipoBloqueio,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private string ResolveSolicitanteNome()
|
||||
{
|
||||
var fromClaim = User.FindFirstValue("name");
|
||||
if (!string.IsNullOrWhiteSpace(fromClaim))
|
||||
{
|
||||
return fromClaim.Trim();
|
||||
}
|
||||
|
||||
var fromIdentity = User.Identity?.Name;
|
||||
if (!string.IsNullOrWhiteSpace(fromIdentity))
|
||||
{
|
||||
return fromIdentity.Trim();
|
||||
}
|
||||
|
||||
var fromEmail = User.FindFirstValue(ClaimTypes.Email) ?? User.FindFirstValue("email");
|
||||
if (!string.IsNullOrWhiteSpace(fromEmail))
|
||||
{
|
||||
return fromEmail.Trim();
|
||||
}
|
||||
|
||||
return "Usuário";
|
||||
}
|
||||
|
||||
private Guid? ResolveSolicitanteUserId()
|
||||
{
|
||||
var raw =
|
||||
User.FindFirstValue(ClaimTypes.NameIdentifier) ??
|
||||
User.FindFirstValue("sub");
|
||||
|
||||
return Guid.TryParse(raw, out var parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
private static string? NormalizeOptionalText(string? value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
|
||||
private static string FormatFranquia(decimal? value)
|
||||
{
|
||||
return value.HasValue
|
||||
? value.Value.ToString("0.##", CultureInfo.GetCultureInfo("pt-BR"))
|
||||
: "-";
|
||||
}
|
||||
|
||||
private static SolicitacaoLinhaListDto ToDto(SolicitacaoLinha solicitacao, string? tenantNome)
|
||||
{
|
||||
return new SolicitacaoLinhaListDto
|
||||
{
|
||||
Id = solicitacao.Id,
|
||||
TenantId = solicitacao.TenantId,
|
||||
TenantNome = tenantNome,
|
||||
MobileLineId = solicitacao.MobileLineId,
|
||||
Linha = solicitacao.Linha,
|
||||
UsuarioLinha = solicitacao.UsuarioLinha,
|
||||
TipoSolicitacao = solicitacao.TipoSolicitacao,
|
||||
FranquiaLineAtual = solicitacao.FranquiaLineAtual,
|
||||
FranquiaLineNova = solicitacao.FranquiaLineNova,
|
||||
SolicitanteNome = solicitacao.SolicitanteNome,
|
||||
Mensagem = solicitacao.Mensagem,
|
||||
Status = solicitacao.Status,
|
||||
CreatedAt = solicitacao.CreatedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ namespace line_gestao_api.Controllers
|
|||
{
|
||||
[ApiController]
|
||||
[Route("api/templates")]
|
||||
[Authorize(Roles = "sysadmin,gestor,financeiro")]
|
||||
[Authorize(Roles = "sysadmin,gestor")]
|
||||
public class TemplatesController : ControllerBase
|
||||
{
|
||||
private readonly GeralSpreadsheetTemplateService _geralSpreadsheetTemplateService;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ public class UsersController : ControllerBase
|
|||
{
|
||||
AppRoles.SysAdmin,
|
||||
AppRoles.Gestor,
|
||||
AppRoles.Financeiro,
|
||||
AppRoles.Cliente
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
|||
|
||||
// ✅ tabela NOTIFICAÇÕES
|
||||
public DbSet<Notification> Notifications => Set<Notification>();
|
||||
public DbSet<SolicitacaoLinha> SolicitacaoLinhas => Set<SolicitacaoLinha>();
|
||||
|
||||
// ✅ tabela RESUMO
|
||||
public DbSet<ResumoMacrophonyPlan> ResumoMacrophonyPlans => Set<ResumoMacrophonyPlan>();
|
||||
|
|
@ -282,26 +281,6 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
|||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<SolicitacaoLinha>(e =>
|
||||
{
|
||||
e.HasIndex(x => x.TenantId);
|
||||
e.HasIndex(x => x.CreatedAt);
|
||||
e.HasIndex(x => x.TipoSolicitacao);
|
||||
e.HasIndex(x => x.Status);
|
||||
e.HasIndex(x => x.MobileLineId);
|
||||
e.HasIndex(x => x.SolicitanteUserId);
|
||||
|
||||
e.HasOne(x => x.MobileLine)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.MobileLineId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
e.HasOne(x => x.SolicitanteUser)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.SolicitanteUserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
// =========================
|
||||
// ✅ PARCELAMENTOS
|
||||
// =========================
|
||||
|
|
@ -401,7 +380,6 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
|||
modelBuilder.Entity<ChipVirgemLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ControleRecebidoLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<Notification>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<SolicitacaoLinha>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ResumoMacrophonyPlan>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ResumoMacrophonyTotal>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
modelBuilder.Entity<ResumoVivoLineResumo>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
|
||||
|
|
|
|||
|
|
@ -71,42 +71,4 @@ 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
namespace line_gestao_api.Dtos;
|
||||
|
||||
public class CreateSolicitacaoLinhaRequestDto
|
||||
{
|
||||
public Guid LineId { get; set; }
|
||||
public string TipoSolicitacao { get; set; } = string.Empty;
|
||||
public decimal? FranquiaLineNova { get; set; }
|
||||
}
|
||||
|
||||
public class SolicitacaoLinhaListDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid TenantId { get; set; }
|
||||
public string? TenantNome { get; set; }
|
||||
public Guid? MobileLineId { get; set; }
|
||||
public string? Linha { get; set; }
|
||||
public string? UsuarioLinha { get; set; }
|
||||
public string TipoSolicitacao { get; set; } = string.Empty;
|
||||
public decimal? FranquiaLineAtual { get; set; }
|
||||
public decimal? FranquiaLineNova { get; set; }
|
||||
public string? SolicitanteNome { get; set; }
|
||||
public string Mensagem { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
using line_gestao_api.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace line_gestao_api.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260305193000_AddSolicitacaoLinhas")]
|
||||
public class AddSolicitacaoLinhas : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
CREATE TABLE IF NOT EXISTS "SolicitacaoLinhas" (
|
||||
"Id" uuid NOT NULL,
|
||||
"TenantId" uuid NOT NULL,
|
||||
"MobileLineId" uuid NULL,
|
||||
"Linha" character varying(30) NULL,
|
||||
"UsuarioLinha" character varying(200) NULL,
|
||||
"TipoSolicitacao" character varying(60) NOT NULL,
|
||||
"FranquiaLineAtual" numeric NULL,
|
||||
"FranquiaLineNova" numeric NULL,
|
||||
"SolicitanteUserId" uuid NULL,
|
||||
"SolicitanteNome" character varying(200) NULL,
|
||||
"Mensagem" character varying(1000) NOT NULL,
|
||||
"Status" character varying(30) NOT NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL,
|
||||
CONSTRAINT "PK_SolicitacaoLinhas" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "FK_SolicitacaoLinhas_AspNetUsers_SolicitanteUserId"
|
||||
FOREIGN KEY ("SolicitanteUserId") REFERENCES "AspNetUsers" ("Id")
|
||||
ON DELETE SET NULL,
|
||||
CONSTRAINT "FK_SolicitacaoLinhas_MobileLines_MobileLineId"
|
||||
FOREIGN KEY ("MobileLineId") REFERENCES "MobileLines" ("Id")
|
||||
ON DELETE SET NULL
|
||||
);
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_TenantId" ON "SolicitacaoLinhas" ("TenantId");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_CreatedAt" ON "SolicitacaoLinhas" ("CreatedAt");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_TipoSolicitacao" ON "SolicitacaoLinhas" ("TipoSolicitacao");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_Status" ON "SolicitacaoLinhas" ("Status");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_MobileLineId" ON "SolicitacaoLinhas" ("MobileLineId");""");
|
||||
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_SolicitacaoLinhas_SolicitanteUserId" ON "SolicitacaoLinhas" ("SolicitanteUserId");""");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_SolicitanteUserId";""");
|
||||
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_MobileLineId";""");
|
||||
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_Status";""");
|
||||
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_TipoSolicitacao";""");
|
||||
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_CreatedAt";""");
|
||||
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_SolicitacaoLinhas_TenantId";""");
|
||||
migrationBuilder.Sql("""DROP TABLE IF EXISTS "SolicitacaoLinhas";""");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -950,74 +950,6 @@ namespace line_gestao_api.Migrations
|
|||
b.ToTable("Notifications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.SolicitacaoLinha", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<decimal?>("FranquiaLineAtual")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<decimal?>("FranquiaLineNova")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Linha")
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("Mensagem")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<Guid?>("MobileLineId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("SolicitanteNome")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<Guid?>("SolicitanteUserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("TipoSolicitacao")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("character varying(60)");
|
||||
|
||||
b.Property<string>("UsuarioLinha")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("MobileLineId");
|
||||
|
||||
b.HasIndex("SolicitanteUserId");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.HasIndex("TipoSolicitacao");
|
||||
|
||||
b.ToTable("SolicitacaoLinhas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.ParcelamentoLine", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -1881,23 +1813,6 @@ namespace line_gestao_api.Migrations
|
|||
b.Navigation("VigenciaLine");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.SolicitacaoLinha", b =>
|
||||
{
|
||||
b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine")
|
||||
.WithMany()
|
||||
.HasForeignKey("MobileLineId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("line_gestao_api.Models.ApplicationUser", "SolicitanteUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("SolicitanteUserId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("MobileLine");
|
||||
|
||||
b.Navigation("SolicitanteUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.ParcelamentoMonthValue", b =>
|
||||
{
|
||||
b.HasOne("line_gestao_api.Models.ParcelamentoLine", "ParcelamentoLine")
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace line_gestao_api.Models;
|
||||
|
||||
public class SolicitacaoLinha : ITenantEntity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
public Guid TenantId { get; set; }
|
||||
|
||||
public Guid? MobileLineId { get; set; }
|
||||
public MobileLine? MobileLine { get; set; }
|
||||
|
||||
[MaxLength(30)]
|
||||
public string? Linha { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? UsuarioLinha { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(60)]
|
||||
public string TipoSolicitacao { get; set; } = string.Empty;
|
||||
|
||||
public decimal? FranquiaLineAtual { get; set; }
|
||||
public decimal? FranquiaLineNova { get; set; }
|
||||
|
||||
public Guid? SolicitanteUserId { get; set; }
|
||||
public ApplicationUser? SolicitanteUser { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? SolicitanteNome { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(1000)]
|
||||
public string Mensagem { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[MaxLength(30)]
|
||||
public string Status { get; set; } = "PENDENTE";
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
|
@ -4,8 +4,7 @@ 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, Financeiro, Cliente];
|
||||
public static readonly string[] All = [SysAdmin, Gestor, Cliente];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ public class TenantProvider : ITenantProvider
|
|||
|
||||
public bool HasGlobalViewAccess =>
|
||||
HasRole(AppRoles.SysAdmin) ||
|
||||
HasRole(AppRoles.Gestor) ||
|
||||
HasRole(AppRoles.Financeiro);
|
||||
HasRole(AppRoles.Gestor);
|
||||
|
||||
public void SetTenantId(Guid? tenantId)
|
||||
{
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB |
Loading…
Reference in New Issue