using line_gestao_api.Data; using line_gestao_api.Dtos; using line_gestao_api.Models; 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 { [ApiController] [Route("api/lines/vigencia")] [Authorize] public class VigenciaController : ControllerBase { private readonly AppDbContext _db; public VigenciaController(AppDbContext db) { _db = db; } // GET /api/lines/vigencia (Linhas - Tabela Interna) [HttpGet] public async Task>> GetVigencia( [FromQuery] string? search, [FromQuery] string? client, [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.VigenciaLines.AsNoTracking(); if (!string.IsNullOrWhiteSpace(client)) { var c = client.Trim(); q = q.Where(x => x.Cliente == c); } 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.Item.ToString(), $"%{s}%") || (hasTotalSearch && x.Total != null && x.Total.Value == searchTotal) || (hasDateSearch && ((x.DtEfetivacaoServico != null && x.DtEfetivacaoServico.Value >= searchDateStartUtc && x.DtEfetivacaoServico.Value < searchDateEndUtc) || (x.DtTerminoFidelizacao != null && x.DtTerminoFidelizacao.Value >= searchDateStartUtc && x.DtTerminoFidelizacao.Value < searchDateEndUtc)))); } var total = await q.CountAsync(); var 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), "total" => desc ? q.OrderByDescending(x => x.Total) : q.OrderBy(x => x.Total), "dttermino" => desc ? q.OrderByDescending(x => x.DtTerminoFidelizacao) : q.OrderBy(x => x.DtTerminoFidelizacao), _ => desc ? q.OrderByDescending(x => x.Item) : q.OrderBy(x => x.Item), }; var items = await q .Skip((page - 1) * pageSize) .Take(pageSize) .Select(x => new VigenciaLineListDto { Id = x.Id, Item = x.Item, Conta = x.Conta, Linha = x.Linha, Cliente = x.Cliente, Usuario = x.Usuario, PlanoContrato = x.PlanoContrato, DtEfetivacaoServico = x.DtEfetivacaoServico, DtTerminoFidelizacao = x.DtTerminoFidelizacao, Total = x.Total }) .ToListAsync(); return Ok(new PagedResult { Page = page, PageSize = pageSize, Total = total, Items = items }); } // ========================================================== // GET /api/lines/vigencia/groups (Cards + KPIs GERAIS) // ========================================================== [HttpGet("groups")] public async Task> GetVigenciaGroups( [FromQuery] string? search, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, [FromQuery] string? sortBy = "cliente", [FromQuery] string? sortDir = "asc") { page = page < 1 ? 1 : page; pageSize = pageSize < 1 ? 20 : pageSize; var todayUtcStart = DateTime.SpecifyKind(DateTime.UtcNow.Date, DateTimeKind.Utc); var limit30ExclusiveUtcStart = todayUtcStart.AddDays(31); // Query Base (Linhas) var q = _db.VigenciaLines.AsNoTracking() .Where(x => x.Cliente != null && x.Cliente != ""); 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.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) // Isso garante que os KPIs mostrem o total do banco (ou do filtro), não só da página. var kpis = new VigenciaKpis { 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 < todayUtcStart), ValorTotal = await q.SumAsync(x => x.Total ?? 0m) }; // Agrupamento para a lista paginada var grouped = q .GroupBy(x => x.Cliente!) .Select(g => new VigenciaClientGroupDto { Cliente = g.Key, Linhas = g.Count(), Total = g.Sum(x => x.Total ?? 0m), 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 var totalGroups = await grouped.CountAsync(); // Ordenação var desc = string.Equals((sortDir ?? "asc").Trim(), "desc", StringComparison.OrdinalIgnoreCase); if (sortBy?.ToLower() == "linhas") grouped = desc ? grouped.OrderByDescending(x => x.Linhas) : grouped.OrderBy(x => x.Linhas); else grouped = desc ? grouped.OrderByDescending(x => x.Cliente) : grouped.OrderBy(x => x.Cliente); // Paginação var items = await grouped .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); // ✅ Retorna objeto composto return Ok(new VigenciaGroupResponse { Data = new PagedResult { Page = page, PageSize = pageSize, Total = totalGroups, // Total de Clientes na paginação Items = items }, Kpis = kpis // KPIs Globais }); } [HttpGet("clients")] public async Task>> GetVigenciaClients() { return await _db.VigenciaLines.AsNoTracking() .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.VigenciaLines.AsNoTracking().FirstOrDefaultAsync(a => a.Id == id); if (x == null) return NotFound(); return Ok(new VigenciaLineDetailDto { Id = x.Id, Item = x.Item, Conta = x.Conta, Linha = x.Linha, Cliente = x.Cliente, Usuario = x.Usuario, PlanoContrato = x.PlanoContrato, DtEfetivacaoServico = x.DtEfetivacaoServico, DtTerminoFidelizacao = x.DtTerminoFidelizacao, Total = x.Total, CreatedAt = x.CreatedAt, UpdatedAt = x.UpdatedAt }); } [HttpPost] [Authorize(Roles = "admin")] public async Task> Create([FromBody] CreateVigenciaRequest req) { var now = DateTime.UtcNow; var linha = TrimOrNull(req.Linha); var conta = TrimOrNull(req.Conta); var cliente = TrimOrNull(req.Cliente); var usuario = TrimOrNull(req.Usuario); var plano = TrimOrNull(req.PlanoContrato); MobileLine? mobile = null; if (!string.IsNullOrWhiteSpace(linha)) { var linhaDigits = OnlyDigits(linha); 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) { conta ??= mobile.Conta; cliente ??= mobile.Cliente; usuario ??= mobile.Usuario; plano ??= mobile.PlanoContrato; } } decimal? total = req.Total; if (!total.HasValue && mobile?.ValorPlanoVivo != null) { total = mobile.ValorPlanoVivo; } if (!total.HasValue && !string.IsNullOrWhiteSpace(plano)) { var planSuggestion = await AutoFillRules.ResolvePlanSuggestionAsync(_db, plano); total = planSuggestion?.ValorPlano; } var item = req.Item; if (!item.HasValue || item.Value <= 0) { var maxItem = await _db.VigenciaLines.MaxAsync(x => (int?)x.Item) ?? 0; item = maxItem + 1; } var e = new VigenciaLine { Id = Guid.NewGuid(), Item = item.Value, Conta = conta, Linha = linha, Cliente = cliente, Usuario = usuario, PlanoContrato = plano, DtEfetivacaoServico = req.DtEfetivacaoServico.HasValue ? ToUtc(req.DtEfetivacaoServico.Value) : null, DtTerminoFidelizacao = req.DtTerminoFidelizacao.HasValue ? ToUtc(req.DtTerminoFidelizacao.Value) : null, Total = total, CreatedAt = now, UpdatedAt = now }; _db.VigenciaLines.Add(e); await _db.SaveChangesAsync(); return CreatedAtAction(nameof(GetById), new { id = e.Id }, new VigenciaLineDetailDto { Id = e.Id, Item = e.Item, Conta = e.Conta, Linha = e.Linha, Cliente = e.Cliente, Usuario = e.Usuario, PlanoContrato = e.PlanoContrato, DtEfetivacaoServico = e.DtEfetivacaoServico, DtTerminoFidelizacao = e.DtTerminoFidelizacao, Total = e.Total, CreatedAt = e.CreatedAt, UpdatedAt = e.UpdatedAt }); } [HttpPut("{id:guid}")] [Authorize(Roles = "admin")] public async Task Update(Guid id, [FromBody] UpdateVigenciaRequest req) { var x = await _db.VigenciaLines.FirstOrDefaultAsync(a => a.Id == id); if (x == null) return NotFound(); if (req.Item.HasValue) x.Item = req.Item.Value; if (req.Conta != null) x.Conta = TrimOrNull(req.Conta); if (req.Linha != null) x.Linha = TrimOrNull(req.Linha); if (req.Cliente != null) x.Cliente = TrimOrNull(req.Cliente); if (req.Usuario != null) x.Usuario = TrimOrNull(req.Usuario); if (req.PlanoContrato != null) x.PlanoContrato = TrimOrNull(req.PlanoContrato); if (req.DtEfetivacaoServico.HasValue) x.DtEfetivacaoServico = ToUtc(req.DtEfetivacaoServico.Value); if (req.DtTerminoFidelizacao.HasValue) x.DtTerminoFidelizacao = ToUtc(req.DtTerminoFidelizacao.Value); if (req.Total.HasValue) x.Total = req.Total.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.VigenciaLines.FirstOrDefaultAsync(a => a.Id == id); if (x == null) return NotFound(); _db.VigenciaLines.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 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; } 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); } } }