diff --git a/Controllers/BillingController.cs b/Controllers/BillingController.cs new file mode 100644 index 0000000..0eb08fe --- /dev/null +++ b/Controllers/BillingController.cs @@ -0,0 +1,122 @@ +using line_gestao_api.Data; +using line_gestao_api.Dtos; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace line_gestao_api.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class BillingController : ControllerBase + { + private readonly AppDbContext _db; + public BillingController(AppDbContext db) => _db = db; + + [HttpGet] + public async Task>> GetAll( + [FromQuery] string? tipo = "PF", + [FromQuery] string? search = null, + [FromQuery] string? client = null, + [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; + + // ✅ FIX CS8072: calcula FORA do expression tree (sem ?. dentro do Where) + var t = string.Equals(tipo?.Trim(), "PJ", StringComparison.OrdinalIgnoreCase) ? "PJ" : "PF"; + + var q = _db.BillingClients.AsNoTracking() + .Where(x => x.Tipo == t); + + if (!string.IsNullOrWhiteSpace(search)) + { + var s = search.Trim(); + q = q.Where(x => EF.Functions.ILike(x.Cliente ?? "", $"%{s}%")); + } + + if (!string.IsNullOrWhiteSpace(client)) + { + var c = client.Trim(); + q = q.Where(x => EF.Functions.ILike(x.Cliente ?? "", c)); + } + + var total = await q.CountAsync(); + + var sb = (sortBy ?? "cliente").Trim().ToLowerInvariant(); + var desc = string.Equals((sortDir ?? "asc").Trim(), "desc", StringComparison.OrdinalIgnoreCase); + + // ✅ Ordenação mais estável (coalesce + ThenBy) + q = sb switch + { + "item" => desc ? q.OrderByDescending(x => x.Item) : q.OrderBy(x => x.Item), + + "qtdlinhas" => desc + ? q.OrderByDescending(x => x.QtdLinhas ?? 0).ThenBy(x => x.Cliente) + : q.OrderBy(x => x.QtdLinhas ?? 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), + + "valorcontratovivo" => desc + ? q.OrderByDescending(x => x.ValorContratoVivo ?? 0).ThenBy(x => x.Cliente) + : q.OrderBy(x => x.ValorContratoVivo ?? 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), + + _ => 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) + .Select(x => new BillingClientListDto + { + Id = x.Id, + Tipo = x.Tipo, + Item = x.Item, + Cliente = x.Cliente, + QtdLinhas = x.QtdLinhas, + FranquiaVivo = x.FranquiaVivo, + ValorContratoVivo = x.ValorContratoVivo, + FranquiaLine = x.FranquiaLine, + ValorContratoLine = x.ValorContratoLine, + Lucro = x.Lucro, + Aparelho = x.Aparelho, + FormaPagamento = x.FormaPagamento + }) + .ToListAsync(); + + return Ok(new PagedResult + { + Page = page, + PageSize = pageSize, + Total = total, + Items = items + }); + } + + [HttpGet("clients")] + public async Task>> GetClients([FromQuery] string? tipo = "PF") + { + // ✅ FIX CS8072: calcula FORA do expression tree + var t = string.Equals(tipo?.Trim(), "PJ", StringComparison.OrdinalIgnoreCase) ? "PJ" : "PF"; + + var clients = await _db.BillingClients.AsNoTracking() + .Where(x => x.Tipo == t && !string.IsNullOrEmpty(x.Cliente)) + .Select(x => x.Cliente!) + .Distinct() + .OrderBy(x => x) + .ToListAsync(); + + return Ok(clients); + } + } +} diff --git a/Controllers/LinesController.cs b/Controllers/LinesController.cs index e3f452f..bde4e5e 100644 --- a/Controllers/LinesController.cs +++ b/Controllers/LinesController.cs @@ -11,7 +11,7 @@ namespace line_gestao_api.Controllers { [ApiController] [Route("api/[controller]")] - //[Authorize] + //[Authorize] public class LinesController : ControllerBase { private readonly AppDbContext _db; @@ -66,8 +66,8 @@ namespace line_gestao_api.Controllers 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%")) + EF.Functions.ILike(x.Status ?? "", "%perda%") || + EF.Functions.ILike(x.Status ?? "", "%roubo%")) }); var totalGroups = await groupedQuery.CountAsync(); @@ -88,15 +88,108 @@ namespace line_gestao_api.Controllers } // ========================================================== - // ✅ 2. ENDPOINT: LISTAR NOMES DE CLIENTES + // ✅ 2. ENDPOINT: LISTAR NOMES DE CLIENTES (CORRIGIDO PARA ACEITAR SKIL) // ========================================================== [HttpGet("clients")] - public async Task>> GetClients() + public async Task>> GetClients([FromQuery] string? skil) { - var clients = await _db.MobileLines - .AsNoTracking() + var query = _db.MobileLines.AsNoTracking(); + + // APLICA O FILTRO DE SKIL ANTES DE SELECIONAR OS NOMES + 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); + } + + // ========================================================== + // ✅ NOVO: 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), + _ => 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) - .Where(x => !string.IsNullOrEmpty(x)) .Distinct() .OrderBy(x => x) .ToListAsync(); @@ -217,35 +310,29 @@ namespace line_gestao_api.Controllers [HttpPost] public async Task> Create([FromBody] CreateMobileLineDto req) { - // Validações Básicas 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." }); - // Sanitização da Linha e Chip (Remove máscara) var linhaLimpa = OnlyDigits(req.Linha); var chipLimpo = OnlyDigits(req.Chip); if (string.IsNullOrWhiteSpace(linhaLimpa)) return BadRequest(new { message = "Número de linha inválido." }); - // Verifica Duplicidade 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." }); - // Lógica de Auto-Incremento do Item - // Busca o maior ID (Item) atual e soma 1. Se não houver nenhum, começa do 1. var maxItem = await _db.MobileLines.MaxAsync(x => (int?)x.Item) ?? 0; var nextItem = maxItem + 1; - // Mapeamento DTO -> Entity var newLine = new MobileLine { Id = Guid.NewGuid(), - Item = nextItem, // Define o Item calculado automaticamente + Item = nextItem, Cliente = req.Cliente.Trim().ToUpper(), Linha = linhaLimpa, Chip = chipLimpo, @@ -257,16 +344,13 @@ namespace line_gestao_api.Controllers Conta = req.Conta?.Trim(), VencConta = req.VencConta?.Trim(), - // Datas DataBloqueio = ToUtc(req.DataBloqueio), DataEntregaOpera = ToUtc(req.DataEntregaOpera), DataEntregaCliente = ToUtc(req.DataEntregaCliente), - // Logística Cedente = req.Cedente?.Trim(), Solicitante = req.Solicitante?.Trim(), - // Financeiro FranquiaVivo = req.FranquiaVivo, ValorPlanoVivo = req.ValorPlanoVivo, GestaoVozDados = req.GestaoVozDados, @@ -286,7 +370,6 @@ namespace line_gestao_api.Controllers UpdatedAt = DateTime.UtcNow }; - // Aplica regra de negócio para Reserva (se necessário) ApplyReservaRule(newLine); _db.MobileLines.Add(newLine); @@ -300,7 +383,6 @@ namespace line_gestao_api.Controllers return StatusCode(500, new { message = "Erro ao salvar no banco de dados." }); } - // Retorna o objeto criado com o código 201 Created return CreatedAtAction(nameof(GetById), new { id = newLine.Id }, ToDetailDto(newLine)); } @@ -320,9 +402,6 @@ namespace line_gestao_api.Controllers if (exists) return Conflict(new { message = "Já existe registro com essa LINHA.", linha = newLinha }); } - // OBS: Não atualizamos x.Item aqui para garantir a integridade histórica. - // O Item é gerado na criação e não muda na edição. - x.Conta = req.Conta?.Trim(); x.Linha = newLinha; x.Chip = OnlyDigits(req.Chip); @@ -376,30 +455,56 @@ namespace line_gestao_api.Controllers } // ========================================================== - // ✅ 8. IMPORT EXCEL + // ✅ 8. IMPORT EXCEL (GERAL + MUREG + FATURAMENTO PF/PJ no mesmo upload) // ========================================================== [HttpPost("import-excel")] [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."); - using var stream = file.OpenReadStream(); using var wb = new XLWorkbook(stream); + var file = form.File; + if (file == null || file.Length == 0) return BadRequest("Arquivo inválido."); + + using var stream = file.OpenReadStream(); + using var wb = new XLWorkbook(stream); + + // ========================= + // ✅ IMPORTA GERAL (igual) + // ========================= 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."); + 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; } - int colItem = GetCol(map, "ITEM"); if (colItem == 0) return BadRequest("Coluna 'ITEM' não encontrada."); - var startRow = headerRow.RowNumber() + 1; - await _db.MobileLines.ExecuteDeleteAsync(); - var buffer = new List(600); var imported = 0; - for (int r = startRow; r <= ws.LastRowUsed().RowNumber(); r++) + foreach (var cell in headerRow.CellsUsed()) { - var itemStr = GetCellString(ws, r, colItem); if (string.IsNullOrWhiteSpace(itemStr)) break; + var k = NormalizeHeader(cell.GetString()); + if (!string.IsNullOrWhiteSpace(k) && !map.ContainsKey(k)) map[k] = cell.Address.ColumnNumber; + } + + int colItem = GetCol(map, "ITEM"); + if (colItem == 0) return BadRequest("Coluna 'ITEM' não encontrada."); + + var startRow = headerRow.RowNumber() + 1; + + await _db.MobileLines.ExecuteDeleteAsync(); + + var buffer = new List(600); + var imported = 0; + + // ✅ FIX: ws.LastRowUsed() pode ser null + 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 e = new MobileLine { + Id = Guid.NewGuid(), // ✅ recomendado Item = TryInt(itemStr), Conta = GetCellByHeader(ws, r, map, "CONTA"), Linha = OnlyDigits(GetCellByHeader(ws, r, map, "LINHA")), @@ -429,19 +534,232 @@ namespace line_gestao_api.Controllers 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") + VencConta = GetCellByHeader(ws, r, map, "VENC. DA CONTA"), + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow }; - ApplyReservaRule(e); buffer.Add(e); imported++; - if (buffer.Count >= 500) { await _db.MobileLines.AddRangeAsync(buffer); await _db.SaveChangesAsync(); buffer.Clear(); } + + 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(); } + + if (buffer.Count > 0) + { + await _db.MobileLines.AddRangeAsync(buffer); + await _db.SaveChangesAsync(); + } + + // ========================= + // ✅ IMPORTA MUREG (igual ao seu) + // ========================= + await ImportMuregFromWorkbook(wb); + + // ========================= + // ✅ NOVO: IMPORTA FATURAMENTO PF/PJ + // ========================= + await ImportBillingFromWorkbook(wb); + return Ok(new ImportResultDto { Imported = imported }); } + // ========================================================== + // ✅ IMPORTAÇÃO DA ABA MUREG (mesmo upload) + // ========================================================== + 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 = 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; + } + + int colItem = GetCol(map, "ITEM"); + if (colItem == 0) return; + + var startRow = headerRow.RowNumber() + 1; + + await _db.MuregLines.ExecuteDeleteAsync(); + + 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 e = new MuregLine + { + Id = Guid.NewGuid(), + Item = TryInt(itemStr), + + LinhaAntiga = OnlyDigits(GetCellByHeader(wsM, r, map, "LINHA ANTIGA")), + LinhaNova = OnlyDigits(GetCellByHeader(wsM, r, map, "LINHA NOVA")), + ICCID = OnlyDigits(GetCellByHeader(wsM, r, map, "ICCID")), + DataDaMureg = TryDate(wsM, r, map, "DATA DA MUREG"), + Cliente = GetCellByHeader(wsM, r, map, "CLIENTE"), + }; + + buffer.Add(e); + + 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(); + } + } + + // ========================================================== + // ✅ NOVO: IMPORTAÇÃO DO FATURAMENTO (PF/PJ) + // Tabs: "FATURAMENTO PF" e "FATURAMENTO 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 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; + } + + var colCliente = GetCol(map, "CLIENTE"); + if (colCliente == 0) return; + + var colItem = GetCol(map, "ITEM"); + if (colItem == 0 && colCliente > 1) colItem = colCliente - 1; + + var startRow = headerRow.RowNumber() + 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; + + var qtdStr = GetCellByAnyHeader(ws, r, map, "QTD DE LINHAS", "QTDDLINHAS", "QTD LINHAS"); + var qtd = TryNullableInt(qtdStr); + + var franquiaVivoStr = GetCellByAnyHeader(ws, r, map, "FRAQUIA VIVO", "FRANQUIA VIVO"); + var valorContratoVivoStr = GetCellByAnyHeader(ws, r, map, "VALOR CONTRATO VIVO", "VALOR DO CONTRATO VIVO", "VALOR CONTRATO VIVO R$"); + + var franquiaLineStr = GetCellByAnyHeader(ws, r, map, "FRAQUIA LINE", "FRANQUIA LINE"); + var valorContratoLineStr = GetCellByAnyHeader(ws, r, map, "VALOR CONTRATO LINE", "VALOR DO CONTRATO LINE", "VALOR CONTRATO LINE R$"); + + var lucroStr = GetCellByAnyHeader(ws, r, map, "LUCRO"); + + var aparelho = GetCellByAnyHeader(ws, r, map, "APARELHO"); + var formaPagto = GetCellByAnyHeader(ws, r, map, "FORMA DE PAGAMENTO", "FORMA PAGAMENTO", "FORMAPAGAMENTO"); + + 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(); + } + } + // ========================================================== // HELPERS // ========================================================== - 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 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() { @@ -488,13 +806,97 @@ namespace line_gestao_api.Controllers } } - private static int GetCol(Dictionary map, string name) => map.TryGetValue(NormalizeHeader(name), out var c) ? c : 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 GetCellString(IXLWorksheet ws, int row, int col) { var c = ws.Cell(row, col); return c == null ? "" : (c.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; var cell = ws.Cell(row, c); if (cell.DataType == XLDataType.DateTime) return ToUtc(cell.GetDateTime()); 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 decimal? TryDecimal(string? s) { if (string.IsNullOrWhiteSpace(s)) return null; s = s.Replace("R$", "").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; return null; } - private static int TryInt(string s) => int.TryParse(OnlyDigits(s), out var n) ? n : 0; - 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 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("ITEM", "ITEM").Replace("USUARIO", "USUARIO").Replace("GESTAO", "GESTAO").Replace("LOCACAO", "LOCACAO").Replace(" ", ""); } + private static int GetCol(Dictionary map, string name) + => map.TryGetValue(NormalizeHeader(name), out var c) ? c : 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 GetCellByAnyHeader(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) + { + 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; + + var cell = ws.Cell(row, c); + + if (cell.DataType == XLDataType.DateTime) + return ToUtc(cell.GetDateTime()); + + 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 decimal? TryDecimal(string? s) + { + if (string.IsNullOrWhiteSpace(s)) return null; + s = s.Replace("R$", "").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; + + 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 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("ITEM", "ITEM") + .Replace("USUARIO", "USUARIO") + .Replace("GESTAO", "GESTAO") + .Replace("LOCACAO", "LOCACAO") + .Replace(" ", ""); + } } -} \ No newline at end of file +} diff --git a/Controllers/MuregController.cs b/Controllers/MuregController.cs new file mode 100644 index 0000000..d7c40b5 --- /dev/null +++ b/Controllers/MuregController.cs @@ -0,0 +1,106 @@ +using line_gestao_api.Data; +using line_gestao_api.Dtos; +using line_gestao_api.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace line_gestao_api.Controllers +{ + [ApiController] + [Route("api/mureg")] + public class MuregController : ControllerBase + { + private readonly AppDbContext _db; + + public MuregController(AppDbContext db) + { + _db = db; + } + + public class ImportExcelForm + { + public IFormFile File { get; set; } = default!; + } + + // ========================================================== + // ✅ GET: /api/mureg (com paginação, busca e ordenação) + // ========================================================== + [HttpGet] + public async Task>> GetAll( + [FromQuery] string? search, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 10, + [FromQuery] string? sortBy = "item", + [FromQuery] string? sortDir = "asc") + { + page = page < 1 ? 1 : page; + pageSize = pageSize < 1 ? 10 : pageSize; + + var q = _db.MuregLines.AsNoTracking(); + + if (!string.IsNullOrWhiteSpace(search)) + { + var s = search.Trim(); + q = q.Where(x => + EF.Functions.ILike((x.LinhaAntiga ?? ""), $"%{s}%") || + EF.Functions.ILike((x.LinhaNova ?? ""), $"%{s}%") || + EF.Functions.ILike((x.ICCID ?? ""), $"%{s}%") || + EF.Functions.ILike((x.Cliente ?? ""), $"%{s}%") || + EF.Functions.ILike(x.Item.ToString(), $"%{s}%")); + } + + var total = await q.CountAsync(); + + var sb = (sortBy ?? "item").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), + "linhaantiga" => desc ? q.OrderByDescending(x => x.LinhaAntiga ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.LinhaAntiga ?? "").ThenBy(x => x.Item), + "linhanova" => desc ? q.OrderByDescending(x => x.LinhaNova ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.LinhaNova ?? "").ThenBy(x => x.Item), + "iccid" => desc ? q.OrderByDescending(x => x.ICCID ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.ICCID ?? "").ThenBy(x => x.Item), + "datadamureg" => desc ? q.OrderByDescending(x => x.DataDaMureg).ThenBy(x => x.Item) : q.OrderBy(x => x.DataDaMureg).ThenBy(x => x.Item), + "cliente" => desc ? q.OrderByDescending(x => x.Cliente ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.Cliente ?? "").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 MuregListDto + { + Id = x.Id, + Item = x.Item, + LinhaAntiga = x.LinhaAntiga, + LinhaNova = x.LinhaNova, + ICCID = x.ICCID, + DataDaMureg = x.DataDaMureg, + Cliente = x.Cliente + }) + .ToListAsync(); + + return Ok(new PagedResult + { + Page = page, + PageSize = pageSize, + Total = total, + Items = items + }); + } + + // ========================================================== + // ✅ POST: /api/mureg/import-excel (opcional) + // Se você usar o botão "Importar" da tela MUREG, ele vai bater aqui + // ========================================================== + [HttpPost("import-excel")] + [Consumes("multipart/form-data")] + [RequestSizeLimit(50_000_000)] + public async Task ImportExcel([FromForm] ImportExcelForm form) + { + // Se você quiser manter "importa só no GERAL", pode remover este endpoint. + // Eu deixei para não quebrar o botão do seu front (que chama /api/mureg/import-excel). + return BadRequest(new { message = "Importe a planilha pela página GERAL. O MUREG será carregado automaticamente." }); + } + } +} diff --git a/Data/AppDbContext.cs b/Data/AppDbContext.cs index bb7499b..23ec984 100644 --- a/Data/AppDbContext.cs +++ b/Data/AppDbContext.cs @@ -9,21 +9,55 @@ public class AppDbContext : DbContext public DbSet Users => Set(); - // ✅ NOVO: tabela para espelhar a planilha (GERAL) + // ✅ tabela para espelhar a planilha (GERAL) public DbSet MobileLines => Set(); + // ✅ tabela para espelhar a aba MUREG + public DbSet MuregLines => Set(); + + // ✅ tabela para espelhar o FATURAMENTO (PF/PJ) + public DbSet BillingClients => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - // ✅ MANTIDO: índice único do User (não mexi em nada aqui) + // ✅ MANTIDO: índice único do User modelBuilder.Entity() .HasIndex(u => u.Email) .IsUnique(); - // ✅ NOVO: índice único para evitar duplicar a mesma linha (telefone) + // ✅ MANTIDO: índice único para evitar duplicar a mesma linha (telefone) modelBuilder.Entity() .HasIndex(x => x.Linha) .IsUnique(); + + // ✅ MANTIDO: índices do MUREG + modelBuilder.Entity().HasIndex(x => x.Item); + modelBuilder.Entity().HasIndex(x => x.Cliente); + modelBuilder.Entity().HasIndex(x => x.ICCID); + modelBuilder.Entity().HasIndex(x => x.LinhaNova); + + // ========================================================== + // ✅ NOVO: MAPEAMENTO DO FATURAMENTO + // (evita problema de tabela "BillingClients" vs postgres case) + // ========================================================== + modelBuilder.Entity(e => + { + // 🔥 Nome físico fixo da tabela no Postgres + e.ToTable("billing_clients"); + + e.HasKey(x => x.Id); + + // (opcional, mas bom pra padronizar) + e.Property(x => x.Tipo).HasMaxLength(2); + e.Property(x => x.Cliente).HasMaxLength(255); + + // índices úteis para filtros/ordenação + e.HasIndex(x => x.Tipo); + e.HasIndex(x => x.Cliente); + e.HasIndex(x => new { x.Tipo, x.Cliente }); + e.HasIndex(x => x.Item); + }); } } diff --git a/Dtos/BillingClientListDto.cs b/Dtos/BillingClientListDto.cs new file mode 100644 index 0000000..11140ca --- /dev/null +++ b/Dtos/BillingClientListDto.cs @@ -0,0 +1,20 @@ +namespace line_gestao_api.Dtos +{ + public class BillingClientListDto + { + public Guid Id { get; set; } + public string Tipo { get; set; } = ""; + public int Item { get; set; } + public string Cliente { get; set; } = ""; + + public int? QtdLinhas { get; set; } + public decimal? FranquiaVivo { get; set; } + public decimal? ValorContratoVivo { get; set; } + public decimal? FranquiaLine { get; set; } + public decimal? ValorContratoLine { get; set; } + public decimal? Lucro { get; set; } + + public string? Aparelho { get; set; } + public string? FormaPagamento { get; set; } + } +} diff --git a/Dtos/MuregListDto.cs b/Dtos/MuregListDto.cs new file mode 100644 index 0000000..f5775a0 --- /dev/null +++ b/Dtos/MuregListDto.cs @@ -0,0 +1,15 @@ +using System; + +namespace line_gestao_api.Dtos +{ + public class MuregListDto + { + public Guid Id { get; set; } + public int Item { get; set; } + public string? LinhaAntiga { get; set; } + public string? LinhaNova { get; set; } + public string? ICCID { get; set; } + public DateTime? DataDaMureg { get; set; } + public string? Cliente { get; set; } + } +} diff --git a/Migrations/20251229122650_AddMuregLines.Designer.cs b/Migrations/20251229122650_AddMuregLines.Designer.cs new file mode 100644 index 0000000..70d8ba2 --- /dev/null +++ b/Migrations/20251229122650_AddMuregLines.Designer.cs @@ -0,0 +1,231 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using line_gestao_api.Data; + +#nullable disable + +namespace line_gestao_api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20251229122650_AddMuregLines")] + partial class AddMuregLines + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("line_gestao_api.Models.MobileLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Cedente") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("Chip") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Cliente") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Conta") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DataBloqueio") + .HasColumnType("timestamp with time zone"); + + b.Property("DataEntregaCliente") + .HasColumnType("timestamp with time zone"); + + b.Property("DataEntregaOpera") + .HasColumnType("timestamp with time zone"); + + b.Property("Desconto") + .HasColumnType("numeric"); + + b.Property("FranquiaGestao") + .HasColumnType("numeric"); + + b.Property("FranquiaLine") + .HasColumnType("numeric"); + + b.Property("FranquiaVivo") + .HasColumnType("numeric"); + + b.Property("GestaoVozDados") + .HasColumnType("numeric"); + + b.Property("Item") + .HasColumnType("integer"); + + b.Property("Linha") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("LocacaoAp") + .HasColumnType("numeric"); + + b.Property("Lucro") + .HasColumnType("numeric"); + + b.Property("Modalidade") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("PlanoContrato") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Skeelo") + .HasColumnType("numeric"); + + b.Property("Skil") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Solicitante") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("Status") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usuario") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ValorContratoLine") + .HasColumnType("numeric"); + + b.Property("ValorContratoVivo") + .HasColumnType("numeric"); + + b.Property("ValorPlanoVivo") + .HasColumnType("numeric"); + + b.Property("VencConta") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("VivoGestaoDispositivo") + .HasColumnType("numeric"); + + b.Property("VivoNewsPlus") + .HasColumnType("numeric"); + + b.Property("VivoTravelMundo") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("Linha") + .IsUnique(); + + b.ToTable("MobileLines"); + }); + + modelBuilder.Entity("line_gestao_api.Models.MuregLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Cliente") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DataDaMureg") + .HasColumnType("timestamp with time zone"); + + b.Property("ICCID") + .HasColumnType("text"); + + b.Property("Item") + .HasColumnType("integer"); + + b.Property("LinhaAntiga") + .HasColumnType("text"); + + b.Property("LinhaNova") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Cliente"); + + b.HasIndex("ICCID"); + + b.HasIndex("Item"); + + b.HasIndex("LinhaNova"); + + b.ToTable("MuregLines"); + }); + + modelBuilder.Entity("line_gestao_api.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20251229122650_AddMuregLines.cs b/Migrations/20251229122650_AddMuregLines.cs new file mode 100644 index 0000000..e781d84 --- /dev/null +++ b/Migrations/20251229122650_AddMuregLines.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace line_gestao_api.Migrations +{ + /// + public partial class AddMuregLines : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "MuregLines", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Item = table.Column(type: "integer", nullable: false), + LinhaAntiga = table.Column(type: "text", nullable: true), + LinhaNova = table.Column(type: "text", nullable: true), + ICCID = table.Column(type: "text", nullable: true), + DataDaMureg = table.Column(type: "timestamp with time zone", nullable: true), + Cliente = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MuregLines", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_MuregLines_Cliente", + table: "MuregLines", + column: "Cliente"); + + migrationBuilder.CreateIndex( + name: "IX_MuregLines_ICCID", + table: "MuregLines", + column: "ICCID"); + + migrationBuilder.CreateIndex( + name: "IX_MuregLines_Item", + table: "MuregLines", + column: "Item"); + + migrationBuilder.CreateIndex( + name: "IX_MuregLines_LinhaNova", + table: "MuregLines", + column: "LinhaNova"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "MuregLines"); + } + } +} diff --git a/Migrations/20260105182438_AddBillingClients.Designer.cs b/Migrations/20260105182438_AddBillingClients.Designer.cs new file mode 100644 index 0000000..4a62fbd --- /dev/null +++ b/Migrations/20260105182438_AddBillingClients.Designer.cs @@ -0,0 +1,231 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using line_gestao_api.Data; + +#nullable disable + +namespace line_gestao_api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260105182438_AddBillingClients")] + partial class AddBillingClients + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("line_gestao_api.Models.MobileLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Cedente") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("Chip") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Cliente") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Conta") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DataBloqueio") + .HasColumnType("timestamp with time zone"); + + b.Property("DataEntregaCliente") + .HasColumnType("timestamp with time zone"); + + b.Property("DataEntregaOpera") + .HasColumnType("timestamp with time zone"); + + b.Property("Desconto") + .HasColumnType("numeric"); + + b.Property("FranquiaGestao") + .HasColumnType("numeric"); + + b.Property("FranquiaLine") + .HasColumnType("numeric"); + + b.Property("FranquiaVivo") + .HasColumnType("numeric"); + + b.Property("GestaoVozDados") + .HasColumnType("numeric"); + + b.Property("Item") + .HasColumnType("integer"); + + b.Property("Linha") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("LocacaoAp") + .HasColumnType("numeric"); + + b.Property("Lucro") + .HasColumnType("numeric"); + + b.Property("Modalidade") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("PlanoContrato") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Skeelo") + .HasColumnType("numeric"); + + b.Property("Skil") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Solicitante") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("Status") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usuario") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ValorContratoLine") + .HasColumnType("numeric"); + + b.Property("ValorContratoVivo") + .HasColumnType("numeric"); + + b.Property("ValorPlanoVivo") + .HasColumnType("numeric"); + + b.Property("VencConta") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("VivoGestaoDispositivo") + .HasColumnType("numeric"); + + b.Property("VivoNewsPlus") + .HasColumnType("numeric"); + + b.Property("VivoTravelMundo") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("Linha") + .IsUnique(); + + b.ToTable("MobileLines"); + }); + + modelBuilder.Entity("line_gestao_api.Models.MuregLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Cliente") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DataDaMureg") + .HasColumnType("timestamp with time zone"); + + b.Property("ICCID") + .HasColumnType("text"); + + b.Property("Item") + .HasColumnType("integer"); + + b.Property("LinhaAntiga") + .HasColumnType("text"); + + b.Property("LinhaNova") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Cliente"); + + b.HasIndex("ICCID"); + + b.HasIndex("Item"); + + b.HasIndex("LinhaNova"); + + b.ToTable("MuregLines"); + }); + + modelBuilder.Entity("line_gestao_api.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260105182438_AddBillingClients.cs b/Migrations/20260105182438_AddBillingClients.cs new file mode 100644 index 0000000..f0b089c --- /dev/null +++ b/Migrations/20260105182438_AddBillingClients.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace line_gestao_api.Migrations +{ + /// + public partial class AddBillingClients : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Migrations/20260105201423_CreateBillingClients.Designer.cs b/Migrations/20260105201423_CreateBillingClients.Designer.cs new file mode 100644 index 0000000..0eda385 --- /dev/null +++ b/Migrations/20260105201423_CreateBillingClients.Designer.cs @@ -0,0 +1,293 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using line_gestao_api.Data; + +#nullable disable + +namespace line_gestao_api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260105201423_CreateBillingClients")] + partial class CreateBillingClients + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("line_gestao_api.Models.BillingClient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Aparelho") + .HasColumnType("text"); + + b.Property("Cliente") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FormaPagamento") + .HasColumnType("text"); + + b.Property("FranquiaLine") + .HasColumnType("numeric"); + + b.Property("FranquiaVivo") + .HasColumnType("numeric"); + + b.Property("Item") + .HasColumnType("integer"); + + b.Property("Lucro") + .HasColumnType("numeric"); + + b.Property("QtdLinhas") + .HasColumnType("integer"); + + b.Property("Tipo") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ValorContratoLine") + .HasColumnType("numeric"); + + b.Property("ValorContratoVivo") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("Cliente"); + + b.HasIndex("Item"); + + b.HasIndex("Tipo"); + + b.HasIndex("Tipo", "Cliente"); + + b.ToTable("billing_clients", (string)null); + }); + + modelBuilder.Entity("line_gestao_api.Models.MobileLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Cedente") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("Chip") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Cliente") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Conta") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DataBloqueio") + .HasColumnType("timestamp with time zone"); + + b.Property("DataEntregaCliente") + .HasColumnType("timestamp with time zone"); + + b.Property("DataEntregaOpera") + .HasColumnType("timestamp with time zone"); + + b.Property("Desconto") + .HasColumnType("numeric"); + + b.Property("FranquiaGestao") + .HasColumnType("numeric"); + + b.Property("FranquiaLine") + .HasColumnType("numeric"); + + b.Property("FranquiaVivo") + .HasColumnType("numeric"); + + b.Property("GestaoVozDados") + .HasColumnType("numeric"); + + b.Property("Item") + .HasColumnType("integer"); + + b.Property("Linha") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("LocacaoAp") + .HasColumnType("numeric"); + + b.Property("Lucro") + .HasColumnType("numeric"); + + b.Property("Modalidade") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("PlanoContrato") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Skeelo") + .HasColumnType("numeric"); + + b.Property("Skil") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("Solicitante") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("Status") + .HasMaxLength(80) + .HasColumnType("character varying(80)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Usuario") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ValorContratoLine") + .HasColumnType("numeric"); + + b.Property("ValorContratoVivo") + .HasColumnType("numeric"); + + b.Property("ValorPlanoVivo") + .HasColumnType("numeric"); + + b.Property("VencConta") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("VivoGestaoDispositivo") + .HasColumnType("numeric"); + + b.Property("VivoNewsPlus") + .HasColumnType("numeric"); + + b.Property("VivoTravelMundo") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("Linha") + .IsUnique(); + + b.ToTable("MobileLines"); + }); + + modelBuilder.Entity("line_gestao_api.Models.MuregLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Cliente") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DataDaMureg") + .HasColumnType("timestamp with time zone"); + + b.Property("ICCID") + .HasColumnType("text"); + + b.Property("Item") + .HasColumnType("integer"); + + b.Property("LinhaAntiga") + .HasColumnType("text"); + + b.Property("LinhaNova") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Cliente"); + + b.HasIndex("ICCID"); + + b.HasIndex("Item"); + + b.HasIndex("LinhaNova"); + + b.ToTable("MuregLines"); + }); + + modelBuilder.Entity("line_gestao_api.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260105201423_CreateBillingClients.cs b/Migrations/20260105201423_CreateBillingClients.cs new file mode 100644 index 0000000..8b48b8b --- /dev/null +++ b/Migrations/20260105201423_CreateBillingClients.cs @@ -0,0 +1,66 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace line_gestao_api.Migrations +{ + /// + public partial class CreateBillingClients : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "billing_clients", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Tipo = table.Column(type: "character varying(2)", maxLength: 2, nullable: false), + Item = table.Column(type: "integer", nullable: false), + Cliente = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + QtdLinhas = table.Column(type: "integer", nullable: true), + FranquiaVivo = table.Column(type: "numeric", nullable: true), + ValorContratoVivo = table.Column(type: "numeric", nullable: true), + FranquiaLine = table.Column(type: "numeric", nullable: true), + ValorContratoLine = table.Column(type: "numeric", nullable: true), + Lucro = table.Column(type: "numeric", nullable: true), + Aparelho = table.Column(type: "text", nullable: true), + FormaPagamento = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_billing_clients", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_billing_clients_Cliente", + table: "billing_clients", + column: "Cliente"); + + migrationBuilder.CreateIndex( + name: "IX_billing_clients_Item", + table: "billing_clients", + column: "Item"); + + migrationBuilder.CreateIndex( + name: "IX_billing_clients_Tipo", + table: "billing_clients", + column: "Tipo"); + + migrationBuilder.CreateIndex( + name: "IX_billing_clients_Tipo_Cliente", + table: "billing_clients", + columns: new[] { "Tipo", "Cliente" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "billing_clients"); + } + } +} diff --git a/Migrations/AppDbContextModelSnapshot.cs b/Migrations/AppDbContextModelSnapshot.cs index b45f421..ea4569e 100644 --- a/Migrations/AppDbContextModelSnapshot.cs +++ b/Migrations/AppDbContextModelSnapshot.cs @@ -22,6 +22,68 @@ namespace line_gestao_api.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("line_gestao_api.Models.BillingClient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Aparelho") + .HasColumnType("text"); + + b.Property("Cliente") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FormaPagamento") + .HasColumnType("text"); + + b.Property("FranquiaLine") + .HasColumnType("numeric"); + + b.Property("FranquiaVivo") + .HasColumnType("numeric"); + + b.Property("Item") + .HasColumnType("integer"); + + b.Property("Lucro") + .HasColumnType("numeric"); + + b.Property("QtdLinhas") + .HasColumnType("integer"); + + b.Property("Tipo") + .IsRequired() + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ValorContratoLine") + .HasColumnType("numeric"); + + b.Property("ValorContratoVivo") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("Cliente"); + + b.HasIndex("Item"); + + b.HasIndex("Tipo"); + + b.HasIndex("Tipo", "Cliente"); + + b.ToTable("billing_clients", (string)null); + }); + modelBuilder.Entity("line_gestao_api.Models.MobileLine", b => { b.Property("Id") @@ -144,6 +206,49 @@ namespace line_gestao_api.Migrations b.ToTable("MobileLines"); }); + modelBuilder.Entity("line_gestao_api.Models.MuregLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Cliente") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DataDaMureg") + .HasColumnType("timestamp with time zone"); + + b.Property("ICCID") + .HasColumnType("text"); + + b.Property("Item") + .HasColumnType("integer"); + + b.Property("LinhaAntiga") + .HasColumnType("text"); + + b.Property("LinhaNova") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Cliente"); + + b.HasIndex("ICCID"); + + b.HasIndex("Item"); + + b.HasIndex("LinhaNova"); + + b.ToTable("MuregLines"); + }); + modelBuilder.Entity("line_gestao_api.Models.User", b => { b.Property("Id") diff --git a/Models/BillingClient.cs b/Models/BillingClient.cs new file mode 100644 index 0000000..eef23e8 --- /dev/null +++ b/Models/BillingClient.cs @@ -0,0 +1,27 @@ +namespace line_gestao_api.Models +{ + public class BillingClient + { + public Guid Id { get; set; } + public string Tipo { get; set; } = "PF"; // "PF" ou "PJ" + + public int Item { get; set; } + public string Cliente { get; set; } = ""; + + public int? QtdLinhas { get; set; } + + public decimal? FranquiaVivo { get; set; } + public decimal? ValorContratoVivo { get; set; } + + public decimal? FranquiaLine { get; set; } + public decimal? ValorContratoLine { get; set; } + + public decimal? Lucro { get; set; } + + public string? Aparelho { get; set; } + public string? FormaPagamento { get; set; } + + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/Models/MuregLine.cs b/Models/MuregLine.cs new file mode 100644 index 0000000..10d70f8 --- /dev/null +++ b/Models/MuregLine.cs @@ -0,0 +1,22 @@ +using System; + +namespace line_gestao_api.Models +{ + public class MuregLine + { + public Guid Id { get; set; } = Guid.NewGuid(); + + public int Item { get; set; } + + public string? LinhaAntiga { get; set; } + public string? LinhaNova { get; set; } + public string? ICCID { get; set; } + + public DateTime? DataDaMureg { get; set; } + + public string? Cliente { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + } +}