line-gestao-api/Controllers/ResumoController.cs

513 lines
20 KiB
C#

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<ActionResult<ResumoResponseDto>> 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<ResumoResponseDto> 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<ResumoResponseDto> 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<ResumoLineTotaisDto>
{
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;
}
}