Minha alteração
This commit is contained in:
parent
514c7ba8cd
commit
142bb60967
|
|
@ -3,6 +3,8 @@ using line_gestao_api.Dtos;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace line_gestao_api.Controllers
|
||||
{
|
||||
|
|
@ -39,9 +41,20 @@ namespace line_gestao_api.Controllers
|
|||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
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}%")
|
||||
|| EF.Functions.ILike(x.Tipo ?? "", $"%{s}%")
|
||||
|| EF.Functions.ILike(x.Item.ToString(), $"%{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))
|
||||
|
|
@ -218,5 +231,16 @@ namespace line_gestao_api.Controllers
|
|||
await _db.SaveChangesAsync();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using line_gestao_api.Models;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace line_gestao_api.Controllers
|
||||
|
|
@ -36,10 +37,15 @@ namespace line_gestao_api.Controllers
|
|||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
var s = search.Trim();
|
||||
var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc);
|
||||
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
|
||||
q = q.Where(x =>
|
||||
EF.Functions.ILike(x.NumeroDoChip ?? "", $"%{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();
|
||||
|
|
@ -169,5 +175,23 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using line_gestao_api.Models;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace line_gestao_api.Controllers
|
||||
|
|
@ -44,6 +45,14 @@ namespace line_gestao_api.Controllers
|
|||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
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 =>
|
||||
EF.Functions.ILike(x.NotaFiscal ?? "", $"%{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.NumeroDaLinha ?? "", $"%{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();
|
||||
|
|
@ -262,5 +283,33 @@ namespace line_gestao_api.Controllers
|
|||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using line_gestao_api.Services;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
|
||||
namespace line_gestao_api.Controllers;
|
||||
|
||||
|
|
@ -67,13 +68,25 @@ public class HistoricoController : ControllerBase
|
|||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
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 =>
|
||||
EF.Functions.ILike(x.UserName ?? "", $"%{s}%") ||
|
||||
EF.Functions.ILike(x.UserEmail ?? "", $"%{s}%") ||
|
||||
EF.Functions.ILike(x.Action ?? "", $"%{s}%") ||
|
||||
EF.Functions.ILike(x.EntityName ?? "", $"%{s}%") ||
|
||||
EF.Functions.ILike(x.EntityLabel ?? "", $"%{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)
|
||||
|
|
@ -158,4 +171,22 @@ public class HistoricoController : ControllerBase
|
|||
|
||||
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
|
|
@ -4,6 +4,7 @@ using line_gestao_api.Models;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
|
||||
namespace line_gestao_api.Controllers
|
||||
{
|
||||
|
|
@ -54,6 +55,8 @@ namespace line_gestao_api.Controllers
|
|||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
var s = search.Trim();
|
||||
var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc);
|
||||
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
|
||||
q = q.Where(x =>
|
||||
EF.Functions.ILike((x.LinhaAntiga ?? ""), $"%{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.Usuario ?? ""), $"%{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();
|
||||
|
|
@ -384,6 +396,24 @@ namespace line_gestao_api.Controllers
|
|||
(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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(s)) return "";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -21,9 +21,10 @@ namespace line_gestao_api.Controllers
|
|||
[HttpGet("dashboard")]
|
||||
public async Task<ActionResult<RelatoriosDashboardDto>> GetDashboard()
|
||||
{
|
||||
var today = DateTime.UtcNow.Date;
|
||||
var last30 = today.AddDays(-30);
|
||||
var limit30 = today.AddDays(30);
|
||||
var todayUtcStart = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc);
|
||||
var tomorrowUtcStart = todayUtcStart.AddDays(1);
|
||||
var last30UtcStart = todayUtcStart.AddDays(-30);
|
||||
var limit30ExclusiveUtcStart = todayUtcStart.AddDays(31);
|
||||
|
||||
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 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
|
||||
.OrderByDescending(x => x.DataDaMureg ?? minUtc)
|
||||
|
|
@ -105,7 +108,7 @@ namespace line_gestao_api.Controllers
|
|||
})
|
||||
.ToListAsync();
|
||||
|
||||
var serieMureg12 = await BuildSerieUltimos12Meses_Mureg(today);
|
||||
var serieMureg12 = await BuildSerieUltimos12Meses_Mureg(todayUtcStart);
|
||||
|
||||
// =========================
|
||||
// TROCA DE NÚMERO
|
||||
|
|
@ -115,7 +118,9 @@ namespace line_gestao_api.Controllers
|
|||
var totalTrocas = await qTroca.CountAsync();
|
||||
|
||||
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
|
||||
.OrderByDescending(x => x.DataTroca ?? minUtc)
|
||||
|
|
@ -133,7 +138,7 @@ namespace line_gestao_api.Controllers
|
|||
})
|
||||
.ToListAsync();
|
||||
|
||||
var serieTroca12 = await BuildSerieUltimos12Meses_Troca(today);
|
||||
var serieTroca12 = await BuildSerieUltimos12Meses_Troca(todayUtcStart);
|
||||
|
||||
// =========================
|
||||
// VIGÊNCIA
|
||||
|
|
@ -143,18 +148,18 @@ namespace line_gestao_api.Controllers
|
|||
var totalVig = await qVig.CountAsync();
|
||||
|
||||
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 =>
|
||||
x.DtTerminoFidelizacao != null &&
|
||||
x.DtTerminoFidelizacao.Value.Date >= today &&
|
||||
x.DtTerminoFidelizacao.Value.Date <= limit30);
|
||||
x.DtTerminoFidelizacao.Value >= todayUtcStart &&
|
||||
x.DtTerminoFidelizacao.Value < limit30ExclusiveUtcStart);
|
||||
|
||||
// ✅ 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
|
||||
var vigBuckets = await BuildVigenciaBuckets(today);
|
||||
var vigBuckets = await BuildVigenciaBuckets(todayUtcStart);
|
||||
|
||||
// =========================
|
||||
// USER DATA
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ public class ResumoController : ControllerBase
|
|||
|
||||
var reservaTotalEntity = await _db.ResumoReservaTotals.AsNoTracking()
|
||||
.FirstOrDefaultAsync();
|
||||
var gbDistribuicaoTotalEntity = await _db.ResumoGbDistribuicaoTotais.AsNoTracking()
|
||||
.FirstOrDefaultAsync();
|
||||
var canonicalTotalLinhas = await _spreadsheetImportAuditService.GetCanonicalTotalLinhasForReadAsync();
|
||||
|
||||
var response = new ResumoResponseDto
|
||||
|
|
@ -135,6 +137,20 @@ public class ResumoController : ControllerBase
|
|||
QtdLinhas = x.QtdLinhas
|
||||
})
|
||||
.ToListAsync(),
|
||||
GbDistribuicao = await _db.ResumoGbDistribuicoes.AsNoTracking()
|
||||
.OrderBy(x => x.Gb)
|
||||
.Select(x => new ResumoGbDistribuicaoDto
|
||||
{
|
||||
Gb = x.Gb,
|
||||
Qtd = x.Qtd,
|
||||
Soma = x.Soma
|
||||
})
|
||||
.ToListAsync(),
|
||||
GbDistribuicaoTotal = gbDistribuicaoTotalEntity == null ? null : new ResumoGbDistribuicaoTotalDto
|
||||
{
|
||||
TotalLinhas = gbDistribuicaoTotalEntity.TotalLinhas,
|
||||
SomaTotal = gbDistribuicaoTotalEntity.SomaTotal
|
||||
},
|
||||
ReservaLines = reservaLines
|
||||
.Select(x => new ResumoReservaLineDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -40,12 +40,19 @@ namespace line_gestao_api.Controllers
|
|||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
var s = search.Trim();
|
||||
var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc);
|
||||
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
|
||||
q = q.Where(x =>
|
||||
EF.Functions.ILike(x.LinhaAntiga ?? "", $"%{s}%") ||
|
||||
EF.Functions.ILike(x.LinhaNova ?? "", $"%{s}%") ||
|
||||
EF.Functions.ILike(x.ICCID ?? "", $"%{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();
|
||||
|
|
@ -201,5 +208,23 @@ namespace line_gestao_api.Controllers
|
|||
foreach (var c in s) if (char.IsDigit(c)) sb.Append(c);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using line_gestao_api.Models;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace line_gestao_api.Controllers
|
||||
|
|
@ -51,15 +52,26 @@ namespace line_gestao_api.Controllers
|
|||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
var s = search.Trim();
|
||||
var hasDateSearch = TryParseSearchDateUtcStart(s, out var searchDateStartUtc);
|
||||
var searchDateEndUtc = hasDateSearch ? searchDateStartUtc.AddDays(1) : DateTime.MinValue;
|
||||
q = q.Where(x =>
|
||||
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
|
||||
EF.Functions.ILike(x.Cliente ?? "", $"%{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.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();
|
||||
|
|
@ -134,7 +146,26 @@ namespace line_gestao_api.Controllers
|
|||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
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)
|
||||
|
|
@ -407,5 +438,23 @@ namespace line_gestao_api.Controllers
|
|||
if (string.IsNullOrWhiteSpace(s)) return "";
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using line_gestao_api.Services;
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace line_gestao_api.Controllers
|
||||
|
|
@ -45,12 +46,24 @@ namespace line_gestao_api.Controllers
|
|||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
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 =>
|
||||
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.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();
|
||||
|
|
@ -108,8 +121,8 @@ namespace line_gestao_api.Controllers
|
|||
page = page < 1 ? 1 : page;
|
||||
pageSize = pageSize < 1 ? 20 : pageSize;
|
||||
|
||||
var today = DateTime.UtcNow.Date; // UTC para evitar erro no PostgreSQL
|
||||
var limit30 = today.AddDays(30);
|
||||
var todayUtcStart = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc);
|
||||
var limit30ExclusiveUtcStart = todayUtcStart.AddDays(31);
|
||||
|
||||
// Query Base (Linhas)
|
||||
var q = _db.VigenciaLines.AsNoTracking()
|
||||
|
|
@ -118,7 +131,25 @@ namespace line_gestao_api.Controllers
|
|||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
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)
|
||||
|
|
@ -128,7 +159,7 @@ namespace line_gestao_api.Controllers
|
|||
TotalLinhas = await q.CountAsync(),
|
||||
// Clientes distintos
|
||||
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)
|
||||
};
|
||||
|
||||
|
|
@ -140,10 +171,10 @@ namespace line_gestao_api.Controllers
|
|||
Cliente = g.Key,
|
||||
Linhas = g.Count(),
|
||||
Total = g.Sum(x => x.Total ?? 0m),
|
||||
Vencidos = g.Count(x => x.DtTerminoFidelizacao != null && x.DtTerminoFidelizacao.Value.Date < today),
|
||||
AVencer30 = g.Count(x => x.DtTerminoFidelizacao != null && x.DtTerminoFidelizacao.Value.Date >= today && x.DtTerminoFidelizacao.Value.Date <= limit30),
|
||||
ProximoVencimento = g.Where(x => x.DtTerminoFidelizacao >= today).Min(x => x.DtTerminoFidelizacao),
|
||||
UltimoVencimento = g.Where(x => x.DtTerminoFidelizacao < today).Max(x => x.DtTerminoFidelizacao)
|
||||
Vencidos = g.Count(x => x.DtTerminoFidelizacao != null && x.DtTerminoFidelizacao.Value < todayUtcStart),
|
||||
AVencer30 = g.Count(x => x.DtTerminoFidelizacao != null && x.DtTerminoFidelizacao.Value >= todayUtcStart && x.DtTerminoFidelizacao.Value < limit30ExclusiveUtcStart),
|
||||
ProximoVencimento = g.Where(x => x.DtTerminoFidelizacao >= todayUtcStart).Min(x => x.DtTerminoFidelizacao),
|
||||
UltimoVencimento = g.Where(x => x.DtTerminoFidelizacao < todayUtcStart).Max(x => x.DtTerminoFidelizacao)
|
||||
});
|
||||
|
||||
// Contagem para paginação
|
||||
|
|
@ -357,5 +388,33 @@ namespace line_gestao_api.Controllers
|
|||
if (string.IsNullOrWhiteSpace(s)) return "";
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
|||
public DbSet<ResumoPlanoContratoResumo> ResumoPlanoContratoResumos => Set<ResumoPlanoContratoResumo>();
|
||||
public DbSet<ResumoPlanoContratoTotal> ResumoPlanoContratoTotals => Set<ResumoPlanoContratoTotal>();
|
||||
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<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<ResumoPlanoContratoTotal>().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<ResumoReservaTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
modelBuilder.Entity<ParcelamentoLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ namespace line_gestao_api.Dtos
|
|||
public DateTime? DataBloqueio { get; set; }
|
||||
public DateTime? DataEntregaOpera { get; set; }
|
||||
public DateTime? DataEntregaCliente { get; set; }
|
||||
public DateTime? DtEfetivacaoServico { get; set; }
|
||||
public DateTime? DtTerminoFidelizacao { get; set; }
|
||||
|
||||
// ==========================
|
||||
// Responsáveis / Logística
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ namespace line_gestao_api.Dtos
|
|||
public class GeralDashboardVivoKpiDto
|
||||
{
|
||||
public int QtdLinhas { get; set; }
|
||||
public decimal TotalFranquiaGb { get; set; }
|
||||
public decimal TotalBaseMensal { get; set; }
|
||||
public decimal TotalAdicionaisMensal { get; set; }
|
||||
public decimal TotalGeralMensal { get; set; }
|
||||
|
|
@ -56,6 +57,7 @@ namespace line_gestao_api.Dtos
|
|||
public GeralDashboardChartDto LinhasPorFranquia { get; set; } = new();
|
||||
public GeralDashboardChartDto AdicionaisPagosPorServico { get; set; } = new();
|
||||
public GeralDashboardChartDto TravelMundo { get; set; } = new();
|
||||
public GeralDashboardChartDto TipoChip { get; set; } = new();
|
||||
}
|
||||
|
||||
public class GeralDashboardChartDto
|
||||
|
|
|
|||
|
|
@ -14,6 +14,15 @@
|
|||
public string? Skil { get; set; }
|
||||
public string? Modalidade { 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
|
||||
|
|
@ -53,6 +62,8 @@
|
|||
public string? Solicitante { get; set; }
|
||||
public DateTime? DataEntregaOpera { get; set; }
|
||||
public DateTime? DataEntregaCliente { get; set; }
|
||||
public DateTime? DtEfetivacaoServico { get; set; }
|
||||
public DateTime? DtTerminoFidelizacao { get; set; }
|
||||
public string? VencConta { get; set; }
|
||||
public string? TipoDeChip { get; set; }
|
||||
}
|
||||
|
|
@ -94,6 +105,8 @@
|
|||
public string? Solicitante { get; set; }
|
||||
public DateTime? DataEntregaOpera { get; set; }
|
||||
public DateTime? DataEntregaCliente { get; set; }
|
||||
public DateTime? DtEfetivacaoServico { get; set; }
|
||||
public DateTime? DtTerminoFidelizacao { get; set; }
|
||||
public string? VencConta { get; set; }
|
||||
public string? TipoDeChip { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@ public sealed class ResumoResponseDto
|
|||
public List<ResumoPlanoContratoResumoDto> PlanoContratoResumos { get; set; } = new();
|
||||
public ResumoPlanoContratoTotalDto? PlanoContratoTotal { get; set; }
|
||||
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<ResumoReservaPorDddDto> ReservaPorDdd { get; set; } = new();
|
||||
public int? TotalGeralLinhasReserva { get; set; }
|
||||
|
|
@ -85,6 +87,19 @@ public sealed class ResumoLineTotaisDto
|
|||
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 string? Ddd { get; set; }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -990,6 +990,61 @@ namespace line_gestao_api.Migrations
|
|||
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 =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using line_gestao_api.Data;
|
||||
using line_gestao_api.Dtos;
|
||||
|
|
@ -14,6 +15,7 @@ namespace line_gestao_api.Services
|
|||
private const string ServiceSkeelo = "SKEELO";
|
||||
private const string ServiceVivoNewsPlus = "VIVO NEWS PLUS";
|
||||
private const string ServiceVivoTravelMundo = "VIVO TRAVEL MUNDO";
|
||||
private const string ServiceVivoSync = "VIVO SYNC";
|
||||
private const string ServiceVivoGestaoDispositivo = "VIVO GESTÃO DISPOSITIVO";
|
||||
|
||||
private readonly AppDbContext _db;
|
||||
|
|
@ -45,6 +47,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null),
|
||||
VivoBaseTotal = g.Sum(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
|
|
@ -54,9 +57,22 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
? (x.ValorPlanoVivo ?? 0m)
|
||||
: 0m),
|
||||
VivoFranquiaTotalGb = g.Sum(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
x.FranquiaVivo != null ||
|
||||
x.ValorContratoVivo != null ||
|
||||
x.GestaoVozDados != null ||
|
||||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
? (x.FranquiaVivo ?? 0m)
|
||||
: 0m),
|
||||
VivoAdicionaisTotal = g.Sum(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
x.FranquiaVivo != null ||
|
||||
|
|
@ -65,9 +81,10 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
? (x.GestaoVozDados ?? 0m) + (x.Skeelo ?? 0m) + (x.VivoNewsPlus ?? 0m) +
|
||||
(x.VivoTravelMundo ?? 0m) + (x.VivoGestaoDispositivo ?? 0m)
|
||||
(x.VivoTravelMundo ?? 0m) + (x.VivoSync ?? 0m) + (x.VivoGestaoDispositivo ?? 0m)
|
||||
: 0m),
|
||||
VivoMinBase = g.Where(x =>
|
||||
x.ValorPlanoVivo != null ||
|
||||
|
|
@ -77,6 +94,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
.Select(x => x.ValorPlanoVivo ?? 0m)
|
||||
.DefaultIfEmpty()
|
||||
|
|
@ -89,41 +107,14 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
.Select(x => x.ValorPlanoVivo ?? 0m)
|
||||
.DefaultIfEmpty()
|
||||
.Max(),
|
||||
TravelCom = g.Count(x =>
|
||||
(x.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),
|
||||
TravelCom = g.Count(x => (x.VivoTravelMundo ?? 0m) > 0m),
|
||||
TravelSem = g.Count(x => (x.VivoTravelMundo ?? 0m) <= 0m),
|
||||
TravelTotal = g.Sum(x => x.VivoTravelMundo ?? 0m),
|
||||
PaidGestaoVozDados = g.Count(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
x.FranquiaVivo != null ||
|
||||
|
|
@ -132,6 +123,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.GestaoVozDados ?? 0m) > 0m),
|
||||
PaidSkeelo = g.Count(x =>
|
||||
|
|
@ -142,6 +134,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.Skeelo ?? 0m) > 0m),
|
||||
PaidNews = g.Count(x =>
|
||||
|
|
@ -152,6 +145,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.VivoNewsPlus ?? 0m) > 0m),
|
||||
PaidTravel = g.Count(x =>
|
||||
|
|
@ -162,6 +156,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.VivoTravelMundo ?? 0m) > 0m),
|
||||
PaidGestaoDispositivo = g.Count(x =>
|
||||
|
|
@ -172,8 +167,20 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.VivoGestaoDispositivo ?? 0m) > 0m),
|
||||
PaidSync = g.Count(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
x.FranquiaVivo != null ||
|
||||
x.ValorContratoVivo != null ||
|
||||
x.GestaoVozDados != null ||
|
||||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.VivoSync ?? 0m) > 0m),
|
||||
NotPaidGestaoVozDados = g.Count(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
x.FranquiaVivo != null ||
|
||||
|
|
@ -182,6 +189,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.GestaoVozDados ?? 0m) <= 0m),
|
||||
NotPaidSkeelo = g.Count(x =>
|
||||
|
|
@ -192,6 +200,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.Skeelo ?? 0m) <= 0m),
|
||||
NotPaidNews = g.Count(x =>
|
||||
|
|
@ -202,6 +211,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.VivoNewsPlus ?? 0m) <= 0m),
|
||||
NotPaidTravel = g.Count(x =>
|
||||
|
|
@ -212,6 +222,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.VivoTravelMundo ?? 0m) <= 0m),
|
||||
NotPaidGestaoDispositivo = g.Count(x =>
|
||||
|
|
@ -222,8 +233,20 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.VivoGestaoDispositivo ?? 0m) <= 0m),
|
||||
NotPaidSync = g.Count(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
x.FranquiaVivo != null ||
|
||||
x.ValorContratoVivo != null ||
|
||||
x.GestaoVozDados != null ||
|
||||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.VivoSync ?? 0m) <= 0m),
|
||||
TotalLinesWithAnyPaidAdditional = g.Count(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
x.FranquiaVivo != null ||
|
||||
|
|
@ -232,11 +255,13 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
((x.GestaoVozDados ?? 0m) > 0m ||
|
||||
(x.Skeelo ?? 0m) > 0m ||
|
||||
(x.VivoNewsPlus ?? 0m) > 0m ||
|
||||
(x.VivoTravelMundo ?? 0m) > 0m ||
|
||||
(x.VivoSync ?? 0m) > 0m ||
|
||||
(x.VivoGestaoDispositivo ?? 0m) > 0m)),
|
||||
TotalLinesWithNoPaidAdditional = g.Count(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
|
|
@ -246,11 +271,13 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.GestaoVozDados ?? 0m) <= 0m &&
|
||||
(x.Skeelo ?? 0m) <= 0m &&
|
||||
(x.VivoNewsPlus ?? 0m) <= 0m &&
|
||||
(x.VivoTravelMundo ?? 0m) <= 0m &&
|
||||
(x.VivoSync ?? 0m) <= 0m &&
|
||||
(x.VivoGestaoDispositivo ?? 0m) <= 0m),
|
||||
TotalGestaoVozDados = g.Sum(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
|
|
@ -260,6 +287,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
? (x.GestaoVozDados ?? 0m)
|
||||
: 0m),
|
||||
|
|
@ -271,6 +299,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
? (x.Skeelo ?? 0m)
|
||||
: 0m),
|
||||
|
|
@ -282,6 +311,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
? (x.VivoNewsPlus ?? 0m)
|
||||
: 0m),
|
||||
|
|
@ -293,6 +323,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
? (x.VivoTravelMundo ?? 0m)
|
||||
: 0m),
|
||||
|
|
@ -304,8 +335,21 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
? (x.VivoGestaoDispositivo ?? 0m)
|
||||
: 0m),
|
||||
TotalSync = g.Sum(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
x.FranquiaVivo != null ||
|
||||
x.ValorContratoVivo != null ||
|
||||
x.GestaoVozDados != null ||
|
||||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
? (x.VivoSync ?? 0m)
|
||||
: 0m)
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
|
|
@ -315,6 +359,8 @@ namespace line_gestao_api.Services
|
|||
.ToListAsync();
|
||||
|
||||
var linhasPorFranquia = BuildFranquiaBuckets(franquiasRaw);
|
||||
var tipoChip = await qLines.Select(x => x.TipoDeChip).ToListAsync();
|
||||
var tipoChipBuckets = BuildTipoChipBuckets(tipoChip);
|
||||
|
||||
var clientGroupsRaw = await qLines
|
||||
.Where(x => x.Cliente != null && x.Cliente != "")
|
||||
|
|
@ -336,27 +382,10 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != 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),
|
||||
TravelCom = g.Count(x => (x.VivoTravelMundo ?? 0m) > 0m),
|
||||
TravelSem = g.Count(x => (x.VivoTravelMundo ?? 0m) <= 0m),
|
||||
PaidGestaoVozDados = g.Count(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
x.FranquiaVivo != null ||
|
||||
|
|
@ -365,6 +394,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.GestaoVozDados ?? 0m) > 0m),
|
||||
PaidSkeelo = g.Count(x =>
|
||||
|
|
@ -375,6 +405,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.Skeelo ?? 0m) > 0m),
|
||||
PaidNews = g.Count(x =>
|
||||
|
|
@ -385,6 +416,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.VivoNewsPlus ?? 0m) > 0m),
|
||||
PaidTravel = g.Count(x =>
|
||||
|
|
@ -395,6 +427,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.VivoTravelMundo ?? 0m) > 0m),
|
||||
PaidGestaoDispositivo = g.Count(x =>
|
||||
|
|
@ -405,8 +438,20 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.VivoGestaoDispositivo ?? 0m) > 0m),
|
||||
PaidSync = g.Count(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
x.FranquiaVivo != null ||
|
||||
x.ValorContratoVivo != null ||
|
||||
x.GestaoVozDados != null ||
|
||||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.VivoSync ?? 0m) > 0m),
|
||||
NotPaidGestaoVozDados = g.Count(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
x.FranquiaVivo != null ||
|
||||
|
|
@ -415,6 +460,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.GestaoVozDados ?? 0m) <= 0m),
|
||||
NotPaidSkeelo = g.Count(x =>
|
||||
|
|
@ -425,6 +471,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.Skeelo ?? 0m) <= 0m),
|
||||
NotPaidNews = g.Count(x =>
|
||||
|
|
@ -435,6 +482,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.VivoNewsPlus ?? 0m) <= 0m),
|
||||
NotPaidTravel = g.Count(x =>
|
||||
|
|
@ -445,6 +493,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.VivoTravelMundo ?? 0m) <= 0m),
|
||||
NotPaidGestaoDispositivo = g.Count(x =>
|
||||
|
|
@ -455,8 +504,20 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.VivoGestaoDispositivo ?? 0m) <= 0m),
|
||||
NotPaidSync = g.Count(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
x.FranquiaVivo != null ||
|
||||
x.ValorContratoVivo != null ||
|
||||
x.GestaoVozDados != null ||
|
||||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null) &&
|
||||
(x.VivoSync ?? 0m) <= 0m),
|
||||
TotalGestaoVozDados = g.Sum(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
x.FranquiaVivo != null ||
|
||||
|
|
@ -465,6 +526,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
? (x.GestaoVozDados ?? 0m)
|
||||
: 0m),
|
||||
|
|
@ -476,6 +538,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
? (x.Skeelo ?? 0m)
|
||||
: 0m),
|
||||
|
|
@ -487,6 +550,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
? (x.VivoNewsPlus ?? 0m)
|
||||
: 0m),
|
||||
|
|
@ -498,6 +562,7 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
? (x.VivoTravelMundo ?? 0m)
|
||||
: 0m),
|
||||
|
|
@ -509,8 +574,21 @@ namespace line_gestao_api.Services
|
|||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
? (x.VivoGestaoDispositivo ?? 0m)
|
||||
: 0m),
|
||||
TotalSync = g.Sum(x =>
|
||||
(x.ValorPlanoVivo != null ||
|
||||
x.FranquiaVivo != null ||
|
||||
x.ValorContratoVivo != null ||
|
||||
x.GestaoVozDados != null ||
|
||||
x.Skeelo != null ||
|
||||
x.VivoNewsPlus != null ||
|
||||
x.VivoTravelMundo != null ||
|
||||
x.VivoSync != null ||
|
||||
x.VivoGestaoDispositivo != null)
|
||||
? (x.VivoSync ?? 0m)
|
||||
: 0m)
|
||||
})
|
||||
.OrderBy(x => x.Cliente)
|
||||
|
|
@ -519,48 +597,13 @@ namespace line_gestao_api.Services
|
|||
var dto = new GeralDashboardInsightsDto
|
||||
{
|
||||
Kpis = BuildKpis(totals),
|
||||
Charts = BuildCharts(totals, linhasPorFranquia),
|
||||
Charts = BuildCharts(totals, linhasPorFranquia, tipoChipBuckets),
|
||||
ClientGroups = BuildClientGroups(clientGroupsRaw)
|
||||
};
|
||||
|
||||
dto.Kpis.TotalLinhas = await ResolveCanonicalTotalLinhasAsync(dto.Kpis.TotalLinhas);
|
||||
|
||||
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)
|
||||
{
|
||||
if (totals == null)
|
||||
|
|
@ -583,6 +626,7 @@ namespace line_gestao_api.Services
|
|||
Vivo = new GeralDashboardVivoKpiDto
|
||||
{
|
||||
QtdLinhas = totals.VivoLinhas,
|
||||
TotalFranquiaGb = totals.VivoFranquiaTotalGb,
|
||||
TotalBaseMensal = totals.VivoBaseTotal,
|
||||
TotalAdicionaisMensal = totals.VivoAdicionaisTotal,
|
||||
TotalGeralMensal = totalGeralMensal,
|
||||
|
|
@ -606,6 +650,7 @@ namespace line_gestao_api.Services
|
|||
new() { ServiceName = ServiceSkeelo, CountLines = totals.PaidSkeelo, TotalValue = totals.TotalSkeelo },
|
||||
new() { ServiceName = ServiceVivoNewsPlus, CountLines = totals.PaidNews, TotalValue = totals.TotalNews },
|
||||
new() { ServiceName = ServiceVivoTravelMundo, CountLines = totals.PaidTravel, TotalValue = totals.TotalTravel },
|
||||
new() { ServiceName = ServiceVivoSync, CountLines = totals.PaidSync, TotalValue = totals.TotalSync },
|
||||
new() { ServiceName = ServiceVivoGestaoDispositivo, CountLines = totals.PaidGestaoDispositivo, TotalValue = totals.TotalGestaoDispositivo }
|
||||
},
|
||||
ServicesNotPaid = new List<GeralDashboardServiceKpiDto>
|
||||
|
|
@ -614,13 +659,14 @@ namespace line_gestao_api.Services
|
|||
new() { ServiceName = ServiceSkeelo, CountLines = totals.NotPaidSkeelo, TotalValue = 0m },
|
||||
new() { ServiceName = ServiceVivoNewsPlus, CountLines = totals.NotPaidNews, TotalValue = 0m },
|
||||
new() { ServiceName = ServiceVivoTravelMundo, CountLines = totals.NotPaidTravel, TotalValue = 0m },
|
||||
new() { ServiceName = ServiceVivoSync, CountLines = totals.NotPaidSync, TotalValue = 0m },
|
||||
new() { ServiceName = ServiceVivoGestaoDispositivo, CountLines = totals.NotPaidGestaoDispositivo, TotalValue = 0m }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static GeralDashboardChartsDto BuildCharts(TotalsProjection? totals, FranquiaBuckets franquias)
|
||||
private static GeralDashboardChartsDto BuildCharts(TotalsProjection? totals, FranquiaBuckets franquias, TipoChipBuckets tipoChip)
|
||||
{
|
||||
var adicionaisLabels = new List<string>
|
||||
{
|
||||
|
|
@ -628,28 +674,31 @@ namespace line_gestao_api.Services
|
|||
ServiceSkeelo,
|
||||
ServiceVivoNewsPlus,
|
||||
ServiceVivoTravelMundo,
|
||||
ServiceVivoSync,
|
||||
ServiceVivoGestaoDispositivo
|
||||
};
|
||||
|
||||
var adicionaisValues = totals == null
|
||||
? new List<int> { 0, 0, 0, 0, 0 }
|
||||
? new List<int> { 0, 0, 0, 0, 0, 0 }
|
||||
: new List<int>
|
||||
{
|
||||
totals.PaidGestaoVozDados,
|
||||
totals.PaidSkeelo,
|
||||
totals.PaidNews,
|
||||
totals.PaidTravel,
|
||||
totals.PaidSync,
|
||||
totals.PaidGestaoDispositivo
|
||||
};
|
||||
|
||||
var adicionaisTotals = totals == null
|
||||
? new List<decimal> { 0m, 0m, 0m, 0m, 0m }
|
||||
? new List<decimal> { 0m, 0m, 0m, 0m, 0m, 0m }
|
||||
: new List<decimal>
|
||||
{
|
||||
totals.TotalGestaoVozDados,
|
||||
totals.TotalSkeelo,
|
||||
totals.TotalNews,
|
||||
totals.TotalTravel,
|
||||
totals.TotalSync,
|
||||
totals.TotalGestaoDispositivo
|
||||
};
|
||||
|
||||
|
|
@ -672,6 +721,11 @@ namespace line_gestao_api.Services
|
|||
Values = totals == null
|
||||
? new List<int> { 0, 0 }
|
||||
: 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 = ServiceVivoTravelMundo, Value = row.PaidTravel.ToString(PtBr) },
|
||||
new() { Label = $"R$ {ServiceVivoTravelMundo}", Value = FormatCurrency(row.TotalTravel) },
|
||||
new() { Label = ServiceVivoSync, Value = row.PaidSync.ToString(PtBr) },
|
||||
new() { Label = $"R$ {ServiceVivoSync}", Value = FormatCurrency(row.TotalSync) },
|
||||
new() { Label = ServiceVivoGestaoDispositivo, Value = row.PaidGestaoDispositivo.ToString(PtBr) },
|
||||
new() { Label = $"R$ {ServiceVivoGestaoDispositivo}", Value = FormatCurrency(row.TotalGestaoDispositivo) }
|
||||
};
|
||||
|
|
@ -737,6 +793,13 @@ namespace line_gestao_api.Services
|
|||
TotalValue = row.TotalTravel
|
||||
},
|
||||
new()
|
||||
{
|
||||
ServiceName = ServiceVivoSync,
|
||||
PaidCount = row.PaidSync,
|
||||
NotPaidCount = row.NotPaidSync,
|
||||
TotalValue = row.TotalSync
|
||||
},
|
||||
new()
|
||||
{
|
||||
ServiceName = ServiceVivoGestaoDispositivo,
|
||||
PaidCount = row.PaidGestaoDispositivo,
|
||||
|
|
@ -761,6 +824,79 @@ namespace line_gestao_api.Services
|
|||
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)
|
||||
{
|
||||
var map = new Dictionary<string, FranquiaBucket>();
|
||||
|
|
@ -885,6 +1021,12 @@ namespace line_gestao_api.Services
|
|||
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
|
||||
{
|
||||
public decimal? FranquiaVivo { get; set; }
|
||||
|
|
@ -897,6 +1039,7 @@ namespace line_gestao_api.Services
|
|||
public int TotalAtivas { get; set; }
|
||||
public int TotalBloqueados { get; set; }
|
||||
public int VivoLinhas { get; set; }
|
||||
public decimal VivoFranquiaTotalGb { get; set; }
|
||||
public decimal VivoBaseTotal { get; set; }
|
||||
public decimal VivoAdicionaisTotal { get; set; }
|
||||
public decimal VivoMinBase { get; set; }
|
||||
|
|
@ -909,11 +1052,13 @@ namespace line_gestao_api.Services
|
|||
public int PaidNews { get; set; }
|
||||
public int PaidTravel { get; set; }
|
||||
public int PaidGestaoDispositivo { get; set; }
|
||||
public int PaidSync { get; set; }
|
||||
public int NotPaidGestaoVozDados { get; set; }
|
||||
public int NotPaidSkeelo { get; set; }
|
||||
public int NotPaidNews { get; set; }
|
||||
public int NotPaidTravel { get; set; }
|
||||
public int NotPaidGestaoDispositivo { get; set; }
|
||||
public int NotPaidSync { get; set; }
|
||||
public int TotalLinesWithAnyPaidAdditional { get; set; }
|
||||
public int TotalLinesWithNoPaidAdditional { get; set; }
|
||||
public decimal TotalGestaoVozDados { get; set; }
|
||||
|
|
@ -921,6 +1066,7 @@ namespace line_gestao_api.Services
|
|||
public decimal TotalNews { get; set; }
|
||||
public decimal TotalTravel { get; set; }
|
||||
public decimal TotalGestaoDispositivo { get; set; }
|
||||
public decimal TotalSync { get; set; }
|
||||
}
|
||||
|
||||
private sealed class ClientGroupProjection
|
||||
|
|
@ -937,16 +1083,19 @@ namespace line_gestao_api.Services
|
|||
public int PaidNews { get; set; }
|
||||
public int PaidTravel { get; set; }
|
||||
public int PaidGestaoDispositivo { get; set; }
|
||||
public int PaidSync { get; set; }
|
||||
public int NotPaidGestaoVozDados { get; set; }
|
||||
public int NotPaidSkeelo { get; set; }
|
||||
public int NotPaidNews { get; set; }
|
||||
public int NotPaidTravel { get; set; }
|
||||
public int NotPaidGestaoDispositivo { get; set; }
|
||||
public int NotPaidSync { get; set; }
|
||||
public decimal TotalGestaoVozDados { get; set; }
|
||||
public decimal TotalSkeelo { get; set; }
|
||||
public decimal TotalNews { get; set; }
|
||||
public decimal TotalTravel { get; set; }
|
||||
public decimal TotalGestaoDispositivo { get; set; }
|
||||
public decimal TotalSync { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ public class VigenciaNotificationBackgroundService : BackgroundService
|
|||
|
||||
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
|
||||
.Distinct()
|
||||
.Where(d => d > 0)
|
||||
|
|
@ -132,7 +132,7 @@ public class VigenciaNotificationBackgroundService : BackgroundService
|
|||
continue;
|
||||
}
|
||||
|
||||
var endDate = vigencia.DtTerminoFidelizacao.Value.Date;
|
||||
var endDate = DateTime.SpecifyKind(vigencia.DtTerminoFidelizacao.Value.Date, DateTimeKind.Utc);
|
||||
var usuario = vigencia.Usuario?.Trim();
|
||||
var cliente = vigencia.Cliente?.Trim();
|
||||
var linha = vigencia.Linha?.Trim();
|
||||
|
|
@ -198,15 +198,33 @@ public class VigenciaNotificationBackgroundService : BackgroundService
|
|||
var candidateTipos = candidates.Select(c => c.Tipo).Distinct().ToList();
|
||||
var candidateDates = candidates
|
||||
.Where(c => c.ReferenciaData.HasValue)
|
||||
.Select(c => c.ReferenciaData!.Value.Date)
|
||||
.Select(c => DateTime.SpecifyKind(c.ReferenciaData!.Value.Date, DateTimeKind.Utc))
|
||||
.Distinct()
|
||||
.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 => 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);
|
||||
|
||||
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 =>
|
||||
BuildDedupKey(
|
||||
n.Tipo,
|
||||
|
|
@ -260,7 +278,7 @@ public class VigenciaNotificationBackgroundService : BackgroundService
|
|||
continue;
|
||||
}
|
||||
|
||||
var endDate = vigencia.DtTerminoFidelizacao.Value.Date;
|
||||
var endDate = DateTime.SpecifyKind(vigencia.DtTerminoFidelizacao.Value.Date, DateTimeKind.Utc);
|
||||
if (endDate < today)
|
||||
{
|
||||
if (notification.Tipo != "Vencido")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"Key": "vI8/oEYEWN5sBDTisNuZFjZAl+YFvXEJ96POb73/eoq3NaFPkOFXyPRdf/HWGAFnUsF3e3QpYL6Wl4Bc2v+l3g==",
|
||||
"Issuer": "LineGestao",
|
||||
"Audience": "LineGestao",
|
||||
"ExpiresMinutes": 180
|
||||
"ExpiresMinutes": 360
|
||||
},
|
||||
"Notifications": {
|
||||
"CheckIntervalMinutes": 60,
|
||||
|
|
|
|||
|
|
@ -89,6 +89,49 @@ namespace line_gestao_api.Tests
|
|||
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)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
|
|
|
|||
Loading…
Reference in New Issue