Feat: Adiciona endpoints de agrupamento, listagem de clientes e filtros no LinesController

This commit is contained in:
Eduardo 2025-12-19 17:21:50 -03:00
parent b4a5525578
commit c830812af3
3 changed files with 196 additions and 101 deletions

View File

@ -1,8 +1,7 @@
using ClosedXML.Excel; using ClosedXML.Excel;
using line_gestao_api.Data; using line_gestao_api.Data;
using line_gestao_api.Dtos; using line_gestao_api.Dtos; // Certifique-se que ClientGroupDto está neste namespace
using line_gestao_api.Models; using line_gestao_api.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Globalization; using System.Globalization;
@ -12,7 +11,7 @@ namespace line_gestao_api.Controllers
{ {
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
//[Authorize] //[Authorize] // Descomente se estiver usando autenticação
public class LinesController : ControllerBase public class LinesController : ControllerBase
{ {
private readonly AppDbContext _db; private readonly AppDbContext _db;
@ -22,15 +21,63 @@ namespace line_gestao_api.Controllers
_db = db; _db = db;
} }
// ✅ DTO do form (pra Swagger entender multipart/form-data) // Classe auxiliar apenas para o upload (não é DTO de banco)
public class ImportExcelForm public class ImportExcelForm
{ {
public IFormFile File { get; set; } = default!; public IFormFile File { get; set; } = default!;
} }
// ==========================================================
// ✅ 1. NOVO ENDPOINT: AGRUPAR POR CLIENTE (Resumo para Aba 'Todos')
// ==========================================================
[HttpGet("groups")]
public async Task<ActionResult<List<ClientGroupDto>>> GetClientGroups()
{
// Agrupa por nome do cliente e calcula os totais
var groups = await _db.MobileLines
.AsNoTracking()
.Where(x => !string.IsNullOrEmpty(x.Cliente))
.GroupBy(x => x.Cliente)
.Select(g => new ClientGroupDto
{
Cliente = g.Key!,
TotalLinhas = g.Count(),
// Conta quantos contêm "ativo" (ignorando maiúsculas/minúsculas)
Ativos = g.Count(x => EF.Functions.ILike(x.Status ?? "", "%ativo%")),
// Conta quantos contêm "bloque" ou similar
Bloqueados = g.Count(x => EF.Functions.ILike(x.Status ?? "", "%bloque%") || EF.Functions.ILike(x.Status ?? "", "%perda%") || EF.Functions.ILike(x.Status ?? "", "%roubo%"))
})
.OrderBy(x => x.Cliente)
.ToListAsync();
return Ok(groups);
}
// ==========================================================
// ✅ 2. ENDPOINT: LISTAR NOMES DE CLIENTES (Para o Dropdown)
// ==========================================================
[HttpGet("clients")]
public async Task<ActionResult<List<string>>> GetClients()
{
var clients = await _db.MobileLines
.AsNoTracking()
.Select(x => x.Cliente)
.Where(x => !string.IsNullOrEmpty(x))
.Distinct()
.OrderBy(x => x)
.ToListAsync();
return Ok(clients);
}
// ==========================================================
// ✅ 3. GET ALL (TABELA PRINCIPAL - Com todos os filtros)
// ==========================================================
[HttpGet] [HttpGet]
public async Task<ActionResult<PagedResult<MobileLineListDto>>> GetAll( public async Task<ActionResult<PagedResult<MobileLineListDto>>> GetAll(
[FromQuery] string? search, [FromQuery] string? search,
[FromQuery] string? skil, // Filtro PF/PJ
[FromQuery] string? client, // Filtro Cliente Específico
[FromQuery] int page = 1, [FromQuery] int page = 1,
[FromQuery] int pageSize = 20, [FromQuery] int pageSize = 20,
[FromQuery] string? sortBy = "item", [FromQuery] string? sortBy = "item",
@ -41,6 +88,21 @@ namespace line_gestao_api.Controllers
var q = _db.MobileLines.AsNoTracking(); var q = _db.MobileLines.AsNoTracking();
// 1. Filtro por SKIL (PF/PJ)
if (!string.IsNullOrWhiteSpace(skil))
{
var sSkil = skil.Trim();
q = q.Where(x => EF.Functions.ILike(x.Skil ?? "", $"%{sSkil}%"));
}
// 2. Filtro por Cliente Específico (usado no dropdown e no accordion)
if (!string.IsNullOrWhiteSpace(client))
{
var sClient = client.Trim();
q = q.Where(x => EF.Functions.ILike(x.Cliente ?? "", sClient));
}
// 3. Busca Genérica (Barra de pesquisa)
if (!string.IsNullOrWhiteSpace(search)) if (!string.IsNullOrWhiteSpace(search))
{ {
var s = search.Trim(); var s = search.Trim();
@ -55,48 +117,26 @@ namespace line_gestao_api.Controllers
var total = await q.CountAsync(); var total = await q.CountAsync();
// ===== ORDENAÇÃO COMPLETA ===== // Ordenação
var sb = (sortBy ?? "item").Trim().ToLowerInvariant(); var sb = (sortBy ?? "item").Trim().ToLowerInvariant();
var desc = string.Equals((sortDir ?? "asc").Trim(), "desc", StringComparison.OrdinalIgnoreCase); var desc = string.Equals((sortDir ?? "asc").Trim(), "desc", StringComparison.OrdinalIgnoreCase);
// sinônimos (pra não quebrar se vier diferente do front)
if (sb == "plano") sb = "planocontrato"; if (sb == "plano") sb = "planocontrato";
if (sb == "contrato") sb = "vencconta"; if (sb == "contrato") sb = "vencconta";
q = sb switch q = sb switch
{ {
"conta" => desc ? q.OrderByDescending(x => x.Conta ?? "").ThenBy(x => x.Item) "conta" => desc ? q.OrderByDescending(x => x.Conta ?? "").ThenBy(x => x.Item) : q.OrderBy(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),
"linha" => desc ? q.OrderByDescending(x => x.Linha ?? "").ThenBy(x => x.Item) "cliente" => desc ? q.OrderByDescending(x => x.Cliente ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.Cliente ?? "").ThenBy(x => x.Item),
: q.OrderBy(x => x.Linha ?? "").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),
"chip" => desc ? q.OrderByDescending(x => x.Chip ?? "").ThenBy(x => x.Item) "vencconta" => desc ? q.OrderByDescending(x => x.VencConta ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.VencConta ?? "").ThenBy(x => x.Item),
: q.OrderBy(x => x.Chip ?? "").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),
"cliente" => desc ? q.OrderByDescending(x => x.Cliente ?? "").ThenBy(x => x.Item) "modalidade" => desc ? q.OrderByDescending(x => x.Modalidade ?? "").ThenBy(x => x.Item) : q.OrderBy(x => x.Modalidade ?? "").ThenBy(x => x.Item),
: q.OrderBy(x => x.Cliente ?? "").ThenBy(x => x.Item), _ => desc ? q.OrderByDescending(x => x.Item) : q.OrderBy(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 var items = await q
@ -128,6 +168,9 @@ namespace line_gestao_api.Controllers
}); });
} }
// ==========================================================
// OBTER DETALHES POR ID
// ==========================================================
[HttpGet("{id:guid}")] [HttpGet("{id:guid}")]
public async Task<ActionResult<MobileLineDetailDto>> GetById(Guid id) public async Task<ActionResult<MobileLineDetailDto>> GetById(Guid id)
{ {
@ -137,19 +180,43 @@ namespace line_gestao_api.Controllers
return Ok(ToDetailDto(x)); return Ok(ToDetailDto(x));
} }
// ==========================================================
// ATUALIZAR (PUT)
// ==========================================================
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateMobileLineRequest req) public async Task<IActionResult> Update(Guid id, [FromBody] UpdateMobileLineRequest req)
{ {
var x = await _db.MobileLines.FirstOrDefaultAsync(a => a.Id == id); var x = await _db.MobileLines.FirstOrDefaultAsync(a => a.Id == id);
if (x == null) return NotFound(); if (x == null) return NotFound();
var newLinha = OnlyDigits(req.Linha);
var newChip = OnlyDigits(req.Chip);
// Verifica duplicidade de linha (se alterou a 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 um registro com essa LINHA. Não é possível salvar duplicado.",
linha = newLinha
});
}
}
// Atualiza campos
x.Item = req.Item; x.Item = req.Item;
x.Conta = req.Conta; x.Conta = req.Conta?.Trim();
x.Linha = req.Linha; x.Linha = string.IsNullOrWhiteSpace(newLinha) ? null : newLinha;
x.Chip = req.Chip; x.Chip = string.IsNullOrWhiteSpace(newChip) ? null : newChip;
x.Cliente = req.Cliente; x.Cliente = req.Cliente?.Trim();
x.Usuario = req.Usuario; x.Usuario = req.Usuario?.Trim();
x.PlanoContrato = req.PlanoContrato; x.PlanoContrato = req.PlanoContrato?.Trim();
x.FranquiaVivo = req.FranquiaVivo; x.FranquiaVivo = req.FranquiaVivo;
x.ValorPlanoVivo = req.ValorPlanoVivo; x.ValorPlanoVivo = req.ValorPlanoVivo;
@ -168,27 +235,40 @@ namespace line_gestao_api.Controllers
x.Desconto = req.Desconto; x.Desconto = req.Desconto;
x.Lucro = req.Lucro; x.Lucro = req.Lucro;
x.Status = req.Status; x.Status = req.Status?.Trim();
x.DataBloqueio = ToUtc(req.DataBloqueio); x.DataBloqueio = ToUtc(req.DataBloqueio);
x.Skil = req.Skil; x.Skil = req.Skil?.Trim();
x.Modalidade = req.Modalidade; x.Modalidade = req.Modalidade?.Trim();
x.Cedente = req.Cedente; x.Cedente = req.Cedente?.Trim();
x.Solicitante = req.Solicitante; x.Solicitante = req.Solicitante?.Trim();
x.DataEntregaOpera = ToUtc(req.DataEntregaOpera); x.DataEntregaOpera = ToUtc(req.DataEntregaOpera);
x.DataEntregaCliente = ToUtc(req.DataEntregaCliente); x.DataEntregaCliente = ToUtc(req.DataEntregaCliente);
x.VencConta = req.VencConta; x.VencConta = req.VencConta?.Trim();
// regra RESERVA
ApplyReservaRule(x); ApplyReservaRule(x);
x.UpdatedAt = DateTime.UtcNow; x.UpdatedAt = DateTime.UtcNow;
await _db.SaveChangesAsync();
try
{
await _db.SaveChangesAsync();
}
catch (DbUpdateException)
{
return Conflict(new
{
message = "Conflito ao salvar. Verifique se a LINHA já existe em outro registro."
});
}
return NoContent(); return NoContent();
} }
// ==========================================================
// DELETAR (DELETE)
// ==========================================================
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id) public async Task<IActionResult> Delete(Guid id)
{ {
@ -200,6 +280,9 @@ namespace line_gestao_api.Controllers
return NoContent(); return NoContent();
} }
// ==========================================================
// IMPORTAR EXCEL
// ==========================================================
[HttpPost("import-excel")] [HttpPost("import-excel")]
[Consumes("multipart/form-data")] [Consumes("multipart/form-data")]
[RequestSizeLimit(50_000_000)] [RequestSizeLimit(50_000_000)]
@ -217,14 +300,12 @@ namespace line_gestao_api.Controllers
if (ws == null) if (ws == null)
return BadRequest("Aba 'GERAL' não encontrada."); return BadRequest("Aba 'GERAL' não encontrada.");
// acha a linha do cabeçalho (onde existe ITÉM)
var headerRow = ws.RowsUsed().FirstOrDefault(r => var headerRow = ws.RowsUsed().FirstOrDefault(r =>
r.CellsUsed().Any(c => NormalizeHeader(c.GetString()) == "ITEM")); r.CellsUsed().Any(c => NormalizeHeader(c.GetString()) == "ITEM"));
if (headerRow == null) if (headerRow == null)
return BadRequest("Cabeçalho da planilha (linha com 'ITÉM') não encontrado."); return BadRequest("Cabeçalho da planilha (linha com 'ITÉM') não encontrado.");
// mapa header -> coluna
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (var cell in headerRow.CellsUsed()) foreach (var cell in headerRow.CellsUsed())
{ {
@ -238,7 +319,7 @@ namespace line_gestao_api.Controllers
var startRow = headerRow.RowNumber() + 1; var startRow = headerRow.RowNumber() + 1;
// REPLACE: apaga tudo e reimporta (pra espelhar 100% o Excel) // Opção: Deletar tudo antes de importar (Cuidado em produção!)
await _db.MobileLines.ExecuteDeleteAsync(); await _db.MobileLines.ExecuteDeleteAsync();
var imported = 0; var imported = 0;
@ -252,15 +333,12 @@ namespace line_gestao_api.Controllers
var entity = new MobileLine var entity = new MobileLine
{ {
Item = TryInt(itemStr), Item = TryInt(itemStr),
Conta = GetCellByHeader(ws, r, map, "CONTA"), Conta = GetCellByHeader(ws, r, map, "CONTA"),
Linha = OnlyDigits(GetCellByHeader(ws, r, map, "LINHA")), Linha = OnlyDigits(GetCellByHeader(ws, r, map, "LINHA")),
Chip = OnlyDigits(GetCellByHeader(ws, r, map, "CHIP")), Chip = OnlyDigits(GetCellByHeader(ws, r, map, "CHIP")),
Cliente = GetCellByHeader(ws, r, map, "CLIENTE"), Cliente = GetCellByHeader(ws, r, map, "CLIENTE"),
Usuario = GetCellByHeader(ws, r, map, "USUARIO"), Usuario = GetCellByHeader(ws, r, map, "USUARIO"),
PlanoContrato = GetCellByHeader(ws, r, map, "PLANO CONTRATO"), PlanoContrato = GetCellByHeader(ws, r, map, "PLANO CONTRATO"),
FranquiaVivo = TryDecimal(GetCellByHeader(ws, r, map, "FRAQUIA")), FranquiaVivo = TryDecimal(GetCellByHeader(ws, r, map, "FRAQUIA")),
ValorPlanoVivo = TryDecimal(GetCellByHeader(ws, r, map, "VALOR DO PLANO R$")), ValorPlanoVivo = TryDecimal(GetCellByHeader(ws, r, map, "VALOR DO PLANO R$")),
GestaoVozDados = TryDecimal(GetCellByHeader(ws, r, map, "GESTAO VOZ E DADOS R$")), GestaoVozDados = TryDecimal(GetCellByHeader(ws, r, map, "GESTAO VOZ E DADOS R$")),
@ -269,30 +347,24 @@ namespace line_gestao_api.Controllers
VivoTravelMundo = TryDecimal(GetCellByHeader(ws, r, map, "VIVO TRAVEL MUNDO")), VivoTravelMundo = TryDecimal(GetCellByHeader(ws, r, map, "VIVO TRAVEL MUNDO")),
VivoGestaoDispositivo = TryDecimal(GetCellByHeader(ws, r, map, "VIVO GESTAO DISPOSITIVO")), VivoGestaoDispositivo = TryDecimal(GetCellByHeader(ws, r, map, "VIVO GESTAO DISPOSITIVO")),
ValorContratoVivo = TryDecimal(GetCellByHeader(ws, r, map, "VALOR CONTRATO VIVO")), ValorContratoVivo = TryDecimal(GetCellByHeader(ws, r, map, "VALOR CONTRATO VIVO")),
FranquiaLine = TryDecimal(GetCellByHeader(ws, r, map, "FRANQUIA LINE")), FranquiaLine = TryDecimal(GetCellByHeader(ws, r, map, "FRANQUIA LINE")),
FranquiaGestao = TryDecimal(GetCellByHeader(ws, r, map, "FRANQUIA GESTAO")), FranquiaGestao = TryDecimal(GetCellByHeader(ws, r, map, "FRANQUIA GESTAO")),
LocacaoAp = TryDecimal(GetCellByHeader(ws, r, map, "LOCACAO AP.")), LocacaoAp = TryDecimal(GetCellByHeader(ws, r, map, "LOCACAO AP.")),
ValorContratoLine = TryDecimal(GetCellByHeader(ws, r, map, "VALOR CONTRATO LINE")), ValorContratoLine = TryDecimal(GetCellByHeader(ws, r, map, "VALOR CONTRATO LINE")),
Desconto = TryDecimal(GetCellByHeader(ws, r, map, "DESCONTO")), Desconto = TryDecimal(GetCellByHeader(ws, r, map, "DESCONTO")),
Lucro = TryDecimal(GetCellByHeader(ws, r, map, "LUCRO")), Lucro = TryDecimal(GetCellByHeader(ws, r, map, "LUCRO")),
Status = GetCellByHeader(ws, r, map, "STATUS"), Status = GetCellByHeader(ws, r, map, "STATUS"),
DataBloqueio = TryDate(ws, r, map, "DATA DO BLOQUEIO"), DataBloqueio = TryDate(ws, r, map, "DATA DO BLOQUEIO"),
Skil = GetCellByHeader(ws, r, map, "SKIL"), Skil = GetCellByHeader(ws, r, map, "SKIL"),
Modalidade = GetCellByHeader(ws, r, map, "MODALIDADE"), Modalidade = GetCellByHeader(ws, r, map, "MODALIDADE"),
Cedente = GetCellByHeader(ws, r, map, "CEDENTE"), Cedente = GetCellByHeader(ws, r, map, "CEDENTE"),
Solicitante = GetCellByHeader(ws, r, map, "SOLICITANTE"), Solicitante = GetCellByHeader(ws, r, map, "SOLICITANTE"),
DataEntregaOpera = TryDate(ws, r, map, "DATA DA ENTREGA OPERA."), DataEntregaOpera = TryDate(ws, r, map, "DATA DA ENTREGA OPERA."),
DataEntregaCliente = TryDate(ws, r, map, "DATA DA ENTREGA CLIENTE"), 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"),
}; };
ApplyReservaRule(entity); ApplyReservaRule(entity);
buffer.Add(entity); buffer.Add(entity);
imported++; imported++;
@ -313,19 +385,19 @@ namespace line_gestao_api.Controllers
return Ok(new ImportResultDto { Imported = imported }); return Ok(new ImportResultDto { Imported = imported });
} }
// ================= helpers ================= // ==========================================================
// HELPERS
// ==========================================================
private static DateTime? ToUtc(DateTime? dt) private static DateTime? ToUtc(DateTime? dt)
{ {
if (dt == null) return null; if (dt == null) return null;
var v = dt.Value; var v = dt.Value;
return v.Kind switch return v.Kind switch
{ {
DateTimeKind.Utc => v, DateTimeKind.Utc => v,
DateTimeKind.Local => v.ToUniversalTime(), DateTimeKind.Local => v.ToUniversalTime(),
_ => DateTime.SpecifyKind(v, DateTimeKind.Utc) // Unspecified -> UTC (sem shift) _ => DateTime.SpecifyKind(v, DateTimeKind.Utc)
}; };
} }
@ -339,7 +411,6 @@ namespace line_gestao_api.Controllers
Cliente = x.Cliente, Cliente = x.Cliente,
Usuario = x.Usuario, Usuario = x.Usuario,
PlanoContrato = x.PlanoContrato, PlanoContrato = x.PlanoContrato,
FranquiaVivo = x.FranquiaVivo, FranquiaVivo = x.FranquiaVivo,
ValorPlanoVivo = x.ValorPlanoVivo, ValorPlanoVivo = x.ValorPlanoVivo,
GestaoVozDados = x.GestaoVozDados, GestaoVozDados = x.GestaoVozDados,
@ -348,15 +419,12 @@ namespace line_gestao_api.Controllers
VivoTravelMundo = x.VivoTravelMundo, VivoTravelMundo = x.VivoTravelMundo,
VivoGestaoDispositivo = x.VivoGestaoDispositivo, VivoGestaoDispositivo = x.VivoGestaoDispositivo,
ValorContratoVivo = x.ValorContratoVivo, ValorContratoVivo = x.ValorContratoVivo,
FranquiaLine = x.FranquiaLine, FranquiaLine = x.FranquiaLine,
FranquiaGestao = x.FranquiaGestao, FranquiaGestao = x.FranquiaGestao,
LocacaoAp = x.LocacaoAp, LocacaoAp = x.LocacaoAp,
ValorContratoLine = x.ValorContratoLine, ValorContratoLine = x.ValorContratoLine,
Desconto = x.Desconto, Desconto = x.Desconto,
Lucro = x.Lucro, Lucro = x.Lucro,
Status = x.Status, Status = x.Status,
DataBloqueio = x.DataBloqueio, DataBloqueio = x.DataBloqueio,
Skil = x.Skil, Skil = x.Skil,
@ -372,7 +440,6 @@ namespace line_gestao_api.Controllers
{ {
var cliente = (x.Cliente ?? "").Trim(); var cliente = (x.Cliente ?? "").Trim();
var usuario = (x.Usuario ?? "").Trim(); var usuario = (x.Usuario ?? "").Trim();
if (cliente.Equals("RESERVA", StringComparison.OrdinalIgnoreCase) || if (cliente.Equals("RESERVA", StringComparison.OrdinalIgnoreCase) ||
usuario.Equals("RESERVA", StringComparison.OrdinalIgnoreCase)) usuario.Equals("RESERVA", StringComparison.OrdinalIgnoreCase))
{ {
@ -382,8 +449,7 @@ namespace line_gestao_api.Controllers
} }
} }
private static int GetCol(Dictionary<string, int> map, string name) private static int GetCol(Dictionary<string, int> map, string name) => map.TryGetValue(NormalizeHeader(name), out var c) ? c : 0;
=> map.TryGetValue(NormalizeHeader(name), out var c) ? c : 0;
private static string GetCellByHeader(IXLWorksheet ws, int row, Dictionary<string, int> map, string header) private static string GetCellByHeader(IXLWorksheet ws, int row, Dictionary<string, int> map, string header)
{ {
@ -406,7 +472,6 @@ namespace line_gestao_api.Controllers
if (!map.TryGetValue(key, out var col)) return null; if (!map.TryGetValue(key, out var col)) return null;
var cell = ws.Cell(row, col); var cell = ws.Cell(row, col);
if (cell.DataType == XLDataType.DateTime) if (cell.DataType == XLDataType.DateTime)
return ToUtc(cell.GetDateTime()); return ToUtc(cell.GetDateTime());
@ -425,28 +490,19 @@ namespace line_gestao_api.Controllers
private static decimal? TryDecimal(string? s) private static decimal? TryDecimal(string? s)
{ {
if (string.IsNullOrWhiteSpace(s)) return null; if (string.IsNullOrWhiteSpace(s)) return null;
// remove "R$", espaços etc.
s = s.Replace("R$", "", StringComparison.OrdinalIgnoreCase).Trim(); 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, new CultureInfo("pt-BR"), out var d)) if (decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out d)) return d;
return d;
if (decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out d))
return d;
return null; return null;
} }
private static int TryInt(string s) private static int TryInt(string s) => int.TryParse(OnlyDigits(s), out var n) ? n : 0;
=> int.TryParse(OnlyDigits(s), out var n) ? n : 0;
private static string OnlyDigits(string? s) private static string OnlyDigits(string? s)
{ {
if (string.IsNullOrWhiteSpace(s)) return ""; if (string.IsNullOrWhiteSpace(s)) return "";
var sb = new StringBuilder(); var sb = new StringBuilder();
foreach (var ch in s) foreach (var ch in s) if (char.IsDigit(ch)) sb.Append(ch);
if (char.IsDigit(ch)) sb.Append(ch);
return sb.ToString(); return sb.ToString();
} }
@ -454,8 +510,6 @@ namespace line_gestao_api.Controllers
{ {
if (string.IsNullOrWhiteSpace(s)) return ""; if (string.IsNullOrWhiteSpace(s)) return "";
s = s.Trim().ToUpperInvariant(); s = s.Trim().ToUpperInvariant();
// remove acentos
var formD = s.Normalize(NormalizationForm.FormD); var formD = s.Normalize(NormalizationForm.FormD);
var sb = new StringBuilder(); var sb = new StringBuilder();
foreach (var ch in formD) foreach (var ch in formD)
@ -463,14 +517,11 @@ namespace line_gestao_api.Controllers
sb.Append(ch); sb.Append(ch);
s = sb.ToString().Normalize(NormalizationForm.FormC); s = sb.ToString().Normalize(NormalizationForm.FormC);
s = s.Replace("ITEM", "ITEM")
.Replace("USUARIO", "USUARIO")
.Replace("GESTAO", "GESTAO")
.Replace("LOCACAO", "LOCACAO");
// normalizações pra casar com a planilha
s = s.Replace("ITÉM", "ITEM")
.Replace("USUÁRIO", "USUARIO")
.Replace("GESTÃO", "GESTAO")
.Replace("LOCAÇÃO", "LOCACAO");
// remove espaços duplicados
s = string.Join(" ", s.Split(' ', StringSplitOptions.RemoveEmptyEntries)); s = string.Join(" ", s.Split(' ', StringSplitOptions.RemoveEmptyEntries));
return s; return s;
} }

10
Dtos/ClientGroupDto.cs Normal file
View File

@ -0,0 +1,10 @@
namespace line_gestao_api.Dtos
{
public class ClientGroupDto
{
public string Cliente { get; set; } = string.Empty;
public int TotalLinhas { get; set; }
public int Ativos { get; set; }
public int Bloqueados { get; set; }
}
}

View File

@ -55,9 +55,43 @@
public string? VencConta { get; set; } public string? VencConta { get; set; }
} }
public class UpdateMobileLineRequest : MobileLineDetailDto // ✅ UPDATE REQUEST (SEM Id)
public class UpdateMobileLineRequest
{ {
// reaproveita os campos; Id vem na rota public int Item { get; set; }
public string? Conta { get; set; }
public string? Linha { get; set; }
public string? Chip { get; set; }
public string? Cliente { get; set; }
public string? Usuario { get; set; }
public string? PlanoContrato { get; set; }
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; }
public decimal? FranquiaLine { get; set; }
public decimal? FranquiaGestao { get; set; }
public decimal? LocacaoAp { get; set; }
public decimal? ValorContratoLine { get; set; }
public decimal? Desconto { get; set; }
public decimal? Lucro { get; set; }
public string? Status { get; set; }
public DateTime? DataBloqueio { get; set; }
public string? Skil { get; set; }
public string? Modalidade { get; set; }
public string? Cedente { get; set; }
public string? Solicitante { get; set; }
public DateTime? DataEntregaOpera { get; set; }
public DateTime? DataEntregaCliente { get; set; }
public string? VencConta { get; set; }
} }
public class ImportResultDto public class ImportResultDto