5907 lines
245 KiB
C#
5907 lines
245 KiB
C#
using ClosedXML.Excel;
|
|
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.Hosting;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.StaticFiles;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using System.Security.Claims;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace line_gestao_api.Controllers
|
|
{
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
[Authorize]
|
|
public class LinesController : ControllerBase
|
|
{
|
|
private readonly AppDbContext _db;
|
|
private readonly ITenantProvider _tenantProvider;
|
|
private readonly IVigenciaNotificationSyncService _vigenciaNotificationSyncService;
|
|
private readonly ParcelamentosImportService _parcelamentosImportService;
|
|
private readonly SpreadsheetImportAuditService _spreadsheetImportAuditService;
|
|
private readonly string _aparelhoAttachmentsRootPath;
|
|
private static readonly FileExtensionContentTypeProvider FileContentTypeProvider = new();
|
|
|
|
private static string NormalizeContaValue(string? conta)
|
|
{
|
|
var raw = (conta ?? string.Empty).Trim();
|
|
if (string.IsNullOrWhiteSpace(raw)) return string.Empty;
|
|
if (!raw.All(char.IsDigit)) return raw.ToUpperInvariant();
|
|
|
|
var withoutLeadingZero = raw.TrimStart('0');
|
|
return string.IsNullOrWhiteSpace(withoutLeadingZero) ? "0" : withoutLeadingZero;
|
|
}
|
|
|
|
private static string? FindEmpresaByConta(string? conta)
|
|
{
|
|
var normalizedConta = NormalizeContaValue(conta);
|
|
if (string.IsNullOrWhiteSpace(normalizedConta)) return null;
|
|
|
|
return OperadoraContaResolver.GetAccountCompanies()
|
|
.FirstOrDefault(group => group.Contas.Any(c => NormalizeContaValue(c) == normalizedConta))
|
|
?.Empresa;
|
|
}
|
|
|
|
private static string? ValidateContaEmpresaBinding(string? conta)
|
|
{
|
|
var contaTrimmed = (conta ?? string.Empty).Trim();
|
|
if (string.IsNullOrWhiteSpace(contaTrimmed))
|
|
return "A Conta é obrigatória.";
|
|
|
|
return string.IsNullOrWhiteSpace(FindEmpresaByConta(contaTrimmed))
|
|
? $"A conta {contaTrimmed} não está vinculada a nenhuma Empresa (Conta) cadastrada."
|
|
: null;
|
|
}
|
|
|
|
public LinesController(
|
|
AppDbContext db,
|
|
ITenantProvider tenantProvider,
|
|
IVigenciaNotificationSyncService vigenciaNotificationSyncService,
|
|
ParcelamentosImportService parcelamentosImportService,
|
|
SpreadsheetImportAuditService spreadsheetImportAuditService,
|
|
IWebHostEnvironment webHostEnvironment)
|
|
{
|
|
_db = db;
|
|
_tenantProvider = tenantProvider;
|
|
_vigenciaNotificationSyncService = vigenciaNotificationSyncService;
|
|
_parcelamentosImportService = parcelamentosImportService;
|
|
_spreadsheetImportAuditService = spreadsheetImportAuditService;
|
|
_aparelhoAttachmentsRootPath = Path.Combine(webHostEnvironment.ContentRootPath, "uploads", "aparelhos");
|
|
}
|
|
|
|
public class ImportExcelForm
|
|
{
|
|
public IFormFile File { get; set; } = default!;
|
|
}
|
|
|
|
public class UploadAparelhoAnexosForm
|
|
{
|
|
public IFormFile? NotaFiscal { get; set; }
|
|
public IFormFile? Recibo { get; set; }
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ 1. ENDPOINT: AGRUPAR POR CLIENTE
|
|
// ==========================================================
|
|
[HttpGet("groups")]
|
|
public async Task<ActionResult<PagedResult<ClientGroupDto>>> GetClientGroups(
|
|
[FromQuery] string? skil,
|
|
[FromQuery] string? search,
|
|
[FromQuery] string? additionalMode,
|
|
[FromQuery] string? additionalServices,
|
|
[FromQuery] int page = 1,
|
|
[FromQuery] int pageSize = 10)
|
|
{
|
|
page = page < 1 ? 1 : page;
|
|
pageSize = pageSize < 1 ? 10 : pageSize;
|
|
|
|
var query = _db.MobileLines.AsNoTracking();
|
|
var reservaFilter = false;
|
|
|
|
// Filtro SKIL
|
|
if (!string.IsNullOrWhiteSpace(skil))
|
|
{
|
|
var sSkil = skil.Trim();
|
|
if (sSkil.Equals("RESERVA", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
reservaFilter = true;
|
|
query = query.Where(x =>
|
|
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
|
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") ||
|
|
EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
|
}
|
|
else
|
|
query = query.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%"));
|
|
}
|
|
|
|
if (!reservaFilter)
|
|
query = ExcludeReservaContext(query);
|
|
|
|
query = ApplyAdditionalFilters(query, additionalMode, additionalServices);
|
|
|
|
IQueryable<ClientGroupDto> groupedQuery;
|
|
|
|
if (reservaFilter)
|
|
{
|
|
var userDataClientByLine = BuildUserDataClientByLineQuery();
|
|
var userDataClientByItem = BuildUserDataClientByItemQuery();
|
|
var reservaRows =
|
|
from line in query
|
|
join udLine in userDataClientByLine on line.Linha equals udLine.Linha into udLineJoin
|
|
from udLine in udLineJoin.DefaultIfEmpty()
|
|
join udItem in userDataClientByItem on line.Item equals udItem.Item into udItemJoin
|
|
from udItem in udItemJoin.DefaultIfEmpty()
|
|
let clienteOriginal = (line.Cliente ?? "").Trim()
|
|
let skilOriginal = (line.Skil ?? "").Trim()
|
|
let clientePorLinha = (udLine.Cliente ?? "").Trim()
|
|
let clientePorItem = (udItem.Cliente ?? "").Trim()
|
|
let reservaEstrita = EF.Functions.ILike(clienteOriginal, "RESERVA") &&
|
|
EF.Functions.ILike(skilOriginal, "RESERVA")
|
|
let clienteEfetivo = reservaEstrita
|
|
? "RESERVA"
|
|
: (!string.IsNullOrEmpty(clienteOriginal) &&
|
|
!EF.Functions.ILike(clienteOriginal, "RESERVA"))
|
|
? clienteOriginal
|
|
: (!string.IsNullOrEmpty(clientePorLinha) &&
|
|
!EF.Functions.ILike(clientePorLinha, "RESERVA"))
|
|
? clientePorLinha
|
|
: (!string.IsNullOrEmpty(clientePorItem) &&
|
|
!EF.Functions.ILike(clientePorItem, "RESERVA"))
|
|
? clientePorItem
|
|
: ""
|
|
select new
|
|
{
|
|
Cliente = string.IsNullOrEmpty(clienteEfetivo) ? "RESERVA" : clienteEfetivo,
|
|
line.Status
|
|
};
|
|
|
|
if (!string.IsNullOrWhiteSpace(search))
|
|
{
|
|
var s = search.Trim();
|
|
reservaRows = reservaRows.Where(x => EF.Functions.ILike(x.Cliente ?? "", $"%{s}%"));
|
|
}
|
|
|
|
groupedQuery = reservaRows
|
|
.GroupBy(x => x.Cliente)
|
|
.Select(g => new ClientGroupDto
|
|
{
|
|
Cliente = g.Key!,
|
|
TotalLinhas = g.Count(),
|
|
Ativos = g.Count(x => EF.Functions.ILike(x.Status ?? "", "%ativo%")),
|
|
Bloqueados = g.Count(x => EF.Functions.ILike(x.Status ?? "", "%bloque%") ||
|
|
EF.Functions.ILike(x.Status ?? "", "%perda%") ||
|
|
EF.Functions.ILike(x.Status ?? "", "%roubo%"))
|
|
});
|
|
}
|
|
else
|
|
{
|
|
query = query.Where(x => !string.IsNullOrEmpty(x.Cliente));
|
|
|
|
if (!string.IsNullOrWhiteSpace(search))
|
|
{
|
|
var s = search.Trim();
|
|
query = query.Where(x => EF.Functions.ILike(x.Cliente ?? "", $"%{s}%"));
|
|
}
|
|
|
|
groupedQuery = query.GroupBy(x => x.Cliente)
|
|
.Select(g => new ClientGroupDto
|
|
{
|
|
Cliente = g.Key!,
|
|
TotalLinhas = g.Count(),
|
|
Ativos = g.Count(x => EF.Functions.ILike(x.Status ?? "", "%ativo%")),
|
|
Bloqueados = g.Count(x => EF.Functions.ILike(x.Status ?? "", "%bloque%") ||
|
|
EF.Functions.ILike(x.Status ?? "", "%perda%") ||
|
|
EF.Functions.ILike(x.Status ?? "", "%roubo%"))
|
|
});
|
|
}
|
|
|
|
var totalGroups = await groupedQuery.CountAsync();
|
|
|
|
var orderedGroupedQuery = reservaFilter
|
|
? groupedQuery
|
|
.OrderByDescending(x => EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"))
|
|
.ThenBy(x => x.Cliente)
|
|
: groupedQuery.OrderBy(x => x.Cliente);
|
|
|
|
var items = await orderedGroupedQuery
|
|
.Skip((page - 1) * pageSize)
|
|
.Take(pageSize)
|
|
.ToListAsync();
|
|
|
|
return Ok(new PagedResult<ClientGroupDto>
|
|
{
|
|
Page = page,
|
|
PageSize = pageSize,
|
|
Total = totalGroups,
|
|
Items = items
|
|
});
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ 2. ENDPOINT: LISTAR NOMES DE CLIENTES (ACEITA SKIL)
|
|
// ==========================================================
|
|
[HttpGet("clients")]
|
|
public async Task<ActionResult<List<string>>> GetClients(
|
|
[FromQuery] string? skil,
|
|
[FromQuery] string? additionalMode,
|
|
[FromQuery] string? additionalServices)
|
|
{
|
|
var query = _db.MobileLines.AsNoTracking();
|
|
var reservaFilter = false;
|
|
|
|
if (!string.IsNullOrWhiteSpace(skil))
|
|
{
|
|
var sSkil = skil.Trim();
|
|
if (sSkil.Equals("RESERVA", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
reservaFilter = true;
|
|
query = query.Where(x =>
|
|
EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") ||
|
|
EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") ||
|
|
EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"));
|
|
}
|
|
else
|
|
query = query.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%"));
|
|
}
|
|
|
|
if (!reservaFilter)
|
|
query = ExcludeReservaContext(query);
|
|
|
|
query = ApplyAdditionalFilters(query, additionalMode, additionalServices);
|
|
|
|
List<string> clients;
|
|
if (reservaFilter)
|
|
{
|
|
var userDataClientByLine = BuildUserDataClientByLineQuery();
|
|
var userDataClientByItem = BuildUserDataClientByItemQuery();
|
|
clients = await (
|
|
from line in query
|
|
join udLine in userDataClientByLine on line.Linha equals udLine.Linha into udLineJoin
|
|
from udLine in udLineJoin.DefaultIfEmpty()
|
|
join udItem in userDataClientByItem on line.Item equals udItem.Item into udItemJoin
|
|
from udItem in udItemJoin.DefaultIfEmpty()
|
|
let clienteOriginal = (line.Cliente ?? "").Trim()
|
|
let skilOriginal = (line.Skil ?? "").Trim()
|
|
let clientePorLinha = (udLine.Cliente ?? "").Trim()
|
|
let clientePorItem = (udItem.Cliente ?? "").Trim()
|
|
let reservaEstrita = EF.Functions.ILike(clienteOriginal, "RESERVA") &&
|
|
EF.Functions.ILike(skilOriginal, "RESERVA")
|
|
let clienteEfetivo = reservaEstrita
|
|
? "RESERVA"
|
|
: (!string.IsNullOrEmpty(clienteOriginal) &&
|
|
!EF.Functions.ILike(clienteOriginal, "RESERVA"))
|
|
? clienteOriginal
|
|
: (!string.IsNullOrEmpty(clientePorLinha) &&
|
|
!EF.Functions.ILike(clientePorLinha, "RESERVA"))
|
|
? clientePorLinha
|
|
: (!string.IsNullOrEmpty(clientePorItem) &&
|
|
!EF.Functions.ILike(clientePorItem, "RESERVA"))
|
|
? clientePorItem
|
|
: ""
|
|
let clienteExibicao = string.IsNullOrEmpty(clienteEfetivo) ? "RESERVA" : clienteEfetivo
|
|
select clienteExibicao
|
|
)
|
|
.Distinct()
|
|
.OrderByDescending(x => EF.Functions.ILike((x ?? "").Trim(), "RESERVA"))
|
|
.ThenBy(x => x)
|
|
.ToListAsync();
|
|
}
|
|
else
|
|
{
|
|
clients = await query
|
|
.Where(x => !string.IsNullOrEmpty(x.Cliente))
|
|
.Select(x => x.Cliente!)
|
|
.Distinct()
|
|
.OrderBy(x => x)
|
|
.ToListAsync();
|
|
}
|
|
|
|
return Ok(clients);
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ ENDPOINTS DO FATURAMENTO (PF/PJ)
|
|
// ==========================================================
|
|
[HttpGet("billing")]
|
|
public async Task<ActionResult<PagedResult<BillingClient>>> GetBilling(
|
|
[FromQuery] string? tipo, // "PF", "PJ" ou null (todos)
|
|
[FromQuery] string? search, // busca por cliente
|
|
[FromQuery] int page = 1,
|
|
[FromQuery] int pageSize = 20,
|
|
[FromQuery] string? sortBy = "cliente",
|
|
[FromQuery] string? sortDir = "asc")
|
|
{
|
|
page = page < 1 ? 1 : page;
|
|
pageSize = pageSize < 1 ? 20 : pageSize;
|
|
|
|
var q = _db.BillingClients.AsNoTracking();
|
|
|
|
if (!string.IsNullOrWhiteSpace(tipo))
|
|
{
|
|
var t = tipo.Trim().ToUpperInvariant();
|
|
if (t == "PF" || t == "PJ") q = q.Where(x => x.Tipo == t);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(search))
|
|
{
|
|
var s = search.Trim();
|
|
q = q.Where(x => EF.Functions.ILike(x.Cliente ?? "", $"%{s}%"));
|
|
}
|
|
|
|
var total = await q.CountAsync();
|
|
|
|
var sb = (sortBy ?? "cliente").Trim().ToLowerInvariant();
|
|
var desc = string.Equals((sortDir ?? "asc").Trim(), "desc", StringComparison.OrdinalIgnoreCase);
|
|
|
|
q = sb switch
|
|
{
|
|
"item" => desc ? q.OrderByDescending(x => x.Item) : q.OrderBy(x => x.Item),
|
|
"tipo" => desc ? q.OrderByDescending(x => x.Tipo).ThenBy(x => x.Cliente) : q.OrderBy(x => x.Tipo).ThenBy(x => x.Cliente),
|
|
"qtdlinhas" => desc ? q.OrderByDescending(x => x.QtdLinhas ?? 0).ThenBy(x => x.Cliente) : q.OrderBy(x => x.QtdLinhas ?? 0).ThenBy(x => x.Cliente),
|
|
"franquiavivo" => desc ? q.OrderByDescending(x => x.FranquiaVivo ?? 0).ThenBy(x => x.Cliente) : q.OrderBy(x => x.FranquiaVivo ?? 0).ThenBy(x => x.Cliente),
|
|
"valorcontratovivo" => desc ? q.OrderByDescending(x => x.ValorContratoVivo ?? 0).ThenBy(x => x.Cliente) : q.OrderBy(x => x.ValorContratoVivo ?? 0).ThenBy(x => x.Cliente),
|
|
"franquialine" => desc ? q.OrderByDescending(x => x.FranquiaLine ?? 0).ThenBy(x => x.Cliente) : q.OrderBy(x => x.FranquiaLine ?? 0).ThenBy(x => x.Cliente),
|
|
"valorcontratoline" => desc ? q.OrderByDescending(x => x.ValorContratoLine ?? 0).ThenBy(x => x.Cliente) : q.OrderBy(x => x.ValorContratoLine ?? 0).ThenBy(x => x.Cliente),
|
|
"lucro" => desc ? q.OrderByDescending(x => x.Lucro ?? 0).ThenBy(x => x.Cliente) : q.OrderBy(x => x.Lucro ?? 0).ThenBy(x => x.Cliente),
|
|
"aparelho" => desc ? q.OrderByDescending(x => x.Aparelho ?? "").ThenBy(x => x.Cliente) : q.OrderBy(x => x.Aparelho ?? "").ThenBy(x => x.Cliente),
|
|
"formapagamento" => desc ? q.OrderByDescending(x => x.FormaPagamento ?? "").ThenBy(x => x.Cliente) : q.OrderBy(x => x.FormaPagamento ?? "").ThenBy(x => x.Cliente),
|
|
_ => desc ? q.OrderByDescending(x => x.Cliente).ThenBy(x => x.Item) : q.OrderBy(x => x.Cliente).ThenBy(x => x.Item),
|
|
};
|
|
|
|
var items = await q
|
|
.Skip((page - 1) * pageSize)
|
|
.Take(pageSize)
|
|
.ToListAsync();
|
|
|
|
return Ok(new PagedResult<BillingClient>
|
|
{
|
|
Page = page,
|
|
PageSize = pageSize,
|
|
Total = total,
|
|
Items = items
|
|
});
|
|
}
|
|
|
|
[HttpGet("billing/clients")]
|
|
public async Task<ActionResult<List<string>>> GetBillingClients([FromQuery] string? tipo)
|
|
{
|
|
var q = _db.BillingClients.AsNoTracking();
|
|
|
|
if (!string.IsNullOrWhiteSpace(tipo))
|
|
{
|
|
var t = tipo.Trim().ToUpperInvariant();
|
|
if (t == "PF" || t == "PJ") q = q.Where(x => x.Tipo == t);
|
|
}
|
|
|
|
var clients = await q
|
|
.Where(x => !string.IsNullOrEmpty(x.Cliente))
|
|
.Select(x => x.Cliente!)
|
|
.Distinct()
|
|
.OrderBy(x => x)
|
|
.ToListAsync();
|
|
|
|
return Ok(clients);
|
|
}
|
|
|
|
[HttpGet("account-companies")]
|
|
public ActionResult<List<AccountCompanyDto>> GetAccountCompanies()
|
|
{
|
|
var items = OperadoraContaResolver.GetAccountCompanies();
|
|
return Ok(items);
|
|
}
|
|
|
|
[HttpGet("accounts")]
|
|
public ActionResult<List<string>> GetAccounts([FromQuery] string? empresa)
|
|
{
|
|
var contas = OperadoraContaResolver.GetAccountsByEmpresa(empresa);
|
|
return Ok(contas);
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ 2.1 ENDPOINT: LINHAS POR CLIENTE (para SELECT do MUREG)
|
|
// GET: /api/lines/by-client?cliente=...
|
|
// ==========================================================
|
|
[HttpGet("by-client")]
|
|
public async Task<ActionResult<List<LineOptionDto>>> GetLinesByClient([FromQuery] string cliente)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(cliente))
|
|
return Ok(new List<LineOptionDto>());
|
|
|
|
var c = cliente.Trim();
|
|
|
|
// ⚠️ use ILike para não depender de maiúscula/minúscula
|
|
var items = await _db.MobileLines
|
|
.AsNoTracking()
|
|
.Where(x => x.Cliente != null && EF.Functions.ILike(x.Cliente, c))
|
|
.Where(x => x.Linha != null && x.Linha != "")
|
|
.OrderBy(x => x.Item)
|
|
.Select(x => new LineOptionDto
|
|
{
|
|
Id = x.Id,
|
|
Item = x.Item,
|
|
Linha = x.Linha,
|
|
Chip = x.Chip,
|
|
Cliente = x.Cliente,
|
|
Usuario = x.Usuario,
|
|
Skil = x.Skil
|
|
})
|
|
.ToListAsync();
|
|
|
|
return Ok(items);
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ 3. GET ALL (GERAL)
|
|
// ==========================================================
|
|
[HttpGet]
|
|
public async Task<ActionResult<PagedResult<MobileLineListDto>>> GetAll(
|
|
[FromQuery] string? search,
|
|
[FromQuery] string? skil,
|
|
[FromQuery] string? client,
|
|
[FromQuery] string? operadora,
|
|
[FromQuery] string? additionalMode,
|
|
[FromQuery] string? additionalServices,
|
|
[FromQuery] int page = 1,
|
|
[FromQuery] int pageSize = 20,
|
|
[FromQuery] string? sortBy = "item",
|
|
[FromQuery] string? sortDir = "asc")
|
|
{
|
|
page = page < 1 ? 1 : page;
|
|
pageSize = pageSize < 1 ? 20 : pageSize;
|
|
|
|
var q = _db.MobileLines.AsNoTracking();
|
|
var reservaFilter = false;
|
|
|
|
if (!string.IsNullOrWhiteSpace(skil))
|
|
{
|
|
var sSkil = skil.Trim();
|
|
if (sSkil.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 ?? "", $"%{sSkil}%"));
|
|
}
|
|
|
|
if (!reservaFilter)
|
|
q = ExcludeReservaContext(q);
|
|
|
|
q = ApplyAdditionalFilters(q, additionalMode, additionalServices);
|
|
q = OperadoraContaResolver.ApplyOperadoraFilter(q, operadora);
|
|
|
|
var sb = (sortBy ?? "item").Trim().ToLowerInvariant();
|
|
var desc = string.Equals((sortDir ?? "asc").Trim(), "desc", StringComparison.OrdinalIgnoreCase);
|
|
|
|
if (sb == "plano") sb = "planocontrato";
|
|
if (sb == "contrato") sb = "vencconta";
|
|
|
|
if (reservaFilter)
|
|
{
|
|
var userDataClientByLine = BuildUserDataClientByLineQuery();
|
|
var userDataClientByItem = BuildUserDataClientByItemQuery();
|
|
var rq =
|
|
from line in q
|
|
join udLine in userDataClientByLine on line.Linha equals udLine.Linha into udLineJoin
|
|
from udLine in udLineJoin.DefaultIfEmpty()
|
|
join udItem in userDataClientByItem on line.Item equals udItem.Item into udItemJoin
|
|
from udItem in udItemJoin.DefaultIfEmpty()
|
|
let clienteOriginal = (line.Cliente ?? "").Trim()
|
|
let skilOriginal = (line.Skil ?? "").Trim()
|
|
let clientePorLinha = (udLine.Cliente ?? "").Trim()
|
|
let clientePorItem = (udItem.Cliente ?? "").Trim()
|
|
let reservaEstrita = EF.Functions.ILike(clienteOriginal, "RESERVA") &&
|
|
EF.Functions.ILike(skilOriginal, "RESERVA")
|
|
let clienteEfetivo = reservaEstrita
|
|
? "RESERVA"
|
|
: (!string.IsNullOrEmpty(clienteOriginal) &&
|
|
!EF.Functions.ILike(clienteOriginal, "RESERVA"))
|
|
? clienteOriginal
|
|
: (!string.IsNullOrEmpty(clientePorLinha) &&
|
|
!EF.Functions.ILike(clientePorLinha, "RESERVA"))
|
|
? clientePorLinha
|
|
: (!string.IsNullOrEmpty(clientePorItem) &&
|
|
!EF.Functions.ILike(clientePorItem, "RESERVA"))
|
|
? clientePorItem
|
|
: ""
|
|
let clienteExibicao = string.IsNullOrEmpty(clienteEfetivo) ? "RESERVA" : clienteEfetivo
|
|
select new
|
|
{
|
|
line.Id,
|
|
line.Item,
|
|
line.Conta,
|
|
line.Linha,
|
|
line.Chip,
|
|
Cliente = clienteExibicao,
|
|
line.Usuario,
|
|
line.CentroDeCustos,
|
|
SetorNome = line.Setor != null ? line.Setor!.Nome : null,
|
|
AparelhoNome = line.Aparelho != null ? line.Aparelho!.Nome : null,
|
|
AparelhoCor = line.Aparelho != null ? line.Aparelho!.Cor : null,
|
|
line.PlanoContrato,
|
|
line.Status,
|
|
line.Skil,
|
|
line.Modalidade,
|
|
line.VencConta,
|
|
line.FranquiaVivo,
|
|
line.FranquiaLine,
|
|
line.GestaoVozDados,
|
|
line.Skeelo,
|
|
line.VivoNewsPlus,
|
|
line.VivoTravelMundo,
|
|
line.VivoSync,
|
|
line.VivoGestaoDispositivo,
|
|
line.TipoDeChip
|
|
};
|
|
|
|
if (!string.IsNullOrWhiteSpace(client))
|
|
rq = rq.Where(x => EF.Functions.ILike(x.Cliente ?? "", client.Trim()));
|
|
|
|
if (!string.IsNullOrWhiteSpace(search))
|
|
{
|
|
var s = search.Trim();
|
|
rq = rq.Where(x =>
|
|
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
|
|
EF.Functions.ILike(x.Chip ?? "", $"%{s}%") ||
|
|
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
|
|
EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") ||
|
|
EF.Functions.ILike(x.Conta ?? "", $"%{s}%") ||
|
|
EF.Functions.ILike(x.Status ?? "", $"%{s}%"));
|
|
}
|
|
|
|
var totalReserva = await rq.CountAsync();
|
|
|
|
rq = sb switch
|
|
{
|
|
"conta" => desc ? rq.OrderByDescending(x => x.Conta ?? "").ThenBy(x => x.Item) : rq.OrderBy(x => x.Conta ?? "").ThenBy(x => x.Item),
|
|
"linha" => desc ? rq.OrderByDescending(x => x.Linha ?? "").ThenBy(x => x.Item) : rq.OrderBy(x => x.Linha ?? "").ThenBy(x => x.Item),
|
|
"chip" => desc ? rq.OrderByDescending(x => x.Chip ?? "").ThenBy(x => x.Item) : rq.OrderBy(x => x.Chip ?? "").ThenBy(x => x.Item),
|
|
"cliente" => desc
|
|
? rq.OrderByDescending(x => EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"))
|
|
.ThenByDescending(x => x.Cliente ?? "")
|
|
.ThenBy(x => x.Item)
|
|
: rq.OrderByDescending(x => EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"))
|
|
.ThenBy(x => x.Cliente ?? "")
|
|
.ThenBy(x => x.Item),
|
|
"usuario" => desc ? rq.OrderByDescending(x => x.Usuario ?? "").ThenBy(x => x.Item) : rq.OrderBy(x => x.Usuario ?? "").ThenBy(x => x.Item),
|
|
"planocontrato" => desc ? rq.OrderByDescending(x => x.PlanoContrato ?? "").ThenBy(x => x.Item) : rq.OrderBy(x => x.PlanoContrato ?? "").ThenBy(x => x.Item),
|
|
"vencconta" => desc ? rq.OrderByDescending(x => x.VencConta ?? "").ThenBy(x => x.Item) : rq.OrderBy(x => x.VencConta ?? "").ThenBy(x => x.Item),
|
|
"status" => desc ? rq.OrderByDescending(x => x.Status ?? "").ThenBy(x => x.Item) : rq.OrderBy(x => x.Status ?? "").ThenBy(x => x.Item),
|
|
"skil" => desc ? rq.OrderByDescending(x => x.Skil ?? "").ThenBy(x => x.Item) : rq.OrderBy(x => x.Skil ?? "").ThenBy(x => x.Item),
|
|
"modalidade" => desc ? rq.OrderByDescending(x => x.Modalidade ?? "").ThenBy(x => x.Item) : rq.OrderBy(x => x.Modalidade ?? "").ThenBy(x => x.Item),
|
|
_ => desc ? rq.OrderByDescending(x => x.Item) : rq.OrderBy(x => x.Item)
|
|
};
|
|
|
|
var itemsReserva = await rq
|
|
.Skip((page - 1) * pageSize)
|
|
.Take(pageSize)
|
|
.Select(x => new MobileLineListDto
|
|
{
|
|
Id = x.Id,
|
|
Item = x.Item,
|
|
Conta = x.Conta,
|
|
Linha = x.Linha,
|
|
Chip = x.Chip,
|
|
Cliente = x.Cliente,
|
|
Usuario = x.Usuario,
|
|
CentroDeCustos = x.CentroDeCustos,
|
|
SetorNome = x.SetorNome,
|
|
AparelhoNome = x.AparelhoNome,
|
|
AparelhoCor = x.AparelhoCor,
|
|
PlanoContrato = x.PlanoContrato,
|
|
Status = x.Status,
|
|
Skil = x.Skil,
|
|
Modalidade = x.Modalidade,
|
|
VencConta = x.VencConta,
|
|
FranquiaVivo = x.FranquiaVivo,
|
|
FranquiaLine = x.FranquiaLine,
|
|
GestaoVozDados = x.GestaoVozDados,
|
|
Skeelo = x.Skeelo,
|
|
VivoNewsPlus = x.VivoNewsPlus,
|
|
VivoTravelMundo = x.VivoTravelMundo,
|
|
VivoSync = x.VivoSync,
|
|
VivoGestaoDispositivo = x.VivoGestaoDispositivo,
|
|
TipoDeChip = x.TipoDeChip
|
|
})
|
|
.ToListAsync();
|
|
|
|
EnrichOperadoraContext(itemsReserva);
|
|
|
|
return Ok(new PagedResult<MobileLineListDto>
|
|
{
|
|
Page = page,
|
|
PageSize = pageSize,
|
|
Total = totalReserva,
|
|
Items = itemsReserva
|
|
});
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(client))
|
|
q = q.Where(x => EF.Functions.ILike(x.Cliente ?? "", client.Trim()));
|
|
|
|
if (!string.IsNullOrWhiteSpace(search))
|
|
{
|
|
var s = search.Trim();
|
|
q = q.Where(x =>
|
|
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
|
|
EF.Functions.ILike(x.Chip ?? "", $"%{s}%") ||
|
|
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
|
|
EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") ||
|
|
EF.Functions.ILike(x.Conta ?? "", $"%{s}%") ||
|
|
EF.Functions.ILike(x.Status ?? "", $"%{s}%"));
|
|
}
|
|
|
|
var total = await q.CountAsync();
|
|
|
|
q = sb switch
|
|
{
|
|
"conta" => desc ? q.OrderByDescending(x => x.Conta ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.Conta ?? "").ThenBy(x => x.Item),
|
|
"linha" => desc ? q.OrderByDescending(x => x.Linha ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.Linha ?? "").ThenBy(x => x.Item),
|
|
"chip" => desc ? q.OrderByDescending(x => x.Chip ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.Chip ?? "").ThenBy(x => x.Item),
|
|
"cliente" => desc ? q.OrderByDescending(x => x.Cliente ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.Cliente ?? "").ThenBy(x => x.Item),
|
|
"usuario" => desc ? q.OrderByDescending(x => x.Usuario ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.Usuario ?? "").ThenBy(x => x.Item),
|
|
"planocontrato" => desc ? q.OrderByDescending(x => x.PlanoContrato ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.PlanoContrato ?? "").ThenBy(x => x.Item),
|
|
"vencconta" => desc ? q.OrderByDescending(x => x.VencConta ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.VencConta ?? "").ThenBy(x => x.Item),
|
|
"status" => desc ? q.OrderByDescending(x => x.Status ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.Status ?? "").ThenBy(x => x.Item),
|
|
"skil" => desc ? q.OrderByDescending(x => x.Skil ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.Skil ?? "").ThenBy(x => x.Item),
|
|
"modalidade" => desc ? q.OrderByDescending(x => x.Modalidade ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.Modalidade ?? "").ThenBy(x => x.Item),
|
|
_ => desc ? q.OrderByDescending(x => x.Item) : q.OrderBy(x => x.Item)
|
|
};
|
|
|
|
var items = await q
|
|
.Skip((page - 1) * pageSize)
|
|
.Take(pageSize)
|
|
.Select(x => new MobileLineListDto
|
|
{
|
|
Id = x.Id,
|
|
Item = x.Item,
|
|
Conta = x.Conta,
|
|
Linha = x.Linha,
|
|
Chip = x.Chip,
|
|
Cliente = x.Cliente,
|
|
Usuario = x.Usuario,
|
|
CentroDeCustos = x.CentroDeCustos,
|
|
SetorNome = x.Setor != null ? x.Setor.Nome : null,
|
|
AparelhoNome = x.Aparelho != null ? x.Aparelho.Nome : null,
|
|
AparelhoCor = x.Aparelho != null ? x.Aparelho.Cor : null,
|
|
PlanoContrato = x.PlanoContrato,
|
|
Status = x.Status,
|
|
Skil = x.Skil,
|
|
Modalidade = x.Modalidade,
|
|
VencConta = x.VencConta,
|
|
FranquiaVivo = x.FranquiaVivo,
|
|
FranquiaLine = x.FranquiaLine,
|
|
GestaoVozDados = x.GestaoVozDados,
|
|
Skeelo = x.Skeelo,
|
|
VivoNewsPlus = x.VivoNewsPlus,
|
|
VivoTravelMundo = x.VivoTravelMundo,
|
|
VivoSync = x.VivoSync,
|
|
VivoGestaoDispositivo = x.VivoGestaoDispositivo,
|
|
TipoDeChip = x.TipoDeChip
|
|
})
|
|
.ToListAsync();
|
|
|
|
EnrichOperadoraContext(items);
|
|
|
|
return Ok(new PagedResult<MobileLineListDto>
|
|
{
|
|
Page = page,
|
|
PageSize = pageSize,
|
|
Total = total,
|
|
Items = items
|
|
});
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ 4. GET BY ID
|
|
// ==========================================================
|
|
[HttpGet("{id:guid}")]
|
|
public async Task<ActionResult<MobileLineDetailDto>> GetById(Guid id)
|
|
{
|
|
var x = await _db.MobileLines
|
|
.AsNoTracking()
|
|
.Include(a => a.Setor)
|
|
.Include(a => a.Aparelho)
|
|
.FirstOrDefaultAsync(a => a.Id == id);
|
|
if (x == null) return NotFound();
|
|
var vigencia = await FindVigenciaByMobileLineAsync(x, null, asNoTracking: true);
|
|
return Ok(ToDetailDto(x, vigencia));
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ 5. CREATE
|
|
// ==========================================================
|
|
[HttpPost]
|
|
[Authorize(Roles = "sysadmin,gestor")]
|
|
public async Task<ActionResult<MobileLineDetailDto>> Create([FromBody] CreateMobileLineDto req)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(req.Cliente))
|
|
return BadRequest(new { message = "O nome do Cliente é obrigatório." });
|
|
|
|
if (string.IsNullOrWhiteSpace(req.Linha))
|
|
return BadRequest(new { message = "O número da Linha é obrigatório." });
|
|
|
|
if (!req.DtEfetivacaoServico.HasValue)
|
|
return BadRequest(new { message = "A Dt. Efetivação Serviço é obrigatória." });
|
|
|
|
if (!req.DtTerminoFidelizacao.HasValue)
|
|
return BadRequest(new { message = "A Dt. Término Fidelização é obrigatória." });
|
|
|
|
var linhaLimpa = OnlyDigits(req.Linha);
|
|
var chipLimpo = OnlyDigits(req.Chip);
|
|
|
|
if (string.IsNullOrWhiteSpace(linhaLimpa))
|
|
return BadRequest(new { message = "Número de linha inválido." });
|
|
|
|
var contaValidationMessage = ValidateContaEmpresaBinding(req.Conta);
|
|
if (!string.IsNullOrWhiteSpace(contaValidationMessage))
|
|
return BadRequest(new { message = contaValidationMessage });
|
|
|
|
MobileLine? lineToPersist = null;
|
|
|
|
if (req.ReservaLineId.HasValue && req.ReservaLineId.Value != Guid.Empty)
|
|
{
|
|
lineToPersist = await _db.MobileLines.FirstOrDefaultAsync(x => x.Id == req.ReservaLineId.Value);
|
|
if (lineToPersist == null)
|
|
return BadRequest(new { message = "A linha selecionada na Reserva não foi encontrada." });
|
|
|
|
if (!IsReservaLineForTransfer(lineToPersist))
|
|
return Conflict(new { message = "A linha selecionada não está mais disponível na Reserva." });
|
|
|
|
if (!string.IsNullOrWhiteSpace(lineToPersist.Linha) &&
|
|
!string.Equals(lineToPersist.Linha, linhaLimpa, StringComparison.Ordinal))
|
|
{
|
|
return BadRequest(new { message = "A linha selecionada na Reserva não corresponde ao número informado." });
|
|
}
|
|
}
|
|
|
|
if (lineToPersist == null)
|
|
{
|
|
var existingByLinha = await _db.MobileLines.FirstOrDefaultAsync(x => x.Linha == linhaLimpa);
|
|
if (existingByLinha != null)
|
|
{
|
|
if (IsReservaLineForTransfer(existingByLinha))
|
|
{
|
|
lineToPersist = existingByLinha;
|
|
}
|
|
else
|
|
{
|
|
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 && (lineToPersist == null || x.Id != lineToPersist.Id));
|
|
if (chipExists)
|
|
return Conflict(new { message = $"O Chip (ICCID) {req.Chip} já está cadastrado no sistema." });
|
|
}
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
var planSuggestion = await AutoFillRules.ResolvePlanSuggestionAsync(_db, req.PlanoContrato);
|
|
var franquiaVivo = req.FranquiaVivo ?? planSuggestion?.FranquiaGb;
|
|
var valorPlanoVivo = req.ValorPlanoVivo ?? planSuggestion?.ValorPlano;
|
|
|
|
var isReassignmentFromReserva = lineToPersist != null;
|
|
if (!isReassignmentFromReserva)
|
|
{
|
|
var maxItem = await _db.MobileLines.MaxAsync(x => (int?)x.Item) ?? 0;
|
|
lineToPersist = new MobileLine
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Item = maxItem + 1,
|
|
CreatedAt = now
|
|
};
|
|
_db.MobileLines.Add(lineToPersist);
|
|
}
|
|
|
|
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)
|
|
{
|
|
lineToPersist.TenantId = ensuredTenant.Id;
|
|
lineToPersist.Cliente = ensuredTenant.NomeOficial;
|
|
}
|
|
|
|
await ApplySetorAndAparelhoToLineAsync(lineToPersist, req);
|
|
|
|
var vigencia = await UpsertVigenciaFromMobileLineAsync(
|
|
lineToPersist,
|
|
req.DtEfetivacaoServico,
|
|
req.DtTerminoFidelizacao,
|
|
overrideDates: false,
|
|
previousLinha: isReassignmentFromReserva ? previousLinha : null);
|
|
|
|
try
|
|
{
|
|
await _db.SaveChangesAsync();
|
|
await _vigenciaNotificationSyncService.SyncCurrentTenantAsync();
|
|
}
|
|
catch (DbUpdateException)
|
|
{
|
|
return StatusCode(500, new { message = "Erro ao salvar no banco de dados." });
|
|
}
|
|
|
|
if (isReassignmentFromReserva)
|
|
{
|
|
return Ok(ToDetailDto(lineToPersist, vigencia));
|
|
}
|
|
|
|
return CreatedAtAction(nameof(GetById), new { id = lineToPersist.Id }, ToDetailDto(lineToPersist, vigencia));
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ 5.1. CREATE BATCH
|
|
// ==========================================================
|
|
[HttpPost("batch")]
|
|
[Authorize(Roles = "sysadmin,gestor")]
|
|
public async Task<ActionResult<CreateMobileLinesBatchResultDto>> CreateBatch([FromBody] CreateMobileLinesBatchRequestDto req)
|
|
{
|
|
var requests = req?.Lines ?? new List<CreateMobileLineDto>();
|
|
if (requests.Count == 0)
|
|
return BadRequest(new { message = "Informe ao menos uma linha para cadastro em lote." });
|
|
|
|
if (requests.Count > 1000)
|
|
return BadRequest(new { message = "O lote excede o limite de 1000 linhas por envio." });
|
|
|
|
var requestedLinhas = requests
|
|
.Select(x => OnlyDigits(x?.Linha))
|
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
|
.Distinct(StringComparer.Ordinal)
|
|
.ToList();
|
|
|
|
var requestedChips = requests
|
|
.Select(x => OnlyDigits(x?.Chip))
|
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
|
.Distinct(StringComparer.Ordinal)
|
|
.ToList();
|
|
|
|
var existingLinhas = requestedLinhas.Count == 0
|
|
? new HashSet<string>(StringComparer.Ordinal)
|
|
: (await _db.MobileLines.AsNoTracking()
|
|
.Where(x => x.Linha != null && requestedLinhas.Contains(x.Linha))
|
|
.Select(x => x.Linha!)
|
|
.ToListAsync())
|
|
.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();
|
|
|
|
try
|
|
{
|
|
var nextItem = (await _db.MobileLines.MaxAsync(x => (int?)x.Item) ?? 0);
|
|
var seenBatchLinhas = new HashSet<string>(StringComparer.Ordinal);
|
|
var seenBatchChips = new HashSet<string>(StringComparer.Ordinal);
|
|
var tenantCache = new Dictionary<string, Tenant?>(StringComparer.Ordinal);
|
|
var createdLines = new List<(MobileLine line, VigenciaLine? vigencia)>(requests.Count);
|
|
|
|
for (var i = 0; i < requests.Count; i++)
|
|
{
|
|
var entry = requests[i];
|
|
var lineNo = i + 1;
|
|
|
|
if (entry == null)
|
|
return BadRequest(new { message = $"Linha do lote #{lineNo} está vazia." });
|
|
|
|
if (string.IsNullOrWhiteSpace(entry.Cliente))
|
|
return BadRequest(new { message = $"Linha do lote #{lineNo}: o nome do Cliente é obrigatório." });
|
|
|
|
if (string.IsNullOrWhiteSpace(entry.Linha))
|
|
return BadRequest(new { message = $"Linha do lote #{lineNo}: o número da Linha é obrigatório." });
|
|
|
|
if (!entry.DtEfetivacaoServico.HasValue)
|
|
return BadRequest(new { message = $"Linha do lote #{lineNo}: a Dt. Efetivação Serviço é obrigatória." });
|
|
|
|
if (!entry.DtTerminoFidelizacao.HasValue)
|
|
return BadRequest(new { message = $"Linha do lote #{lineNo}: a Dt. Término Fidelização é obrigatória." });
|
|
|
|
var linhaLimpa = OnlyDigits(entry.Linha);
|
|
var chipLimpo = OnlyDigits(entry.Chip);
|
|
|
|
if (string.IsNullOrWhiteSpace(linhaLimpa))
|
|
return BadRequest(new { message = $"Linha do lote #{lineNo}: número de linha inválido." });
|
|
|
|
if (!seenBatchLinhas.Add(linhaLimpa))
|
|
return Conflict(new { message = $"A linha {entry.Linha} está duplicada dentro do lote (registro #{lineNo})." });
|
|
|
|
if (existingLinhas.Contains(linhaLimpa))
|
|
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})." });
|
|
}
|
|
|
|
var contaValidationMessage = ValidateContaEmpresaBinding(entry.Conta);
|
|
if (!string.IsNullOrWhiteSpace(contaValidationMessage))
|
|
return BadRequest(new { message = $"Linha do lote #{lineNo}: {contaValidationMessage}" });
|
|
|
|
nextItem++;
|
|
|
|
var planSuggestion = await AutoFillRules.ResolvePlanSuggestionAsync(_db, entry.PlanoContrato);
|
|
var franquiaVivo = entry.FranquiaVivo ?? planSuggestion?.FranquiaGb;
|
|
var valorPlanoVivo = entry.ValorPlanoVivo ?? planSuggestion?.ValorPlano;
|
|
var now = DateTime.UtcNow;
|
|
|
|
var newLine = new MobileLine
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Item = nextItem,
|
|
Cliente = entry.Cliente.Trim(),
|
|
Linha = linhaLimpa,
|
|
Chip = string.IsNullOrWhiteSpace(chipLimpo) ? null : chipLimpo,
|
|
Usuario = entry.Usuario?.Trim(),
|
|
CentroDeCustos = NormalizeOptionalText(entry.CentroDeCustos),
|
|
Status = entry.Status?.Trim(),
|
|
Skil = entry.Skil?.Trim(),
|
|
Modalidade = entry.Modalidade?.Trim(),
|
|
PlanoContrato = entry.PlanoContrato?.Trim(),
|
|
Conta = entry.Conta?.Trim(),
|
|
VencConta = entry.VencConta?.Trim(),
|
|
|
|
DataBloqueio = ToUtc(entry.DataBloqueio),
|
|
DataEntregaOpera = ToUtc(entry.DataEntregaOpera),
|
|
DataEntregaCliente = ToUtc(entry.DataEntregaCliente),
|
|
|
|
Cedente = entry.Cedente?.Trim(),
|
|
Solicitante = entry.Solicitante?.Trim(),
|
|
|
|
FranquiaVivo = franquiaVivo,
|
|
ValorPlanoVivo = valorPlanoVivo,
|
|
GestaoVozDados = entry.GestaoVozDados,
|
|
Skeelo = entry.Skeelo,
|
|
VivoNewsPlus = entry.VivoNewsPlus,
|
|
VivoTravelMundo = entry.VivoTravelMundo,
|
|
VivoSync = entry.VivoSync,
|
|
VivoGestaoDispositivo = entry.VivoGestaoDispositivo,
|
|
ValorContratoVivo = entry.ValorContratoVivo,
|
|
FranquiaLine = entry.FranquiaLine,
|
|
FranquiaGestao = entry.FranquiaGestao,
|
|
LocacaoAp = entry.LocacaoAp,
|
|
ValorContratoLine = entry.ValorContratoLine,
|
|
Desconto = entry.Desconto,
|
|
Lucro = entry.Lucro,
|
|
TipoDeChip = entry.TipoDeChip?.Trim(),
|
|
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
};
|
|
|
|
ApplyReservaRule(newLine);
|
|
|
|
var tenantCacheKey = NormalizeTenantKeyValue(newLine.Cliente ?? string.Empty);
|
|
if (!tenantCache.TryGetValue(tenantCacheKey, out var ensuredTenant))
|
|
{
|
|
ensuredTenant = await EnsureTenantForClientAsync(newLine.Cliente);
|
|
tenantCache[tenantCacheKey] = ensuredTenant;
|
|
}
|
|
|
|
if (ensuredTenant != null)
|
|
{
|
|
newLine.TenantId = ensuredTenant.Id;
|
|
newLine.Cliente = ensuredTenant.NomeOficial;
|
|
}
|
|
|
|
await ApplySetorAndAparelhoToLineAsync(newLine, entry);
|
|
|
|
_db.MobileLines.Add(newLine);
|
|
|
|
var vigencia = await UpsertVigenciaFromMobileLineAsync(
|
|
newLine,
|
|
entry.DtEfetivacaoServico,
|
|
entry.DtTerminoFidelizacao,
|
|
overrideDates: false);
|
|
|
|
createdLines.Add((newLine, vigencia));
|
|
}
|
|
|
|
await _db.SaveChangesAsync();
|
|
await tx.CommitAsync();
|
|
await _vigenciaNotificationSyncService.SyncCurrentTenantAsync();
|
|
|
|
return Ok(new CreateMobileLinesBatchResultDto
|
|
{
|
|
Created = createdLines.Count,
|
|
Items = createdLines
|
|
.Select(x => new CreateMobileLinesBatchCreatedItemDto
|
|
{
|
|
Id = x.line.Id,
|
|
Item = x.line.Item,
|
|
Linha = x.line.Linha,
|
|
Cliente = x.line.Cliente
|
|
})
|
|
.ToList()
|
|
});
|
|
}
|
|
catch (DbUpdateException)
|
|
{
|
|
await tx.RollbackAsync();
|
|
return StatusCode(500, new { message = "Erro ao salvar o lote no banco de dados." });
|
|
}
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ 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 ensuredTenantDestino = await EnsureTenantForClientAsync(clienteDestino);
|
|
var clienteDestinoOficial = ensuredTenantDestino?.NomeOficial ?? clienteDestino;
|
|
|
|
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 = clienteDestinoOficial;
|
|
if (ensuredTenantDestino != null)
|
|
{
|
|
line.TenantId = ensuredTenantDestino.Id;
|
|
}
|
|
|
|
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." });
|
|
}
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ 5.5. BLOQUEIO / DESBLOQUEIO EM LOTE (GERAL)
|
|
// ==========================================================
|
|
[HttpPost("batch-status-update")]
|
|
[Authorize(Roles = "sysadmin,gestor")]
|
|
public async Task<ActionResult<BatchLineStatusUpdateResultDto>> BatchStatusUpdate([FromBody] BatchLineStatusUpdateRequestDto? req)
|
|
{
|
|
if (req is null)
|
|
return BadRequest(new { message = "Payload inválido para processamento em lote." });
|
|
|
|
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;
|
|
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
|
|
// ==========================================================
|
|
[HttpPut("{id:guid}")]
|
|
[Authorize(Roles = "sysadmin,gestor,cliente")]
|
|
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateMobileLineRequest req)
|
|
{
|
|
var x = await _db.MobileLines
|
|
.Include(a => a.Setor)
|
|
.Include(a => a.Aparelho)
|
|
.FirstOrDefaultAsync(a => a.Id == id);
|
|
if (x == null) return NotFound();
|
|
|
|
var canManageFullLine = User.IsInRole(AppRoles.SysAdmin) || User.IsInRole(AppRoles.Gestor);
|
|
if (!canManageFullLine)
|
|
{
|
|
var tenantId = x.TenantId != Guid.Empty
|
|
? x.TenantId
|
|
: (_tenantProvider.ActorTenantId ?? Guid.Empty);
|
|
if (tenantId == Guid.Empty)
|
|
{
|
|
return BadRequest(new { message = "Tenant inválido para atualizar linha." });
|
|
}
|
|
|
|
if (x.TenantId == Guid.Empty)
|
|
{
|
|
x.TenantId = tenantId;
|
|
}
|
|
|
|
x.Usuario = NormalizeOptionalText(req.Usuario);
|
|
x.CentroDeCustos = NormalizeOptionalText(req.CentroDeCustos);
|
|
|
|
await ApplySetorToLineAsync(x, tenantId, req.SetorId, req.SetorNome);
|
|
await ApplyAparelhoToLineAsync(
|
|
x,
|
|
tenantId,
|
|
req.AparelhoId,
|
|
req.AparelhoNome,
|
|
req.AparelhoCor,
|
|
req.AparelhoImei);
|
|
|
|
x.UpdatedAt = DateTime.UtcNow;
|
|
|
|
try
|
|
{
|
|
await _db.SaveChangesAsync();
|
|
await _vigenciaNotificationSyncService.SyncCurrentTenantAsync();
|
|
}
|
|
catch (DbUpdateException)
|
|
{
|
|
return Conflict(new { message = "Conflito ao salvar." });
|
|
}
|
|
|
|
return NoContent();
|
|
}
|
|
|
|
var previousLinha = x.Linha;
|
|
var previousCliente = x.Cliente;
|
|
|
|
var newLinha = OnlyDigits(req.Linha);
|
|
if (!string.IsNullOrWhiteSpace(newLinha) && !string.Equals((x.Linha ?? ""), newLinha, StringComparison.Ordinal))
|
|
{
|
|
var exists = await _db.MobileLines.AsNoTracking().AnyAsync(m => m.Linha == newLinha && m.Id != id);
|
|
if (exists) return Conflict(new { message = "Já existe registro com essa LINHA.", linha = newLinha });
|
|
}
|
|
|
|
var contaValidationMessage = ValidateContaEmpresaBinding(req.Conta);
|
|
if (!string.IsNullOrWhiteSpace(contaValidationMessage))
|
|
return BadRequest(new { message = contaValidationMessage });
|
|
|
|
x.Conta = req.Conta?.Trim();
|
|
x.Linha = string.IsNullOrWhiteSpace(newLinha) ? null : newLinha;
|
|
|
|
var newChip = OnlyDigits(req.Chip);
|
|
x.Chip = string.IsNullOrWhiteSpace(newChip) ? null : newChip;
|
|
|
|
x.Cliente = NormalizeOptionalText(req.Cliente);
|
|
x.Usuario = NormalizeOptionalText(req.Usuario);
|
|
x.CentroDeCustos = NormalizeOptionalText(req.CentroDeCustos);
|
|
x.PlanoContrato = NormalizeOptionalText(req.PlanoContrato);
|
|
x.FranquiaVivo = req.FranquiaVivo;
|
|
x.ValorPlanoVivo = req.ValorPlanoVivo;
|
|
x.GestaoVozDados = req.GestaoVozDados;
|
|
x.Skeelo = req.Skeelo;
|
|
x.VivoNewsPlus = req.VivoNewsPlus;
|
|
x.VivoTravelMundo = req.VivoTravelMundo;
|
|
x.VivoSync = req.VivoSync;
|
|
x.VivoGestaoDispositivo = req.VivoGestaoDispositivo;
|
|
x.ValorContratoVivo = req.ValorContratoVivo;
|
|
x.FranquiaLine = req.FranquiaLine;
|
|
x.FranquiaGestao = req.FranquiaGestao;
|
|
x.LocacaoAp = req.LocacaoAp;
|
|
x.ValorContratoLine = req.ValorContratoLine;
|
|
x.Desconto = req.Desconto;
|
|
x.Lucro = req.Lucro;
|
|
x.Status = NormalizeOptionalText(req.Status);
|
|
x.DataBloqueio = ToUtc(req.DataBloqueio);
|
|
x.Skil = NormalizeOptionalText(req.Skil);
|
|
x.Modalidade = NormalizeOptionalText(req.Modalidade);
|
|
x.Cedente = NormalizeOptionalText(req.Cedente);
|
|
x.Solicitante = NormalizeOptionalText(req.Solicitante);
|
|
x.DataEntregaOpera = ToUtc(req.DataEntregaOpera);
|
|
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);
|
|
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);
|
|
var ensuredTenant = await EnsureTenantForClientAsync(x.Cliente);
|
|
if (ensuredTenant != null)
|
|
{
|
|
x.TenantId = ensuredTenant.Id;
|
|
x.Cliente = ensuredTenant.NomeOficial;
|
|
}
|
|
|
|
await ApplySetorToLineAsync(x, x.TenantId, req.SetorId, req.SetorNome);
|
|
await ApplyAparelhoToLineAsync(
|
|
x,
|
|
x.TenantId,
|
|
req.AparelhoId,
|
|
req.AparelhoNome,
|
|
req.AparelhoCor,
|
|
req.AparelhoImei);
|
|
|
|
await UpsertVigenciaFromMobileLineAsync(
|
|
x,
|
|
req.DtEfetivacaoServico,
|
|
req.DtTerminoFidelizacao,
|
|
overrideDates: false,
|
|
previousLinha: previousLinha);
|
|
x.UpdatedAt = DateTime.UtcNow;
|
|
|
|
try
|
|
{
|
|
await _db.SaveChangesAsync();
|
|
await _vigenciaNotificationSyncService.SyncCurrentTenantAsync();
|
|
}
|
|
catch (DbUpdateException) { return Conflict(new { message = "Conflito ao salvar." }); }
|
|
|
|
return NoContent();
|
|
}
|
|
|
|
[HttpPost("{id:guid}/aparelho/anexos")]
|
|
[Authorize(Roles = "sysadmin,gestor,cliente")]
|
|
[Consumes("multipart/form-data")]
|
|
[RequestSizeLimit(25_000_000)]
|
|
public async Task<ActionResult<MobileLineDetailDto>> UploadAparelhoAnexos(
|
|
Guid id,
|
|
[FromForm] UploadAparelhoAnexosForm form)
|
|
{
|
|
var hasNotaFiscal = form.NotaFiscal != null && form.NotaFiscal.Length > 0;
|
|
var hasRecibo = form.Recibo != null && form.Recibo.Length > 0;
|
|
|
|
if (!hasNotaFiscal && !hasRecibo)
|
|
{
|
|
return BadRequest(new { message = "Envie ao menos um arquivo (Nota Fiscal ou Recibo)." });
|
|
}
|
|
|
|
var line = await _db.MobileLines
|
|
.Include(x => x.Aparelho)
|
|
.FirstOrDefaultAsync(x => x.Id == id);
|
|
if (line == null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
var tenantId = line.TenantId != Guid.Empty
|
|
? line.TenantId
|
|
: (_tenantProvider.ActorTenantId ?? Guid.Empty);
|
|
if (tenantId == Guid.Empty)
|
|
{
|
|
return BadRequest(new { message = "Tenant inválido para salvar anexo." });
|
|
}
|
|
|
|
if (line.TenantId == Guid.Empty)
|
|
{
|
|
line.TenantId = tenantId;
|
|
}
|
|
|
|
var aparelho = await EnsureLineHasAparelhoAsync(line, tenantId);
|
|
|
|
try
|
|
{
|
|
if (hasNotaFiscal && form.NotaFiscal != null)
|
|
{
|
|
aparelho.NotaFiscalArquivoPath = await SaveAparelhoAttachmentAsync(
|
|
form.NotaFiscal,
|
|
tenantId,
|
|
line.Id,
|
|
"nota-fiscal",
|
|
aparelho.NotaFiscalArquivoPath);
|
|
}
|
|
|
|
if (hasRecibo && form.Recibo != null)
|
|
{
|
|
aparelho.ReciboArquivoPath = await SaveAparelhoAttachmentAsync(
|
|
form.Recibo,
|
|
tenantId,
|
|
line.Id,
|
|
"recibo",
|
|
aparelho.ReciboArquivoPath);
|
|
}
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
return BadRequest(new { message = ex.Message });
|
|
}
|
|
catch (IOException)
|
|
{
|
|
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Falha ao salvar anexo no servidor." });
|
|
}
|
|
|
|
line.UpdatedAt = DateTime.UtcNow;
|
|
aparelho.UpdatedAt = DateTime.UtcNow;
|
|
|
|
await _db.SaveChangesAsync();
|
|
await _vigenciaNotificationSyncService.SyncCurrentTenantAsync();
|
|
|
|
var vigencia = await FindVigenciaByMobileLineAsync(line, null, asNoTracking: true);
|
|
return Ok(ToDetailDto(line, vigencia));
|
|
}
|
|
|
|
[HttpGet("{id:guid}/aparelho/anexos/{tipo}")]
|
|
[Authorize(Roles = "sysadmin,gestor,cliente")]
|
|
public async Task<IActionResult> DownloadAparelhoAnexo(Guid id, string tipo)
|
|
{
|
|
var line = await _db.MobileLines
|
|
.AsNoTracking()
|
|
.Include(x => x.Aparelho)
|
|
.FirstOrDefaultAsync(x => x.Id == id);
|
|
if (line == null || line.Aparelho == null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
var normalizedTipo = (tipo ?? string.Empty).Trim().ToLowerInvariant();
|
|
var relativePath = normalizedTipo switch
|
|
{
|
|
"nota-fiscal" or "nota_fiscal" or "notafiscal" => line.Aparelho.NotaFiscalArquivoPath,
|
|
"recibo" => line.Aparelho.ReciboArquivoPath,
|
|
_ => null
|
|
};
|
|
|
|
if (string.IsNullOrWhiteSpace(relativePath))
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
var fullPath = TryResolveAttachmentFullPath(relativePath);
|
|
if (string.IsNullOrWhiteSpace(fullPath) || !System.IO.File.Exists(fullPath))
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (!FileContentTypeProvider.TryGetContentType(fullPath, out var contentType))
|
|
{
|
|
contentType = "application/octet-stream";
|
|
}
|
|
|
|
var downloadName = Path.GetFileName(fullPath);
|
|
var stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
return File(stream, contentType, downloadName);
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ 7. DELETE
|
|
// ==========================================================
|
|
[HttpDelete("{id:guid}")]
|
|
[Authorize(Roles = "sysadmin")]
|
|
public async Task<IActionResult> Delete(Guid id)
|
|
{
|
|
var x = await _db.MobileLines.FirstOrDefaultAsync(a => a.Id == id);
|
|
if (x == null) return NotFound();
|
|
_db.MobileLines.Remove(x);
|
|
await _db.SaveChangesAsync();
|
|
await _vigenciaNotificationSyncService.SyncCurrentTenantAsync();
|
|
return NoContent();
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ 8. IMPORT EXCEL
|
|
// ==========================================================
|
|
[HttpPost("import-excel")]
|
|
[Authorize(Roles = "sysadmin")]
|
|
[Consumes("multipart/form-data")]
|
|
[RequestSizeLimit(50_000_000)]
|
|
public async Task<ActionResult<ImportResultDto>> ImportExcel([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("Arquivo inválido.");
|
|
|
|
await using var tx = await _db.Database.BeginTransactionAsync();
|
|
SpreadsheetImportAuditSession? auditSession = null;
|
|
|
|
try
|
|
{
|
|
using var stream = file.OpenReadStream();
|
|
using var wb = new XLWorkbook(stream);
|
|
|
|
// =========================
|
|
// ✅ IMPORTA GERAL
|
|
// =========================
|
|
var ws = wb.Worksheets.FirstOrDefault(w => w.Name.Trim().Equals("GERAL", StringComparison.OrdinalIgnoreCase));
|
|
if (ws == null) return BadRequest("Aba 'GERAL' não encontrada.");
|
|
|
|
var headerRow = ws.RowsUsed().FirstOrDefault(r => r.CellsUsed().Any(c => NormalizeHeader(c.GetString()) == "ITEM"));
|
|
if (headerRow == null) return BadRequest("Cabeçalho 'ITEM' não encontrado na aba GERAL.");
|
|
|
|
var map = BuildHeaderMap(headerRow);
|
|
|
|
int colItem = GetCol(map, "ITEM");
|
|
if (colItem == 0) return BadRequest("Coluna 'ITEM' não encontrada na aba GERAL.");
|
|
|
|
var startRow = headerRow.RowNumber() + 1;
|
|
|
|
// limpa tudo antes (idempotente)
|
|
// ⚠️ limpa dependências primeiro (evita FK Restrict da MUREG)
|
|
await _db.MuregLines.ExecuteDeleteAsync();
|
|
await _db.MobileLines.ExecuteDeleteAsync();
|
|
|
|
var buffer = new List<MobileLine>(600);
|
|
var imported = 0;
|
|
var maxItemFromGeral = 0;
|
|
|
|
var lastRow = ws.LastRowUsed()?.RowNumber() ?? startRow;
|
|
|
|
for (int r = startRow; r <= lastRow; r++)
|
|
{
|
|
var itemStr = GetCellString(ws, r, colItem);
|
|
if (string.IsNullOrWhiteSpace(itemStr)) break;
|
|
var item = TryInt(itemStr);
|
|
|
|
var linhaDigits = OnlyDigits(GetCellByHeader(ws, r, map, "LINHA"));
|
|
var chipDigits = OnlyDigits(GetCellByHeader(ws, r, map, "CHIP"));
|
|
|
|
// ✅ se vier vazio, vira null (evita duplicidade de "")
|
|
var linhaVal = string.IsNullOrWhiteSpace(linhaDigits) ? null : linhaDigits;
|
|
var chipVal = string.IsNullOrWhiteSpace(chipDigits) ? null : chipDigits;
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
var e = new MobileLine
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Item = item,
|
|
Conta = GetCellByHeader(ws, r, map, "CONTA"),
|
|
Linha = linhaVal,
|
|
Chip = chipVal,
|
|
Cliente = GetCellByHeader(ws, r, map, "CLIENTE"),
|
|
Usuario = GetCellByHeader(ws, r, map, "USUARIO"),
|
|
PlanoContrato = GetCellByHeader(ws, r, map, "PLANO CONTRATO"),
|
|
|
|
FranquiaVivo = TryDecimal(GetCellByHeaderAny(ws, r, map, "FRAQUIA", "FRANQUIA", "FRANQUIA VIVO", "FRAQUIA VIVO")),
|
|
ValorPlanoVivo = TryDecimal(GetCellByHeaderAny(ws, r, map, "VALOR DO PLANO R$", "VALOR DO PLANO", "VALORPLANO")),
|
|
GestaoVozDados = TryDecimal(GetCellByHeaderAny(ws, r, map, "GESTAO VOZ E DADOS R$", "GESTAO VOZ E DADOS", "GESTAOVOZEDADOS")),
|
|
Skeelo = TryDecimal(GetCellByHeaderAny(ws, r, map, "SKEELO")),
|
|
VivoNewsPlus = TryDecimal(GetCellByHeaderAny(ws, r, map, "VIVO NEWS PLUS")),
|
|
VivoTravelMundo = TryDecimal(GetCellByHeaderAny(ws, r, map, "VIVO TRAVEL MUNDO")),
|
|
VivoSync = TryDecimal(GetCellByHeaderAny(ws, r, map, "VIVO SYNC")),
|
|
VivoGestaoDispositivo = TryDecimal(GetCellByHeaderAny(ws, r, map, "VIVO GESTAO DISPOSITIVO")),
|
|
ValorContratoVivo = TryDecimal(GetCellByHeaderAny(ws, r, map, "VALOR CONTRATO VIVO", "VALOR DO CONTRATO VIVO")),
|
|
FranquiaLine = TryDecimal(GetCellByHeaderAny(ws, r, map, "FRANQUIA LINE", "FRAQUIA LINE")),
|
|
FranquiaGestao = TryDecimal(GetCellByHeaderAny(ws, r, map, "FRANQUIA GESTAO", "FRAQUIA GESTAO")),
|
|
LocacaoAp = TryDecimal(GetCellByHeaderAny(ws, r, map, "LOCACAO AP.", "LOCACAO AP", "LOCACAOAP")),
|
|
ValorContratoLine = TryDecimal(GetCellByHeaderAny(ws, r, map, "VALOR CONTRATO LINE", "VALOR DO CONTRATO LINE")),
|
|
Desconto = TryDecimal(GetCellByHeaderAny(ws, r, map, "DESCONTO")),
|
|
Lucro = TryDecimal(GetCellByHeaderAny(ws, r, map, "LUCRO")),
|
|
Status = GetCellByHeader(ws, r, map, "STATUS"),
|
|
DataBloqueio = TryDate(ws, r, map, "DATA DO BLOQUEIO"),
|
|
Skil = GetCellByHeader(ws, r, map, "SKIL"),
|
|
Modalidade = GetCellByHeader(ws, r, map, "MODALIDADE"),
|
|
Cedente = GetCellByHeader(ws, r, map, "CEDENTE"),
|
|
Solicitante = GetCellByHeader(ws, r, map, "SOLICITANTE"),
|
|
DataEntregaOpera = TryDate(ws, r, map, "DATA DA ENTREGA OPERA."),
|
|
DataEntregaCliente = TryDate(ws, r, map, "DATA DA ENTREGA CLIENTE"),
|
|
VencConta = GetCellByHeader(ws, r, map, "VENC. DA CONTA"),
|
|
TipoDeChip = GetCellByHeaderAny(ws, r, map, "TIPO DE CHIP", "TIPO CHIP"),
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
};
|
|
|
|
ApplyReservaRule(e);
|
|
|
|
buffer.Add(e);
|
|
imported++;
|
|
if (item > maxItemFromGeral) maxItemFromGeral = item;
|
|
|
|
if (buffer.Count >= 500)
|
|
{
|
|
await _db.MobileLines.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
buffer.Clear();
|
|
}
|
|
}
|
|
|
|
if (buffer.Count > 0)
|
|
{
|
|
await _db.MobileLines.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
auditSession = _spreadsheetImportAuditService.StartRun(
|
|
file.FileName,
|
|
maxItemFromGeral,
|
|
imported);
|
|
|
|
// =========================
|
|
// ✅ IMPORTA MUREG (ALTERADO: NÃO ESTOURA ERRO SE LINHANOVA JÁ EXISTIR)
|
|
// =========================
|
|
await ImportMuregFromWorkbook(wb);
|
|
|
|
// =========================
|
|
// ✅ IMPORTA FATURAMENTO PF/PJ
|
|
// =========================
|
|
await ImportBillingFromWorkbook(wb);
|
|
|
|
// =========================
|
|
// ✅ IMPORTA DADOS DOS USUÁRIOS (UserDatas)
|
|
// =========================
|
|
var userDataImported = await ImportUserDatasFromWorkbook(wb);
|
|
if (userDataImported)
|
|
{
|
|
await RepairReservaClientAssignmentsAsync();
|
|
}
|
|
|
|
// =========================
|
|
// ✅ IMPORTA VIGÊNCIA
|
|
// =========================
|
|
await ImportVigenciaFromWorkbook(wb);
|
|
|
|
// =========================
|
|
// ✅ IMPORTA TROCA DE NÚMERO
|
|
// =========================
|
|
await ImportTrocaNumeroFromWorkbook(wb);
|
|
|
|
// =========================
|
|
// ✅ IMPORTA CHIPS VIRGENS
|
|
// =========================
|
|
await ImportChipsVirgensFromWorkbook(wb);
|
|
|
|
// =========================
|
|
// ✅ IMPORTA CONTROLE DE RECEBIDOS
|
|
// =========================
|
|
await ImportControleRecebidosFromWorkbook(wb);
|
|
|
|
// =========================
|
|
// ✅ IMPORTA RESUMO
|
|
// =========================
|
|
if (auditSession == null)
|
|
{
|
|
throw new InvalidOperationException("Sessão de auditoria não iniciada.");
|
|
}
|
|
|
|
await ImportResumoFromWorkbook(wb, auditSession);
|
|
|
|
// =========================
|
|
// ✅ IMPORTA PARCELAMENTOS
|
|
// =========================
|
|
var parcelamentosSummary = await _parcelamentosImportService.ImportFromWorkbookAsync(wb, replaceAll: true);
|
|
if (auditSession != null)
|
|
{
|
|
await _spreadsheetImportAuditService.SaveRunAsync(auditSession);
|
|
}
|
|
|
|
await AddSpreadsheetImportHistoryAsync(file.FileName, imported, parcelamentosSummary);
|
|
await tx.CommitAsync();
|
|
return Ok(new ImportResultDto { Imported = imported, Parcelamentos = parcelamentosSummary });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await tx.RollbackAsync();
|
|
return StatusCode(500, new { message = "Erro ao importar Excel.", detail = ex.Message });
|
|
}
|
|
}
|
|
|
|
private async Task AddSpreadsheetImportHistoryAsync(
|
|
string? fileName,
|
|
int imported,
|
|
ParcelamentosImportSummaryDto? parcelamentosSummary)
|
|
{
|
|
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 changes = new List<AuditFieldChangeDto>
|
|
{
|
|
new() { Field = "Arquivo", ChangeType = "imported", NewValue = string.IsNullOrWhiteSpace(fileName) ? "-" : fileName },
|
|
new() { Field = "LinhasImportadasGeral", ChangeType = "imported", NewValue = imported.ToString(CultureInfo.InvariantCulture) },
|
|
new() { Field = "ParcelamentosLidos", ChangeType = "imported", NewValue = (parcelamentosSummary?.Lidos ?? 0).ToString(CultureInfo.InvariantCulture) },
|
|
new() { Field = "ParcelamentosInseridos", ChangeType = "imported", NewValue = (parcelamentosSummary?.Inseridos ?? 0).ToString(CultureInfo.InvariantCulture) },
|
|
new() { Field = "ParcelamentosAtualizados", ChangeType = "imported", NewValue = (parcelamentosSummary?.Atualizados ?? 0).ToString(CultureInfo.InvariantCulture) }
|
|
};
|
|
|
|
_db.AuditLogs.Add(new AuditLog
|
|
{
|
|
TenantId = tenantId.Value,
|
|
OccurredAtUtc = DateTime.UtcNow,
|
|
UserId = userId,
|
|
UserName = string.IsNullOrWhiteSpace(userName) ? "USUARIO" : userName,
|
|
UserEmail = userEmail,
|
|
Action = "IMPORT",
|
|
Page = AuditLogBuilder.SpreadsheetImportPageName,
|
|
EntityName = "SpreadsheetImport",
|
|
EntityId = null,
|
|
EntityLabel = "Importação Excel",
|
|
ChangesJson = JsonSerializer.Serialize(changes),
|
|
RequestPath = HttpContext.Request.Path.Value,
|
|
RequestMethod = HttpContext.Request.Method,
|
|
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString()
|
|
});
|
|
|
|
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:
|
|
// - Se LinhaNova já existir em OUTRA linha da GERAL => NÃO atualiza a GERAL, NÃO dá erro
|
|
// - Mesmo assim salva o registro na MUREG normalmente
|
|
// - Evita duplicidade na coluna Linha da GERAL
|
|
// ==========================================================
|
|
private async Task ImportMuregFromWorkbook(XLWorkbook wb)
|
|
{
|
|
var wsM = wb.Worksheets.FirstOrDefault(w => w.Name.Trim().Equals("MUREG", StringComparison.OrdinalIgnoreCase))
|
|
?? wb.Worksheets.FirstOrDefault(w => w.Name.Trim().ToUpperInvariant().Contains("MUREG"));
|
|
|
|
if (wsM == null) return;
|
|
|
|
var headerRow = wsM.RowsUsed().FirstOrDefault(r =>
|
|
{
|
|
var keys = r.CellsUsed()
|
|
.Select(c => NormalizeHeader(c.GetString()))
|
|
.Where(k => !string.IsNullOrWhiteSpace(k))
|
|
.ToArray();
|
|
|
|
if (keys.Length == 0) return false;
|
|
|
|
var hasItem = keys.Any(k => k == "ITEM" || k == "ITEMID" || k.Contains("ITEM"));
|
|
var hasMuregColumns = keys.Any(k =>
|
|
k.Contains("LINHAANTIGA") ||
|
|
k.Contains("LINHANOVA") ||
|
|
k.Contains("ICCID") ||
|
|
k.Contains("DATAMUREG") ||
|
|
k.Contains("DATADAMUREG"));
|
|
|
|
return hasItem && hasMuregColumns;
|
|
});
|
|
if (headerRow == null) return;
|
|
|
|
var lastCol = GetLastUsedColumn(wsM, headerRow.RowNumber());
|
|
|
|
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;
|
|
|
|
// limpa MUREG antes (idempotente)
|
|
await _db.MuregLines.ExecuteDeleteAsync();
|
|
|
|
// ✅ dicionários para resolver MobileLineId por Linha/Chip
|
|
var mobilePairs = await _db.MobileLines
|
|
.AsNoTracking()
|
|
.Select(x => new { x.Id, x.Item, x.Linha, x.Chip })
|
|
.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);
|
|
if (!string.IsNullOrWhiteSpace(k) && !mobileByLinha.ContainsKey(k))
|
|
mobileByLinha[k] = m.Id;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(m.Chip))
|
|
{
|
|
var k = OnlyDigits(m.Chip);
|
|
if (!string.IsNullOrWhiteSpace(k) && !mobileByChip.ContainsKey(k))
|
|
mobileByChip[k] = m.Id;
|
|
}
|
|
}
|
|
|
|
// ✅ cache de entidades tracked para atualizar a GERAL sem consultar toda hora
|
|
var mobileCache = new Dictionary<Guid, MobileLine>();
|
|
|
|
var buffer = new List<MuregLine>(600);
|
|
var lastRow = wsM.LastRowUsed()?.RowNumber() ?? startRow;
|
|
|
|
for (int r = startRow; r <= lastRow; r++)
|
|
{
|
|
var itemStr = colItem > 0 ? GetCellString(wsM, r, colItem) : "";
|
|
var linhaAntigaRaw = colLinhaAntiga > 0 ? GetCellString(wsM, r, colLinhaAntiga) : "";
|
|
var linhaNovaRaw = colLinhaNova > 0 ? GetCellString(wsM, r, colLinhaNova) : "";
|
|
var iccidRaw = colIccid > 0 ? GetCellString(wsM, r, colIccid) : "";
|
|
|
|
if (string.IsNullOrWhiteSpace(itemStr)
|
|
&& string.IsNullOrWhiteSpace(linhaAntigaRaw)
|
|
&& string.IsNullOrWhiteSpace(linhaNovaRaw)
|
|
&& string.IsNullOrWhiteSpace(iccidRaw))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
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)
|
|
continue;
|
|
|
|
// ✅ snapshot da linha antiga: se vier vazia na planilha, pega a linha atual da GERAL
|
|
string? linhaAntigaSnapshot = linhaAntiga;
|
|
if (string.IsNullOrWhiteSpace(linhaAntigaSnapshot))
|
|
{
|
|
if (!mobileCache.TryGetValue(mobileLineId, out var mobTmp))
|
|
{
|
|
mobTmp = await _db.MobileLines.FirstOrDefaultAsync(x => x.Id == mobileLineId);
|
|
if (mobTmp != null) mobileCache[mobileLineId] = mobTmp;
|
|
}
|
|
|
|
linhaAntigaSnapshot = mobTmp?.Linha;
|
|
}
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
// ✅ salva MUREG sempre
|
|
var e = new MuregLine
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Item = item,
|
|
MobileLineId = mobileLineId,
|
|
LinhaAntiga = linhaAntigaSnapshot,
|
|
LinhaNova = linhaNova,
|
|
ICCID = iccid,
|
|
DataDaMureg = dataMureg,
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
};
|
|
|
|
buffer.Add(e);
|
|
|
|
// ✅ REFLETE NA GERAL (somente se NÃO houver conflito)
|
|
if (!string.IsNullOrWhiteSpace(linhaNova))
|
|
{
|
|
// Se LinhaNova já existe na GERAL em OUTRA MobileLine => ignora update (não duplica)
|
|
if (mobileByLinha.TryGetValue(linhaNova, out var idJaExiste) && idJaExiste != mobileLineId)
|
|
{
|
|
// ignora update da GERAL
|
|
}
|
|
else
|
|
{
|
|
// carrega entity tracked (cache) e atualiza
|
|
if (!mobileCache.TryGetValue(mobileLineId, out var mobile))
|
|
{
|
|
mobile = await _db.MobileLines.FirstOrDefaultAsync(x => x.Id == mobileLineId);
|
|
if (mobile != null) mobileCache[mobileLineId] = mobile;
|
|
}
|
|
|
|
if (mobile != null)
|
|
{
|
|
// valida conflito de ICCID também (evita duplicidade de CHIP)
|
|
var iccidConflita = false;
|
|
if (!string.IsNullOrWhiteSpace(iccid) &&
|
|
mobileByChip.TryGetValue(iccid, out var chipJaExiste) &&
|
|
chipJaExiste != mobileLineId)
|
|
{
|
|
iccidConflita = true;
|
|
}
|
|
|
|
// atualiza Linha
|
|
mobile.Linha = linhaNova;
|
|
|
|
// atualiza Chip se ICCID vier e NÃO conflitar
|
|
if (!string.IsNullOrWhiteSpace(iccid) && !iccidConflita)
|
|
mobile.Chip = iccid;
|
|
|
|
mobile.UpdatedAt = DateTime.UtcNow;
|
|
|
|
// atualiza os dicionários para próximas linhas do MUREG
|
|
mobileByLinha[linhaNova] = mobileLineId;
|
|
|
|
if (!string.IsNullOrWhiteSpace(iccid) && !iccidConflita)
|
|
mobileByChip[iccid] = mobileLineId;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (buffer.Count >= 500)
|
|
{
|
|
await _db.MuregLines.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
buffer.Clear();
|
|
}
|
|
}
|
|
|
|
if (buffer.Count > 0)
|
|
{
|
|
await _db.MuregLines.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ IMPORTAÇÃO DO FATURAMENTO (PF/PJ)
|
|
// ==========================================================
|
|
private async Task ImportBillingFromWorkbook(XLWorkbook wb)
|
|
{
|
|
await _db.BillingClients.ExecuteDeleteAsync();
|
|
|
|
// PF
|
|
var wsPf = wb.Worksheets.FirstOrDefault(w => w.Name.Trim().Equals("FATURAMENTO PF", StringComparison.OrdinalIgnoreCase))
|
|
?? wb.Worksheets.FirstOrDefault(w => w.Name.Trim().ToUpperInvariant().Contains("FATURAMENTO") && w.Name.Trim().ToUpperInvariant().Contains("PF"));
|
|
if (wsPf != null)
|
|
await ImportBillingSheet(wsPf, "PF");
|
|
|
|
// PJ
|
|
var wsPj = wb.Worksheets.FirstOrDefault(w => w.Name.Trim().Equals("FATURAMENTO PJ", StringComparison.OrdinalIgnoreCase))
|
|
?? wb.Worksheets.FirstOrDefault(w => w.Name.Trim().ToUpperInvariant().Contains("FATURAMENTO") && w.Name.Trim().ToUpperInvariant().Contains("PJ"));
|
|
if (wsPj != null)
|
|
await ImportBillingSheet(wsPj, "PJ");
|
|
}
|
|
|
|
private async Task ImportBillingSheet(IXLWorksheet ws, string tipo)
|
|
{
|
|
var headerRow =
|
|
ws.RowsUsed().FirstOrDefault(r =>
|
|
r.CellsUsed().Any(c => NormalizeHeader(c.GetString()) == "CLIENTE"));
|
|
|
|
if (headerRow == null) return;
|
|
|
|
var headerRowIndex = headerRow.RowNumber();
|
|
var map = BuildHeaderMap(headerRow);
|
|
|
|
// linha acima (grupos VIVO / LINE)
|
|
var groupRowIndex = Math.Max(1, headerRowIndex - 1);
|
|
var groupRow = ws.Row(groupRowIndex);
|
|
|
|
var lastCol = GetLastUsedColumn(ws, headerRowIndex);
|
|
|
|
var colItem = FindColByAny(headerRow, lastCol, "ITEM");
|
|
var colCliente = FindColByAny(headerRow, lastCol, "CLIENTE");
|
|
if (colCliente == 0) return;
|
|
|
|
var colQtd = FindColByAny(headerRow, lastCol, "QTD DE LINHAS", "QTD LINHAS", "QTDDLINHAS");
|
|
var colLucro = FindColByAny(headerRow, lastCol, "LUCRO");
|
|
var colAparelho = FindColByAny(headerRow, lastCol, "APARELHO");
|
|
var colForma = FindColByAny(headerRow, lastCol, "FORMA DE PAGAMENTO", "FORMA PAGAMENTO", "FORMAPAGAMENTO");
|
|
|
|
var hasAnyGroup = RowHasAnyText(groupRow);
|
|
|
|
int colFranquiaVivo = 0;
|
|
int colValorVivo = 0;
|
|
int colFranquiaLine = 0;
|
|
int colValorLine = 0;
|
|
|
|
if (hasAnyGroup)
|
|
{
|
|
colFranquiaVivo = FindColInGroup(groupRow, headerRow, lastCol, "VIVO",
|
|
"FRANQUIA", "FRAQUIA", "FRANQUIAVIVO", "FRAQUIAVIVO");
|
|
|
|
colValorVivo = FindColInGroup(groupRow, headerRow, lastCol, "VIVO",
|
|
"VALOR CONTRATO VIVO", "VALOR DO CONTRATO VIVO", "VALOR VIVO", "VALOR",
|
|
"R$", "RS", "");
|
|
|
|
colFranquiaLine = FindColInGroup(groupRow, headerRow, lastCol, "LINE",
|
|
"FRANQUIA LINE", "FRAQUIA LINE", "FRANQUIA", "FRAQUIA", "FRANQUIALINE", "FRAQUIALINE");
|
|
|
|
colValorLine = FindColInGroup(groupRow, headerRow, lastCol, "LINE",
|
|
"VALOR CONTRATO LINE", "VALOR DO CONTRATO LINE", "VALOR LINE", "VALOR",
|
|
"R$", "RS", "");
|
|
|
|
if (colValorVivo == 0 && colFranquiaVivo > 0)
|
|
{
|
|
var cand = colFranquiaVivo + 1;
|
|
if (cand <= lastCol)
|
|
{
|
|
var g = GetMergedGroupKeyAt(groupRow, cand);
|
|
if (!string.IsNullOrWhiteSpace(g) && g.Contains(NormalizeHeader("VIVO")))
|
|
colValorVivo = cand;
|
|
}
|
|
}
|
|
|
|
if (colValorLine == 0 && colFranquiaLine > 0)
|
|
{
|
|
var cand = colFranquiaLine + 1;
|
|
if (cand <= lastCol)
|
|
{
|
|
var g = GetMergedGroupKeyAt(groupRow, cand);
|
|
if (!string.IsNullOrWhiteSpace(g) && g.Contains(NormalizeHeader("LINE")))
|
|
colValorLine = cand;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (colFranquiaVivo == 0 || colValorVivo == 0 || colFranquiaLine == 0 || colValorLine == 0)
|
|
{
|
|
if (colFranquiaLine == 0)
|
|
colFranquiaLine = GetColAny(map, "FRAQUIA LINE", "FRANQUIA LINE", "FRANQUIALINE", "FRAQUIALINE");
|
|
|
|
if (colFranquiaVivo == 0)
|
|
{
|
|
colFranquiaVivo = GetColAny(map, "FRAQUIA VIVO", "FRANQUIA VIVO", "FRANQUIAVIVO", "FRAQUIAVIVO");
|
|
if (colFranquiaVivo == 0)
|
|
{
|
|
var colFranquia = GetColAny(map, "FRAQUIA", "FRANQUIA");
|
|
if (colFranquia != 0 && colFranquia != colFranquiaLine) colFranquiaVivo = colFranquia;
|
|
}
|
|
}
|
|
|
|
if (colValorVivo == 0)
|
|
colValorVivo = GetColAny(map,
|
|
"VALOR CONTRATO VIVO",
|
|
"VALOR DO CONTRATO VIVO",
|
|
"VALOR CONTRATO VIVO R$",
|
|
"VALOR VIVO");
|
|
|
|
if (colValorLine == 0)
|
|
colValorLine = GetColAny(map,
|
|
"VALOR CONTRATO LINE",
|
|
"VALOR DO CONTRATO LINE",
|
|
"VALOR CONTRATO LINE R$",
|
|
"VALOR LINE");
|
|
}
|
|
|
|
var colRazao = GetColAny(map, "RAZAO SOCIAL", "RAZÃO SOCIAL", "RAZAOSOCIAL");
|
|
var colNome = GetColAny(map, "NOME", "NOME COMPLETO");
|
|
|
|
var startRow = headerRowIndex + 1;
|
|
var lastRow = ws.LastRowUsed()?.RowNumber() ?? startRow;
|
|
|
|
var buffer = new List<BillingClient>(400);
|
|
var seqItem = 0;
|
|
|
|
for (int r = startRow; r <= lastRow; r++)
|
|
{
|
|
var cliente = colCliente > 0 ? GetCellString(ws, r, colCliente) : "";
|
|
var nome = colNome > 0 ? GetCellString(ws, r, colNome) : "";
|
|
var razao = colRazao > 0 ? GetCellString(ws, r, colRazao) : "";
|
|
|
|
if (string.IsNullOrWhiteSpace(cliente))
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(razao)) cliente = razao;
|
|
else if (!string.IsNullOrWhiteSpace(nome)) cliente = nome;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(cliente)) break;
|
|
|
|
seqItem++;
|
|
|
|
var itemStr = colItem > 0 ? GetCellString(ws, r, colItem) : "";
|
|
var item = !string.IsNullOrWhiteSpace(itemStr) ? TryInt(itemStr) : seqItem;
|
|
|
|
int? qtd = null;
|
|
if (colQtd > 0)
|
|
{
|
|
var qtdStr = GetCellString(ws, r, colQtd);
|
|
qtd = TryNullableInt(qtdStr);
|
|
}
|
|
|
|
var franquiaVivoStr = colFranquiaVivo > 0 ? GetCellString(ws, r, colFranquiaVivo) : "";
|
|
var franquiaLineStr = colFranquiaLine > 0 ? GetCellString(ws, r, colFranquiaLine) : "";
|
|
|
|
var valorContratoVivoStr = colValorVivo > 0 ? GetCellString(ws, r, colValorVivo) : "";
|
|
var valorContratoLineStr = colValorLine > 0 ? GetCellString(ws, r, colValorLine) : "";
|
|
|
|
var lucroStr = colLucro > 0 ? GetCellString(ws, r, colLucro) : "";
|
|
var aparelho = colAparelho > 0 ? GetCellString(ws, r, colAparelho) : "";
|
|
var formaPagto = colForma > 0 ? GetCellString(ws, r, colForma) : "";
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
var e = new BillingClient
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Tipo = tipo,
|
|
Item = item,
|
|
Cliente = cliente.Trim(),
|
|
QtdLinhas = qtd,
|
|
|
|
FranquiaVivo = TryDecimal(franquiaVivoStr),
|
|
ValorContratoVivo = TryDecimal(valorContratoVivoStr),
|
|
|
|
FranquiaLine = TryDecimal(franquiaLineStr),
|
|
ValorContratoLine = TryDecimal(valorContratoLineStr),
|
|
|
|
Lucro = TryDecimal(lucroStr),
|
|
|
|
Aparelho = string.IsNullOrWhiteSpace(aparelho) ? null : aparelho.Trim(),
|
|
FormaPagamento = string.IsNullOrWhiteSpace(formaPagto) ? null : formaPagto.Trim(),
|
|
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
};
|
|
|
|
buffer.Add(e);
|
|
|
|
if (buffer.Count >= 300)
|
|
{
|
|
await _db.BillingClients.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
buffer.Clear();
|
|
}
|
|
}
|
|
|
|
if (buffer.Count > 0)
|
|
{
|
|
await _db.BillingClients.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ IMPORTAÇÃO: DADOS DOS USUÁRIOS (UserDatas)
|
|
// ==========================================================
|
|
private async Task<bool> ImportUserDatasFromWorkbook(XLWorkbook wb)
|
|
{
|
|
var ws = wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("DADOS DOS USUÁRIOS"))
|
|
?? wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("DADOS DOS USUARIOS"))
|
|
?? wb.Worksheets.FirstOrDefault(w =>
|
|
NormalizeHeader(w.Name).Contains("DADOS") &&
|
|
NormalizeHeader(w.Name).Contains("USUAR"));
|
|
|
|
if (ws == null) return false;
|
|
|
|
var headerRow = ws.RowsUsed().FirstOrDefault(r =>
|
|
r.CellsUsed().Any(c =>
|
|
NormalizeHeader(c.GetString()) == "ITEM" ||
|
|
NormalizeHeader(c.GetString()) == "CLIENTE" ||
|
|
NormalizeHeader(c.GetString()) == "RAZAO SOCIAL" ||
|
|
NormalizeHeader(c.GetString()) == "NOME"));
|
|
|
|
if (headerRow == null) return false;
|
|
|
|
var map = BuildHeaderMap(headerRow);
|
|
|
|
var colItem = GetCol(map, "ITEM");
|
|
var colCliente = GetCol(map, "CLIENTE");
|
|
var colRazao = GetColAny(map, "RAZAO SOCIAL", "RAZÃO SOCIAL", "RAZAOSOCIAL");
|
|
var colNome = GetColAny(map, "NOME", "NOME COMPLETO");
|
|
var colLinha = GetCol(map, "LINHA");
|
|
|
|
if (colCliente == 0 && colRazao == 0 && colNome == 0) return false;
|
|
|
|
await _db.UserDatas.ExecuteDeleteAsync();
|
|
|
|
var startRow = headerRow.RowNumber() + 1;
|
|
var lastRow = ws.LastRowUsed()?.RowNumber() ?? startRow;
|
|
|
|
var buffer = new List<UserData>(500);
|
|
var seq = 0;
|
|
|
|
var colCpf = GetColAny(map, "CPF");
|
|
var colCnpj = GetColAny(map, "CNPJ");
|
|
var colRg = GetColAny(map, "RG");
|
|
var colEmail = GetColAny(map, "EMAIL", "E-MAIL");
|
|
var colEndereco = GetColAny(map, "ENDERECO", "ENDEREÇO");
|
|
var colCelular = GetColAny(map, "CELULAR", "CEL");
|
|
var colFixo = GetColAny(map, "TELEFONE FIXO", "TELEFONEFIXO", "FIXO", "TELEFONE");
|
|
|
|
var colDataNasc = GetColAny(map,
|
|
"DATA DE NASCIMENTO",
|
|
"DATADENASCIMENTO",
|
|
"DATA NASCIMENTO",
|
|
"DATANASCIMENTO",
|
|
"NASCIMENTO",
|
|
"DTNASC");
|
|
|
|
for (int r = startRow; r <= lastRow; r++)
|
|
{
|
|
var cliente = colCliente > 0 ? GetCellString(ws, r, colCliente) : "";
|
|
var razao = colRazao > 0 ? GetCellString(ws, r, colRazao) : "";
|
|
var nome = colNome > 0 ? GetCellString(ws, r, colNome) : "";
|
|
|
|
if (string.IsNullOrWhiteSpace(cliente) && string.IsNullOrWhiteSpace(razao) && string.IsNullOrWhiteSpace(nome)) break;
|
|
|
|
if (string.IsNullOrWhiteSpace(cliente))
|
|
{
|
|
cliente = !string.IsNullOrWhiteSpace(razao) ? razao : nome;
|
|
}
|
|
|
|
seq++;
|
|
|
|
int item;
|
|
if (colItem > 0)
|
|
{
|
|
var itemStr = GetCellString(ws, r, colItem);
|
|
item = !string.IsNullOrWhiteSpace(itemStr) ? TryInt(itemStr) : seq;
|
|
}
|
|
else item = seq;
|
|
|
|
var linha = colLinha > 0 ? NullIfEmptyDigits(GetCellString(ws, r, colLinha)) : null;
|
|
|
|
var cpf = colCpf > 0 ? NullIfEmptyDigits(GetCellString(ws, r, colCpf)) : null;
|
|
var cnpj = colCnpj > 0 ? NullIfEmptyDigits(GetCellString(ws, r, colCnpj)) : null;
|
|
var rg = colRg > 0 ? NullIfEmptyDigits(GetCellString(ws, r, colRg)) : null;
|
|
|
|
DateTime? dataNascimento = null;
|
|
if (colDataNasc > 0)
|
|
dataNascimento = TryDateCell(ws, r, colDataNasc);
|
|
|
|
var email = colEmail > 0 ? GetCellString(ws, r, colEmail) : "";
|
|
var endereco = colEndereco > 0 ? GetCellString(ws, r, colEndereco) : "";
|
|
|
|
var celular = colCelular > 0 ? NullIfEmptyDigits(GetCellString(ws, r, colCelular)) : null;
|
|
var fixo = colFixo > 0 ? NullIfEmptyDigits(GetCellString(ws, r, colFixo)) : null;
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
var tipoPessoa = !string.IsNullOrWhiteSpace(cnpj) || !string.IsNullOrWhiteSpace(razao)
|
|
? "PJ"
|
|
: "PF";
|
|
|
|
var nomeFinal = string.IsNullOrWhiteSpace(nome) ? cliente : nome.Trim();
|
|
var razaoFinal = string.IsNullOrWhiteSpace(razao) ? cliente : razao.Trim();
|
|
|
|
var e = new UserData
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Item = item,
|
|
Linha = linha,
|
|
Cliente = cliente.Trim(),
|
|
|
|
TipoPessoa = tipoPessoa,
|
|
Nome = tipoPessoa == "PF" ? nomeFinal : null,
|
|
RazaoSocial = tipoPessoa == "PJ" ? razaoFinal : null,
|
|
Cnpj = tipoPessoa == "PJ" ? cnpj : null,
|
|
Cpf = cpf,
|
|
Rg = rg,
|
|
DataNascimento = ToUtc(dataNascimento),
|
|
|
|
Email = string.IsNullOrWhiteSpace(email) ? null : email.Trim(),
|
|
Endereco = string.IsNullOrWhiteSpace(endereco) ? null : endereco.Trim(),
|
|
|
|
Celular = celular,
|
|
TelefoneFixo = fixo,
|
|
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
};
|
|
|
|
buffer.Add(e);
|
|
|
|
if (buffer.Count >= 400)
|
|
{
|
|
await _db.UserDatas.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
buffer.Clear();
|
|
}
|
|
}
|
|
|
|
if (buffer.Count > 0)
|
|
{
|
|
await _db.UserDatas.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private async Task<int> RepairReservaClientAssignmentsAsync()
|
|
{
|
|
var reservaLines = await _db.MobileLines
|
|
.Where(x => EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"))
|
|
.Where(x => EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA"))
|
|
.Where(x => !EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA"))
|
|
.ToListAsync();
|
|
|
|
if (reservaLines.Count == 0) return 0;
|
|
|
|
var userDataByLine = await BuildUserDataClientByLineQuery()
|
|
.ToDictionaryAsync(x => x.Linha, x => x.Cliente);
|
|
var userDataByItem = await BuildUserDataClientByItemQuery()
|
|
.ToDictionaryAsync(x => x.Item, x => x.Cliente);
|
|
|
|
var updated = 0;
|
|
var now = DateTime.UtcNow;
|
|
|
|
foreach (var line in reservaLines)
|
|
{
|
|
var resolvedClient = "";
|
|
|
|
if (!string.IsNullOrWhiteSpace(line.Linha) &&
|
|
userDataByLine.TryGetValue(line.Linha, out var clientByLine) &&
|
|
!string.IsNullOrWhiteSpace(clientByLine))
|
|
{
|
|
resolvedClient = clientByLine.Trim();
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(resolvedClient) &&
|
|
userDataByItem.TryGetValue(line.Item, out var clientByItem) &&
|
|
!string.IsNullOrWhiteSpace(clientByItem))
|
|
{
|
|
resolvedClient = clientByItem.Trim();
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(resolvedClient) ||
|
|
string.Equals(resolvedClient, "RESERVA", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (string.Equals((line.Cliente ?? "").Trim(), resolvedClient, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
line.Cliente = resolvedClient;
|
|
line.UpdatedAt = now;
|
|
updated++;
|
|
}
|
|
|
|
if (updated > 0)
|
|
{
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
return updated;
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ IMPORTAÇÃO: VIGÊNCIA
|
|
// ==========================================================
|
|
private async Task ImportVigenciaFromWorkbook(XLWorkbook wb)
|
|
{
|
|
var ws = wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("VIGÊNCIA"))
|
|
?? wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("VIGENCIA"))
|
|
?? wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name).Contains("VIGEN"));
|
|
|
|
if (ws == null) return;
|
|
|
|
var headerRow = ws.RowsUsed().FirstOrDefault(r =>
|
|
r.CellsUsed().Any(c =>
|
|
{
|
|
var k = NormalizeHeader(c.GetString());
|
|
return k == "ITEM" || k == "ITEM(ID)" || k == "ITEMID";
|
|
}));
|
|
|
|
if (headerRow == null) return;
|
|
|
|
var map = BuildHeaderMap(headerRow);
|
|
|
|
var colItem = GetColAny(map, "ITEM", "ITEM(ID)", "ITEMID", "ITEM (ID)", "ITÉM (ID)");
|
|
if (colItem == 0) return;
|
|
|
|
var startRow = headerRow.RowNumber() + 1;
|
|
var lastRow = ws.LastRowUsed()?.RowNumber() ?? startRow;
|
|
|
|
var tenantId = GetTenantIdFromClaims();
|
|
var notificationsQuery = _db.Notifications
|
|
.IgnoreQueryFilters()
|
|
.Where(n => n.VigenciaLineId != null);
|
|
if (tenantId.HasValue)
|
|
{
|
|
notificationsQuery = notificationsQuery.Where(n => n.TenantId == tenantId.Value);
|
|
}
|
|
await notificationsQuery.ExecuteUpdateAsync(setters =>
|
|
setters.SetProperty(n => n.VigenciaLineId, n => null));
|
|
|
|
var vigenciaQuery = _db.VigenciaLines.IgnoreQueryFilters();
|
|
if (tenantId.HasValue)
|
|
{
|
|
vigenciaQuery = vigenciaQuery.Where(v => v.TenantId == tenantId.Value);
|
|
}
|
|
await vigenciaQuery.ExecuteDeleteAsync();
|
|
|
|
var buffer = new List<VigenciaLine>(600);
|
|
|
|
for (int r = startRow; r <= lastRow; r++)
|
|
{
|
|
var itemStr = GetCellString(ws, r, colItem);
|
|
if (string.IsNullOrWhiteSpace(itemStr)) break;
|
|
|
|
var conta = GetCellByHeader(ws, r, map, "CONTA");
|
|
var linha = NullIfEmptyDigits(GetCellByHeader(ws, r, map, "LINHA"));
|
|
var cliente = GetCellByHeader(ws, r, map, "CLIENTE");
|
|
var usuario = GetCellByHeader(ws, r, map, "USUÁRIO");
|
|
if (string.IsNullOrWhiteSpace(usuario))
|
|
usuario = GetCellByHeader(ws, r, map, "USUARIO");
|
|
|
|
var plano = GetCellByHeader(ws, r, map, "PLANO CONTRATO");
|
|
|
|
var dtEfet = TryDateNoUtc(ws, r, map, "DT. DE EFETIVAÇÃO DO SERVIÇO");
|
|
if (dtEfet == null) dtEfet = TryDateNoUtc(ws, r, map, "DT. DE EFETIVACAO DO SERVICO");
|
|
|
|
var dtFim = TryDateNoUtc(ws, r, map, "DT. DE TÉRMINO DA FIDELIZAÇÃO");
|
|
if (dtFim == null) dtFim = TryDateNoUtc(ws, r, map, "DT. DE TERMINO DA FIDELIZACAO");
|
|
|
|
var totalStr = GetCellByHeader(ws, r, map, "TOTAL");
|
|
var now = DateTime.UtcNow;
|
|
|
|
var e = new VigenciaLine
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Item = TryInt(itemStr),
|
|
Conta = string.IsNullOrWhiteSpace(conta) ? null : conta.Trim(),
|
|
Linha = linha,
|
|
Cliente = string.IsNullOrWhiteSpace(cliente) ? null : cliente.Trim(),
|
|
Usuario = string.IsNullOrWhiteSpace(usuario) ? null : usuario.Trim(),
|
|
PlanoContrato = string.IsNullOrWhiteSpace(plano) ? null : plano.Trim(),
|
|
DtEfetivacaoServico = dtEfet,
|
|
DtTerminoFidelizacao = dtFim,
|
|
Total = TryDecimal(totalStr),
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
};
|
|
|
|
buffer.Add(e);
|
|
|
|
if (buffer.Count >= 500)
|
|
{
|
|
await _db.VigenciaLines.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
buffer.Clear();
|
|
}
|
|
}
|
|
|
|
if (buffer.Count > 0)
|
|
{
|
|
await _db.VigenciaLines.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ IMPORTAÇÃO: TROCA DE NÚMERO
|
|
// ==========================================================
|
|
private async Task ImportTrocaNumeroFromWorkbook(XLWorkbook wb)
|
|
{
|
|
var ws = wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("TROCA DE NÚMERO"))
|
|
?? wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("TROCA DE NUMERO"))
|
|
?? wb.Worksheets.FirstOrDefault(w =>
|
|
NormalizeHeader(w.Name).Contains("TROCA") &&
|
|
NormalizeHeader(w.Name).Contains("NUMER"));
|
|
|
|
if (ws == null) return;
|
|
|
|
var headerRow = ws.RowsUsed().FirstOrDefault(r =>
|
|
r.CellsUsed().Any(c => NormalizeHeader(c.GetString()) == "ITEM"));
|
|
|
|
if (headerRow == null) return;
|
|
|
|
var map = BuildHeaderMap(headerRow);
|
|
|
|
var colItem = GetCol(map, "ITEM");
|
|
if (colItem == 0) return;
|
|
|
|
var startRow = headerRow.RowNumber() + 1;
|
|
var lastRow = ws.LastRowUsed()?.RowNumber() ?? startRow;
|
|
|
|
await _db.TrocaNumeroLines.ExecuteDeleteAsync();
|
|
|
|
var buffer = new List<TrocaNumeroLine>(600);
|
|
|
|
for (int r = startRow; r <= lastRow; r++)
|
|
{
|
|
var itemStr = GetCellString(ws, r, colItem);
|
|
if (string.IsNullOrWhiteSpace(itemStr)) break;
|
|
|
|
var linhaAntiga = NullIfEmptyDigits(GetCellByHeader(ws, r, map, "LINHA ANTIGA"));
|
|
var linhaNova = NullIfEmptyDigits(GetCellByHeader(ws, r, map, "LINHA NOVA"));
|
|
var iccid = NullIfEmptyDigits(GetCellByHeader(ws, r, map, "ICCID"));
|
|
|
|
var dataTroca = TryDate(ws, r, map, "DATA TROCA");
|
|
if (dataTroca == null) dataTroca = TryDate(ws, r, map, "DATA DA TROCA");
|
|
|
|
var motivo = GetCellByHeader(ws, r, map, "MOTIVO");
|
|
var obs = GetCellByHeader(ws, r, map, "OBSERVAÇÃO");
|
|
if (string.IsNullOrWhiteSpace(obs)) obs = GetCellByHeader(ws, r, map, "OBSERVACAO");
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
var e = new TrocaNumeroLine
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Item = TryInt(itemStr),
|
|
LinhaAntiga = linhaAntiga,
|
|
LinhaNova = linhaNova,
|
|
ICCID = iccid,
|
|
DataTroca = dataTroca,
|
|
Motivo = string.IsNullOrWhiteSpace(motivo) ? null : motivo.Trim(),
|
|
Observacao = string.IsNullOrWhiteSpace(obs) ? null : obs.Trim(),
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
};
|
|
|
|
buffer.Add(e);
|
|
|
|
if (buffer.Count >= 500)
|
|
{
|
|
await _db.TrocaNumeroLines.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
buffer.Clear();
|
|
}
|
|
}
|
|
|
|
if (buffer.Count > 0)
|
|
{
|
|
await _db.TrocaNumeroLines.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ IMPORTAÇÃO CHIPS VIRGENS
|
|
// ==========================================================
|
|
private async Task ImportChipsVirgensFromWorkbook(XLWorkbook wb)
|
|
{
|
|
var ws = wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("CHIP VIRGENS E CONTROLE DE RECEBIDOS"))
|
|
?? wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("CHIPS VIRGENS"))
|
|
?? wb.Worksheets.FirstOrDefault(w =>
|
|
{
|
|
var name = NormalizeHeader(w.Name);
|
|
return name.Contains("CHIP") && name.Contains("VIRGEN");
|
|
});
|
|
|
|
if (ws == null) return;
|
|
|
|
var headers = new List<IXLRow>();
|
|
foreach (var rowIndex in new[] { 7, 8 })
|
|
{
|
|
var row = ws.Row(rowIndex);
|
|
if (IsChipsVirgensHeader(row))
|
|
{
|
|
headers.Add(row);
|
|
}
|
|
}
|
|
|
|
if (headers.Count == 0)
|
|
{
|
|
headers = ws.RowsUsed()
|
|
.Where(IsChipsVirgensHeader)
|
|
.OrderBy(r => r.RowNumber())
|
|
.ToList();
|
|
}
|
|
if (headers.Count == 0) return;
|
|
|
|
await _db.ChipVirgemLines.ExecuteDeleteAsync();
|
|
|
|
var buffer = new List<ChipVirgemLine>(500);
|
|
var lastRow = ws.LastRowUsed()?.RowNumber() ?? 1;
|
|
|
|
for (int i = 0; i < headers.Count; i++)
|
|
{
|
|
var headerRow = headers[i];
|
|
var lastCol = headerRow.LastCellUsed()?.Address.ColumnNumber
|
|
?? ws.LastColumnUsed()?.ColumnNumber()
|
|
?? headerRow.LastCellUsed()?.Address.ColumnNumber
|
|
?? 1;
|
|
var itemColumns = headerRow.CellsUsed()
|
|
.Where(c => NormalizeHeader(c.GetString()) == "ITEM")
|
|
.Select(c => c.Address.ColumnNumber)
|
|
.OrderBy(c => c)
|
|
.ToList();
|
|
if (itemColumns.Count == 0) continue;
|
|
|
|
var startRow = headerRow.RowNumber() + 1;
|
|
var endRow = lastRow;
|
|
|
|
for (int tableIndex = 0; tableIndex < itemColumns.Count; tableIndex++)
|
|
{
|
|
var startCol = itemColumns[tableIndex];
|
|
var endCol = tableIndex + 1 < itemColumns.Count
|
|
? itemColumns[tableIndex + 1] - 1
|
|
: lastCol;
|
|
|
|
int colItem = startCol;
|
|
int colChip = FindHeaderColumn(headerRow, startCol, endCol, "CHIP");
|
|
int colObs = FindHeaderColumn(headerRow, startCol, endCol, "OBS");
|
|
if (colItem == 0 || colChip == 0 || colObs == 0) continue;
|
|
|
|
for (int r = startRow; r <= endRow; r++)
|
|
{
|
|
var itemStr = GetCellString(ws, r, colItem);
|
|
if (string.IsNullOrWhiteSpace(itemStr)) break;
|
|
|
|
var chipRaw = GetCellString(ws, r, colChip);
|
|
var numeroChip = NullIfEmptyDigits(chipRaw);
|
|
if (string.IsNullOrWhiteSpace(numeroChip) && !string.IsNullOrWhiteSpace(chipRaw))
|
|
{
|
|
numeroChip = chipRaw.Trim();
|
|
}
|
|
var observacoes = GetCellString(ws, r, colObs);
|
|
if (string.IsNullOrWhiteSpace(numeroChip) && string.IsNullOrWhiteSpace(observacoes))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
var e = new ChipVirgemLine
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Item = TryInt(itemStr),
|
|
NumeroDoChip = numeroChip,
|
|
Observacoes = string.IsNullOrWhiteSpace(observacoes) ? null : observacoes.Trim(),
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
};
|
|
|
|
buffer.Add(e);
|
|
|
|
if (buffer.Count >= 500)
|
|
{
|
|
await _db.ChipVirgemLines.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
buffer.Clear();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (buffer.Count > 0)
|
|
{
|
|
await _db.ChipVirgemLines.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ IMPORTAÇÃO CONTROLE DE RECEBIDOS (2022-2025)
|
|
// ==========================================================
|
|
private async Task ImportControleRecebidosFromWorkbook(XLWorkbook wb)
|
|
{
|
|
await _db.ControleRecebidoLines.ExecuteDeleteAsync();
|
|
|
|
var years = new[] { 2022, 2023, 2024, 2025 };
|
|
var importedYears = new HashSet<int>();
|
|
|
|
foreach (var info in GetControleRecebidosWorksheets(wb))
|
|
{
|
|
await ImportControleRecebidosSheet(info.Sheet, info.Year);
|
|
importedYears.Add(info.Year);
|
|
}
|
|
|
|
foreach (var year in years)
|
|
{
|
|
if (importedYears.Contains(year)) continue;
|
|
|
|
var ws = FindControleRecebidosWorksheet(wb, year);
|
|
if (ws == null) continue;
|
|
|
|
await ImportControleRecebidosSheet(ws, year);
|
|
}
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ IMPORTAÇÃO DA ABA RESUMO
|
|
// ==========================================================
|
|
private async Task ImportResumoFromWorkbook(XLWorkbook wb, SpreadsheetImportAuditSession auditSession)
|
|
{
|
|
var ws = wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("RESUMO"));
|
|
if (ws == null) return;
|
|
|
|
await _db.ResumoMacrophonyPlans.ExecuteDeleteAsync();
|
|
await _db.ResumoMacrophonyTotals.ExecuteDeleteAsync();
|
|
await _db.ResumoVivoLineResumos.ExecuteDeleteAsync();
|
|
await _db.ResumoVivoLineTotals.ExecuteDeleteAsync();
|
|
await _db.ResumoClienteEspeciais.ExecuteDeleteAsync();
|
|
await _db.ResumoPlanoContratoResumos.ExecuteDeleteAsync();
|
|
await _db.ResumoPlanoContratoTotals.ExecuteDeleteAsync();
|
|
await _db.ResumoLineTotais.ExecuteDeleteAsync();
|
|
await _db.ResumoGbDistribuicoes.ExecuteDeleteAsync();
|
|
await _db.ResumoGbDistribuicaoTotais.ExecuteDeleteAsync();
|
|
await _db.ResumoReservaLines.ExecuteDeleteAsync();
|
|
await _db.ResumoReservaTotals.ExecuteDeleteAsync();
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
await ImportResumoTabela1(ws, now, auditSession);
|
|
await ImportResumoTabela2(ws, now, auditSession);
|
|
await ImportResumoTabela3(ws, now);
|
|
await ImportResumoTabela4(ws, now);
|
|
await ImportResumoTabela7(ws, now);
|
|
await ImportResumoTabela5(ws, now);
|
|
await ImportResumoTabela6(ws, now);
|
|
}
|
|
|
|
private async Task ImportResumoTabela1(IXLWorksheet ws, DateTime now, SpreadsheetImportAuditSession auditSession)
|
|
{
|
|
var lastRowUsed = ws.LastRowUsed()?.RowNumber() ?? 1;
|
|
var headerRow = FindHeaderRowForMacrophonyPlans(ws, 1, lastRowUsed);
|
|
if (headerRow == 0) return;
|
|
|
|
var map = BuildHeaderMap(ws.Row(headerRow));
|
|
var colPlano = GetCol(map, "PLANO CONTRATO");
|
|
var colGb = GetCol(map, "GB");
|
|
var colValorIndividual = GetColAny(map, "VALOR INDIVIDUAL C/ SVAs", "VALOR INDIVIDUAL C/ SVAS", "VALOR INDIVIDUAL");
|
|
var colFranquiaGb = GetColAny(map, "FRANQUIA GB", "FRAQUIA GB");
|
|
var colTotalLinhas = GetColAny(map, "TOTAL DE LINHAS", "TOTAL LINHAS");
|
|
var colValorTotal = GetCol(map, "VALOR TOTAL");
|
|
|
|
var buffer = new List<ResumoMacrophonyPlan>(200);
|
|
string? lastPlanoContrato = null;
|
|
decimal? lastGb = null;
|
|
var dataStarted = false;
|
|
var emptyDataStreak = 0;
|
|
int? totalRowIndex = null;
|
|
var missingPlanoCount = 0;
|
|
var missingGbCount = 0;
|
|
|
|
for (int r = headerRow + 1; r <= lastRowUsed; r++)
|
|
{
|
|
var plano = GetCellString(ws, r, colPlano);
|
|
var gb = GetCellString(ws, r, colGb);
|
|
var valorInd = GetCellString(ws, r, colValorIndividual);
|
|
var franquia = GetCellString(ws, r, colFranquiaGb);
|
|
var totalLinhas = GetCellString(ws, r, colTotalLinhas);
|
|
var valorTotal = GetCellString(ws, r, colValorTotal);
|
|
|
|
var isPlanoTotal = !string.IsNullOrWhiteSpace(plano)
|
|
&& NormalizeHeader(plano) == NormalizeHeader("TOTAL");
|
|
|
|
if (isPlanoTotal)
|
|
{
|
|
totalRowIndex = r;
|
|
break;
|
|
}
|
|
|
|
var hasAnyValue = !(string.IsNullOrWhiteSpace(plano)
|
|
&& string.IsNullOrWhiteSpace(gb)
|
|
&& string.IsNullOrWhiteSpace(valorInd)
|
|
&& string.IsNullOrWhiteSpace(franquia)
|
|
&& string.IsNullOrWhiteSpace(totalLinhas)
|
|
&& string.IsNullOrWhiteSpace(valorTotal));
|
|
|
|
if (!hasAnyValue)
|
|
{
|
|
if (dataStarted)
|
|
{
|
|
emptyDataStreak++;
|
|
if (emptyDataStreak >= 2) break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
emptyDataStreak = 0;
|
|
|
|
var franquiaValue = TryDecimal(franquia);
|
|
var totalLinhasValue = TryNullableInt(totalLinhas);
|
|
var isDataRow = franquiaValue.HasValue || totalLinhasValue.HasValue;
|
|
if (isDataRow) dataStarted = true;
|
|
if (!isDataRow && dataStarted)
|
|
{
|
|
break;
|
|
}
|
|
if (!isDataRow)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var planoNormalized = NormalizeHeader(plano);
|
|
if (!string.IsNullOrWhiteSpace(plano)
|
|
&& planoNormalized != NormalizeHeader("PLANO CONTRATO")
|
|
&& planoNormalized != NormalizeHeader("TOTAL"))
|
|
{
|
|
lastPlanoContrato = plano.Trim();
|
|
}
|
|
|
|
var gbValue = TryDecimal(gb);
|
|
if (gbValue.HasValue)
|
|
{
|
|
lastGb = gbValue;
|
|
}
|
|
|
|
var resolvedPlano = isDataRow
|
|
? (string.IsNullOrWhiteSpace(plano) ? lastPlanoContrato : plano.Trim())
|
|
: (string.IsNullOrWhiteSpace(plano) ? null : plano.Trim());
|
|
|
|
var resolvedGb = isDataRow
|
|
? (gbValue ?? lastGb)
|
|
: gbValue;
|
|
|
|
if (isDataRow && string.IsNullOrWhiteSpace(resolvedPlano))
|
|
{
|
|
missingPlanoCount++;
|
|
}
|
|
|
|
if (isDataRow && !resolvedGb.HasValue)
|
|
{
|
|
missingGbCount++;
|
|
}
|
|
|
|
var vivoTravelCell = ws.Cell(r, 8).GetString();
|
|
var vivoTravel = !string.IsNullOrWhiteSpace(vivoTravelCell)
|
|
&& vivoTravelCell.Contains("VIVO TRAVEL", StringComparison.OrdinalIgnoreCase);
|
|
|
|
buffer.Add(new ResumoMacrophonyPlan
|
|
{
|
|
PlanoContrato = string.IsNullOrWhiteSpace(resolvedPlano) ? null : resolvedPlano,
|
|
Gb = resolvedGb,
|
|
ValorIndividualComSvas = TryDecimal(valorInd),
|
|
FranquiaGb = franquiaValue,
|
|
TotalLinhas = totalLinhasValue,
|
|
ValorTotal = TryDecimal(valorTotal),
|
|
VivoTravel = vivoTravel,
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
});
|
|
}
|
|
|
|
if (buffer.Count > 0)
|
|
{
|
|
await _db.ResumoMacrophonyPlans.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
if (missingPlanoCount > 0 || missingGbCount > 0)
|
|
{
|
|
throw new InvalidOperationException($"Import RESUMO/MACROPHONY: {missingPlanoCount} linhas sem PLANO CONTRATO e {missingGbCount} linhas sem GB.");
|
|
}
|
|
|
|
if (totalRowIndex == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var totalLinhasSource = TryNullableInt(GetCellString(ws, totalRowIndex.Value, colTotalLinhas));
|
|
var total = new ResumoMacrophonyTotal
|
|
{
|
|
FranquiaGbTotal = TryDecimal(GetCellString(ws, totalRowIndex.Value, colFranquiaGb)),
|
|
TotalLinhasTotal = _spreadsheetImportAuditService.CanonicalizeTotalLinhas(
|
|
auditSession,
|
|
"RESUMO.MACROPHONY_TOTAL",
|
|
"TotalLinhasTotal",
|
|
totalLinhasSource,
|
|
"HIGH"),
|
|
ValorTotal = TryDecimal(GetCellString(ws, totalRowIndex.Value, colValorTotal)),
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
};
|
|
|
|
await _db.ResumoMacrophonyTotals.AddAsync(total);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
private static int FindHeaderRowForMacrophonyPlans(IXLWorksheet ws, int startRow, int lastRow)
|
|
{
|
|
for (int r = startRow; r <= lastRow; r++)
|
|
{
|
|
var row = ws.Row(r);
|
|
if (!row.CellsUsed().Any()) continue;
|
|
|
|
var map = BuildHeaderMap(row);
|
|
var hasPlano = GetCol(map, "PLANO CONTRATO") > 0;
|
|
var hasGb = GetCol(map, "GB") > 0;
|
|
var hasTotalLinhas = GetColAny(map, "TOTAL DE LINHAS", "TOTAL LINHAS") > 0;
|
|
|
|
if (hasPlano && hasGb && hasTotalLinhas)
|
|
{
|
|
return r;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private async Task ImportResumoTabela2(IXLWorksheet ws, DateTime now, SpreadsheetImportAuditSession auditSession)
|
|
{
|
|
const int headerRow = 5;
|
|
const int totalRow = 219;
|
|
var lastRow = Math.Min(totalRow - 1, ws.LastRowUsed()?.RowNumber() ?? totalRow - 1);
|
|
|
|
var map = BuildHeaderMap(ws.Row(headerRow));
|
|
var colSkil = GetCol(map, "SKIL");
|
|
var colCliente = GetCol(map, "CLIENTE");
|
|
var colQtdLinhas = GetColAny(map, "QTD DE LINHAS", "QTD. DE LINHAS", "QTD LINHAS");
|
|
var colFranquiaTotal = GetColAny(map, "FRANQUIA TOTAL", "FRAQUIA TOTAL");
|
|
var colValorContratoVivo = GetColAny(map, "VALOR CONTRATO VIVO", "VALOR DO CONTRATO VIVO");
|
|
var colFranquiaLine = GetColAny(map, "FRANQUIA LINE", "FRAQUIA LINE");
|
|
var colValorContratoLine = GetColAny(map, "VALOR CONTRATO LINE", "VALOR DO CONTRATO LINE");
|
|
var colLucro = GetCol(map, "LUCRO");
|
|
|
|
var buffer = new List<ResumoVivoLineResumo>(400);
|
|
|
|
for (int r = headerRow + 1; r <= lastRow; r++)
|
|
{
|
|
var skil = GetCellString(ws, r, colSkil);
|
|
var cliente = GetCellString(ws, r, colCliente);
|
|
var qtdLinhas = GetCellString(ws, r, colQtdLinhas);
|
|
var franquiaTotal = GetCellString(ws, r, colFranquiaTotal);
|
|
var valorContratoVivo = GetCellString(ws, r, colValorContratoVivo);
|
|
var franquiaLine = GetCellString(ws, r, colFranquiaLine);
|
|
var valorContratoLine = GetCellString(ws, r, colValorContratoLine);
|
|
var lucro = GetCellString(ws, r, colLucro);
|
|
|
|
if (string.IsNullOrWhiteSpace(skil)
|
|
&& string.IsNullOrWhiteSpace(cliente)
|
|
&& string.IsNullOrWhiteSpace(qtdLinhas)
|
|
&& string.IsNullOrWhiteSpace(franquiaTotal)
|
|
&& string.IsNullOrWhiteSpace(valorContratoVivo)
|
|
&& string.IsNullOrWhiteSpace(franquiaLine)
|
|
&& string.IsNullOrWhiteSpace(valorContratoLine)
|
|
&& string.IsNullOrWhiteSpace(lucro))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
buffer.Add(new ResumoVivoLineResumo
|
|
{
|
|
Skil = string.IsNullOrWhiteSpace(skil) ? null : skil.Trim(),
|
|
Cliente = string.IsNullOrWhiteSpace(cliente) ? null : cliente.Trim(),
|
|
QtdLinhas = TryNullableInt(qtdLinhas),
|
|
FranquiaTotal = TryDecimal(franquiaTotal),
|
|
ValorContratoVivo = TryDecimal(valorContratoVivo),
|
|
FranquiaLine = TryDecimal(franquiaLine),
|
|
ValorContratoLine = TryDecimal(valorContratoLine),
|
|
Lucro = TryDecimal(lucro),
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
});
|
|
}
|
|
|
|
if (buffer.Count > 0)
|
|
{
|
|
await _db.ResumoVivoLineResumos.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
var qtdLinhasTotalSource = TryNullableInt(GetCellString(ws, totalRow, colQtdLinhas));
|
|
var total = new ResumoVivoLineTotal
|
|
{
|
|
QtdLinhasTotal = _spreadsheetImportAuditService.CanonicalizeTotalLinhas(
|
|
auditSession,
|
|
"RESUMO.VIVO_LINE_TOTAL",
|
|
"QtdLinhasTotal",
|
|
qtdLinhasTotalSource,
|
|
"HIGH"),
|
|
FranquiaTotal = TryDecimal(GetCellString(ws, totalRow, colFranquiaTotal)),
|
|
ValorContratoVivo = TryDecimal(GetCellString(ws, totalRow, colValorContratoVivo)),
|
|
FranquiaLine = TryDecimal(GetCellString(ws, totalRow, colFranquiaLine)),
|
|
ValorContratoLine = TryDecimal(GetCellString(ws, totalRow, colValorContratoLine)),
|
|
Lucro = TryDecimal(GetCellString(ws, totalRow, colLucro)),
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
};
|
|
|
|
await _db.ResumoVivoLineTotals.AddAsync(total);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
private async Task ImportResumoTabela3(IXLWorksheet ws, DateTime now)
|
|
{
|
|
const int headerStartRow = 223;
|
|
const int headerEndRow = 225;
|
|
const int valuesRow = 227;
|
|
|
|
var headerColumns = new Dictionary<int, string>();
|
|
for (int row = headerStartRow; row <= headerEndRow; row++)
|
|
{
|
|
var rowData = ws.Row(row);
|
|
var lastCol = rowData.LastCellUsed()?.Address.ColumnNumber ?? 1;
|
|
for (int col = 1; col <= lastCol; col++)
|
|
{
|
|
var name = rowData.Cell(col).GetString();
|
|
if (string.IsNullOrWhiteSpace(name)) continue;
|
|
if (!headerColumns.ContainsKey(col))
|
|
{
|
|
headerColumns[col] = name.Trim();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (headerColumns.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var buffer = new List<ResumoClienteEspecial>(headerColumns.Count);
|
|
foreach (var entry in headerColumns)
|
|
{
|
|
var valueStr = ws.Cell(valuesRow, entry.Key).GetString();
|
|
buffer.Add(new ResumoClienteEspecial
|
|
{
|
|
Nome = entry.Value,
|
|
Valor = TryDecimal(valueStr),
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
});
|
|
}
|
|
|
|
await _db.ResumoClienteEspeciais.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
private async Task ImportResumoTabela4(IXLWorksheet ws, DateTime now)
|
|
{
|
|
var lastRowUsed = ws.LastRowUsed()?.RowNumber() ?? 1;
|
|
var headerRow = FindHeaderRowForPlanoContratoResumo(ws, 1, lastRowUsed);
|
|
if (headerRow == 0) return;
|
|
|
|
var map = BuildHeaderMap(ws.Row(headerRow));
|
|
var colPlano = GetCol(map, "PLANO CONTRATO");
|
|
var colGb = GetCol(map, "GB");
|
|
var colValorIndividual = GetColAny(map, "VALOR INDIVIDUAL C/ SVAs", "VALOR INDIVIDUAL C/ SVAS", "VALOR INDIVIDUAL");
|
|
var colFranquiaGb = GetColAny(map, "FRANQUIA GB", "FRAQUIA GB");
|
|
var colTotalLinhas = GetColAny(map, "TOTAL DE LINHAS", "TOTAL LINHAS");
|
|
var colValorTotal = GetCol(map, "VALOR TOTAL");
|
|
var colCliente = GetCol(map, "CLIENTE");
|
|
var colQtdLinhas = GetColAny(map, "QTD DE LINHAS", "QTD. DE LINHAS", "QTD LINHAS");
|
|
|
|
var buffer = new List<ResumoPlanoContratoResumo>(200);
|
|
string? lastPlanoContrato = null;
|
|
var dataStarted = false;
|
|
var emptyDataStreak = 0;
|
|
int? totalRowIndex = null;
|
|
var missingPlanoCount = 0;
|
|
|
|
for (int r = headerRow + 1; r <= lastRowUsed; r++)
|
|
{
|
|
var plano = GetCellString(ws, r, colPlano);
|
|
var gb = GetCellString(ws, r, colGb);
|
|
var valorInd = GetCellString(ws, r, colValorIndividual);
|
|
var franquia = GetCellString(ws, r, colFranquiaGb);
|
|
var totalLinhas = GetCellString(ws, r, colTotalLinhas);
|
|
var valorTotal = GetCellString(ws, r, colValorTotal);
|
|
var cliente = GetCellString(ws, r, colCliente);
|
|
var qtdLinhas = GetCellString(ws, r, colQtdLinhas);
|
|
|
|
var isPlanoTotal = !string.IsNullOrWhiteSpace(plano)
|
|
&& NormalizeHeader(plano) == NormalizeHeader("TOTAL");
|
|
|
|
if (isPlanoTotal)
|
|
{
|
|
totalRowIndex = r;
|
|
break;
|
|
}
|
|
|
|
var hasAnyValue = !(string.IsNullOrWhiteSpace(plano)
|
|
&& string.IsNullOrWhiteSpace(gb)
|
|
&& string.IsNullOrWhiteSpace(valorInd)
|
|
&& string.IsNullOrWhiteSpace(franquia)
|
|
&& string.IsNullOrWhiteSpace(totalLinhas)
|
|
&& string.IsNullOrWhiteSpace(valorTotal)
|
|
&& string.IsNullOrWhiteSpace(cliente)
|
|
&& string.IsNullOrWhiteSpace(qtdLinhas));
|
|
|
|
if (!hasAnyValue)
|
|
{
|
|
if (dataStarted)
|
|
{
|
|
emptyDataStreak++;
|
|
if (emptyDataStreak >= 2) break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
emptyDataStreak = 0;
|
|
|
|
var isDataRow = !string.IsNullOrWhiteSpace(cliente) || TryNullableInt(qtdLinhas).HasValue;
|
|
|
|
var planoNormalized = NormalizeHeader(plano);
|
|
if (!string.IsNullOrWhiteSpace(plano)
|
|
&& planoNormalized != NormalizeHeader("PLANO CONTRATO")
|
|
&& planoNormalized != NormalizeHeader("TOTAL"))
|
|
{
|
|
lastPlanoContrato = plano.Trim();
|
|
}
|
|
|
|
if (!isDataRow && dataStarted)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (isDataRow) dataStarted = true;
|
|
|
|
var resolvedPlano = isDataRow
|
|
? (string.IsNullOrWhiteSpace(plano) ? lastPlanoContrato : plano.Trim())
|
|
: (string.IsNullOrWhiteSpace(plano) ? null : plano.Trim());
|
|
|
|
if (isDataRow && string.IsNullOrWhiteSpace(resolvedPlano))
|
|
{
|
|
missingPlanoCount++;
|
|
}
|
|
|
|
buffer.Add(new ResumoPlanoContratoResumo
|
|
{
|
|
PlanoContrato = string.IsNullOrWhiteSpace(resolvedPlano) ? null : resolvedPlano,
|
|
Gb = TryDecimal(gb),
|
|
ValorIndividualComSvas = TryDecimal(valorInd),
|
|
FranquiaGb = TryDecimal(franquia),
|
|
TotalLinhas = TryNullableInt(totalLinhas),
|
|
ValorTotal = TryDecimal(valorTotal),
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
});
|
|
}
|
|
|
|
if (buffer.Count > 0)
|
|
{
|
|
await _db.ResumoPlanoContratoResumos.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
if (missingPlanoCount > 0)
|
|
{
|
|
throw new InvalidOperationException($"Import RESUMO/PLANO CONTRATO: {missingPlanoCount} linhas de dados ficaram sem PLANO CONTRATO.");
|
|
}
|
|
|
|
if (totalRowIndex == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var total = new ResumoPlanoContratoTotal
|
|
{
|
|
ValorTotal = TryDecimal(GetCellString(ws, totalRowIndex.Value, colValorTotal)),
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
};
|
|
|
|
await _db.ResumoPlanoContratoTotals.AddAsync(total);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
private static int FindHeaderRowForPlanoContratoResumo(IXLWorksheet ws, int startRow, int lastRow)
|
|
{
|
|
for (int r = startRow; r <= lastRow; r++)
|
|
{
|
|
var row = ws.Row(r);
|
|
if (!row.CellsUsed().Any()) continue;
|
|
|
|
var map = BuildHeaderMap(row);
|
|
var hasPlano = GetCol(map, "PLANO CONTRATO") > 0;
|
|
var hasCliente = GetCol(map, "CLIENTE") > 0;
|
|
var hasQtd = GetColAny(map, "QTD DE LINHAS", "QTD. DE LINHAS", "QTD LINHAS") > 0;
|
|
|
|
if (hasPlano && hasCliente && hasQtd)
|
|
{
|
|
return r;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private async Task ImportResumoTabela7(IXLWorksheet ws, DateTime now)
|
|
{
|
|
var lastRowUsed = ws.LastRowUsed()?.RowNumber() ?? 1;
|
|
var headerRow = FindHeaderRowForGbDistribuicao(ws, 1, lastRowUsed);
|
|
if (headerRow == 0)
|
|
{
|
|
Console.WriteLine("[WARN] RESUMO/GB_QTD_SOMA: cabeçalho GB/QTD/SOMA não encontrado.");
|
|
return;
|
|
}
|
|
|
|
var map = BuildHeaderMap(ws.Row(headerRow));
|
|
var colGb = GetCol(map, "GB");
|
|
var colQtd = GetColAny(map, "QTD", "QUANTIDADE");
|
|
var colSoma = GetCol(map, "SOMA");
|
|
|
|
if (colGb == 0 || colQtd == 0 || colSoma == 0)
|
|
{
|
|
Console.WriteLine("[WARN] RESUMO/GB_QTD_SOMA: colunas obrigatórias incompletas no cabeçalho.");
|
|
return;
|
|
}
|
|
|
|
var buffer = new List<ResumoGbDistribuicao>(120);
|
|
var dataStarted = false;
|
|
var emptyRowStreak = 0;
|
|
int? totalRowIndex = null;
|
|
|
|
for (int r = headerRow + 1; r <= lastRowUsed; r++)
|
|
{
|
|
var gbText = GetCellString(ws, r, colGb);
|
|
var qtdText = GetCellString(ws, r, colQtd);
|
|
var somaText = GetCellString(ws, r, colSoma);
|
|
|
|
var hasAnyValue = !(string.IsNullOrWhiteSpace(gbText)
|
|
&& string.IsNullOrWhiteSpace(qtdText)
|
|
&& string.IsNullOrWhiteSpace(somaText));
|
|
|
|
if (!hasAnyValue)
|
|
{
|
|
if (dataStarted)
|
|
{
|
|
emptyRowStreak++;
|
|
if (emptyRowStreak >= 2) break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
emptyRowStreak = 0;
|
|
|
|
var gbKey = NormalizeHeader(gbText);
|
|
var isTotalRow = gbKey == NormalizeHeader("TOTAL")
|
|
|| gbKey == NormalizeHeader("TOTAL GERAL");
|
|
if (isTotalRow)
|
|
{
|
|
totalRowIndex = r;
|
|
break;
|
|
}
|
|
|
|
var gbValue = TryDecimal(gbText);
|
|
var qtdValue = TryNullableInt(qtdText);
|
|
var somaValue = TryDecimal(somaText);
|
|
var isDataRow = gbValue.HasValue || qtdValue.HasValue || somaValue.HasValue;
|
|
|
|
if (!isDataRow)
|
|
{
|
|
if (dataStarted) break;
|
|
continue;
|
|
}
|
|
|
|
dataStarted = true;
|
|
|
|
buffer.Add(new ResumoGbDistribuicao
|
|
{
|
|
Gb = gbValue,
|
|
Qtd = qtdValue,
|
|
Soma = somaValue,
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
});
|
|
}
|
|
|
|
if (buffer.Count > 0)
|
|
{
|
|
await _db.ResumoGbDistribuicoes.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
int? totalLinhas = null;
|
|
decimal? somaTotal = null;
|
|
if (totalRowIndex.HasValue)
|
|
{
|
|
totalLinhas = TryNullableInt(GetCellString(ws, totalRowIndex.Value, colQtd));
|
|
somaTotal = TryDecimal(GetCellString(ws, totalRowIndex.Value, colSoma));
|
|
}
|
|
|
|
totalLinhas ??= buffer.Sum(x => x.Qtd ?? 0);
|
|
somaTotal ??= buffer.Sum(x => x.Soma ?? 0m);
|
|
|
|
if (totalLinhas.HasValue || somaTotal.HasValue)
|
|
{
|
|
await _db.ResumoGbDistribuicaoTotais.AddAsync(new ResumoGbDistribuicaoTotal
|
|
{
|
|
TotalLinhas = totalLinhas,
|
|
SomaTotal = somaTotal,
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
});
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
private static int FindHeaderRowForGbDistribuicao(IXLWorksheet ws, int startRow, int lastRow)
|
|
{
|
|
for (int r = startRow; r <= lastRow; r++)
|
|
{
|
|
var row = ws.Row(r);
|
|
if (!row.CellsUsed().Any()) continue;
|
|
|
|
var map = BuildHeaderMap(row);
|
|
var hasGb = GetCol(map, "GB") > 0;
|
|
var hasQtd = GetColAny(map, "QTD", "QUANTIDADE") > 0;
|
|
var hasSoma = GetCol(map, "SOMA") > 0;
|
|
|
|
if (hasGb && hasQtd && hasSoma)
|
|
{
|
|
return r;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private async Task ImportResumoTabela5(IXLWorksheet ws, DateTime now)
|
|
{
|
|
var lastRowUsed = ws.LastRowUsed()?.RowNumber() ?? 1;
|
|
var headerRow = FindHeaderRowForLineTotais(ws, 1, lastRowUsed);
|
|
if (headerRow == 0)
|
|
{
|
|
Console.WriteLine("[WARN] RESUMO/TOTAIS_LINE: cabeçalho VALOR TOTAL LINE/LUCRO TOTAL LINE/QTD. LINHAS não encontrado.");
|
|
return;
|
|
}
|
|
|
|
var map = BuildHeaderMap(ws.Row(headerRow));
|
|
var colValorTotalLine = GetColAny(map, "VALOR TOTAL LINE", "VALOR TOTAL LINE R$");
|
|
var colLucroTotalLine = GetColAny(map, "LUCRO TOTAL LINE", "LUCRO TOTAL LINE R$");
|
|
var colQtdLinhas = GetColAny(map, "QTD. LINHAS", "QTD LINHAS", "QTD. DE LINHAS", "QTDLINHAS");
|
|
var firstMetricCol = new[] { colValorTotalLine, colLucroTotalLine, colQtdLinhas }
|
|
.Where(c => c > 0)
|
|
.DefaultIfEmpty(2)
|
|
.Min();
|
|
var lastLabelCol = Math.Max(1, firstMetricCol - 1);
|
|
|
|
var buffer = new List<ResumoLineTotais>(8);
|
|
var dataStarted = false;
|
|
var emptyRowStreak = 0;
|
|
|
|
for (int r = headerRow + 1; r <= lastRowUsed; r++)
|
|
{
|
|
var tipo = GetFirstNonEmptyCellInRange(ws, r, 1, lastLabelCol);
|
|
var valorRaw = GetCellString(ws, r, colValorTotalLine);
|
|
var lucroRaw = GetCellString(ws, r, colLucroTotalLine);
|
|
var qtdRaw = GetCellString(ws, r, colQtdLinhas);
|
|
|
|
var hasAnyValue = !(string.IsNullOrWhiteSpace(tipo)
|
|
&& string.IsNullOrWhiteSpace(valorRaw)
|
|
&& string.IsNullOrWhiteSpace(lucroRaw)
|
|
&& string.IsNullOrWhiteSpace(qtdRaw));
|
|
|
|
if (!hasAnyValue)
|
|
{
|
|
if (dataStarted)
|
|
{
|
|
emptyRowStreak++;
|
|
if (emptyRowStreak >= 2) break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
emptyRowStreak = 0;
|
|
|
|
var valor = TryDecimal(valorRaw);
|
|
var lucro = TryDecimal(lucroRaw);
|
|
var qtd = TryNullableInt(qtdRaw);
|
|
var isDataRow = valor.HasValue || lucro.HasValue || qtd.HasValue;
|
|
|
|
if (!isDataRow)
|
|
{
|
|
if (dataStarted) break;
|
|
continue;
|
|
}
|
|
|
|
dataStarted = true;
|
|
|
|
buffer.Add(new ResumoLineTotais
|
|
{
|
|
Tipo = string.IsNullOrWhiteSpace(tipo) ? null : tipo.Trim(),
|
|
ValorTotalLine = valor,
|
|
LucroTotalLine = lucro,
|
|
QtdLinhas = qtd,
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
});
|
|
}
|
|
|
|
if (buffer.Count == 0)
|
|
{
|
|
Console.WriteLine("[WARN] RESUMO/TOTAIS_LINE: cabeçalho encontrado, mas nenhuma linha de PF/PJ/DIFERENÇA foi importada.");
|
|
return;
|
|
}
|
|
|
|
await _db.ResumoLineTotais.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
private static int FindHeaderRowForLineTotais(IXLWorksheet ws, int startRow, int lastRow)
|
|
{
|
|
for (int r = startRow; r <= lastRow; r++)
|
|
{
|
|
var row = ws.Row(r);
|
|
if (!row.CellsUsed().Any()) continue;
|
|
|
|
var map = BuildHeaderMap(row);
|
|
var hasValor = GetColAny(map, "VALOR TOTAL LINE", "VALOR TOTAL LINE R$") > 0;
|
|
var hasLucro = GetColAny(map, "LUCRO TOTAL LINE", "LUCRO TOTAL LINE R$") > 0;
|
|
var hasQtd = GetColAny(map, "QTD. LINHAS", "QTD LINHAS", "QTD. DE LINHAS", "QTDLINHAS") > 0;
|
|
|
|
if (hasValor && hasLucro && hasQtd)
|
|
{
|
|
return r;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private async Task ImportResumoTabela6(IXLWorksheet ws, DateTime now)
|
|
{
|
|
var lastRowUsed = ws.LastRowUsed()?.RowNumber() ?? 1;
|
|
var sectionRow = FindSectionRow(ws, "LINHAS NA RESERVA");
|
|
if (sectionRow == 0) return;
|
|
|
|
var headerRow = FindHeaderRowForReserva(ws, sectionRow + 1, lastRowUsed);
|
|
if (headerRow == 0) return;
|
|
|
|
var map = BuildHeaderMap(ws.Row(headerRow));
|
|
var colDdd = GetCol(map, "DDD");
|
|
var colFranquiaGb = GetColAny(map, "FRANQUIA GB", "FRAQUIA GB");
|
|
var colQtdLinhas = GetColAny(map, "QTD. DE LINHAS", "QTD DE LINHAS", "QTD. LINHAS", "QTDLINHAS");
|
|
var colTotal = GetCol(map, "TOTAL");
|
|
|
|
var buffer = new List<ResumoReservaLine>(200);
|
|
string? lastDddValid = null;
|
|
decimal? lastTotalForDdd = null;
|
|
var dataStarted = false;
|
|
var emptyRowStreak = 0;
|
|
int? totalRowIndex = null;
|
|
var totalsFromSheetByDdd = new Dictionary<string, decimal?>();
|
|
var sumQtdByDdd = new Dictionary<string, int>();
|
|
|
|
for (int r = headerRow + 1; r <= lastRowUsed; r++)
|
|
{
|
|
var ddd = GetCellString(ws, r, colDdd);
|
|
var franquia = GetCellString(ws, r, colFranquiaGb);
|
|
var qtdLinhas = GetCellString(ws, r, colQtdLinhas);
|
|
var total = GetCellString(ws, r, colTotal);
|
|
|
|
var hasAnyValue = !(string.IsNullOrWhiteSpace(ddd)
|
|
&& string.IsNullOrWhiteSpace(franquia)
|
|
&& string.IsNullOrWhiteSpace(qtdLinhas)
|
|
&& string.IsNullOrWhiteSpace(total));
|
|
|
|
if (!hasAnyValue)
|
|
{
|
|
if (dataStarted)
|
|
{
|
|
emptyRowStreak++;
|
|
if (emptyRowStreak >= 2) break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
emptyRowStreak = 0;
|
|
|
|
var dddCandidate = NullIfEmptyDigits(ddd);
|
|
var hasFranquiaText = !string.IsNullOrWhiteSpace(franquia);
|
|
var hasQtdText = !string.IsNullOrWhiteSpace(qtdLinhas);
|
|
var hasTotalText = !string.IsNullOrWhiteSpace(total);
|
|
|
|
// ✅ Rodapé "TOTAL GERAL" (DDD vazio + franquia vazia + qtd + total preenchidos)
|
|
var isTotalGeralRow = string.IsNullOrWhiteSpace(dddCandidate)
|
|
&& !hasFranquiaText
|
|
&& hasQtdText
|
|
&& hasTotalText;
|
|
|
|
if (isTotalGeralRow)
|
|
{
|
|
totalRowIndex = r;
|
|
break;
|
|
}
|
|
|
|
var franquiaValue = TryDecimal(franquia);
|
|
var qtdValue = TryNullableInt(qtdLinhas);
|
|
var isDataRow = franquiaValue.HasValue || qtdValue.HasValue;
|
|
|
|
if (!string.IsNullOrWhiteSpace(dddCandidate))
|
|
{
|
|
if (!string.Equals(lastDddValid, dddCandidate, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
lastTotalForDdd = null;
|
|
}
|
|
lastDddValid = dddCandidate;
|
|
}
|
|
|
|
if (!isDataRow && dataStarted)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (isDataRow) dataStarted = true;
|
|
|
|
var resolvedDdd = isDataRow
|
|
? (dddCandidate ?? lastDddValid)
|
|
: dddCandidate;
|
|
|
|
var totalValue = TryDecimal(total);
|
|
if (!string.IsNullOrWhiteSpace(resolvedDdd) && totalValue.HasValue)
|
|
{
|
|
lastTotalForDdd = totalValue;
|
|
totalsFromSheetByDdd[resolvedDdd] = totalValue;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(resolvedDdd) && qtdValue.HasValue)
|
|
{
|
|
if (sumQtdByDdd.TryGetValue(resolvedDdd, out var acc))
|
|
sumQtdByDdd[resolvedDdd] = acc + qtdValue.Value;
|
|
else
|
|
sumQtdByDdd[resolvedDdd] = qtdValue.Value;
|
|
}
|
|
|
|
buffer.Add(new ResumoReservaLine
|
|
{
|
|
Ddd = string.IsNullOrWhiteSpace(resolvedDdd) ? null : resolvedDdd,
|
|
FranquiaGb = franquiaValue,
|
|
QtdLinhas = qtdValue,
|
|
Total = totalValue ?? lastTotalForDdd,
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
});
|
|
}
|
|
|
|
var missingDddCount = buffer.Count(x => x.Ddd == null && (x.FranquiaGb.HasValue || x.QtdLinhas.HasValue));
|
|
if (missingDddCount > 0)
|
|
{
|
|
throw new InvalidOperationException($"Import RESUMO/RESERVA: {missingDddCount} linhas de dados ficaram sem DDD.");
|
|
}
|
|
|
|
if (buffer.Count > 0)
|
|
{
|
|
await _db.ResumoReservaLines.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
if (totalsFromSheetByDdd.Count > 0)
|
|
{
|
|
foreach (var kv in totalsFromSheetByDdd)
|
|
{
|
|
if (!kv.Value.HasValue) continue;
|
|
if (!sumQtdByDdd.TryGetValue(kv.Key, out var sum)) continue;
|
|
var totalInt = (int)Math.Round(kv.Value.Value);
|
|
if (totalInt != sum)
|
|
{
|
|
Console.WriteLine($"[WARN] RESUMO/RESERVA DDD {kv.Key}: TOTAL(planilha)={totalInt} vs SOMA(QTD)={sum}");
|
|
}
|
|
}
|
|
}
|
|
|
|
var totalGeralLinhasReserva = totalRowIndex.HasValue
|
|
? TryNullableInt(GetCellString(ws, totalRowIndex.Value, colQtdLinhas))
|
|
: null;
|
|
|
|
if (totalGeralLinhasReserva.HasValue)
|
|
{
|
|
var somaPorDdd = sumQtdByDdd.Values.Sum();
|
|
if (somaPorDdd != totalGeralLinhasReserva.Value)
|
|
{
|
|
Console.WriteLine($"[WARN] RESUMO/RESERVA TOTAL GERAL: planilha={totalGeralLinhasReserva.Value} vs SOMA(DDD)={somaPorDdd}");
|
|
}
|
|
}
|
|
|
|
if (totalRowIndex == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var totalEntity = new ResumoReservaTotal
|
|
{
|
|
TotalGeralLinhasReserva = totalGeralLinhasReserva,
|
|
QtdLinhasTotal = TryNullableInt(GetCellString(ws, totalRowIndex.Value, colQtdLinhas)),
|
|
Total = TryDecimal(GetCellString(ws, totalRowIndex.Value, colTotal)),
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
};
|
|
|
|
await _db.ResumoReservaTotals.AddAsync(totalEntity);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
|
|
private static int FindSectionRow(IXLWorksheet ws, string sectionName)
|
|
{
|
|
var normalizedTarget = NormalizeHeader(sectionName);
|
|
foreach (var row in ws.RowsUsed())
|
|
{
|
|
foreach (var cell in row.CellsUsed())
|
|
{
|
|
var key = NormalizeHeader(cell.GetString());
|
|
if (string.IsNullOrWhiteSpace(key)) continue;
|
|
if (key.Contains(normalizedTarget)) return row.RowNumber();
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private static int FindHeaderRowForReserva(IXLWorksheet ws, int startRow, int lastRow)
|
|
{
|
|
for (int r = startRow; r <= lastRow; r++)
|
|
{
|
|
var row = ws.Row(r);
|
|
if (!row.CellsUsed().Any()) continue;
|
|
|
|
var map = BuildHeaderMap(row);
|
|
var hasDdd = GetCol(map, "DDD") > 0;
|
|
var hasFranquia = GetColAny(map, "FRANQUIA GB", "FRAQUIA GB") > 0;
|
|
var hasQtd = GetColAny(map, "QTD. DE LINHAS", "QTD DE LINHAS", "QTD. LINHAS", "QTDLINHAS") > 0;
|
|
|
|
if (hasDdd && hasFranquia && hasQtd)
|
|
{
|
|
return r;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private async Task ImportControleRecebidosSheet(IXLWorksheet ws, int year)
|
|
{
|
|
var buffer = new List<ControleRecebidoLine>(500);
|
|
|
|
var firstRow = ws.FirstRowUsed()?.RowNumber() ?? 1;
|
|
var lastRow = ws.LastRowUsed()?.RowNumber() ?? firstRow;
|
|
var rowIndex = firstRow;
|
|
decimal? lastValorDaNf = null;
|
|
decimal? lastValorUnit = null;
|
|
|
|
while (rowIndex <= lastRow)
|
|
{
|
|
var row = ws.Row(rowIndex);
|
|
if (!IsControleRecebidosHeader(row))
|
|
{
|
|
rowIndex++;
|
|
continue;
|
|
}
|
|
|
|
var map = BuildHeaderMap(row);
|
|
int colItem = GetCol(map, "ITEM");
|
|
if (colItem == 0)
|
|
{
|
|
rowIndex++;
|
|
continue;
|
|
}
|
|
|
|
var isResumo = GetColAny(map, "QTD.", "QTD", "QUANTIDADE") > 0
|
|
&& GetColAny(map, "CHIP") == 0
|
|
&& GetColAny(map, "SERIAL") == 0;
|
|
|
|
lastValorDaNf = null;
|
|
lastValorUnit = null;
|
|
rowIndex++;
|
|
|
|
for (; rowIndex <= lastRow; rowIndex++)
|
|
{
|
|
var currentRow = ws.Row(rowIndex);
|
|
if (IsControleRecebidosHeader(currentRow))
|
|
{
|
|
rowIndex--;
|
|
break;
|
|
}
|
|
|
|
var itemStr = GetCellString(ws, rowIndex, colItem);
|
|
if (string.IsNullOrWhiteSpace(itemStr)) break;
|
|
|
|
var notaFiscal = GetCellByHeaderAny(ws, rowIndex, map, "NOTA FISCAL", "NOTA", "NF");
|
|
var chip = NullIfEmptyDigits(GetCellByHeaderAny(ws, rowIndex, map, "CHIP"));
|
|
var serial = GetCellByHeaderAny(ws, rowIndex, map, "SERIAL");
|
|
var conteudo = GetCellByHeaderAny(ws, rowIndex, map, "CONTEÚDO DA NF", "CONTEUDO DA NF");
|
|
var numeroLinha = NullIfEmptyDigits(GetCellByHeaderAny(ws, rowIndex, map, "NÚMERO DA LINHA", "NUMERO DA LINHA"));
|
|
var valorUnit = TryDecimal(GetCellByHeaderAny(ws, rowIndex, map, "VALOR UNIT.", "VALOR UNIT", "VALOR UNITÁRIO", "VALOR UNITARIO"));
|
|
if (valorUnit.HasValue)
|
|
{
|
|
lastValorUnit = valorUnit;
|
|
}
|
|
else
|
|
{
|
|
valorUnit = lastValorUnit;
|
|
}
|
|
var valorDaNf = TryDecimal(GetCellByHeaderAny(ws, rowIndex, map, "VALOR DA NF", "VALOR DA N F"));
|
|
if (valorDaNf.HasValue)
|
|
{
|
|
lastValorDaNf = valorDaNf;
|
|
}
|
|
else
|
|
{
|
|
valorDaNf = lastValorDaNf;
|
|
}
|
|
var dataDaNf = TryDateNoUtc(ws, rowIndex, map, "DATA DA NF");
|
|
var dataReceb = TryDateNoUtc(ws, rowIndex, map, "DATA DO RECEBIMENTO");
|
|
var qtd = TryNullableInt(GetCellByHeaderAny(ws, rowIndex, map, "QTD.", "QTD", "QUANTIDADE"));
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
var e = new ControleRecebidoLine
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Ano = year,
|
|
Item = TryInt(itemStr),
|
|
NotaFiscal = string.IsNullOrWhiteSpace(notaFiscal) ? null : notaFiscal.Trim(),
|
|
Chip = chip,
|
|
Serial = string.IsNullOrWhiteSpace(serial) ? null : serial.Trim(),
|
|
ConteudoDaNf = string.IsNullOrWhiteSpace(conteudo) ? null : conteudo.Trim(),
|
|
NumeroDaLinha = numeroLinha,
|
|
ValorUnit = valorUnit,
|
|
ValorDaNf = valorDaNf,
|
|
DataDaNf = dataDaNf,
|
|
DataDoRecebimento = dataReceb,
|
|
Quantidade = qtd,
|
|
IsResumo = isResumo,
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
};
|
|
|
|
buffer.Add(e);
|
|
|
|
if (buffer.Count >= 500)
|
|
{
|
|
await _db.ControleRecebidoLines.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
buffer.Clear();
|
|
}
|
|
}
|
|
|
|
rowIndex++;
|
|
}
|
|
|
|
if (buffer.Count > 0)
|
|
{
|
|
await _db.ControleRecebidoLines.AddRangeAsync(buffer);
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
private sealed class ControleRecebidosWorksheetInfo
|
|
{
|
|
public ControleRecebidosWorksheetInfo(IXLWorksheet sheet, int year)
|
|
{
|
|
Sheet = sheet;
|
|
Year = year;
|
|
}
|
|
|
|
public IXLWorksheet Sheet { get; }
|
|
public int Year { get; }
|
|
}
|
|
|
|
private static IEnumerable<ControleRecebidosWorksheetInfo> GetControleRecebidosWorksheets(XLWorkbook wb)
|
|
{
|
|
var years = new[] { 2022, 2023, 2024, 2025 };
|
|
|
|
foreach (var ws in wb.Worksheets)
|
|
{
|
|
var name = NormalizeHeader(ws.Name);
|
|
var isControleRecebidos = name.Contains("CONTROLE") && name.Contains("RECEBIDOS");
|
|
var isRomaneio = name.Contains("ROMANEIO");
|
|
|
|
if (!isControleRecebidos && !isRomaneio) continue;
|
|
|
|
var year = years.FirstOrDefault(y => name.Contains(y.ToString()));
|
|
if (year == 0) continue;
|
|
|
|
yield return new ControleRecebidosWorksheetInfo(ws, year);
|
|
}
|
|
}
|
|
|
|
private static IXLWorksheet? FindControleRecebidosWorksheet(XLWorkbook wb, int year)
|
|
{
|
|
var normalizedName = NormalizeHeader($"CONTROLE DE RECEBIDOS {year}");
|
|
|
|
var ws = wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == normalizedName);
|
|
if (ws != null) return ws;
|
|
|
|
ws = wb.Worksheets.FirstOrDefault(w =>
|
|
{
|
|
var name = NormalizeHeader(w.Name);
|
|
return name.Contains("CONTROLE") && name.Contains("RECEBIDOS") && name.Contains(year.ToString());
|
|
});
|
|
if (ws != null) return ws;
|
|
|
|
if (year == 2024)
|
|
{
|
|
ws = wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("CONTROLE DE RECEBIDOS"));
|
|
}
|
|
|
|
return ws;
|
|
}
|
|
|
|
private static bool IsControleRecebidosHeader(IXLRow row)
|
|
{
|
|
var hasItem = false;
|
|
var hasNota = false;
|
|
|
|
foreach (var cell in row.CellsUsed())
|
|
{
|
|
var k = NormalizeHeader(cell.GetString());
|
|
if (k == "ITEM") hasItem = true;
|
|
if (k == "NOTAFISCAL") hasNota = true;
|
|
if (hasItem && hasNota) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool IsChipsVirgensHeader(IXLRow row)
|
|
{
|
|
var hasItem = false;
|
|
var hasNumeroChip = false;
|
|
var hasObs = false;
|
|
|
|
foreach (var cell in row.CellsUsed())
|
|
{
|
|
var k = NormalizeHeader(cell.GetString());
|
|
if (k == "ITEM") hasItem = true;
|
|
if (k.Contains("CHIP")) hasNumeroChip = true;
|
|
if (k.Contains("OBS")) hasObs = true;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
private static void ApplyCreateRequestToLine(
|
|
MobileLine line,
|
|
CreateMobileLineDto req,
|
|
string linhaLimpa,
|
|
string? chipLimpo,
|
|
decimal? franquiaVivo,
|
|
decimal? valorPlanoVivo,
|
|
DateTime now)
|
|
{
|
|
line.Cliente = req.Cliente?.Trim();
|
|
line.Linha = linhaLimpa;
|
|
line.Chip = string.IsNullOrWhiteSpace(chipLimpo) ? null : chipLimpo;
|
|
line.Usuario = req.Usuario?.Trim();
|
|
line.CentroDeCustos = NormalizeOptionalText(req.CentroDeCustos);
|
|
line.Status = req.Status?.Trim();
|
|
line.Skil = req.Skil?.Trim();
|
|
line.Modalidade = req.Modalidade?.Trim();
|
|
line.PlanoContrato = req.PlanoContrato?.Trim();
|
|
line.Conta = req.Conta?.Trim();
|
|
line.VencConta = req.VencConta?.Trim();
|
|
|
|
line.DataBloqueio = ToUtc(req.DataBloqueio);
|
|
line.DataEntregaOpera = ToUtc(req.DataEntregaOpera);
|
|
line.DataEntregaCliente = ToUtc(req.DataEntregaCliente);
|
|
|
|
line.Cedente = req.Cedente?.Trim();
|
|
line.Solicitante = req.Solicitante?.Trim();
|
|
|
|
line.FranquiaVivo = franquiaVivo;
|
|
line.ValorPlanoVivo = valorPlanoVivo;
|
|
line.GestaoVozDados = req.GestaoVozDados;
|
|
line.Skeelo = req.Skeelo;
|
|
line.VivoNewsPlus = req.VivoNewsPlus;
|
|
line.VivoTravelMundo = req.VivoTravelMundo;
|
|
line.VivoSync = req.VivoSync;
|
|
line.VivoGestaoDispositivo = req.VivoGestaoDispositivo;
|
|
line.ValorContratoVivo = req.ValorContratoVivo;
|
|
line.FranquiaLine = req.FranquiaLine;
|
|
line.FranquiaGestao = req.FranquiaGestao;
|
|
line.LocacaoAp = req.LocacaoAp;
|
|
line.ValorContratoLine = req.ValorContratoLine;
|
|
line.Desconto = req.Desconto;
|
|
line.Lucro = req.Lucro;
|
|
line.TipoDeChip = req.TipoDeChip?.Trim();
|
|
line.UpdatedAt = now;
|
|
}
|
|
|
|
private async Task ApplySetorAndAparelhoToLineAsync(MobileLine line, CreateMobileLineDto req)
|
|
{
|
|
var tenantId = line.TenantId != Guid.Empty
|
|
? line.TenantId
|
|
: (_tenantProvider.ActorTenantId ?? Guid.Empty);
|
|
if (tenantId == Guid.Empty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (line.TenantId == Guid.Empty)
|
|
{
|
|
line.TenantId = tenantId;
|
|
}
|
|
|
|
await ApplySetorToLineAsync(line, tenantId, req.SetorId, req.SetorNome);
|
|
await ApplyAparelhoToLineAsync(
|
|
line,
|
|
tenantId,
|
|
req.AparelhoId,
|
|
req.AparelhoNome,
|
|
req.AparelhoCor,
|
|
req.AparelhoImei);
|
|
}
|
|
|
|
private async Task ApplySetorToLineAsync(MobileLine line, Guid tenantId, Guid? setorId, string? setorNome)
|
|
{
|
|
var hasSetorId = setorId.HasValue && setorId.Value != Guid.Empty;
|
|
var setorNomeInformado = setorNome != null;
|
|
var setorNomeNormalizado = NormalizeOptionalText(setorNome);
|
|
|
|
if (!hasSetorId && !setorNomeInformado)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!hasSetorId && setorNomeInformado && string.IsNullOrWhiteSpace(setorNomeNormalizado))
|
|
{
|
|
line.SetorId = null;
|
|
line.Setor = null;
|
|
return;
|
|
}
|
|
|
|
var setor = await ResolveSetorAsync(tenantId, setorId, setorNomeNormalizado);
|
|
if (setor == null)
|
|
{
|
|
if (setorNomeInformado)
|
|
{
|
|
line.SetorId = null;
|
|
line.Setor = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
line.SetorId = setor.Id;
|
|
line.Setor = setor;
|
|
}
|
|
|
|
private async Task<Setor?> ResolveSetorAsync(Guid tenantId, Guid? setorId, string? setorNome)
|
|
{
|
|
if (setorId.HasValue && setorId.Value != Guid.Empty)
|
|
{
|
|
var byId = await _db.Setores.FirstOrDefaultAsync(s => s.TenantId == tenantId && s.Id == setorId.Value);
|
|
if (byId != null)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(setorNome) &&
|
|
!string.Equals(byId.Nome, setorNome, StringComparison.Ordinal))
|
|
{
|
|
byId.Nome = setorNome;
|
|
byId.UpdatedAt = DateTime.UtcNow;
|
|
}
|
|
return byId;
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(setorNome))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var normalized = NormalizeTenantKeyValue(setorNome);
|
|
var candidates = await _db.Setores.Where(s => s.TenantId == tenantId).ToListAsync();
|
|
var existing = candidates.FirstOrDefault(s =>
|
|
string.Equals(NormalizeTenantKeyValue(s.Nome), normalized, StringComparison.Ordinal));
|
|
if (existing != null)
|
|
{
|
|
if (!string.Equals(existing.Nome, setorNome, StringComparison.Ordinal))
|
|
{
|
|
existing.Nome = setorNome;
|
|
existing.UpdatedAt = DateTime.UtcNow;
|
|
}
|
|
return existing;
|
|
}
|
|
|
|
var now = DateTime.UtcNow;
|
|
var created = new Setor
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TenantId = tenantId,
|
|
Nome = setorNome,
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
};
|
|
|
|
_db.Setores.Add(created);
|
|
return created;
|
|
}
|
|
|
|
private async Task ApplyAparelhoToLineAsync(
|
|
MobileLine line,
|
|
Guid tenantId,
|
|
Guid? aparelhoId,
|
|
string? aparelhoNome,
|
|
string? aparelhoCor,
|
|
string? aparelhoImei)
|
|
{
|
|
var hasAparelhoId = aparelhoId.HasValue && aparelhoId.Value != Guid.Empty;
|
|
var hasAnyPayload =
|
|
aparelhoNome != null ||
|
|
aparelhoCor != null ||
|
|
aparelhoImei != null;
|
|
|
|
if (!hasAparelhoId && !hasAnyPayload)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var normalizedNome = NormalizeOptionalText(aparelhoNome);
|
|
var normalizedCor = NormalizeOptionalText(aparelhoCor);
|
|
var normalizedImei = NormalizeOptionalText(aparelhoImei);
|
|
|
|
var shouldDetach =
|
|
!hasAparelhoId &&
|
|
hasAnyPayload &&
|
|
string.IsNullOrWhiteSpace(normalizedNome) &&
|
|
string.IsNullOrWhiteSpace(normalizedCor) &&
|
|
string.IsNullOrWhiteSpace(normalizedImei);
|
|
|
|
if (shouldDetach)
|
|
{
|
|
line.AparelhoId = null;
|
|
line.Aparelho = null;
|
|
return;
|
|
}
|
|
|
|
var aparelho = await ResolveAparelhoAsync(tenantId, aparelhoId, line.AparelhoId);
|
|
if (aparelho == null)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
aparelho = new Aparelho
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TenantId = tenantId,
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
};
|
|
_db.Aparelhos.Add(aparelho);
|
|
}
|
|
|
|
if (aparelhoNome != null) aparelho.Nome = normalizedNome;
|
|
if (aparelhoCor != null) aparelho.Cor = normalizedCor;
|
|
if (aparelhoImei != null) aparelho.Imei = normalizedImei;
|
|
|
|
aparelho.UpdatedAt = DateTime.UtcNow;
|
|
|
|
line.AparelhoId = aparelho.Id;
|
|
line.Aparelho = aparelho;
|
|
}
|
|
|
|
private async Task<Aparelho?> ResolveAparelhoAsync(Guid tenantId, Guid? aparelhoId, Guid? lineAparelhoId)
|
|
{
|
|
if (aparelhoId.HasValue && aparelhoId.Value != Guid.Empty)
|
|
{
|
|
var byRequestId = await _db.Aparelhos
|
|
.FirstOrDefaultAsync(a => a.TenantId == tenantId && a.Id == aparelhoId.Value);
|
|
if (byRequestId != null)
|
|
{
|
|
return byRequestId;
|
|
}
|
|
}
|
|
|
|
if (lineAparelhoId.HasValue && lineAparelhoId.Value != Guid.Empty)
|
|
{
|
|
return await _db.Aparelhos
|
|
.FirstOrDefaultAsync(a => a.TenantId == tenantId && a.Id == lineAparelhoId.Value);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private async Task<Aparelho> EnsureLineHasAparelhoAsync(MobileLine line, Guid tenantId)
|
|
{
|
|
if (line.Aparelho != null && line.Aparelho.TenantId == tenantId)
|
|
{
|
|
if (line.AparelhoId != line.Aparelho.Id)
|
|
{
|
|
line.AparelhoId = line.Aparelho.Id;
|
|
}
|
|
return line.Aparelho;
|
|
}
|
|
|
|
var aparelho = await ResolveAparelhoAsync(tenantId, line.AparelhoId, line.AparelhoId);
|
|
if (aparelho == null)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
aparelho = new Aparelho
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
TenantId = tenantId,
|
|
CreatedAt = now,
|
|
UpdatedAt = now
|
|
};
|
|
_db.Aparelhos.Add(aparelho);
|
|
}
|
|
|
|
line.AparelhoId = aparelho.Id;
|
|
line.Aparelho = aparelho;
|
|
return aparelho;
|
|
}
|
|
|
|
private async Task<string> SaveAparelhoAttachmentAsync(
|
|
IFormFile file,
|
|
Guid tenantId,
|
|
Guid lineId,
|
|
string attachmentKind,
|
|
string? previousRelativePath)
|
|
{
|
|
const long maxBytes = 15 * 1024 * 1024;
|
|
if (file.Length <= 0)
|
|
{
|
|
throw new InvalidOperationException("Arquivo de anexo inválido.");
|
|
}
|
|
|
|
if (file.Length > maxBytes)
|
|
{
|
|
throw new InvalidOperationException("O anexo excede o limite de 15MB.");
|
|
}
|
|
|
|
var extension = (Path.GetExtension(file.FileName) ?? string.Empty).Trim().ToLowerInvariant();
|
|
var allowedExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
".pdf", ".png", ".jpg", ".jpeg", ".webp"
|
|
};
|
|
|
|
if (string.IsNullOrWhiteSpace(extension) || !allowedExtensions.Contains(extension))
|
|
{
|
|
throw new InvalidOperationException("Formato de arquivo inválido. Use PDF, PNG, JPG, JPEG ou WEBP.");
|
|
}
|
|
|
|
var targetDirectory = Path.Combine(
|
|
_aparelhoAttachmentsRootPath,
|
|
tenantId.ToString("N"),
|
|
lineId.ToString("N"),
|
|
attachmentKind);
|
|
|
|
Directory.CreateDirectory(targetDirectory);
|
|
|
|
var safeName = $"{DateTime.UtcNow:yyyyMMddHHmmssfff}_{Guid.NewGuid():N}{extension}";
|
|
var fullPath = Path.Combine(targetDirectory, safeName);
|
|
|
|
await using (var stream = new FileStream(fullPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
|
{
|
|
await file.CopyToAsync(stream);
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(previousRelativePath))
|
|
{
|
|
var oldPath = TryResolveAttachmentFullPath(previousRelativePath);
|
|
if (!string.IsNullOrWhiteSpace(oldPath) &&
|
|
!string.Equals(oldPath, fullPath, StringComparison.OrdinalIgnoreCase) &&
|
|
System.IO.File.Exists(oldPath))
|
|
{
|
|
try
|
|
{
|
|
System.IO.File.Delete(oldPath);
|
|
}
|
|
catch
|
|
{
|
|
// Falha ao remover arquivo antigo não deve impedir o fluxo principal.
|
|
}
|
|
}
|
|
}
|
|
|
|
var relativePath = Path.GetRelativePath(_aparelhoAttachmentsRootPath, fullPath);
|
|
return relativePath.Replace('\\', '/');
|
|
}
|
|
|
|
private string? TryResolveAttachmentFullPath(string? relativePath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(relativePath))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var normalized = relativePath
|
|
.Replace('\\', Path.DirectorySeparatorChar)
|
|
.TrimStart(Path.DirectorySeparatorChar, '/');
|
|
|
|
if (normalized.Contains("..", StringComparison.Ordinal))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var root = Path.GetFullPath(_aparelhoAttachmentsRootPath);
|
|
var fullPath = Path.GetFullPath(Path.Combine(root, normalized));
|
|
if (!fullPath.StartsWith(root, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return fullPath;
|
|
}
|
|
|
|
private static string? NormalizeOptionalText(string? value)
|
|
{
|
|
if (value == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var trimmed = value.Trim();
|
|
return trimmed.Length == 0 ? null : trimmed;
|
|
}
|
|
|
|
private async Task<Tenant?> EnsureTenantForClientAsync(string? rawClientName)
|
|
{
|
|
var clientName = (rawClientName ?? string.Empty).Trim();
|
|
if (string.IsNullOrWhiteSpace(clientName) || IsReservaValue(clientName))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var normalizedClient = NormalizeTenantKeyValue(clientName);
|
|
if (string.IsNullOrWhiteSpace(normalizedClient) ||
|
|
string.Equals(normalizedClient, "RESERVA", StringComparison.Ordinal))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var existingCandidates = await _db.Tenants
|
|
.IgnoreQueryFilters()
|
|
.Where(t => !t.IsSystem && t.SourceType == SystemTenantConstants.MobileLinesClienteSourceType)
|
|
.ToListAsync();
|
|
|
|
var tenant = existingCandidates.FirstOrDefault(t =>
|
|
string.Equals(NormalizeTenantKeyValue(t.SourceKey ?? string.Empty), normalizedClient, StringComparison.Ordinal) ||
|
|
string.Equals(NormalizeTenantKeyValue(t.NomeOficial ?? string.Empty), normalizedClient, StringComparison.Ordinal));
|
|
|
|
if (tenant != null)
|
|
{
|
|
var changed = false;
|
|
if (!tenant.Ativo)
|
|
{
|
|
tenant.Ativo = true;
|
|
changed = true;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(tenant.NomeOficial))
|
|
{
|
|
tenant.NomeOficial = clientName;
|
|
changed = true;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(tenant.SourceKey))
|
|
{
|
|
tenant.SourceKey = clientName;
|
|
changed = true;
|
|
}
|
|
|
|
if (!string.Equals(tenant.SourceType, SystemTenantConstants.MobileLinesClienteSourceType, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
tenant.SourceType = SystemTenantConstants.MobileLinesClienteSourceType;
|
|
changed = true;
|
|
}
|
|
|
|
if (changed)
|
|
{
|
|
tenant.NomeOficial = tenant.NomeOficial.Trim();
|
|
tenant.SourceKey = tenant.SourceKey?.Trim();
|
|
}
|
|
|
|
return tenant;
|
|
}
|
|
|
|
var deterministicId = DeterministicGuid.FromString($"{SystemTenantConstants.MobileLinesClienteSourceType}:{normalizedClient}");
|
|
var idAlreadyExists = await _db.Tenants.IgnoreQueryFilters().AnyAsync(t => t.Id == deterministicId);
|
|
|
|
tenant = new Tenant
|
|
{
|
|
Id = idAlreadyExists ? Guid.NewGuid() : deterministicId,
|
|
NomeOficial = clientName,
|
|
IsSystem = false,
|
|
Ativo = true,
|
|
SourceType = SystemTenantConstants.MobileLinesClienteSourceType,
|
|
SourceKey = clientName,
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
_db.Tenants.Add(tenant);
|
|
return tenant;
|
|
}
|
|
|
|
private static string NormalizeTenantKeyValue(string value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var normalized = value.Trim().Normalize(NormalizationForm.FormD);
|
|
var sb = new StringBuilder(normalized.Length);
|
|
var previousWasSpace = false;
|
|
|
|
foreach (var ch in normalized)
|
|
{
|
|
var category = CharUnicodeInfo.GetUnicodeCategory(ch);
|
|
if (category == UnicodeCategory.NonSpacingMark)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (char.IsWhiteSpace(ch))
|
|
{
|
|
if (!previousWasSpace)
|
|
{
|
|
sb.Append(' ');
|
|
previousWasSpace = true;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
sb.Append(char.ToUpperInvariant(ch));
|
|
previousWasSpace = false;
|
|
}
|
|
|
|
return sb.ToString().Trim();
|
|
}
|
|
|
|
// ==========================================================
|
|
// HELPERS (SEUS)
|
|
// ==========================================================
|
|
private static Dictionary<string, int> BuildHeaderMap(IXLRow headerRow)
|
|
{
|
|
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var cell in headerRow.CellsUsed())
|
|
{
|
|
var k = NormalizeHeader(cell.GetString());
|
|
if (!string.IsNullOrWhiteSpace(k) && !map.ContainsKey(k))
|
|
map[k] = cell.Address.ColumnNumber;
|
|
}
|
|
return map;
|
|
}
|
|
|
|
private static Dictionary<string, int> BuildHeaderMapRange(IXLRow headerRow, int startCol, int endCol)
|
|
{
|
|
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var cell in headerRow.CellsUsed())
|
|
{
|
|
var col = cell.Address.ColumnNumber;
|
|
if (col < startCol || col > endCol) continue;
|
|
|
|
var k = NormalizeHeader(cell.GetString());
|
|
if (!string.IsNullOrWhiteSpace(k) && !map.ContainsKey(k))
|
|
map[k] = col;
|
|
}
|
|
return map;
|
|
}
|
|
|
|
private static int FindHeaderColumn(IXLRow headerRow, int startCol, int endCol, params string[] headerKeys)
|
|
{
|
|
var normalizedKeys = headerKeys.Select(NormalizeHeader).Where(k => !string.IsNullOrWhiteSpace(k)).ToArray();
|
|
if (normalizedKeys.Length == 0) return 0;
|
|
|
|
for (int col = startCol; col <= endCol; col++)
|
|
{
|
|
var key = NormalizeHeader(headerRow.Cell(col).GetString());
|
|
if (string.IsNullOrWhiteSpace(key)) continue;
|
|
|
|
foreach (var wanted in normalizedKeys)
|
|
{
|
|
if (key == wanted || key.Contains(wanted))
|
|
{
|
|
return col;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private static DateTime? ToUtc(DateTime? dt)
|
|
{
|
|
if (dt == null) return null;
|
|
var v = dt.Value;
|
|
return v.Kind == DateTimeKind.Utc ? v :
|
|
(v.Kind == DateTimeKind.Local ? v.ToUniversalTime() : DateTime.SpecifyKind(v, DateTimeKind.Utc));
|
|
}
|
|
|
|
private async Task<VigenciaLine?> FindVigenciaByMobileLineAsync(MobileLine mobileLine, string? previousLinha, bool asNoTracking)
|
|
{
|
|
var currentLinha = NullIfEmptyDigits(mobileLine.Linha);
|
|
var oldLinha = NullIfEmptyDigits(previousLinha);
|
|
if (!string.IsNullOrWhiteSpace(currentLinha) &&
|
|
string.Equals(currentLinha, oldLinha, StringComparison.Ordinal))
|
|
{
|
|
oldLinha = null;
|
|
}
|
|
|
|
IQueryable<VigenciaLine> q = _db.VigenciaLines;
|
|
if (asNoTracking)
|
|
{
|
|
q = q.AsNoTracking();
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(currentLinha) && !string.IsNullOrWhiteSpace(oldLinha))
|
|
{
|
|
var byEitherLinha = await q.FirstOrDefaultAsync(v => v.Linha == currentLinha || v.Linha == oldLinha);
|
|
if (byEitherLinha != null) return byEitherLinha;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(currentLinha))
|
|
{
|
|
var byLinha = await q.FirstOrDefaultAsync(v => v.Linha == currentLinha);
|
|
if (byLinha != null) return byLinha;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(oldLinha))
|
|
{
|
|
var byPreviousLinha = await q.FirstOrDefaultAsync(v => v.Linha == oldLinha);
|
|
if (byPreviousLinha != null) return byPreviousLinha;
|
|
}
|
|
|
|
if (mobileLine.Item > 0 && string.IsNullOrWhiteSpace(currentLinha) && string.IsNullOrWhiteSpace(oldLinha))
|
|
{
|
|
return await q.FirstOrDefaultAsync(v => v.Item == mobileLine.Item);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private async Task<VigenciaLine> UpsertVigenciaFromMobileLineAsync(
|
|
MobileLine mobileLine,
|
|
DateTime? dtEfetivacaoServico,
|
|
DateTime? dtTerminoFidelizacao,
|
|
bool overrideDates,
|
|
string? previousLinha = null)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var vigencia = await FindVigenciaByMobileLineAsync(mobileLine, previousLinha, asNoTracking: false);
|
|
|
|
if (vigencia == null)
|
|
{
|
|
vigencia = new VigenciaLine
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
CreatedAt = now
|
|
};
|
|
_db.VigenciaLines.Add(vigencia);
|
|
}
|
|
|
|
vigencia.Item = mobileLine.Item;
|
|
vigencia.Conta = string.IsNullOrWhiteSpace(mobileLine.Conta) ? null : mobileLine.Conta.Trim();
|
|
vigencia.Cliente = string.IsNullOrWhiteSpace(mobileLine.Cliente) ? null : mobileLine.Cliente.Trim();
|
|
vigencia.Usuario = string.IsNullOrWhiteSpace(mobileLine.Usuario) ? null : mobileLine.Usuario.Trim();
|
|
vigencia.PlanoContrato = string.IsNullOrWhiteSpace(mobileLine.PlanoContrato) ? null : mobileLine.PlanoContrato.Trim();
|
|
|
|
var linhaDigits = NullIfEmptyDigits(mobileLine.Linha) ?? NullIfEmptyDigits(previousLinha);
|
|
if (!string.IsNullOrWhiteSpace(linhaDigits))
|
|
{
|
|
vigencia.Linha = linhaDigits;
|
|
}
|
|
|
|
var totalFromLine = mobileLine.ValorPlanoVivo ?? mobileLine.ValorContratoVivo;
|
|
if (totalFromLine.HasValue)
|
|
{
|
|
vigencia.Total = totalFromLine.Value;
|
|
}
|
|
|
|
if (overrideDates || dtEfetivacaoServico.HasValue)
|
|
{
|
|
vigencia.DtEfetivacaoServico = ToUtc(dtEfetivacaoServico);
|
|
}
|
|
if (overrideDates || dtTerminoFidelizacao.HasValue)
|
|
{
|
|
vigencia.DtTerminoFidelizacao = ToUtc(dtTerminoFidelizacao);
|
|
}
|
|
|
|
vigencia.UpdatedAt = now;
|
|
return vigencia;
|
|
}
|
|
|
|
private static void EnrichOperadoraContext(IEnumerable<MobileLineListDto> items)
|
|
{
|
|
foreach (var item in items ?? Enumerable.Empty<MobileLineListDto>())
|
|
{
|
|
var context = OperadoraContaResolver.Resolve(item.Conta);
|
|
item.ContaEmpresa = context.Empresa;
|
|
item.Operadora = context.Operadora;
|
|
}
|
|
}
|
|
|
|
private static MobileLineDetailDto ToDetailDto(MobileLine x, VigenciaLine? vigencia = null) => new()
|
|
{
|
|
Id = x.Id,
|
|
Item = x.Item,
|
|
ContaEmpresa = FindEmpresaByConta(x.Conta),
|
|
Conta = x.Conta,
|
|
Linha = x.Linha,
|
|
Chip = x.Chip,
|
|
Cliente = x.Cliente,
|
|
Usuario = x.Usuario,
|
|
CentroDeCustos = x.CentroDeCustos,
|
|
SetorId = x.SetorId,
|
|
SetorNome = x.Setor?.Nome,
|
|
AparelhoId = x.AparelhoId,
|
|
AparelhoNome = x.Aparelho?.Nome,
|
|
AparelhoCor = x.Aparelho?.Cor,
|
|
AparelhoImei = x.Aparelho?.Imei,
|
|
AparelhoNotaFiscalTemArquivo = !string.IsNullOrWhiteSpace(x.Aparelho?.NotaFiscalArquivoPath),
|
|
AparelhoReciboTemArquivo = !string.IsNullOrWhiteSpace(x.Aparelho?.ReciboArquivoPath),
|
|
PlanoContrato = x.PlanoContrato,
|
|
FranquiaVivo = x.FranquiaVivo,
|
|
ValorPlanoVivo = x.ValorPlanoVivo,
|
|
GestaoVozDados = x.GestaoVozDados,
|
|
Skeelo = x.Skeelo,
|
|
VivoNewsPlus = x.VivoNewsPlus,
|
|
VivoTravelMundo = x.VivoTravelMundo,
|
|
VivoSync = x.VivoSync,
|
|
VivoGestaoDispositivo = x.VivoGestaoDispositivo,
|
|
ValorContratoVivo = x.ValorContratoVivo,
|
|
FranquiaLine = x.FranquiaLine,
|
|
FranquiaGestao = x.FranquiaGestao,
|
|
LocacaoAp = x.LocacaoAp,
|
|
ValorContratoLine = x.ValorContratoLine,
|
|
Desconto = x.Desconto,
|
|
Lucro = x.Lucro,
|
|
Status = x.Status,
|
|
DataBloqueio = x.DataBloqueio,
|
|
Skil = x.Skil,
|
|
Modalidade = x.Modalidade,
|
|
Cedente = x.Cedente,
|
|
Solicitante = x.Solicitante,
|
|
DataEntregaOpera = x.DataEntregaOpera,
|
|
DataEntregaCliente = x.DataEntregaCliente,
|
|
DtEfetivacaoServico = vigencia?.DtEfetivacaoServico,
|
|
DtTerminoFidelizacao = vigencia?.DtTerminoFidelizacao,
|
|
VencConta = x.VencConta,
|
|
TipoDeChip = x.TipoDeChip
|
|
};
|
|
|
|
private static void ApplyReservaRule(MobileLine x)
|
|
{
|
|
if (IsReservaValue(x.Cliente)) x.Cliente = "RESERVA";
|
|
if (IsReservaValue(x.Usuario)) x.Usuario = "RESERVA";
|
|
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);
|
|
|
|
private static int GetCol(Dictionary<string, int> map, string name)
|
|
=> map.TryGetValue(NormalizeHeader(name), out var c) ? c : 0;
|
|
|
|
private Guid? GetTenantIdFromClaims()
|
|
{
|
|
var claim = User?.FindFirst("tenantId")?.Value
|
|
?? User?.FindFirst("tenant")?.Value;
|
|
return Guid.TryParse(claim, out var tenantId) ? tenantId : null;
|
|
}
|
|
|
|
private static int GetColAny(Dictionary<string, int> map, params string[] headers)
|
|
{
|
|
foreach (var h in headers)
|
|
{
|
|
var k = NormalizeHeader(h);
|
|
if (map.TryGetValue(k, out var c)) return c;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
private static string GetCellByHeader(IXLWorksheet ws, int row, Dictionary<string, int> map, string header)
|
|
{
|
|
var k = NormalizeHeader(header);
|
|
return map.TryGetValue(k, out var c) ? GetCellString(ws, row, c) : "";
|
|
}
|
|
|
|
private static string GetCellByHeaderAny(IXLWorksheet ws, int row, Dictionary<string, int> map, params string[] headers)
|
|
{
|
|
foreach (var h in headers)
|
|
{
|
|
var k = NormalizeHeader(h);
|
|
if (map.TryGetValue(k, out var c))
|
|
return GetCellString(ws, row, c);
|
|
}
|
|
return "";
|
|
}
|
|
|
|
private static string GetCellString(IXLWorksheet ws, int row, int col)
|
|
{
|
|
if (col <= 0) return "";
|
|
return (ws.Cell(row, col).GetValue<string>() ?? "").Trim();
|
|
}
|
|
|
|
private static string GetFirstNonEmptyCellInRange(IXLWorksheet ws, int row, int startCol, int endCol)
|
|
{
|
|
if (endCol < startCol) return "";
|
|
for (int col = startCol; col <= endCol; col++)
|
|
{
|
|
var value = GetCellString(ws, row, col);
|
|
if (!string.IsNullOrWhiteSpace(value)) return value;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
private sealed class UserDataClientByLine
|
|
{
|
|
public string Linha { get; set; } = "";
|
|
public string Cliente { get; set; } = "";
|
|
}
|
|
|
|
private sealed class UserDataClientByItem
|
|
{
|
|
public int Item { get; set; }
|
|
public string Cliente { get; set; } = "";
|
|
}
|
|
|
|
private IQueryable<UserDataClientByLine> BuildUserDataClientByLineQuery()
|
|
{
|
|
return _db.UserDatas
|
|
.AsNoTracking()
|
|
.Where(x => x.Linha != null && x.Linha != "")
|
|
.Where(x => x.Cliente != null && x.Cliente != "")
|
|
.Where(x => !EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"))
|
|
.GroupBy(x => x.Linha!)
|
|
.Select(g => new UserDataClientByLine
|
|
{
|
|
Linha = g.Key,
|
|
Cliente = g.Max(x => x.Cliente!)!
|
|
});
|
|
}
|
|
|
|
private IQueryable<UserDataClientByItem> BuildUserDataClientByItemQuery()
|
|
{
|
|
return _db.UserDatas
|
|
.AsNoTracking()
|
|
.Where(x => x.Item > 0)
|
|
.Where(x => x.Cliente != null && x.Cliente != "")
|
|
.Where(x => !EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA"))
|
|
.GroupBy(x => x.Item)
|
|
.Select(g => new UserDataClientByItem
|
|
{
|
|
Item = g.Key,
|
|
Cliente = g.Max(x => x.Cliente!)!
|
|
});
|
|
}
|
|
|
|
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(
|
|
IQueryable<MobileLine> query,
|
|
string? additionalMode,
|
|
string? additionalServices)
|
|
{
|
|
var mode = (additionalMode ?? "").Trim().ToLowerInvariant();
|
|
var withOnly = mode is "with" or "com";
|
|
var withoutOnly = mode is "without" or "sem";
|
|
|
|
var selected = ParseAdditionalServices(additionalServices);
|
|
var hasServiceFilter = selected.Count > 0;
|
|
|
|
var includeGvd = selected.Contains("gvd");
|
|
var includeSkeelo = selected.Contains("skeelo");
|
|
var includeNews = selected.Contains("news");
|
|
var includeTravel = selected.Contains("travel");
|
|
var includeSync = selected.Contains("sync");
|
|
var includeDispositivo = selected.Contains("dispositivo");
|
|
|
|
if (hasServiceFilter)
|
|
{
|
|
if (withoutOnly)
|
|
{
|
|
return query.Where(x =>
|
|
(!includeGvd || (x.GestaoVozDados ?? 0m) <= 0m) &&
|
|
(!includeSkeelo || (x.Skeelo ?? 0m) <= 0m) &&
|
|
(!includeNews || (x.VivoNewsPlus ?? 0m) <= 0m) &&
|
|
(!includeTravel || (x.VivoTravelMundo ?? 0m) <= 0m) &&
|
|
(!includeSync || (x.VivoSync ?? 0m) <= 0m) &&
|
|
(!includeDispositivo || (x.VivoGestaoDispositivo ?? 0m) <= 0m));
|
|
}
|
|
|
|
// "with" e também "all" com serviços selecionados:
|
|
// filtra linhas que tenham qualquer um dos adicionais selecionados com valor > 0.
|
|
return query.Where(x =>
|
|
(includeGvd && (x.GestaoVozDados ?? 0m) > 0m) ||
|
|
(includeSkeelo && (x.Skeelo ?? 0m) > 0m) ||
|
|
(includeNews && (x.VivoNewsPlus ?? 0m) > 0m) ||
|
|
(includeTravel && (x.VivoTravelMundo ?? 0m) > 0m) ||
|
|
(includeSync && (x.VivoSync ?? 0m) > 0m) ||
|
|
(includeDispositivo && (x.VivoGestaoDispositivo ?? 0m) > 0m));
|
|
}
|
|
|
|
if (withOnly)
|
|
{
|
|
return query.Where(x =>
|
|
(x.GestaoVozDados ?? 0m) > 0m ||
|
|
(x.Skeelo ?? 0m) > 0m ||
|
|
(x.VivoNewsPlus ?? 0m) > 0m ||
|
|
(x.VivoTravelMundo ?? 0m) > 0m ||
|
|
(x.VivoSync ?? 0m) > 0m ||
|
|
(x.VivoGestaoDispositivo ?? 0m) > 0m);
|
|
}
|
|
|
|
if (withoutOnly)
|
|
{
|
|
return query.Where(x =>
|
|
(x.GestaoVozDados ?? 0m) <= 0m &&
|
|
(x.Skeelo ?? 0m) <= 0m &&
|
|
(x.VivoNewsPlus ?? 0m) <= 0m &&
|
|
(x.VivoTravelMundo ?? 0m) <= 0m &&
|
|
(x.VivoSync ?? 0m) <= 0m &&
|
|
(x.VivoGestaoDispositivo ?? 0m) <= 0m);
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
private static HashSet<string> ParseAdditionalServices(string? raw)
|
|
{
|
|
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
if (string.IsNullOrWhiteSpace(raw)) return set;
|
|
|
|
var chunks = raw.Split(new[] { ',', ';', '|', ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
|
foreach (var chunk in chunks)
|
|
{
|
|
var normalized = NormalizeAdditionalServiceKey(chunk);
|
|
if (!string.IsNullOrWhiteSpace(normalized))
|
|
set.Add(normalized);
|
|
}
|
|
|
|
return set;
|
|
}
|
|
|
|
private static string NormalizeAdditionalServiceKey(string? raw)
|
|
{
|
|
var key = (raw ?? "").Trim().ToLowerInvariant()
|
|
.Replace("-", "")
|
|
.Replace("_", "")
|
|
.Replace(" ", "");
|
|
|
|
return key switch
|
|
{
|
|
"gvd" => "gvd",
|
|
"gestaovozedados" => "gvd",
|
|
"gestaovozdados" => "gvd",
|
|
|
|
"skeelo" => "skeelo",
|
|
|
|
"news" => "news",
|
|
"vivonewsplus" => "news",
|
|
|
|
"travel" => "travel",
|
|
"vivotravelmundo" => "travel",
|
|
|
|
"sync" => "sync",
|
|
"vivosync" => "sync",
|
|
|
|
"dispositivo" => "dispositivo",
|
|
"gestaodispositivo" => "dispositivo",
|
|
"vivogestaodispositivo" => "dispositivo",
|
|
|
|
_ => string.Empty
|
|
};
|
|
}
|
|
|
|
private static DateTime? TryDate(IXLWorksheet ws, int row, Dictionary<string, int> map, string header)
|
|
{
|
|
var k = NormalizeHeader(header);
|
|
if (!map.TryGetValue(k, out var c)) return null;
|
|
return TryDateCell(ws, row, c);
|
|
}
|
|
|
|
private static DateTime? TryDateNoUtc(IXLWorksheet ws, int row, Dictionary<string, int> map, string header)
|
|
{
|
|
var k = NormalizeHeader(header);
|
|
if (!map.TryGetValue(k, out var c)) return null;
|
|
return TryDateCellNoUtc(ws, row, c);
|
|
}
|
|
|
|
private static DateTime? TryDateCell(IXLWorksheet ws, int row, int col)
|
|
{
|
|
if (col <= 0) return null;
|
|
|
|
var cell = ws.Cell(row, col);
|
|
|
|
if (cell.DataType == XLDataType.DateTime)
|
|
return ToUtc(cell.GetDateTime());
|
|
|
|
if (cell.TryGetValue<DateTime>(out var dt))
|
|
return ToUtc(dt);
|
|
|
|
var s = cell.GetValue<string>()?.Trim();
|
|
if (string.IsNullOrWhiteSpace(s)) return null;
|
|
|
|
if (DateTime.TryParse(s, new CultureInfo("pt-BR"), DateTimeStyles.None, out var d))
|
|
return ToUtc(d);
|
|
|
|
return null;
|
|
}
|
|
|
|
private static DateTime? TryDateCellNoUtc(IXLWorksheet ws, int row, int col)
|
|
{
|
|
if (col <= 0) return null;
|
|
|
|
var cell = ws.Cell(row, col);
|
|
|
|
if (cell.DataType == XLDataType.DateTime)
|
|
return ToUtcDateOnly(cell.GetDateTime());
|
|
|
|
if (cell.TryGetValue<DateTime>(out var dt))
|
|
return ToUtcDateOnly(dt);
|
|
|
|
var s = cell.GetValue<string>()?.Trim();
|
|
if (string.IsNullOrWhiteSpace(s)) return null;
|
|
|
|
if (DateTime.TryParse(s, new CultureInfo("pt-BR"), DateTimeStyles.None, out var d))
|
|
return ToUtcDateOnly(d);
|
|
|
|
return null;
|
|
}
|
|
|
|
private static DateTime ToUtcDateOnly(DateTime dt)
|
|
{
|
|
return new DateTime(dt.Year, dt.Month, dt.Day, 12, 0, 0, DateTimeKind.Utc);
|
|
}
|
|
|
|
private static decimal? TryDecimal(string? input)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(input)) return null;
|
|
|
|
var normalized = NormalizeDecimalInput(input);
|
|
if (string.IsNullOrWhiteSpace(normalized)) return null;
|
|
|
|
return decimal.TryParse(normalized, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)
|
|
? value
|
|
: null;
|
|
}
|
|
|
|
private static string NormalizeDecimalInput(string raw)
|
|
{
|
|
var s = raw
|
|
.Replace("R$", "", StringComparison.OrdinalIgnoreCase)
|
|
.Replace("%", "", StringComparison.OrdinalIgnoreCase)
|
|
.Replace(" ", "")
|
|
.Replace("\u00A0", "")
|
|
.Trim();
|
|
|
|
if (string.IsNullOrWhiteSpace(s)) return string.Empty;
|
|
|
|
var negativeByParentheses = s.StartsWith("(") && s.EndsWith(")");
|
|
if (negativeByParentheses && s.Length >= 2)
|
|
{
|
|
s = s[1..^1];
|
|
}
|
|
|
|
var allowed = new StringBuilder(s.Length);
|
|
foreach (var ch in s)
|
|
{
|
|
if (char.IsDigit(ch) || ch == '.' || ch == ',' || ch == '-' || ch == '+' || ch == 'e' || ch == 'E')
|
|
{
|
|
allowed.Append(ch);
|
|
}
|
|
}
|
|
|
|
s = allowed.ToString();
|
|
if (string.IsNullOrWhiteSpace(s)) return string.Empty;
|
|
|
|
string normalized;
|
|
var commaCount = s.Count(c => c == ',');
|
|
var dotCount = s.Count(c => c == '.');
|
|
|
|
if (s.IndexOf('e') >= 0 || s.IndexOf('E') >= 0)
|
|
{
|
|
normalized = s.Replace(",", ".");
|
|
}
|
|
else if (commaCount > 0 && dotCount > 0)
|
|
{
|
|
var lastComma = s.LastIndexOf(',');
|
|
var lastDot = s.LastIndexOf('.');
|
|
|
|
if (lastComma > lastDot)
|
|
{
|
|
// Ex.: 1.234,56 -> 1234.56
|
|
normalized = s.Replace(".", "").Replace(",", ".");
|
|
}
|
|
else
|
|
{
|
|
// Ex.: 1,234.56 -> 1234.56
|
|
normalized = s.Replace(",", "");
|
|
}
|
|
}
|
|
else if (commaCount > 0)
|
|
{
|
|
normalized = NormalizeSingleSeparatorNumber(s, ',');
|
|
}
|
|
else if (dotCount > 0)
|
|
{
|
|
normalized = NormalizeSingleSeparatorNumber(s, '.');
|
|
}
|
|
else
|
|
{
|
|
normalized = s;
|
|
}
|
|
|
|
if (negativeByParentheses && !normalized.StartsWith("-"))
|
|
{
|
|
normalized = "-" + normalized;
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
private static string NormalizeSingleSeparatorNumber(string value, char separator)
|
|
{
|
|
var separatorCount = value.Count(c => c == separator);
|
|
if (separatorCount <= 0) return value;
|
|
|
|
if (separatorCount == 1)
|
|
{
|
|
var idx = value.IndexOf(separator);
|
|
var leftDigits = value[..idx].Count(char.IsDigit);
|
|
var rightDigits = value[(idx + 1)..].Count(char.IsDigit);
|
|
|
|
// "1.234"/"1,234" normalmente representa milhar.
|
|
var looksLikeThousandsSeparator = rightDigits == 3 && leftDigits > 0 && leftDigits <= 3;
|
|
if (looksLikeThousandsSeparator)
|
|
{
|
|
return value.Replace(separator.ToString(), "");
|
|
}
|
|
|
|
return separator == ',' ? value.Replace(",", ".") : value;
|
|
}
|
|
|
|
var parts = value.Split(separator);
|
|
var allGroupsAreThousands = parts.Skip(1).All(p => p.Length == 3 && p.All(char.IsDigit));
|
|
if (allGroupsAreThousands)
|
|
{
|
|
return value.Replace(separator.ToString(), "");
|
|
}
|
|
|
|
var last = value.LastIndexOf(separator);
|
|
var whole = value[..last].Replace(separator.ToString(), "");
|
|
var fraction = value[(last + 1)..];
|
|
return $"{whole}.{fraction}";
|
|
}
|
|
|
|
private static int TryInt(string s)
|
|
=> int.TryParse(OnlyDigits(s), out var n) ? n : 0;
|
|
|
|
private static int? TryNullableInt(string? s)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(s)) return null;
|
|
var d = OnlyDigits(s);
|
|
if (string.IsNullOrWhiteSpace(d)) return null;
|
|
return int.TryParse(d, out var n) ? n : null;
|
|
}
|
|
|
|
private static string OnlyDigits(string? s)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(s)) return "";
|
|
var sb = new StringBuilder();
|
|
foreach (var c in s) if (char.IsDigit(c)) sb.Append(c);
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string? NullIfEmptyDigits(string? s)
|
|
{
|
|
var d = OnlyDigits(s);
|
|
return string.IsNullOrWhiteSpace(d) ? null : d;
|
|
}
|
|
|
|
private static string NormalizeHeader(string? s)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(s)) return "";
|
|
s = s.Trim().ToUpperInvariant().Normalize(NormalizationForm.FormD);
|
|
|
|
var sb = new StringBuilder();
|
|
foreach (var c in s)
|
|
if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark)
|
|
sb.Append(c);
|
|
|
|
return sb.ToString()
|
|
.Normalize(NormalizationForm.FormC)
|
|
.Replace(" ", "")
|
|
.Replace("\t", "")
|
|
.Replace("\n", "")
|
|
.Replace("\r", "");
|
|
}
|
|
|
|
// ==========================================================
|
|
// ✅ BILLING HELPERS
|
|
// ==========================================================
|
|
private static int GetLastUsedColumn(IXLWorksheet ws, int headerRowIndex)
|
|
{
|
|
var row = ws.Row(headerRowIndex);
|
|
var last = row.LastCellUsed()?.Address.ColumnNumber ?? 1;
|
|
var last2 = ws.LastColumnUsed()?.ColumnNumber() ?? last;
|
|
return Math.Max(last, last2);
|
|
}
|
|
|
|
private static bool RowHasAnyText(IXLRow row)
|
|
{
|
|
foreach (var c in row.CellsUsed())
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(c.GetValue<string>()))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static int FindColByAny(IXLRow headerRow, int lastCol, params string[] headers)
|
|
{
|
|
var wanted = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var h in headers) wanted.Add(NormalizeHeader(h));
|
|
|
|
for (int col = 1; col <= lastCol; col++)
|
|
{
|
|
var key = NormalizeHeader(headerRow.Cell(col).GetString());
|
|
if (string.IsNullOrWhiteSpace(key)) continue;
|
|
|
|
if (wanted.Contains(key)) return col;
|
|
|
|
foreach (var w in wanted)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(w) && key.Contains(w))
|
|
return col;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
private static string GetMergedGroupKeyAt(IXLRow groupRow, int col)
|
|
{
|
|
for (int c = col; c >= 1; c--)
|
|
{
|
|
var g = NormalizeHeader(groupRow.Cell(c).GetString());
|
|
if (!string.IsNullOrWhiteSpace(g))
|
|
return g;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
private static int FindColInGroup(IXLRow groupRow, IXLRow headerRow, int lastCol, string groupKey, params string[] headerKeys)
|
|
{
|
|
var gk = NormalizeHeader(groupKey);
|
|
var wanted = headerKeys.Select(NormalizeHeader).ToArray();
|
|
|
|
for (int col = 1; col <= lastCol; col++)
|
|
{
|
|
var groupAtCol = GetMergedGroupKeyAt(groupRow, col);
|
|
if (string.IsNullOrWhiteSpace(groupAtCol)) continue;
|
|
|
|
if (!groupAtCol.Contains(gk)) continue;
|
|
|
|
var h = NormalizeHeader(headerRow.Cell(col).GetString());
|
|
|
|
if (string.IsNullOrWhiteSpace(h) && wanted.Any(w => w == ""))
|
|
return col;
|
|
|
|
foreach (var w in wanted)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(w)) continue;
|
|
|
|
if (h == w) return col;
|
|
if (!string.IsNullOrWhiteSpace(h) && h.Contains(w)) return col;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
}
|