using line_gestao_api.Data; using line_gestao_api.Dtos; 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 { [ApiController] [Route("api/user-data")] [Authorize] public class UserDataController : ControllerBase { private readonly AppDbContext _db; public UserDataController(AppDbContext db) => _db = db; // ========================================================== // GET /api/user-data (LINHAS - Tabela Interna) // ========================================================== [HttpGet] public async Task>> GetAll( [FromQuery] string? search, [FromQuery] string? client, // Filtro por cliente [FromQuery] string? tipo, // PF/PJ [FromQuery] int page = 1, [FromQuery] int pageSize = 20, [FromQuery] string? sortBy = "item", [FromQuery] string? sortDir = "asc") { page = page < 1 ? 1 : page; pageSize = pageSize < 1 ? 20 : pageSize; var q = _db.UserDatas.AsNoTracking(); // Filtro exato por cliente (quando abre o card) if (!string.IsNullOrWhiteSpace(client)) { var c = client.Trim(); q = q.Where(x => x.Cliente == c); } if (!string.IsNullOrWhiteSpace(tipo)) { var t = tipo.Trim().ToUpperInvariant(); if (t == "PF" || t == "PJ") q = q.Where(x => x.TipoPessoa == t); } // Busca global 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.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 sb = (sortBy ?? "item").Trim().ToLowerInvariant(); var desc = string.Equals((sortDir ?? "asc").Trim(), "desc", StringComparison.OrdinalIgnoreCase); q = sb switch { "item" => desc ? q.OrderByDescending(x => x.Item) : q.OrderBy(x => x.Item), "linha" => desc ? q.OrderByDescending(x => x.Linha) : q.OrderBy(x => x.Linha), "cliente" => desc ? q.OrderByDescending(x => x.Cliente) : q.OrderBy(x => x.Cliente), _ => desc ? q.OrderByDescending(x => x.Item) : q.OrderBy(x => x.Item), }; var items = await q .Skip((page - 1) * pageSize) .Take(pageSize) .Select(x => new UserDataListDto { Id = x.Id, Item = x.Item, Linha = x.Linha, Cliente = x.Cliente, TipoPessoa = x.TipoPessoa, Nome = x.Nome, RazaoSocial = x.RazaoSocial, Cnpj = x.Cnpj, Cpf = x.Cpf, Rg = x.Rg, DataNascimento = x.DataNascimento != null ? x.DataNascimento.Value.ToString("yyyy-MM-dd") : null, Email = x.Email, Endereco = x.Endereco, Celular = x.Celular, TelefoneFixo = x.TelefoneFixo }) .ToListAsync(); return Ok(new PagedResult { Page = page, PageSize = pageSize, Total = total, Items = items }); } // ========================================================== // GET /api/user-data/groups (CARDS + KPIs GERAIS) // ========================================================== [HttpGet("groups")] public async Task> GetGroups( [FromQuery] string? search, [FromQuery] string? tipo, [FromQuery] int page = 1, [FromQuery] int pageSize = 10, [FromQuery] string? sortBy = "cliente", [FromQuery] string? sortDir = "asc") { page = page < 1 ? 1 : page; pageSize = pageSize < 1 ? 10 : pageSize; var q = _db.UserDatas.AsNoTracking() .Where(x => x.Cliente != null && x.Cliente != ""); if (!string.IsNullOrWhiteSpace(tipo)) { var t = tipo.Trim().ToUpperInvariant(); if (t == "PF" || t == "PJ") q = q.Where(x => x.TipoPessoa == t); } 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.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) var kpis = new UserDataKpisDto { TotalRegistros = await q.CountAsync(), ClientesUnicos = await q.Select(x => x.Cliente).Distinct().CountAsync(), ComCpf = await q.CountAsync(x => x.Cpf != null && x.Cpf != ""), ComCnpj = await q.CountAsync(x => x.Cnpj != null && x.Cnpj != ""), ComEmail = await q.CountAsync(x => x.Email != null && x.Email != "") }; // ✅ 2. AGRUPAMENTO (Para os Cards) var grouped = q .GroupBy(x => x.Cliente!) .Select(g => new UserDataClientGroupDto { Cliente = g.Key, TotalRegistros = g.Count(), ComCpf = g.Count(x => x.Cpf != null && x.Cpf != ""), ComCnpj = g.Count(x => x.Cnpj != null && x.Cnpj != ""), ComEmail = g.Count(x => x.Email != null && x.Email != "") }); var totalGroups = await grouped.CountAsync(); // Ordenação var desc = string.Equals((sortDir ?? "asc").Trim(), "desc", StringComparison.OrdinalIgnoreCase); grouped = desc ? grouped.OrderByDescending(x => x.Cliente) : grouped.OrderBy(x => x.Cliente); // Paginação dos Grupos var items = await grouped .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return Ok(new UserDataGroupResponse { Data = new PagedResult { Page = page, PageSize = pageSize, Total = totalGroups, // Total de Clientes Items = items }, Kpis = kpis // KPIs Totais }); } [HttpGet("clients")] public async Task>> GetClients([FromQuery] string? tipo) { var q = _db.UserDatas.AsNoTracking(); if (!string.IsNullOrWhiteSpace(tipo)) { var t = tipo.Trim().ToUpperInvariant(); if (t == "PF" || t == "PJ") q = q.Where(x => x.TipoPessoa == t); } return await q .Where(x => !string.IsNullOrEmpty(x.Cliente)) .Select(x => x.Cliente!) .Distinct() .OrderBy(x => x) .ToListAsync(); } [HttpGet("{id:guid}")] public async Task> GetById(Guid id) { var x = await _db.UserDatas.AsNoTracking().FirstOrDefaultAsync(a => a.Id == id); if (x == null) return NotFound(); return Ok(new UserDataDetailDto { Id = x.Id, Item = x.Item, Linha = x.Linha, Cliente = x.Cliente, TipoPessoa = x.TipoPessoa, Nome = x.Nome, RazaoSocial = x.RazaoSocial, Cnpj = x.Cnpj, Cpf = x.Cpf, Rg = x.Rg, Email = x.Email, Celular = x.Celular, Endereco = x.Endereco, TelefoneFixo = x.TelefoneFixo, DataNascimento = x.DataNascimento }); } [HttpPost] [Authorize(Roles = "admin")] public async Task> Create([FromBody] CreateUserDataRequest req) { var now = DateTime.UtcNow; var linha = TrimOrNull(req.Linha); var cliente = TrimOrNull(req.Cliente); var nome = TrimOrNull(req.Nome); var razao = TrimOrNull(req.RazaoSocial); var cpf = NullIfEmptyDigits(req.Cpf); var cnpj = NullIfEmptyDigits(req.Cnpj); var tipo = NormalizeTipo(req.TipoPessoa, cpf, cnpj, razao, nome); if (string.IsNullOrWhiteSpace(cliente) && !string.IsNullOrWhiteSpace(linha)) { var linhaDigits = OnlyDigits(linha); MobileLine? mobile = null; if (!string.IsNullOrWhiteSpace(linhaDigits)) { mobile = await _db.MobileLines.AsNoTracking() .FirstOrDefaultAsync(x => x.Linha == linhaDigits); } else { var raw = linha.Trim(); mobile = await _db.MobileLines.AsNoTracking() .FirstOrDefaultAsync(x => EF.Functions.ILike(x.Linha ?? "", raw)); } if (mobile != null) { cliente = mobile.Cliente; if (string.IsNullOrWhiteSpace(tipo)) { var skil = (mobile.Skil ?? "").Trim().ToUpperInvariant(); if (skil.Contains("JUR")) tipo = "PJ"; else if (skil.Contains("FIS")) tipo = "PF"; } } } if (string.IsNullOrWhiteSpace(cliente)) { cliente = !string.IsNullOrWhiteSpace(razao) ? razao : nome; } if (string.IsNullOrWhiteSpace(nome) && tipo == "PF") nome = cliente; if (string.IsNullOrWhiteSpace(razao) && tipo == "PJ") razao = cliente; var item = req.Item; if (!item.HasValue || item.Value <= 0) { var maxItem = await _db.UserDatas.MaxAsync(x => (int?)x.Item) ?? 0; item = maxItem + 1; } var e = new UserData { Id = Guid.NewGuid(), Item = item.Value, Linha = linha, Cliente = cliente, TipoPessoa = tipo ?? "PF", Nome = nome, RazaoSocial = razao, Cnpj = cnpj, Cpf = cpf, Rg = TrimOrNull(req.Rg), Email = TrimOrNull(req.Email), Endereco = TrimOrNull(req.Endereco), Celular = TrimOrNull(req.Celular), TelefoneFixo = TrimOrNull(req.TelefoneFixo), DataNascimento = req.DataNascimento.HasValue ? ToUtc(req.DataNascimento.Value) : null, CreatedAt = now, UpdatedAt = now }; _db.UserDatas.Add(e); await _db.SaveChangesAsync(); return CreatedAtAction(nameof(GetById), new { id = e.Id }, new UserDataDetailDto { Id = e.Id, Item = e.Item, Linha = e.Linha, Cliente = e.Cliente, TipoPessoa = e.TipoPessoa, Nome = e.Nome, RazaoSocial = e.RazaoSocial, Cnpj = e.Cnpj, Cpf = e.Cpf, Rg = e.Rg, Email = e.Email, Celular = e.Celular, Endereco = e.Endereco, TelefoneFixo = e.TelefoneFixo, DataNascimento = e.DataNascimento }); } [HttpPut("{id:guid}")] [Authorize(Roles = "admin")] public async Task Update(Guid id, [FromBody] UpdateUserDataRequest req) { var x = await _db.UserDatas.FirstOrDefaultAsync(a => a.Id == id); if (x == null) return NotFound(); if (req.Item.HasValue) x.Item = req.Item.Value; if (req.Linha != null) x.Linha = TrimOrNull(req.Linha); if (req.Cliente != null) x.Cliente = TrimOrNull(req.Cliente); if (req.TipoPessoa != null) x.TipoPessoa = NormalizeTipo(req.TipoPessoa, req.Cpf, req.Cnpj, req.RazaoSocial, req.Nome) ?? x.TipoPessoa; if (req.Nome != null) x.Nome = TrimOrNull(req.Nome); if (req.RazaoSocial != null) x.RazaoSocial = TrimOrNull(req.RazaoSocial); if (req.Cnpj != null) x.Cnpj = NullIfEmptyDigits(req.Cnpj); if (req.Cpf != null) x.Cpf = NullIfEmptyDigits(req.Cpf); if (req.Rg != null) x.Rg = TrimOrNull(req.Rg); if (req.Email != null) x.Email = TrimOrNull(req.Email); if (req.Endereco != null) x.Endereco = TrimOrNull(req.Endereco); if (req.Celular != null) x.Celular = TrimOrNull(req.Celular); if (req.TelefoneFixo != null) x.TelefoneFixo = TrimOrNull(req.TelefoneFixo); if (req.DataNascimento.HasValue) { x.DataNascimento = ToUtc(req.DataNascimento.Value); } x.UpdatedAt = DateTime.UtcNow; await _db.SaveChangesAsync(); return NoContent(); } [HttpDelete("{id:guid}")] [Authorize(Roles = "admin")] public async Task Delete(Guid id) { var x = await _db.UserDatas.FirstOrDefaultAsync(a => a.Id == id); if (x == null) return NotFound(); _db.UserDatas.Remove(x); await _db.SaveChangesAsync(); return NoContent(); } private static string? TrimOrNull(string? s) { if (string.IsNullOrWhiteSpace(s)) return null; return s.Trim(); } private static DateTime ToUtc(DateTime dt) { return dt.Kind == DateTimeKind.Utc ? dt : (dt.Kind == DateTimeKind.Local ? dt.ToUniversalTime() : DateTime.SpecifyKind(dt, DateTimeKind.Utc)); } private static string? NormalizeTipo(string? tipo, string? cpf, string? cnpj, string? razao, string? nome) { var t = (tipo ?? "").Trim().ToUpperInvariant(); if (t == "PF" || t == "PJ") return t; if (!string.IsNullOrWhiteSpace(cnpj) || !string.IsNullOrWhiteSpace(razao)) return "PJ"; if (!string.IsNullOrWhiteSpace(cpf) || !string.IsNullOrWhiteSpace(nome)) return "PF"; return null; } private static string? NullIfEmptyDigits(string? s) { var d = OnlyDigits(s); return string.IsNullOrWhiteSpace(d) ? null : d; } private static string OnlyDigits(string? s) { 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; } } }