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.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; namespace line_gestao_api.Controllers { [ApiController] [Route("api/[controller]")] //[Authorize] public class LinesController : ControllerBase { private readonly AppDbContext _db; private readonly ParcelamentosImportService _parcelamentosImportService; public LinesController(AppDbContext db, ParcelamentosImportService parcelamentosImportService) { _db = db; _parcelamentosImportService = parcelamentosImportService; } public class ImportExcelForm { public IFormFile File { get; set; } = default!; } // ========================================================== // ✅ 1. ENDPOINT: AGRUPAR POR CLIENTE // ========================================================== [HttpGet("groups")] public async Task>> GetClientGroups( [FromQuery] string? skil, [FromQuery] string? search, [FromQuery] int page = 1, [FromQuery] int pageSize = 10) { page = page < 1 ? 1 : page; pageSize = pageSize < 1 ? 10 : pageSize; var query = _db.MobileLines.AsNoTracking().Where(x => !string.IsNullOrEmpty(x.Cliente)); // Filtro SKIL if (!string.IsNullOrWhiteSpace(skil)) { var sSkil = skil.Trim(); if (sSkil.Equals("RESERVA", StringComparison.OrdinalIgnoreCase)) query = query.Where(x => x.Skil == "RESERVA" || EF.Functions.ILike(x.Skil ?? "", "%RESERVA%")); else query = query.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%")); } // Filtro SEARCH (Busca pelo Nome do Cliente) if (!string.IsNullOrWhiteSpace(search)) { var s = search.Trim(); query = query.Where(x => EF.Functions.ILike(x.Cliente ?? "", $"%{s}%")); } var 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 items = await groupedQuery .OrderBy(x => x.Cliente) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return Ok(new PagedResult { Page = page, PageSize = pageSize, Total = totalGroups, Items = items }); } // ========================================================== // ✅ 2. ENDPOINT: LISTAR NOMES DE CLIENTES (ACEITA SKIL) // ========================================================== [HttpGet("clients")] public async Task>> GetClients([FromQuery] string? skil) { var query = _db.MobileLines.AsNoTracking(); if (!string.IsNullOrWhiteSpace(skil)) { var sSkil = skil.Trim(); if (sSkil.Equals("RESERVA", StringComparison.OrdinalIgnoreCase)) query = query.Where(x => x.Skil == "RESERVA" || EF.Functions.ILike(x.Skil ?? "", "%RESERVA%")); else query = query.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%")); } var 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>> 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 { Page = page, PageSize = pageSize, Total = total, Items = items }); } [HttpGet("billing/clients")] public async Task>> 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); } // ========================================================== // ✅ 2.1 ENDPOINT: LINHAS POR CLIENTE (para SELECT do MUREG) // GET: /api/lines/by-client?cliente=... // ========================================================== [HttpGet("by-client")] public async Task>> GetLinesByClient([FromQuery] string cliente) { if (string.IsNullOrWhiteSpace(cliente)) return Ok(new List()); 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>> GetAll( [FromQuery] string? search, [FromQuery] string? skil, [FromQuery] string? client, [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(); if (!string.IsNullOrWhiteSpace(skil)) { var sSkil = skil.Trim(); if (sSkil.Equals("RESERVA", StringComparison.OrdinalIgnoreCase)) q = q.Where(x => x.Skil == "RESERVA" || EF.Functions.ILike(x.Skil ?? "", "%RESERVA%")); else q = q.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%")); } 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(); 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"; 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, PlanoContrato = x.PlanoContrato, Status = x.Status, Skil = x.Skil, Modalidade = x.Modalidade, VencConta = x.VencConta }) .ToListAsync(); return Ok(new PagedResult { Page = page, PageSize = pageSize, Total = total, Items = items }); } // ========================================================== // ✅ 4. GET BY ID // ========================================================== [HttpGet("{id:guid}")] public async Task> GetById(Guid id) { var x = await _db.MobileLines.AsNoTracking().FirstOrDefaultAsync(a => a.Id == id); if (x == null) return NotFound(); return Ok(ToDetailDto(x)); } // ========================================================== // ✅ 5. CREATE // ========================================================== [HttpPost] public async Task> 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." }); 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 exists = await _db.MobileLines.AsNoTracking().AnyAsync(x => x.Linha == linhaLimpa); if (exists) return Conflict(new { message = $"A linha {req.Linha} já está cadastrada no sistema." }); var maxItem = await _db.MobileLines.MaxAsync(x => (int?)x.Item) ?? 0; var nextItem = maxItem + 1; var now = DateTime.UtcNow; var newLine = new MobileLine { Id = Guid.NewGuid(), Item = nextItem, Cliente = req.Cliente.Trim().ToUpper(), Linha = linhaLimpa, Chip = string.IsNullOrWhiteSpace(chipLimpo) ? null : chipLimpo, Usuario = req.Usuario?.Trim(), Status = req.Status?.Trim(), Skil = req.Skil?.Trim(), Modalidade = req.Modalidade?.Trim(), PlanoContrato = req.PlanoContrato?.Trim(), Conta = req.Conta?.Trim(), VencConta = req.VencConta?.Trim(), DataBloqueio = ToUtc(req.DataBloqueio), DataEntregaOpera = ToUtc(req.DataEntregaOpera), DataEntregaCliente = ToUtc(req.DataEntregaCliente), Cedente = req.Cedente?.Trim(), Solicitante = req.Solicitante?.Trim(), FranquiaVivo = req.FranquiaVivo, ValorPlanoVivo = req.ValorPlanoVivo, GestaoVozDados = req.GestaoVozDados, Skeelo = req.Skeelo, VivoNewsPlus = req.VivoNewsPlus, VivoTravelMundo = req.VivoTravelMundo, VivoGestaoDispositivo = req.VivoGestaoDispositivo, ValorContratoVivo = req.ValorContratoVivo, FranquiaLine = req.FranquiaLine, FranquiaGestao = req.FranquiaGestao, LocacaoAp = req.LocacaoAp, ValorContratoLine = req.ValorContratoLine, Desconto = req.Desconto, Lucro = req.Lucro, CreatedAt = now, UpdatedAt = now }; ApplyReservaRule(newLine); _db.MobileLines.Add(newLine); try { await _db.SaveChangesAsync(); } catch (DbUpdateException) { return StatusCode(500, new { message = "Erro ao salvar no banco de dados." }); } return CreatedAtAction(nameof(GetById), new { id = newLine.Id }, ToDetailDto(newLine)); } // ========================================================== // ✅ 6. UPDATE // ========================================================== [HttpPut("{id:guid}")] public async Task Update(Guid id, [FromBody] UpdateMobileLineRequest req) { var x = await _db.MobileLines.FirstOrDefaultAsync(a => a.Id == id); if (x == null) return NotFound(); 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 }); } 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 = req.Cliente?.Trim(); x.Usuario = req.Usuario?.Trim(); x.PlanoContrato = req.PlanoContrato?.Trim(); 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.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 = req.Status?.Trim(); x.DataBloqueio = ToUtc(req.DataBloqueio); x.Skil = req.Skil?.Trim(); x.Modalidade = req.Modalidade?.Trim(); x.Cedente = req.Cedente?.Trim(); x.Solicitante = req.Solicitante?.Trim(); x.DataEntregaOpera = ToUtc(req.DataEntregaOpera); x.DataEntregaCliente = ToUtc(req.DataEntregaCliente); x.VencConta = req.VencConta?.Trim(); ApplyReservaRule(x); x.UpdatedAt = DateTime.UtcNow; try { await _db.SaveChangesAsync(); } catch (DbUpdateException) { return Conflict(new { message = "Conflito ao salvar." }); } return NoContent(); } // ========================================================== // ✅ 7. DELETE // ========================================================== [HttpDelete("{id:guid}")] public async Task 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(); return NoContent(); } // ========================================================== // ✅ 8. IMPORT EXCEL // ========================================================== [HttpPost("import-excel")] [Authorize] [Consumes("multipart/form-data")] [RequestSizeLimit(50_000_000)] public async Task> ImportExcel([FromForm] ImportExcelForm form) { var file = form.File; if (file == null || file.Length == 0) return BadRequest("Arquivo inválido."); await using var tx = await _db.Database.BeginTransactionAsync(); 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(600); var imported = 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 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 = TryInt(itemStr), 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")), 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"), CreatedAt = now, UpdatedAt = now }; ApplyReservaRule(e); buffer.Add(e); imported++; 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(); } // ========================= // ✅ 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) // ========================= await ImportUserDatasFromWorkbook(wb); // ========================= // ✅ 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 // ========================= await ImportResumoFromWorkbook(wb); // ========================= // ✅ IMPORTA PARCELAMENTOS // ========================= var parcelamentosSummary = await _parcelamentosImportService.ImportFromWorkbookAsync(wb, replaceAll: true); 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 }); } } // ========================================================== // ✅ 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 => r.CellsUsed().Any(c => NormalizeHeader(c.GetString()) == "ITEM")); if (headerRow == null) return; var map = BuildHeaderMap(headerRow); int colItem = GetCol(map, "ITEM"); if (colItem == 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.Linha, x.Chip }) .ToListAsync(); var mobileByLinha = new Dictionary(StringComparer.Ordinal); var mobileByChip = new Dictionary(StringComparer.Ordinal); foreach (var m in mobilePairs) { 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(); var buffer = new List(600); var lastRow = wsM.LastRowUsed()?.RowNumber() ?? startRow; for (int r = startRow; r <= lastRow; r++) { var itemStr = GetCellString(wsM, r, colItem); if (string.IsNullOrWhiteSpace(itemStr)) break; var linhaAntiga = NullIfEmptyDigits(GetCellByHeader(wsM, r, map, "LINHA ANTIGA")); var linhaNova = NullIfEmptyDigits(GetCellByHeader(wsM, r, map, "LINHA NOVA")); var iccid = NullIfEmptyDigits(GetCellByHeader(wsM, r, map, "ICCID")); var dataMureg = TryDate(wsM, r, map, "DATA DA MUREG"); // ✅ resolve MobileLineId (prioridade: LinhaAntiga, depois ICCID) Guid mobileLineId = Guid.Empty; if (!string.IsNullOrWhiteSpace(linhaAntiga) && mobileByLinha.TryGetValue(linhaAntiga, out var idPorLinha)) mobileLineId = idPorLinha; else if (!string.IsNullOrWhiteSpace(iccid) && mobileByChip.TryGetValue(iccid, out var idPorChip)) mobileLineId = idPorChip; // 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 = TryInt(itemStr), 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(); // 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) { var map = BuildHeaderMap(headerRow); 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 startRow = headerRowIndex + 1; var lastRow = ws.LastRowUsed()?.RowNumber() ?? startRow; var buffer = new List(400); var seqItem = 0; for (int r = startRow; r <= lastRow; r++) { var cliente = GetCellString(ws, r, colCliente); 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 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; var headerRow = ws.RowsUsed().FirstOrDefault(r => r.CellsUsed().Any(c => NormalizeHeader(c.GetString()) == "ITEM" || NormalizeHeader(c.GetString()) == "CLIENTE")); if (headerRow == null) return; var map = BuildHeaderMap(headerRow); var colItem = GetCol(map, "ITEM"); var colCliente = GetCol(map, "CLIENTE"); var colLinha = GetCol(map, "LINHA"); if (colCliente == 0) return; await _db.UserDatas.ExecuteDeleteAsync(); var startRow = headerRow.RowNumber() + 1; var lastRow = ws.LastRowUsed()?.RowNumber() ?? startRow; var buffer = new List(500); var seq = 0; var colCpf = GetColAny(map, "CPF"); 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 = GetCellString(ws, r, colCliente); if (string.IsNullOrWhiteSpace(cliente)) break; 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 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 e = new UserData { Id = Guid.NewGuid(), Item = item, Linha = linha, Cliente = cliente.Trim(), 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(); } } // ========================================================== // ✅ 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(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(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(); 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(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(); 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) { 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.ResumoReservaLines.ExecuteDeleteAsync(); await _db.ResumoReservaTotals.ExecuteDeleteAsync(); var now = DateTime.UtcNow; await ImportResumoTabela1(ws, now); await ImportResumoTabela2(ws, now); await ImportResumoTabela3(ws, now); await ImportResumoTabela4(ws, now); await ImportResumoTabela5(ws, now); await ImportResumoTabela6(ws, now); } private async Task ImportResumoTabela1(IXLWorksheet ws, DateTime now) { const int headerRow = 5; const int totalRow = 72; var lastRow = Math.Min(totalRow - 1, ws.LastRowUsed()?.RowNumber() ?? totalRow - 1); 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(200); for (int r = headerRow + 1; r <= lastRow; 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); if (string.IsNullOrWhiteSpace(plano) && string.IsNullOrWhiteSpace(gb) && string.IsNullOrWhiteSpace(valorInd) && string.IsNullOrWhiteSpace(franquia) && string.IsNullOrWhiteSpace(totalLinhas) && string.IsNullOrWhiteSpace(valorTotal)) { continue; } 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(plano) ? null : plano.Trim(), Gb = TryDecimal(gb), ValorIndividualComSvas = TryDecimal(valorInd), FranquiaGb = TryDecimal(franquia), TotalLinhas = TryNullableInt(totalLinhas), ValorTotal = TryDecimal(valorTotal), VivoTravel = vivoTravel, CreatedAt = now, UpdatedAt = now }); } if (buffer.Count > 0) { await _db.ResumoMacrophonyPlans.AddRangeAsync(buffer); await _db.SaveChangesAsync(); } var total = new ResumoMacrophonyTotal { FranquiaGbTotal = TryDecimal(GetCellString(ws, totalRow, colFranquiaGb)), TotalLinhasTotal = TryNullableInt(GetCellString(ws, totalRow, colTotalLinhas)), ValorTotal = TryDecimal(GetCellString(ws, totalRow, colValorTotal)), CreatedAt = now, UpdatedAt = now }; await _db.ResumoMacrophonyTotals.AddAsync(total); await _db.SaveChangesAsync(); } private async Task ImportResumoTabela2(IXLWorksheet ws, DateTime now) { 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(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 total = new ResumoVivoLineTotal { QtdLinhasTotal = TryNullableInt(GetCellString(ws, totalRow, colQtdLinhas)), 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(); 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(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) { const int headerRow = 74; const int totalRow = 81; var lastRow = Math.Min(totalRow - 1, ws.LastRowUsed()?.RowNumber() ?? totalRow - 1); 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(200); for (int r = headerRow + 1; r <= lastRow; 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); if (string.IsNullOrWhiteSpace(plano) && string.IsNullOrWhiteSpace(gb) && string.IsNullOrWhiteSpace(valorInd) && string.IsNullOrWhiteSpace(franquia) && string.IsNullOrWhiteSpace(totalLinhas) && string.IsNullOrWhiteSpace(valorTotal)) { continue; } buffer.Add(new ResumoPlanoContratoResumo { PlanoContrato = string.IsNullOrWhiteSpace(plano) ? null : plano.Trim(), 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(); } var total = new ResumoPlanoContratoTotal { ValorTotal = TryDecimal(ws.Cell(totalRow, 7).GetString()), CreatedAt = now, UpdatedAt = now }; await _db.ResumoPlanoContratoTotals.AddAsync(total); await _db.SaveChangesAsync(); } private async Task ImportResumoTabela5(IXLWorksheet ws, DateTime now) { const int headerRow = 83; var map = BuildHeaderMap(ws.Row(headerRow)); var colValorTotalLine = GetColAny(map, "VALOR TOTAL LINE", "VALOR TOTAL LINE R$", "VALOR TOTAL LINE R$"); var colLucroTotalLine = GetColAny(map, "LUCRO TOTAL LINE", "LUCRO TOTAL LINE R$", "LUCRO TOTAL LINE R$"); var colQtdLinhas = GetColAny(map, "QTD. LINHAS", "QTD LINHAS", "QTD. DE LINHAS"); var buffer = new List(3); for (int r = headerRow + 1; r <= headerRow + 3; r++) { var tipo = ws.Cell(r, 2).GetString(); if (string.IsNullOrWhiteSpace(tipo)) { continue; } buffer.Add(new ResumoLineTotais { Tipo = tipo.Trim(), ValorTotalLine = TryDecimal(GetCellString(ws, r, colValorTotalLine)), LucroTotalLine = TryDecimal(GetCellString(ws, r, colLucroTotalLine)), QtdLinhas = TryNullableInt(GetCellString(ws, r, colQtdLinhas)), CreatedAt = now, UpdatedAt = now }); } if (buffer.Count > 0) { await _db.ResumoLineTotais.AddRangeAsync(buffer); await _db.SaveChangesAsync(); } } 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(200); decimal? lastTotal = null; string? lastDddValid = null; var dataStarted = false; var emptyRowStreak = 0; int? totalRowIndex = null; 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 franquiaValue = TryDecimal(franquia); var qtdValue = TryNullableInt(qtdLinhas); var isDataRow = franquiaValue.HasValue || qtdValue.HasValue; var dddCandidate = NullIfEmptyDigits(ddd); if (!string.IsNullOrWhiteSpace(dddCandidate)) { lastDddValid = dddCandidate; } var isTotalRow = !isDataRow && !string.IsNullOrWhiteSpace(total); if (isTotalRow) { totalRowIndex = r; break; } if (!isDataRow && dataStarted) { break; } if (isDataRow) dataStarted = true; var resolvedDdd = isDataRow ? (dddCandidate ?? lastDddValid) : dddCandidate; var totalValue = TryDecimal(total); if (!totalValue.HasValue && lastTotal.HasValue) { totalValue = lastTotal; } else if (totalValue.HasValue) { lastTotal = totalValue; } buffer.Add(new ResumoReservaLine { Ddd = string.IsNullOrWhiteSpace(resolvedDdd) ? null : resolvedDdd, FranquiaGb = franquiaValue, QtdLinhas = qtdValue, Total = totalValue, 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 (totalRowIndex == null) { return; } var totalEntity = new ResumoReservaTotal { 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(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 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; } // ========================================================== // HELPERS (SEUS) // ========================================================== private static Dictionary BuildHeaderMap(IXLRow headerRow) { var map = new Dictionary(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 BuildHeaderMapRange(IXLRow headerRow, int startCol, int endCol) { var map = new Dictionary(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 static MobileLineDetailDto ToDetailDto(MobileLine x) => new() { Id = x.Id, Item = x.Item, Conta = x.Conta, Linha = x.Linha, Chip = x.Chip, Cliente = x.Cliente, Usuario = x.Usuario, PlanoContrato = x.PlanoContrato, FranquiaVivo = x.FranquiaVivo, ValorPlanoVivo = x.ValorPlanoVivo, GestaoVozDados = x.GestaoVozDados, Skeelo = x.Skeelo, VivoNewsPlus = x.VivoNewsPlus, VivoTravelMundo = x.VivoTravelMundo, 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, VencConta = x.VencConta }; private static void ApplyReservaRule(MobileLine x) { if ((x.Cliente ?? "").Trim().ToUpper() == "RESERVA" || (x.Usuario ?? "").Trim().ToUpper() == "RESERVA") { x.Cliente = "RESERVA"; x.Usuario = "RESERVA"; x.Skil = "RESERVA"; } } private static int GetCol(Dictionary 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 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 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 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() ?? "").Trim(); } private static DateTime? TryDate(IXLWorksheet ws, int row, Dictionary 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 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(out var dt)) return ToUtc(dt); var s = cell.GetValue()?.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(out var dt)) return ToUtcDateOnly(dt); var s = cell.GetValue()?.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? s) { if (string.IsNullOrWhiteSpace(s)) return null; s = s.Replace("R$", "", StringComparison.OrdinalIgnoreCase).Trim(); if (decimal.TryParse(s, NumberStyles.Any, new CultureInfo("pt-BR"), out var d)) return d; if (decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out d)) return d; var s2 = s.Replace(".", "").Replace(",", "."); if (decimal.TryParse(s2, NumberStyles.Any, CultureInfo.InvariantCulture, out d)) return d; return null; } 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())) return true; } return false; } private static int FindColByAny(IXLRow headerRow, int lastCol, params string[] headers) { var wanted = new HashSet(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; } } }