Merge bde42cbe52 into 8cb0b72474
This commit is contained in:
commit
e687a2e406
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
|
|
||||||
for (int r = firstRow; r <= lastRow; r++)
|
|
||||||
{
|
{
|
||||||
var row = ws.Row(r);
|
(11, 2025, 12)
|
||||||
foreach (var cell in row.CellsUsed())
|
};
|
||||||
{
|
|
||||||
if (NormalizeHeader(cell.GetString()) == NormalizeHeader("LINHA"))
|
for (int month = 1; month <= 12; month++)
|
||||||
{
|
{
|
||||||
return r;
|
map.Add((11 + month, 2026, month));
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
for (int month = 1; month <= 6; month++)
|
||||||
|
{
|
||||||
|
map.Add((23 + month, 2027, month));
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<int, int> BuildYearMap(IXLWorksheet ws, int yearRowIndex, IXLRow headerRow)
|
private static List<ParcelamentoMonthValue> BuildMonthValuesFromMap(
|
||||||
{
|
|
||||||
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,
|
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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue