Compare commits
2 Commits
437bd2de4f
...
8f69e768b9
| Author | SHA1 | Date |
|---|---|---|
|
|
8f69e768b9 | |
|
|
8cb0b72474 |
|
|
@ -2,6 +2,7 @@
|
||||||
using line_gestao_api.Data;
|
using line_gestao_api.Data;
|
||||||
using line_gestao_api.Dtos;
|
using line_gestao_api.Dtos;
|
||||||
using line_gestao_api.Models;
|
using line_gestao_api.Models;
|
||||||
|
using line_gestao_api.Services;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
@ -20,10 +21,12 @@ namespace line_gestao_api.Controllers
|
||||||
public class LinesController : ControllerBase
|
public class LinesController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
|
private readonly ParcelamentosImportService _parcelamentosImportService;
|
||||||
|
|
||||||
public LinesController(AppDbContext db)
|
public LinesController(AppDbContext db, ParcelamentosImportService parcelamentosImportService)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
|
_parcelamentosImportService = parcelamentosImportService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ImportExcelForm
|
public class ImportExcelForm
|
||||||
|
|
@ -651,8 +654,13 @@ namespace line_gestao_api.Controllers
|
||||||
// =========================
|
// =========================
|
||||||
await ImportResumoFromWorkbook(wb);
|
await ImportResumoFromWorkbook(wb);
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// ✅ IMPORTA PARCELAMENTOS
|
||||||
|
// =========================
|
||||||
|
var parcelamentosSummary = await _parcelamentosImportService.ImportFromWorkbookAsync(wb, replaceAll: true);
|
||||||
|
|
||||||
await tx.CommitAsync();
|
await tx.CommitAsync();
|
||||||
return Ok(new ImportResultDto { Imported = imported });
|
return Ok(new ImportResultDto { Imported = imported, Parcelamentos = parcelamentosSummary });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
using line_gestao_api.Data;
|
||||||
|
using line_gestao_api.Dtos;
|
||||||
|
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 items = await query
|
||||||
|
.OrderBy(x => x.Item)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(x => new ParcelamentoListDto
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
AnoRef = x.AnoRef,
|
||||||
|
Item = x.Item,
|
||||||
|
Linha = x.Linha,
|
||||||
|
Cliente = x.Cliente,
|
||||||
|
QtParcelas = x.QtParcelas,
|
||||||
|
ParcelaAtual = x.ParcelaAtual,
|
||||||
|
TotalParcelas = x.TotalParcelas,
|
||||||
|
ValorCheio = x.ValorCheio,
|
||||||
|
Desconto = x.Desconto,
|
||||||
|
ValorComDesconto = x.ValorComDesconto
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
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 item = await _db.ParcelamentoLines
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(x => x.MonthValues)
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
|
||||||
|
if (item == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var dto = new ParcelamentoDetailDto
|
||||||
|
{
|
||||||
|
Id = item.Id,
|
||||||
|
AnoRef = item.AnoRef,
|
||||||
|
Item = item.Item,
|
||||||
|
Linha = item.Linha,
|
||||||
|
Cliente = item.Cliente,
|
||||||
|
QtParcelas = item.QtParcelas,
|
||||||
|
ParcelaAtual = item.ParcelaAtual,
|
||||||
|
TotalParcelas = item.TotalParcelas,
|
||||||
|
ValorCheio = item.ValorCheio,
|
||||||
|
Desconto = item.Desconto,
|
||||||
|
ValorComDesconto = item.ValorComDesconto,
|
||||||
|
MonthValues = item.MonthValues
|
||||||
|
.OrderBy(x => x.Competencia)
|
||||||
|
.Select(x => new ParcelamentoMonthDto
|
||||||
|
{
|
||||||
|
Competencia = x.Competencia,
|
||||||
|
Valor = x.Valor
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -56,6 +56,10 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
||||||
public DbSet<ResumoReservaLine> ResumoReservaLines => Set<ResumoReservaLine>();
|
public DbSet<ResumoReservaLine> ResumoReservaLines => Set<ResumoReservaLine>();
|
||||||
public DbSet<ResumoReservaTotal> ResumoReservaTotals => Set<ResumoReservaTotal>();
|
public DbSet<ResumoReservaTotal> ResumoReservaTotals => Set<ResumoReservaTotal>();
|
||||||
|
|
||||||
|
// ✅ tabela PARCELAMENTOS
|
||||||
|
public DbSet<ParcelamentoLine> ParcelamentoLines => Set<ParcelamentoLine>();
|
||||||
|
public DbSet<ParcelamentoMonthValue> ParcelamentoMonthValues => Set<ParcelamentoMonthValue>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
@ -215,6 +219,35 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// ✅ PARCELAMENTOS
|
||||||
|
// =========================
|
||||||
|
modelBuilder.Entity<ParcelamentoLine>(e =>
|
||||||
|
{
|
||||||
|
e.Property(x => x.Linha).HasMaxLength(32);
|
||||||
|
e.Property(x => x.Cliente).HasMaxLength(120);
|
||||||
|
e.Property(x => x.QtParcelas).HasMaxLength(32);
|
||||||
|
e.Property(x => x.ValorCheio).HasPrecision(18, 2);
|
||||||
|
e.Property(x => x.Desconto).HasPrecision(18, 2);
|
||||||
|
e.Property(x => x.ValorComDesconto).HasPrecision(18, 2);
|
||||||
|
|
||||||
|
e.HasIndex(x => new { x.AnoRef, x.Item }).IsUnique();
|
||||||
|
e.HasIndex(x => x.Linha);
|
||||||
|
e.HasIndex(x => x.TenantId);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<ParcelamentoMonthValue>(e =>
|
||||||
|
{
|
||||||
|
e.Property(x => x.Valor).HasPrecision(18, 2);
|
||||||
|
e.HasIndex(x => new { x.ParcelamentoLineId, x.Competencia }).IsUnique();
|
||||||
|
e.HasIndex(x => x.TenantId);
|
||||||
|
|
||||||
|
e.HasOne(x => x.ParcelamentoLine)
|
||||||
|
.WithMany(x => x.MonthValues)
|
||||||
|
.HasForeignKey(x => x.ParcelamentoLineId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<MobileLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
modelBuilder.Entity<MobileLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||||
modelBuilder.Entity<MuregLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
modelBuilder.Entity<MuregLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||||
modelBuilder.Entity<BillingClient>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
modelBuilder.Entity<BillingClient>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||||
|
|
@ -234,6 +267,8 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
|
||||||
modelBuilder.Entity<ResumoLineTotais>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
modelBuilder.Entity<ResumoLineTotais>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||||
modelBuilder.Entity<ResumoReservaLine>().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<ResumoReservaTotal>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||||
|
modelBuilder.Entity<ParcelamentoLine>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||||
|
modelBuilder.Entity<ParcelamentoMonthValue>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||||
modelBuilder.Entity<ApplicationUser>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
modelBuilder.Entity<ApplicationUser>().HasQueryFilter(x => _tenantProvider.TenantId != null && x.TenantId == _tenantProvider.TenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@
|
||||||
public class ImportResultDto
|
public class ImportResultDto
|
||||||
{
|
{
|
||||||
public int Imported { get; set; }
|
public int Imported { get; set; }
|
||||||
|
public ParcelamentosImportSummaryDto? Parcelamentos { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class LineOptionDto
|
public class LineOptionDto
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
namespace line_gestao_api.Dtos;
|
||||||
|
|
||||||
|
public sealed class ParcelamentoListDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public int? AnoRef { get; set; }
|
||||||
|
public int? Item { get; set; }
|
||||||
|
public string? Linha { get; set; }
|
||||||
|
public string? Cliente { get; set; }
|
||||||
|
public string? QtParcelas { get; set; }
|
||||||
|
public int? ParcelaAtual { get; set; }
|
||||||
|
public int? TotalParcelas { get; set; }
|
||||||
|
public decimal? ValorCheio { get; set; }
|
||||||
|
public decimal? Desconto { get; set; }
|
||||||
|
public decimal? ValorComDesconto { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ParcelamentoMonthDto
|
||||||
|
{
|
||||||
|
public DateOnly Competencia { get; set; }
|
||||||
|
public decimal? Valor { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ParcelamentoDetailDto : ParcelamentoListDto
|
||||||
|
{
|
||||||
|
public List<ParcelamentoMonthDto> MonthValues { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ParcelamentosImportErrorDto
|
||||||
|
{
|
||||||
|
public int LinhaExcel { get; set; }
|
||||||
|
public string Motivo { get; set; } = string.Empty;
|
||||||
|
public string? Valor { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ParcelamentosImportSummaryDto
|
||||||
|
{
|
||||||
|
public int Lidos { get; set; }
|
||||||
|
public int Inseridos { get; set; }
|
||||||
|
public int Atualizados { get; set; }
|
||||||
|
public int ParcelasInseridas { get; set; }
|
||||||
|
public List<ParcelamentosImportErrorDto> Erros { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
namespace line_gestao_api.Models;
|
||||||
|
|
||||||
|
public class ParcelamentoLine : ITenantEntity
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
|
||||||
|
public int? AnoRef { get; set; }
|
||||||
|
public int? Item { get; set; }
|
||||||
|
public string? Linha { get; set; }
|
||||||
|
public string? Cliente { get; set; }
|
||||||
|
public string? QtParcelas { get; set; }
|
||||||
|
public int? ParcelaAtual { get; set; }
|
||||||
|
public int? TotalParcelas { get; set; }
|
||||||
|
public decimal? ValorCheio { get; set; }
|
||||||
|
public decimal? Desconto { get; set; }
|
||||||
|
public decimal? ValorComDesconto { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
public List<ParcelamentoMonthValue> MonthValues { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
namespace line_gestao_api.Models;
|
||||||
|
|
||||||
|
public class ParcelamentoMonthValue : ITenantEntity
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public Guid TenantId { get; set; }
|
||||||
|
|
||||||
|
public Guid ParcelamentoLineId { get; set; }
|
||||||
|
public ParcelamentoLine? ParcelamentoLine { get; set; }
|
||||||
|
|
||||||
|
public DateOnly Competencia { get; set; }
|
||||||
|
public decimal? Valor { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,7 @@ builder.Services.AddDbContext<AppDbContext>(options =>
|
||||||
|
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddScoped<ITenantProvider, TenantProvider>();
|
builder.Services.AddScoped<ITenantProvider, TenantProvider>();
|
||||||
|
builder.Services.AddScoped<ParcelamentosImportService>();
|
||||||
|
|
||||||
builder.Services.AddIdentityCore<ApplicationUser>(options =>
|
builder.Services.AddIdentityCore<ApplicationUser>(options =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,447 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using ClosedXML.Excel;
|
||||||
|
using line_gestao_api.Data;
|
||||||
|
using line_gestao_api.Dtos;
|
||||||
|
using line_gestao_api.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace line_gestao_api.Services;
|
||||||
|
|
||||||
|
public sealed class ParcelamentosImportService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _db;
|
||||||
|
|
||||||
|
public ParcelamentosImportService(AppDbContext db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ParcelamentosImportSummaryDto> ImportFromWorkbookAsync(XLWorkbook wb, bool replaceAll, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var ws = FindWorksheet(wb);
|
||||||
|
if (ws == null)
|
||||||
|
{
|
||||||
|
return new ParcelamentosImportSummaryDto
|
||||||
|
{
|
||||||
|
Erros =
|
||||||
|
{
|
||||||
|
new ParcelamentosImportErrorDto
|
||||||
|
{
|
||||||
|
LinhaExcel = 0,
|
||||||
|
Motivo = "Aba 'PARCELAMENTOS DE APARELHOS' ou 'PARCELAMENTOS' não encontrada."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replaceAll)
|
||||||
|
{
|
||||||
|
await _db.ParcelamentoMonthValues.ExecuteDeleteAsync(cancellationToken);
|
||||||
|
await _db.ParcelamentoLines.ExecuteDeleteAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerRowIndex = FindHeaderRow(ws);
|
||||||
|
if (headerRowIndex == 0)
|
||||||
|
{
|
||||||
|
return new ParcelamentosImportSummaryDto
|
||||||
|
{
|
||||||
|
Erros =
|
||||||
|
{
|
||||||
|
new ParcelamentosImportErrorDto
|
||||||
|
{
|
||||||
|
LinhaExcel = 0,
|
||||||
|
Motivo = "Cabeçalho 'LINHA' não encontrado na aba de parcelamentos."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var yearRowIndex = headerRowIndex - 1;
|
||||||
|
var headerRow = ws.Row(headerRowIndex);
|
||||||
|
var map = BuildHeaderMap(headerRow);
|
||||||
|
|
||||||
|
var colLinha = GetCol(map, "LINHA");
|
||||||
|
var colCliente = GetCol(map, "CLIENTE");
|
||||||
|
var colQtParcelas = GetColAny(map, "QT PARCELAS", "QT. PARCELAS", "QT PARCELAS (NN/TT)", "QTDE PARCELAS");
|
||||||
|
var colValorCheio = GetColAny(map, "VALOR CHEIO");
|
||||||
|
var colDesconto = GetColAny(map, "DESCONTO");
|
||||||
|
var colValorComDesconto = GetColAny(map, "VALOR C/ DESCONTO", "VALOR COM DESCONTO");
|
||||||
|
|
||||||
|
if (colLinha == 0 || colValorComDesconto == 0)
|
||||||
|
{
|
||||||
|
return new ParcelamentosImportSummaryDto
|
||||||
|
{
|
||||||
|
Erros =
|
||||||
|
{
|
||||||
|
new ParcelamentosImportErrorDto
|
||||||
|
{
|
||||||
|
LinhaExcel = headerRowIndex,
|
||||||
|
Motivo = "Colunas obrigatórias não encontradas (LINHA / VALOR C/ DESCONTO)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var yearMap = BuildYearMap(ws, yearRowIndex, headerRow);
|
||||||
|
var monthColumns = BuildMonthColumns(headerRow, colValorComDesconto + 1);
|
||||||
|
|
||||||
|
var existing = await _db.ParcelamentoLines
|
||||||
|
.AsNoTracking()
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var existingByKey = existing
|
||||||
|
.Where(x => x.AnoRef.HasValue && x.Item.HasValue)
|
||||||
|
.ToDictionary(x => (x.AnoRef!.Value, x.Item!.Value), x => x.Id);
|
||||||
|
|
||||||
|
var summary = new ParcelamentosImportSummaryDto();
|
||||||
|
var lastRow = ws.LastRowUsed()?.RowNumber() ?? headerRowIndex;
|
||||||
|
|
||||||
|
for (int row = headerRowIndex + 1; row <= lastRow; row++)
|
||||||
|
{
|
||||||
|
var linhaValue = GetCellString(ws, row, colLinha);
|
||||||
|
var itemStr = GetCellString(ws, row, 4);
|
||||||
|
if (string.IsNullOrWhiteSpace(itemStr) && string.IsNullOrWhiteSpace(linhaValue))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.Lidos++;
|
||||||
|
|
||||||
|
var anoRef = TryNullableInt(GetCellString(ws, row, 3));
|
||||||
|
var item = TryNullableInt(itemStr);
|
||||||
|
|
||||||
|
if (!item.HasValue)
|
||||||
|
{
|
||||||
|
summary.Erros.Add(new ParcelamentosImportErrorDto
|
||||||
|
{
|
||||||
|
LinhaExcel = row,
|
||||||
|
Motivo = "Item inválido ou vazio.",
|
||||||
|
Valor = itemStr
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anoRef.HasValue)
|
||||||
|
{
|
||||||
|
summary.Erros.Add(new ParcelamentosImportErrorDto
|
||||||
|
{
|
||||||
|
LinhaExcel = row,
|
||||||
|
Motivo = "AnoRef inválido ou vazio.",
|
||||||
|
Valor = GetCellString(ws, row, 3)
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var qtParcelas = GetCellString(ws, row, colQtParcelas);
|
||||||
|
ParseParcelas(qtParcelas, out var parcelaAtual, out var totalParcelas);
|
||||||
|
|
||||||
|
var parcelamento = new ParcelamentoLine
|
||||||
|
{
|
||||||
|
AnoRef = anoRef,
|
||||||
|
Item = item,
|
||||||
|
Linha = string.IsNullOrWhiteSpace(linhaValue) ? null : linhaValue.Trim(),
|
||||||
|
Cliente = NormalizeText(GetCellString(ws, row, colCliente)),
|
||||||
|
QtParcelas = string.IsNullOrWhiteSpace(qtParcelas) ? null : qtParcelas.Trim(),
|
||||||
|
ParcelaAtual = parcelaAtual,
|
||||||
|
TotalParcelas = totalParcelas,
|
||||||
|
ValorCheio = TryDecimal(GetCellString(ws, row, colValorCheio)),
|
||||||
|
Desconto = TryDecimal(GetCellString(ws, row, colDesconto)),
|
||||||
|
ValorComDesconto = TryDecimal(GetCellString(ws, row, colValorComDesconto)),
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
if (existingByKey.TryGetValue((anoRef.Value, item.Value), out var existingId))
|
||||||
|
{
|
||||||
|
var existingEntity = await _db.ParcelamentoLines
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == existingId, cancellationToken);
|
||||||
|
if (existingEntity == null)
|
||||||
|
{
|
||||||
|
existingByKey.Remove((anoRef ?? 0, item.Value));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existingEntity.AnoRef = parcelamento.AnoRef;
|
||||||
|
existingEntity.Item = parcelamento.Item;
|
||||||
|
existingEntity.Linha = parcelamento.Linha;
|
||||||
|
existingEntity.Cliente = parcelamento.Cliente;
|
||||||
|
existingEntity.QtParcelas = parcelamento.QtParcelas;
|
||||||
|
existingEntity.ParcelaAtual = parcelamento.ParcelaAtual;
|
||||||
|
existingEntity.TotalParcelas = parcelamento.TotalParcelas;
|
||||||
|
existingEntity.ValorCheio = parcelamento.ValorCheio;
|
||||||
|
existingEntity.Desconto = parcelamento.Desconto;
|
||||||
|
existingEntity.ValorComDesconto = parcelamento.ValorComDesconto;
|
||||||
|
existingEntity.UpdatedAt = parcelamento.UpdatedAt;
|
||||||
|
|
||||||
|
await _db.ParcelamentoMonthValues
|
||||||
|
.Where(x => x.ParcelamentoLineId == existingEntity.Id)
|
||||||
|
.ExecuteDeleteAsync(cancellationToken);
|
||||||
|
|
||||||
|
var monthValues = BuildMonthValues(ws, headerRowIndex, row, monthColumns, yearMap, existingEntity.Id, summary);
|
||||||
|
if (monthValues.Count > 0)
|
||||||
|
{
|
||||||
|
await _db.ParcelamentoMonthValues.AddRangeAsync(monthValues, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.Atualizados++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parcelamento.CreatedAt = DateTime.UtcNow;
|
||||||
|
parcelamento.MonthValues = BuildMonthValues(ws, headerRowIndex, row, monthColumns, yearMap, parcelamento.Id, summary);
|
||||||
|
summary.Inseridos++;
|
||||||
|
|
||||||
|
await _db.ParcelamentoLines.AddAsync(parcelamento, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _db.SaveChangesAsync(cancellationToken);
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IXLWorksheet? FindWorksheet(XLWorkbook wb)
|
||||||
|
{
|
||||||
|
return wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("PARCELAMENTOS DE APARELHOS"))
|
||||||
|
?? wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("PARCELAMENTOS"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FindHeaderRow(IXLWorksheet ws)
|
||||||
|
{
|
||||||
|
var firstRow = ws.FirstRowUsed()?.RowNumber() ?? 1;
|
||||||
|
var lastRow = Math.Min(firstRow + 20, ws.LastRowUsed()?.RowNumber() ?? firstRow);
|
||||||
|
|
||||||
|
for (int r = firstRow; r <= lastRow; r++)
|
||||||
|
{
|
||||||
|
var row = ws.Row(r);
|
||||||
|
foreach (var cell in row.CellsUsed())
|
||||||
|
{
|
||||||
|
if (NormalizeHeader(cell.GetString()) == NormalizeHeader("LINHA"))
|
||||||
|
{
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<int, int> BuildYearMap(IXLWorksheet ws, int yearRowIndex, IXLRow headerRow)
|
||||||
|
{
|
||||||
|
var yearMap = new Dictionary<int, int>();
|
||||||
|
var lastCol = headerRow.LastCellUsed()?.Address.ColumnNumber ?? 1;
|
||||||
|
|
||||||
|
if (yearRowIndex <= 0)
|
||||||
|
{
|
||||||
|
return yearMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int col = 1; col <= lastCol; col++)
|
||||||
|
{
|
||||||
|
var yearCell = ws.Cell(yearRowIndex, col);
|
||||||
|
var yearText = yearCell.GetString();
|
||||||
|
if (string.IsNullOrWhiteSpace(yearText))
|
||||||
|
{
|
||||||
|
var merged = ws.MergedRanges.FirstOrDefault(r =>
|
||||||
|
r.RangeAddress.FirstAddress.RowNumber == yearRowIndex &&
|
||||||
|
r.RangeAddress.FirstAddress.ColumnNumber <= col &&
|
||||||
|
r.RangeAddress.LastAddress.ColumnNumber >= col);
|
||||||
|
|
||||||
|
if (merged != null)
|
||||||
|
{
|
||||||
|
yearText = merged.FirstCell().GetString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (int.TryParse(OnlyDigits(yearText), out var year))
|
||||||
|
{
|
||||||
|
yearMap[col] = year;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return yearMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<int> BuildMonthColumns(IXLRow headerRow, int startCol)
|
||||||
|
{
|
||||||
|
var lastCol = headerRow.LastCellUsed()?.Address.ColumnNumber ?? startCol;
|
||||||
|
var months = new List<int>();
|
||||||
|
for (int col = startCol; col <= lastCol; col++)
|
||||||
|
{
|
||||||
|
var header = NormalizeHeader(headerRow.Cell(col).GetString());
|
||||||
|
if (ToMonthNumber(header).HasValue)
|
||||||
|
{
|
||||||
|
months.Add(col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return months;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ParcelamentoMonthValue> BuildMonthValues(
|
||||||
|
IXLWorksheet ws,
|
||||||
|
int headerRowIndex,
|
||||||
|
int row,
|
||||||
|
List<int> monthColumns,
|
||||||
|
Dictionary<int, int> yearMap,
|
||||||
|
Guid parcelamentoId,
|
||||||
|
ParcelamentosImportSummaryDto summary)
|
||||||
|
{
|
||||||
|
var monthValues = new List<ParcelamentoMonthValue>();
|
||||||
|
foreach (var col in monthColumns)
|
||||||
|
{
|
||||||
|
if (!yearMap.TryGetValue(col, out var year))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var header = NormalizeHeader(ws.Cell(headerRowIndex, col).GetString());
|
||||||
|
var monthNumber = ToMonthNumber(header);
|
||||||
|
if (!monthNumber.HasValue)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var valueStr = ws.Cell(row, col).GetString();
|
||||||
|
var value = TryDecimal(valueStr);
|
||||||
|
if (!value.HasValue)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
monthValues.Add(new ParcelamentoMonthValue
|
||||||
|
{
|
||||||
|
ParcelamentoLineId = parcelamentoId,
|
||||||
|
Competencia = new DateOnly(year, monthNumber.Value, 1),
|
||||||
|
Valor = value,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
summary.ParcelasInseridas++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return monthValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseParcelas(string? qtParcelas, out int? parcelaAtual, out int? totalParcelas)
|
||||||
|
{
|
||||||
|
parcelaAtual = null;
|
||||||
|
totalParcelas = null;
|
||||||
|
if (string.IsNullOrWhiteSpace(qtParcelas))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = qtParcelas.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
if (parts.Length >= 1 && int.TryParse(OnlyDigits(parts[0]), out var atual))
|
||||||
|
{
|
||||||
|
parcelaAtual = atual;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.Length >= 2 && int.TryParse(OnlyDigits(parts[1]), out var total))
|
||||||
|
{
|
||||||
|
totalParcelas = total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, int> BuildHeaderMap(IXLRow headerRow)
|
||||||
|
{
|
||||||
|
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var cell in headerRow.CellsUsed())
|
||||||
|
{
|
||||||
|
var k = NormalizeHeader(cell.GetString());
|
||||||
|
if (!string.IsNullOrWhiteSpace(k) && !map.ContainsKey(k))
|
||||||
|
map[k] = cell.Address.ColumnNumber;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetCol(Dictionary<string, int> map, string name)
|
||||||
|
=> map.TryGetValue(NormalizeHeader(name), out var c) ? c : 0;
|
||||||
|
|
||||||
|
private static int GetColAny(Dictionary<string, int> map, params string[] headers)
|
||||||
|
{
|
||||||
|
foreach (var h in headers)
|
||||||
|
{
|
||||||
|
var k = NormalizeHeader(h);
|
||||||
|
if (map.TryGetValue(k, out var c)) return c;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCellString(IXLWorksheet ws, int row, int col)
|
||||||
|
{
|
||||||
|
if (col <= 0) return "";
|
||||||
|
return (ws.Cell(row, col).GetValue<string>() ?? "").Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeText(string value)
|
||||||
|
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
|
||||||
|
private static decimal? TryDecimal(string? s)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(s)) return null;
|
||||||
|
|
||||||
|
s = s.Replace("R$", "", StringComparison.OrdinalIgnoreCase).Trim();
|
||||||
|
|
||||||
|
if (decimal.TryParse(s, NumberStyles.Any, new CultureInfo("pt-BR"), out var d)) return d;
|
||||||
|
if (decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out d)) return d;
|
||||||
|
|
||||||
|
var s2 = s.Replace(".", "").Replace(",", ".");
|
||||||
|
if (decimal.TryParse(s2, NumberStyles.Any, CultureInfo.InvariantCulture, out d)) return d;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int? TryNullableInt(string? s)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(s)) return null;
|
||||||
|
var d = OnlyDigits(s);
|
||||||
|
if (string.IsNullOrWhiteSpace(d)) return null;
|
||||||
|
return int.TryParse(d, out var n) ? n : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string OnlyDigits(string? s)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(s)) return "";
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var c in s) if (char.IsDigit(c)) sb.Append(c);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int? ToMonthNumber(string? month)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(month)) return null;
|
||||||
|
return NormalizeHeader(month) switch
|
||||||
|
{
|
||||||
|
"JAN" => 1,
|
||||||
|
"FEV" => 2,
|
||||||
|
"MAR" => 3,
|
||||||
|
"ABR" => 4,
|
||||||
|
"MAI" => 5,
|
||||||
|
"JUN" => 6,
|
||||||
|
"JUL" => 7,
|
||||||
|
"AGO" => 8,
|
||||||
|
"SET" => 9,
|
||||||
|
"OUT" => 10,
|
||||||
|
"NOV" => 11,
|
||||||
|
"DEZ" => 12,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeHeader(string? s)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(s)) return "";
|
||||||
|
s = s.Trim().ToUpperInvariant().Normalize(NormalizationForm.FormD);
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var c in s)
|
||||||
|
if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark)
|
||||||
|
sb.Append(c);
|
||||||
|
|
||||||
|
return sb.ToString()
|
||||||
|
.Normalize(NormalizationForm.FormC)
|
||||||
|
.Replace(" ", "")
|
||||||
|
.Replace("\t", "")
|
||||||
|
.Replace("\n", "")
|
||||||
|
.Replace("\r", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue