using System.Globalization; using System.Text; using System.Text.RegularExpressions; using line_gestao_api.Data; using line_gestao_api.Dtos; using Microsoft.EntityFrameworkCore; namespace line_gestao_api.Services { public class GeralDashboardInsightsService { private static readonly CultureInfo PtBr = new("pt-BR"); private const string ServiceGestaoVozDados = "GESTÃO VOZ E DADOS"; private const string ServiceSkeelo = "SKEELO"; private const string ServiceVivoNewsPlus = "VIVO NEWS PLUS"; private const string ServiceVivoTravelMundo = "VIVO TRAVEL MUNDO"; private const string ServiceVivoSync = "VIVO SYNC"; private const string ServiceVivoGestaoDispositivo = "VIVO GESTÃO DISPOSITIVO"; private readonly AppDbContext _db; public GeralDashboardInsightsService(AppDbContext db) { _db = db; } public async Task GetInsightsAsync() { var qLines = _db.MobileLines.AsNoTracking(); var totals = await qLines .GroupBy(_ => 1) .Select(g => new TotalsProjection { TotalLinhas = g.Count(), TotalAtivas = g.Count(x => (x.Status ?? "").ToLower().Contains("ativo")), TotalBloqueados = g.Count(x => (x.Status ?? "").ToLower().Contains("bloque") || (x.Status ?? "").ToLower().Contains("perda") || (x.Status ?? "").ToLower().Contains("roubo")), VivoLinhas = g.Count(x => x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null), VivoBaseTotal = g.Sum(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) ? (x.ValorPlanoVivo ?? 0m) : 0m), VivoFranquiaTotalGb = g.Sum(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) ? (x.FranquiaVivo ?? 0m) : 0m), VivoAdicionaisTotal = g.Sum(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) ? (x.GestaoVozDados ?? 0m) + (x.Skeelo ?? 0m) + (x.VivoNewsPlus ?? 0m) + (x.VivoTravelMundo ?? 0m) + (x.VivoSync ?? 0m) + (x.VivoGestaoDispositivo ?? 0m) : 0m), VivoMinBase = g.Where(x => x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) .Select(x => x.ValorPlanoVivo ?? 0m) .DefaultIfEmpty() .Min(), VivoMaxBase = g.Where(x => x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) .Select(x => x.ValorPlanoVivo ?? 0m) .DefaultIfEmpty() .Max(), TravelCom = g.Count(x => (x.VivoTravelMundo ?? 0m) > 0m), TravelSem = g.Count(x => (x.VivoTravelMundo ?? 0m) <= 0m), TravelTotal = g.Sum(x => x.VivoTravelMundo ?? 0m), PaidGestaoVozDados = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.GestaoVozDados ?? 0m) > 0m), PaidSkeelo = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.Skeelo ?? 0m) > 0m), PaidNews = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.VivoNewsPlus ?? 0m) > 0m), PaidTravel = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.VivoTravelMundo ?? 0m) > 0m), PaidGestaoDispositivo = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.VivoGestaoDispositivo ?? 0m) > 0m), PaidSync = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.VivoSync ?? 0m) > 0m), NotPaidGestaoVozDados = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.GestaoVozDados ?? 0m) <= 0m), NotPaidSkeelo = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.Skeelo ?? 0m) <= 0m), NotPaidNews = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.VivoNewsPlus ?? 0m) <= 0m), NotPaidTravel = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.VivoTravelMundo ?? 0m) <= 0m), NotPaidGestaoDispositivo = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.VivoGestaoDispositivo ?? 0m) <= 0m), NotPaidSync = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.VivoSync ?? 0m) <= 0m), TotalLinesWithAnyPaidAdditional = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && ((x.GestaoVozDados ?? 0m) > 0m || (x.Skeelo ?? 0m) > 0m || (x.VivoNewsPlus ?? 0m) > 0m || (x.VivoTravelMundo ?? 0m) > 0m || (x.VivoSync ?? 0m) > 0m || (x.VivoGestaoDispositivo ?? 0m) > 0m)), TotalLinesWithNoPaidAdditional = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.GestaoVozDados ?? 0m) <= 0m && (x.Skeelo ?? 0m) <= 0m && (x.VivoNewsPlus ?? 0m) <= 0m && (x.VivoTravelMundo ?? 0m) <= 0m && (x.VivoSync ?? 0m) <= 0m && (x.VivoGestaoDispositivo ?? 0m) <= 0m), TotalGestaoVozDados = g.Sum(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) ? (x.GestaoVozDados ?? 0m) : 0m), TotalSkeelo = g.Sum(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) ? (x.Skeelo ?? 0m) : 0m), TotalNews = g.Sum(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) ? (x.VivoNewsPlus ?? 0m) : 0m), TotalTravel = g.Sum(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) ? (x.VivoTravelMundo ?? 0m) : 0m), TotalGestaoDispositivo = g.Sum(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) ? (x.VivoGestaoDispositivo ?? 0m) : 0m), TotalSync = g.Sum(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) ? (x.VivoSync ?? 0m) : 0m) }) .FirstOrDefaultAsync(); var franquiasRaw = await qLines .Select(x => new FranquiaProjection { FranquiaVivo = x.FranquiaVivo, PlanoContrato = x.PlanoContrato }) .ToListAsync(); var linhasPorFranquia = BuildFranquiaBuckets(franquiasRaw); var tipoChip = await qLines.Select(x => x.TipoDeChip).ToListAsync(); var tipoChipBuckets = BuildTipoChipBuckets(tipoChip); var clientGroupsRaw = await qLines .Where(x => x.Cliente != null && x.Cliente != "") .GroupBy(x => x.Cliente!) .Select(g => new ClientGroupProjection { Cliente = g.Key, TotalLinhas = g.Count(), Ativos = g.Count(x => (x.Status ?? "").ToLower().Contains("ativo")), Bloqueados = g.Count(x => (x.Status ?? "").ToLower().Contains("bloque") || (x.Status ?? "").ToLower().Contains("perda") || (x.Status ?? "").ToLower().Contains("roubo")), LinhasVivo = g.Count(x => x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null), TravelCom = g.Count(x => (x.VivoTravelMundo ?? 0m) > 0m), TravelSem = g.Count(x => (x.VivoTravelMundo ?? 0m) <= 0m), PaidGestaoVozDados = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.GestaoVozDados ?? 0m) > 0m), PaidSkeelo = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.Skeelo ?? 0m) > 0m), PaidNews = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.VivoNewsPlus ?? 0m) > 0m), PaidTravel = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.VivoTravelMundo ?? 0m) > 0m), PaidGestaoDispositivo = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.VivoGestaoDispositivo ?? 0m) > 0m), PaidSync = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.VivoSync ?? 0m) > 0m), NotPaidGestaoVozDados = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.GestaoVozDados ?? 0m) <= 0m), NotPaidSkeelo = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.Skeelo ?? 0m) <= 0m), NotPaidNews = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.VivoNewsPlus ?? 0m) <= 0m), NotPaidTravel = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.VivoTravelMundo ?? 0m) <= 0m), NotPaidGestaoDispositivo = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.VivoGestaoDispositivo ?? 0m) <= 0m), NotPaidSync = g.Count(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) && (x.VivoSync ?? 0m) <= 0m), TotalGestaoVozDados = g.Sum(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) ? (x.GestaoVozDados ?? 0m) : 0m), TotalSkeelo = g.Sum(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) ? (x.Skeelo ?? 0m) : 0m), TotalNews = g.Sum(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) ? (x.VivoNewsPlus ?? 0m) : 0m), TotalTravel = g.Sum(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) ? (x.VivoTravelMundo ?? 0m) : 0m), TotalGestaoDispositivo = g.Sum(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) ? (x.VivoGestaoDispositivo ?? 0m) : 0m), TotalSync = g.Sum(x => (x.ValorPlanoVivo != null || x.FranquiaVivo != null || x.ValorContratoVivo != null || x.GestaoVozDados != null || x.Skeelo != null || x.VivoNewsPlus != null || x.VivoTravelMundo != null || x.VivoSync != null || x.VivoGestaoDispositivo != null) ? (x.VivoSync ?? 0m) : 0m) }) .OrderBy(x => x.Cliente) .ToListAsync(); var dto = new GeralDashboardInsightsDto { Kpis = BuildKpis(totals), Charts = BuildCharts(totals, linhasPorFranquia, tipoChipBuckets), ClientGroups = BuildClientGroups(clientGroupsRaw) }; return dto; } private static GeralDashboardKpisDto BuildKpis(TotalsProjection? totals) { if (totals == null) { return new GeralDashboardKpisDto { Vivo = new GeralDashboardVivoKpiDto(), TravelMundo = new GeralDashboardTravelKpiDto(), Adicionais = new GeralDashboardAdditionalKpiDto() }; } var totalGeralMensal = totals.VivoBaseTotal + totals.VivoAdicionaisTotal; var media = totals.VivoLinhas > 0 ? totalGeralMensal / totals.VivoLinhas : 0m; return new GeralDashboardKpisDto { TotalLinhas = totals.TotalLinhas, TotalAtivas = totals.TotalAtivas, Vivo = new GeralDashboardVivoKpiDto { QtdLinhas = totals.VivoLinhas, TotalFranquiaGb = totals.VivoFranquiaTotalGb, TotalBaseMensal = totals.VivoBaseTotal, TotalAdicionaisMensal = totals.VivoAdicionaisTotal, TotalGeralMensal = totalGeralMensal, MediaPorLinha = media, MinPorLinha = totals.VivoLinhas > 0 ? totals.VivoMinBase : null, MaxPorLinha = totals.VivoLinhas > 0 ? totals.VivoMaxBase : null }, TravelMundo = new GeralDashboardTravelKpiDto { ComTravel = totals.TravelCom, SemTravel = totals.TravelSem, TotalValue = totals.TravelTotal }, Adicionais = new GeralDashboardAdditionalKpiDto { TotalLinesWithAnyPaidAdditional = totals.TotalLinesWithAnyPaidAdditional, TotalLinesWithNoPaidAdditional = totals.TotalLinesWithNoPaidAdditional, ServicesPaid = new List { new() { ServiceName = ServiceGestaoVozDados, CountLines = totals.PaidGestaoVozDados, TotalValue = totals.TotalGestaoVozDados }, new() { ServiceName = ServiceSkeelo, CountLines = totals.PaidSkeelo, TotalValue = totals.TotalSkeelo }, new() { ServiceName = ServiceVivoNewsPlus, CountLines = totals.PaidNews, TotalValue = totals.TotalNews }, new() { ServiceName = ServiceVivoTravelMundo, CountLines = totals.PaidTravel, TotalValue = totals.TotalTravel }, new() { ServiceName = ServiceVivoSync, CountLines = totals.PaidSync, TotalValue = totals.TotalSync }, new() { ServiceName = ServiceVivoGestaoDispositivo, CountLines = totals.PaidGestaoDispositivo, TotalValue = totals.TotalGestaoDispositivo } }, ServicesNotPaid = new List { new() { ServiceName = ServiceGestaoVozDados, CountLines = totals.NotPaidGestaoVozDados, TotalValue = 0m }, new() { ServiceName = ServiceSkeelo, CountLines = totals.NotPaidSkeelo, TotalValue = 0m }, new() { ServiceName = ServiceVivoNewsPlus, CountLines = totals.NotPaidNews, TotalValue = 0m }, new() { ServiceName = ServiceVivoTravelMundo, CountLines = totals.NotPaidTravel, TotalValue = 0m }, new() { ServiceName = ServiceVivoSync, CountLines = totals.NotPaidSync, TotalValue = 0m }, new() { ServiceName = ServiceVivoGestaoDispositivo, CountLines = totals.NotPaidGestaoDispositivo, TotalValue = 0m } } } }; } private static GeralDashboardChartsDto BuildCharts(TotalsProjection? totals, FranquiaBuckets franquias, TipoChipBuckets tipoChip) { var adicionaisLabels = new List { ServiceGestaoVozDados, ServiceSkeelo, ServiceVivoNewsPlus, ServiceVivoTravelMundo, ServiceVivoSync, ServiceVivoGestaoDispositivo }; var adicionaisValues = totals == null ? new List { 0, 0, 0, 0, 0, 0 } : new List { totals.PaidGestaoVozDados, totals.PaidSkeelo, totals.PaidNews, totals.PaidTravel, totals.PaidSync, totals.PaidGestaoDispositivo }; var adicionaisTotals = totals == null ? new List { 0m, 0m, 0m, 0m, 0m, 0m } : new List { totals.TotalGestaoVozDados, totals.TotalSkeelo, totals.TotalNews, totals.TotalTravel, totals.TotalSync, totals.TotalGestaoDispositivo }; return new GeralDashboardChartsDto { LinhasPorFranquia = new GeralDashboardChartDto { Labels = franquias.Labels, Values = franquias.Values }, AdicionaisPagosPorServico = new GeralDashboardChartDto { Labels = adicionaisLabels, Values = adicionaisValues, Totals = adicionaisTotals }, TravelMundo = new GeralDashboardChartDto { Labels = new List { "Com", "Sem" }, Values = totals == null ? new List { 0, 0 } : new List { totals.TravelCom, totals.TravelSem } }, TipoChip = new GeralDashboardChartDto { Labels = tipoChip.Labels, Values = tipoChip.Values } }; } private static List BuildClientGroups(IEnumerable rows) { var list = new List(); foreach (var row in rows) { var baseTags = new List { new() { Label = "LINHAS", Value = row.TotalLinhas.ToString(PtBr) }, new() { Label = "ATIVAS", Value = row.Ativos.ToString(PtBr) }, new() { Label = "BLOQUEADAS", Value = row.Bloqueados.ToString(PtBr) }, new() { Label = "LINHAS VIVO", Value = row.LinhasVivo.ToString(PtBr) } }; var extras = new List { new() { Label = "TRAVEL MUNDO", Value = row.TravelCom.ToString(PtBr) }, new() { Label = "SEM TRAVEL", Value = row.TravelSem.ToString(PtBr) }, new() { Label = ServiceGestaoVozDados, Value = row.PaidGestaoVozDados.ToString(PtBr) }, new() { Label = $"R$ {ServiceGestaoVozDados}", Value = FormatCurrency(row.TotalGestaoVozDados) }, new() { Label = ServiceSkeelo, Value = row.PaidSkeelo.ToString(PtBr) }, new() { Label = $"R$ {ServiceSkeelo}", Value = FormatCurrency(row.TotalSkeelo) }, new() { Label = ServiceVivoNewsPlus, Value = row.PaidNews.ToString(PtBr) }, new() { Label = $"R$ {ServiceVivoNewsPlus}", Value = FormatCurrency(row.TotalNews) }, new() { Label = ServiceVivoTravelMundo, Value = row.PaidTravel.ToString(PtBr) }, new() { Label = $"R$ {ServiceVivoTravelMundo}", Value = FormatCurrency(row.TotalTravel) }, new() { Label = ServiceVivoSync, Value = row.PaidSync.ToString(PtBr) }, new() { Label = $"R$ {ServiceVivoSync}", Value = FormatCurrency(row.TotalSync) }, new() { Label = ServiceVivoGestaoDispositivo, Value = row.PaidGestaoDispositivo.ToString(PtBr) }, new() { Label = $"R$ {ServiceVivoGestaoDispositivo}", Value = FormatCurrency(row.TotalGestaoDispositivo) } }; var serviceDetails = new List { new() { ServiceName = ServiceGestaoVozDados, PaidCount = row.PaidGestaoVozDados, NotPaidCount = row.NotPaidGestaoVozDados, TotalValue = row.TotalGestaoVozDados }, new() { ServiceName = ServiceSkeelo, PaidCount = row.PaidSkeelo, NotPaidCount = row.NotPaidSkeelo, TotalValue = row.TotalSkeelo }, new() { ServiceName = ServiceVivoNewsPlus, PaidCount = row.PaidNews, NotPaidCount = row.NotPaidNews, TotalValue = row.TotalNews }, new() { ServiceName = ServiceVivoTravelMundo, PaidCount = row.PaidTravel, NotPaidCount = row.NotPaidTravel, TotalValue = row.TotalTravel }, new() { ServiceName = ServiceVivoSync, PaidCount = row.PaidSync, NotPaidCount = row.NotPaidSync, TotalValue = row.TotalSync }, new() { ServiceName = ServiceVivoGestaoDispositivo, PaidCount = row.PaidGestaoDispositivo, NotPaidCount = row.NotPaidGestaoDispositivo, TotalValue = row.TotalGestaoDispositivo } }; list.Add(new GeralDashboardClientGroupDto { Cliente = row.Cliente, TotalLinhas = row.TotalLinhas, Ativos = row.Ativos, Bloqueados = row.Bloqueados, LinhasVivo = row.LinhasVivo, TagsBase = baseTags, TagsExtras = extras, AdicionaisPorServico = serviceDetails }); } return list; } private static TipoChipBuckets BuildTipoChipBuckets(IEnumerable chipTypes) { var esim = 0; var simcard = 0; foreach (var raw in chipTypes) { var normalized = NormalizeChipType(raw); if (normalized == "ESIM") { esim++; continue; } if (normalized == "SIMCARD") { simcard++; } } return new TipoChipBuckets { Labels = new List { "e-SIM", "SIMCARD" }, Values = new List { esim, simcard } }; } private static string NormalizeChipType(string? value) { var normalized = NormalizeText(value); if (string.IsNullOrWhiteSpace(normalized)) { return string.Empty; } if (normalized.Contains("ESIM")) { return "ESIM"; } if (normalized.Contains("SIM") || normalized.Contains("CHIP") || normalized.Contains("CARD") || normalized.Contains("FISIC")) { return "SIMCARD"; } return string.Empty; } private static string NormalizeText(string? value) { if (string.IsNullOrWhiteSpace(value)) return string.Empty; var decomposed = value .Trim() .ToUpperInvariant() .Normalize(NormalizationForm.FormD); var builder = new StringBuilder(decomposed.Length); foreach (var ch in decomposed) { if (CharUnicodeInfo.GetUnicodeCategory(ch) == UnicodeCategory.NonSpacingMark) continue; if (char.IsLetterOrDigit(ch)) builder.Append(ch); } return builder.ToString(); } private static FranquiaBuckets BuildFranquiaBuckets(IEnumerable rows) { var map = new Dictionary(); foreach (var row in rows) { var label = NormalizeFranquiaLabel(row.FranquiaVivo, row.PlanoContrato); var key = label; if (!map.TryGetValue(key, out var bucket)) { bucket = new FranquiaBucket { Label = label, SortKey = BuildFranquiaSortKey(label) }; map[key] = bucket; } bucket.Count++; } var ordered = map.Values .OrderBy(x => x.SortKey) .ThenBy(x => x.Label) .ToList(); return new FranquiaBuckets { Labels = ordered.Select(x => x.Label).ToList(), Values = ordered.Select(x => x.Count).ToList() }; } private static string NormalizeFranquiaLabel(decimal? value, string? planoContrato) { if (value.HasValue && value.Value > 0) { return NormalizeNumericFranquia(value.Value); } var parsed = ParseFranquiaFromPlano(planoContrato); return string.IsNullOrWhiteSpace(parsed) ? "Sem Franquia" : parsed; } private static string? ParseFranquiaFromPlano(string? planoContrato) { if (string.IsNullOrWhiteSpace(planoContrato)) return null; var match = Regex.Match(planoContrato, @"(?\d+(?:[.,]\d+)?)\s*(?GB|MB)", RegexOptions.IgnoreCase); if (!match.Success) return null; var valueRaw = match.Groups["val"].Value.Replace(",", "."); if (!decimal.TryParse(valueRaw, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) return null; var unit = match.Groups["unit"].Value.ToUpperInvariant(); if (unit == "GB") { return $"{TrimDecimal(value)}GB"; } return $"{Math.Round(value):0}MB"; } private static string NormalizeNumericFranquia(decimal value) { if (value < 1m) { var mb = Math.Round(value * 1000m); return $"{mb:0}MB"; } if (value >= 100m && value < 1024m) { return $"{Math.Round(value):0}MB"; } if (value >= 1024m) { var gb = value / 1024m; return $"{TrimDecimal(gb)}GB"; } return $"{TrimDecimal(value)}GB"; } private static decimal BuildFranquiaSortKey(string label) { if (label.Equals("Sem Franquia", StringComparison.OrdinalIgnoreCase)) { return decimal.MaxValue; } var match = Regex.Match(label, @"(?\d+(?:[.,]\d+)?)\s*(?GB|MB)", RegexOptions.IgnoreCase); if (!match.Success) return decimal.MaxValue - 1; var valueRaw = match.Groups["val"].Value.Replace(",", "."); if (!decimal.TryParse(valueRaw, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) { return decimal.MaxValue - 1; } var unit = match.Groups["unit"].Value.ToUpperInvariant(); return unit == "MB" ? value / 1000m : value; } private static string TrimDecimal(decimal value) { return value % 1 == 0 ? value.ToString("0", CultureInfo.InvariantCulture) : value.ToString("0.#", CultureInfo.InvariantCulture); } private static string FormatCurrency(decimal value) { return value.ToString("C", PtBr); } private sealed class FranquiaBucket { public string Label { get; set; } = string.Empty; public int Count { get; set; } public decimal SortKey { get; set; } } private sealed class FranquiaBuckets { public List Labels { get; set; } = new(); public List Values { get; set; } = new(); } private sealed class TipoChipBuckets { public List Labels { get; set; } = new(); public List Values { get; set; } = new(); } private sealed class FranquiaProjection { public decimal? FranquiaVivo { get; set; } public string? PlanoContrato { get; set; } } private sealed class TotalsProjection { public int TotalLinhas { get; set; } public int TotalAtivas { get; set; } public int TotalBloqueados { get; set; } public int VivoLinhas { get; set; } public decimal VivoFranquiaTotalGb { get; set; } public decimal VivoBaseTotal { get; set; } public decimal VivoAdicionaisTotal { get; set; } public decimal VivoMinBase { get; set; } public decimal VivoMaxBase { get; set; } public int TravelCom { get; set; } public int TravelSem { get; set; } public decimal TravelTotal { get; set; } public int PaidGestaoVozDados { get; set; } public int PaidSkeelo { get; set; } public int PaidNews { get; set; } public int PaidTravel { get; set; } public int PaidGestaoDispositivo { get; set; } public int PaidSync { get; set; } public int NotPaidGestaoVozDados { get; set; } public int NotPaidSkeelo { get; set; } public int NotPaidNews { get; set; } public int NotPaidTravel { get; set; } public int NotPaidGestaoDispositivo { get; set; } public int NotPaidSync { get; set; } public int TotalLinesWithAnyPaidAdditional { get; set; } public int TotalLinesWithNoPaidAdditional { get; set; } public decimal TotalGestaoVozDados { get; set; } public decimal TotalSkeelo { get; set; } public decimal TotalNews { get; set; } public decimal TotalTravel { get; set; } public decimal TotalGestaoDispositivo { get; set; } public decimal TotalSync { get; set; } } private sealed class ClientGroupProjection { public string Cliente { get; set; } = string.Empty; public int TotalLinhas { get; set; } public int Ativos { get; set; } public int Bloqueados { get; set; } public int LinhasVivo { get; set; } public int TravelCom { get; set; } public int TravelSem { get; set; } public int PaidGestaoVozDados { get; set; } public int PaidSkeelo { get; set; } public int PaidNews { get; set; } public int PaidTravel { get; set; } public int PaidGestaoDispositivo { get; set; } public int PaidSync { get; set; } public int NotPaidGestaoVozDados { get; set; } public int NotPaidSkeelo { get; set; } public int NotPaidNews { get; set; } public int NotPaidTravel { get; set; } public int NotPaidGestaoDispositivo { get; set; } public int NotPaidSync { get; set; } public decimal TotalGestaoVozDados { get; set; } public decimal TotalSkeelo { get; set; } public decimal TotalNews { get; set; } public decimal TotalTravel { get; set; } public decimal TotalGestaoDispositivo { get; set; } public decimal TotalSync { get; set; } } } }