Add Massive Import Products
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 26m25s

This commit is contained in:
Leandro Hernan Rojas 2025-07-14 16:16:05 -03:00
parent 27439cbd95
commit 369190695b
25 changed files with 614 additions and 56 deletions

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View 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);
}
}

View 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; }
}
}

View File

@ -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);

View File

@ -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);
}
}

View 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();
}
}

View File

@ -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);
}
}
}

View File

@ -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
};
}
}
}

View 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();
}
}
}

View File

@ -0,0 +1,10 @@
// Transversal/Interfaces/IXLSXImportBase.cs
using Domain.Dtos.Stock;
namespace Transversal.Interfaces
{
public interface IXLSXImportBase
{
List<ProductImportPreviewDto> ReadProductImport(byte[] fileBytes);
}
}

View 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;
}
}
}

View File

@ -11,4 +11,8 @@
<PackageReference Include="PuppeteerSharp" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Domain\Domain.csproj" />
</ItemGroup>
</Project>

View File

@ -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);
}
}
}

View 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" }
}
}
}
};
}
}

View 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>();
}

View File

@ -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",

View File

@ -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": {

View File

@ -3294,6 +3294,7 @@
"type": "project",
"framework": ".NETCoreApp,Version=v8.0",
"dependencies": {
"Domain": "1.0.0",
"EPPlus": "7.7.2",
"PuppeteerSharp": "6.0.0"
},

View File

@ -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": {

View File

@ -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",

View File

@ -1,4 +1,5 @@
@page "/stock/productimport"
@using Domain.Dtos.Stock
@using phronCare.UIBlazor.Services.Stock
@inject IJSRuntime JS
@ -23,7 +24,9 @@
@if (UploadedFile != null)
{
<div class="mb-4">
<button class="btn btn-warning" @onclick="ProcessFile">Procesar archivo Excel</button>
<div class="mb-4">
<button class="btn btn-warning" @onclick="ProcessFile">Procesar archivo Excel</button>
</div>
</div>
}
@ -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()
{
// Lógica futura para simular importación
try
{
_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
{
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);
try
{
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}");
}
}
}

View File

@ -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>();
}
}
}

View File

@ -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": {

View File

@ -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"
},