Minha alteração

This commit is contained in:
Eduardo 2026-02-09 16:26:49 -03:00
parent 514c7ba8cd
commit 142bb60967
27 changed files with 2001 additions and 206 deletions

View File

@ -3,6 +3,8 @@ using line_gestao_api.Dtos;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Globalization;
using System.Linq;
namespace line_gestao_api.Controllers namespace line_gestao_api.Controllers
{ {
@ -39,9 +41,20 @@ namespace line_gestao_api.Controllers
if (!string.IsNullOrWhiteSpace(search)) if (!string.IsNullOrWhiteSpace(search))
{ {
var s = search.Trim(); var s = search.Trim();
var hasNumberSearch = TryParseSearchDecimal(s, out var searchNumber);
var hasIntSearch = int.TryParse(new string(s.Where(char.IsDigit).ToArray()), out var searchInt);
q = q.Where(x => EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") q = q.Where(x => EF.Functions.ILike(x.Cliente ?? "", $"%{s}%")
|| EF.Functions.ILike(x.Tipo ?? "", $"%{s}%")
|| EF.Functions.ILike(x.Item.ToString(), $"%{s}%")
|| EF.Functions.ILike(x.Aparelho ?? "", $"%{s}%") || EF.Functions.ILike(x.Aparelho ?? "", $"%{s}%")
|| EF.Functions.ILike(x.FormaPagamento ?? "", $"%{s}%")); || EF.Functions.ILike(x.FormaPagamento ?? "", $"%{s}%")
|| (hasIntSearch && (x.QtdLinhas ?? 0) == searchInt)
|| (hasNumberSearch &&
(((x.FranquiaVivo ?? 0m) == searchNumber) ||
((x.ValorContratoVivo ?? 0m) == searchNumber) ||
((x.FranquiaLine ?? 0m) == searchNumber) ||
((x.ValorContratoLine ?? 0m) == searchNumber) ||
((x.Lucro ?? 0m) == searchNumber))));
} }
if (!string.IsNullOrWhiteSpace(client)) if (!string.IsNullOrWhiteSpace(client))
@ -218,5 +231,16 @@ namespace line_gestao_api.Controllers
await _db.SaveChangesAsync(); await _db.SaveChangesAsync();
return NoContent(); return NoContent();
} }
private static bool TryParseSearchDecimal(string value, out decimal parsed)
{
parsed = 0m;
if (string.IsNullOrWhiteSpace(value)) return false;
var s = value.Trim().Replace("R$", "", StringComparison.OrdinalIgnoreCase).Trim();
return decimal.TryParse(s, NumberStyles.Any, new CultureInfo("pt-BR"), out parsed) ||
decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out parsed);
}
} }
} }

View File

@ -4,6 +4,7 @@ using line_gestao_api.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Globalization;
using System.Text; using System.Text;
namespace line_gestao_api.Controllers namespace line_gestao_api.Controllers
@ -36,10 +37,15 @@ namespace line_gestao_api.Controllers
if (!string.IsNullOrWhiteSpace(search)) if (!string.IsNullOrWhiteSpace(search))
{ {
var s = search.Trim(); var s = search.Trim();
var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc);
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
q = q.Where(x => q = q.Where(x =>
EF.Functions.ILike(x.NumeroDoChip ?? "", $"%{s}%") || EF.Functions.ILike(x.NumeroDoChip ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Observacoes ?? "", $"%{s}%") || EF.Functions.ILike(x.Observacoes ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Item.ToString(), $"%{s}%")); EF.Functions.ILike(x.Item.ToString(), $"%{s}%") ||
(hasDateSearch &&
((x.CreatedAt >= searchDateStartUtc && x.CreatedAt < searchDateEndUtc) ||
(x.UpdatedAt >= searchDateStartUtc && x.UpdatedAt < searchDateEndUtc))));
} }
var total = await q.CountAsync(); var total = await q.CountAsync();
@ -169,5 +175,23 @@ namespace line_gestao_api.Controllers
} }
return sb.ToString(); return sb.ToString();
} }
private static bool TryParseSearchDateUtcStart(string value, out DateTime utcStart)
{
utcStart = default;
if (string.IsNullOrWhiteSpace(value)) return false;
var s = value.Trim();
DateTime parsed;
if (DateTime.TryParse(s, new CultureInfo("pt-BR"), DateTimeStyles.None, out parsed) ||
DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsed))
{
utcStart = DateTime.SpecifyKind(parsed.Date, DateTimeKind.Utc);
return true;
}
return false;
}
} }
} }

View File

@ -4,6 +4,7 @@ using line_gestao_api.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Globalization;
using System.Text; using System.Text;
namespace line_gestao_api.Controllers namespace line_gestao_api.Controllers
@ -44,6 +45,14 @@ namespace line_gestao_api.Controllers
if (!string.IsNullOrWhiteSpace(search)) if (!string.IsNullOrWhiteSpace(search))
{ {
var s = search.Trim(); var s = search.Trim();
var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc);
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
var hasDecimalSearch = TryParseSearchDecimal(s, out var searchDecimal);
var hasIntSearch = int.TryParse(OnlyDigits(s), out var searchInt);
var searchResumo = s.Equals("resumo", StringComparison.OrdinalIgnoreCase);
var searchDetalhado = s.Equals("detalhado", StringComparison.OrdinalIgnoreCase) ||
s.Equals("detalhe", StringComparison.OrdinalIgnoreCase);
q = q.Where(x => q = q.Where(x =>
EF.Functions.ILike(x.NotaFiscal ?? "", $"%{s}%") || EF.Functions.ILike(x.NotaFiscal ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Chip ?? "", $"%{s}%") || EF.Functions.ILike(x.Chip ?? "", $"%{s}%") ||
@ -51,7 +60,19 @@ namespace line_gestao_api.Controllers
EF.Functions.ILike(x.ConteudoDaNf ?? "", $"%{s}%") || EF.Functions.ILike(x.ConteudoDaNf ?? "", $"%{s}%") ||
EF.Functions.ILike(x.NumeroDaLinha ?? "", $"%{s}%") || EF.Functions.ILike(x.NumeroDaLinha ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Item.ToString(), $"%{s}%") || EF.Functions.ILike(x.Item.ToString(), $"%{s}%") ||
EF.Functions.ILike(x.Ano.ToString(), $"%{s}%")); EF.Functions.ILike(x.Ano.ToString(), $"%{s}%") ||
(hasIntSearch && ((x.Quantidade ?? 0) == searchInt)) ||
(hasDecimalSearch &&
(((x.ValorUnit ?? 0m) == searchDecimal) || ((x.ValorDaNf ?? 0m) == searchDecimal))) ||
(searchResumo && x.IsResumo) ||
(searchDetalhado && !x.IsResumo) ||
(hasDateSearch &&
((x.DataDaNf != null &&
x.DataDaNf.Value >= searchDateStartUtc &&
x.DataDaNf.Value < searchDateEndUtc) ||
(x.DataDoRecebimento != null &&
x.DataDoRecebimento.Value >= searchDateStartUtc &&
x.DataDoRecebimento.Value < searchDateEndUtc))));
} }
var total = await q.CountAsync(); var total = await q.CountAsync();
@ -262,5 +283,33 @@ namespace line_gestao_api.Controllers
} }
return sb.ToString(); return sb.ToString();
} }
private static bool TryParseSearchDateUtcStart(string value, out DateTime utcStart)
{
utcStart = default;
if (string.IsNullOrWhiteSpace(value)) return false;
var s = value.Trim();
DateTime parsed;
if (DateTime.TryParse(s, new CultureInfo("pt-BR"), DateTimeStyles.None, out parsed) ||
DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsed))
{
utcStart = DateTime.SpecifyKind(parsed.Date, DateTimeKind.Utc);
return true;
}
return false;
}
private static bool TryParseSearchDecimal(string value, out decimal parsed)
{
parsed = 0m;
if (string.IsNullOrWhiteSpace(value)) return false;
var s = value.Trim().Replace("R$", "", StringComparison.OrdinalIgnoreCase).Trim();
return decimal.TryParse(s, NumberStyles.Any, new CultureInfo("pt-BR"), out parsed) ||
decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out parsed);
}
} }
} }

View File

@ -5,6 +5,7 @@ using line_gestao_api.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Globalization;
namespace line_gestao_api.Controllers; namespace line_gestao_api.Controllers;
@ -67,13 +68,25 @@ public class HistoricoController : ControllerBase
if (!string.IsNullOrWhiteSpace(search)) if (!string.IsNullOrWhiteSpace(search))
{ {
var s = search.Trim(); var s = search.Trim();
var hasGuidSearch = Guid.TryParse(s, out var searchGuid);
var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc);
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
q = q.Where(x => q = q.Where(x =>
EF.Functions.ILike(x.UserName ?? "", $"%{s}%") || EF.Functions.ILike(x.UserName ?? "", $"%{s}%") ||
EF.Functions.ILike(x.UserEmail ?? "", $"%{s}%") || EF.Functions.ILike(x.UserEmail ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Action ?? "", $"%{s}%") ||
EF.Functions.ILike(x.EntityName ?? "", $"%{s}%") || EF.Functions.ILike(x.EntityName ?? "", $"%{s}%") ||
EF.Functions.ILike(x.EntityLabel ?? "", $"%{s}%") || EF.Functions.ILike(x.EntityLabel ?? "", $"%{s}%") ||
EF.Functions.ILike(x.EntityId ?? "", $"%{s}%") || EF.Functions.ILike(x.EntityId ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Page ?? "", $"%{s}%")); EF.Functions.ILike(x.Page ?? "", $"%{s}%") ||
EF.Functions.ILike(x.RequestPath ?? "", $"%{s}%") ||
EF.Functions.ILike(x.RequestMethod ?? "", $"%{s}%") ||
EF.Functions.ILike(x.IpAddress ?? "", $"%{s}%") ||
// ChangesJson is stored as jsonb; applying ILIKE directly causes PostgreSQL 42883.
(hasGuidSearch && (x.Id == searchGuid || x.UserId == searchGuid)) ||
(hasDateSearch &&
x.OccurredAtUtc >= searchDateStartUtc &&
x.OccurredAtUtc < searchDateEndUtc));
} }
if (dateFrom.HasValue) if (dateFrom.HasValue)
@ -158,4 +171,22 @@ public class HistoricoController : ControllerBase
return DateTime.SpecifyKind(value, DateTimeKind.Utc); return DateTime.SpecifyKind(value, DateTimeKind.Utc);
} }
private static bool TryParseSearchDateUtcStart(string value, out DateTime utcStart)
{
utcStart = default;
if (string.IsNullOrWhiteSpace(value)) return false;
var s = value.Trim();
DateTime parsed;
if (DateTime.TryParse(s, new CultureInfo("pt-BR"), DateTimeStyles.None, out parsed) ||
DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsed))
{
utcStart = DateTime.SpecifyKind(parsed.Date, DateTimeKind.Utc);
return true;
}
return false;
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@ using line_gestao_api.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Globalization;
namespace line_gestao_api.Controllers namespace line_gestao_api.Controllers
{ {
@ -54,6 +55,8 @@ namespace line_gestao_api.Controllers
if (!string.IsNullOrWhiteSpace(search)) if (!string.IsNullOrWhiteSpace(search))
{ {
var s = search.Trim(); var s = search.Trim();
var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc);
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
q = q.Where(x => q = q.Where(x =>
EF.Functions.ILike((x.LinhaAntiga ?? ""), $"%{s}%") || EF.Functions.ILike((x.LinhaAntiga ?? ""), $"%{s}%") ||
EF.Functions.ILike((x.LinhaNova ?? ""), $"%{s}%") || EF.Functions.ILike((x.LinhaNova ?? ""), $"%{s}%") ||
@ -61,7 +64,16 @@ namespace line_gestao_api.Controllers
EF.Functions.ILike((x.MobileLine.Cliente ?? ""), $"%{s}%") || EF.Functions.ILike((x.MobileLine.Cliente ?? ""), $"%{s}%") ||
EF.Functions.ILike((x.MobileLine.Usuario ?? ""), $"%{s}%") || EF.Functions.ILike((x.MobileLine.Usuario ?? ""), $"%{s}%") ||
EF.Functions.ILike((x.MobileLine.Skil ?? ""), $"%{s}%") || EF.Functions.ILike((x.MobileLine.Skil ?? ""), $"%{s}%") ||
EF.Functions.ILike(x.Item.ToString(), $"%{s}%")); EF.Functions.ILike((x.MobileLine.Conta ?? ""), $"%{s}%") ||
EF.Functions.ILike((x.MobileLine.Status ?? ""), $"%{s}%") ||
EF.Functions.ILike((x.MobileLine.PlanoContrato ?? ""), $"%{s}%") ||
EF.Functions.ILike((x.MobileLine.VencConta ?? ""), $"%{s}%") ||
EF.Functions.ILike((x.MobileLine.Chip ?? ""), $"%{s}%") ||
EF.Functions.ILike(x.Item.ToString(), $"%{s}%") ||
(hasDateSearch &&
x.DataDaMureg != null &&
x.DataDaMureg.Value >= searchDateStartUtc &&
x.DataDaMureg.Value < searchDateEndUtc));
} }
var total = await q.CountAsync(); var total = await q.CountAsync();
@ -384,6 +396,24 @@ namespace line_gestao_api.Controllers
(v.Kind == DateTimeKind.Local ? v.ToUniversalTime() : DateTime.SpecifyKind(v, DateTimeKind.Utc)); (v.Kind == DateTimeKind.Local ? v.ToUniversalTime() : DateTime.SpecifyKind(v, DateTimeKind.Utc));
} }
private static bool TryParseSearchDateUtcStart(string value, out DateTime utcStart)
{
utcStart = default;
if (string.IsNullOrWhiteSpace(value)) return false;
var s = value.Trim();
DateTime parsed;
if (DateTime.TryParse(s, new CultureInfo("pt-BR"), DateTimeStyles.None, out parsed) ||
DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsed))
{
utcStart = DateTime.SpecifyKind(parsed.Date, DateTimeKind.Utc);
return true;
}
return false;
}
private static string OnlyDigits(string? s) private static string OnlyDigits(string? s)
{ {
if (string.IsNullOrWhiteSpace(s)) return ""; if (string.IsNullOrWhiteSpace(s)) return "";

View File

@ -0,0 +1,264 @@
using System.ComponentModel.DataAnnotations;
using System.Security.Claims;
using line_gestao_api.Dtos;
using line_gestao_api.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace line_gestao_api.Controllers;
[ApiController]
[Route("api/profile")]
[Authorize]
public class ProfileController : ControllerBase
{
private static readonly EmailAddressAttribute EmailValidator = new();
private readonly UserManager<ApplicationUser> _userManager;
public ProfileController(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
[HttpGet("me")]
public async Task<ActionResult<ProfileMeDto>> GetMe()
{
var user = await GetAuthenticatedUserAsync();
if (user == null)
{
return Unauthorized(new { message = "Usuário não autenticado." });
}
return Ok(ToProfileDto(user));
}
[HttpPatch]
public async Task<ActionResult<ProfileMeDto>> UpdateProfile([FromBody] UpdateProfileRequest req)
{
var user = await GetAuthenticatedUserAsync();
if (user == null)
{
return Unauthorized(new { message = "Usuário não autenticado." });
}
var errors = await ValidateProfileUpdateAsync(user.Id, req);
if (errors.Count > 0)
{
return BadRequest(new ValidationErrorResponse { Errors = errors });
}
var nome = req.Nome.Trim();
var email = req.Email.Trim().ToLowerInvariant();
user.Name = nome;
if (!string.Equals(user.Email, email, StringComparison.OrdinalIgnoreCase))
{
var setEmailResult = await _userManager.SetEmailAsync(user, email);
if (!setEmailResult.Succeeded)
{
return BadRequest(ToValidationErrorResponse("email", setEmailResult.Errors));
}
var setUserNameResult = await _userManager.SetUserNameAsync(user, email);
if (!setUserNameResult.Succeeded)
{
return BadRequest(ToValidationErrorResponse("email", setUserNameResult.Errors));
}
}
var updateResult = await _userManager.UpdateAsync(user);
if (!updateResult.Succeeded)
{
return BadRequest(ToValidationErrorResponse("perfil", updateResult.Errors));
}
return Ok(ToProfileDto(user));
}
[HttpPost("change-password")]
public async Task<IActionResult> ChangePassword([FromBody] ChangeMyPasswordRequest req)
{
var errors = ValidatePasswordChange(req);
if (errors.Count > 0)
{
return BadRequest(new ValidationErrorResponse { Errors = errors });
}
var user = await GetAuthenticatedUserAsync();
if (user == null)
{
return Unauthorized(new { message = "Usuário não autenticado." });
}
var result = await _userManager.ChangePasswordAsync(user, req.CredencialAtual, req.NovaCredencial);
if (!result.Succeeded)
{
return BadRequest(MapPasswordChangeErrors(result.Errors));
}
return NoContent();
}
private async Task<ApplicationUser?> GetAuthenticatedUserAsync()
{
var userIdRaw = User.FindFirstValue(ClaimTypes.NameIdentifier)
?? User.FindFirstValue("sub");
if (!Guid.TryParse(userIdRaw, out var userId))
{
return null;
}
return await _userManager.Users
.FirstOrDefaultAsync(u => u.Id == userId && u.IsActive);
}
private async Task<List<ValidationErrorDto>> ValidateProfileUpdateAsync(Guid userId, UpdateProfileRequest req)
{
var errors = new List<ValidationErrorDto>();
if (string.IsNullOrWhiteSpace(req.Nome) || req.Nome.Trim().Length < 2)
{
errors.Add(new ValidationErrorDto
{
Field = "nome",
Message = "Nome é obrigatório e deve ter pelo menos 2 caracteres."
});
}
if (string.IsNullOrWhiteSpace(req.Email))
{
errors.Add(new ValidationErrorDto
{
Field = "email",
Message = "Email é obrigatório."
});
}
else
{
var email = req.Email.Trim().ToLowerInvariant();
if (!EmailValidator.IsValid(email))
{
errors.Add(new ValidationErrorDto
{
Field = "email",
Message = "Email inválido."
});
}
else
{
var normalized = _userManager.NormalizeEmail(email);
var exists = await _userManager.Users
.AnyAsync(u => u.Id != userId && u.NormalizedEmail == normalized);
if (exists)
{
errors.Add(new ValidationErrorDto
{
Field = "email",
Message = "E-mail já cadastrado."
});
}
}
}
return errors;
}
private static List<ValidationErrorDto> ValidatePasswordChange(ChangeMyPasswordRequest req)
{
var errors = new List<ValidationErrorDto>();
if (string.IsNullOrWhiteSpace(req.CredencialAtual))
{
errors.Add(new ValidationErrorDto
{
Field = "credencialAtual",
Message = "Credencial atual é obrigatória."
});
}
if (string.IsNullOrWhiteSpace(req.NovaCredencial))
{
errors.Add(new ValidationErrorDto
{
Field = "novaCredencial",
Message = "Nova credencial é obrigatória."
});
}
else if (req.NovaCredencial.Length < 8)
{
errors.Add(new ValidationErrorDto
{
Field = "novaCredencial",
Message = "Nova credencial deve ter pelo menos 8 caracteres."
});
}
if (string.IsNullOrWhiteSpace(req.ConfirmarNovaCredencial))
{
errors.Add(new ValidationErrorDto
{
Field = "confirmarNovaCredencial",
Message = "Confirmação da nova credencial é obrigatória."
});
}
else if (!string.Equals(req.NovaCredencial, req.ConfirmarNovaCredencial, StringComparison.Ordinal))
{
errors.Add(new ValidationErrorDto
{
Field = "confirmarNovaCredencial",
Message = "Confirmação da nova credencial inválida."
});
}
return errors;
}
private static ValidationErrorResponse ToValidationErrorResponse(string field, IEnumerable<IdentityError> identityErrors)
{
return new ValidationErrorResponse
{
Errors = identityErrors.Select(e => new ValidationErrorDto
{
Field = field,
Message = e.Description
}).ToList()
};
}
private static ValidationErrorResponse MapPasswordChangeErrors(IEnumerable<IdentityError> identityErrors)
{
var errors = identityErrors.Select(e =>
{
var field = string.Equals(e.Code, "PasswordMismatch", StringComparison.OrdinalIgnoreCase)
? "credencialAtual"
: "novaCredencial";
var message = string.Equals(e.Code, "PasswordMismatch", StringComparison.OrdinalIgnoreCase)
? "Credencial atual inválida."
: e.Description;
return new ValidationErrorDto
{
Field = field,
Message = message
};
}).ToList();
return new ValidationErrorResponse { Errors = errors };
}
private static ProfileMeDto ToProfileDto(ApplicationUser user)
{
return new ProfileMeDto
{
Id = user.Id,
Nome = user.Name,
Email = user.Email ?? string.Empty
};
}
}

View File

@ -21,9 +21,10 @@ namespace line_gestao_api.Controllers
[HttpGet("dashboard")] [HttpGet("dashboard")]
public async Task<ActionResult<RelatoriosDashboardDto>> GetDashboard() public async Task<ActionResult<RelatoriosDashboardDto>> GetDashboard()
{ {
var today = DateTime.UtcNow.Date; var todayUtcStart = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc);
var last30 = today.AddDays(-30); var tomorrowUtcStart = todayUtcStart.AddDays(1);
var limit30 = today.AddDays(30); var last30UtcStart = todayUtcStart.AddDays(-30);
var limit30ExclusiveUtcStart = todayUtcStart.AddDays(31);
var minUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); var minUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
@ -86,7 +87,9 @@ namespace line_gestao_api.Controllers
var totalMuregs = await qMureg.CountAsync(); var totalMuregs = await qMureg.CountAsync();
var muregsUltimos30 = await qMureg.CountAsync(x => var muregsUltimos30 = await qMureg.CountAsync(x =>
x.DataDaMureg != null && x.DataDaMureg.Value.Date >= last30); x.DataDaMureg != null &&
x.DataDaMureg.Value >= last30UtcStart &&
x.DataDaMureg.Value < tomorrowUtcStart);
var muregsRecentes = await qMureg var muregsRecentes = await qMureg
.OrderByDescending(x => x.DataDaMureg ?? minUtc) .OrderByDescending(x => x.DataDaMureg ?? minUtc)
@ -105,7 +108,7 @@ namespace line_gestao_api.Controllers
}) })
.ToListAsync(); .ToListAsync();
var serieMureg12 = await BuildSerieUltimos12Meses_Mureg(today); var serieMureg12 = await BuildSerieUltimos12Meses_Mureg(todayUtcStart);
// ========================= // =========================
// TROCA DE NÚMERO // TROCA DE NÚMERO
@ -115,7 +118,9 @@ namespace line_gestao_api.Controllers
var totalTrocas = await qTroca.CountAsync(); var totalTrocas = await qTroca.CountAsync();
var trocasUltimos30 = await qTroca.CountAsync(x => var trocasUltimos30 = await qTroca.CountAsync(x =>
x.DataTroca != null && x.DataTroca.Value.Date >= last30); x.DataTroca != null &&
x.DataTroca.Value >= last30UtcStart &&
x.DataTroca.Value < tomorrowUtcStart);
var trocasRecentes = await qTroca var trocasRecentes = await qTroca
.OrderByDescending(x => x.DataTroca ?? minUtc) .OrderByDescending(x => x.DataTroca ?? minUtc)
@ -133,7 +138,7 @@ namespace line_gestao_api.Controllers
}) })
.ToListAsync(); .ToListAsync();
var serieTroca12 = await BuildSerieUltimos12Meses_Troca(today); var serieTroca12 = await BuildSerieUltimos12Meses_Troca(todayUtcStart);
// ========================= // =========================
// VIGÊNCIA // VIGÊNCIA
@ -143,18 +148,18 @@ namespace line_gestao_api.Controllers
var totalVig = await qVig.CountAsync(); var totalVig = await qVig.CountAsync();
var vigVencidos = await qVig.CountAsync(x => var vigVencidos = await qVig.CountAsync(x =>
x.DtTerminoFidelizacao != null && x.DtTerminoFidelizacao.Value.Date < today); x.DtTerminoFidelizacao != null && x.DtTerminoFidelizacao.Value < todayUtcStart);
var vigAVencer30 = await qVig.CountAsync(x => var vigAVencer30 = await qVig.CountAsync(x =>
x.DtTerminoFidelizacao != null && x.DtTerminoFidelizacao != null &&
x.DtTerminoFidelizacao.Value.Date >= today && x.DtTerminoFidelizacao.Value >= todayUtcStart &&
x.DtTerminoFidelizacao.Value.Date <= limit30); x.DtTerminoFidelizacao.Value < limit30ExclusiveUtcStart);
// ✅ NOVO: série próximos 12 meses (mês/ano) // ✅ NOVO: série próximos 12 meses (mês/ano)
var serieVigProx12 = await BuildSerieProximos12Meses_VigenciaEncerramentos(today); var serieVigProx12 = await BuildSerieProximos12Meses_VigenciaEncerramentos(todayUtcStart);
// ✅ NOVO: buckets de supervisão // ✅ NOVO: buckets de supervisão
var vigBuckets = await BuildVigenciaBuckets(today); var vigBuckets = await BuildVigenciaBuckets(todayUtcStart);
// ========================= // =========================
// USER DATA // USER DATA

View File

@ -49,6 +49,8 @@ public class ResumoController : ControllerBase
var reservaTotalEntity = await _db.ResumoReservaTotals.AsNoTracking() var reservaTotalEntity = await _db.ResumoReservaTotals.AsNoTracking()
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
var gbDistribuicaoTotalEntity = await _db.ResumoGbDistribuicaoTotais.AsNoTracking()
.FirstOrDefaultAsync();
var canonicalTotalLinhas = await _spreadsheetImportAuditService.GetCanonicalTotalLinhasForReadAsync(); var canonicalTotalLinhas = await _spreadsheetImportAuditService.GetCanonicalTotalLinhasForReadAsync();
var response = new ResumoResponseDto var response = new ResumoResponseDto
@ -135,6 +137,20 @@ public class ResumoController : ControllerBase
QtdLinhas = x.QtdLinhas QtdLinhas = x.QtdLinhas
}) })
.ToListAsync(), .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 ReservaLines = reservaLines
.Select(x => new ResumoReservaLineDto .Select(x => new ResumoReservaLineDto
{ {

View File

@ -40,12 +40,19 @@ namespace line_gestao_api.Controllers
if (!string.IsNullOrWhiteSpace(search)) if (!string.IsNullOrWhiteSpace(search))
{ {
var s = search.Trim(); var s = search.Trim();
var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc);
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
q = q.Where(x => q = q.Where(x =>
EF.Functions.ILike(x.LinhaAntiga ?? "", $"%{s}%") || EF.Functions.ILike(x.LinhaAntiga ?? "", $"%{s}%") ||
EF.Functions.ILike(x.LinhaNova ?? "", $"%{s}%") || EF.Functions.ILike(x.LinhaNova ?? "", $"%{s}%") ||
EF.Functions.ILike(x.ICCID ?? "", $"%{s}%") || EF.Functions.ILike(x.ICCID ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Motivo ?? "", $"%{s}%") || EF.Functions.ILike(x.Motivo ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Observacao ?? "", $"%{s}%")); EF.Functions.ILike(x.Observacao ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Item.ToString(), $"%{s}%") ||
(hasDateSearch &&
x.DataTroca != null &&
x.DataTroca.Value >= searchDateStartUtc &&
x.DataTroca.Value < searchDateEndUtc));
} }
var total = await q.CountAsync(); var total = await q.CountAsync();
@ -201,5 +208,23 @@ namespace line_gestao_api.Controllers
foreach (var c in s) if (char.IsDigit(c)) sb.Append(c); foreach (var c in s) if (char.IsDigit(c)) sb.Append(c);
return sb.ToString(); return sb.ToString();
} }
private static bool TryParseSearchDateUtcStart(string value, out DateTime utcStart)
{
utcStart = default;
if (string.IsNullOrWhiteSpace(value)) return false;
var s = value.Trim();
DateTime parsed;
if (DateTime.TryParse(s, new CultureInfo("pt-BR"), DateTimeStyles.None, out parsed) ||
DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsed))
{
utcStart = DateTime.SpecifyKind(parsed.Date, DateTimeKind.Utc);
return true;
}
return false;
}
} }
} }

View File

@ -4,6 +4,7 @@ using line_gestao_api.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Globalization;
using System.Linq; using System.Linq;
namespace line_gestao_api.Controllers namespace line_gestao_api.Controllers
@ -51,15 +52,26 @@ namespace line_gestao_api.Controllers
if (!string.IsNullOrWhiteSpace(search)) if (!string.IsNullOrWhiteSpace(search))
{ {
var s = search.Trim(); var s = search.Trim();
var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc);
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
q = q.Where(x => q = q.Where(x =>
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") || EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") || EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
EF.Functions.ILike(x.TipoPessoa ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Nome ?? "", $"%{s}%") || EF.Functions.ILike(x.Nome ?? "", $"%{s}%") ||
EF.Functions.ILike(x.RazaoSocial ?? "", $"%{s}%") || EF.Functions.ILike(x.RazaoSocial ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Cpf ?? "", $"%{s}%") || EF.Functions.ILike(x.Cpf ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Cnpj ?? "", $"%{s}%") || EF.Functions.ILike(x.Cnpj ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Rg ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Email ?? "", $"%{s}%") || EF.Functions.ILike(x.Email ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Celular ?? "", $"%{s}%")); EF.Functions.ILike(x.Celular ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Endereco ?? "", $"%{s}%") ||
EF.Functions.ILike(x.TelefoneFixo ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Item.ToString(), $"%{s}%") ||
(hasDateSearch &&
x.DataNascimento != null &&
x.DataNascimento.Value >= searchDateStartUtc &&
x.DataNascimento.Value < searchDateEndUtc));
} }
var total = await q.CountAsync(); var total = await q.CountAsync();
@ -134,7 +146,26 @@ namespace line_gestao_api.Controllers
if (!string.IsNullOrWhiteSpace(search)) if (!string.IsNullOrWhiteSpace(search))
{ {
var s = search.Trim(); var s = search.Trim();
q = q.Where(x => EF.Functions.ILike(x.Cliente ?? "", $"%{s}%")); var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc);
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
q = q.Where(x =>
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
EF.Functions.ILike(x.TipoPessoa ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Nome ?? "", $"%{s}%") ||
EF.Functions.ILike(x.RazaoSocial ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Cpf ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Cnpj ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Rg ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Email ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Celular ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Endereco ?? "", $"%{s}%") ||
EF.Functions.ILike(x.TelefoneFixo ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Item.ToString(), $"%{s}%") ||
(hasDateSearch &&
x.DataNascimento != null &&
x.DataNascimento.Value >= searchDateStartUtc &&
x.DataNascimento.Value < searchDateEndUtc));
} }
// ✅ 1. CÁLCULO DOS KPIS GERAIS (Baseado em todos os dados filtrados, sem paginação) // ✅ 1. CÁLCULO DOS KPIS GERAIS (Baseado em todos os dados filtrados, sem paginação)
@ -407,5 +438,23 @@ namespace line_gestao_api.Controllers
if (string.IsNullOrWhiteSpace(s)) return ""; if (string.IsNullOrWhiteSpace(s)) return "";
return new string(s.Where(char.IsDigit).ToArray()); return new string(s.Where(char.IsDigit).ToArray());
} }
private static bool TryParseSearchDateUtcStart(string value, out DateTime utcStart)
{
utcStart = default;
if (string.IsNullOrWhiteSpace(value)) return false;
var s = value.Trim();
DateTime parsed;
if (DateTime.TryParse(s, new CultureInfo("pt-BR"), DateTimeStyles.None, out parsed) ||
DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsed))
{
utcStart = DateTime.SpecifyKind(parsed.Date, DateTimeKind.Utc);
return true;
}
return false;
}
} }
} }

View File

@ -5,6 +5,7 @@ using line_gestao_api.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Globalization;
using System.Linq; using System.Linq;
namespace line_gestao_api.Controllers namespace line_gestao_api.Controllers
@ -45,12 +46,24 @@ namespace line_gestao_api.Controllers
if (!string.IsNullOrWhiteSpace(search)) if (!string.IsNullOrWhiteSpace(search))
{ {
var s = search.Trim(); var s = search.Trim();
var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc);
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
var hasTotalSearch = TryParseSearchDecimal(s, out var searchTotal);
q = q.Where(x => q = q.Where(x =>
EF.Functions.ILike(x.Conta ?? "", $"%{s}%") || EF.Functions.ILike(x.Conta ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") || EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") || EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") || EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") ||
EF.Functions.ILike(x.PlanoContrato ?? "", $"%{s}%")); EF.Functions.ILike(x.PlanoContrato ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Item.ToString(), $"%{s}%") ||
(hasTotalSearch && x.Total != null && x.Total.Value == searchTotal) ||
(hasDateSearch &&
((x.DtEfetivacaoServico != null &&
x.DtEfetivacaoServico.Value >= searchDateStartUtc &&
x.DtEfetivacaoServico.Value < searchDateEndUtc) ||
(x.DtTerminoFidelizacao != null &&
x.DtTerminoFidelizacao.Value >= searchDateStartUtc &&
x.DtTerminoFidelizacao.Value < searchDateEndUtc))));
} }
var total = await q.CountAsync(); var total = await q.CountAsync();
@ -108,8 +121,8 @@ namespace line_gestao_api.Controllers
page = page < 1 ? 1 : page; page = page < 1 ? 1 : page;
pageSize = pageSize < 1 ? 20 : pageSize; pageSize = pageSize < 1 ? 20 : pageSize;
var today = DateTime.UtcNow.Date; // UTC para evitar erro no PostgreSQL var todayUtcStart = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc);
var limit30 = today.AddDays(30); var limit30ExclusiveUtcStart = todayUtcStart.AddDays(31);
// Query Base (Linhas) // Query Base (Linhas)
var q = _db.VigenciaLines.AsNoTracking() var q = _db.VigenciaLines.AsNoTracking()
@ -118,7 +131,25 @@ namespace line_gestao_api.Controllers
if (!string.IsNullOrWhiteSpace(search)) if (!string.IsNullOrWhiteSpace(search))
{ {
var s = search.Trim(); var s = search.Trim();
q = q.Where(x => EF.Functions.ILike(x.Cliente ?? "", $"%{s}%")); var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc);
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
var hasTotalSearch = TryParseSearchDecimal(s, out var searchTotal);
q = q.Where(x =>
EF.Functions.ILike(x.Conta ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") ||
EF.Functions.ILike(x.PlanoContrato ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Item.ToString(), $"%{s}%") ||
(hasTotalSearch && x.Total != null && x.Total.Value == searchTotal) ||
(hasDateSearch &&
((x.DtEfetivacaoServico != null &&
x.DtEfetivacaoServico.Value >= searchDateStartUtc &&
x.DtEfetivacaoServico.Value < searchDateEndUtc) ||
(x.DtTerminoFidelizacao != null &&
x.DtTerminoFidelizacao.Value >= searchDateStartUtc &&
x.DtTerminoFidelizacao.Value < searchDateEndUtc))));
} }
// ✅ CÁLCULO DOS KPIS GERAIS (Antes do agrupamento/paginação) // ✅ CÁLCULO DOS KPIS GERAIS (Antes do agrupamento/paginação)
@ -128,7 +159,7 @@ namespace line_gestao_api.Controllers
TotalLinhas = await q.CountAsync(), TotalLinhas = await q.CountAsync(),
// Clientes distintos // Clientes distintos
TotalClientes = await q.Select(x => x.Cliente).Distinct().CountAsync(), TotalClientes = await q.Select(x => x.Cliente).Distinct().CountAsync(),
TotalVencidos = await q.CountAsync(x => x.DtTerminoFidelizacao != null && x.DtTerminoFidelizacao.Value.Date < today), TotalVencidos = await q.CountAsync(x => x.DtTerminoFidelizacao != null && x.DtTerminoFidelizacao.Value < todayUtcStart),
ValorTotal = await q.SumAsync(x => x.Total ?? 0m) ValorTotal = await q.SumAsync(x => x.Total ?? 0m)
}; };
@ -140,10 +171,10 @@ namespace line_gestao_api.Controllers
Cliente = g.Key, Cliente = g.Key,
Linhas = g.Count(), Linhas = g.Count(),
Total = g.Sum(x => x.Total ?? 0m), Total = g.Sum(x => x.Total ?? 0m),
Vencidos = g.Count(x => x.DtTerminoFidelizacao != null && x.DtTerminoFidelizacao.Value.Date < today), Vencidos = g.Count(x => x.DtTerminoFidelizacao != null && x.DtTerminoFidelizacao.Value < todayUtcStart),
AVencer30 = g.Count(x => x.DtTerminoFidelizacao != null && x.DtTerminoFidelizacao.Value.Date >= today && x.DtTerminoFidelizacao.Value.Date <= limit30), AVencer30 = g.Count(x => x.DtTerminoFidelizacao != null && x.DtTerminoFidelizacao.Value >= todayUtcStart && x.DtTerminoFidelizacao.Value < limit30ExclusiveUtcStart),
ProximoVencimento = g.Where(x => x.DtTerminoFidelizacao >= today).Min(x => x.DtTerminoFidelizacao), ProximoVencimento = g.Where(x => x.DtTerminoFidelizacao >= todayUtcStart).Min(x => x.DtTerminoFidelizacao),
UltimoVencimento = g.Where(x => x.DtTerminoFidelizacao < today).Max(x => x.DtTerminoFidelizacao) UltimoVencimento = g.Where(x => x.DtTerminoFidelizacao < todayUtcStart).Max(x => x.DtTerminoFidelizacao)
}); });
// Contagem para paginação // Contagem para paginação
@ -357,5 +388,33 @@ namespace line_gestao_api.Controllers
if (string.IsNullOrWhiteSpace(s)) return ""; if (string.IsNullOrWhiteSpace(s)) return "";
return new string(s.Where(char.IsDigit).ToArray()); return new string(s.Where(char.IsDigit).ToArray());
} }
private static bool TryParseSearchDateUtcStart(string value, out DateTime utcStart)
{
utcStart = default;
if (string.IsNullOrWhiteSpace(value)) return false;
var s = value.Trim();
DateTime parsed;
if (DateTime.TryParse(s, new CultureInfo("pt-BR"), DateTimeStyles.None, out parsed) ||
DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsed))
{
utcStart = DateTime.SpecifyKind(parsed.Date, DateTimeKind.Utc);
return true;
}
return false;
}
private static bool TryParseSearchDecimal(string value, out decimal parsed)
{
parsed = 0m;
if (string.IsNullOrWhiteSpace(value)) return false;
var s = value.Trim().Replace("R$", "", StringComparison.OrdinalIgnoreCase).Trim();
return decimal.TryParse(s, NumberStyles.Any, new CultureInfo("pt-BR"), out parsed) ||
decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out parsed);
}
} }
} }

View File

@ -58,6 +58,8 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
public DbSet<ResumoPlanoContratoResumo> ResumoPlanoContratoResumos => Set<ResumoPlanoContratoResumo>(); public DbSet<ResumoPlanoContratoResumo> ResumoPlanoContratoResumos => Set<ResumoPlanoContratoResumo>();
public DbSet<ResumoPlanoContratoTotal> ResumoPlanoContratoTotals => Set<ResumoPlanoContratoTotal>(); public DbSet<ResumoPlanoContratoTotal> ResumoPlanoContratoTotals => Set<ResumoPlanoContratoTotal>();
public DbSet<ResumoLineTotais> ResumoLineTotais => Set<ResumoLineTotais>(); public DbSet<ResumoLineTotais> ResumoLineTotais => Set<ResumoLineTotais>();
public DbSet<ResumoGbDistribuicao> ResumoGbDistribuicoes => Set<ResumoGbDistribuicao>();
public DbSet<ResumoGbDistribuicaoTotal> ResumoGbDistribuicaoTotais => Set<ResumoGbDistribuicaoTotal>();
public DbSet<ResumoReservaLine> ResumoReservaLines => Set<ResumoReservaLine>(); public DbSet<ResumoReservaLine> ResumoReservaLines => Set<ResumoReservaLine>();
public DbSet<ResumoReservaTotal> ResumoReservaTotals => Set<ResumoReservaTotal>(); public DbSet<ResumoReservaTotal> ResumoReservaTotals => Set<ResumoReservaTotal>();
@ -333,6 +335,8 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
modelBuilder.Entity<ResumoPlanoContratoResumo>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); modelBuilder.Entity<ResumoPlanoContratoResumo>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoPlanoContratoTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); modelBuilder.Entity<ResumoPlanoContratoTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoLineTotais>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); modelBuilder.Entity<ResumoLineTotais>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoGbDistribuicao>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoGbDistribuicaoTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoReservaLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); modelBuilder.Entity<ResumoReservaLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ResumoReservaTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); modelBuilder.Entity<ResumoReservaTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
modelBuilder.Entity<ParcelamentoLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId); modelBuilder.Entity<ParcelamentoLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);

View File

@ -33,6 +33,8 @@ namespace line_gestao_api.Dtos
public DateTime? DataBloqueio { get; set; } public DateTime? DataBloqueio { get; set; }
public DateTime? DataEntregaOpera { get; set; } public DateTime? DataEntregaOpera { get; set; }
public DateTime? DataEntregaCliente { get; set; } public DateTime? DataEntregaCliente { get; set; }
public DateTime? DtEfetivacaoServico { get; set; }
public DateTime? DtTerminoFidelizacao { get; set; }
// ========================== // ==========================
// Responsáveis / Logística // Responsáveis / Logística

View File

@ -21,6 +21,7 @@ namespace line_gestao_api.Dtos
public class GeralDashboardVivoKpiDto public class GeralDashboardVivoKpiDto
{ {
public int QtdLinhas { get; set; } public int QtdLinhas { get; set; }
public decimal TotalFranquiaGb { get; set; }
public decimal TotalBaseMensal { get; set; } public decimal TotalBaseMensal { get; set; }
public decimal TotalAdicionaisMensal { get; set; } public decimal TotalAdicionaisMensal { get; set; }
public decimal TotalGeralMensal { get; set; } public decimal TotalGeralMensal { get; set; }
@ -56,6 +57,7 @@ namespace line_gestao_api.Dtos
public GeralDashboardChartDto LinhasPorFranquia { get; set; } = new(); public GeralDashboardChartDto LinhasPorFranquia { get; set; } = new();
public GeralDashboardChartDto AdicionaisPagosPorServico { get; set; } = new(); public GeralDashboardChartDto AdicionaisPagosPorServico { get; set; } = new();
public GeralDashboardChartDto TravelMundo { get; set; } = new(); public GeralDashboardChartDto TravelMundo { get; set; } = new();
public GeralDashboardChartDto TipoChip { get; set; } = new();
} }
public class GeralDashboardChartDto public class GeralDashboardChartDto

View File

@ -14,6 +14,15 @@
public string? Skil { get; set; } public string? Skil { get; set; }
public string? Modalidade { get; set; } public string? Modalidade { get; set; }
public string? VencConta { get; set; } public string? VencConta { get; set; }
// Campos para filtro deterministico de adicionais no frontend
public decimal? GestaoVozDados { get; set; }
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 string? TipoDeChip { get; set; }
} }
public class MobileLineDetailDto public class MobileLineDetailDto
@ -53,6 +62,8 @@
public string? Solicitante { get; set; } public string? Solicitante { get; set; }
public DateTime? DataEntregaOpera { get; set; } public DateTime? DataEntregaOpera { get; set; }
public DateTime? DataEntregaCliente { get; set; } public DateTime? DataEntregaCliente { get; set; }
public DateTime? DtEfetivacaoServico { get; set; }
public DateTime? DtTerminoFidelizacao { get; set; }
public string? VencConta { get; set; } public string? VencConta { get; set; }
public string? TipoDeChip { get; set; } public string? TipoDeChip { get; set; }
} }
@ -94,6 +105,8 @@
public string? Solicitante { get; set; } public string? Solicitante { get; set; }
public DateTime? DataEntregaOpera { get; set; } public DateTime? DataEntregaOpera { get; set; }
public DateTime? DataEntregaCliente { get; set; } public DateTime? DataEntregaCliente { get; set; }
public DateTime? DtEfetivacaoServico { get; set; }
public DateTime? DtTerminoFidelizacao { get; set; }
public string? VencConta { get; set; } public string? VencConta { get; set; }
public string? TipoDeChip { get; set; } public string? TipoDeChip { get; set; }
} }

21
Dtos/ProfileDtos.cs Normal file
View File

@ -0,0 +1,21 @@
namespace line_gestao_api.Dtos;
public class ProfileMeDto
{
public Guid Id { get; set; }
public string Nome { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
public class UpdateProfileRequest
{
public string Nome { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
public class ChangeMyPasswordRequest
{
public string CredencialAtual { get; set; } = string.Empty;
public string NovaCredencial { get; set; } = string.Empty;
public string ConfirmarNovaCredencial { get; set; } = string.Empty;
}

View File

@ -10,6 +10,8 @@ public sealed class ResumoResponseDto
public List<ResumoPlanoContratoResumoDto> PlanoContratoResumos { get; set; } = new(); public List<ResumoPlanoContratoResumoDto> PlanoContratoResumos { get; set; } = new();
public ResumoPlanoContratoTotalDto? PlanoContratoTotal { get; set; } public ResumoPlanoContratoTotalDto? PlanoContratoTotal { get; set; }
public List<ResumoLineTotaisDto> LineTotais { get; set; } = new(); public List<ResumoLineTotaisDto> LineTotais { get; set; } = new();
public List<ResumoGbDistribuicaoDto> GbDistribuicao { get; set; } = new();
public ResumoGbDistribuicaoTotalDto? GbDistribuicaoTotal { get; set; }
public List<ResumoReservaLineDto> ReservaLines { get; set; } = new(); public List<ResumoReservaLineDto> ReservaLines { get; set; } = new();
public List<ResumoReservaPorDddDto> ReservaPorDdd { get; set; } = new(); public List<ResumoReservaPorDddDto> ReservaPorDdd { get; set; } = new();
public int? TotalGeralLinhasReserva { get; set; } public int? TotalGeralLinhasReserva { get; set; }
@ -85,6 +87,19 @@ public sealed class ResumoLineTotaisDto
public int? QtdLinhas { get; set; } public int? QtdLinhas { get; set; }
} }
public sealed class ResumoGbDistribuicaoDto
{
public decimal? Gb { get; set; }
public int? Qtd { get; set; }
public decimal? Soma { get; set; }
}
public sealed class ResumoGbDistribuicaoTotalDto
{
public int? TotalLinhas { get; set; }
public decimal? SomaTotal { get; set; }
}
public sealed class ResumoReservaLineDto public sealed class ResumoReservaLineDto
{ {
public string? Ddd { get; set; } public string? Ddd { get; set; }

View File

@ -0,0 +1,19 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using line_gestao_api.Data;
#nullable disable
namespace line_gestao_api.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260214120000_AddResumoGbDistribuicao")]
partial class AddResumoGbDistribuicao
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
}
}
}

View File

@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace line_gestao_api.Migrations
{
/// <inheritdoc />
public partial class AddResumoGbDistribuicao : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ResumoGbDistribuicoes",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
TenantId = table.Column<Guid>(type: "uuid", nullable: false),
Gb = table.Column<decimal>(type: "numeric", nullable: true),
Qtd = table.Column<int>(type: "integer", nullable: true),
Soma = table.Column<decimal>(type: "numeric", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ResumoGbDistribuicoes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ResumoGbDistribuicaoTotais",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
TenantId = table.Column<Guid>(type: "uuid", nullable: false),
TotalLinhas = table.Column<int>(type: "integer", nullable: true),
SomaTotal = table.Column<decimal>(type: "numeric", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ResumoGbDistribuicaoTotais", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ResumoGbDistribuicoes");
migrationBuilder.DropTable(
name: "ResumoGbDistribuicaoTotais");
}
}
}

View File

@ -990,6 +990,61 @@ namespace line_gestao_api.Migrations
b.ToTable("ResumoClienteEspeciais"); b.ToTable("ResumoClienteEspeciais");
}); });
modelBuilder.Entity("line_gestao_api.Models.ResumoGbDistribuicao", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("Gb")
.HasColumnType("numeric");
b.Property<int?>("Qtd")
.HasColumnType("integer");
b.Property<decimal?>("Soma")
.HasColumnType("numeric");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("ResumoGbDistribuicoes");
});
modelBuilder.Entity("line_gestao_api.Models.ResumoGbDistribuicaoTotal", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal?>("SomaTotal")
.HasColumnType("numeric");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<int?>("TotalLinhas")
.HasColumnType("integer");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("ResumoGbDistribuicaoTotais");
});
modelBuilder.Entity("line_gestao_api.Models.ResumoLineTotais", b => modelBuilder.Entity("line_gestao_api.Models.ResumoLineTotais", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")

View File

@ -0,0 +1,15 @@
namespace line_gestao_api.Models;
public class ResumoGbDistribuicao : ITenantEntity
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid TenantId { get; set; }
public decimal? Gb { get; set; }
public int? Qtd { get; set; }
public decimal? Soma { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}

View File

@ -0,0 +1,14 @@
namespace line_gestao_api.Models;
public class ResumoGbDistribuicaoTotal : ITenantEntity
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid TenantId { get; set; }
public int? TotalLinhas { get; set; }
public decimal? SomaTotal { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}

View File

@ -1,4 +1,5 @@
using System.Globalization; using System.Globalization;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using line_gestao_api.Data; using line_gestao_api.Data;
using line_gestao_api.Dtos; using line_gestao_api.Dtos;
@ -14,6 +15,7 @@ namespace line_gestao_api.Services
private const string ServiceSkeelo = "SKEELO"; private const string ServiceSkeelo = "SKEELO";
private const string ServiceVivoNewsPlus = "VIVO NEWS PLUS"; private const string ServiceVivoNewsPlus = "VIVO NEWS PLUS";
private const string ServiceVivoTravelMundo = "VIVO TRAVEL MUNDO"; private const string ServiceVivoTravelMundo = "VIVO TRAVEL MUNDO";
private const string ServiceVivoSync = "VIVO SYNC";
private const string ServiceVivoGestaoDispositivo = "VIVO GESTÃO DISPOSITIVO"; private const string ServiceVivoGestaoDispositivo = "VIVO GESTÃO DISPOSITIVO";
private readonly AppDbContext _db; private readonly AppDbContext _db;
@ -45,6 +47,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null), x.VivoGestaoDispositivo != null),
VivoBaseTotal = g.Sum(x => VivoBaseTotal = g.Sum(x =>
(x.ValorPlanoVivo != null || (x.ValorPlanoVivo != null ||
@ -54,9 +57,22 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) x.VivoGestaoDispositivo != null)
? (x.ValorPlanoVivo ?? 0m) ? (x.ValorPlanoVivo ?? 0m)
: 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 => VivoAdicionaisTotal = g.Sum(x =>
(x.ValorPlanoVivo != null || (x.ValorPlanoVivo != null ||
x.FranquiaVivo != null || x.FranquiaVivo != null ||
@ -65,9 +81,10 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) x.VivoGestaoDispositivo != null)
? (x.GestaoVozDados ?? 0m) + (x.Skeelo ?? 0m) + (x.VivoNewsPlus ?? 0m) + ? (x.GestaoVozDados ?? 0m) + (x.Skeelo ?? 0m) + (x.VivoNewsPlus ?? 0m) +
(x.VivoTravelMundo ?? 0m) + (x.VivoGestaoDispositivo ?? 0m) (x.VivoTravelMundo ?? 0m) + (x.VivoSync ?? 0m) + (x.VivoGestaoDispositivo ?? 0m)
: 0m), : 0m),
VivoMinBase = g.Where(x => VivoMinBase = g.Where(x =>
x.ValorPlanoVivo != null || x.ValorPlanoVivo != null ||
@ -77,6 +94,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) x.VivoGestaoDispositivo != null)
.Select(x => x.ValorPlanoVivo ?? 0m) .Select(x => x.ValorPlanoVivo ?? 0m)
.DefaultIfEmpty() .DefaultIfEmpty()
@ -89,41 +107,14 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) x.VivoGestaoDispositivo != null)
.Select(x => x.ValorPlanoVivo ?? 0m) .Select(x => x.ValorPlanoVivo ?? 0m)
.DefaultIfEmpty() .DefaultIfEmpty()
.Max(), .Max(),
TravelCom = g.Count(x => TravelCom = g.Count(x => (x.VivoTravelMundo ?? 0m) > 0m),
(x.ValorPlanoVivo != null || TravelSem = g.Count(x => (x.VivoTravelMundo ?? 0m) <= 0m),
x.FranquiaVivo != null || TravelTotal = g.Sum(x => x.VivoTravelMundo ?? 0m),
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 => PaidGestaoVozDados = g.Count(x =>
(x.ValorPlanoVivo != null || (x.ValorPlanoVivo != null ||
x.FranquiaVivo != null || x.FranquiaVivo != null ||
@ -132,6 +123,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.GestaoVozDados ?? 0m) > 0m), (x.GestaoVozDados ?? 0m) > 0m),
PaidSkeelo = g.Count(x => PaidSkeelo = g.Count(x =>
@ -142,6 +134,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.Skeelo ?? 0m) > 0m), (x.Skeelo ?? 0m) > 0m),
PaidNews = g.Count(x => PaidNews = g.Count(x =>
@ -152,6 +145,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.VivoNewsPlus ?? 0m) > 0m), (x.VivoNewsPlus ?? 0m) > 0m),
PaidTravel = g.Count(x => PaidTravel = g.Count(x =>
@ -162,6 +156,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.VivoTravelMundo ?? 0m) > 0m), (x.VivoTravelMundo ?? 0m) > 0m),
PaidGestaoDispositivo = g.Count(x => PaidGestaoDispositivo = g.Count(x =>
@ -172,8 +167,20 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.VivoGestaoDispositivo ?? 0m) > 0m), (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 => NotPaidGestaoVozDados = g.Count(x =>
(x.ValorPlanoVivo != null || (x.ValorPlanoVivo != null ||
x.FranquiaVivo != null || x.FranquiaVivo != null ||
@ -182,6 +189,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.GestaoVozDados ?? 0m) <= 0m), (x.GestaoVozDados ?? 0m) <= 0m),
NotPaidSkeelo = g.Count(x => NotPaidSkeelo = g.Count(x =>
@ -192,6 +200,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.Skeelo ?? 0m) <= 0m), (x.Skeelo ?? 0m) <= 0m),
NotPaidNews = g.Count(x => NotPaidNews = g.Count(x =>
@ -202,6 +211,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.VivoNewsPlus ?? 0m) <= 0m), (x.VivoNewsPlus ?? 0m) <= 0m),
NotPaidTravel = g.Count(x => NotPaidTravel = g.Count(x =>
@ -212,6 +222,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.VivoTravelMundo ?? 0m) <= 0m), (x.VivoTravelMundo ?? 0m) <= 0m),
NotPaidGestaoDispositivo = g.Count(x => NotPaidGestaoDispositivo = g.Count(x =>
@ -222,8 +233,20 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.VivoGestaoDispositivo ?? 0m) <= 0m), (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 => TotalLinesWithAnyPaidAdditional = g.Count(x =>
(x.ValorPlanoVivo != null || (x.ValorPlanoVivo != null ||
x.FranquiaVivo != null || x.FranquiaVivo != null ||
@ -232,11 +255,13 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
((x.GestaoVozDados ?? 0m) > 0m || ((x.GestaoVozDados ?? 0m) > 0m ||
(x.Skeelo ?? 0m) > 0m || (x.Skeelo ?? 0m) > 0m ||
(x.VivoNewsPlus ?? 0m) > 0m || (x.VivoNewsPlus ?? 0m) > 0m ||
(x.VivoTravelMundo ?? 0m) > 0m || (x.VivoTravelMundo ?? 0m) > 0m ||
(x.VivoSync ?? 0m) > 0m ||
(x.VivoGestaoDispositivo ?? 0m) > 0m)), (x.VivoGestaoDispositivo ?? 0m) > 0m)),
TotalLinesWithNoPaidAdditional = g.Count(x => TotalLinesWithNoPaidAdditional = g.Count(x =>
(x.ValorPlanoVivo != null || (x.ValorPlanoVivo != null ||
@ -246,11 +271,13 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.GestaoVozDados ?? 0m) <= 0m && (x.GestaoVozDados ?? 0m) <= 0m &&
(x.Skeelo ?? 0m) <= 0m && (x.Skeelo ?? 0m) <= 0m &&
(x.VivoNewsPlus ?? 0m) <= 0m && (x.VivoNewsPlus ?? 0m) <= 0m &&
(x.VivoTravelMundo ?? 0m) <= 0m && (x.VivoTravelMundo ?? 0m) <= 0m &&
(x.VivoSync ?? 0m) <= 0m &&
(x.VivoGestaoDispositivo ?? 0m) <= 0m), (x.VivoGestaoDispositivo ?? 0m) <= 0m),
TotalGestaoVozDados = g.Sum(x => TotalGestaoVozDados = g.Sum(x =>
(x.ValorPlanoVivo != null || (x.ValorPlanoVivo != null ||
@ -260,6 +287,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) x.VivoGestaoDispositivo != null)
? (x.GestaoVozDados ?? 0m) ? (x.GestaoVozDados ?? 0m)
: 0m), : 0m),
@ -271,6 +299,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) x.VivoGestaoDispositivo != null)
? (x.Skeelo ?? 0m) ? (x.Skeelo ?? 0m)
: 0m), : 0m),
@ -282,6 +311,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) x.VivoGestaoDispositivo != null)
? (x.VivoNewsPlus ?? 0m) ? (x.VivoNewsPlus ?? 0m)
: 0m), : 0m),
@ -293,6 +323,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) x.VivoGestaoDispositivo != null)
? (x.VivoTravelMundo ?? 0m) ? (x.VivoTravelMundo ?? 0m)
: 0m), : 0m),
@ -304,8 +335,21 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) x.VivoGestaoDispositivo != null)
? (x.VivoGestaoDispositivo ?? 0m) ? (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) : 0m)
}) })
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
@ -315,6 +359,8 @@ namespace line_gestao_api.Services
.ToListAsync(); .ToListAsync();
var linhasPorFranquia = BuildFranquiaBuckets(franquiasRaw); var linhasPorFranquia = BuildFranquiaBuckets(franquiasRaw);
var tipoChip = await qLines.Select(x => x.TipoDeChip).ToListAsync();
var tipoChipBuckets = BuildTipoChipBuckets(tipoChip);
var clientGroupsRaw = await qLines var clientGroupsRaw = await qLines
.Where(x => x.Cliente != null && x.Cliente != "") .Where(x => x.Cliente != null && x.Cliente != "")
@ -336,27 +382,10 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null), x.VivoGestaoDispositivo != null),
TravelCom = g.Count(x => TravelCom = g.Count(x => (x.VivoTravelMundo ?? 0m) > 0m),
(x.ValorPlanoVivo != null || TravelSem = g.Count(x => (x.VivoTravelMundo ?? 0m) <= 0m),
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 => PaidGestaoVozDados = g.Count(x =>
(x.ValorPlanoVivo != null || (x.ValorPlanoVivo != null ||
x.FranquiaVivo != null || x.FranquiaVivo != null ||
@ -365,6 +394,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.GestaoVozDados ?? 0m) > 0m), (x.GestaoVozDados ?? 0m) > 0m),
PaidSkeelo = g.Count(x => PaidSkeelo = g.Count(x =>
@ -375,6 +405,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.Skeelo ?? 0m) > 0m), (x.Skeelo ?? 0m) > 0m),
PaidNews = g.Count(x => PaidNews = g.Count(x =>
@ -385,6 +416,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.VivoNewsPlus ?? 0m) > 0m), (x.VivoNewsPlus ?? 0m) > 0m),
PaidTravel = g.Count(x => PaidTravel = g.Count(x =>
@ -395,6 +427,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.VivoTravelMundo ?? 0m) > 0m), (x.VivoTravelMundo ?? 0m) > 0m),
PaidGestaoDispositivo = g.Count(x => PaidGestaoDispositivo = g.Count(x =>
@ -405,8 +438,20 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.VivoGestaoDispositivo ?? 0m) > 0m), (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 => NotPaidGestaoVozDados = g.Count(x =>
(x.ValorPlanoVivo != null || (x.ValorPlanoVivo != null ||
x.FranquiaVivo != null || x.FranquiaVivo != null ||
@ -415,6 +460,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.GestaoVozDados ?? 0m) <= 0m), (x.GestaoVozDados ?? 0m) <= 0m),
NotPaidSkeelo = g.Count(x => NotPaidSkeelo = g.Count(x =>
@ -425,6 +471,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.Skeelo ?? 0m) <= 0m), (x.Skeelo ?? 0m) <= 0m),
NotPaidNews = g.Count(x => NotPaidNews = g.Count(x =>
@ -435,6 +482,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.VivoNewsPlus ?? 0m) <= 0m), (x.VivoNewsPlus ?? 0m) <= 0m),
NotPaidTravel = g.Count(x => NotPaidTravel = g.Count(x =>
@ -445,6 +493,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.VivoTravelMundo ?? 0m) <= 0m), (x.VivoTravelMundo ?? 0m) <= 0m),
NotPaidGestaoDispositivo = g.Count(x => NotPaidGestaoDispositivo = g.Count(x =>
@ -455,8 +504,20 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) && x.VivoGestaoDispositivo != null) &&
(x.VivoGestaoDispositivo ?? 0m) <= 0m), (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 => TotalGestaoVozDados = g.Sum(x =>
(x.ValorPlanoVivo != null || (x.ValorPlanoVivo != null ||
x.FranquiaVivo != null || x.FranquiaVivo != null ||
@ -465,6 +526,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) x.VivoGestaoDispositivo != null)
? (x.GestaoVozDados ?? 0m) ? (x.GestaoVozDados ?? 0m)
: 0m), : 0m),
@ -476,6 +538,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) x.VivoGestaoDispositivo != null)
? (x.Skeelo ?? 0m) ? (x.Skeelo ?? 0m)
: 0m), : 0m),
@ -487,6 +550,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) x.VivoGestaoDispositivo != null)
? (x.VivoNewsPlus ?? 0m) ? (x.VivoNewsPlus ?? 0m)
: 0m), : 0m),
@ -498,6 +562,7 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) x.VivoGestaoDispositivo != null)
? (x.VivoTravelMundo ?? 0m) ? (x.VivoTravelMundo ?? 0m)
: 0m), : 0m),
@ -509,8 +574,21 @@ namespace line_gestao_api.Services
x.Skeelo != null || x.Skeelo != null ||
x.VivoNewsPlus != null || x.VivoNewsPlus != null ||
x.VivoTravelMundo != null || x.VivoTravelMundo != null ||
x.VivoSync != null ||
x.VivoGestaoDispositivo != null) x.VivoGestaoDispositivo != null)
? (x.VivoGestaoDispositivo ?? 0m) ? (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) : 0m)
}) })
.OrderBy(x => x.Cliente) .OrderBy(x => x.Cliente)
@ -519,48 +597,13 @@ namespace line_gestao_api.Services
var dto = new GeralDashboardInsightsDto var dto = new GeralDashboardInsightsDto
{ {
Kpis = BuildKpis(totals), Kpis = BuildKpis(totals),
Charts = BuildCharts(totals, linhasPorFranquia), Charts = BuildCharts(totals, linhasPorFranquia, tipoChipBuckets),
ClientGroups = BuildClientGroups(clientGroupsRaw) ClientGroups = BuildClientGroups(clientGroupsRaw)
}; };
dto.Kpis.TotalLinhas = await ResolveCanonicalTotalLinhasAsync(dto.Kpis.TotalLinhas);
return dto; return dto;
} }
private async Task<int> ResolveCanonicalTotalLinhasAsync(int fallback)
{
try
{
var fromLatestRun = await _db.ImportAuditRuns
.AsNoTracking()
.OrderByDescending(x => x.ImportedAt)
.ThenByDescending(x => x.Id)
.Select(x => (int?)x.CanonicalTotalLinhas)
.FirstOrDefaultAsync();
if (fromLatestRun.HasValue && fromLatestRun.Value > 0)
{
return fromLatestRun.Value;
}
}
catch
{
// Fallback para ambientes em que a migration ainda não foi aplicada.
}
var fromMaxItem = await _db.MobileLines
.AsNoTracking()
.MaxAsync(x => (int?)x.Item);
if (fromMaxItem.HasValue && fromMaxItem.Value > 0)
{
return fromMaxItem.Value;
}
return fallback;
}
private static GeralDashboardKpisDto BuildKpis(TotalsProjection? totals) private static GeralDashboardKpisDto BuildKpis(TotalsProjection? totals)
{ {
if (totals == null) if (totals == null)
@ -583,6 +626,7 @@ namespace line_gestao_api.Services
Vivo = new GeralDashboardVivoKpiDto Vivo = new GeralDashboardVivoKpiDto
{ {
QtdLinhas = totals.VivoLinhas, QtdLinhas = totals.VivoLinhas,
TotalFranquiaGb = totals.VivoFranquiaTotalGb,
TotalBaseMensal = totals.VivoBaseTotal, TotalBaseMensal = totals.VivoBaseTotal,
TotalAdicionaisMensal = totals.VivoAdicionaisTotal, TotalAdicionaisMensal = totals.VivoAdicionaisTotal,
TotalGeralMensal = totalGeralMensal, TotalGeralMensal = totalGeralMensal,
@ -606,6 +650,7 @@ namespace line_gestao_api.Services
new() { ServiceName = ServiceSkeelo, CountLines = totals.PaidSkeelo, TotalValue = totals.TotalSkeelo }, new() { ServiceName = ServiceSkeelo, CountLines = totals.PaidSkeelo, TotalValue = totals.TotalSkeelo },
new() { ServiceName = ServiceVivoNewsPlus, CountLines = totals.PaidNews, TotalValue = totals.TotalNews }, new() { ServiceName = ServiceVivoNewsPlus, CountLines = totals.PaidNews, TotalValue = totals.TotalNews },
new() { ServiceName = ServiceVivoTravelMundo, CountLines = totals.PaidTravel, TotalValue = totals.TotalTravel }, 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 } new() { ServiceName = ServiceVivoGestaoDispositivo, CountLines = totals.PaidGestaoDispositivo, TotalValue = totals.TotalGestaoDispositivo }
}, },
ServicesNotPaid = new List<GeralDashboardServiceKpiDto> ServicesNotPaid = new List<GeralDashboardServiceKpiDto>
@ -614,13 +659,14 @@ namespace line_gestao_api.Services
new() { ServiceName = ServiceSkeelo, CountLines = totals.NotPaidSkeelo, TotalValue = 0m }, new() { ServiceName = ServiceSkeelo, CountLines = totals.NotPaidSkeelo, TotalValue = 0m },
new() { ServiceName = ServiceVivoNewsPlus, CountLines = totals.NotPaidNews, TotalValue = 0m }, new() { ServiceName = ServiceVivoNewsPlus, CountLines = totals.NotPaidNews, TotalValue = 0m },
new() { ServiceName = ServiceVivoTravelMundo, CountLines = totals.NotPaidTravel, 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 } new() { ServiceName = ServiceVivoGestaoDispositivo, CountLines = totals.NotPaidGestaoDispositivo, TotalValue = 0m }
} }
} }
}; };
} }
private static GeralDashboardChartsDto BuildCharts(TotalsProjection? totals, FranquiaBuckets franquias) private static GeralDashboardChartsDto BuildCharts(TotalsProjection? totals, FranquiaBuckets franquias, TipoChipBuckets tipoChip)
{ {
var adicionaisLabels = new List<string> var adicionaisLabels = new List<string>
{ {
@ -628,28 +674,31 @@ namespace line_gestao_api.Services
ServiceSkeelo, ServiceSkeelo,
ServiceVivoNewsPlus, ServiceVivoNewsPlus,
ServiceVivoTravelMundo, ServiceVivoTravelMundo,
ServiceVivoSync,
ServiceVivoGestaoDispositivo ServiceVivoGestaoDispositivo
}; };
var adicionaisValues = totals == null var adicionaisValues = totals == null
? new List<int> { 0, 0, 0, 0, 0 } ? new List<int> { 0, 0, 0, 0, 0, 0 }
: new List<int> : new List<int>
{ {
totals.PaidGestaoVozDados, totals.PaidGestaoVozDados,
totals.PaidSkeelo, totals.PaidSkeelo,
totals.PaidNews, totals.PaidNews,
totals.PaidTravel, totals.PaidTravel,
totals.PaidSync,
totals.PaidGestaoDispositivo totals.PaidGestaoDispositivo
}; };
var adicionaisTotals = totals == null var adicionaisTotals = totals == null
? new List<decimal> { 0m, 0m, 0m, 0m, 0m } ? new List<decimal> { 0m, 0m, 0m, 0m, 0m, 0m }
: new List<decimal> : new List<decimal>
{ {
totals.TotalGestaoVozDados, totals.TotalGestaoVozDados,
totals.TotalSkeelo, totals.TotalSkeelo,
totals.TotalNews, totals.TotalNews,
totals.TotalTravel, totals.TotalTravel,
totals.TotalSync,
totals.TotalGestaoDispositivo totals.TotalGestaoDispositivo
}; };
@ -672,6 +721,11 @@ namespace line_gestao_api.Services
Values = totals == null Values = totals == null
? new List<int> { 0, 0 } ? new List<int> { 0, 0 }
: new List<int> { totals.TravelCom, totals.TravelSem } : new List<int> { totals.TravelCom, totals.TravelSem }
},
TipoChip = new GeralDashboardChartDto
{
Labels = tipoChip.Labels,
Values = tipoChip.Values
} }
}; };
} }
@ -702,6 +756,8 @@ namespace line_gestao_api.Services
new() { Label = $"R$ {ServiceVivoNewsPlus}", Value = FormatCurrency(row.TotalNews) }, new() { Label = $"R$ {ServiceVivoNewsPlus}", Value = FormatCurrency(row.TotalNews) },
new() { Label = ServiceVivoTravelMundo, Value = row.PaidTravel.ToString(PtBr) }, new() { Label = ServiceVivoTravelMundo, Value = row.PaidTravel.ToString(PtBr) },
new() { Label = $"R$ {ServiceVivoTravelMundo}", Value = FormatCurrency(row.TotalTravel) }, 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 = ServiceVivoGestaoDispositivo, Value = row.PaidGestaoDispositivo.ToString(PtBr) },
new() { Label = $"R$ {ServiceVivoGestaoDispositivo}", Value = FormatCurrency(row.TotalGestaoDispositivo) } new() { Label = $"R$ {ServiceVivoGestaoDispositivo}", Value = FormatCurrency(row.TotalGestaoDispositivo) }
}; };
@ -737,6 +793,13 @@ namespace line_gestao_api.Services
TotalValue = row.TotalTravel TotalValue = row.TotalTravel
}, },
new() new()
{
ServiceName = ServiceVivoSync,
PaidCount = row.PaidSync,
NotPaidCount = row.NotPaidSync,
TotalValue = row.TotalSync
},
new()
{ {
ServiceName = ServiceVivoGestaoDispositivo, ServiceName = ServiceVivoGestaoDispositivo,
PaidCount = row.PaidGestaoDispositivo, PaidCount = row.PaidGestaoDispositivo,
@ -761,6 +824,79 @@ namespace line_gestao_api.Services
return list; return list;
} }
private static TipoChipBuckets BuildTipoChipBuckets(IEnumerable<string?> 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<string> { "e-SIM", "SIMCARD" },
Values = new List<int> { 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<FranquiaProjection> rows) private static FranquiaBuckets BuildFranquiaBuckets(IEnumerable<FranquiaProjection> rows)
{ {
var map = new Dictionary<string, FranquiaBucket>(); var map = new Dictionary<string, FranquiaBucket>();
@ -885,6 +1021,12 @@ namespace line_gestao_api.Services
public List<int> Values { get; set; } = new(); public List<int> Values { get; set; } = new();
} }
private sealed class TipoChipBuckets
{
public List<string> Labels { get; set; } = new();
public List<int> Values { get; set; } = new();
}
private sealed class FranquiaProjection private sealed class FranquiaProjection
{ {
public decimal? FranquiaVivo { get; set; } public decimal? FranquiaVivo { get; set; }
@ -897,6 +1039,7 @@ namespace line_gestao_api.Services
public int TotalAtivas { get; set; } public int TotalAtivas { get; set; }
public int TotalBloqueados { get; set; } public int TotalBloqueados { get; set; }
public int VivoLinhas { get; set; } public int VivoLinhas { get; set; }
public decimal VivoFranquiaTotalGb { get; set; }
public decimal VivoBaseTotal { get; set; } public decimal VivoBaseTotal { get; set; }
public decimal VivoAdicionaisTotal { get; set; } public decimal VivoAdicionaisTotal { get; set; }
public decimal VivoMinBase { get; set; } public decimal VivoMinBase { get; set; }
@ -909,11 +1052,13 @@ namespace line_gestao_api.Services
public int PaidNews { get; set; } public int PaidNews { get; set; }
public int PaidTravel { get; set; } public int PaidTravel { get; set; }
public int PaidGestaoDispositivo { get; set; } public int PaidGestaoDispositivo { get; set; }
public int PaidSync { get; set; }
public int NotPaidGestaoVozDados { get; set; } public int NotPaidGestaoVozDados { get; set; }
public int NotPaidSkeelo { get; set; } public int NotPaidSkeelo { get; set; }
public int NotPaidNews { get; set; } public int NotPaidNews { get; set; }
public int NotPaidTravel { get; set; } public int NotPaidTravel { get; set; }
public int NotPaidGestaoDispositivo { get; set; } public int NotPaidGestaoDispositivo { get; set; }
public int NotPaidSync { get; set; }
public int TotalLinesWithAnyPaidAdditional { get; set; } public int TotalLinesWithAnyPaidAdditional { get; set; }
public int TotalLinesWithNoPaidAdditional { get; set; } public int TotalLinesWithNoPaidAdditional { get; set; }
public decimal TotalGestaoVozDados { get; set; } public decimal TotalGestaoVozDados { get; set; }
@ -921,6 +1066,7 @@ namespace line_gestao_api.Services
public decimal TotalNews { get; set; } public decimal TotalNews { get; set; }
public decimal TotalTravel { get; set; } public decimal TotalTravel { get; set; }
public decimal TotalGestaoDispositivo { get; set; } public decimal TotalGestaoDispositivo { get; set; }
public decimal TotalSync { get; set; }
} }
private sealed class ClientGroupProjection private sealed class ClientGroupProjection
@ -937,16 +1083,19 @@ namespace line_gestao_api.Services
public int PaidNews { get; set; } public int PaidNews { get; set; }
public int PaidTravel { get; set; } public int PaidTravel { get; set; }
public int PaidGestaoDispositivo { get; set; } public int PaidGestaoDispositivo { get; set; }
public int PaidSync { get; set; }
public int NotPaidGestaoVozDados { get; set; } public int NotPaidGestaoVozDados { get; set; }
public int NotPaidSkeelo { get; set; } public int NotPaidSkeelo { get; set; }
public int NotPaidNews { get; set; } public int NotPaidNews { get; set; }
public int NotPaidTravel { get; set; } public int NotPaidTravel { get; set; }
public int NotPaidGestaoDispositivo { get; set; } public int NotPaidGestaoDispositivo { get; set; }
public int NotPaidSync { get; set; }
public decimal TotalGestaoVozDados { get; set; } public decimal TotalGestaoVozDados { get; set; }
public decimal TotalSkeelo { get; set; } public decimal TotalSkeelo { get; set; }
public decimal TotalNews { get; set; } public decimal TotalNews { get; set; }
public decimal TotalTravel { get; set; } public decimal TotalTravel { get; set; }
public decimal TotalGestaoDispositivo { get; set; } public decimal TotalGestaoDispositivo { get; set; }
public decimal TotalSync { get; set; }
} }
} }
} }

View File

@ -96,7 +96,7 @@ public class VigenciaNotificationBackgroundService : BackgroundService
private async Task ProcessTenantAsync(AppDbContext db, Guid tenantId, CancellationToken stoppingToken) private async Task ProcessTenantAsync(AppDbContext db, Guid tenantId, CancellationToken stoppingToken)
{ {
var today = DateTime.UtcNow.Date; var today = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc);
var reminderDays = _options.ReminderDays var reminderDays = _options.ReminderDays
.Distinct() .Distinct()
.Where(d => d > 0) .Where(d => d > 0)
@ -132,7 +132,7 @@ public class VigenciaNotificationBackgroundService : BackgroundService
continue; continue;
} }
var endDate = vigencia.DtTerminoFidelizacao.Value.Date; var endDate = DateTime.SpecifyKind(vigencia.DtTerminoFidelizacao.Value.Date, DateTimeKind.Utc);
var usuario = vigencia.Usuario?.Trim(); var usuario = vigencia.Usuario?.Trim();
var cliente = vigencia.Cliente?.Trim(); var cliente = vigencia.Cliente?.Trim();
var linha = vigencia.Linha?.Trim(); var linha = vigencia.Linha?.Trim();
@ -198,15 +198,33 @@ public class VigenciaNotificationBackgroundService : BackgroundService
var candidateTipos = candidates.Select(c => c.Tipo).Distinct().ToList(); var candidateTipos = candidates.Select(c => c.Tipo).Distinct().ToList();
var candidateDates = candidates var candidateDates = candidates
.Where(c => c.ReferenciaData.HasValue) .Where(c => c.ReferenciaData.HasValue)
.Select(c => c.ReferenciaData!.Value.Date) .Select(c => DateTime.SpecifyKind(c.ReferenciaData!.Value.Date, DateTimeKind.Utc))
.Distinct() .Distinct()
.ToList(); .ToList();
var existingNotifications = await db.Notifications.AsNoTracking()
List<Notification> existingNotifications = new();
if (candidateDates.Count > 0)
{
var minCandidateUtc = candidateDates.Min();
var maxCandidateUtcExclusive = candidateDates.Max().AddDays(1);
var candidateDateKeys = candidateDates
.Select(d => d.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture))
.ToHashSet();
existingNotifications = await db.Notifications.AsNoTracking()
.Where(n => n.TenantId == tenantId) .Where(n => n.TenantId == tenantId)
.Where(n => candidateTipos.Contains(n.Tipo)) .Where(n => candidateTipos.Contains(n.Tipo))
.Where(n => n.ReferenciaData != null && candidateDates.Contains(n.ReferenciaData.Value.Date)) .Where(n => n.ReferenciaData != null &&
n.ReferenciaData.Value >= minCandidateUtc &&
n.ReferenciaData.Value < maxCandidateUtcExclusive)
.ToListAsync(stoppingToken); .ToListAsync(stoppingToken);
existingNotifications = existingNotifications
.Where(n => n.ReferenciaData != null &&
candidateDateKeys.Contains(n.ReferenciaData.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)))
.ToList();
}
var existingSet = new HashSet<string>(existingNotifications.Select(n => var existingSet = new HashSet<string>(existingNotifications.Select(n =>
BuildDedupKey( BuildDedupKey(
n.Tipo, n.Tipo,
@ -260,7 +278,7 @@ public class VigenciaNotificationBackgroundService : BackgroundService
continue; continue;
} }
var endDate = vigencia.DtTerminoFidelizacao.Value.Date; var endDate = DateTime.SpecifyKind(vigencia.DtTerminoFidelizacao.Value.Date, DateTimeKind.Utc);
if (endDate < today) if (endDate < today)
{ {
if (notification.Tipo != "Vencido") if (notification.Tipo != "Vencido")

View File

@ -6,7 +6,7 @@
"Key": "vI8/oEYEWN5sBDTisNuZFjZAl+YFvXEJ96POb73/eoq3NaFPkOFXyPRdf/HWGAFnUsF3e3QpYL6Wl4Bc2v+l3g==", "Key": "vI8/oEYEWN5sBDTisNuZFjZAl+YFvXEJ96POb73/eoq3NaFPkOFXyPRdf/HWGAFnUsF3e3QpYL6Wl4Bc2v+l3g==",
"Issuer": "LineGestao", "Issuer": "LineGestao",
"Audience": "LineGestao", "Audience": "LineGestao",
"ExpiresMinutes": 180 "ExpiresMinutes": 360
}, },
"Notifications": { "Notifications": {
"CheckIntervalMinutes": 60, "CheckIntervalMinutes": 60,

View File

@ -89,6 +89,49 @@ namespace line_gestao_api.Tests
Assert.NotEmpty(result.Charts.AdicionaisPagosPorServico.Labels); Assert.NotEmpty(result.Charts.AdicionaisPagosPorServico.Labels);
} }
[Fact]
public async Task GetInsightsAsync_TravelZeroIsCountedAsSemTravel()
{
var tenantId = Guid.NewGuid();
var provider = new TestTenantProvider(tenantId);
var db = BuildContext(provider);
db.MobileLines.AddRange(
new MobileLine
{
TenantId = tenantId,
Cliente = "Cliente Travel",
Status = "Ativo",
ValorPlanoVivo = 120m,
VivoTravelMundo = 0m
},
new MobileLine
{
TenantId = tenantId,
Cliente = "Cliente Travel",
Status = "Ativo",
ValorPlanoVivo = 120m,
VivoTravelMundo = 15m
},
new MobileLine
{
TenantId = tenantId,
Cliente = "Cliente Travel",
Status = "Ativo",
ValorPlanoVivo = null,
VivoTravelMundo = null
});
await db.SaveChangesAsync();
var service = new GeralDashboardInsightsService(db);
var result = await service.GetInsightsAsync();
Assert.Equal(1, result.Kpis.TravelMundo.ComTravel);
Assert.Equal(2, result.Kpis.TravelMundo.SemTravel);
Assert.Equal(new[] { 1, 2 }, result.Charts.TravelMundo.Values);
}
private static AppDbContext BuildContext(TestTenantProvider provider) private static AppDbContext BuildContext(TestTenantProvider provider)
{ {
var options = new DbContextOptionsBuilder<AppDbContext>() var options = new DbContextOptionsBuilder<AppDbContext>()