429 lines
14 KiB
C#
429 lines
14 KiB
C#
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<ActionResult<PagedResult<ParcelamentoListDto>>> 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<ParcelamentoListDto>
|
|
{
|
|
Page = page,
|
|
PageSize = pageSize,
|
|
Total = total,
|
|
Items = items
|
|
});
|
|
}
|
|
|
|
[HttpGet("{id:guid}")]
|
|
public async Task<ActionResult<ParcelamentoDetailDto>> 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<ActionResult<ParcelamentoDetailDto>> 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<IActionResult> 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<IActionResult> 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<ParcelamentoAnnualRowDto> BuildAnnualRows(List<ParcelamentoMonthDto> monthValues)
|
|
{
|
|
if (monthValues.Count == 0) return new List<ParcelamentoAnnualRowDto>();
|
|
|
|
var groups = monthValues
|
|
.GroupBy(m => m.Competencia.Year)
|
|
.OrderBy(g => g.Key);
|
|
|
|
var rows = new List<ParcelamentoAnnualRowDto>();
|
|
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<ParcelamentoMonthValue> BuildMonthValues(
|
|
List<ParcelamentoMonthInputDto> inputs,
|
|
Guid parcelamentoId,
|
|
DateTime now)
|
|
{
|
|
var map = new Dictionary<DateOnly, decimal?>();
|
|
foreach (var input in inputs ?? new List<ParcelamentoMonthInputDto>())
|
|
{
|
|
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<DateOnly> competencias)
|
|
{
|
|
var list = competencias?.ToList() ?? new List<DateOnly>();
|
|
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());
|
|
}
|
|
}
|