Compare commits
4 Commits
8f0fa83b78
...
1f255888b0
| Author | SHA1 | Date |
|---|---|---|
|
|
1f255888b0 | |
|
|
024b7d299d | |
|
|
242f8bc707 | |
|
|
7a7b5db73e |
|
|
@ -6,6 +6,9 @@
|
||||||
# dotenv files
|
# dotenv files
|
||||||
.env
|
.env
|
||||||
appsettings.Local.json
|
appsettings.Local.json
|
||||||
|
appsettings*.json
|
||||||
|
line-gestao-api.csproj
|
||||||
|
line-gestao-api.http
|
||||||
|
|
||||||
# User-specific files
|
# User-specific files
|
||||||
*.rsuser
|
*.rsuser
|
||||||
|
|
|
||||||
|
|
@ -43,11 +43,20 @@ namespace line_gestao_api.Controllers
|
||||||
var s = search.Trim();
|
var s = search.Trim();
|
||||||
var hasNumberSearch = TryParseSearchDecimal(s, out var searchNumber);
|
var hasNumberSearch = TryParseSearchDecimal(s, out var searchNumber);
|
||||||
var hasIntSearch = int.TryParse(new string(s.Where(char.IsDigit).ToArray()), out var searchInt);
|
var hasIntSearch = int.TryParse(new string(s.Where(char.IsDigit).ToArray()), out var searchInt);
|
||||||
|
var matchingClientsByLineOrChip = _db.MobileLines.AsNoTracking()
|
||||||
|
.Where(m =>
|
||||||
|
EF.Functions.ILike(m.Linha ?? "", $"%{s}%") ||
|
||||||
|
EF.Functions.ILike(m.Chip ?? "", $"%{s}%"))
|
||||||
|
.Where(m => m.Cliente != null && m.Cliente != "")
|
||||||
|
.Select(m => m.Cliente!)
|
||||||
|
.Distinct();
|
||||||
|
|
||||||
q = q.Where(x => EF.Functions.ILike(x.Cliente ?? "", $"%{s}%")
|
q = q.Where(x => EF.Functions.ILike(x.Cliente ?? "", $"%{s}%")
|
||||||
|| EF.Functions.ILike(x.Tipo ?? "", $"%{s}%")
|
|| EF.Functions.ILike(x.Tipo ?? "", $"%{s}%")
|
||||||
|| EF.Functions.ILike(x.Item.ToString(), $"%{s}%")
|
|| EF.Functions.ILike(x.Item.ToString(), $"%{s}%")
|
||||||
|| EF.Functions.ILike(x.Aparelho ?? "", $"%{s}%")
|
|| EF.Functions.ILike(x.Aparelho ?? "", $"%{s}%")
|
||||||
|| EF.Functions.ILike(x.FormaPagamento ?? "", $"%{s}%")
|
|| EF.Functions.ILike(x.FormaPagamento ?? "", $"%{s}%")
|
||||||
|
|| (x.Cliente != null && matchingClientsByLineOrChip.Contains(x.Cliente))
|
||||||
|| (hasIntSearch && (x.QtdLinhas ?? 0) == searchInt)
|
|| (hasIntSearch && (x.QtdLinhas ?? 0) == searchInt)
|
||||||
|| (hasNumberSearch &&
|
|| (hasNumberSearch &&
|
||||||
(((x.FranquiaVivo ?? 0m) == searchNumber) ||
|
(((x.FranquiaVivo ?? 0m) == searchNumber) ||
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ public class HistoricoController : ControllerBase
|
||||||
[FromQuery] string? pageName,
|
[FromQuery] string? pageName,
|
||||||
[FromQuery] string? action,
|
[FromQuery] string? action,
|
||||||
[FromQuery] string? entity,
|
[FromQuery] string? entity,
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery] string? user,
|
||||||
[FromQuery] string? search,
|
[FromQuery] string? search,
|
||||||
[FromQuery] DateTime? dateFrom,
|
[FromQuery] DateTime? dateFrom,
|
||||||
[FromQuery] DateTime? dateTo,
|
[FromQuery] DateTime? dateTo,
|
||||||
|
|
@ -60,15 +60,17 @@ public class HistoricoController : ControllerBase
|
||||||
q = q.Where(x => EF.Functions.ILike(x.EntityName, $"%{e}%"));
|
q = q.Where(x => EF.Functions.ILike(x.EntityName, $"%{e}%"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId.HasValue)
|
if (!string.IsNullOrWhiteSpace(user))
|
||||||
{
|
{
|
||||||
q = q.Where(x => x.UserId == userId.Value);
|
var u = user.Trim();
|
||||||
|
q = q.Where(x =>
|
||||||
|
EF.Functions.ILike(x.UserName ?? "", $"%{u}%") ||
|
||||||
|
EF.Functions.ILike(x.UserEmail ?? "", $"%{u}%"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(search))
|
if (!string.IsNullOrWhiteSpace(search))
|
||||||
{
|
{
|
||||||
var s = search.Trim();
|
var s = search.Trim();
|
||||||
var hasGuidSearch = Guid.TryParse(s, out var searchGuid);
|
|
||||||
var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc);
|
var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc);
|
||||||
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
|
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
|
||||||
q = q.Where(x =>
|
q = q.Where(x =>
|
||||||
|
|
@ -83,7 +85,6 @@ public class HistoricoController : ControllerBase
|
||||||
EF.Functions.ILike(x.RequestMethod ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.RequestMethod ?? "", $"%{s}%") ||
|
||||||
EF.Functions.ILike(x.IpAddress ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.IpAddress ?? "", $"%{s}%") ||
|
||||||
// ChangesJson is stored as jsonb; applying ILIKE directly causes PostgreSQL 42883.
|
// ChangesJson is stored as jsonb; applying ILIKE directly causes PostgreSQL 42883.
|
||||||
(hasGuidSearch && (x.Id == searchGuid || x.UserId == searchGuid)) ||
|
|
||||||
(hasDateSearch &&
|
(hasDateSearch &&
|
||||||
x.OccurredAtUtc >= searchDateStartUtc &&
|
x.OccurredAtUtc >= searchDateStartUtc &&
|
||||||
x.OccurredAtUtc < searchDateEndUtc));
|
x.OccurredAtUtc < searchDateEndUtc));
|
||||||
|
|
|
||||||
|
|
@ -98,13 +98,16 @@ namespace line_gestao_api.Controllers
|
||||||
reservaFilter = true;
|
reservaFilter = true;
|
||||||
query = query.Where(x =>
|
query = query.Where(x =>
|
||||||
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
||||||
(EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA") &&
|
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") ||
|
||||||
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA")));
|
EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
query = query.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%"));
|
query = query.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!reservaFilter)
|
||||||
|
query = ExcludeReservaContext(query);
|
||||||
|
|
||||||
query = ApplyAdditionalFilters(query, additionalMode, additionalServices);
|
query = ApplyAdditionalFilters(query, additionalMode, additionalServices);
|
||||||
|
|
||||||
IQueryable<ClientGroupDto> groupedQuery;
|
IQueryable<ClientGroupDto> groupedQuery;
|
||||||
|
|
@ -225,13 +228,16 @@ namespace line_gestao_api.Controllers
|
||||||
reservaFilter = true;
|
reservaFilter = true;
|
||||||
query = query.Where(x =>
|
query = query.Where(x =>
|
||||||
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
||||||
(EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA") &&
|
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") ||
|
||||||
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA")));
|
EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
query = query.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%"));
|
query = query.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!reservaFilter)
|
||||||
|
query = ExcludeReservaContext(query);
|
||||||
|
|
||||||
query = ApplyAdditionalFilters(query, additionalMode, additionalServices);
|
query = ApplyAdditionalFilters(query, additionalMode, additionalServices);
|
||||||
|
|
||||||
List<string> clients;
|
List<string> clients;
|
||||||
|
|
@ -461,13 +467,16 @@ namespace line_gestao_api.Controllers
|
||||||
reservaFilter = true;
|
reservaFilter = true;
|
||||||
q = q.Where(x =>
|
q = q.Where(x =>
|
||||||
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
||||||
(EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA") &&
|
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") ||
|
||||||
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA")));
|
EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
q = q.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%"));
|
q = q.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!reservaFilter)
|
||||||
|
q = ExcludeReservaContext(q);
|
||||||
|
|
||||||
q = ApplyAdditionalFilters(q, additionalMode, additionalServices);
|
q = ApplyAdditionalFilters(q, additionalMode, additionalServices);
|
||||||
|
|
||||||
var sb = (sortBy ?? "item").Trim().ToLowerInvariant();
|
var sb = (sortBy ?? "item").Trim().ToLowerInvariant();
|
||||||
|
|
@ -711,6 +720,13 @@ namespace line_gestao_api.Controllers
|
||||||
if (exists)
|
if (exists)
|
||||||
return Conflict(new { message = $"A linha {req.Linha} já está cadastrada no sistema." });
|
return Conflict(new { message = $"A linha {req.Linha} já está cadastrada no sistema." });
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(chipLimpo))
|
||||||
|
{
|
||||||
|
var chipExists = await _db.MobileLines.AsNoTracking().AnyAsync(x => x.Chip == chipLimpo);
|
||||||
|
if (chipExists)
|
||||||
|
return Conflict(new { message = $"O Chip (ICCID) {req.Chip} já está cadastrado no sistema." });
|
||||||
|
}
|
||||||
|
|
||||||
var maxItem = await _db.MobileLines.MaxAsync(x => (int?)x.Item) ?? 0;
|
var maxItem = await _db.MobileLines.MaxAsync(x => (int?)x.Item) ?? 0;
|
||||||
var nextItem = maxItem + 1;
|
var nextItem = maxItem + 1;
|
||||||
|
|
||||||
|
|
@ -805,6 +821,12 @@ namespace line_gestao_api.Controllers
|
||||||
.Distinct(StringComparer.Ordinal)
|
.Distinct(StringComparer.Ordinal)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
var requestedChips = requests
|
||||||
|
.Select(x => OnlyDigits(x?.Chip))
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
var existingLinhas = requestedLinhas.Count == 0
|
var existingLinhas = requestedLinhas.Count == 0
|
||||||
? new HashSet<string>(StringComparer.Ordinal)
|
? new HashSet<string>(StringComparer.Ordinal)
|
||||||
: (await _db.MobileLines.AsNoTracking()
|
: (await _db.MobileLines.AsNoTracking()
|
||||||
|
|
@ -813,12 +835,21 @@ namespace line_gestao_api.Controllers
|
||||||
.ToListAsync())
|
.ToListAsync())
|
||||||
.ToHashSet(StringComparer.Ordinal);
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var existingChips = requestedChips.Count == 0
|
||||||
|
? new HashSet<string>(StringComparer.Ordinal)
|
||||||
|
: (await _db.MobileLines.AsNoTracking()
|
||||||
|
.Where(x => x.Chip != null && requestedChips.Contains(x.Chip))
|
||||||
|
.Select(x => x.Chip!)
|
||||||
|
.ToListAsync())
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
await using var tx = await _db.Database.BeginTransactionAsync();
|
await using var tx = await _db.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var nextItem = (await _db.MobileLines.MaxAsync(x => (int?)x.Item) ?? 0);
|
var nextItem = (await _db.MobileLines.MaxAsync(x => (int?)x.Item) ?? 0);
|
||||||
var seenBatchLinhas = new HashSet<string>(StringComparer.Ordinal);
|
var seenBatchLinhas = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
var seenBatchChips = new HashSet<string>(StringComparer.Ordinal);
|
||||||
var createdLines = new List<(MobileLine line, VigenciaLine? vigencia)>(requests.Count);
|
var createdLines = new List<(MobileLine line, VigenciaLine? vigencia)>(requests.Count);
|
||||||
|
|
||||||
for (var i = 0; i < requests.Count; i++)
|
for (var i = 0; i < requests.Count; i++)
|
||||||
|
|
@ -853,6 +884,15 @@ namespace line_gestao_api.Controllers
|
||||||
if (existingLinhas.Contains(linhaLimpa))
|
if (existingLinhas.Contains(linhaLimpa))
|
||||||
return Conflict(new { message = $"A linha {entry.Linha} já está cadastrada no sistema (registro #{lineNo})." });
|
return Conflict(new { message = $"A linha {entry.Linha} já está cadastrada no sistema (registro #{lineNo})." });
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(chipLimpo))
|
||||||
|
{
|
||||||
|
if (!seenBatchChips.Add(chipLimpo))
|
||||||
|
return Conflict(new { message = $"O Chip (ICCID) {entry.Chip} está duplicado dentro do lote (registro #{lineNo})." });
|
||||||
|
|
||||||
|
if (existingChips.Contains(chipLimpo))
|
||||||
|
return Conflict(new { message = $"O Chip (ICCID) {entry.Chip} já está cadastrado no sistema (registro #{lineNo})." });
|
||||||
|
}
|
||||||
|
|
||||||
nextItem++;
|
nextItem++;
|
||||||
|
|
||||||
var planSuggestion = await AutoFillRules.ResolvePlanSuggestionAsync(_db, entry.PlanoContrato);
|
var planSuggestion = await AutoFillRules.ResolvePlanSuggestionAsync(_db, entry.PlanoContrato);
|
||||||
|
|
@ -941,6 +981,281 @@ namespace line_gestao_api.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// ✅ 5.2. PREVIEW IMPORTAÇÃO EXCEL PARA LOTE (ABA GERAL)
|
||||||
|
// ==========================================================
|
||||||
|
[HttpPost("batch/import-preview")]
|
||||||
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
|
[Consumes("multipart/form-data")]
|
||||||
|
[RequestSizeLimit(20_000_000)]
|
||||||
|
public async Task<ActionResult<LinesBatchExcelPreviewResultDto>> PreviewBatchImportExcel([FromForm] ImportExcelForm form)
|
||||||
|
{
|
||||||
|
var tenantId = _tenantProvider.TenantId;
|
||||||
|
if (!tenantId.HasValue || tenantId.Value == Guid.Empty)
|
||||||
|
return Unauthorized("Tenant inválido.");
|
||||||
|
|
||||||
|
var file = form.File;
|
||||||
|
if (file == null || file.Length == 0)
|
||||||
|
return BadRequest(new { message = "Arquivo inválido." });
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = file.OpenReadStream();
|
||||||
|
using var wb = new XLWorkbook(stream);
|
||||||
|
var preview = await BuildLinesBatchExcelPreviewAsync(wb, file.FileName);
|
||||||
|
return Ok(preview);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BadRequest(new { message = $"Falha ao ler planilha: {ex.Message}" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// ✅ 5.3. ATRIBUIR LINHAS DA RESERVA PARA CLIENTE
|
||||||
|
// ==========================================================
|
||||||
|
[HttpPost("reserva/assign-client")]
|
||||||
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
|
public async Task<ActionResult<AssignReservaLinesResultDto>> AssignReservaLinesToClient([FromBody] AssignReservaLinesRequestDto req)
|
||||||
|
{
|
||||||
|
var ids = (req?.LineIds ?? new List<Guid>())
|
||||||
|
.Where(x => x != Guid.Empty)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (ids.Count == 0)
|
||||||
|
return BadRequest(new { message = "Selecione ao menos uma linha da Reserva." });
|
||||||
|
|
||||||
|
if (ids.Count > 1000)
|
||||||
|
return BadRequest(new { message = "Limite de 1000 linhas por atribuição em lote." });
|
||||||
|
|
||||||
|
var clienteDestino = (req?.ClienteDestino ?? "").Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(clienteDestino))
|
||||||
|
return BadRequest(new { message = "Informe o cliente de destino." });
|
||||||
|
|
||||||
|
if (IsReservaValue(clienteDestino))
|
||||||
|
return BadRequest(new { message = "O cliente de destino não pode ser RESERVA." });
|
||||||
|
|
||||||
|
var usuarioDestino = string.IsNullOrWhiteSpace(req?.UsuarioDestino) ? null : req!.UsuarioDestino!.Trim();
|
||||||
|
var skilDestinoSolicitado = string.IsNullOrWhiteSpace(req?.SkilDestino) ? null : req!.SkilDestino!.Trim();
|
||||||
|
var clienteDestinoUpper = clienteDestino.ToUpperInvariant();
|
||||||
|
|
||||||
|
var linhas = await _db.MobileLines
|
||||||
|
.Where(x => ids.Contains(x.Id))
|
||||||
|
.OrderBy(x => x.Item)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var byId = linhas.ToDictionary(x => x.Id, x => x);
|
||||||
|
|
||||||
|
var inferSkilDestino = await _db.MobileLines.AsNoTracking()
|
||||||
|
.Where(x => x.Cliente != null && EF.Functions.ILike(x.Cliente, clienteDestino))
|
||||||
|
.Where(x => x.Skil != null && x.Skil != "" && !EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA"))
|
||||||
|
.OrderByDescending(x => x.UpdatedAt)
|
||||||
|
.Select(x => x.Skil)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
var result = new AssignReservaLinesResultDto
|
||||||
|
{
|
||||||
|
Requested = ids.Count
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var id in ids)
|
||||||
|
{
|
||||||
|
if (!byId.TryGetValue(id, out var line))
|
||||||
|
{
|
||||||
|
result.Items.Add(new AssignReservaLineItemResultDto
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Success = false,
|
||||||
|
Message = "Linha não encontrada."
|
||||||
|
});
|
||||||
|
result.Failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsReservaLineForTransfer(line))
|
||||||
|
{
|
||||||
|
result.Items.Add(new AssignReservaLineItemResultDto
|
||||||
|
{
|
||||||
|
Id = line.Id,
|
||||||
|
Item = line.Item,
|
||||||
|
Linha = line.Linha,
|
||||||
|
Chip = line.Chip,
|
||||||
|
ClienteAnterior = line.Cliente,
|
||||||
|
ClienteNovo = line.Cliente,
|
||||||
|
Success = false,
|
||||||
|
Message = "A linha não está apta para transferência (não está na Reserva)."
|
||||||
|
});
|
||||||
|
result.Failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clienteAnterior = line.Cliente;
|
||||||
|
line.Cliente = clienteDestinoUpper;
|
||||||
|
|
||||||
|
if (IsReservaValue(line.Usuario))
|
||||||
|
{
|
||||||
|
line.Usuario = usuarioDestino;
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(usuarioDestino))
|
||||||
|
{
|
||||||
|
line.Usuario = usuarioDestino;
|
||||||
|
}
|
||||||
|
|
||||||
|
var skilDestinoEfetivo = skilDestinoSolicitado;
|
||||||
|
if (string.IsNullOrWhiteSpace(skilDestinoEfetivo) && IsReservaValue(line.Skil))
|
||||||
|
{
|
||||||
|
skilDestinoEfetivo = inferSkilDestino;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(skilDestinoEfetivo))
|
||||||
|
{
|
||||||
|
line.Skil = skilDestinoEfetivo;
|
||||||
|
}
|
||||||
|
else if (IsReservaValue(line.Skil))
|
||||||
|
{
|
||||||
|
line.Skil = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
line.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
result.Items.Add(new AssignReservaLineItemResultDto
|
||||||
|
{
|
||||||
|
Id = line.Id,
|
||||||
|
Item = line.Item,
|
||||||
|
Linha = line.Linha,
|
||||||
|
Chip = line.Chip,
|
||||||
|
ClienteAnterior = clienteAnterior,
|
||||||
|
ClienteNovo = line.Cliente,
|
||||||
|
Success = true,
|
||||||
|
Message = "Linha atribuída com sucesso."
|
||||||
|
});
|
||||||
|
result.Updated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Updated <= 0)
|
||||||
|
{
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
await _vigenciaNotificationSyncService.SyncCurrentTenantAsync();
|
||||||
|
result.Failed = result.Requested - result.Updated;
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (DbUpdateException)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { message = "Erro ao atribuir linhas da Reserva." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// ✅ 5.4. MOVER LINHAS DE CLIENTE PARA RESERVA
|
||||||
|
// ==========================================================
|
||||||
|
[HttpPost("move-to-reserva")]
|
||||||
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
|
public async Task<ActionResult<AssignReservaLinesResultDto>> MoveLinesToReserva([FromBody] MoveLinesToReservaRequestDto req)
|
||||||
|
{
|
||||||
|
var ids = (req?.LineIds ?? new List<Guid>())
|
||||||
|
.Where(x => x != Guid.Empty)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (ids.Count == 0)
|
||||||
|
return BadRequest(new { message = "Selecione ao menos uma linha para enviar à Reserva." });
|
||||||
|
|
||||||
|
if (ids.Count > 1000)
|
||||||
|
return BadRequest(new { message = "Limite de 1000 linhas por movimentação em lote." });
|
||||||
|
|
||||||
|
var linhas = await _db.MobileLines
|
||||||
|
.Where(x => ids.Contains(x.Id))
|
||||||
|
.OrderBy(x => x.Item)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var byId = linhas.ToDictionary(x => x.Id, x => x);
|
||||||
|
var result = new AssignReservaLinesResultDto
|
||||||
|
{
|
||||||
|
Requested = ids.Count
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var id in ids)
|
||||||
|
{
|
||||||
|
if (!byId.TryGetValue(id, out var line))
|
||||||
|
{
|
||||||
|
result.Items.Add(new AssignReservaLineItemResultDto
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Success = false,
|
||||||
|
Message = "Linha não encontrada."
|
||||||
|
});
|
||||||
|
result.Failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsReservaLineForTransfer(line))
|
||||||
|
{
|
||||||
|
result.Items.Add(new AssignReservaLineItemResultDto
|
||||||
|
{
|
||||||
|
Id = line.Id,
|
||||||
|
Item = line.Item,
|
||||||
|
Linha = line.Linha,
|
||||||
|
Chip = line.Chip,
|
||||||
|
ClienteAnterior = line.Cliente,
|
||||||
|
ClienteNovo = line.Cliente,
|
||||||
|
Success = false,
|
||||||
|
Message = "A linha já está disponível na Reserva."
|
||||||
|
});
|
||||||
|
result.Failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clienteAnterior = line.Cliente;
|
||||||
|
var clienteOrigemNormalizado = string.IsNullOrWhiteSpace(clienteAnterior)
|
||||||
|
? null
|
||||||
|
: clienteAnterior.Trim();
|
||||||
|
|
||||||
|
// Mantém o cliente de origem para que o agrupamento no filtro "Reserva"
|
||||||
|
// continue exibindo o cliente correto após o envio.
|
||||||
|
line.Cliente = string.IsNullOrWhiteSpace(clienteOrigemNormalizado)
|
||||||
|
? "RESERVA"
|
||||||
|
: clienteOrigemNormalizado;
|
||||||
|
line.Usuario = "RESERVA";
|
||||||
|
line.Skil = "RESERVA";
|
||||||
|
ApplyReservaRule(line);
|
||||||
|
line.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
result.Items.Add(new AssignReservaLineItemResultDto
|
||||||
|
{
|
||||||
|
Id = line.Id,
|
||||||
|
Item = line.Item,
|
||||||
|
Linha = line.Linha,
|
||||||
|
Chip = line.Chip,
|
||||||
|
ClienteAnterior = clienteAnterior,
|
||||||
|
ClienteNovo = "RESERVA",
|
||||||
|
Success = true,
|
||||||
|
Message = "Linha enviada para a Reserva com sucesso."
|
||||||
|
});
|
||||||
|
result.Updated++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Updated <= 0)
|
||||||
|
return Ok(result);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
await _vigenciaNotificationSyncService.SyncCurrentTenantAsync();
|
||||||
|
result.Failed = result.Requested - result.Updated;
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (DbUpdateException)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { message = "Erro ao mover linhas para a Reserva." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
// ✅ 6. UPDATE
|
// ✅ 6. UPDATE
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
|
|
@ -951,6 +1266,7 @@ namespace line_gestao_api.Controllers
|
||||||
var x = await _db.MobileLines.FirstOrDefaultAsync(a => a.Id == id);
|
var x = await _db.MobileLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||||
if (x == null) return NotFound();
|
if (x == null) return NotFound();
|
||||||
var previousLinha = x.Linha;
|
var previousLinha = x.Linha;
|
||||||
|
var previousCliente = x.Cliente;
|
||||||
|
|
||||||
var newLinha = OnlyDigits(req.Linha);
|
var newLinha = OnlyDigits(req.Linha);
|
||||||
if (!string.IsNullOrWhiteSpace(newLinha) && !string.Equals((x.Linha ?? ""), newLinha, StringComparison.Ordinal))
|
if (!string.IsNullOrWhiteSpace(newLinha) && !string.Equals((x.Linha ?? ""), newLinha, StringComparison.Ordinal))
|
||||||
|
|
@ -994,6 +1310,26 @@ namespace line_gestao_api.Controllers
|
||||||
x.VencConta = req.VencConta?.Trim();
|
x.VencConta = req.VencConta?.Trim();
|
||||||
x.TipoDeChip = req.TipoDeChip?.Trim();
|
x.TipoDeChip = req.TipoDeChip?.Trim();
|
||||||
|
|
||||||
|
var previousClienteNormalized = string.IsNullOrWhiteSpace(previousCliente) ? null : previousCliente.Trim();
|
||||||
|
var clienteAtualIsReserva = IsReservaValue(x.Cliente);
|
||||||
|
var usuarioAtualIsReserva = IsReservaValue(x.Usuario);
|
||||||
|
var skilAtualIsReserva = IsReservaValue(x.Skil);
|
||||||
|
var reserveContextRequested = clienteAtualIsReserva || usuarioAtualIsReserva || skilAtualIsReserva;
|
||||||
|
|
||||||
|
// Quando uma linha é enviada para Reserva por edição, preserva o cliente de origem para
|
||||||
|
// o agrupamento no filtro "Reservas" (desde que o cliente anterior não fosse RESERVA).
|
||||||
|
if (reserveContextRequested && (string.IsNullOrWhiteSpace(x.Cliente) || clienteAtualIsReserva))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(previousClienteNormalized) && !IsReservaValue(previousClienteNormalized))
|
||||||
|
{
|
||||||
|
x.Cliente = previousClienteNormalized;
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrWhiteSpace(x.Cliente))
|
||||||
|
{
|
||||||
|
x.Cliente = "RESERVA";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ApplyReservaRule(x);
|
ApplyReservaRule(x);
|
||||||
|
|
||||||
await UpsertVigenciaFromMobileLineAsync(
|
await UpsertVigenciaFromMobileLineAsync(
|
||||||
|
|
@ -3331,6 +3667,385 @@ namespace line_gestao_api.Controllers
|
||||||
return hasItem && hasNumeroChip && hasObs;
|
return hasItem && hasNumeroChip && hasObs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<LinesBatchExcelPreviewResultDto> BuildLinesBatchExcelPreviewAsync(XLWorkbook wb, string? fileName)
|
||||||
|
{
|
||||||
|
var result = new LinesBatchExcelPreviewResultDto
|
||||||
|
{
|
||||||
|
FileName = fileName
|
||||||
|
};
|
||||||
|
|
||||||
|
static LinesBatchExcelIssueDto Issue(string? column, string message) => new()
|
||||||
|
{
|
||||||
|
Column = column,
|
||||||
|
Message = message
|
||||||
|
};
|
||||||
|
|
||||||
|
static string? NullIfWhite(string? v)
|
||||||
|
=> string.IsNullOrWhiteSpace(v) ? null : v.Trim();
|
||||||
|
|
||||||
|
static bool LooksLikeBatchGeralHeader(IXLRow row)
|
||||||
|
{
|
||||||
|
if (row == null) return false;
|
||||||
|
|
||||||
|
var map = BuildHeaderMap(row);
|
||||||
|
if (map.Count == 0) return false;
|
||||||
|
|
||||||
|
var hits = 0;
|
||||||
|
if (GetColAny(map, "CONTA") > 0) hits++;
|
||||||
|
if (GetColAny(map, "LINHA") > 0) hits++;
|
||||||
|
if (GetColAny(map, "CHIP") > 0) hits++;
|
||||||
|
if (GetColAny(map, "CLIENTE") > 0) hits++;
|
||||||
|
if (GetColAny(map, "USUARIO") > 0) hits++;
|
||||||
|
if (GetColAny(map, "PLANO CONTRATO") > 0) hits++;
|
||||||
|
if (GetColAny(map, "STATUS") > 0) hits++;
|
||||||
|
if (GetColAny(map, "TIPO DE CHIP", "TIPO CHIP") > 0) hits++;
|
||||||
|
|
||||||
|
// Assinatura mínima para reconhecer uma planilha nova com cabeçalhos da GERAL
|
||||||
|
return hits >= 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ws = wb.Worksheets.FirstOrDefault(w => w.Name.Trim().Equals("GERAL", StringComparison.OrdinalIgnoreCase));
|
||||||
|
IXLRow? headerRow = null;
|
||||||
|
var usedFallbackSheet = false;
|
||||||
|
|
||||||
|
if (ws != null)
|
||||||
|
{
|
||||||
|
headerRow = ws.RowsUsed()
|
||||||
|
.Take(100)
|
||||||
|
.FirstOrDefault(LooksLikeBatchGeralHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ws == null || headerRow == null || !LooksLikeBatchGeralHeader(headerRow))
|
||||||
|
{
|
||||||
|
foreach (var candidateWs in wb.Worksheets)
|
||||||
|
{
|
||||||
|
var candidateHeader = candidateWs.RowsUsed()
|
||||||
|
.Take(100)
|
||||||
|
.FirstOrDefault(LooksLikeBatchGeralHeader);
|
||||||
|
|
||||||
|
if (candidateHeader == null) continue;
|
||||||
|
|
||||||
|
ws = candidateWs;
|
||||||
|
headerRow = candidateHeader;
|
||||||
|
usedFallbackSheet = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ws == null || headerRow == null)
|
||||||
|
{
|
||||||
|
result.HeaderErrors.Add(Issue(
|
||||||
|
"ABA/CABEÇALHO",
|
||||||
|
"Nenhuma aba compatível encontrada. A planilha deve conter os cabeçalhos da GERAL (ex.: CONTA, LINHA, CHIP, CLIENTE...)."));
|
||||||
|
result.CanProceed = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.SheetName = ws.Name;
|
||||||
|
|
||||||
|
if (usedFallbackSheet && !ws.Name.Trim().Equals("GERAL", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.HeaderWarnings.Add(Issue(
|
||||||
|
"ABA",
|
||||||
|
$"Aba 'GERAL' não foi encontrada. Usando a aba '{ws.Name}' por conter cabeçalhos compatíveis."));
|
||||||
|
}
|
||||||
|
|
||||||
|
var map = BuildHeaderMap(headerRow);
|
||||||
|
|
||||||
|
var expectedHeaders = new List<(string Display, bool Required, string[] Aliases)>
|
||||||
|
{
|
||||||
|
("CONTA", true, new[] { "CONTA" }),
|
||||||
|
("LINHA", true, new[] { "LINHA" }),
|
||||||
|
("CHIP", true, new[] { "CHIP" }),
|
||||||
|
("CLIENTE", true, new[] { "CLIENTE" }),
|
||||||
|
("USUÁRIO", true, new[] { "USUARIO" }),
|
||||||
|
("PLANO CONTRATO", true, new[] { "PLANO CONTRATO" }),
|
||||||
|
("FRAQUIA", true, new[] { "FRAQUIA", "FRANQUIA", "FRANQUIA VIVO", "FRAQUIA VIVO" }),
|
||||||
|
("VALOR DO PLANO R$", true, new[] { "VALOR DO PLANO R$", "VALOR DO PLANO", "VALORPLANO" }),
|
||||||
|
("GESTÃO VOZ E DADOS R$", true, new[] { "GESTAO VOZ E DADOS R$", "GESTAO VOZ E DADOS", "GESTAOVOZEDADOS" }),
|
||||||
|
("SKEELO", true, new[] { "SKEELO" }),
|
||||||
|
("VIVO NEWS PLUS", true, new[] { "VIVO NEWS PLUS" }),
|
||||||
|
("VIVO TRAVEL MUNDO", true, new[] { "VIVO TRAVEL MUNDO" }),
|
||||||
|
("VIVO SYNC", true, new[] { "VIVO SYNC" }),
|
||||||
|
("VIVO GESTÃO DISPOSITIVO", true, new[] { "VIVO GESTAO DISPOSITIVO" }),
|
||||||
|
("VALOR CONTRATO VIVO", true, new[] { "VALOR CONTRATO VIVO", "VALOR DO CONTRATO VIVO" }),
|
||||||
|
("FRANQUIA LINE", true, new[] { "FRANQUIA LINE", "FRAQUIA LINE" }),
|
||||||
|
("FRANQUIA GESTÃO", true, new[] { "FRANQUIA GESTAO", "FRAQUIA GESTAO" }),
|
||||||
|
("LOCAÇÃO AP.", true, new[] { "LOCACAO AP.", "LOCACAO AP", "LOCACAOAP" }),
|
||||||
|
("VALOR CONTRATO LINE", true, new[] { "VALOR CONTRATO LINE", "VALOR DO CONTRATO LINE" }),
|
||||||
|
("DESCONTO", true, new[] { "DESCONTO" }),
|
||||||
|
("LUCRO", true, new[] { "LUCRO" }),
|
||||||
|
("STATUS", true, new[] { "STATUS" }),
|
||||||
|
("DATA DO BLOQUEIO", true, new[] { "DATA DO BLOQUEIO" }),
|
||||||
|
("SKIL", true, new[] { "SKIL" }),
|
||||||
|
("MODALIDADE", true, new[] { "MODALIDADE" }),
|
||||||
|
("CEDENTE", true, new[] { "CEDENTE" }),
|
||||||
|
("SOLICITANTE", true, new[] { "SOLICITANTE" }),
|
||||||
|
("DATA DA ENTREGA OPERA.", true, new[] { "DATA DA ENTREGA OPERA." }),
|
||||||
|
("DATA DA ENTREGA CLIENTE", true, new[] { "DATA DA ENTREGA CLIENTE" }),
|
||||||
|
("VENC. DA CONTA", true, new[] { "VENC. DA CONTA", "VENC DA CONTA", "VENCIMENTO DA CONTA" }),
|
||||||
|
("TIPO DE CHIP", true, new[] { "TIPO DE CHIP", "TIPO CHIP" })
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var spec in expectedHeaders)
|
||||||
|
{
|
||||||
|
if (GetColAny(map, spec.Aliases) > 0) continue;
|
||||||
|
var issue = Issue(spec.Display, $"Coluna obrigatória ausente: '{spec.Display}'.");
|
||||||
|
if (spec.Required) result.HeaderErrors.Add(issue);
|
||||||
|
else result.HeaderWarnings.Add(issue);
|
||||||
|
}
|
||||||
|
|
||||||
|
var colDtEfetivacao = GetColAny(map,
|
||||||
|
"DT EFETIVACAO SERVICO",
|
||||||
|
"DT. EFETIVACAO SERVICO",
|
||||||
|
"DT. EFETIVAÇÃO SERVIÇO",
|
||||||
|
"DT EFETIVAÇÃO SERVIÇO");
|
||||||
|
var colDtTermino = GetColAny(map,
|
||||||
|
"DT TERMINO FIDELIZACAO",
|
||||||
|
"DT. TERMINO FIDELIZACAO",
|
||||||
|
"DT. TÉRMINO FIDELIZAÇÃO",
|
||||||
|
"DT TÉRMINO FIDELIZAÇÃO");
|
||||||
|
|
||||||
|
if (colDtEfetivacao == 0 || colDtTermino == 0)
|
||||||
|
{
|
||||||
|
result.HeaderWarnings.Add(Issue(
|
||||||
|
"DT. EFETIVAÇÃO / DT. TÉRMINO",
|
||||||
|
"As colunas de vigência não foram encontradas na aba GERAL. Elas continuam obrigatórias para salvar o lote e podem ser preenchidas no modal (detalhes/parâmetros padrão)."));
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedCols = expectedHeaders
|
||||||
|
.Select(x => GetColAny(map, x.Aliases))
|
||||||
|
.Where(c => c > 0)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
if (colDtEfetivacao > 0) expectedCols.Add(colDtEfetivacao);
|
||||||
|
if (colDtTermino > 0) expectedCols.Add(colDtTermino);
|
||||||
|
|
||||||
|
var startRow = headerRow.RowNumber() + 1;
|
||||||
|
var lastRow = ws.LastRowUsed()?.RowNumber() ?? startRow;
|
||||||
|
var rows = new List<LinesBatchExcelPreviewRowDto>();
|
||||||
|
|
||||||
|
for (var r = startRow; r <= lastRow; r++)
|
||||||
|
{
|
||||||
|
var rowHasAnyValue = expectedCols.Any(c => !string.IsNullOrWhiteSpace(GetCellString(ws, r, c)));
|
||||||
|
if (!rowHasAnyValue)
|
||||||
|
{
|
||||||
|
if (rows.Count > 0) break;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rowDto = new LinesBatchExcelPreviewRowDto
|
||||||
|
{
|
||||||
|
SourceRowNumber = r,
|
||||||
|
Data = new CreateMobileLineDto { Item = 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
void AddRowError(string? column, string message) => rowDto.Errors.Add(Issue(column, message));
|
||||||
|
void AddRowWarning(string? column, string message) => rowDto.Warnings.Add(Issue(column, message));
|
||||||
|
|
||||||
|
string CellBy(params string[] headers) => GetCellByHeaderAny(ws, r, map, headers);
|
||||||
|
|
||||||
|
decimal? ParseDecimalField(string columnDisplay, params string[] headers)
|
||||||
|
{
|
||||||
|
var raw = CellBy(headers);
|
||||||
|
if (string.IsNullOrWhiteSpace(raw)) return null;
|
||||||
|
var parsed = TryDecimal(raw);
|
||||||
|
if (!parsed.HasValue)
|
||||||
|
AddRowError(columnDisplay, $"Valor numérico inválido: '{raw}'.");
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime? ParseDateField(string columnDisplay, params string[] headers)
|
||||||
|
{
|
||||||
|
var col = GetColAny(map, headers);
|
||||||
|
if (col <= 0) return null;
|
||||||
|
var raw = GetCellString(ws, r, col);
|
||||||
|
if (string.IsNullOrWhiteSpace(raw) && ws.Cell(r, col).IsEmpty()) return null;
|
||||||
|
var parsed = TryDateCell(ws, r, col);
|
||||||
|
if (!parsed.HasValue && !string.IsNullOrWhiteSpace(raw))
|
||||||
|
AddRowError(columnDisplay, $"Data inválida: '{raw}'.");
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemRaw = CellBy("ITEM");
|
||||||
|
if (!string.IsNullOrWhiteSpace(itemRaw))
|
||||||
|
{
|
||||||
|
var parsedItem = TryNullableInt(itemRaw);
|
||||||
|
rowDto.SourceItem = parsedItem;
|
||||||
|
AddRowWarning("ITÉM", "Valor informado será ignorado. O sistema gera a sequência automaticamente.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var linhaRaw = CellBy("LINHA");
|
||||||
|
var linhaDigits = OnlyDigits(linhaRaw);
|
||||||
|
if (string.IsNullOrWhiteSpace(linhaRaw))
|
||||||
|
AddRowError("LINHA", "Campo obrigatório.");
|
||||||
|
else if (string.IsNullOrWhiteSpace(linhaDigits))
|
||||||
|
AddRowError("LINHA", $"Valor inválido: '{linhaRaw}'.");
|
||||||
|
rowDto.Data.Linha = string.IsNullOrWhiteSpace(linhaDigits) ? NullIfWhite(linhaRaw) : linhaDigits;
|
||||||
|
|
||||||
|
var chipRaw = CellBy("CHIP");
|
||||||
|
var chipDigits = OnlyDigits(chipRaw);
|
||||||
|
if (string.IsNullOrWhiteSpace(chipRaw))
|
||||||
|
AddRowError("CHIP", "Campo obrigatório.");
|
||||||
|
else if (string.IsNullOrWhiteSpace(chipDigits))
|
||||||
|
AddRowError("CHIP", $"Valor inválido: '{chipRaw}'.");
|
||||||
|
rowDto.Data.Chip = string.IsNullOrWhiteSpace(chipDigits) ? NullIfWhite(chipRaw) : chipDigits;
|
||||||
|
|
||||||
|
rowDto.Data.Conta = NullIfWhite(CellBy("CONTA"));
|
||||||
|
if (string.IsNullOrWhiteSpace(rowDto.Data.Conta))
|
||||||
|
AddRowError("CONTA", "Campo obrigatório.");
|
||||||
|
|
||||||
|
rowDto.Data.Cliente = NullIfWhite(CellBy("CLIENTE"));
|
||||||
|
rowDto.Data.Usuario = NullIfWhite(CellBy("USUARIO"));
|
||||||
|
rowDto.Data.PlanoContrato = NullIfWhite(CellBy("PLANO CONTRATO"));
|
||||||
|
if (string.IsNullOrWhiteSpace(rowDto.Data.PlanoContrato))
|
||||||
|
AddRowError("PLANO CONTRATO", "Campo obrigatório.");
|
||||||
|
|
||||||
|
rowDto.Data.Status = NullIfWhite(CellBy("STATUS"));
|
||||||
|
if (string.IsNullOrWhiteSpace(rowDto.Data.Status))
|
||||||
|
AddRowError("STATUS", "Campo obrigatório.");
|
||||||
|
|
||||||
|
rowDto.Data.Skil = NullIfWhite(CellBy("SKIL"));
|
||||||
|
rowDto.Data.Modalidade = NullIfWhite(CellBy("MODALIDADE"));
|
||||||
|
rowDto.Data.Cedente = NullIfWhite(CellBy("CEDENTE"));
|
||||||
|
rowDto.Data.Solicitante = NullIfWhite(CellBy("SOLICITANTE"));
|
||||||
|
rowDto.Data.VencConta = NullIfWhite(CellBy("VENC. DA CONTA", "VENC DA CONTA", "VENCIMENTO DA CONTA"));
|
||||||
|
rowDto.Data.TipoDeChip = NullIfWhite(CellBy("TIPO DE CHIP", "TIPO CHIP"));
|
||||||
|
|
||||||
|
rowDto.Data.FranquiaVivo = ParseDecimalField("FRAQUIA", "FRAQUIA", "FRANQUIA", "FRANQUIA VIVO", "FRAQUIA VIVO");
|
||||||
|
rowDto.Data.ValorPlanoVivo = ParseDecimalField("VALOR DO PLANO R$", "VALOR DO PLANO R$", "VALOR DO PLANO", "VALORPLANO");
|
||||||
|
rowDto.Data.GestaoVozDados = ParseDecimalField("GESTÃO VOZ E DADOS R$", "GESTAO VOZ E DADOS R$", "GESTAO VOZ E DADOS", "GESTAOVOZEDADOS");
|
||||||
|
rowDto.Data.Skeelo = ParseDecimalField("SKEELO", "SKEELO");
|
||||||
|
rowDto.Data.VivoNewsPlus = ParseDecimalField("VIVO NEWS PLUS", "VIVO NEWS PLUS");
|
||||||
|
rowDto.Data.VivoTravelMundo = ParseDecimalField("VIVO TRAVEL MUNDO", "VIVO TRAVEL MUNDO");
|
||||||
|
rowDto.Data.VivoSync = ParseDecimalField("VIVO SYNC", "VIVO SYNC");
|
||||||
|
rowDto.Data.VivoGestaoDispositivo = ParseDecimalField("VIVO GESTÃO DISPOSITIVO", "VIVO GESTAO DISPOSITIVO");
|
||||||
|
rowDto.Data.ValorContratoVivo = ParseDecimalField("VALOR CONTRATO VIVO", "VALOR CONTRATO VIVO", "VALOR DO CONTRATO VIVO");
|
||||||
|
rowDto.Data.FranquiaLine = ParseDecimalField("FRANQUIA LINE", "FRANQUIA LINE", "FRAQUIA LINE");
|
||||||
|
rowDto.Data.FranquiaGestao = ParseDecimalField("FRANQUIA GESTÃO", "FRANQUIA GESTAO", "FRAQUIA GESTAO");
|
||||||
|
rowDto.Data.LocacaoAp = ParseDecimalField("LOCAÇÃO AP.", "LOCACAO AP.", "LOCACAO AP", "LOCACAOAP");
|
||||||
|
rowDto.Data.ValorContratoLine = ParseDecimalField("VALOR CONTRATO LINE", "VALOR CONTRATO LINE", "VALOR DO CONTRATO LINE");
|
||||||
|
rowDto.Data.Desconto = ParseDecimalField("DESCONTO", "DESCONTO");
|
||||||
|
rowDto.Data.Lucro = ParseDecimalField("LUCRO", "LUCRO");
|
||||||
|
|
||||||
|
rowDto.Data.DataBloqueio = ParseDateField("DATA DO BLOQUEIO", "DATA DO BLOQUEIO");
|
||||||
|
rowDto.Data.DataEntregaOpera = ParseDateField("DATA DA ENTREGA OPERA.", "DATA DA ENTREGA OPERA.");
|
||||||
|
rowDto.Data.DataEntregaCliente = ParseDateField("DATA DA ENTREGA CLIENTE", "DATA DA ENTREGA CLIENTE");
|
||||||
|
rowDto.Data.DtEfetivacaoServico = colDtEfetivacao > 0 ? ParseDateField("DT. EFETIVAÇÃO SERVIÇO",
|
||||||
|
"DT EFETIVACAO SERVICO", "DT. EFETIVACAO SERVICO", "DT. EFETIVAÇÃO SERVIÇO", "DT EFETIVAÇÃO SERVIÇO") : null;
|
||||||
|
rowDto.Data.DtTerminoFidelizacao = colDtTermino > 0 ? ParseDateField("DT. TÉRMINO FIDELIZAÇÃO",
|
||||||
|
"DT TERMINO FIDELIZACAO", "DT. TERMINO FIDELIZACAO", "DT. TÉRMINO FIDELIZAÇÃO", "DT TÉRMINO FIDELIZAÇÃO") : null;
|
||||||
|
|
||||||
|
if (colDtEfetivacao == 0 || !rowDto.Data.DtEfetivacaoServico.HasValue)
|
||||||
|
AddRowWarning("DT. EFETIVAÇÃO SERVIÇO", "Campo não preenchido pela aba GERAL; complete no modal antes de salvar.");
|
||||||
|
if (colDtTermino == 0 || !rowDto.Data.DtTerminoFidelizacao.HasValue)
|
||||||
|
AddRowWarning("DT. TÉRMINO FIDELIZAÇÃO", "Campo não preenchido pela aba GERAL; complete no modal antes de salvar.");
|
||||||
|
|
||||||
|
rows.Add(rowDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
var linhasArquivo = rows
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.Data.Linha))
|
||||||
|
.GroupBy(x => x.Data.Linha!, StringComparer.Ordinal)
|
||||||
|
.Where(g => g.Count() > 1)
|
||||||
|
.Select(g => g.Key)
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var chipsArquivo = rows
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.Data.Chip))
|
||||||
|
.GroupBy(x => x.Data.Chip!, StringComparer.Ordinal)
|
||||||
|
.Where(g => g.Count() > 1)
|
||||||
|
.Select(g => g.Key)
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var linhasParaConsultar = rows
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.Data.Linha))
|
||||||
|
.Select(x => x.Data.Linha!)
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var chipsParaConsultar = rows
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.Data.Chip))
|
||||||
|
.Select(x => x.Data.Chip!)
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var linhasExistentes = linhasParaConsultar.Count == 0
|
||||||
|
? new HashSet<string>(StringComparer.Ordinal)
|
||||||
|
: (await _db.MobileLines.AsNoTracking()
|
||||||
|
.Where(x => x.Linha != null && linhasParaConsultar.Contains(x.Linha))
|
||||||
|
.Select(x => x.Linha!)
|
||||||
|
.ToListAsync())
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var chipsExistentes = chipsParaConsultar.Count == 0
|
||||||
|
? new HashSet<string>(StringComparer.Ordinal)
|
||||||
|
: (await _db.MobileLines.AsNoTracking()
|
||||||
|
.Where(x => x.Chip != null && chipsParaConsultar.Contains(x.Chip))
|
||||||
|
.Select(x => x.Chip!)
|
||||||
|
.ToListAsync())
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var maxItem = await _db.MobileLines.AsNoTracking().MaxAsync(x => (int?)x.Item) ?? 0;
|
||||||
|
result.NextItemStart = maxItem + 1;
|
||||||
|
var previewNextItem = maxItem;
|
||||||
|
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
var linha = row.Data.Linha;
|
||||||
|
var chip = row.Data.Chip;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(linha) && linhasArquivo.Contains(linha))
|
||||||
|
{
|
||||||
|
row.DuplicateLinhaInFile = true;
|
||||||
|
row.Errors.Add(Issue("LINHA", $"Linha duplicada dentro da planilha: {linha}."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(chip) && chipsArquivo.Contains(chip))
|
||||||
|
{
|
||||||
|
row.DuplicateChipInFile = true;
|
||||||
|
row.Errors.Add(Issue("CHIP", $"Chip (ICCID) duplicado dentro da planilha: {chip}."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(linha) && linhasExistentes.Contains(linha))
|
||||||
|
{
|
||||||
|
row.DuplicateLinhaInSystem = true;
|
||||||
|
row.Errors.Add(Issue("LINHA", $"Linha já cadastrada no sistema: {linha}."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(chip) && chipsExistentes.Contains(chip))
|
||||||
|
{
|
||||||
|
row.DuplicateChipInSystem = true;
|
||||||
|
row.Errors.Add(Issue("CHIP", $"Chip (ICCID) já cadastrado no sistema: {chip}."));
|
||||||
|
}
|
||||||
|
|
||||||
|
row.Valid = row.Errors.Count == 0;
|
||||||
|
if (row.Valid)
|
||||||
|
{
|
||||||
|
previewNextItem++;
|
||||||
|
row.GeneratedItemPreview = previewNextItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Rows = rows;
|
||||||
|
result.TotalRows = rows.Count;
|
||||||
|
result.ValidRows = rows.Count(x => x.Valid);
|
||||||
|
result.InvalidRows = rows.Count(x => !x.Valid);
|
||||||
|
result.DuplicateRows = rows.Count(x =>
|
||||||
|
x.DuplicateLinhaInFile || x.DuplicateChipInFile || x.DuplicateLinhaInSystem || x.DuplicateChipInSystem);
|
||||||
|
result.CanProceed = result.HeaderErrors.Count == 0 && result.ValidRows > 0;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsReservaLineForTransfer(MobileLine line)
|
||||||
|
{
|
||||||
|
if (line == null) return false;
|
||||||
|
return IsReservaValue(line.Usuario)
|
||||||
|
|| IsReservaValue(line.Skil)
|
||||||
|
|| IsReservaValue(line.Cliente);
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
// HELPERS (SEUS)
|
// HELPERS (SEUS)
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
|
|
@ -3630,6 +4345,14 @@ namespace line_gestao_api.Controllers
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IQueryable<MobileLine> ExcludeReservaContext(IQueryable<MobileLine> query)
|
||||||
|
{
|
||||||
|
return query.Where(x =>
|
||||||
|
!EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") &&
|
||||||
|
!EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") &&
|
||||||
|
!EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
||||||
|
}
|
||||||
|
|
||||||
private static IQueryable<MobileLine> ApplyAdditionalFilters(
|
private static IQueryable<MobileLine> ApplyAdditionalFilters(
|
||||||
IQueryable<MobileLine> query,
|
IQueryable<MobileLine> query,
|
||||||
string? additionalMode,
|
string? additionalMode,
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,9 @@ public class NotificationsController : ControllerBase
|
||||||
DiasParaVencer = notification.DiasParaVencer,
|
DiasParaVencer = notification.DiasParaVencer,
|
||||||
Lida = notification.Lida,
|
Lida = notification.Lida,
|
||||||
LidaEm = notification.LidaEm,
|
LidaEm = notification.LidaEm,
|
||||||
VigenciaLineId = notification.VigenciaLineId,
|
VigenciaLineId = notification.VigenciaLineId
|
||||||
|
?? (vigencia != null ? (Guid?)vigencia.Id : null)
|
||||||
|
?? (vigenciaByLinha != null ? (Guid?)vigenciaByLinha.Id : null),
|
||||||
Cliente = notification.Cliente
|
Cliente = notification.Cliente
|
||||||
?? (vigencia != null ? vigencia.Cliente : null)
|
?? (vigencia != null ? vigencia.Cliente : null)
|
||||||
?? (vigenciaByLinha != null ? vigenciaByLinha.Cliente : null),
|
?? (vigenciaByLinha != null ? vigenciaByLinha.Cliente : null),
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ namespace line_gestao_api.Controllers
|
||||||
// GERAL (MobileLines)
|
// GERAL (MobileLines)
|
||||||
// =========================
|
// =========================
|
||||||
var qLines = _db.MobileLines.AsNoTracking();
|
var qLines = _db.MobileLines.AsNoTracking();
|
||||||
|
var qLinesWithClient = qLines.Where(x => x.Cliente != null && x.Cliente != "");
|
||||||
|
|
||||||
var totalLinhas = await qLines.CountAsync();
|
var totalLinhas = await qLines.CountAsync();
|
||||||
|
|
||||||
|
|
@ -44,27 +45,35 @@ namespace line_gestao_api.Controllers
|
||||||
var ativos = await qLines.CountAsync(x =>
|
var ativos = await qLines.CountAsync(x =>
|
||||||
EF.Functions.ILike((x.Status ?? "").Trim(), "%ativo%"));
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%ativo%"));
|
||||||
|
|
||||||
var bloqueadosPerdaRoubo = await qLines.CountAsync(x =>
|
var bloqueadosPerdaRoubo = await qLinesWithClient.CountAsync(x =>
|
||||||
EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") ||
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") ||
|
||||||
EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%"));
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%"));
|
||||||
|
|
||||||
var bloqueados120Dias = await qLines.CountAsync(x =>
|
var bloqueados120Dias = await qLinesWithClient.CountAsync(x =>
|
||||||
EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") &&
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") &&
|
||||||
EF.Functions.ILike((x.Status ?? "").Trim(), "%120%") &&
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%120%") &&
|
||||||
EF.Functions.ILike((x.Status ?? "").Trim(), "%dia%"));
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%dia%") &&
|
||||||
|
!(EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") ||
|
||||||
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%")));
|
||||||
|
|
||||||
var bloqueadosOutros = await qLines.CountAsync(x =>
|
var bloqueadosOutros = await qLinesWithClient.CountAsync(x =>
|
||||||
EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") &&
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") &&
|
||||||
!(EF.Functions.ILike((x.Status ?? "").Trim(), "%120%") && EF.Functions.ILike((x.Status ?? "").Trim(), "%dia%")) &&
|
!(EF.Functions.ILike((x.Status ?? "").Trim(), "%120%") && EF.Functions.ILike((x.Status ?? "").Trim(), "%dia%")) &&
|
||||||
!(EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") || EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%"))
|
!(EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") || EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%"))
|
||||||
);
|
);
|
||||||
|
|
||||||
var bloqueados = bloqueadosPerdaRoubo + bloqueados120Dias + bloqueadosOutros;
|
// Regra do KPI "Bloqueadas" alinhada ao critério da página Geral:
|
||||||
|
// status contendo "bloque", "perda" ou "roubo".
|
||||||
|
var bloqueados = await qLinesWithClient.CountAsync(x =>
|
||||||
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") ||
|
||||||
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") ||
|
||||||
|
EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%"));
|
||||||
|
|
||||||
|
// Regra alinhada ao filtro "Reservas" da página Geral.
|
||||||
var reservas = await qLines.CountAsync(x =>
|
var reservas = await qLines.CountAsync(x =>
|
||||||
EF.Functions.ILike((x.Cliente ?? "").Trim(), "%RESERVA%") ||
|
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
||||||
EF.Functions.ILike((x.Usuario ?? "").Trim(), "%RESERVA%") ||
|
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") ||
|
||||||
EF.Functions.ILike((x.Skil ?? "").Trim(), "%RESERVA%"));
|
EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
||||||
|
|
||||||
var topClientes = await qLines
|
var topClientes = await qLines
|
||||||
.Where(x => x.Cliente != null && x.Cliente != "")
|
.Where(x => x.Cliente != null && x.Cliente != "")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
using line_gestao_api.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/templates")]
|
||||||
|
[Authorize(Roles = "sysadmin,gestor")]
|
||||||
|
public class TemplatesController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly GeralSpreadsheetTemplateService _geralSpreadsheetTemplateService;
|
||||||
|
|
||||||
|
public TemplatesController(GeralSpreadsheetTemplateService geralSpreadsheetTemplateService)
|
||||||
|
{
|
||||||
|
_geralSpreadsheetTemplateService = geralSpreadsheetTemplateService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("planilha-geral")]
|
||||||
|
public IActionResult DownloadPlanilhaGeral()
|
||||||
|
{
|
||||||
|
Response.Headers["Cache-Control"] = "no-store, no-cache, must-revalidate";
|
||||||
|
Response.Headers["Pragma"] = "no-cache";
|
||||||
|
Response.Headers["Expires"] = "0";
|
||||||
|
|
||||||
|
var bytes = _geralSpreadsheetTemplateService.BuildPlanilhaGeralTemplate();
|
||||||
|
return File(
|
||||||
|
bytes,
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"MODELO_GERAL_LINEGESTAO.xlsx");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -56,6 +56,7 @@ namespace line_gestao_api.Controllers
|
||||||
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
|
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
|
||||||
q = q.Where(x =>
|
q = q.Where(x =>
|
||||||
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
|
||||||
|
(x.Linha != null && _db.MobileLines.Any(m => m.Linha == x.Linha && EF.Functions.ILike(m.Chip ?? "", $"%{s}%"))) ||
|
||||||
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
|
||||||
EF.Functions.ILike(x.TipoPessoa ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.TipoPessoa ?? "", $"%{s}%") ||
|
||||||
EF.Functions.ILike(x.Nome ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.Nome ?? "", $"%{s}%") ||
|
||||||
|
|
@ -151,6 +152,7 @@ namespace line_gestao_api.Controllers
|
||||||
q = q.Where(x =>
|
q = q.Where(x =>
|
||||||
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
|
||||||
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
|
||||||
|
(x.Linha != null && _db.MobileLines.Any(m => m.Linha == x.Linha && EF.Functions.ILike(m.Chip ?? "", $"%{s}%"))) ||
|
||||||
EF.Functions.ILike(x.TipoPessoa ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.TipoPessoa ?? "", $"%{s}%") ||
|
||||||
EF.Functions.ILike(x.Nome ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.Nome ?? "", $"%{s}%") ||
|
||||||
EF.Functions.ILike(x.RazaoSocial ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.RazaoSocial ?? "", $"%{s}%") ||
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ namespace line_gestao_api.Controllers
|
||||||
q = q.Where(x =>
|
q = q.Where(x =>
|
||||||
EF.Functions.ILike(x.Conta ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.Conta ?? "", $"%{s}%") ||
|
||||||
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
|
||||||
|
(x.Linha != null && _db.MobileLines.Any(m => m.Linha == x.Linha && EF.Functions.ILike(m.Chip ?? "", $"%{s}%"))) ||
|
||||||
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
|
||||||
EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") ||
|
||||||
EF.Functions.ILike(x.PlanoContrato ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.PlanoContrato ?? "", $"%{s}%") ||
|
||||||
|
|
@ -98,6 +99,10 @@ namespace line_gestao_api.Controllers
|
||||||
PlanoContrato = x.PlanoContrato,
|
PlanoContrato = x.PlanoContrato,
|
||||||
DtEfetivacaoServico = x.DtEfetivacaoServico,
|
DtEfetivacaoServico = x.DtEfetivacaoServico,
|
||||||
DtTerminoFidelizacao = x.DtTerminoFidelizacao,
|
DtTerminoFidelizacao = x.DtTerminoFidelizacao,
|
||||||
|
AutoRenewYears = x.AutoRenewYears,
|
||||||
|
AutoRenewReferenceEndDate = x.AutoRenewReferenceEndDate,
|
||||||
|
AutoRenewConfiguredAt = x.AutoRenewConfiguredAt,
|
||||||
|
LastAutoRenewedAt = x.LastAutoRenewedAt,
|
||||||
Total = x.Total
|
Total = x.Total
|
||||||
})
|
})
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
@ -142,6 +147,7 @@ namespace line_gestao_api.Controllers
|
||||||
q = q.Where(x =>
|
q = q.Where(x =>
|
||||||
EF.Functions.ILike(x.Conta ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.Conta ?? "", $"%{s}%") ||
|
||||||
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
|
||||||
|
(x.Linha != null && _db.MobileLines.Any(m => m.Linha == x.Linha && EF.Functions.ILike(m.Chip ?? "", $"%{s}%"))) ||
|
||||||
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
|
||||||
EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") ||
|
||||||
EF.Functions.ILike(x.PlanoContrato ?? "", $"%{s}%") ||
|
EF.Functions.ILike(x.PlanoContrato ?? "", $"%{s}%") ||
|
||||||
|
|
@ -239,6 +245,10 @@ namespace line_gestao_api.Controllers
|
||||||
PlanoContrato = x.PlanoContrato,
|
PlanoContrato = x.PlanoContrato,
|
||||||
DtEfetivacaoServico = x.DtEfetivacaoServico,
|
DtEfetivacaoServico = x.DtEfetivacaoServico,
|
||||||
DtTerminoFidelizacao = x.DtTerminoFidelizacao,
|
DtTerminoFidelizacao = x.DtTerminoFidelizacao,
|
||||||
|
AutoRenewYears = x.AutoRenewYears,
|
||||||
|
AutoRenewReferenceEndDate = x.AutoRenewReferenceEndDate,
|
||||||
|
AutoRenewConfiguredAt = x.AutoRenewConfiguredAt,
|
||||||
|
LastAutoRenewedAt = x.LastAutoRenewedAt,
|
||||||
Total = x.Total,
|
Total = x.Total,
|
||||||
CreatedAt = x.CreatedAt,
|
CreatedAt = x.CreatedAt,
|
||||||
UpdatedAt = x.UpdatedAt
|
UpdatedAt = x.UpdatedAt
|
||||||
|
|
@ -333,6 +343,10 @@ namespace line_gestao_api.Controllers
|
||||||
PlanoContrato = e.PlanoContrato,
|
PlanoContrato = e.PlanoContrato,
|
||||||
DtEfetivacaoServico = e.DtEfetivacaoServico,
|
DtEfetivacaoServico = e.DtEfetivacaoServico,
|
||||||
DtTerminoFidelizacao = e.DtTerminoFidelizacao,
|
DtTerminoFidelizacao = e.DtTerminoFidelizacao,
|
||||||
|
AutoRenewYears = e.AutoRenewYears,
|
||||||
|
AutoRenewReferenceEndDate = e.AutoRenewReferenceEndDate,
|
||||||
|
AutoRenewConfiguredAt = e.AutoRenewConfiguredAt,
|
||||||
|
LastAutoRenewedAt = e.LastAutoRenewedAt,
|
||||||
Total = e.Total,
|
Total = e.Total,
|
||||||
CreatedAt = e.CreatedAt,
|
CreatedAt = e.CreatedAt,
|
||||||
UpdatedAt = e.UpdatedAt
|
UpdatedAt = e.UpdatedAt
|
||||||
|
|
@ -346,6 +360,9 @@ namespace line_gestao_api.Controllers
|
||||||
var x = await _db.VigenciaLines.FirstOrDefaultAsync(a => a.Id == id);
|
var x = await _db.VigenciaLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||||
if (x == null) return NotFound();
|
if (x == null) return NotFound();
|
||||||
|
|
||||||
|
var previousEfetivacao = x.DtEfetivacaoServico;
|
||||||
|
var previousTermino = x.DtTerminoFidelizacao;
|
||||||
|
|
||||||
if (req.Item.HasValue) x.Item = req.Item.Value;
|
if (req.Item.HasValue) x.Item = req.Item.Value;
|
||||||
if (req.Conta != null) x.Conta = TrimOrNull(req.Conta);
|
if (req.Conta != null) x.Conta = TrimOrNull(req.Conta);
|
||||||
if (req.Linha != null) x.Linha = TrimOrNull(req.Linha);
|
if (req.Linha != null) x.Linha = TrimOrNull(req.Linha);
|
||||||
|
|
@ -358,6 +375,13 @@ namespace line_gestao_api.Controllers
|
||||||
|
|
||||||
if (req.Total.HasValue) x.Total = req.Total.Value;
|
if (req.Total.HasValue) x.Total = req.Total.Value;
|
||||||
|
|
||||||
|
var efetivacaoChanged = req.DtEfetivacaoServico.HasValue && !IsSameUtcDate(previousEfetivacao, x.DtEfetivacaoServico);
|
||||||
|
var terminoChanged = req.DtTerminoFidelizacao.HasValue && !IsSameUtcDate(previousTermino, x.DtTerminoFidelizacao);
|
||||||
|
if (efetivacaoChanged || terminoChanged)
|
||||||
|
{
|
||||||
|
ClearAutoRenewSchedule(x);
|
||||||
|
}
|
||||||
|
|
||||||
x.UpdatedAt = DateTime.UtcNow;
|
x.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
|
|
@ -365,6 +389,39 @@ namespace line_gestao_api.Controllers
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id:guid}/renew")]
|
||||||
|
public async Task<IActionResult> ConfigureAutoRenew(Guid id, [FromBody] ConfigureVigenciaRenewalRequest req)
|
||||||
|
{
|
||||||
|
if (req.Years != 2)
|
||||||
|
{
|
||||||
|
return BadRequest(new { message = "A renovação automática permite somente 2 anos." });
|
||||||
|
}
|
||||||
|
|
||||||
|
var x = await _db.VigenciaLines.FirstOrDefaultAsync(a => a.Id == id);
|
||||||
|
if (x == null) return NotFound();
|
||||||
|
|
||||||
|
if (!x.DtTerminoFidelizacao.HasValue)
|
||||||
|
{
|
||||||
|
return BadRequest(new { message = "A linha não possui data de término de fidelização para programar renovação." });
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
x.AutoRenewYears = 2;
|
||||||
|
x.AutoRenewReferenceEndDate = DateTime.SpecifyKind(x.DtTerminoFidelizacao.Value.Date, DateTimeKind.Utc);
|
||||||
|
x.AutoRenewConfiguredAt = now;
|
||||||
|
x.UpdatedAt = now;
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync();
|
||||||
|
await _vigenciaNotificationSyncService.SyncCurrentTenantAsync();
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
message = "Renovação automática de +2 anos programada para o vencimento.",
|
||||||
|
autoRenewYears = x.AutoRenewYears,
|
||||||
|
autoRenewReferenceEndDate = x.AutoRenewReferenceEndDate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
[Authorize(Roles = "sysadmin")]
|
[Authorize(Roles = "sysadmin")]
|
||||||
public async Task<IActionResult> Delete(Guid id)
|
public async Task<IActionResult> Delete(Guid id)
|
||||||
|
|
@ -390,6 +447,20 @@ namespace line_gestao_api.Controllers
|
||||||
: (dt.Kind == DateTimeKind.Local ? dt.ToUniversalTime() : DateTime.SpecifyKind(dt, DateTimeKind.Utc));
|
: (dt.Kind == DateTimeKind.Local ? dt.ToUniversalTime() : DateTime.SpecifyKind(dt, DateTimeKind.Utc));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ClearAutoRenewSchedule(VigenciaLine line)
|
||||||
|
{
|
||||||
|
line.AutoRenewYears = null;
|
||||||
|
line.AutoRenewReferenceEndDate = null;
|
||||||
|
line.AutoRenewConfiguredAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSameUtcDate(DateTime? a, DateTime? b)
|
||||||
|
{
|
||||||
|
if (!a.HasValue && !b.HasValue) return true;
|
||||||
|
if (!a.HasValue || !b.HasValue) return false;
|
||||||
|
return DateTime.SpecifyKind(a.Value.Date, DateTimeKind.Utc) == DateTime.SpecifyKind(b.Value.Date, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
private static string OnlyDigits(string? s)
|
private static string OnlyDigits(string? s)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(s)) return "";
|
if (string.IsNullOrWhiteSpace(s)) return "";
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,7 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
||||||
e.HasIndex(x => x.Cliente);
|
e.HasIndex(x => x.Cliente);
|
||||||
e.HasIndex(x => x.Linha);
|
e.HasIndex(x => x.Linha);
|
||||||
e.HasIndex(x => x.DtTerminoFidelizacao);
|
e.HasIndex(x => x.DtTerminoFidelizacao);
|
||||||
|
e.HasIndex(x => x.AutoRenewReferenceEndDate);
|
||||||
e.HasIndex(x => x.TenantId);
|
e.HasIndex(x => x.TenantId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Dtos
|
||||||
|
{
|
||||||
|
public sealed class LinesBatchExcelPreviewResultDto
|
||||||
|
{
|
||||||
|
public string? FileName { get; set; }
|
||||||
|
public string? SheetName { get; set; }
|
||||||
|
public int NextItemStart { get; set; }
|
||||||
|
public int TotalRows { get; set; }
|
||||||
|
public int ValidRows { get; set; }
|
||||||
|
public int InvalidRows { get; set; }
|
||||||
|
public int DuplicateRows { get; set; }
|
||||||
|
public bool CanProceed { get; set; }
|
||||||
|
public List<LinesBatchExcelIssueDto> HeaderErrors { get; set; } = new();
|
||||||
|
public List<LinesBatchExcelIssueDto> HeaderWarnings { get; set; } = new();
|
||||||
|
public List<LinesBatchExcelPreviewRowDto> Rows { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LinesBatchExcelPreviewRowDto
|
||||||
|
{
|
||||||
|
public int SourceRowNumber { get; set; }
|
||||||
|
public int? SourceItem { get; set; }
|
||||||
|
public int? GeneratedItemPreview { get; set; }
|
||||||
|
public bool Valid { get; set; }
|
||||||
|
public bool DuplicateLinhaInFile { get; set; }
|
||||||
|
public bool DuplicateChipInFile { get; set; }
|
||||||
|
public bool DuplicateLinhaInSystem { get; set; }
|
||||||
|
public bool DuplicateChipInSystem { get; set; }
|
||||||
|
public CreateMobileLineDto Data { get; set; } = new();
|
||||||
|
public List<LinesBatchExcelIssueDto> Errors { get; set; } = new();
|
||||||
|
public List<LinesBatchExcelIssueDto> Warnings { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LinesBatchExcelIssueDto
|
||||||
|
{
|
||||||
|
public string? Column { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class AssignReservaLinesRequestDto
|
||||||
|
{
|
||||||
|
public string? ClienteDestino { get; set; }
|
||||||
|
public string? UsuarioDestino { get; set; }
|
||||||
|
public string? SkilDestino { get; set; }
|
||||||
|
public List<Guid> LineIds { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class MoveLinesToReservaRequestDto
|
||||||
|
{
|
||||||
|
public List<Guid> LineIds { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class AssignReservaLinesResultDto
|
||||||
|
{
|
||||||
|
public int Requested { get; set; }
|
||||||
|
public int Updated { get; set; }
|
||||||
|
public int Failed { get; set; }
|
||||||
|
public List<AssignReservaLineItemResultDto> Items { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class AssignReservaLineItemResultDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public int Item { get; set; }
|
||||||
|
public string? Linha { get; set; }
|
||||||
|
public string? Chip { get; set; }
|
||||||
|
public string? ClienteAnterior { get; set; }
|
||||||
|
public string? ClienteNovo { get; set; }
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,10 @@ namespace line_gestao_api.Dtos
|
||||||
public string? PlanoContrato { get; set; }
|
public string? PlanoContrato { get; set; }
|
||||||
public DateTime? DtEfetivacaoServico { get; set; }
|
public DateTime? DtEfetivacaoServico { get; set; }
|
||||||
public DateTime? DtTerminoFidelizacao { get; set; }
|
public DateTime? DtTerminoFidelizacao { get; set; }
|
||||||
|
public int? AutoRenewYears { get; set; }
|
||||||
|
public DateTime? AutoRenewReferenceEndDate { get; set; }
|
||||||
|
public DateTime? AutoRenewConfiguredAt { get; set; }
|
||||||
|
public DateTime? LastAutoRenewedAt { get; set; }
|
||||||
public decimal? Total { get; set; }
|
public decimal? Total { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,6 +53,11 @@ namespace line_gestao_api.Dtos
|
||||||
public decimal? Total { get; set; }
|
public decimal? Total { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ConfigureVigenciaRenewalRequest
|
||||||
|
{
|
||||||
|
public int Years { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class VigenciaClientGroupDto
|
public class VigenciaClientGroupDto
|
||||||
{
|
{
|
||||||
public string Cliente { get; set; } = "";
|
public string Cliente { get; set; } = "";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
using line_gestao_api.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace line_gestao_api.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260227120000_AddVigenciaAutoRenewal")]
|
||||||
|
public partial class AddVigenciaAutoRenewal : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "AutoRenewConfiguredAt",
|
||||||
|
table: "VigenciaLines",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "AutoRenewReferenceEndDate",
|
||||||
|
table: "VigenciaLines",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "AutoRenewYears",
|
||||||
|
table: "VigenciaLines",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "LastAutoRenewedAt",
|
||||||
|
table: "VigenciaLines",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_VigenciaLines_AutoRenewReferenceEndDate",
|
||||||
|
table: "VigenciaLines",
|
||||||
|
column: "AutoRenewReferenceEndDate");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_VigenciaLines_AutoRenewReferenceEndDate",
|
||||||
|
table: "VigenciaLines");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AutoRenewConfiguredAt",
|
||||||
|
table: "VigenciaLines");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AutoRenewReferenceEndDate",
|
||||||
|
table: "VigenciaLines");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AutoRenewYears",
|
||||||
|
table: "VigenciaLines");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LastAutoRenewedAt",
|
||||||
|
table: "VigenciaLines");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1549,6 +1549,15 @@ namespace line_gestao_api.Migrations
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("AutoRenewConfiguredAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("AutoRenewReferenceEndDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int?>("AutoRenewYears")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
b.Property<string>("Cliente")
|
b.Property<string>("Cliente")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
|
@ -1567,6 +1576,9 @@ namespace line_gestao_api.Migrations
|
||||||
b.Property<int>("Item")
|
b.Property<int>("Item")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastAutoRenewedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<string>("Linha")
|
b.Property<string>("Linha")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
|
@ -1589,6 +1601,8 @@ namespace line_gestao_api.Migrations
|
||||||
|
|
||||||
b.HasIndex("Cliente");
|
b.HasIndex("Cliente");
|
||||||
|
|
||||||
|
b.HasIndex("AutoRenewReferenceEndDate");
|
||||||
|
|
||||||
b.HasIndex("DtTerminoFidelizacao");
|
b.HasIndex("DtTerminoFidelizacao");
|
||||||
|
|
||||||
b.HasIndex("Item");
|
b.HasIndex("Item");
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ namespace line_gestao_api.Models
|
||||||
|
|
||||||
public DateTime? DtEfetivacaoServico { get; set; }
|
public DateTime? DtEfetivacaoServico { get; set; }
|
||||||
public DateTime? DtTerminoFidelizacao { get; set; }
|
public DateTime? DtTerminoFidelizacao { get; set; }
|
||||||
|
public int? AutoRenewYears { get; set; }
|
||||||
|
public DateTime? AutoRenewReferenceEndDate { get; set; }
|
||||||
|
public DateTime? AutoRenewConfiguredAt { get; set; }
|
||||||
|
public DateTime? LastAutoRenewedAt { get; set; }
|
||||||
|
|
||||||
public decimal? Total { get; set; }
|
public decimal? Total { get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ builder.Services.AddScoped<ISystemAuditService, SystemAuditService>();
|
||||||
builder.Services.AddScoped<IVigenciaNotificationSyncService, VigenciaNotificationSyncService>();
|
builder.Services.AddScoped<IVigenciaNotificationSyncService, VigenciaNotificationSyncService>();
|
||||||
builder.Services.AddScoped<ParcelamentosImportService>();
|
builder.Services.AddScoped<ParcelamentosImportService>();
|
||||||
builder.Services.AddScoped<GeralDashboardInsightsService>();
|
builder.Services.AddScoped<GeralDashboardInsightsService>();
|
||||||
|
builder.Services.AddScoped<GeralSpreadsheetTemplateService>();
|
||||||
builder.Services.AddScoped<SpreadsheetImportAuditService>();
|
builder.Services.AddScoped<SpreadsheetImportAuditService>();
|
||||||
|
|
||||||
builder.Services.AddIdentityCore<ApplicationUser>(options =>
|
builder.Services.AddIdentityCore<ApplicationUser>(options =>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
using ClosedXML.Excel;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Services
|
||||||
|
{
|
||||||
|
public class GeralSpreadsheetTemplateService
|
||||||
|
{
|
||||||
|
private const string WorksheetName = "GERAL";
|
||||||
|
private const int HeaderRow = 1;
|
||||||
|
private const int FirstDataRow = 2;
|
||||||
|
private const int LastColumn = 31; // A..AE
|
||||||
|
|
||||||
|
private static readonly string[] Headers =
|
||||||
|
{
|
||||||
|
"CONTA",
|
||||||
|
"LINHA",
|
||||||
|
"CHIP",
|
||||||
|
"CLIENTE",
|
||||||
|
"USUÁRIO",
|
||||||
|
"PLANO CONTRATO",
|
||||||
|
"FRAQUIA",
|
||||||
|
"VALOR DO PLANO R$",
|
||||||
|
"GESTÃO VOZ E DADOS R$",
|
||||||
|
"SKEELO",
|
||||||
|
"VIVO NEWS PLUS",
|
||||||
|
"VIVO TRAVEL MUNDO",
|
||||||
|
"VIVO SYNC",
|
||||||
|
"VIVO GESTÃO DISPOSITIVO",
|
||||||
|
"VALOR CONTRATO VIVO",
|
||||||
|
"FRANQUIA LINE",
|
||||||
|
"FRANQUIA GESTÃO",
|
||||||
|
"LOCAÇÃO AP.",
|
||||||
|
"VALOR CONTRATO LINE",
|
||||||
|
"DESCONTO",
|
||||||
|
"LUCRO",
|
||||||
|
"STATUS",
|
||||||
|
"DATA DO BLOQUEIO",
|
||||||
|
"SKIL",
|
||||||
|
"MODALIDADE",
|
||||||
|
"CEDENTE",
|
||||||
|
"SOLICITANTE",
|
||||||
|
"DATA DA ENTREGA OPERA.",
|
||||||
|
"DATA DA ENTREGA CLIENTE",
|
||||||
|
"VENC. DA CONTA",
|
||||||
|
"TIPO DE CHIP"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly double[] ColumnWidths =
|
||||||
|
{
|
||||||
|
11.00, 12.00, 21.43, 70.14, 58.29, 27.71, 11.00, 18.14,
|
||||||
|
20.71, 11.00, 14.57, 18.14, 11.00, 19.57, 21.43, 14.57,
|
||||||
|
16.29, 13.00, 20.71, 13.00, 13.00, 14.57, 16.29, 13.00,
|
||||||
|
16.29, 39.43, 27.86, 25.00, 27.71, 16.29, 13.00
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly int[] TextColumns =
|
||||||
|
{
|
||||||
|
1, 2, 3, 4, 5, 6, 7, 16, 17, 22, 24, 25, 26, 27, 31
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly int[] CurrencyColumns =
|
||||||
|
{
|
||||||
|
8, 9, 10, 11, 12, 13, 14, 15, 18, 19, 20, 21
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly int[] DateColumns =
|
||||||
|
{
|
||||||
|
23, 28, 29, 30
|
||||||
|
};
|
||||||
|
|
||||||
|
public byte[] BuildPlanilhaGeralTemplate()
|
||||||
|
{
|
||||||
|
using var workbook = new XLWorkbook();
|
||||||
|
var ws = workbook.Worksheets.Add(WorksheetName);
|
||||||
|
|
||||||
|
BuildHeader(ws);
|
||||||
|
ConfigureColumns(ws);
|
||||||
|
ConfigureDataFormatting(ws);
|
||||||
|
ConfigureSheetView(ws);
|
||||||
|
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
workbook.SaveAs(stream);
|
||||||
|
return stream.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BuildHeader(IXLWorksheet ws)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < Headers.Length; i++)
|
||||||
|
{
|
||||||
|
ws.Cell(HeaderRow, i + 1).Value = Headers[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerRange = ws.Range(HeaderRow, 1, HeaderRow, LastColumn);
|
||||||
|
headerRange.Style.Font.FontName = "Calibri";
|
||||||
|
headerRange.Style.Font.FontSize = 11;
|
||||||
|
headerRange.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
|
||||||
|
headerRange.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
|
||||||
|
headerRange.Style.Border.TopBorder = XLBorderStyleValues.Thin;
|
||||||
|
headerRange.Style.Border.BottomBorder = XLBorderStyleValues.Thin;
|
||||||
|
headerRange.Style.Border.LeftBorder = XLBorderStyleValues.Thin;
|
||||||
|
headerRange.Style.Border.RightBorder = XLBorderStyleValues.Thin;
|
||||||
|
|
||||||
|
ws.Row(HeaderRow).Height = 14.25;
|
||||||
|
|
||||||
|
var navy = XLColor.FromHtml("#002060");
|
||||||
|
var purple = XLColor.FromHtml("#7030A0");
|
||||||
|
var orange = XLColor.FromHtml("#D9A87E");
|
||||||
|
var red = XLColor.FromHtml("#FF0000");
|
||||||
|
var yellow = XLColor.FromHtml("#FFFF00");
|
||||||
|
var white = XLColor.White;
|
||||||
|
var black = XLColor.Black;
|
||||||
|
|
||||||
|
ApplyHeaderBlock(ws, 1, 6, navy, white, bold: true); // A-F
|
||||||
|
ApplyHeaderBlock(ws, 22, 31, navy, white, bold: true); // V-AE
|
||||||
|
ApplyHeaderBlock(ws, 7, 15, purple, white, bold: true); // G-O
|
||||||
|
ApplyHeaderBlock(ws, 16, 19, orange, white, bold: true);// P-S
|
||||||
|
ApplyHeaderBlock(ws, 20, 20, red, white, bold: true); // T
|
||||||
|
ApplyHeaderBlock(ws, 21, 21, yellow, black, bold: true);// U
|
||||||
|
|
||||||
|
// Exceções no bloco azul (sem negrito): CHIP, CLIENTE, USUÁRIO => C, D, E
|
||||||
|
ws.Cell(1, 3).Style.Font.Bold = false;
|
||||||
|
ws.Cell(1, 4).Style.Font.Bold = false;
|
||||||
|
ws.Cell(1, 5).Style.Font.Bold = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyHeaderBlock(
|
||||||
|
IXLWorksheet ws,
|
||||||
|
int startCol,
|
||||||
|
int endCol,
|
||||||
|
XLColor bgColor,
|
||||||
|
XLColor fontColor,
|
||||||
|
bool bold)
|
||||||
|
{
|
||||||
|
var range = ws.Range(HeaderRow, startCol, HeaderRow, endCol);
|
||||||
|
range.Style.Fill.BackgroundColor = bgColor;
|
||||||
|
range.Style.Font.FontColor = fontColor;
|
||||||
|
range.Style.Font.Bold = bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureColumns(IXLWorksheet ws)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < ColumnWidths.Length; i++)
|
||||||
|
{
|
||||||
|
ws.Column(i + 1).Width = ColumnWidths[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureDataFormatting(IXLWorksheet ws)
|
||||||
|
{
|
||||||
|
// Prepara um range vazio com estilo base para facilitar preenchimento manual
|
||||||
|
var dataPreviewRange = ws.Range(FirstDataRow, 1, 1000, LastColumn);
|
||||||
|
dataPreviewRange.Style.Font.FontName = "Calibri";
|
||||||
|
dataPreviewRange.Style.Font.FontSize = 11;
|
||||||
|
dataPreviewRange.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
|
||||||
|
|
||||||
|
foreach (var col in TextColumns)
|
||||||
|
{
|
||||||
|
ws.Column(col).Style.NumberFormat.Format = "@";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var col in CurrencyColumns)
|
||||||
|
{
|
||||||
|
ws.Column(col).Style.NumberFormat.Format = "\"R$\" #,##0.00";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var col in DateColumns)
|
||||||
|
{
|
||||||
|
ws.Column(col).Style.DateFormat.Format = "dd/MM/yyyy";
|
||||||
|
}
|
||||||
|
|
||||||
|
// O campo ITÉM é gerado internamente pelo sistema e não faz parte do template.
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureSheetView(IXLWorksheet ws)
|
||||||
|
{
|
||||||
|
ws.ShowGridLines = false;
|
||||||
|
ws.SheetView.FreezeRows(1); // Freeze em A2 (mantém linha 1 fixa)
|
||||||
|
ws.Range(1, 1, 1, LastColumn).SetAutoFilter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -83,6 +83,8 @@ public class VigenciaNotificationSyncService : IVigenciaNotificationSyncService
|
||||||
.Where(u => !string.IsNullOrWhiteSpace(u.Email))
|
.Where(u => !string.IsNullOrWhiteSpace(u.Email))
|
||||||
.ToDictionary(u => u.Email!.Trim().ToLowerInvariant(), u => u.Id);
|
.ToDictionary(u => u.Email!.Trim().ToLowerInvariant(), u => u.Id);
|
||||||
|
|
||||||
|
await ApplyAutoRenewalsAsync(tenantId, today, userByName, userByEmail, cancellationToken);
|
||||||
|
|
||||||
var vigencias = await _db.VigenciaLines.AsNoTracking()
|
var vigencias = await _db.VigenciaLines.AsNoTracking()
|
||||||
.Where(v => v.DtTerminoFidelizacao != null)
|
.Where(v => v.DtTerminoFidelizacao != null)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
@ -213,6 +215,112 @@ public class VigenciaNotificationSyncService : IVigenciaNotificationSyncService
|
||||||
await _db.SaveChangesAsync(cancellationToken);
|
await _db.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ApplyAutoRenewalsAsync(
|
||||||
|
Guid tenantId,
|
||||||
|
DateTime todayUtc,
|
||||||
|
IReadOnlyDictionary<string, Guid> userByName,
|
||||||
|
IReadOnlyDictionary<string, Guid> userByEmail,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var scheduledLines = await _db.VigenciaLines
|
||||||
|
.Where(v =>
|
||||||
|
v.AutoRenewYears != null &&
|
||||||
|
v.AutoRenewReferenceEndDate != null &&
|
||||||
|
v.DtTerminoFidelizacao != null)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (scheduledLines.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var changed = false;
|
||||||
|
var autoRenewNotifications = new List<Notification>();
|
||||||
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
foreach (var vigencia in scheduledLines)
|
||||||
|
{
|
||||||
|
var years = NormalizeAutoRenewYears(vigencia.AutoRenewYears);
|
||||||
|
if (!years.HasValue || !vigencia.DtTerminoFidelizacao.HasValue || !vigencia.AutoRenewReferenceEndDate.HasValue)
|
||||||
|
{
|
||||||
|
if (vigencia.AutoRenewYears.HasValue || vigencia.AutoRenewReferenceEndDate.HasValue || vigencia.AutoRenewConfiguredAt.HasValue)
|
||||||
|
{
|
||||||
|
ClearAutoRenewSchedule(vigencia);
|
||||||
|
vigencia.UpdatedAt = nowUtc;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentEndUtc = ToUtcDate(vigencia.DtTerminoFidelizacao.Value);
|
||||||
|
var referenceEndUtc = ToUtcDate(vigencia.AutoRenewReferenceEndDate.Value);
|
||||||
|
|
||||||
|
// As datas de vigência foram alteradas manualmente após o agendamento:
|
||||||
|
// não renova automaticamente e limpa o agendamento.
|
||||||
|
if (currentEndUtc != referenceEndUtc)
|
||||||
|
{
|
||||||
|
ClearAutoRenewSchedule(vigencia);
|
||||||
|
vigencia.UpdatedAt = nowUtc;
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Só executa a renovação no vencimento (ou se já passou e segue sem alteração manual).
|
||||||
|
if (currentEndUtc > todayUtc)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newStartUtc = currentEndUtc.AddDays(1);
|
||||||
|
var newEndUtc = currentEndUtc.AddYears(years.Value);
|
||||||
|
|
||||||
|
vigencia.DtEfetivacaoServico = newStartUtc;
|
||||||
|
vigencia.DtTerminoFidelizacao = newEndUtc;
|
||||||
|
vigencia.LastAutoRenewedAt = nowUtc;
|
||||||
|
ClearAutoRenewSchedule(vigencia);
|
||||||
|
vigencia.UpdatedAt = nowUtc;
|
||||||
|
changed = true;
|
||||||
|
|
||||||
|
autoRenewNotifications.Add(BuildAutoRenewNotification(
|
||||||
|
vigencia,
|
||||||
|
years.Value,
|
||||||
|
currentEndUtc,
|
||||||
|
newEndUtc,
|
||||||
|
ResolveUserId(vigencia.Usuario, userByName, userByEmail),
|
||||||
|
tenantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed && autoRenewNotifications.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoRenewNotifications.Count > 0)
|
||||||
|
{
|
||||||
|
var dedupKeys = autoRenewNotifications
|
||||||
|
.Select(n => n.DedupKey)
|
||||||
|
.Distinct(StringComparer.Ordinal)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var existingDedupKeys = await _db.Notifications.AsNoTracking()
|
||||||
|
.Where(n => dedupKeys.Contains(n.DedupKey))
|
||||||
|
.Select(n => n.DedupKey)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var existingSet = existingDedupKeys.ToHashSet(StringComparer.Ordinal);
|
||||||
|
autoRenewNotifications = autoRenewNotifications
|
||||||
|
.Where(n => !existingSet.Contains(n.DedupKey))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (autoRenewNotifications.Count > 0)
|
||||||
|
{
|
||||||
|
await _db.Notifications.AddRangeAsync(autoRenewNotifications, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CleanupOutdatedNotificationsAsync(
|
private async Task CleanupOutdatedNotificationsAsync(
|
||||||
IReadOnlyCollection<VigenciaLine> vigencias,
|
IReadOnlyCollection<VigenciaLine> vigencias,
|
||||||
bool notifyAllFutureDates,
|
bool notifyAllFutureDates,
|
||||||
|
|
@ -349,6 +457,38 @@ public class VigenciaNotificationSyncService : IVigenciaNotificationSyncService
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Notification BuildAutoRenewNotification(
|
||||||
|
VigenciaLine vigencia,
|
||||||
|
int years,
|
||||||
|
DateTime previousEndUtc,
|
||||||
|
DateTime newEndUtc,
|
||||||
|
Guid? userId,
|
||||||
|
Guid tenantId)
|
||||||
|
{
|
||||||
|
var linha = vigencia.Linha?.Trim();
|
||||||
|
var cliente = vigencia.Cliente?.Trim();
|
||||||
|
var usuario = vigencia.Usuario?.Trim();
|
||||||
|
var dedupKey = BuildAutoRenewDedupKey(tenantId, vigencia.Id, previousEndUtc, years);
|
||||||
|
|
||||||
|
return new Notification
|
||||||
|
{
|
||||||
|
Tipo = "RenovacaoAutomatica",
|
||||||
|
Titulo = $"Renovação automática concluída{FormatLinha(linha)}",
|
||||||
|
Mensagem = $"A linha{FormatLinha(linha)} do cliente {cliente ?? "(sem cliente)"} foi renovada automaticamente por {years} ano(s): {previousEndUtc:dd/MM/yyyy} → {newEndUtc:dd/MM/yyyy}.",
|
||||||
|
Data = DateTime.UtcNow,
|
||||||
|
ReferenciaData = newEndUtc,
|
||||||
|
DiasParaVencer = null,
|
||||||
|
Lida = false,
|
||||||
|
DedupKey = dedupKey,
|
||||||
|
UserId = userId,
|
||||||
|
Usuario = usuario,
|
||||||
|
Cliente = cliente,
|
||||||
|
Linha = linha,
|
||||||
|
VigenciaLineId = vigencia.Id,
|
||||||
|
TenantId = tenantId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static string BuildDedupKey(
|
private static string BuildDedupKey(
|
||||||
string tipo,
|
string tipo,
|
||||||
DateTime referenciaData,
|
DateTime referenciaData,
|
||||||
|
|
@ -370,6 +510,59 @@ public class VigenciaNotificationSyncService : IVigenciaNotificationSyncService
|
||||||
return string.Join('|', parts);
|
return string.Join('|', parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string BuildAutoRenewDedupKey(Guid tenantId, Guid vigenciaLineId, DateTime referenceEndDateUtc, int years)
|
||||||
|
{
|
||||||
|
return string.Join('|', new[]
|
||||||
|
{
|
||||||
|
"renovacaoautomatica",
|
||||||
|
tenantId.ToString("N"),
|
||||||
|
vigenciaLineId.ToString("N"),
|
||||||
|
referenceEndDateUtc.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||||
|
years.ToString(CultureInfo.InvariantCulture)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Guid? ResolveUserId(
|
||||||
|
string? usuario,
|
||||||
|
IReadOnlyDictionary<string, Guid> userByName,
|
||||||
|
IReadOnlyDictionary<string, Guid> userByEmail)
|
||||||
|
{
|
||||||
|
var key = usuario?.Trim().ToLowerInvariant();
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userByEmail.TryGetValue(key, out var byEmail))
|
||||||
|
{
|
||||||
|
return byEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userByName.TryGetValue(key, out var byName))
|
||||||
|
{
|
||||||
|
return byName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int? NormalizeAutoRenewYears(int? years)
|
||||||
|
{
|
||||||
|
return years == 2 ? years : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime ToUtcDate(DateTime value)
|
||||||
|
{
|
||||||
|
return DateTime.SpecifyKind(value.Date, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ClearAutoRenewSchedule(VigenciaLine vigencia)
|
||||||
|
{
|
||||||
|
vigencia.AutoRenewYears = null;
|
||||||
|
vigencia.AutoRenewReferenceEndDate = null;
|
||||||
|
vigencia.AutoRenewConfiguredAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
private static string FormatLinha(string? linha)
|
private static string FormatLinha(string? linha)
|
||||||
{
|
{
|
||||||
return string.IsNullOrWhiteSpace(linha) ? string.Empty : $" {linha}";
|
return string.IsNullOrWhiteSpace(linha) ? string.Empty : $" {linha}";
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue