Compare commits

..

3 Commits

Author SHA1 Message Date
Leon 64ffb9f2e5 Merge remote-tracking branch 'origin/feat/exportar-em-todas-as-paginas' into dev 2026-03-06 14:15:51 -03:00
Eduardo Lopes 82dd0bf2d0 Feat: Novas Implementações 2026-03-06 13:08:52 -03:00
Leon 8a562777df feat: Cliente poder alterar e solicitar alteracoes 2026-03-05 18:32:05 -03:00
21 changed files with 1130 additions and 38 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
// ==========================================================
@ -1339,22 +1500,31 @@ 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,
x.TenantId,
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
@ -1415,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);
@ -1859,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:
@ -1873,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;
@ -1889,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);
@ -1920,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)
@ -1959,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,
@ -4983,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

@ -0,0 +1,240 @@
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
};
}
}

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

@ -50,6 +50,7 @@ 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>();
@ -281,6 +282,26 @@ 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
// =========================
@ -380,6 +401,7 @@ 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));

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

@ -0,0 +1,25 @@
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; }
}

View File

@ -0,0 +1,59 @@
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";""");
}
}
}

View File

@ -950,6 +950,74 @@ 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")
@ -1813,6 +1881,23 @@ 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")

View File

@ -0,0 +1,42 @@
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;
}

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