This commit is contained in:
Eduardo Lopes 2026-02-02 16:50:04 -03:00 committed by GitHub
commit e687a2e406
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 64 additions and 146 deletions

View File

@ -3,6 +3,7 @@ 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 line_gestao_api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -503,6 +504,7 @@ namespace line_gestao_api.Controllers
// ✅ 8. IMPORT EXCEL // ✅ 8. IMPORT EXCEL
// ========================================================== // ==========================================================
[HttpPost("import-excel")] [HttpPost("import-excel")]
[Authorize]
[Consumes("multipart/form-data")] [Consumes("multipart/form-data")]
[RequestSizeLimit(50_000_000)] [RequestSizeLimit(50_000_000)]
public async Task<ActionResult<ImportResultDto>> ImportExcel([FromForm] ImportExcelForm form) public async Task<ActionResult<ImportResultDto>> ImportExcel([FromForm] ImportExcelForm form)

View File

@ -1,6 +1,6 @@
namespace line_gestao_api.Dtos; namespace line_gestao_api.Dtos;
public sealed class ParcelamentoListDto public class ParcelamentoListDto
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public int? AnoRef { get; set; } public int? AnoRef { get; set; }

View File

@ -41,50 +41,16 @@ public sealed class ParcelamentosImportService
await _db.ParcelamentoLines.ExecuteDeleteAsync(cancellationToken); await _db.ParcelamentoLines.ExecuteDeleteAsync(cancellationToken);
} }
var headerRowIndex = FindHeaderRow(ws); const int startRow = 6;
if (headerRowIndex == 0) const int colAnoRef = 3;
{ const int colItem = 4;
return new ParcelamentosImportSummaryDto const int colLinha = 5;
{ const int colCliente = 6;
Erros = const int colQtParcelas = 7;
{ const int colValorCheio = 8;
new ParcelamentosImportErrorDto const int colDesconto = 9;
{ const int colValorComDesconto = 10;
LinhaExcel = 0, var monthMap = BuildFixedMonthMap();
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 var existing = await _db.ParcelamentoLines
.AsNoTracking() .AsNoTracking()
@ -94,20 +60,23 @@ public sealed class ParcelamentosImportService
.ToDictionary(x => (x.AnoRef!.Value, x.Item!.Value), x => x.Id); .ToDictionary(x => (x.AnoRef!.Value, x.Item!.Value), x => x.Id);
var summary = new ParcelamentosImportSummaryDto(); var summary = new ParcelamentosImportSummaryDto();
var lastRow = ws.LastRowUsed()?.RowNumber() ?? headerRowIndex; var lastRow = ws.LastRowUsed()?.RowNumber() ?? startRow;
for (int row = headerRowIndex + 1; row <= lastRow; row++) for (int row = startRow; row <= lastRow; row++)
{ {
var linhaValue = GetCellString(ws, row, colLinha); var linhaValue = GetCellStringValue(ws, row, colLinha);
var itemStr = GetCellString(ws, row, 4); var itemStr = GetCellStringValue(ws, row, colItem);
if (string.IsNullOrWhiteSpace(itemStr) && string.IsNullOrWhiteSpace(linhaValue)) var anoRefStr = GetCellStringValue(ws, row, colAnoRef);
if (string.IsNullOrWhiteSpace(itemStr)
&& string.IsNullOrWhiteSpace(linhaValue)
&& string.IsNullOrWhiteSpace(anoRefStr))
{ {
break; continue;
} }
summary.Lidos++; summary.Lidos++;
var anoRef = TryNullableInt(GetCellString(ws, row, 3)); var anoRef = TryNullableInt(anoRefStr);
var item = TryNullableInt(itemStr); var item = TryNullableInt(itemStr);
if (!item.HasValue) if (!item.HasValue)
@ -127,12 +96,12 @@ public sealed class ParcelamentosImportService
{ {
LinhaExcel = row, LinhaExcel = row,
Motivo = "AnoRef inválido ou vazio.", Motivo = "AnoRef inválido ou vazio.",
Valor = GetCellString(ws, row, 3) Valor = anoRefStr
}); });
continue; continue;
} }
var qtParcelas = GetCellString(ws, row, colQtParcelas); var qtParcelas = GetCellStringValue(ws, row, colQtParcelas);
ParseParcelas(qtParcelas, out var parcelaAtual, out var totalParcelas); ParseParcelas(qtParcelas, out var parcelaAtual, out var totalParcelas);
var parcelamento = new ParcelamentoLine var parcelamento = new ParcelamentoLine
@ -140,13 +109,13 @@ public sealed class ParcelamentosImportService
AnoRef = anoRef, AnoRef = anoRef,
Item = item, Item = item,
Linha = string.IsNullOrWhiteSpace(linhaValue) ? null : linhaValue.Trim(), Linha = string.IsNullOrWhiteSpace(linhaValue) ? null : linhaValue.Trim(),
Cliente = NormalizeText(GetCellString(ws, row, colCliente)), Cliente = NormalizeText(GetCellStringValue(ws, row, colCliente)),
QtParcelas = string.IsNullOrWhiteSpace(qtParcelas) ? null : qtParcelas.Trim(), QtParcelas = string.IsNullOrWhiteSpace(qtParcelas) ? null : qtParcelas.Trim(),
ParcelaAtual = parcelaAtual, ParcelaAtual = parcelaAtual,
TotalParcelas = totalParcelas, TotalParcelas = totalParcelas,
ValorCheio = TryDecimal(GetCellString(ws, row, colValorCheio)), ValorCheio = TryDecimal(GetCellStringValue(ws, row, colValorCheio)),
Desconto = TryDecimal(GetCellString(ws, row, colDesconto)), Desconto = TryDecimal(GetCellStringValue(ws, row, colDesconto)),
ValorComDesconto = TryDecimal(GetCellString(ws, row, colValorComDesconto)), ValorComDesconto = TryDecimal(GetCellStringValue(ws, row, colValorComDesconto)),
UpdatedAt = DateTime.UtcNow UpdatedAt = DateTime.UtcNow
}; };
@ -176,7 +145,7 @@ public sealed class ParcelamentosImportService
.Where(x => x.ParcelamentoLineId == existingEntity.Id) .Where(x => x.ParcelamentoLineId == existingEntity.Id)
.ExecuteDeleteAsync(cancellationToken); .ExecuteDeleteAsync(cancellationToken);
var monthValues = BuildMonthValues(ws, headerRowIndex, row, monthColumns, yearMap, existingEntity.Id, summary); var monthValues = BuildMonthValuesFromMap(ws, row, monthMap, existingEntity.Id, summary);
if (monthValues.Count > 0) if (monthValues.Count > 0)
{ {
await _db.ParcelamentoMonthValues.AddRangeAsync(monthValues, cancellationToken); await _db.ParcelamentoMonthValues.AddRangeAsync(monthValues, cancellationToken);
@ -188,7 +157,7 @@ public sealed class ParcelamentosImportService
} }
parcelamento.CreatedAt = DateTime.UtcNow; parcelamento.CreatedAt = DateTime.UtcNow;
parcelamento.MonthValues = BuildMonthValues(ws, headerRowIndex, row, monthColumns, yearMap, parcelamento.Id, summary); parcelamento.MonthValues = BuildMonthValuesFromMap(ws, row, monthMap, parcelamento.Id, summary);
summary.Inseridos++; summary.Inseridos++;
await _db.ParcelamentoLines.AddAsync(parcelamento, cancellationToken); await _db.ParcelamentoLines.AddAsync(parcelamento, cancellationToken);
@ -201,106 +170,45 @@ public sealed class ParcelamentosImportService
private static IXLWorksheet? FindWorksheet(XLWorkbook wb) private static IXLWorksheet? FindWorksheet(XLWorkbook wb)
{ {
return wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("PARCELAMENTOS DE APARELHOS")) return wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("PARCELAMENTOS DE APARELHOS"))
?? wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("PARCELAMENTOS")); ?? wb.Worksheets.FirstOrDefault(w => NormalizeHeader(w.Name) == NormalizeHeader("PARCELAMENTOS"))
?? wb.Worksheets.FirstOrDefault(w =>
{
var normalized = NormalizeHeader(w.Name);
return normalized.Contains("PARCELAMENTO") || normalized.Contains("PARCELAMENTOS") || normalized.Contains("PARECALEMENTO");
});
} }
private static int FindHeaderRow(IXLWorksheet ws) private static List<(int Column, int Year, int Month)> BuildFixedMonthMap()
{ {
var firstRow = ws.FirstRowUsed()?.RowNumber() ?? 1; var map = new List<(int Column, int Year, int Month)>
var lastRow = Math.Min(firstRow + 20, ws.LastRowUsed()?.RowNumber() ?? firstRow); {
(11, 2025, 12)
};
for (int r = firstRow; r <= lastRow; r++) for (int month = 1; month <= 12; month++)
{ {
var row = ws.Row(r); map.Add((11 + month, 2026, month));
foreach (var cell in row.CellsUsed())
{
if (NormalizeHeader(cell.GetString()) == NormalizeHeader("LINHA"))
{
return r;
}
}
} }
return 0; for (int month = 1; month <= 6; month++)
{
map.Add((23 + month, 2027, month));
} }
private static Dictionary<int, int> BuildYearMap(IXLWorksheet ws, int yearRowIndex, IXLRow headerRow) return map;
{
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++) private static List<ParcelamentoMonthValue> BuildMonthValuesFromMap(
{
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, IXLWorksheet ws,
int headerRowIndex,
int row, int row,
List<int> monthColumns, List<(int Column, int Year, int Month)> monthMap,
Dictionary<int, int> yearMap,
Guid parcelamentoId, Guid parcelamentoId,
ParcelamentosImportSummaryDto summary) ParcelamentosImportSummaryDto summary)
{ {
var monthValues = new List<ParcelamentoMonthValue>(); var monthValues = new List<ParcelamentoMonthValue>();
foreach (var col in monthColumns) foreach (var (column, year, month) in monthMap)
{ {
if (!yearMap.TryGetValue(col, out var year)) var valueStr = GetCellStringValue(ws, row, column);
{
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); var value = TryDecimal(valueStr);
if (!value.HasValue) if (!value.HasValue)
{ {
@ -310,7 +218,7 @@ public sealed class ParcelamentosImportService
monthValues.Add(new ParcelamentoMonthValue monthValues.Add(new ParcelamentoMonthValue
{ {
ParcelamentoLineId = parcelamentoId, ParcelamentoLineId = parcelamentoId,
Competencia = new DateOnly(year, monthNumber.Value, 1), Competencia = new DateOnly(year, month, 1),
Valor = value, Valor = value,
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow
}); });
@ -320,6 +228,14 @@ public sealed class ParcelamentosImportService
return monthValues; return monthValues;
} }
private static string GetCellStringValue(IXLWorksheet ws, int row, int col)
{
if (col <= 0) return "";
var cell = ws.Cell(row, col);
if (cell.IsEmpty()) return "";
return (cell.GetValue<string>() ?? "").Trim();
}
private static void ParseParcelas(string? qtParcelas, out int? parcelaAtual, out int? totalParcelas) private static void ParseParcelas(string? qtParcelas, out int? parcelaAtual, out int? totalParcelas)
{ {
parcelaAtual = null; parcelaAtual = null;