Add Massive Import Products
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 26m25s
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 26m25s
This commit is contained in:
parent
27439cbd95
commit
369190695b
@ -1,4 +1,5 @@
|
||||
using Domain.Entities;
|
||||
using Domain.Dtos.Stock;
|
||||
using Domain.Entities;
|
||||
using Domain.Generics;
|
||||
|
||||
namespace Core.Interfaces
|
||||
@ -12,5 +13,8 @@ namespace Core.Interfaces
|
||||
Task<bool> DeleteAsync(int id);
|
||||
Task<byte[]> ExportToExcelAsync(LSProductSearchParams searchParams);
|
||||
byte[] GetImportTemplate();
|
||||
List<ProductImportPreviewDto> PreviewImportFromExcel(byte[] fileBytes);
|
||||
Task<ProductImportResultDto> ImportProductsAsync(List<ProductImportPreviewDto> items);
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,10 @@
|
||||
using Core.Interfaces;
|
||||
using Domain.Dtos.Stock;
|
||||
using Domain.Entities;
|
||||
using Domain.Generics;
|
||||
using Models.Interfaces;
|
||||
using System.Reflection;
|
||||
using Transversal.Interfaces;
|
||||
using Transversal.Services;
|
||||
|
||||
namespace Core.Services
|
||||
@ -10,10 +12,20 @@ namespace Core.Services
|
||||
public class LSProductService : ILSProductDom
|
||||
{
|
||||
private readonly IPhLSMProductRepository _repository;
|
||||
private readonly IPhLSMProductDivisionRepository _divisionRepo;
|
||||
private readonly IPhLSMUnitOfMeasureRepository _unitRepo;
|
||||
|
||||
public LSProductService(IPhLSMProductRepository repository)
|
||||
private List<string> _divisionCodes = [];
|
||||
private List<string> _unitCodes = [];
|
||||
|
||||
public LSProductService(
|
||||
IPhLSMProductRepository repository,
|
||||
IPhLSMProductDivisionRepository divisionRepo ,
|
||||
IPhLSMUnitOfMeasureRepository unitRepo)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
_divisionRepo = divisionRepo ?? throw new ArgumentNullException(nameof(divisionRepo));
|
||||
_unitRepo = unitRepo ?? throw new ArgumentNullException(nameof(unitRepo));
|
||||
}
|
||||
|
||||
public async Task<PagedResult<ELSProduct>> SearchAsync(LSProductSearchParams searchParams)
|
||||
@ -86,7 +98,44 @@ namespace Core.Services
|
||||
|
||||
return File.ReadAllBytes(path);
|
||||
}
|
||||
public List<ProductImportPreviewDto> PreviewImportFromExcel(byte[] fileBytes)
|
||||
{
|
||||
var importador = new XLSXImportBase();
|
||||
LoadReferenceCodes(); // Cargar códigos de referencia antes de procesar el archivo
|
||||
var rawItems = importador.ReadProductImport(fileBytes);
|
||||
foreach (var item in rawItems)
|
||||
{
|
||||
// Validaciones de dominio
|
||||
if (!_divisionCodes.Contains(item.DivisionCode))
|
||||
item.ErrorMessage = "División no reconocida";
|
||||
else if (!_unitCodes.Contains(item.UnitCode))
|
||||
item.ErrorMessage = "Unidad no reconocida";
|
||||
else if (item.ProductType < 1 || item.ProductType > 3)
|
||||
item.ErrorMessage = "Tipo de producto inválido";
|
||||
// Agregar validaciones extras...
|
||||
}
|
||||
|
||||
return rawItems;
|
||||
}
|
||||
private void LoadReferenceCodes()
|
||||
{
|
||||
_divisionCodes = _divisionRepo.GetAllAsync().Result.Items.Select(x => x.Code).ToList();
|
||||
_unitCodes = _unitRepo.GetAllAsync().Result.Items.Select(x => x.Code).ToList();
|
||||
}
|
||||
public async Task<ProductImportResultDto> ImportProductsAsync(List<ProductImportPreviewDto> items)
|
||||
{
|
||||
if (items == null || !items.Any())
|
||||
return new ProductImportResultDto { Inserted = 0, Skipped = 0 };
|
||||
|
||||
try
|
||||
{
|
||||
return await _repository.ImportProductsAsync(items);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var method = MethodBase.GetCurrentMethod()?.Name ?? "ImportProductsAsync";
|
||||
throw new Exception($"Error en {method}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
Domain/Dtos/Stock/ProductImportPreviewDto.cs
Normal file
18
Domain/Dtos/Stock/ProductImportPreviewDto.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace Domain.Dtos.Stock
|
||||
{
|
||||
public class ProductImportPreviewDto
|
||||
{
|
||||
public string FactoryCode { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public int ProductType { get; set; }
|
||||
public int TraceabilityType { get; set; }
|
||||
public string DivisionCode { get; set; } = string.Empty;
|
||||
public string UnitCode { get; set; } = string.Empty;
|
||||
public bool PlusProcess { get; set; }
|
||||
public string ExternalCode { get; set; } = string.Empty;
|
||||
public string? ErrorMessage { get; set; }
|
||||
public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage);
|
||||
}
|
||||
|
||||
}
|
||||
9
Domain/Dtos/Stock/ProductImportResultDto.cs
Normal file
9
Domain/Dtos/Stock/ProductImportResultDto.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Domain.Dtos.Stock
|
||||
{
|
||||
public class ProductImportResultDto
|
||||
{
|
||||
public int Inserted { get; set; }
|
||||
public int Skipped { get; set; }
|
||||
public List<string>? Errors { get; set; }
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,9 @@ namespace Models.Interfaces
|
||||
{
|
||||
Task<ELSProductDivision> CreateAsync(ELSProductDivision entity);
|
||||
Task<bool> DeleteAsync(int id);
|
||||
Task<bool> ExistsByCodeAsync(string code);
|
||||
Task<PagedResult<ELSProductDivision>> GetAllAsync(int page = 1, int pageSize = 50);
|
||||
Task<List<string>> GetAllCodesAsync();
|
||||
Task<ELSProductDivision?> GetByIdAsync(int id);
|
||||
Task<PagedResult<ELSProductDivision>> SearchAsync(string? term, int page = 1, int pageSize = 50);
|
||||
Task<bool> UpdateAsync(ELSProductDivision entity);
|
||||
|
||||
@ -1,15 +1,52 @@
|
||||
using Domain.Entities;
|
||||
using Domain.Dtos.Stock;
|
||||
using Domain.Entities;
|
||||
using Domain.Generics;
|
||||
using Models.Models;
|
||||
|
||||
namespace Models.Interfaces
|
||||
{
|
||||
public interface IPhLSMProductRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Realiza una búsqueda paginada de productos según los parámetros provistos.
|
||||
/// </summary>
|
||||
/// <param name="searchParams">Parámetros de búsqueda y paginación.</param>
|
||||
/// <returns>Página de productos que cumplen con los filtros.</returns>
|
||||
Task<PagedResult<ELSProduct>> SearchAsync(LSProductSearchParams searchParams);
|
||||
|
||||
/// <summary>
|
||||
/// Obtiene un producto por su identificador único.
|
||||
/// </summary>
|
||||
/// <param name="id">ID del producto.</param>
|
||||
/// <returns>Producto encontrado o null si no existe.</returns>
|
||||
Task<ELSProduct?> GetByIdAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Inserta una lista de productos importados. Devuelve la cantidad de insertados y los omitidos/skipped.
|
||||
/// </summary>
|
||||
/// <param name="items">Lista de productos a importar (vista previa validada).</param>
|
||||
/// <returns>Resultado de la importación con cantidades y errores.</returns>
|
||||
Task<ProductImportResultDto> ImportProductsAsync(List<ProductImportPreviewDto> items);
|
||||
|
||||
/// <summary>
|
||||
/// Crea un nuevo producto en la base de datos.
|
||||
/// </summary>
|
||||
/// <param name="entity">Entidad de producto a crear.</param>
|
||||
/// <returns>Producto creado.</returns>
|
||||
Task<ELSProduct> CreateAsync(ELSProduct entity);
|
||||
|
||||
/// <summary>
|
||||
/// Actualiza un producto existente.
|
||||
/// </summary>
|
||||
/// <param name="entity">Entidad de producto con los datos actualizados.</param>
|
||||
/// <returns>True si la actualización fue exitosa.</returns>
|
||||
Task<bool> UpdateAsync(ELSProduct entity);
|
||||
|
||||
/// <summary>
|
||||
/// Elimina un producto por su identificador único.
|
||||
/// </summary>
|
||||
/// <param name="id">ID del producto a eliminar.</param>
|
||||
/// <returns>True si la eliminación fue exitosa.</returns>
|
||||
Task<bool> DeleteAsync(int id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
16
Models/Interfaces/IPhLSMUnitOfMeasureRepository.cs
Normal file
16
Models/Interfaces/IPhLSMUnitOfMeasureRepository.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using Domain.Entities;
|
||||
using Domain.Generics;
|
||||
|
||||
namespace Models.Interfaces
|
||||
{
|
||||
public interface IPhLSMUnitOfMeasureRepository
|
||||
{
|
||||
Task<PagedResult<ELSUnitOfMeasure>> GetAllAsync(int page = 1, int pageSize = 100);
|
||||
|
||||
/// <summary>
|
||||
/// Retorna true si el código existe
|
||||
/// </summary>
|
||||
Task<bool> ExistsByCodeAsync(string code);
|
||||
Task<List<string>> GetAllCodesAsync();
|
||||
}
|
||||
}
|
||||
@ -107,5 +107,19 @@ namespace Models.Repositories.Stock
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
public async Task<List<string>> GetAllCodesAsync()
|
||||
{
|
||||
return await _context.PhLsmProductDivisions
|
||||
.Where(x => !string.IsNullOrEmpty(x.Code))
|
||||
.Select(x => x.Code!)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ExistsByCodeAsync(string code)
|
||||
{
|
||||
return await _context.PhLsmProductDivisions
|
||||
.AnyAsync(x => x.Code != null && x.Code == code);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Domain.Entities;
|
||||
using Domain.Dtos.Stock;
|
||||
using Domain.Entities;
|
||||
using Domain.Generics;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Models.Helpers;
|
||||
@ -97,5 +98,72 @@ namespace Models.Repositories
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<ProductImportResultDto> ImportProductsAsync(List<ProductImportPreviewDto> items)
|
||||
{
|
||||
// 1. Prevenir nulos/vacíos
|
||||
if (items == null || items.Count == 0)
|
||||
return new ProductImportResultDto { Inserted = 0, Skipped = 0 };
|
||||
|
||||
// 2. Obtener todos los códigos únicos de División y Unidad
|
||||
var divisionCodes = items.Select(x => x.DivisionCode).Distinct().ToList();
|
||||
var unitCodes = items.Select(x => x.UnitCode).Distinct().ToList();
|
||||
|
||||
// 3. Mapear a IDs desde la base
|
||||
var divisionMap = await _context.PhLsmProductDivisions
|
||||
.Where(d => divisionCodes.Contains(d.Code))
|
||||
.ToDictionaryAsync(d => d.Code, d => d.Id);
|
||||
|
||||
var unitMap = await _context.PhLsmUnitOfMeasures
|
||||
.Where(u => unitCodes.Contains(u.Code))
|
||||
.ToDictionaryAsync(u => u.Code, u => u.Id);
|
||||
|
||||
// 4. Armar entidades para insertar (sólo si las FK existen)
|
||||
var toInsert = new List<PhLsmProduct>();
|
||||
int skipped = 0;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
// Validaciones de existencia de Division y Unidad
|
||||
if (!divisionMap.TryGetValue(item.DivisionCode, out var divisionId)
|
||||
|| !unitMap.TryGetValue(item.UnitCode, out var unitId))
|
||||
{
|
||||
skipped++;
|
||||
continue; // Saltea el producto si alguna FK no existe
|
||||
}
|
||||
|
||||
// Armá la entidad
|
||||
var entity = new PhLsmProduct
|
||||
{
|
||||
FactoryCode = item.FactoryCode,
|
||||
Name = item.Name,
|
||||
Descripcion = item.Description,
|
||||
ProductType = item.ProductType,
|
||||
TraceabilityType = item.TraceabilityType,
|
||||
DivisionId = divisionId,
|
||||
UnitId = unitId,
|
||||
PlusProcess = item.PlusProcess,
|
||||
ExternalCode = item.ExternalCode,
|
||||
// otros campos...
|
||||
};
|
||||
|
||||
toInsert.Add(entity);
|
||||
}
|
||||
|
||||
// 5. Insertar en batch
|
||||
if (toInsert.Count > 0)
|
||||
{
|
||||
_context.PhLsmProducts.AddRange(toInsert);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// 6. Retornar resumen
|
||||
return new ProductImportResultDto
|
||||
{
|
||||
Inserted = toInsert.Count,
|
||||
Skipped = skipped
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
41
Models/Repositories/Stock/PhLSMUnitOfMeasureRepository.cs
Normal file
41
Models/Repositories/Stock/PhLSMUnitOfMeasureRepository.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using Domain.Entities;
|
||||
using Domain.Generics;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Models.Helpers;
|
||||
using Models.Interfaces;
|
||||
using Models.Models;
|
||||
|
||||
namespace Models.Repositories.Stock
|
||||
{
|
||||
public class PhLSMUnitOfMeasureRepository(PhronCareOperationsHubContext context) : IPhLSMUnitOfMeasureRepository
|
||||
{
|
||||
private readonly PhronCareOperationsHubContext _context = context;
|
||||
public async Task<PagedResult<ELSUnitOfMeasure>> GetAllAsync(int page = 1, int pageSize = 100)
|
||||
{
|
||||
var query = _context.PhLsmUnitOfMeasures.AsQueryable();
|
||||
var paged = await query.ToPagedResultAsync(page, pageSize);
|
||||
|
||||
return new PagedResult<ELSUnitOfMeasure>
|
||||
{
|
||||
Items = paged.Items.Select(EntityMapper.MapEntity<PhLsmUnitOfMeasure, ELSUnitOfMeasure>),
|
||||
TotalItems = paged.TotalItems,
|
||||
Page = paged.Page,
|
||||
PageSize = paged.PageSize
|
||||
};
|
||||
}
|
||||
public async Task<bool> ExistsByCodeAsync(string code)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
return false;
|
||||
|
||||
return await _context.PhLsmUnitOfMeasures
|
||||
.AnyAsync(x => x.Code.ToLower() == code.ToLower());
|
||||
}
|
||||
public async Task<List<string>> GetAllCodesAsync()
|
||||
{
|
||||
return await _context.PhLsmUnitOfMeasures
|
||||
.Select(x => x.Code)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Transversal/Interfaces/IXLSXImportBase.cs
Normal file
10
Transversal/Interfaces/IXLSXImportBase.cs
Normal file
@ -0,0 +1,10 @@
|
||||
// Transversal/Interfaces/IXLSXImportBase.cs
|
||||
using Domain.Dtos.Stock;
|
||||
|
||||
namespace Transversal.Interfaces
|
||||
{
|
||||
public interface IXLSXImportBase
|
||||
{
|
||||
List<ProductImportPreviewDto> ReadProductImport(byte[] fileBytes);
|
||||
}
|
||||
}
|
||||
48
Transversal/Services/XLSXImportBase.cs
Normal file
48
Transversal/Services/XLSXImportBase.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using OfficeOpenXml;
|
||||
using Domain.Dtos.Stock;
|
||||
using Transversal.Interfaces;
|
||||
|
||||
namespace Transversal.Services
|
||||
{
|
||||
public class XLSXImportBase : IXLSXImportBase
|
||||
{
|
||||
public List<ProductImportPreviewDto> ReadProductImport(byte[] fileBytes)
|
||||
{
|
||||
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
|
||||
|
||||
var result = new List<ProductImportPreviewDto>();
|
||||
|
||||
using var stream = new MemoryStream(fileBytes);
|
||||
using var package = new ExcelPackage(stream);
|
||||
|
||||
var worksheet = package.Workbook.Worksheets.FirstOrDefault();
|
||||
if (worksheet == null)
|
||||
throw new Exception("El archivo no contiene hojas válidas.");
|
||||
|
||||
int row = 2;
|
||||
while (true)
|
||||
{
|
||||
var factoryCode = worksheet.Cells[row, 1].Text?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(factoryCode)) break; // fin del archivo
|
||||
|
||||
var item = new ProductImportPreviewDto
|
||||
{
|
||||
FactoryCode = factoryCode,
|
||||
Name = worksheet.Cells[row, 2].Text?.Trim(),
|
||||
Description = worksheet.Cells[row, 3].Text?.Trim(),
|
||||
ProductType = int.TryParse(worksheet.Cells[row, 4].Text, out var pt) ? pt : 0,
|
||||
TraceabilityType = int.TryParse(worksheet.Cells[row, 5].Text, out var tt) ? tt : 0,
|
||||
DivisionCode = worksheet.Cells[row, 6].Text?.Trim(),
|
||||
UnitCode = worksheet.Cells[row, 7].Text?.Trim(),
|
||||
PlusProcess = worksheet.Cells[row, 8].Text.Trim().ToLower() == "sí" || worksheet.Cells[row, 8].Text.Trim().ToLower() == "si",
|
||||
ExternalCode = worksheet.Cells[row, 9].Text?.Trim(),
|
||||
};
|
||||
|
||||
result.Add(item);
|
||||
row++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,4 +11,8 @@
|
||||
<PackageReference Include="PuppeteerSharp" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Domain\Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Core.Interfaces;
|
||||
using Domain.Dtos.Stock;
|
||||
using Domain.Entities;
|
||||
using Domain.Generics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -73,5 +74,36 @@ namespace API.Controllers.Stock
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"plantilla_productos.xlsx");
|
||||
}
|
||||
[HttpPost("preview-import")]
|
||||
public ActionResult<List<ProductImportPreviewDto>> PreviewImport([FromForm] ProductImportFormDto dto)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
dto.File.CopyTo(ms);
|
||||
var fileBytes = ms.ToArray();
|
||||
|
||||
var preview = _service.PreviewImportFromExcel(fileBytes);
|
||||
return Ok(preview);
|
||||
}
|
||||
public class ProductImportFormDto
|
||||
{
|
||||
public IFormFile File { get; set; } = default!;
|
||||
}
|
||||
/// <summary>
|
||||
/// Importa productos desde la vista previa. Espera una lista de ProductImportPreviewDto SIN errores.
|
||||
/// </summary>
|
||||
[HttpPost("import")]
|
||||
public async Task<ActionResult<ProductImportResultDto>> ImportProducts([FromBody] List<ProductImportPreviewDto> items)
|
||||
{
|
||||
if (items == null || !items.Any())
|
||||
return BadRequest("No se enviaron productos para importar.");
|
||||
|
||||
// Opcional: validación de errores
|
||||
if (items.Any(x => x.HasError))
|
||||
return BadRequest("Hay productos con errores. Corríjalos antes de importar.");
|
||||
|
||||
var result = await _service.ImportProductsAsync(items);
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
34
phronCare.API/Helpers/FileUploadOperationFilter.cs
Normal file
34
phronCare.API/Helpers/FileUploadOperationFilter.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
public class FileUploadOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
var hasFormFile = context.MethodInfo.GetParameters()
|
||||
.Any(p => p.ParameterType == typeof(IFormFile));
|
||||
|
||||
if (!hasFormFile) return;
|
||||
|
||||
operation.RequestBody = new OpenApiRequestBody
|
||||
{
|
||||
Content = {
|
||||
["multipart/form-data"] = new OpenApiMediaType
|
||||
{
|
||||
Schema = new OpenApiSchema
|
||||
{
|
||||
Type = "object",
|
||||
Properties = {
|
||||
["file"] = new OpenApiSchema
|
||||
{
|
||||
Type = "string",
|
||||
Format = "binary"
|
||||
}
|
||||
},
|
||||
Required = { "file" }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -132,6 +132,8 @@ builder.Services.AddSwaggerGen(option =>
|
||||
new string[]{}
|
||||
}
|
||||
});
|
||||
//option.OperationFilter<FileUploadOperationFilter>();
|
||||
|
||||
});
|
||||
#endregion
|
||||
|
||||
@ -265,4 +267,6 @@ static void RepositorysAndServices(WebApplicationBuilder builder)
|
||||
|
||||
builder.Services.AddScoped<IPhLSMLookUpRepository, PhLSMLookUpRepository>();
|
||||
builder.Services.AddScoped<ILSMLookUpDom, LSMLookUpService>();
|
||||
builder.Services.AddScoped<IPhLSMUnitOfMeasureRepository, PhLSMUnitOfMeasureRepository>();
|
||||
|
||||
}
|
||||
@ -1106,6 +1106,58 @@
|
||||
],
|
||||
"ReturnTypes": []
|
||||
},
|
||||
{
|
||||
"ContainingType": "API.Controllers.Stock.LSProductController",
|
||||
"Method": "ImportProducts",
|
||||
"RelativePath": "api/LSProduct/import",
|
||||
"HttpMethod": "POST",
|
||||
"IsController": true,
|
||||
"Order": 0,
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "items",
|
||||
"Type": "System.Collections.Generic.List\u00601[[Domain.Dtos.Stock.ProductImportPreviewDto, Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]",
|
||||
"IsRequired": true
|
||||
}
|
||||
],
|
||||
"ReturnTypes": [
|
||||
{
|
||||
"Type": "Domain.Dtos.Stock.ProductImportResultDto",
|
||||
"MediaTypes": [
|
||||
"text/plain",
|
||||
"application/json",
|
||||
"text/json"
|
||||
],
|
||||
"StatusCode": 200
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ContainingType": "API.Controllers.Stock.LSProductController",
|
||||
"Method": "PreviewImport",
|
||||
"RelativePath": "api/LSProduct/preview-import",
|
||||
"HttpMethod": "POST",
|
||||
"IsController": true,
|
||||
"Order": 0,
|
||||
"Parameters": [
|
||||
{
|
||||
"Name": "File",
|
||||
"Type": "Microsoft.AspNetCore.Http.IFormFile",
|
||||
"IsRequired": false
|
||||
}
|
||||
],
|
||||
"ReturnTypes": [
|
||||
{
|
||||
"Type": "System.Collections.Generic.List\u00601[[Domain.Dtos.Stock.ProductImportPreviewDto, Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]",
|
||||
"MediaTypes": [
|
||||
"text/plain",
|
||||
"application/json",
|
||||
"text/json"
|
||||
],
|
||||
"StatusCode": 200
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ContainingType": "API.Controllers.Stock.LSProductController",
|
||||
"Method": "Search",
|
||||
|
||||
@ -591,7 +591,11 @@
|
||||
"frameworks": {
|
||||
"net8.0": {
|
||||
"targetAlias": "net8.0",
|
||||
"projectReferences": {}
|
||||
"projectReferences": {
|
||||
"C:\\Users\\maski\\source\\repos\\SaludLAB\\phronCare\\Domain\\Domain.csproj": {
|
||||
"projectPath": "C:\\Users\\maski\\source\\repos\\SaludLAB\\phronCare\\Domain\\Domain.csproj"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
|
||||
@ -3294,6 +3294,7 @@
|
||||
"type": "project",
|
||||
"framework": ".NETCoreApp,Version=v8.0",
|
||||
"dependencies": {
|
||||
"Domain": "1.0.0",
|
||||
"EPPlus": "7.7.2",
|
||||
"PuppeteerSharp": "6.0.0"
|
||||
},
|
||||
|
||||
@ -4,6 +4,72 @@
|
||||
"C:\\Users\\maski\\source\\repos\\SaludLAB\\phronCare\\phronCare.Test\\phronCare.Test.csproj": {}
|
||||
},
|
||||
"projects": {
|
||||
"C:\\Users\\maski\\source\\repos\\SaludLAB\\phronCare\\Domain\\Domain.csproj": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "C:\\Users\\maski\\source\\repos\\SaludLAB\\phronCare\\Domain\\Domain.csproj",
|
||||
"projectName": "Domain",
|
||||
"projectPath": "C:\\Users\\maski\\source\\repos\\SaludLAB\\phronCare\\Domain\\Domain.csproj",
|
||||
"packagesPath": "C:\\Users\\maski\\.nuget\\packages\\",
|
||||
"outputPath": "C:\\Users\\maski\\source\\repos\\SaludLAB\\phronCare\\Domain\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"fallbackFolders": [
|
||||
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
|
||||
],
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\maski\\AppData\\Roaming\\NuGet\\NuGet.Config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net8.0"
|
||||
],
|
||||
"sources": {
|
||||
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
|
||||
"C:\\Program Files\\dotnet\\library-packs": {},
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net8.0": {
|
||||
"targetAlias": "net8.0",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "9.0.300"
|
||||
},
|
||||
"frameworks": {
|
||||
"net8.0": {
|
||||
"targetAlias": "net8.0",
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
"net47",
|
||||
"net471",
|
||||
"net472",
|
||||
"net48",
|
||||
"net481"
|
||||
],
|
||||
"assetTargetFallback": true,
|
||||
"warn": true,
|
||||
"frameworkReferences": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"privateAssets": "all"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.300/PortableRuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"C:\\Users\\maski\\source\\repos\\SaludLAB\\phronCare\\phronCare.Test\\phronCare.Test.csproj": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
@ -132,7 +198,11 @@
|
||||
"frameworks": {
|
||||
"net8.0": {
|
||||
"targetAlias": "net8.0",
|
||||
"projectReferences": {}
|
||||
"projectReferences": {
|
||||
"C:\\Users\\maski\\source\\repos\\SaludLAB\\phronCare\\Domain\\Domain.csproj": {
|
||||
"projectPath": "C:\\Users\\maski\\source\\repos\\SaludLAB\\phronCare\\Domain\\Domain.csproj"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
|
||||
@ -911,10 +911,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Domain/1.0.0": {
|
||||
"type": "project",
|
||||
"framework": ".NETCoreApp,Version=v8.0",
|
||||
"compile": {
|
||||
"bin/placeholder/Domain.dll": {}
|
||||
},
|
||||
"runtime": {
|
||||
"bin/placeholder/Domain.dll": {}
|
||||
}
|
||||
},
|
||||
"Transversal/1.0.0": {
|
||||
"type": "project",
|
||||
"framework": ".NETCoreApp,Version=v8.0",
|
||||
"dependencies": {
|
||||
"Domain": "1.0.0",
|
||||
"EPPlus": "7.7.2",
|
||||
"PuppeteerSharp": "6.0.0"
|
||||
},
|
||||
@ -2369,6 +2380,11 @@
|
||||
"version.txt"
|
||||
]
|
||||
},
|
||||
"Domain/1.0.0": {
|
||||
"type": "project",
|
||||
"path": "../Domain/Domain.csproj",
|
||||
"msbuildProject": "../Domain/Domain.csproj"
|
||||
},
|
||||
"Transversal/1.0.0": {
|
||||
"type": "project",
|
||||
"path": "../Transversal/Transversal.csproj",
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
@page "/stock/productimport"
|
||||
@using Domain.Dtos.Stock
|
||||
@using phronCare.UIBlazor.Services.Stock
|
||||
|
||||
@inject IJSRuntime JS
|
||||
@ -22,9 +23,11 @@
|
||||
|
||||
@if (UploadedFile != null)
|
||||
{
|
||||
<div class="mb-4">
|
||||
<div class="mb-4">
|
||||
<button class="btn btn-warning" @onclick="ProcessFile">Procesar archivo Excel</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (PreviewItems != null)
|
||||
@ -64,27 +67,17 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="mt-3 d-flex justify-content-end gap-2">
|
||||
<button class="btn btn-outline-secondary" @onclick="SimulateImport">Simular importación</button>
|
||||
<button class="btn btn-success" @onclick="ConfirmImport" disabled="@(PreviewItems.All(x => x.HasError))">Importar productos</button>
|
||||
<div class="mt-3 d-flex justify-content-end">
|
||||
<button class="btn btn-success" @onclick="ConfirmImport" disabled="@(PreviewItems.Any(x => x.HasError))">
|
||||
Importar productos
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<ProductImportPreviewDto>? PreviewItems;
|
||||
private IBrowserFile? UploadedFile;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
PreviewItems = new List<ProductImportPreviewDto>
|
||||
{
|
||||
new() { FactoryCode = "ZIM001", Name = "Clavo tibial largo", Description = "Clavo para tibia", ProductType = 1, TraceabilityType = 3, DivisionCode = "ZIM", UnitCode = "UN", PlusProcess = true, ExternalCode = "EXT001" },
|
||||
new() { FactoryCode = "ZIM002", Name = "Tornillo esponjoso", Description = "Tornillo de 6.5mm", ProductType = 1, TraceabilityType = 3, DivisionCode = "ZIM", UnitCode = "UN", PlusProcess = false, ExternalCode = "EXT002" },
|
||||
new() { FactoryCode = "ZIM003", Name = "Placa de compresión", Description = "Placa DCP 4.5", ProductType = 1, TraceabilityType = 2, DivisionCode = "ZIM", UnitCode = "UN", PlusProcess = false, ExternalCode = "EXT003" },
|
||||
new() { FactoryCode = "ZIM004", Name = "Caja instrumental", Description = "Caja para implantes", ProductType = 2, TraceabilityType = 1, DivisionCode = "ZIM", UnitCode = "CJ", PlusProcess = false, ExternalCode = "EXT004", ErrorMessage = "Unidad no reconocida" },
|
||||
new() { FactoryCode = "ZIM005", Name = "Perno cortical", Description = "Perno de fijación", ProductType = 1, TraceabilityType = 3, DivisionCode = "ZIM", UnitCode = "UN", PlusProcess = true, ExternalCode = "EXT005" }
|
||||
};
|
||||
}
|
||||
private bool _isUploading;
|
||||
|
||||
private async Task DownloadTemplate()
|
||||
{
|
||||
@ -98,48 +91,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleFileSelected(InputFileChangeEventArgs e)
|
||||
private void HandleFileSelected(InputFileChangeEventArgs e)
|
||||
{
|
||||
UploadedFile = e.File;
|
||||
//PreviewItems = null; // Limpiar preview anterior
|
||||
PreviewItems = null; // limpiar vista previa anterior
|
||||
}
|
||||
|
||||
private async Task ProcessFile()
|
||||
{
|
||||
if (UploadedFile == null) return;
|
||||
|
||||
using var stream = UploadedFile.OpenReadStream(10 * 1024 * 1024);
|
||||
using var ms = new MemoryStream();
|
||||
await stream.CopyToAsync(ms);
|
||||
var content = ms.ToArray();
|
||||
|
||||
// Aquí deberías llamar al backend para validar y obtener la vista previa
|
||||
// Por ahora se simula con los datos ya cargados en OnInitialized
|
||||
// PreviewItems = await Http.PostAsJsonAsync(...)
|
||||
}
|
||||
|
||||
private async Task SimulateImport()
|
||||
try
|
||||
{
|
||||
// Lógica futura para simular importación
|
||||
_isUploading = true;
|
||||
PreviewItems = await productService.PreviewImportAsync(UploadedFile);
|
||||
toastService.ShowSuccess("Vista previa generada correctamente.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
toastService.ShowError($"Error al procesar el archivo: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isUploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConfirmImport()
|
||||
{
|
||||
// Lógica futura para guardar los productos
|
||||
if (PreviewItems is null || PreviewItems.Any(x => x.HasError))
|
||||
{
|
||||
toastService.ShowWarning("Existen errores; corríjalos antes de importar.");
|
||||
return;
|
||||
}
|
||||
|
||||
public class ProductImportPreviewDto
|
||||
try
|
||||
{
|
||||
public string FactoryCode { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public int ProductType { get; set; }
|
||||
public int TraceabilityType { get; set; }
|
||||
public string DivisionCode { get; set; } = string.Empty;
|
||||
public string UnitCode { get; set; } = string.Empty;
|
||||
public bool PlusProcess { get; set; }
|
||||
public string ExternalCode { get; set; } = string.Empty;
|
||||
public string? ErrorMessage { get; set; }
|
||||
public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage);
|
||||
var result = await productService.ConfirmImportAsync(PreviewItems);
|
||||
toastService.ShowSuccess($"Se importaron {result?.Inserted ?? 0} productos.");
|
||||
UploadedFile = null;
|
||||
PreviewItems = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
toastService.ShowError($"Error al importar: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
using Domain.Entities;
|
||||
using Domain.Dtos.Stock;
|
||||
using Domain.Entities;
|
||||
using Domain.Generics;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Net.Http.Json;
|
||||
using System.Reflection;
|
||||
@ -87,6 +89,28 @@ namespace phronCare.UIBlazor.Services.Stock
|
||||
var base64 = Convert.ToBase64String(bytes);
|
||||
await _js.InvokeVoidAsync("saveAsFile", "plantilla_productos.xlsx", base64);
|
||||
}
|
||||
public async Task<List<ProductImportPreviewDto>> PreviewImportAsync(IBrowserFile file)
|
||||
{
|
||||
using var content = new MultipartFormDataContent();
|
||||
var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024); // 10MB
|
||||
content.Add(new StreamContent(stream), "file", file.Name);
|
||||
|
||||
var response = await _http.PostAsync("api/LSProduct/preview-import", content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<List<ProductImportPreviewDto>>();
|
||||
return result ?? new();
|
||||
}
|
||||
|
||||
public async Task<ProductImportResultDto?> ConfirmImportAsync(IEnumerable<ProductImportPreviewDto> items)
|
||||
{
|
||||
// solo envía los registros válidos
|
||||
var valid = items.Where(x => !x.HasError).ToList();
|
||||
|
||||
var response = await _http.PostAsJsonAsync("api/LSProduct/import", valid);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<ProductImportResultDto>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -225,7 +225,11 @@
|
||||
"frameworks": {
|
||||
"net8.0": {
|
||||
"targetAlias": "net8.0",
|
||||
"projectReferences": {}
|
||||
"projectReferences": {
|
||||
"C:\\Users\\maski\\source\\repos\\SaludLAB\\phronCare\\Domain\\Domain.csproj": {
|
||||
"projectPath": "C:\\Users\\maski\\source\\repos\\SaludLAB\\phronCare\\Domain\\Domain.csproj"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
|
||||
@ -2323,6 +2323,7 @@
|
||||
"type": "project",
|
||||
"framework": ".NETCoreApp,Version=v8.0",
|
||||
"dependencies": {
|
||||
"Domain": "1.0.0",
|
||||
"EPPlus": "7.7.2",
|
||||
"PuppeteerSharp": "6.0.0"
|
||||
},
|
||||
@ -4658,6 +4659,7 @@
|
||||
"type": "project",
|
||||
"framework": ".NETCoreApp,Version=v8.0",
|
||||
"dependencies": {
|
||||
"Domain": "1.0.0",
|
||||
"EPPlus": "7.7.2",
|
||||
"PuppeteerSharp": "6.0.0"
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user