using System.Text.RegularExpressions; using System.Linq; 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; namespace line_gestao_api.Controllers; [ApiController] [Route("api/resumo")] [Authorize] public class ResumoController : ControllerBase { private static readonly Regex PlanGbRegex = new(@"(\d+(?:[.,]\d+)?)\s*(GB|MB)\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); private readonly AppDbContext _db; private readonly SpreadsheetImportAuditService _spreadsheetImportAuditService; public ResumoController(AppDbContext db, SpreadsheetImportAuditService spreadsheetImportAuditService) { _db = db; _spreadsheetImportAuditService = spreadsheetImportAuditService; } [HttpGet] public async Task> GetResumo() { var spreadsheetResponse = await BuildSpreadsheetResumoAsync(); var hasLiveLines = await _db.MobileLines.AsNoTracking().AnyAsync(); if (hasLiveLines) { var live = await BuildLiveResumoAsync(); spreadsheetResponse.MacrophonyPlans = live.MacrophonyPlans; spreadsheetResponse.MacrophonyTotals = live.MacrophonyTotals; spreadsheetResponse.VivoLineResumos = live.VivoLineResumos; spreadsheetResponse.VivoLineTotals = live.VivoLineTotals; spreadsheetResponse.PlanoContratoResumos = live.PlanoContratoResumos; spreadsheetResponse.PlanoContratoTotal = live.PlanoContratoTotal; spreadsheetResponse.LineTotais = live.LineTotais; spreadsheetResponse.GbDistribuicao = live.GbDistribuicao; spreadsheetResponse.GbDistribuicaoTotal = live.GbDistribuicaoTotal; spreadsheetResponse.ReservaLines = live.ReservaLines; spreadsheetResponse.ReservaPorDdd = live.ReservaPorDdd; spreadsheetResponse.TotalGeralLinhasReserva = live.TotalGeralLinhasReserva; spreadsheetResponse.ReservaTotal = live.ReservaTotal; } spreadsheetResponse.MacrophonyTotals ??= new ResumoMacrophonyTotalDto(); spreadsheetResponse.VivoLineTotals ??= new ResumoVivoLineTotalDto(); return Ok(spreadsheetResponse); } private async Task BuildSpreadsheetResumoAsync() { var reservaLines = await _db.ResumoReservaLines.AsNoTracking() .OrderBy(x => x.Ddd) .ToListAsync(); var reservaPorDdd = reservaLines .Where(x => !string.IsNullOrWhiteSpace(x.Ddd)) .GroupBy(x => x.Ddd!.Trim()) .Select(g => new ResumoReservaPorDddDto { Ddd = g.Key, TotalLinhas = g.Sum(x => x.QtdLinhas ?? 0), PorFranquia = g.GroupBy(x => x.FranquiaGb) .Select(fg => new ResumoReservaPorFranquiaDto { FranquiaGb = fg.Key, TotalLinhas = fg.Sum(x => x.QtdLinhas ?? 0) }) .OrderBy(x => x.FranquiaGb) .ToList() }) .OrderBy(x => x.Ddd) .ToList(); var reservaTotalEntity = await _db.ResumoReservaTotals.AsNoTracking() .FirstOrDefaultAsync(); var gbDistribuicaoTotalEntity = await _db.ResumoGbDistribuicaoTotais.AsNoTracking() .FirstOrDefaultAsync(); var canonicalTotalLinhas = await _spreadsheetImportAuditService.GetCanonicalTotalLinhasForReadAsync(); var response = new ResumoResponseDto { MacrophonyPlans = await _db.ResumoMacrophonyPlans.AsNoTracking() .OrderBy(x => x.PlanoContrato) .Select(x => new ResumoMacrophonyPlanDto { PlanoContrato = x.PlanoContrato, Gb = x.Gb, ValorIndividualComSvas = x.ValorIndividualComSvas, FranquiaGb = x.FranquiaGb, TotalLinhas = x.TotalLinhas, ValorTotal = x.ValorTotal, VivoTravel = x.VivoTravel }) .ToListAsync(), MacrophonyTotals = await _db.ResumoMacrophonyTotals.AsNoTracking() .Select(x => new ResumoMacrophonyTotalDto { FranquiaGbTotal = x.FranquiaGbTotal, TotalLinhasTotal = canonicalTotalLinhas, ValorTotal = x.ValorTotal }) .FirstOrDefaultAsync(), VivoLineResumos = await _db.ResumoVivoLineResumos.AsNoTracking() .OrderBy(x => x.Cliente) .Select(x => new ResumoVivoLineResumoDto { Skil = x.Skil, Cliente = x.Cliente, QtdLinhas = x.QtdLinhas, FranquiaTotal = x.FranquiaTotal, ValorContratoVivo = x.ValorContratoVivo, FranquiaLine = x.FranquiaLine, ValorContratoLine = x.ValorContratoLine, Lucro = x.Lucro }) .ToListAsync(), VivoLineTotals = await _db.ResumoVivoLineTotals.AsNoTracking() .Select(x => new ResumoVivoLineTotalDto { QtdLinhasTotal = canonicalTotalLinhas, FranquiaTotal = x.FranquiaTotal, ValorContratoVivo = x.ValorContratoVivo, FranquiaLine = x.FranquiaLine, ValorContratoLine = x.ValorContratoLine, Lucro = x.Lucro }) .FirstOrDefaultAsync(), ClienteEspeciais = await _db.ResumoClienteEspeciais.AsNoTracking() .OrderBy(x => x.Nome) .Select(x => new ResumoClienteEspecialDto { Nome = x.Nome, Valor = x.Valor }) .ToListAsync(), PlanoContratoResumos = await _db.ResumoPlanoContratoResumos.AsNoTracking() .OrderBy(x => x.PlanoContrato) .Select(x => new ResumoPlanoContratoResumoDto { PlanoContrato = x.PlanoContrato, Gb = x.Gb, ValorIndividualComSvas = x.ValorIndividualComSvas, FranquiaGb = x.FranquiaGb, TotalLinhas = x.TotalLinhas, ValorTotal = x.ValorTotal }) .ToListAsync(), PlanoContratoTotal = await _db.ResumoPlanoContratoTotals.AsNoTracking() .Select(x => new ResumoPlanoContratoTotalDto { ValorTotal = x.ValorTotal }) .FirstOrDefaultAsync(), LineTotais = await _db.ResumoLineTotais.AsNoTracking() .OrderBy(x => x.Tipo) .Select(x => new ResumoLineTotaisDto { Tipo = x.Tipo, ValorTotalLine = x.ValorTotalLine, LucroTotalLine = x.LucroTotalLine, QtdLinhas = x.QtdLinhas }) .ToListAsync(), GbDistribuicao = await _db.ResumoGbDistribuicoes.AsNoTracking() .OrderBy(x => x.Gb) .Select(x => new ResumoGbDistribuicaoDto { Gb = x.Gb, Qtd = x.Qtd, Soma = x.Soma }) .ToListAsync(), GbDistribuicaoTotal = gbDistribuicaoTotalEntity == null ? null : new ResumoGbDistribuicaoTotalDto { TotalLinhas = gbDistribuicaoTotalEntity.TotalLinhas, SomaTotal = gbDistribuicaoTotalEntity.SomaTotal }, ReservaLines = reservaLines .Select(x => new ResumoReservaLineDto { Ddd = x.Ddd, FranquiaGb = x.FranquiaGb, QtdLinhas = x.QtdLinhas, Total = x.Total }) .ToList(), ReservaPorDdd = reservaPorDdd, TotalGeralLinhasReserva = reservaTotalEntity?.TotalGeralLinhasReserva ?? reservaTotalEntity?.QtdLinhasTotal ?? reservaPorDdd.Sum(x => x.TotalLinhas), ReservaTotal = reservaTotalEntity == null ? null : new ResumoReservaTotalDto { QtdLinhasTotal = reservaTotalEntity.QtdLinhasTotal, Total = reservaTotalEntity.Total } }; response.MacrophonyTotals ??= new ResumoMacrophonyTotalDto(); response.MacrophonyTotals.TotalLinhasTotal = canonicalTotalLinhas; response.VivoLineTotals ??= new ResumoVivoLineTotalDto(); response.VivoLineTotals.QtdLinhasTotal = canonicalTotalLinhas; return response; } private async Task BuildLiveResumoAsync() { var allLines = _db.MobileLines.AsNoTracking(); var nonReservaLines = allLines.Where(x => !EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") && !EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") && !EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA")); var reservaLinesQuery = allLines.Where(x => EF.Functions.ILike((x.Usuario ?? "").Trim(), "RESERVA") || EF.Functions.ILike((x.Skil ?? "").Trim(), "RESERVA") || EF.Functions.ILike((x.Cliente ?? "").Trim(), "RESERVA")); var planosAgg = await nonReservaLines .GroupBy(x => (x.PlanoContrato ?? "").Trim()) .Select(g => new { Plano = g.Key, TotalLinhas = g.Count(), FranquiaMedia = g.Average(x => (decimal?)x.FranquiaVivo), ValorTotal = g.Sum(x => x.ValorContratoVivo ?? 0m), TravelCount = g.Count(x => (x.VivoTravelMundo ?? 0m) > 0m) }) .ToListAsync(); var planoRows = planosAgg .Select(row => { var planoLabel = string.IsNullOrWhiteSpace(row.Plano) ? "SEM PLANO" : row.Plano; var franquiaGb = (row.FranquiaMedia ?? 0m) > 0m ? row.FranquiaMedia : ExtractGbFromPlanName(planoLabel); var valorIndividual = row.TotalLinhas > 0 ? row.ValorTotal / row.TotalLinhas : (decimal?)null; return new ResumoPlanoContratoResumoDto { PlanoContrato = planoLabel, Gb = franquiaGb, FranquiaGb = franquiaGb, TotalLinhas = row.TotalLinhas, ValorTotal = row.ValorTotal, ValorIndividualComSvas = valorIndividual }; }) .OrderByDescending(x => x.TotalLinhas ?? 0) .ThenBy(x => x.PlanoContrato) .ToList(); var macrophonyRows = planosAgg .Select(row => { var planoLabel = string.IsNullOrWhiteSpace(row.Plano) ? "SEM PLANO" : row.Plano; var franquiaGb = (row.FranquiaMedia ?? 0m) > 0m ? row.FranquiaMedia : ExtractGbFromPlanName(planoLabel); var valorIndividual = row.TotalLinhas > 0 ? row.ValorTotal / row.TotalLinhas : (decimal?)null; return new ResumoMacrophonyPlanDto { PlanoContrato = planoLabel, Gb = franquiaGb, FranquiaGb = franquiaGb, TotalLinhas = row.TotalLinhas, ValorTotal = row.ValorTotal, ValorIndividualComSvas = valorIndividual, VivoTravel = row.TravelCount > 0 }; }) .OrderByDescending(x => x.TotalLinhas ?? 0) .ThenBy(x => x.PlanoContrato) .ToList(); var clientesAgg = await nonReservaLines .GroupBy(x => (x.Cliente ?? "").Trim()) .Select(g => new { Cliente = g.Key, QtdLinhas = g.Count(), FranquiaTotal = g.Sum(x => x.FranquiaVivo ?? 0m), ValorContratoVivo = g.Sum(x => x.ValorContratoVivo ?? 0m), FranquiaLine = g.Sum(x => x.FranquiaLine ?? 0m), ValorContratoLine = g.Sum(x => x.ValorContratoLine ?? 0m), Lucro = g.Sum(x => x.Lucro ?? 0m) }) .ToListAsync(); var clientesRows = clientesAgg .Select(row => new ResumoVivoLineResumoDto { Cliente = string.IsNullOrWhiteSpace(row.Cliente) ? "SEM CLIENTE" : row.Cliente, QtdLinhas = row.QtdLinhas, FranquiaTotal = row.FranquiaTotal, ValorContratoVivo = row.ValorContratoVivo, FranquiaLine = row.FranquiaLine, ValorContratoLine = row.ValorContratoLine, Lucro = row.Lucro }) .OrderByDescending(x => x.QtdLinhas ?? 0) .ThenBy(x => x.Cliente) .ToList(); var totalLinhasNaoReserva = clientesRows.Sum(x => x.QtdLinhas ?? 0); var totalFranquiaNaoReserva = clientesRows.Sum(x => x.FranquiaTotal ?? 0m); var totalValorContratoVivo = clientesRows.Sum(x => x.ValorContratoVivo ?? 0m); var totalFranquiaLine = clientesRows.Sum(x => x.FranquiaLine ?? 0m); var totalValorContratoLine = clientesRows.Sum(x => x.ValorContratoLine ?? 0m); var totalLucro = clientesRows.Sum(x => x.Lucro ?? 0m); var pfAtivas = await nonReservaLines .Where(x => EF.Functions.ILike((x.Status ?? "").Trim(), "%ativo%")) .Where(x => EF.Functions.ILike((x.Skil ?? "").Trim(), "%fís%") || EF.Functions.ILike((x.Skil ?? "").Trim(), "%fis%") || EF.Functions.ILike((x.Skil ?? "").Trim(), "%pf%")) .GroupBy(_ => 1) .Select(g => new { QtdLinhas = g.Count(), ValorTotal = g.Sum(x => x.ValorContratoLine ?? 0m), LucroTotal = g.Sum(x => x.Lucro ?? 0m) }) .FirstOrDefaultAsync(); var pjAtivas = await nonReservaLines .Where(x => EF.Functions.ILike((x.Status ?? "").Trim(), "%ativo%")) .Where(x => EF.Functions.ILike((x.Skil ?? "").Trim(), "%jur%") || EF.Functions.ILike((x.Skil ?? "").Trim(), "%pj%")) .GroupBy(_ => 1) .Select(g => new { QtdLinhas = g.Count(), ValorTotal = g.Sum(x => x.ValorContratoLine ?? 0m), LucroTotal = g.Sum(x => x.Lucro ?? 0m) }) .FirstOrDefaultAsync(); var totaisLine = new List { new() { Tipo = "PF", QtdLinhas = pfAtivas?.QtdLinhas ?? 0, ValorTotalLine = pfAtivas?.ValorTotal ?? 0m, LucroTotalLine = pfAtivas?.LucroTotal ?? 0m }, new() { Tipo = "PJ", QtdLinhas = pjAtivas?.QtdLinhas ?? 0, ValorTotalLine = pjAtivas?.ValorTotal ?? 0m, LucroTotalLine = pjAtivas?.LucroTotal ?? 0m }, new() { Tipo = "TOTAL", QtdLinhas = totalLinhasNaoReserva, ValorTotalLine = totalValorContratoLine, LucroTotalLine = totalLucro } }; var gbDistribuicao = await nonReservaLines .Where(x => (x.FranquiaVivo ?? 0m) > 0m) .GroupBy(x => x.FranquiaVivo ?? 0m) .Select(g => new ResumoGbDistribuicaoDto { Gb = g.Key, Qtd = g.Count(), Soma = g.Sum(x => x.FranquiaVivo ?? 0m) }) .OrderBy(x => x.Gb) .ToListAsync(); var gbDistribuicaoTotal = new ResumoGbDistribuicaoTotalDto { TotalLinhas = gbDistribuicao.Sum(x => x.Qtd ?? 0), SomaTotal = gbDistribuicao.Sum(x => x.Soma ?? 0m) }; var reservaSnapshot = await reservaLinesQuery .Select(x => new { x.Linha, x.FranquiaVivo, x.ValorContratoVivo }) .ToListAsync(); var reservaGroupRaw = reservaSnapshot .Select(row => new { Ddd = ExtractDddFromLine(row.Linha) ?? "-", FranquiaGb = row.FranquiaVivo }) .GroupBy(x => new { x.Ddd, x.FranquiaGb }) .Select(g => new ResumoReservaLineDto { Ddd = g.Key.Ddd, FranquiaGb = g.Key.FranquiaGb, QtdLinhas = g.Count(), Total = null }) .OrderBy(x => x.Ddd) .ThenBy(x => x.FranquiaGb ?? 0m) .ToList(); var reservaPorDdd = reservaGroupRaw .GroupBy(x => x.Ddd ?? "-") .Select(g => new ResumoReservaPorDddDto { Ddd = g.Key, TotalLinhas = g.Sum(x => x.QtdLinhas ?? 0), PorFranquia = g .GroupBy(x => x.FranquiaGb) .Select(fg => new ResumoReservaPorFranquiaDto { FranquiaGb = fg.Key, TotalLinhas = fg.Sum(x => x.QtdLinhas ?? 0) }) .OrderBy(x => x.FranquiaGb ?? 0m) .ToList() }) .OrderBy(x => x.Ddd) .ToList(); var reservaTotalLinhas = reservaSnapshot.Count; var reservaTotalValor = reservaSnapshot.Sum(x => x.ValorContratoVivo ?? 0m); return new ResumoResponseDto { MacrophonyPlans = macrophonyRows, MacrophonyTotals = new ResumoMacrophonyTotalDto { TotalLinhasTotal = totalLinhasNaoReserva, FranquiaGbTotal = totalFranquiaNaoReserva, ValorTotal = totalValorContratoVivo }, VivoLineResumos = clientesRows, VivoLineTotals = new ResumoVivoLineTotalDto { QtdLinhasTotal = totalLinhasNaoReserva, FranquiaTotal = totalFranquiaNaoReserva, ValorContratoVivo = totalValorContratoVivo, FranquiaLine = totalFranquiaLine, ValorContratoLine = totalValorContratoLine, Lucro = totalLucro }, PlanoContratoResumos = planoRows, PlanoContratoTotal = new ResumoPlanoContratoTotalDto { ValorTotal = planoRows.Sum(x => x.ValorTotal ?? 0m) }, LineTotais = totaisLine, GbDistribuicao = gbDistribuicao, GbDistribuicaoTotal = gbDistribuicaoTotal, ReservaLines = reservaGroupRaw, ReservaPorDdd = reservaPorDdd, TotalGeralLinhasReserva = reservaTotalLinhas, ReservaTotal = new ResumoReservaTotalDto { QtdLinhasTotal = reservaTotalLinhas, Total = reservaTotalValor } }; } private static decimal? ExtractGbFromPlanName(string? planoContrato) { if (string.IsNullOrWhiteSpace(planoContrato)) return null; var match = PlanGbRegex.Match(planoContrato); if (!match.Success) return null; var normalized = match.Groups[1].Value.Replace(',', '.'); if (!decimal.TryParse(normalized, out var rawValue)) return null; var unit = match.Groups[2].Value.ToUpperInvariant(); return unit == "MB" ? rawValue / 1000m : rawValue; } private static string? ExtractDddFromLine(string? linha) { if (string.IsNullOrWhiteSpace(linha)) return null; var digits = new string(linha.Where(char.IsDigit).ToArray()); if (digits.Length >= 12 && digits.StartsWith("55")) return digits.Substring(2, 2); if (digits.Length >= 10) return digits.Substring(0, 2); return null; } }