This commit is contained in:
Eduardo Lopes 2026-02-03 17:06:27 -03:00 committed by GitHub
commit 20dcaad10c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1518 additions and 35 deletions

View File

@ -0,0 +1,25 @@
using line_gestao_api.Dtos;
using line_gestao_api.Services;
using Microsoft.AspNetCore.Mvc;
namespace line_gestao_api.Controllers
{
[ApiController]
[Route("api/dashboard/geral")]
public class DashboardGeralController : ControllerBase
{
private readonly GeralDashboardInsightsService _service;
public DashboardGeralController(GeralDashboardInsightsService service)
{
_service = service;
}
[HttpGet("insights")]
public async Task<ActionResult<GeralDashboardInsightsDto>> GetInsights()
{
var dto = await _service.GetInsightsAsync();
return Ok(dto);
}
}
}

View File

@ -400,6 +400,7 @@ namespace line_gestao_api.Controllers
Skeelo = req.Skeelo,
VivoNewsPlus = req.VivoNewsPlus,
VivoTravelMundo = req.VivoTravelMundo,
VivoSync = req.VivoSync,
VivoGestaoDispositivo = req.VivoGestaoDispositivo,
ValorContratoVivo = req.ValorContratoVivo,
FranquiaLine = req.FranquiaLine,
@ -408,6 +409,7 @@ namespace line_gestao_api.Controllers
ValorContratoLine = req.ValorContratoLine,
Desconto = req.Desconto,
Lucro = req.Lucro,
TipoDeChip = req.TipoDeChip?.Trim(),
CreatedAt = now,
UpdatedAt = now
@ -460,6 +462,7 @@ namespace line_gestao_api.Controllers
x.Skeelo = req.Skeelo;
x.VivoNewsPlus = req.VivoNewsPlus;
x.VivoTravelMundo = req.VivoTravelMundo;
x.VivoSync = req.VivoSync;
x.VivoGestaoDispositivo = req.VivoGestaoDispositivo;
x.ValorContratoVivo = req.ValorContratoVivo;
x.FranquiaLine = req.FranquiaLine;
@ -477,6 +480,7 @@ namespace line_gestao_api.Controllers
x.DataEntregaOpera = ToUtc(req.DataEntregaOpera);
x.DataEntregaCliente = ToUtc(req.DataEntregaCliente);
x.VencConta = req.VencConta?.Trim();
x.TipoDeChip = req.TipoDeChip?.Trim();
ApplyReservaRule(x);
x.UpdatedAt = DateTime.UtcNow;
@ -576,6 +580,7 @@ namespace line_gestao_api.Controllers
Skeelo = TryDecimal(GetCellByHeaderAny(ws, r, map, "SKEELO")),
VivoNewsPlus = TryDecimal(GetCellByHeaderAny(ws, r, map, "VIVO NEWS PLUS")),
VivoTravelMundo = TryDecimal(GetCellByHeaderAny(ws, r, map, "VIVO TRAVEL MUNDO")),
VivoSync = TryDecimal(GetCellByHeaderAny(ws, r, map, "VIVO SYNC")),
VivoGestaoDispositivo = TryDecimal(GetCellByHeaderAny(ws, r, map, "VIVO GESTAO DISPOSITIVO")),
ValorContratoVivo = TryDecimal(GetCellByHeaderAny(ws, r, map, "VALOR CONTRATO VIVO", "VALOR DO CONTRATO VIVO")),
FranquiaLine = TryDecimal(GetCellByHeaderAny(ws, r, map, "FRANQUIA LINE", "FRAQUIA LINE")),
@ -593,6 +598,7 @@ namespace line_gestao_api.Controllers
DataEntregaOpera = TryDate(ws, r, map, "DATA DA ENTREGA OPERA."),
DataEntregaCliente = TryDate(ws, r, map, "DATA DA ENTREGA CLIENTE"),
VencConta = GetCellByHeader(ws, r, map, "VENC. DA CONTA"),
TipoDeChip = GetCellByHeaderAny(ws, r, map, "TIPO DE CHIP", "TIPO CHIP"),
CreatedAt = now,
UpdatedAt = now
};
@ -1520,9 +1526,9 @@ namespace line_gestao_api.Controllers
private async Task ImportResumoTabela1(IXLWorksheet ws, DateTime now)
{
const int headerRow = 5;
const int totalRow = 72;
var lastRow = Math.Min(totalRow - 1, ws.LastRowUsed()?.RowNumber() ?? totalRow - 1);
var lastRowUsed = ws.LastRowUsed()?.RowNumber() ?? 1;
var headerRow = FindHeaderRowForMacrophonyPlans(ws, 1, lastRowUsed);
if (headerRow == 0) return;
var map = BuildHeaderMap(ws.Row(headerRow));
var colPlano = GetCol(map, "PLANO CONTRATO");
@ -1533,8 +1539,15 @@ namespace line_gestao_api.Controllers
var colValorTotal = GetCol(map, "VALOR TOTAL");
var buffer = new List<ResumoMacrophonyPlan>(200);
string? lastPlanoContrato = null;
decimal? lastGb = null;
var dataStarted = false;
var emptyDataStreak = 0;
int? totalRowIndex = null;
var missingPlanoCount = 0;
var missingGbCount = 0;
for (int r = headerRow + 1; r <= lastRow; r++)
for (int r = headerRow + 1; r <= lastRowUsed; r++)
{
var plano = GetCellString(ws, r, colPlano);
var gb = GetCellString(ws, r, colGb);
@ -1543,27 +1556,90 @@ namespace line_gestao_api.Controllers
var totalLinhas = GetCellString(ws, r, colTotalLinhas);
var valorTotal = GetCellString(ws, r, colValorTotal);
if (string.IsNullOrWhiteSpace(plano)
var isPlanoTotal = !string.IsNullOrWhiteSpace(plano)
&& NormalizeHeader(plano) == NormalizeHeader("TOTAL");
if (isPlanoTotal)
{
totalRowIndex = r;
break;
}
var hasAnyValue = !(string.IsNullOrWhiteSpace(plano)
&& string.IsNullOrWhiteSpace(gb)
&& string.IsNullOrWhiteSpace(valorInd)
&& string.IsNullOrWhiteSpace(franquia)
&& string.IsNullOrWhiteSpace(totalLinhas)
&& string.IsNullOrWhiteSpace(valorTotal))
&& string.IsNullOrWhiteSpace(valorTotal));
if (!hasAnyValue)
{
if (dataStarted)
{
emptyDataStreak++;
if (emptyDataStreak >= 2) break;
}
continue;
}
emptyDataStreak = 0;
var franquiaValue = TryDecimal(franquia);
var totalLinhasValue = TryNullableInt(totalLinhas);
var isDataRow = franquiaValue.HasValue || totalLinhasValue.HasValue;
if (isDataRow) dataStarted = true;
if (!isDataRow && dataStarted)
{
break;
}
if (!isDataRow)
{
continue;
}
var planoNormalized = NormalizeHeader(plano);
if (!string.IsNullOrWhiteSpace(plano)
&& planoNormalized != NormalizeHeader("PLANO CONTRATO")
&& planoNormalized != NormalizeHeader("TOTAL"))
{
lastPlanoContrato = plano.Trim();
}
var gbValue = TryDecimal(gb);
if (gbValue.HasValue)
{
lastGb = gbValue;
}
var resolvedPlano = isDataRow
? (string.IsNullOrWhiteSpace(plano) ? lastPlanoContrato : plano.Trim())
: (string.IsNullOrWhiteSpace(plano) ? null : plano.Trim());
var resolvedGb = isDataRow
? (gbValue ?? lastGb)
: gbValue;
if (isDataRow && string.IsNullOrWhiteSpace(resolvedPlano))
{
missingPlanoCount++;
}
if (isDataRow && !resolvedGb.HasValue)
{
missingGbCount++;
}
var vivoTravelCell = ws.Cell(r, 8).GetString();
var vivoTravel = !string.IsNullOrWhiteSpace(vivoTravelCell)
&& vivoTravelCell.Contains("VIVO TRAVEL", StringComparison.OrdinalIgnoreCase);
buffer.Add(new ResumoMacrophonyPlan
{
PlanoContrato = string.IsNullOrWhiteSpace(plano) ? null : plano.Trim(),
Gb = TryDecimal(gb),
PlanoContrato = string.IsNullOrWhiteSpace(resolvedPlano) ? null : resolvedPlano,
Gb = resolvedGb,
ValorIndividualComSvas = TryDecimal(valorInd),
FranquiaGb = TryDecimal(franquia),
TotalLinhas = TryNullableInt(totalLinhas),
FranquiaGb = franquiaValue,
TotalLinhas = totalLinhasValue,
ValorTotal = TryDecimal(valorTotal),
VivoTravel = vivoTravel,
CreatedAt = now,
@ -1577,11 +1653,21 @@ namespace line_gestao_api.Controllers
await _db.SaveChangesAsync();
}
if (missingPlanoCount > 0 || missingGbCount > 0)
{
throw new InvalidOperationException($"Import RESUMO/MACROPHONY: {missingPlanoCount} linhas sem PLANO CONTRATO e {missingGbCount} linhas sem GB.");
}
if (totalRowIndex == null)
{
return;
}
var total = new ResumoMacrophonyTotal
{
FranquiaGbTotal = TryDecimal(GetCellString(ws, totalRow, colFranquiaGb)),
TotalLinhasTotal = TryNullableInt(GetCellString(ws, totalRow, colTotalLinhas)),
ValorTotal = TryDecimal(GetCellString(ws, totalRow, colValorTotal)),
FranquiaGbTotal = TryDecimal(GetCellString(ws, totalRowIndex.Value, colFranquiaGb)),
TotalLinhasTotal = TryNullableInt(GetCellString(ws, totalRowIndex.Value, colTotalLinhas)),
ValorTotal = TryDecimal(GetCellString(ws, totalRowIndex.Value, colValorTotal)),
CreatedAt = now,
UpdatedAt = now
};
@ -1590,6 +1676,27 @@ namespace line_gestao_api.Controllers
await _db.SaveChangesAsync();
}
private static int FindHeaderRowForMacrophonyPlans(IXLWorksheet ws, int startRow, int lastRow)
{
for (int r = startRow; r <= lastRow; r++)
{
var row = ws.Row(r);
if (!row.CellsUsed().Any()) continue;
var map = BuildHeaderMap(row);
var hasPlano = GetCol(map, "PLANO CONTRATO") > 0;
var hasGb = GetCol(map, "GB") > 0;
var hasTotalLinhas = GetColAny(map, "TOTAL DE LINHAS", "TOTAL LINHAS") > 0;
if (hasPlano && hasGb && hasTotalLinhas)
{
return r;
}
}
return 0;
}
private async Task ImportResumoTabela2(IXLWorksheet ws, DateTime now)
{
const int headerRow = 5;
@ -1714,9 +1821,9 @@ namespace line_gestao_api.Controllers
private async Task ImportResumoTabela4(IXLWorksheet ws, DateTime now)
{
const int headerRow = 74;
const int totalRow = 81;
var lastRow = Math.Min(totalRow - 1, ws.LastRowUsed()?.RowNumber() ?? totalRow - 1);
var lastRowUsed = ws.LastRowUsed()?.RowNumber() ?? 1;
var headerRow = FindHeaderRowForPlanoContratoResumo(ws, 1, lastRowUsed);
if (headerRow == 0) return;
var map = BuildHeaderMap(ws.Row(headerRow));
var colPlano = GetCol(map, "PLANO CONTRATO");
@ -1725,10 +1832,17 @@ namespace line_gestao_api.Controllers
var colFranquiaGb = GetColAny(map, "FRANQUIA GB", "FRAQUIA GB");
var colTotalLinhas = GetColAny(map, "TOTAL DE LINHAS", "TOTAL LINHAS");
var colValorTotal = GetCol(map, "VALOR TOTAL");
var colCliente = GetCol(map, "CLIENTE");
var colQtdLinhas = GetColAny(map, "QTD DE LINHAS", "QTD. DE LINHAS", "QTD LINHAS");
var buffer = new List<ResumoPlanoContratoResumo>(200);
string? lastPlanoContrato = null;
var dataStarted = false;
var emptyDataStreak = 0;
int? totalRowIndex = null;
var missingPlanoCount = 0;
for (int r = headerRow + 1; r <= lastRow; r++)
for (int r = headerRow + 1; r <= lastRowUsed; r++)
{
var plano = GetCellString(ws, r, colPlano);
var gb = GetCellString(ws, r, colGb);
@ -1736,20 +1850,68 @@ namespace line_gestao_api.Controllers
var franquia = GetCellString(ws, r, colFranquiaGb);
var totalLinhas = GetCellString(ws, r, colTotalLinhas);
var valorTotal = GetCellString(ws, r, colValorTotal);
var cliente = GetCellString(ws, r, colCliente);
var qtdLinhas = GetCellString(ws, r, colQtdLinhas);
if (string.IsNullOrWhiteSpace(plano)
var isPlanoTotal = !string.IsNullOrWhiteSpace(plano)
&& NormalizeHeader(plano) == NormalizeHeader("TOTAL");
if (isPlanoTotal)
{
totalRowIndex = r;
break;
}
var hasAnyValue = !(string.IsNullOrWhiteSpace(plano)
&& string.IsNullOrWhiteSpace(gb)
&& string.IsNullOrWhiteSpace(valorInd)
&& string.IsNullOrWhiteSpace(franquia)
&& string.IsNullOrWhiteSpace(totalLinhas)
&& string.IsNullOrWhiteSpace(valorTotal))
&& string.IsNullOrWhiteSpace(valorTotal)
&& string.IsNullOrWhiteSpace(cliente)
&& string.IsNullOrWhiteSpace(qtdLinhas));
if (!hasAnyValue)
{
if (dataStarted)
{
emptyDataStreak++;
if (emptyDataStreak >= 2) break;
}
continue;
}
emptyDataStreak = 0;
var isDataRow = !string.IsNullOrWhiteSpace(cliente) || TryNullableInt(qtdLinhas).HasValue;
var planoNormalized = NormalizeHeader(plano);
if (!string.IsNullOrWhiteSpace(plano)
&& planoNormalized != NormalizeHeader("PLANO CONTRATO")
&& planoNormalized != NormalizeHeader("TOTAL"))
{
lastPlanoContrato = plano.Trim();
}
if (!isDataRow && dataStarted)
{
break;
}
if (isDataRow) dataStarted = true;
var resolvedPlano = isDataRow
? (string.IsNullOrWhiteSpace(plano) ? lastPlanoContrato : plano.Trim())
: (string.IsNullOrWhiteSpace(plano) ? null : plano.Trim());
if (isDataRow && string.IsNullOrWhiteSpace(resolvedPlano))
{
missingPlanoCount++;
}
buffer.Add(new ResumoPlanoContratoResumo
{
PlanoContrato = string.IsNullOrWhiteSpace(plano) ? null : plano.Trim(),
PlanoContrato = string.IsNullOrWhiteSpace(resolvedPlano) ? null : resolvedPlano,
Gb = TryDecimal(gb),
ValorIndividualComSvas = TryDecimal(valorInd),
FranquiaGb = TryDecimal(franquia),
@ -1766,9 +1928,19 @@ namespace line_gestao_api.Controllers
await _db.SaveChangesAsync();
}
if (missingPlanoCount > 0)
{
throw new InvalidOperationException($"Import RESUMO/PLANO CONTRATO: {missingPlanoCount} linhas de dados ficaram sem PLANO CONTRATO.");
}
if (totalRowIndex == null)
{
return;
}
var total = new ResumoPlanoContratoTotal
{
ValorTotal = TryDecimal(ws.Cell(totalRow, 7).GetString()),
ValorTotal = TryDecimal(GetCellString(ws, totalRowIndex.Value, colValorTotal)),
CreatedAt = now,
UpdatedAt = now
};
@ -1777,6 +1949,27 @@ namespace line_gestao_api.Controllers
await _db.SaveChangesAsync();
}
private static int FindHeaderRowForPlanoContratoResumo(IXLWorksheet ws, int startRow, int lastRow)
{
for (int r = startRow; r <= lastRow; r++)
{
var row = ws.Row(r);
if (!row.CellsUsed().Any()) continue;
var map = BuildHeaderMap(row);
var hasPlano = GetCol(map, "PLANO CONTRATO") > 0;
var hasCliente = GetCol(map, "CLIENTE") > 0;
var hasQtd = GetColAny(map, "QTD DE LINHAS", "QTD. DE LINHAS", "QTD LINHAS") > 0;
if (hasPlano && hasCliente && hasQtd)
{
return r;
}
}
return 0;
}
private async Task ImportResumoTabela5(IXLWorksheet ws, DateTime now)
{
const int headerRow = 83;
@ -1814,34 +2007,78 @@ namespace line_gestao_api.Controllers
private async Task ImportResumoTabela6(IXLWorksheet ws, DateTime now)
{
const int headerRow = 91;
const int totalRow = 139;
var lastRow = Math.Min(totalRow - 1, ws.LastRowUsed()?.RowNumber() ?? totalRow - 1);
var lastRowUsed = ws.LastRowUsed()?.RowNumber() ?? 1;
var sectionRow = FindSectionRow(ws, "LINHAS NA RESERVA");
if (sectionRow == 0) return;
var headerRow = FindHeaderRowForReserva(ws, sectionRow + 1, lastRowUsed);
if (headerRow == 0) return;
var map = BuildHeaderMap(ws.Row(headerRow));
var colDdd = GetCol(map, "DDD");
var colFranquiaGb = GetColAny(map, "FRANQUIA GB", "FRAQUIA GB");
var colQtdLinhas = GetColAny(map, "QTD. DE LINHAS", "QTD DE LINHAS", "QTD. LINHAS");
var colQtdLinhas = GetColAny(map, "QTD. DE LINHAS", "QTD DE LINHAS", "QTD. LINHAS", "QTDLINHAS");
var colTotal = GetCol(map, "TOTAL");
var buffer = new List<ResumoReservaLine>(200);
decimal? lastTotal = null;
string? lastDddValid = null;
var dataStarted = false;
var emptyRowStreak = 0;
int? totalRowIndex = null;
for (int r = headerRow + 1; r <= lastRow; r++)
for (int r = headerRow + 1; r <= lastRowUsed; r++)
{
var ddd = GetCellString(ws, r, colDdd);
var franquia = GetCellString(ws, r, colFranquiaGb);
var qtdLinhas = GetCellString(ws, r, colQtdLinhas);
var total = GetCellString(ws, r, colTotal);
if (string.IsNullOrWhiteSpace(ddd)
var hasAnyValue = !(string.IsNullOrWhiteSpace(ddd)
&& string.IsNullOrWhiteSpace(franquia)
&& string.IsNullOrWhiteSpace(qtdLinhas)
&& string.IsNullOrWhiteSpace(total))
&& string.IsNullOrWhiteSpace(total));
if (!hasAnyValue)
{
if (dataStarted)
{
emptyRowStreak++;
if (emptyRowStreak >= 2) break;
}
continue;
}
emptyRowStreak = 0;
var franquiaValue = TryDecimal(franquia);
var qtdValue = TryNullableInt(qtdLinhas);
var isDataRow = franquiaValue.HasValue || qtdValue.HasValue;
var dddCandidate = NullIfEmptyDigits(ddd);
if (!string.IsNullOrWhiteSpace(dddCandidate))
{
lastDddValid = dddCandidate;
}
var isTotalRow = !isDataRow && !string.IsNullOrWhiteSpace(total);
if (isTotalRow)
{
totalRowIndex = r;
break;
}
if (!isDataRow && dataStarted)
{
break;
}
if (isDataRow) dataStarted = true;
var resolvedDdd = isDataRow
? (dddCandidate ?? lastDddValid)
: dddCandidate;
var totalValue = TryDecimal(total);
if (!totalValue.HasValue && lastTotal.HasValue)
{
@ -1854,25 +2091,36 @@ namespace line_gestao_api.Controllers
buffer.Add(new ResumoReservaLine
{
Ddd = string.IsNullOrWhiteSpace(ddd) ? null : ddd.Trim(),
FranquiaGb = TryDecimal(franquia),
QtdLinhas = TryNullableInt(qtdLinhas),
Ddd = string.IsNullOrWhiteSpace(resolvedDdd) ? null : resolvedDdd,
FranquiaGb = franquiaValue,
QtdLinhas = qtdValue,
Total = totalValue,
CreatedAt = now,
UpdatedAt = now
});
}
var missingDddCount = buffer.Count(x => x.Ddd == null && (x.FranquiaGb.HasValue || x.QtdLinhas.HasValue));
if (missingDddCount > 0)
{
throw new InvalidOperationException($"Import RESUMO/RESERVA: {missingDddCount} linhas de dados ficaram sem DDD.");
}
if (buffer.Count > 0)
{
await _db.ResumoReservaLines.AddRangeAsync(buffer);
await _db.SaveChangesAsync();
}
if (totalRowIndex == null)
{
return;
}
var totalEntity = new ResumoReservaTotal
{
QtdLinhasTotal = TryNullableInt(GetCellString(ws, totalRow, colQtdLinhas)),
Total = TryDecimal(GetCellString(ws, totalRow, colTotal)),
QtdLinhasTotal = TryNullableInt(GetCellString(ws, totalRowIndex.Value, colQtdLinhas)),
Total = TryDecimal(GetCellString(ws, totalRowIndex.Value, colTotal)),
CreatedAt = now,
UpdatedAt = now
};
@ -1881,6 +2129,43 @@ namespace line_gestao_api.Controllers
await _db.SaveChangesAsync();
}
private static int FindSectionRow(IXLWorksheet ws, string sectionName)
{
var normalizedTarget = NormalizeHeader(sectionName);
foreach (var row in ws.RowsUsed())
{
foreach (var cell in row.CellsUsed())
{
var key = NormalizeHeader(cell.GetString());
if (string.IsNullOrWhiteSpace(key)) continue;
if (key.Contains(normalizedTarget)) return row.RowNumber();
}
}
return 0;
}
private static int FindHeaderRowForReserva(IXLWorksheet ws, int startRow, int lastRow)
{
for (int r = startRow; r <= lastRow; r++)
{
var row = ws.Row(r);
if (!row.CellsUsed().Any()) continue;
var map = BuildHeaderMap(row);
var hasDdd = GetCol(map, "DDD") > 0;
var hasFranquia = GetColAny(map, "FRANQUIA GB", "FRAQUIA GB") > 0;
var hasQtd = GetColAny(map, "QTD. DE LINHAS", "QTD DE LINHAS", "QTD. LINHAS", "QTDLINHAS") > 0;
if (hasDdd && hasFranquia && hasQtd)
{
return r;
}
}
return 0;
}
private async Task ImportControleRecebidosSheet(IXLWorksheet ws, int year)
{
var buffer = new List<ControleRecebidoLine>(500);
@ -2159,6 +2444,7 @@ namespace line_gestao_api.Controllers
Skeelo = x.Skeelo,
VivoNewsPlus = x.VivoNewsPlus,
VivoTravelMundo = x.VivoTravelMundo,
VivoSync = x.VivoSync,
VivoGestaoDispositivo = x.VivoGestaoDispositivo,
ValorContratoVivo = x.ValorContratoVivo,
FranquiaLine = x.FranquiaLine,
@ -2175,7 +2461,8 @@ namespace line_gestao_api.Controllers
Solicitante = x.Solicitante,
DataEntregaOpera = x.DataEntregaOpera,
DataEntregaCliente = x.DataEntregaCliente,
VencConta = x.VencConta
VencConta = x.VencConta,
TipoDeChip = x.TipoDeChip
};
private static void ApplyReservaRule(MobileLine x)

View File

@ -49,6 +49,7 @@ namespace line_gestao_api.Dtos
public decimal? Skeelo { get; set; }
public decimal? VivoNewsPlus { get; set; }
public decimal? VivoTravelMundo { get; set; }
public decimal? VivoSync { get; set; }
public decimal? VivoGestaoDispositivo { get; set; }
public decimal? ValorContratoVivo { get; set; }
@ -65,5 +66,10 @@ namespace line_gestao_api.Dtos
// ==========================
public decimal? Desconto { get; set; }
public decimal? Lucro { get; set; }
// ==========================
// Identificação adicional
// ==========================
public string? TipoDeChip { get; set; }
}
}

View File

@ -0,0 +1,93 @@
using System.Collections.Generic;
namespace line_gestao_api.Dtos
{
public class GeralDashboardInsightsDto
{
public GeralDashboardKpisDto Kpis { get; set; } = new();
public GeralDashboardChartsDto Charts { get; set; } = new();
public List<GeralDashboardClientGroupDto> ClientGroups { get; set; } = new();
}
public class GeralDashboardKpisDto
{
public int TotalLinhas { get; set; }
public int TotalAtivas { get; set; }
public GeralDashboardVivoKpiDto Vivo { get; set; } = new();
public GeralDashboardTravelKpiDto TravelMundo { get; set; } = new();
public GeralDashboardAdditionalKpiDto Adicionais { get; set; } = new();
}
public class GeralDashboardVivoKpiDto
{
public int QtdLinhas { get; set; }
public decimal TotalBaseMensal { get; set; }
public decimal TotalAdicionaisMensal { get; set; }
public decimal TotalGeralMensal { get; set; }
public decimal MediaPorLinha { get; set; }
public decimal? MinPorLinha { get; set; }
public decimal? MaxPorLinha { get; set; }
}
public class GeralDashboardTravelKpiDto
{
public int ComTravel { get; set; }
public int SemTravel { get; set; }
public decimal TotalValue { get; set; }
}
public class GeralDashboardAdditionalKpiDto
{
public int TotalLinesWithAnyPaidAdditional { get; set; }
public int TotalLinesWithNoPaidAdditional { get; set; }
public List<GeralDashboardServiceKpiDto> ServicesPaid { get; set; } = new();
public List<GeralDashboardServiceKpiDto> ServicesNotPaid { get; set; } = new();
}
public class GeralDashboardServiceKpiDto
{
public string ServiceName { get; set; } = string.Empty;
public int CountLines { get; set; }
public decimal TotalValue { get; set; }
}
public class GeralDashboardChartsDto
{
public GeralDashboardChartDto LinhasPorFranquia { get; set; } = new();
public GeralDashboardChartDto AdicionaisPagosPorServico { get; set; } = new();
public GeralDashboardChartDto TravelMundo { get; set; } = new();
}
public class GeralDashboardChartDto
{
public List<string> Labels { get; set; } = new();
public List<int> Values { get; set; } = new();
public List<decimal>? Totals { get; set; }
}
public class GeralDashboardClientGroupDto
{
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 List<GeralDashboardTagDto> TagsBase { get; set; } = new();
public List<GeralDashboardTagDto> TagsExtras { get; set; } = new();
public List<GeralDashboardClientServiceDto> AdicionaisPorServico { get; set; } = new();
}
public class GeralDashboardClientServiceDto
{
public string ServiceName { get; set; } = string.Empty;
public int PaidCount { get; set; }
public int NotPaidCount { get; set; }
public decimal TotalValue { get; set; }
}
public class GeralDashboardTagDto
{
public string Label { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
}
}

View File

@ -33,6 +33,7 @@
public decimal? Skeelo { get; set; }
public decimal? VivoNewsPlus { get; set; }
public decimal? VivoTravelMundo { get; set; }
public decimal? VivoSync { get; set; }
public decimal? VivoGestaoDispositivo { get; set; }
public decimal? ValorContratoVivo { get; set; }
@ -53,6 +54,7 @@
public DateTime? DataEntregaOpera { get; set; }
public DateTime? DataEntregaCliente { get; set; }
public string? VencConta { get; set; }
public string? TipoDeChip { get; set; }
}
// ✅ UPDATE REQUEST (SEM Id)
@ -72,6 +74,7 @@
public decimal? Skeelo { get; set; }
public decimal? VivoNewsPlus { get; set; }
public decimal? VivoTravelMundo { get; set; }
public decimal? VivoSync { get; set; }
public decimal? VivoGestaoDispositivo { get; set; }
public decimal? ValorContratoVivo { get; set; }
@ -92,6 +95,7 @@
public DateTime? DataEntregaOpera { get; set; }
public DateTime? DataEntregaCliente { get; set; }
public string? VencConta { get; set; }
public string? TipoDeChip { get; set; }
}
public class ImportResultDto

View File

@ -29,6 +29,7 @@ namespace line_gestao_api.Models
public decimal? Skeelo { get; set; }
public decimal? VivoNewsPlus { get; set; }
public decimal? VivoTravelMundo { get; set; }
public decimal? VivoSync { get; set; }
public decimal? VivoGestaoDispositivo { get; set; }
public decimal? ValorContratoVivo { get; set; }
@ -59,6 +60,8 @@ namespace line_gestao_api.Models
[MaxLength(50)]
public string? VencConta { get; set; }
[MaxLength(80)]
public string? TipoDeChip { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;

View File

@ -36,6 +36,7 @@ builder.Services.AddDbContext<AppDbContext>(options =>
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ITenantProvider, TenantProvider>();
builder.Services.AddScoped<ParcelamentosImportService>();
builder.Services.AddScoped<GeralDashboardInsightsService>();
builder.Services.AddIdentityCore<ApplicationUser>(options =>
{

View File

@ -0,0 +1,917 @@
using System.Globalization;
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 ServiceVivoGestaoDispositivo = "VIVO GESTÃO DISPOSITIVO";
private readonly AppDbContext _db;
public GeralDashboardInsightsService(AppDbContext db)
{
_db = db;
}
public async Task<GeralDashboardInsightsDto> 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.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.VivoGestaoDispositivo != null)
? (x.ValorPlanoVivo ?? 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.VivoGestaoDispositivo != null)
? (x.GestaoVozDados ?? 0m) + (x.Skeelo ?? 0m) + (x.VivoNewsPlus ?? 0m) +
(x.VivoTravelMundo ?? 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.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.VivoGestaoDispositivo != null)
.Select(x => x.ValorPlanoVivo ?? 0m)
.DefaultIfEmpty()
.Max(),
TravelCom = 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.VivoGestaoDispositivo != null) &&
x.VivoTravelMundo != null),
TravelSem = 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.VivoGestaoDispositivo != null) &&
x.VivoTravelMundo == null),
TravelTotal = 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.VivoGestaoDispositivo != null)
? (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.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.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.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.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.VivoGestaoDispositivo != null) &&
(x.VivoGestaoDispositivo ?? 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.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.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.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.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.VivoGestaoDispositivo != null) &&
(x.VivoGestaoDispositivo ?? 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.VivoGestaoDispositivo != null) &&
((x.GestaoVozDados ?? 0m) > 0m ||
(x.Skeelo ?? 0m) > 0m ||
(x.VivoNewsPlus ?? 0m) > 0m ||
(x.VivoTravelMundo ?? 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.VivoGestaoDispositivo != null) &&
(x.GestaoVozDados ?? 0m) <= 0m &&
(x.Skeelo ?? 0m) <= 0m &&
(x.VivoNewsPlus ?? 0m) <= 0m &&
(x.VivoTravelMundo ?? 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.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.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.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.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.VivoGestaoDispositivo != null)
? (x.VivoGestaoDispositivo ?? 0m)
: 0m)
})
.FirstOrDefaultAsync();
var franquiasRaw = await qLines
.Select(x => new FranquiaProjection { FranquiaVivo = x.FranquiaVivo, PlanoContrato = x.PlanoContrato })
.ToListAsync();
var linhasPorFranquia = BuildFranquiaBuckets(franquiasRaw);
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.VivoGestaoDispositivo != null),
TravelCom = 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.VivoGestaoDispositivo != null) &&
x.VivoTravelMundo != null),
TravelSem = 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.VivoGestaoDispositivo != null) &&
x.VivoTravelMundo == null),
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.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.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.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.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.VivoGestaoDispositivo != null) &&
(x.VivoGestaoDispositivo ?? 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.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.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.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.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.VivoGestaoDispositivo != null) &&
(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.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.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.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.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.VivoGestaoDispositivo != null)
? (x.VivoGestaoDispositivo ?? 0m)
: 0m)
})
.OrderBy(x => x.Cliente)
.ToListAsync();
var dto = new GeralDashboardInsightsDto
{
Kpis = BuildKpis(totals),
Charts = BuildCharts(totals, linhasPorFranquia),
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,
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<GeralDashboardServiceKpiDto>
{
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 = ServiceVivoGestaoDispositivo, CountLines = totals.PaidGestaoDispositivo, TotalValue = totals.TotalGestaoDispositivo }
},
ServicesNotPaid = new List<GeralDashboardServiceKpiDto>
{
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 = ServiceVivoGestaoDispositivo, CountLines = totals.NotPaidGestaoDispositivo, TotalValue = 0m }
}
}
};
}
private static GeralDashboardChartsDto BuildCharts(TotalsProjection? totals, FranquiaBuckets franquias)
{
var adicionaisLabels = new List<string>
{
ServiceGestaoVozDados,
ServiceSkeelo,
ServiceVivoNewsPlus,
ServiceVivoTravelMundo,
ServiceVivoGestaoDispositivo
};
var adicionaisValues = totals == null
? new List<int> { 0, 0, 0, 0, 0 }
: new List<int>
{
totals.PaidGestaoVozDados,
totals.PaidSkeelo,
totals.PaidNews,
totals.PaidTravel,
totals.PaidGestaoDispositivo
};
var adicionaisTotals = totals == null
? new List<decimal> { 0m, 0m, 0m, 0m, 0m }
: new List<decimal>
{
totals.TotalGestaoVozDados,
totals.TotalSkeelo,
totals.TotalNews,
totals.TotalTravel,
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<string> { "Com", "Sem" },
Values = totals == null
? new List<int> { 0, 0 }
: new List<int> { totals.TravelCom, totals.TravelSem }
}
};
}
private static List<GeralDashboardClientGroupDto> BuildClientGroups(IEnumerable<ClientGroupProjection> rows)
{
var list = new List<GeralDashboardClientGroupDto>();
foreach (var row in rows)
{
var baseTags = new List<GeralDashboardTagDto>
{
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<GeralDashboardTagDto>
{
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 = ServiceVivoGestaoDispositivo, Value = row.PaidGestaoDispositivo.ToString(PtBr) },
new() { Label = $"R$ {ServiceVivoGestaoDispositivo}", Value = FormatCurrency(row.TotalGestaoDispositivo) }
};
var serviceDetails = new List<GeralDashboardClientServiceDto>
{
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 = 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 FranquiaBuckets BuildFranquiaBuckets(IEnumerable<FranquiaProjection> rows)
{
var map = new Dictionary<string, FranquiaBucket>();
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, @"(?<val>\d+(?:[.,]\d+)?)\s*(?<unit>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, @"(?<val>\d+(?:[.,]\d+)?)\s*(?<unit>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<string> Labels { get; set; } = new();
public List<int> 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 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 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 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; }
}
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 NotPaidGestaoVozDados { get; set; }
public int NotPaidSkeelo { get; set; }
public int NotPaidNews { get; set; }
public int NotPaidTravel { get; set; }
public int NotPaidGestaoDispositivo { 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; }
}
}
}

View File

@ -0,0 +1,116 @@
using line_gestao_api.Data;
using line_gestao_api.Models;
using line_gestao_api.Services;
using Microsoft.EntityFrameworkCore;
using Xunit;
namespace line_gestao_api.Tests
{
public class GeralDashboardInsightsServiceTests
{
[Fact]
public async Task GetInsightsAsync_ReturnsZerosWhenNoLines()
{
var tenantId = Guid.NewGuid();
var provider = new TestTenantProvider(tenantId);
var db = BuildContext(provider);
var service = new GeralDashboardInsightsService(db);
var result = await service.GetInsightsAsync();
Assert.NotNull(result);
Assert.Equal(0, result.Kpis.TotalLinhas);
Assert.Equal(0, result.Kpis.Vivo.QtdLinhas);
Assert.NotNull(result.Charts.LinhasPorFranquia);
Assert.NotNull(result.ClientGroups);
}
[Fact]
public async Task GetInsightsAsync_RespectsTenantIsolation()
{
var tenantA = Guid.NewGuid();
var tenantB = Guid.NewGuid();
var provider = new TestTenantProvider(tenantA);
var db = BuildContext(provider);
db.MobileLines.AddRange(
new MobileLine
{
TenantId = tenantA,
Cliente = "Cliente A",
Status = "Ativo",
ValorPlanoVivo = 100m,
GestaoVozDados = 10m
},
new MobileLine
{
TenantId = tenantB,
Cliente = "Cliente B",
Status = "Ativo",
ValorPlanoVivo = 200m,
GestaoVozDados = 20m
});
await db.SaveChangesAsync();
var service = new GeralDashboardInsightsService(db);
var result = await service.GetInsightsAsync();
Assert.Equal(1, result.Kpis.TotalLinhas);
Assert.Equal(1, result.Kpis.Vivo.QtdLinhas);
Assert.Equal("Cliente A", result.ClientGroups.Single().Cliente);
}
[Fact]
public async Task GetInsightsAsync_ReturnsNonNullCollections()
{
var tenantId = Guid.NewGuid();
var provider = new TestTenantProvider(tenantId);
var db = BuildContext(provider);
db.MobileLines.Add(new MobileLine
{
TenantId = tenantId,
Cliente = "Cliente X",
Status = "Ativo",
ValorPlanoVivo = 120m,
Skeelo = 5m,
VivoTravelMundo = 0m
});
await db.SaveChangesAsync();
var service = new GeralDashboardInsightsService(db);
var result = await service.GetInsightsAsync();
Assert.NotNull(result.Kpis.Adicionais.ServicesPaid);
Assert.NotNull(result.Kpis.Adicionais.ServicesNotPaid);
Assert.NotEmpty(result.ClientGroups);
Assert.NotEmpty(result.Charts.AdicionaisPagosPorServico.Labels);
}
private static AppDbContext BuildContext(TestTenantProvider provider)
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new AppDbContext(options, provider);
}
private sealed class TestTenantProvider : ITenantProvider
{
public TestTenantProvider(Guid tenantId)
{
TenantId = tenantId;
}
public Guid? TenantId { get; private set; }
public void SetTenantId(Guid? tenantId)
{
TenantId = tenantId;
}
}
}
}

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\line-gestao-api.csproj" />
</ItemGroup>
</Project>

View File

@ -17,6 +17,13 @@
<None Remove="NovaPasta\**" />
</ItemGroup>
<ItemGroup>
<Compile Remove="line-gestao-api.Tests\**" />
<Content Remove="line-gestao-api.Tests\**" />
<EmbeddedResource Remove="line-gestao-api.Tests\**" />
<None Remove="line-gestao-api.Tests\**" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Controllers\WeatherForecastController.cs" />
<Compile Remove="WeatherForecast.cs" />