From 78f403105c484599504f1968a5cf42fd71d9e07e Mon Sep 17 00:00:00 2001 From: Leon Date: Mon, 9 Mar 2026 15:17:28 -0300 Subject: [PATCH] Aplicando filtros por operadora e endpoints para filtragem --- Controllers/DashboardGeralController.cs | 4 +- Controllers/LinesController.cs | 63 ++---- Controllers/RelatoriosController.cs | 43 ++-- Dtos/GeralDashboardInsightsDto.cs | 1 + Dtos/MobileLineDtos.cs | 3 + Models/MobileLine.cs | 1 - Services/GeralDashboardInsightsService.cs | 7 +- Services/OperadoraContaResolver.cs | 260 ++++++++++++++++++++++ 8 files changed, 321 insertions(+), 61 deletions(-) create mode 100644 Services/OperadoraContaResolver.cs diff --git a/Controllers/DashboardGeralController.cs b/Controllers/DashboardGeralController.cs index 52987c8..9bdd8c3 100644 --- a/Controllers/DashboardGeralController.cs +++ b/Controllers/DashboardGeralController.cs @@ -18,9 +18,9 @@ namespace line_gestao_api.Controllers } [HttpGet("insights")] - public async Task> GetInsights() + public async Task> GetInsights([FromQuery] string? operadora = null) { - var dto = await _service.GetInsightsAsync(); + var dto = await _service.GetInsightsAsync(operadora); return Ok(dto); } } diff --git a/Controllers/LinesController.cs b/Controllers/LinesController.cs index 2cfc246..2cb595c 100644 --- a/Controllers/LinesController.cs +++ b/Controllers/LinesController.cs @@ -33,29 +33,6 @@ namespace line_gestao_api.Controllers private readonly SpreadsheetImportAuditService _spreadsheetImportAuditService; private readonly string _aparelhoAttachmentsRootPath; private static readonly FileExtensionContentTypeProvider FileContentTypeProvider = new(); - private static readonly List AccountCompanies = new() - { - new AccountCompanyDto - { - Empresa = "CLARO LINE MÓVEL", - Contas = new List { "172593311", "172593840" } - }, - new AccountCompanyDto - { - Empresa = "VIVO MACROPHONY", - Contas = new List { "0430237019", "0437488125", "0449508564", "0454371844" } - }, - new AccountCompanyDto - { - Empresa = "VIVO LINE MÓVEL", - Contas = new List { "0435288088" } - }, - new AccountCompanyDto - { - Empresa = "TIM LINE MÓVEL", - Contas = new List { "0072046192" } - } - }; public LinesController( AppDbContext db, @@ -390,31 +367,14 @@ namespace line_gestao_api.Controllers [HttpGet("account-companies")] public ActionResult> GetAccountCompanies() { - var items = AccountCompanies - .Select(x => new AccountCompanyDto - { - Empresa = x.Empresa, - Contas = x.Contas.ToList() - }) - .ToList(); - + var items = OperadoraContaResolver.GetAccountCompanies(); return Ok(items); } [HttpGet("accounts")] public ActionResult> GetAccounts([FromQuery] string? empresa) { - if (string.IsNullOrWhiteSpace(empresa)) - return Ok(new List()); - - var target = empresa.Trim(); - - var contas = AccountCompanies - .FirstOrDefault(x => string.Equals(x.Empresa, target, StringComparison.OrdinalIgnoreCase)) - ?.Contas - ?.ToList() - ?? new List(); - + var contas = OperadoraContaResolver.GetAccountsByEmpresa(empresa); return Ok(contas); } @@ -459,6 +419,7 @@ namespace line_gestao_api.Controllers [FromQuery] string? search, [FromQuery] string? skil, [FromQuery] string? client, + [FromQuery] string? operadora, [FromQuery] string? additionalMode, [FromQuery] string? additionalServices, [FromQuery] int page = 1, @@ -491,6 +452,7 @@ namespace line_gestao_api.Controllers q = ExcludeReservaContext(q); q = ApplyAdditionalFilters(q, additionalMode, additionalServices); + q = OperadoraContaResolver.ApplyOperadoraFilter(q, operadora); var sb = (sortBy ?? "item").Trim().ToLowerInvariant(); var desc = string.Equals((sortDir ?? "asc").Trim(), "desc", StringComparison.OrdinalIgnoreCase); @@ -545,6 +507,7 @@ namespace line_gestao_api.Controllers line.Skil, line.Modalidade, line.VencConta, + line.FranquiaVivo, line.FranquiaLine, line.GestaoVozDados, line.Skeelo, @@ -614,6 +577,7 @@ namespace line_gestao_api.Controllers Skil = x.Skil, Modalidade = x.Modalidade, VencConta = x.VencConta, + FranquiaVivo = x.FranquiaVivo, FranquiaLine = x.FranquiaLine, GestaoVozDados = x.GestaoVozDados, Skeelo = x.Skeelo, @@ -625,6 +589,8 @@ namespace line_gestao_api.Controllers }) .ToListAsync(); + EnrichOperadoraContext(itemsReserva); + return Ok(new PagedResult { Page = page, @@ -687,6 +653,7 @@ namespace line_gestao_api.Controllers Skil = x.Skil, Modalidade = x.Modalidade, VencConta = x.VencConta, + FranquiaVivo = x.FranquiaVivo, FranquiaLine = x.FranquiaLine, GestaoVozDados = x.GestaoVozDados, Skeelo = x.Skeelo, @@ -698,6 +665,8 @@ namespace line_gestao_api.Controllers }) .ToListAsync(); + EnrichOperadoraContext(items); + return Ok(new PagedResult { Page = page, @@ -5273,6 +5242,16 @@ namespace line_gestao_api.Controllers return vigencia; } + private static void EnrichOperadoraContext(IEnumerable items) + { + foreach (var item in items ?? Enumerable.Empty()) + { + var context = OperadoraContaResolver.Resolve(item.Conta); + item.ContaEmpresa = context.Empresa; + item.Operadora = context.Operadora; + } + } + private static MobileLineDetailDto ToDetailDto(MobileLine x, VigenciaLine? vigencia = null) => new() { Id = x.Id, diff --git a/Controllers/RelatoriosController.cs b/Controllers/RelatoriosController.cs index f17c415..3d916f2 100644 --- a/Controllers/RelatoriosController.cs +++ b/Controllers/RelatoriosController.cs @@ -1,5 +1,6 @@ using line_gestao_api.Data; using line_gestao_api.Dtos; +using line_gestao_api.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -19,7 +20,7 @@ namespace line_gestao_api.Controllers } [HttpGet("dashboard")] - public async Task> GetDashboard() + public async Task> GetDashboard([FromQuery] string? operadora = null) { var todayUtcStart = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc); var tomorrowUtcStart = todayUtcStart.AddDays(1); @@ -31,32 +32,40 @@ namespace line_gestao_api.Controllers // ========================= // GERAL (MobileLines) // ========================= - var qLines = _db.MobileLines.AsNoTracking(); - var qLinesWithClient = qLines.Where(x => x.Cliente != null && x.Cliente != ""); + var qLines = OperadoraContaResolver.ApplyOperadoraFilter(_db.MobileLines.AsNoTracking(), operadora); + var qReserva = qLines.Where(x => + EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") || + EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") || + EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA")); + var qOperacionais = qLines.Where(x => + !EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") && + !EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") && + !EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA")); + var qOperacionaisWithClient = qOperacionais.Where(x => x.Cliente != null && x.Cliente != ""); var totalLinhas = await qLines.CountAsync(); - var clientesUnicos = await qLines + var clientesUnicos = await qOperacionais .Where(x => x.Cliente != null && x.Cliente != "") .Select(x => x.Cliente!) .Distinct() .CountAsync(); - var ativos = await qLines.CountAsync(x => + var ativos = await qOperacionais.CountAsync(x => EF.Functions.ILike((x.Status ?? "").Trim(), "%ativo%")); - var bloqueadosPerdaRoubo = await qLinesWithClient.CountAsync(x => + var bloqueadosPerdaRoubo = await qOperacionaisWithClient.CountAsync(x => EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") || EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%")); - var bloqueados120Dias = await qLinesWithClient.CountAsync(x => + var bloqueados120Dias = await qOperacionaisWithClient.CountAsync(x => EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") && EF.Functions.ILike((x.Status ?? "").Trim(), "%120%") && EF.Functions.ILike((x.Status ?? "").Trim(), "%dia%") && !(EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") || EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%"))); - var bloqueadosOutros = await qLinesWithClient.CountAsync(x => + var bloqueadosOutros = await qOperacionaisWithClient.CountAsync(x => EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") && !(EF.Functions.ILike((x.Status ?? "").Trim(), "%120%") && EF.Functions.ILike((x.Status ?? "").Trim(), "%dia%")) && !(EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") || EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%")) @@ -64,16 +73,13 @@ namespace line_gestao_api.Controllers // Regra do KPI "Bloqueadas" alinhada ao critério da página Geral: // status contendo "bloque", "perda" ou "roubo". - var bloqueados = await qLinesWithClient.CountAsync(x => + var bloqueados = await qOperacionaisWithClient.CountAsync(x => EF.Functions.ILike((x.Status ?? "").Trim(), "%bloque%") || EF.Functions.ILike((x.Status ?? "").Trim(), "%perda%") || EF.Functions.ILike((x.Status ?? "").Trim(), "%roubo%")); // Regra alinhada ao filtro "Reservas" da página Geral. - var reservas = await qLines.CountAsync(x => - EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") || - EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") || - EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA")); + var reservas = await qReserva.CountAsync(); var topClientes = await qLines .Where(x => x.Cliente != null && x.Cliente != "") @@ -173,7 +179,16 @@ namespace line_gestao_api.Controllers // ========================= // USER DATA // ========================= - var qUserData = _db.UserDatas.AsNoTracking(); + var qLineItems = qLines + .Where(x => x.Item > 0) + .Select(x => x.Item); + var qLineNumbers = qLines + .Where(x => x.Linha != null && x.Linha != "") + .Select(x => x.Linha!); + var qUserData = _db.UserDatas.AsNoTracking() + .Where(x => + (x.Item > 0 && qLineItems.Contains(x.Item)) || + (x.Linha != null && x.Linha != "" && qLineNumbers.Contains(x.Linha))); var userDataRegistros = await qUserData.CountAsync(); var userDataComCpf = await qUserData.CountAsync(x => x.Cpf != null && x.Cpf != ""); diff --git a/Dtos/GeralDashboardInsightsDto.cs b/Dtos/GeralDashboardInsightsDto.cs index 72ed164..8721ab2 100644 --- a/Dtos/GeralDashboardInsightsDto.cs +++ b/Dtos/GeralDashboardInsightsDto.cs @@ -31,6 +31,7 @@ namespace line_gestao_api.Dtos { public int QtdLinhas { get; set; } public decimal TotalFranquiaGb { get; set; } + public decimal TotalFranquiaLine { get; set; } public decimal TotalBaseMensal { get; set; } public decimal TotalAdicionaisMensal { get; set; } public decimal TotalGeralMensal { get; set; } diff --git a/Dtos/MobileLineDtos.cs b/Dtos/MobileLineDtos.cs index 3d9afab..f437793 100644 --- a/Dtos/MobileLineDtos.cs +++ b/Dtos/MobileLineDtos.cs @@ -18,6 +18,9 @@ public string? Skil { get; set; } public string? Modalidade { get; set; } public string? VencConta { get; set; } + public string? ContaEmpresa { get; set; } + public string? Operadora { get; set; } + public decimal? FranquiaVivo { get; set; } public decimal? FranquiaLine { get; set; } // Campos para filtro deterministico de adicionais no frontend diff --git a/Models/MobileLine.cs b/Models/MobileLine.cs index db8bef5..9c097e3 100644 --- a/Models/MobileLine.cs +++ b/Models/MobileLine.cs @@ -75,7 +75,6 @@ namespace line_gestao_api.Models public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - // ✅ Navegação (1 MobileLine -> N Muregs) public ICollection Muregs { get; set; } = new List(); } } diff --git a/Services/GeralDashboardInsightsService.cs b/Services/GeralDashboardInsightsService.cs index a696546..d95f8ab 100644 --- a/Services/GeralDashboardInsightsService.cs +++ b/Services/GeralDashboardInsightsService.cs @@ -25,9 +25,9 @@ namespace line_gestao_api.Services _db = db; } - public async Task GetInsightsAsync() + public async Task GetInsightsAsync(string? operadora = null) { - var qLines = _db.MobileLines.AsNoTracking(); + var qLines = OperadoraContaResolver.ApplyOperadoraFilter(_db.MobileLines.AsNoTracking(), operadora); var totals = await qLines .GroupBy(_ => 1) @@ -99,6 +99,7 @@ namespace line_gestao_api.Services x.VivoGestaoDispositivo != null) ? (x.FranquiaVivo ?? 0m) : 0m), + TotalFranquiaLine = g.Sum(x => x.FranquiaLine ?? 0m), VivoAdicionaisTotal = g.Sum(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || @@ -653,6 +654,7 @@ namespace line_gestao_api.Services { QtdLinhas = totals.VivoLinhas, TotalFranquiaGb = totals.VivoFranquiaTotalGb, + TotalFranquiaLine = totals.TotalFranquiaLine, TotalBaseMensal = totals.VivoBaseTotal, TotalAdicionaisMensal = totals.VivoAdicionaisTotal, TotalGeralMensal = totalGeralMensal, @@ -1110,6 +1112,7 @@ namespace line_gestao_api.Services public int TotalBloqueados { get; set; } public int VivoLinhas { get; set; } public decimal VivoFranquiaTotalGb { get; set; } + public decimal TotalFranquiaLine { get; set; } public decimal VivoBaseTotal { get; set; } public decimal VivoAdicionaisTotal { get; set; } public decimal VivoMinBase { get; set; } diff --git a/Services/OperadoraContaResolver.cs b/Services/OperadoraContaResolver.cs new file mode 100644 index 0000000..188f6e5 --- /dev/null +++ b/Services/OperadoraContaResolver.cs @@ -0,0 +1,260 @@ +using System.Globalization; +using System.Text; +using line_gestao_api.Dtos; +using line_gestao_api.Models; +using Microsoft.EntityFrameworkCore; + +namespace line_gestao_api.Services +{ + public sealed record OperadoraContaContext(string Operadora, string Empresa, string? VivoEmpresaGrupo = null); + + public static class OperadoraContaResolver + { + private const string OperadoraVivo = "VIVO"; + private const string OperadoraClaro = "CLARO"; + private const string OperadoraTim = "TIM"; + private const string OperadoraOutra = "OUTRA"; + + private const string EmpresaVivoMacrophony = "VIVO MACROPHONY"; + private const string EmpresaVivoLineMovel = "VIVO LINE MÓVEL"; + private const string EmpresaClaroLineMovel = "CLARO LINE MÓVEL"; + private const string EmpresaTimLineMovel = "TIM LINE MÓVEL"; + + private static readonly IReadOnlyList CompanyRules = new List + { + new() + { + Empresa = EmpresaClaroLineMovel, + Contas = new List { "172593311", "172593840", "187890982" } + }, + new() + { + Empresa = EmpresaVivoMacrophony, + Contas = new List + { + "0430237019", + "0437488125", + "0449508564", + "0454371844", + "455371844", + "460161507" + } + }, + new() + { + Empresa = EmpresaVivoLineMovel, + Contas = new List { "0435288088" } + }, + new() + { + Empresa = EmpresaTimLineMovel, + Contas = new List { "TIM" } + } + }; + + private static readonly string[] VivoContaFilters = BuildContaFilterSet(EmpresaVivoMacrophony, EmpresaVivoLineMovel); + private static readonly string[] ClaroContaFilters = BuildContaFilterSet(EmpresaClaroLineMovel); + private static readonly string[] TimContaFilters = BuildContaFilterSet(EmpresaTimLineMovel); + private static readonly Dictionary EmpresaByConta = BuildEmpresaByConta(); + + public static List GetAccountCompanies() + { + return CompanyRules + .Select(x => new AccountCompanyDto + { + Empresa = x.Empresa, + Contas = x.Contas.ToList() + }) + .ToList(); + } + + public static List GetAccountsByEmpresa(string? empresa) + { + if (string.IsNullOrWhiteSpace(empresa)) return new List(); + var target = NormalizeToken(empresa); + if (string.IsNullOrWhiteSpace(target)) return new List(); + + return CompanyRules + .FirstOrDefault(x => NormalizeToken(x.Empresa) == target) + ?.Contas + ?.ToList() + ?? new List(); + } + + public static OperadoraContaContext Resolve(string? conta) + { + var contaRaw = (conta ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(contaRaw)) + { + return new OperadoraContaContext(OperadoraOutra, string.Empty, null); + } + + var normalizedConta = NormalizeConta(contaRaw); + if (!string.IsNullOrWhiteSpace(normalizedConta) && EmpresaByConta.TryGetValue(normalizedConta, out var empresaDeterministica)) + { + return BuildContextByEmpresa(empresaDeterministica); + } + + var token = NormalizeToken(contaRaw); + if (token.Contains("TIM", StringComparison.Ordinal)) + { + return BuildContextByEmpresa(EmpresaTimLineMovel); + } + + if (token.Contains("CLARO", StringComparison.Ordinal)) + { + return BuildContextByEmpresa(EmpresaClaroLineMovel); + } + + if (token.Contains("MACROPHONY", StringComparison.Ordinal)) + { + return BuildContextByEmpresa(EmpresaVivoMacrophony); + } + + if (token.Contains("VIVO", StringComparison.Ordinal)) + { + return BuildContextByEmpresa(EmpresaVivoLineMovel); + } + + return new OperadoraContaContext(OperadoraOutra, string.Empty, null); + } + + public static IQueryable ApplyOperadoraFilter(IQueryable query, string? operadora) + { + var token = NormalizeToken(operadora); + if (string.IsNullOrWhiteSpace(token) || token == "TODOS" || token == "ALL") + { + return query; + } + + return token switch + { + OperadoraVivo => query.Where(x => + (x.Conta != null && VivoContaFilters.Contains(x.Conta.Trim())) + || EF.Functions.ILike((x.Conta ?? string.Empty).Trim(), "%VIVO%") + || EF.Functions.ILike((x.Conta ?? string.Empty).Trim(), "%MACROPHONY%")), + OperadoraClaro => query.Where(x => + (x.Conta != null && ClaroContaFilters.Contains(x.Conta.Trim())) + || EF.Functions.ILike((x.Conta ?? string.Empty).Trim(), "%CLARO%")), + OperadoraTim => query.Where(x => + (x.Conta != null && TimContaFilters.Contains(x.Conta.Trim())) + || EF.Functions.ILike((x.Conta ?? string.Empty).Trim(), "%TIM%")), + _ => query + }; + } + + public static string NormalizeConta(string? conta) + { + var raw = (conta ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(raw)) return string.Empty; + + if (raw.All(char.IsDigit)) + { + var noLeadingZero = raw.TrimStart('0'); + return string.IsNullOrWhiteSpace(noLeadingZero) ? "0" : noLeadingZero; + } + + return RemoveDiacritics(raw).ToUpperInvariant(); + } + + private static OperadoraContaContext BuildContextByEmpresa(string empresa) + { + var token = NormalizeToken(empresa); + var operadora = token.Contains("CLARO", StringComparison.Ordinal) + ? OperadoraClaro + : token.Contains("TIM", StringComparison.Ordinal) + ? OperadoraTim + : token.Contains("VIVO", StringComparison.Ordinal) || token.Contains("MACROPHONY", StringComparison.Ordinal) + ? OperadoraVivo + : OperadoraOutra; + + var vivoGrupo = operadora == OperadoraVivo + ? token.Contains("MACROPHONY", StringComparison.Ordinal) + ? "MACROPHONY" + : token.Contains("LINEMOVEL", StringComparison.Ordinal) + ? "LINE MOVEL" + : null + : null; + + return new OperadoraContaContext(operadora, empresa, vivoGrupo); + } + + private static Dictionary BuildEmpresaByConta() + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var group in CompanyRules) + { + foreach (var conta in group.Contas ?? new List()) + { + var normalized = NormalizeConta(conta); + if (string.IsNullOrWhiteSpace(normalized)) continue; + map[normalized] = group.Empresa; + } + } + + return map; + } + + private static string[] BuildContaFilterSet(params string[] empresas) + { + var empresaTokens = new HashSet(empresas.Select(NormalizeToken), StringComparer.OrdinalIgnoreCase); + var set = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var group in CompanyRules) + { + if (!empresaTokens.Contains(NormalizeToken(group.Empresa))) continue; + + foreach (var conta in group.Contas ?? new List()) + { + var trimmed = (conta ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(trimmed)) continue; + + set.Add(trimmed); + + if (trimmed.All(char.IsDigit)) + { + var noLeading = trimmed.TrimStart('0'); + if (!string.IsNullOrWhiteSpace(noLeading)) + { + set.Add(noLeading); + } + } + } + } + + return set.ToArray(); + } + + private static string NormalizeToken(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return string.Empty; + var raw = RemoveDiacritics(value); + var sb = new StringBuilder(raw.Length); + foreach (var ch in raw) + { + if (char.IsLetterOrDigit(ch)) sb.Append(char.ToUpperInvariant(ch)); + } + + return sb.ToString(); + } + + private static string RemoveDiacritics(string value) + { + if (string.IsNullOrWhiteSpace(value)) return string.Empty; + + var normalized = value.Normalize(NormalizationForm.FormD); + var sb = new StringBuilder(normalized.Length); + foreach (var c in normalized) + { + var category = CharUnicodeInfo.GetUnicodeCategory(c); + if (category != UnicodeCategory.NonSpacingMark) + { + sb.Append(c); + } + } + + return sb.ToString().Normalize(NormalizationForm.FormC); + } + } +}