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; namespace line_gestao_api.Controllers; [ApiController] [Route("api/parcelamentos")] [Authorize] public class ParcelamentosController : ControllerBase { private readonly AppDbContext _db; public ParcelamentosController(AppDbContext db) { _db = db; } [HttpGet] public async Task>> GetAll( [FromQuery] int? anoRef, [FromQuery] string? linha, [FromQuery] string? cliente, [FromQuery] int? competenciaAno, [FromQuery] int? competenciaMes, [FromQuery] int page = 1, [FromQuery] int pageSize = 20) { page = page < 1 ? 1 : page; pageSize = pageSize < 1 ? 20 : pageSize; var query = _db.ParcelamentoLines.AsNoTracking(); if (anoRef.HasValue) { query = query.Where(x => x.AnoRef == anoRef.Value); } if (!string.IsNullOrWhiteSpace(linha)) { var l = linha.Trim(); query = query.Where(x => x.Linha == l); } if (!string.IsNullOrWhiteSpace(cliente)) { var c = cliente.Trim(); query = query.Where(x => x.Cliente != null && EF.Functions.ILike(x.Cliente, $"%{c}%")); } if (competenciaAno.HasValue && competenciaMes.HasValue) { var competencia = new DateOnly(competenciaAno.Value, competenciaMes.Value, 1); query = query.Where(x => x.MonthValues.Any(m => m.Competencia == competencia)); } var total = await query.CountAsync(); var today = DateOnly.FromDateTime(DateTime.Today); var rows = await query .OrderBy(x => x.Item) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(x => new { x.Id, x.AnoRef, x.Item, x.Linha, x.Cliente, x.QtParcelas, x.ParcelaAtual, x.TotalParcelas, x.ValorCheio, x.Desconto, x.ValorComDesconto, ParcelaAtualCalc = x.MonthValues.Count(m => m.Competencia <= today), TotalMeses = x.MonthValues.Count() }) .ToListAsync(); var items = rows.Select(x => { var (parcelaAtual, totalParcelas) = ResolveParcelasFromCounts( x.ParcelaAtual, x.TotalParcelas, x.QtParcelas, x.ParcelaAtualCalc, x.TotalMeses); return new ParcelamentoListDto { Id = x.Id, AnoRef = x.AnoRef, Item = x.Item, Linha = x.Linha, Cliente = x.Cliente, QtParcelas = BuildQtParcelas(parcelaAtual, totalParcelas, x.QtParcelas), ParcelaAtual = parcelaAtual, TotalParcelas = totalParcelas, ValorCheio = x.ValorCheio, Desconto = x.Desconto, ValorComDesconto = x.ValorComDesconto }; }).ToList(); return Ok(new PagedResult { Page = page, PageSize = pageSize, Total = total, Items = items }); } [HttpGet("{id:guid}")] public async Task> GetById(Guid id) { var entity = await _db.ParcelamentoLines .AsNoTracking() .Include(x => x.MonthValues) .FirstOrDefaultAsync(x => x.Id == id); if (entity == null) { return NotFound(); } var monthValues = entity.MonthValues .OrderBy(m => m.Competencia) .Select(m => new ParcelamentoMonthDto { Competencia = m.Competencia, Valor = m.Valor }) .ToList(); var (parcelaAtual, totalParcelas) = ResolveParcelasFromCompetencias( entity.ParcelaAtual, entity.TotalParcelas, entity.QtParcelas, monthValues.Select(m => m.Competencia)); var dto = new ParcelamentoDetailDto { Id = entity.Id, AnoRef = entity.AnoRef, Item = entity.Item, Linha = entity.Linha, Cliente = entity.Cliente, QtParcelas = BuildQtParcelas(parcelaAtual, totalParcelas, entity.QtParcelas), ParcelaAtual = parcelaAtual, TotalParcelas = totalParcelas, ValorCheio = entity.ValorCheio, Desconto = entity.Desconto, ValorComDesconto = entity.ValorComDesconto, MonthValues = monthValues, AnnualRows = BuildAnnualRows(monthValues) }; return Ok(dto); } [HttpPost] [Authorize(Roles = "admin,gestor")] public async Task> Create([FromBody] ParcelamentoUpsertDto req) { var now = DateTime.UtcNow; var entity = new ParcelamentoLine { Id = Guid.NewGuid(), AnoRef = req.AnoRef, Item = req.Item, Linha = TrimOrNull(req.Linha), Cliente = TrimOrNull(req.Cliente), QtParcelas = TrimOrNull(req.QtParcelas), ParcelaAtual = req.ParcelaAtual, TotalParcelas = req.TotalParcelas, ValorCheio = req.ValorCheio, Desconto = req.Desconto, ValorComDesconto = ResolveValorComDesconto(req.ValorComDesconto, req.ValorCheio, req.Desconto), CreatedAt = now, UpdatedAt = now }; if (req.AnoRef.HasValue && req.Item.HasValue) { var exists = await _db.ParcelamentoLines.AnyAsync(x => x.AnoRef == req.AnoRef && x.Item == req.Item); if (exists) return Conflict("Já existe um parcelamento com o mesmo AnoRef e Item."); } entity.MonthValues = BuildMonthValues(req.MonthValues, entity.Id, now); ApplyComputedParcelas(entity); _db.ParcelamentoLines.Add(entity); await _db.SaveChangesAsync(); return CreatedAtAction(nameof(GetById), new { id = entity.Id }, ToDetailDto(entity)); } [HttpPut("{id:guid}")] [Authorize(Roles = "admin,gestor")] public async Task Update(Guid id, [FromBody] ParcelamentoUpsertDto req) { var entity = await _db.ParcelamentoLines .Include(x => x.MonthValues) .FirstOrDefaultAsync(x => x.Id == id); if (entity == null) return NotFound(); if (req.AnoRef.HasValue && req.Item.HasValue) { var exists = await _db.ParcelamentoLines.AnyAsync(x => x.Id != id && x.AnoRef == req.AnoRef && x.Item == req.Item); if (exists) return Conflict("Já existe um parcelamento com o mesmo AnoRef e Item."); } entity.AnoRef = req.AnoRef; entity.Item = req.Item; entity.Linha = TrimOrNull(req.Linha); entity.Cliente = TrimOrNull(req.Cliente); entity.QtParcelas = TrimOrNull(req.QtParcelas); entity.ParcelaAtual = req.ParcelaAtual; entity.TotalParcelas = req.TotalParcelas; entity.ValorCheio = req.ValorCheio; entity.Desconto = req.Desconto; entity.ValorComDesconto = ResolveValorComDesconto(req.ValorComDesconto, req.ValorCheio, req.Desconto); entity.UpdatedAt = DateTime.UtcNow; _db.ParcelamentoMonthValues.RemoveRange(entity.MonthValues); entity.MonthValues = BuildMonthValues(req.MonthValues, entity.Id, entity.UpdatedAt); ApplyComputedParcelas(entity); await _db.SaveChangesAsync(); return NoContent(); } [HttpDelete("{id:guid}")] [Authorize(Roles = "admin")] public async Task Delete(Guid id) { var entity = await _db.ParcelamentoLines.FirstOrDefaultAsync(x => x.Id == id); if (entity == null) return NotFound(); _db.ParcelamentoLines.Remove(entity); await _db.SaveChangesAsync(); return NoContent(); } private static List BuildAnnualRows(List monthValues) { if (monthValues.Count == 0) return new List(); var groups = monthValues .GroupBy(m => m.Competencia.Year) .OrderBy(g => g.Key); var rows = new List(); foreach (var group in groups) { var monthMap = group .GroupBy(m => m.Competencia.Month) .ToDictionary(g => g.Key, g => g.Sum(x => x.Valor ?? 0m)); var months = Enumerable.Range(1, 12) .Select(month => new ParcelamentoAnnualMonthDto { Month = month, Valor = monthMap.TryGetValue(month, out var value) ? value : null }) .ToList(); var total = months.Sum(m => m.Valor ?? 0m); rows.Add(new ParcelamentoAnnualRowDto { Year = group.Key, Total = total, Months = months }); } return rows; } private static List BuildMonthValues( List inputs, Guid parcelamentoId, DateTime now) { var map = new Dictionary(); foreach (var input in inputs ?? new List()) { if (input.Competencia == default) continue; map[input.Competencia] = input.Valor; } return map .OrderBy(x => x.Key) .Select(x => new ParcelamentoMonthValue { Id = Guid.NewGuid(), ParcelamentoLineId = parcelamentoId, Competencia = x.Key, Valor = x.Value, CreatedAt = now }) .ToList(); } private static void ApplyComputedParcelas(ParcelamentoLine entity) { var competencias = entity.MonthValues.Select(m => m.Competencia).ToList(); var (parcelaAtual, totalParcelas) = ResolveParcelasFromCompetencias( entity.ParcelaAtual, entity.TotalParcelas, entity.QtParcelas, competencias); entity.ParcelaAtual = parcelaAtual; entity.TotalParcelas = totalParcelas; entity.QtParcelas = BuildQtParcelas(parcelaAtual, totalParcelas, entity.QtParcelas); } private static ParcelamentoDetailDto ToDetailDto(ParcelamentoLine entity) { var monthValues = entity.MonthValues .OrderBy(m => m.Competencia) .Select(m => new ParcelamentoMonthDto { Competencia = m.Competencia, Valor = m.Valor }) .ToList(); var (parcelaAtual, totalParcelas) = ResolveParcelasFromCompetencias( entity.ParcelaAtual, entity.TotalParcelas, entity.QtParcelas, monthValues.Select(m => m.Competencia)); return new ParcelamentoDetailDto { Id = entity.Id, AnoRef = entity.AnoRef, Item = entity.Item, Linha = entity.Linha, Cliente = entity.Cliente, QtParcelas = BuildQtParcelas(parcelaAtual, totalParcelas, entity.QtParcelas), ParcelaAtual = parcelaAtual, TotalParcelas = totalParcelas, ValorCheio = entity.ValorCheio, Desconto = entity.Desconto, ValorComDesconto = entity.ValorComDesconto, MonthValues = monthValues, AnnualRows = BuildAnnualRows(monthValues) }; } private static (int? ParcelaAtual, int? TotalParcelas) ResolveParcelasFromCounts( int? storedAtual, int? storedTotal, string? qtParcelas, int mesesAteHoje, int totalMeses) { var total = storedTotal ?? ParseQtParcelas(qtParcelas).Total ?? (totalMeses > 0 ? totalMeses : (int?)null); int? atual = totalMeses > 0 ? mesesAteHoje : storedAtual ?? ParseQtParcelas(qtParcelas).Atual; if (atual.HasValue && atual.Value < 0) atual = 0; if (total.HasValue && atual.HasValue) { atual = Math.Min(atual.Value, total.Value); } return (atual, total); } private static (int? ParcelaAtual, int? TotalParcelas) ResolveParcelasFromCompetencias( int? storedAtual, int? storedTotal, string? qtParcelas, IEnumerable competencias) { var list = competencias?.ToList() ?? new List(); var totalMeses = list.Count; var hoje = DateOnly.FromDateTime(DateTime.Today); var mesesAteHoje = list.Count(c => c <= hoje); return ResolveParcelasFromCounts(storedAtual, storedTotal, qtParcelas, mesesAteHoje, totalMeses); } private static (int? Atual, int? Total) ParseQtParcelas(string? raw) { if (string.IsNullOrWhiteSpace(raw)) return (null, null); var parts = raw.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); int? atual = null; int? total = null; if (parts.Length >= 1 && int.TryParse(OnlyDigits(parts[0]), out var a)) atual = a; if (parts.Length >= 2 && int.TryParse(OnlyDigits(parts[1]), out var t)) total = t; return (atual, total); } private static string? BuildQtParcelas(int? atual, int? total, string? fallback) { if (atual.HasValue && total.HasValue) return $"{atual}/{total}"; return string.IsNullOrWhiteSpace(fallback) ? null : fallback.Trim(); } private static decimal? ResolveValorComDesconto(decimal? valorCom, decimal? valorCheio, decimal? desconto) { if (valorCom.HasValue) return valorCom; if (!valorCheio.HasValue) return null; return Math.Max(0, valorCheio.Value - (desconto ?? 0m)); } private static string? TrimOrNull(string? s) { if (string.IsNullOrWhiteSpace(s)) return null; return s.Trim(); } private static string OnlyDigits(string? s) { if (string.IsNullOrWhiteSpace(s)) return ""; return new string(s.Where(char.IsDigit).ToArray()); } }