diff --git a/Controllers/LinesController.cs b/Controllers/LinesController.cs index cfadd9a..e3f452f 100644 --- a/Controllers/LinesController.cs +++ b/Controllers/LinesController.cs @@ -27,14 +27,13 @@ namespace line_gestao_api.Controllers } // ========================================================== - // ✅ 1. ENDPOINT: AGRUPAR POR CLIENTE (COM BUSCA E PAGINAÇÃO) + // ✅ 1. ENDPOINT: AGRUPAR POR CLIENTE // ========================================================== - // Alterado para aceitar 'search', 'page' e retornar PagedResult [HttpGet("groups")] public async Task>> GetClientGroups( [FromQuery] string? skil, - [FromQuery] string? search, // 🔍 Busca por Nome do Cliente - [FromQuery] int page = 1, // 📄 Paginação + [FromQuery] string? search, + [FromQuery] int page = 1, [FromQuery] int pageSize = 10) { page = page < 1 ? 1 : page; @@ -42,30 +41,23 @@ namespace line_gestao_api.Controllers var query = _db.MobileLines.AsNoTracking().Where(x => !string.IsNullOrEmpty(x.Cliente)); - // 1. Filtro SKIL (PF, PJ ou RESERVA) + // 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}%")); - } } - // 2. Filtro SEARCH (Busca pelo Nome do Cliente nos grupos) - // Aqui garantimos que se o usuário digitar "ADR", filtramos apenas os clientes que tem ADR no nome + // 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}%")); } - // Montagem do Agrupamento var groupedQuery = query .GroupBy(x => x.Cliente) .Select(g => new ClientGroupDto @@ -74,21 +66,18 @@ 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%")) }); - // Contagem total para a paginação var totalGroups = await groupedQuery.CountAsync(); - // Aplicação da Paginação var items = await groupedQuery .OrderBy(x => x.Cliente) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); - // Retorna formato paginado return Ok(new PagedResult { Page = page, @@ -116,7 +105,7 @@ namespace line_gestao_api.Controllers } // ========================================================== - // ✅ 3. GET ALL (TABELA / DETALHES DO GRUPO / BUSCA ESPECÍFICA) + // ✅ 3. GET ALL (TABELA / DETALHES DO GRUPO) // ========================================================== [HttpGet] public async Task>> GetAll( @@ -133,7 +122,6 @@ namespace line_gestao_api.Controllers var q = _db.MobileLines.AsNoTracking(); - // Filtro SKIL if (!string.IsNullOrWhiteSpace(skil)) { var sSkil = skil.Trim(); @@ -143,21 +131,18 @@ namespace line_gestao_api.Controllers q = q.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%")); } - // Filtro Cliente Específico (usado ao expandir o grupo) if (!string.IsNullOrWhiteSpace(client)) { - var sClient = client.Trim(); - q = q.Where(x => EF.Functions.ILike(x.Cliente ?? "", sClient)); + q = q.Where(x => EF.Functions.ILike(x.Cliente ?? "", client.Trim())); } - // Busca Genérica (usada quando o frontend detecta números ou busca específica) 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}%") || // Busca cliente aqui também caso seja modo tabela + EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") || EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") || EF.Functions.ILike(x.Conta ?? "", $"%{s}%") || EF.Functions.ILike(x.Status ?? "", $"%{s}%")); @@ -216,33 +201,183 @@ namespace line_gestao_api.Controllers } // ========================================================== - // DEMAIS MÉTODOS (CRUD, IMPORT, HELPERS) - MANTIDOS + // ✅ 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)); } + [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 (AUTO-INCREMENTO DE ID/ITEM) + // ========================================================== + [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 + Cliente = req.Cliente.Trim().ToUpper(), + Linha = linhaLimpa, + Chip = 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(), + + // 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, + 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 = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + // Aplica regra de negócio para Reserva (se necessário) + ApplyReservaRule(newLine); + + _db.MobileLines.Add(newLine); + + try + { + await _db.SaveChangesAsync(); + } + catch (DbUpdateException) + { + 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)); + } + + // ========================================================== + // ✅ 6. UPDATE (ITEM PROTEGIDO) + // ========================================================== [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 }); } + 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.Item = req.Item; x.Conta = req.Conta?.Trim(); x.Linha = newLinha; x.Chip = OnlyDigits(req.Chip); 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; + // 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); + 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." }); } - try { await _db.SaveChangesAsync(); } catch (DbUpdateException) { return Conflict(new { message = "Conflito ao salvar." }); } return NoContent(); } - [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(); } + // ========================================================== + // ✅ 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")] [Consumes("multipart/form-data")] [RequestSizeLimit(50_000_000)] @@ -303,10 +438,56 @@ namespace line_gestao_api.Controllers return Ok(new ImportResultDto { Imported = imported }); } - // Helpers + // ========================================================== + // 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 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 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 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(); } diff --git a/Dtos/CreateMobileLineDto.cs b/Dtos/CreateMobileLineDto.cs new file mode 100644 index 0000000..ee9ce98 --- /dev/null +++ b/Dtos/CreateMobileLineDto.cs @@ -0,0 +1,69 @@ +using System; + +namespace line_gestao_api.Dtos +{ + public class CreateMobileLineDto + { + // ========================== + // Identificação Básica + // ========================== + public int Item { get; set; } + public string? Linha { get; set; } // Obrigatório na validação do Controller + public string? Chip { get; set; } // ICCID + public string? Cliente { get; set; } // Obrigatório na validação do Controller + public string? Usuario { get; set; } + + // ========================== + // Classificação e Status + // ========================== + public string? Status { get; set; } // Ex: ATIVA, BLOQUEADA + public string? Skil { get; set; } // Ex: PF, PJ, RESERVA + public string? Modalidade { get; set; } + + // ========================== + // Dados Contratuais + // ========================== + public string? PlanoContrato { get; set; } + public string? Conta { get; set; } + public string? VencConta { get; set; } // Dia do vencimento (string) + + // ========================== + // Datas Importantes + // ========================== + public DateTime? DataBloqueio { get; set; } + public DateTime? DataEntregaOpera { get; set; } + public DateTime? DataEntregaCliente { get; set; } + + // ========================== + // Responsáveis / Logística + // ========================== + public string? Cedente { get; set; } + public string? Solicitante { get; set; } + + // ========================== + // Financeiro - Vivo + // ========================== + public decimal? FranquiaVivo { get; set; } + public decimal? ValorPlanoVivo { get; set; } + public decimal? GestaoVozDados { get; set; } + public decimal? Skeelo { get; set; } + public decimal? VivoNewsPlus { get; set; } + public decimal? VivoTravelMundo { get; set; } + public decimal? VivoGestaoDispositivo { get; set; } + public decimal? ValorContratoVivo { get; set; } + + // ========================== + // Financeiro - Line Móvel + // ========================== + public decimal? FranquiaLine { get; set; } + public decimal? FranquiaGestao { get; set; } + public decimal? LocacaoAp { get; set; } + public decimal? ValorContratoLine { get; set; } + + // ========================== + // Resultado Financeiro + // ========================== + public decimal? Desconto { get; set; } + public decimal? Lucro { get; set; } + } +} \ No newline at end of file