diff --git a/Core/Interfaces/Stock/ILSProductDom.cs b/Core/Interfaces/Stock/ILSProductDom.cs index 4c76f25..d360c34 100644 --- a/Core/Interfaces/Stock/ILSProductDom.cs +++ b/Core/Interfaces/Stock/ILSProductDom.cs @@ -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 DeleteAsync(int id); Task ExportToExcelAsync(LSProductSearchParams searchParams); byte[] GetImportTemplate(); + List PreviewImportFromExcel(byte[] fileBytes); + Task ImportProductsAsync(List items); + } } \ No newline at end of file diff --git a/Core/Services/Stock/LSProductService.cs b/Core/Services/Stock/LSProductService.cs index a9cd721..d651a3b 100644 --- a/Core/Services/Stock/LSProductService.cs +++ b/Core/Services/Stock/LSProductService.cs @@ -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 _divisionCodes = []; + private List _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> SearchAsync(LSProductSearchParams searchParams) @@ -86,7 +98,44 @@ namespace Core.Services return File.ReadAllBytes(path); } + public List 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 ImportProductsAsync(List 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); + } + } } } diff --git a/Domain/Dtos/Stock/ProductImportPreviewDto.cs b/Domain/Dtos/Stock/ProductImportPreviewDto.cs new file mode 100644 index 0000000..0239da8 --- /dev/null +++ b/Domain/Dtos/Stock/ProductImportPreviewDto.cs @@ -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); + } + +} diff --git a/Domain/Dtos/Stock/ProductImportResultDto.cs b/Domain/Dtos/Stock/ProductImportResultDto.cs new file mode 100644 index 0000000..d5cf69f --- /dev/null +++ b/Domain/Dtos/Stock/ProductImportResultDto.cs @@ -0,0 +1,9 @@ +namespace Domain.Dtos.Stock +{ + public class ProductImportResultDto + { + public int Inserted { get; set; } + public int Skipped { get; set; } + public List? Errors { get; set; } + } +} diff --git a/Models/Interfaces/IPhLSMProductDivisionRepository.cs b/Models/Interfaces/IPhLSMProductDivisionRepository.cs index b2a0169..e2da817 100644 --- a/Models/Interfaces/IPhLSMProductDivisionRepository.cs +++ b/Models/Interfaces/IPhLSMProductDivisionRepository.cs @@ -7,7 +7,9 @@ namespace Models.Interfaces { Task CreateAsync(ELSProductDivision entity); Task DeleteAsync(int id); + Task ExistsByCodeAsync(string code); Task> GetAllAsync(int page = 1, int pageSize = 50); + Task> GetAllCodesAsync(); Task GetByIdAsync(int id); Task> SearchAsync(string? term, int page = 1, int pageSize = 50); Task UpdateAsync(ELSProductDivision entity); diff --git a/Models/Interfaces/IPhLSMProductRepository.cs b/Models/Interfaces/IPhLSMProductRepository.cs index f104811..e61bb56 100644 --- a/Models/Interfaces/IPhLSMProductRepository.cs +++ b/Models/Interfaces/IPhLSMProductRepository.cs @@ -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 { + /// + /// Realiza una búsqueda paginada de productos según los parámetros provistos. + /// + /// Parámetros de búsqueda y paginación. + /// Página de productos que cumplen con los filtros. Task> SearchAsync(LSProductSearchParams searchParams); + + /// + /// Obtiene un producto por su identificador único. + /// + /// ID del producto. + /// Producto encontrado o null si no existe. Task GetByIdAsync(int id); + + /// + /// Inserta una lista de productos importados. Devuelve la cantidad de insertados y los omitidos/skipped. + /// + /// Lista de productos a importar (vista previa validada). + /// Resultado de la importación con cantidades y errores. + Task ImportProductsAsync(List items); + + /// + /// Crea un nuevo producto en la base de datos. + /// + /// Entidad de producto a crear. + /// Producto creado. Task CreateAsync(ELSProduct entity); + + /// + /// Actualiza un producto existente. + /// + /// Entidad de producto con los datos actualizados. + /// True si la actualización fue exitosa. Task UpdateAsync(ELSProduct entity); + + /// + /// Elimina un producto por su identificador único. + /// + /// ID del producto a eliminar. + /// True si la eliminación fue exitosa. Task DeleteAsync(int id); } } + diff --git a/Models/Interfaces/IPhLSMUnitOfMeasureRepository.cs b/Models/Interfaces/IPhLSMUnitOfMeasureRepository.cs new file mode 100644 index 0000000..4b904a3 --- /dev/null +++ b/Models/Interfaces/IPhLSMUnitOfMeasureRepository.cs @@ -0,0 +1,16 @@ +using Domain.Entities; +using Domain.Generics; + +namespace Models.Interfaces +{ + public interface IPhLSMUnitOfMeasureRepository + { + Task> GetAllAsync(int page = 1, int pageSize = 100); + + /// + /// Retorna true si el código existe + /// + Task ExistsByCodeAsync(string code); + Task> GetAllCodesAsync(); + } +} diff --git a/Models/Repositories/Stock/PhLSMProductDivisionRepository.cs b/Models/Repositories/Stock/PhLSMProductDivisionRepository.cs index 3b9b247..ef7cfdf 100644 --- a/Models/Repositories/Stock/PhLSMProductDivisionRepository.cs +++ b/Models/Repositories/Stock/PhLSMProductDivisionRepository.cs @@ -107,5 +107,19 @@ namespace Models.Repositories.Stock await _context.SaveChangesAsync(); return true; } + public async Task> GetAllCodesAsync() + { + return await _context.PhLsmProductDivisions + .Where(x => !string.IsNullOrEmpty(x.Code)) + .Select(x => x.Code!) + .ToListAsync(); + } + + public async Task ExistsByCodeAsync(string code) + { + return await _context.PhLsmProductDivisions + .AnyAsync(x => x.Code != null && x.Code == code); + } + } } diff --git a/Models/Repositories/Stock/PhLSMProductRepository.cs b/Models/Repositories/Stock/PhLSMProductRepository.cs index b54203a..077d50c 100644 --- a/Models/Repositories/Stock/PhLSMProductRepository.cs +++ b/Models/Repositories/Stock/PhLSMProductRepository.cs @@ -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 ImportProductsAsync(List 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(); + 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 + }; + } + } } diff --git a/Models/Repositories/Stock/PhLSMUnitOfMeasureRepository.cs b/Models/Repositories/Stock/PhLSMUnitOfMeasureRepository.cs new file mode 100644 index 0000000..b59e474 --- /dev/null +++ b/Models/Repositories/Stock/PhLSMUnitOfMeasureRepository.cs @@ -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> GetAllAsync(int page = 1, int pageSize = 100) + { + var query = _context.PhLsmUnitOfMeasures.AsQueryable(); + var paged = await query.ToPagedResultAsync(page, pageSize); + + return new PagedResult + { + Items = paged.Items.Select(EntityMapper.MapEntity), + TotalItems = paged.TotalItems, + Page = paged.Page, + PageSize = paged.PageSize + }; + } + public async Task ExistsByCodeAsync(string code) + { + if (string.IsNullOrWhiteSpace(code)) + return false; + + return await _context.PhLsmUnitOfMeasures + .AnyAsync(x => x.Code.ToLower() == code.ToLower()); + } + public async Task> GetAllCodesAsync() + { + return await _context.PhLsmUnitOfMeasures + .Select(x => x.Code) + .ToListAsync(); + } + } +} diff --git a/Transversal/Interfaces/IXLSXImportBase.cs b/Transversal/Interfaces/IXLSXImportBase.cs new file mode 100644 index 0000000..8b28918 --- /dev/null +++ b/Transversal/Interfaces/IXLSXImportBase.cs @@ -0,0 +1,10 @@ +// Transversal/Interfaces/IXLSXImportBase.cs +using Domain.Dtos.Stock; + +namespace Transversal.Interfaces +{ + public interface IXLSXImportBase + { + List ReadProductImport(byte[] fileBytes); + } +} diff --git a/Transversal/Services/XLSXImportBase.cs b/Transversal/Services/XLSXImportBase.cs new file mode 100644 index 0000000..2669d38 --- /dev/null +++ b/Transversal/Services/XLSXImportBase.cs @@ -0,0 +1,48 @@ +using OfficeOpenXml; +using Domain.Dtos.Stock; +using Transversal.Interfaces; + +namespace Transversal.Services +{ + public class XLSXImportBase : IXLSXImportBase + { + public List ReadProductImport(byte[] fileBytes) + { + ExcelPackage.LicenseContext = LicenseContext.NonCommercial; + + var result = new List(); + + 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; + } + } +} diff --git a/Transversal/Transversal.csproj b/Transversal/Transversal.csproj index 8738eec..15d2cfd 100644 --- a/Transversal/Transversal.csproj +++ b/Transversal/Transversal.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/phronCare.API/Controllers/Stock/LSProductController.cs b/phronCare.API/Controllers/Stock/LSProductController.cs index 4c01c38..42df6f8 100644 --- a/phronCare.API/Controllers/Stock/LSProductController.cs +++ b/phronCare.API/Controllers/Stock/LSProductController.cs @@ -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> 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!; + } + /// + /// Importa productos desde la vista previa. Espera una lista de ProductImportPreviewDto SIN errores. + /// + [HttpPost("import")] + public async Task> ImportProducts([FromBody] List 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); + } + } } diff --git a/phronCare.API/Helpers/FileUploadOperationFilter.cs b/phronCare.API/Helpers/FileUploadOperationFilter.cs new file mode 100644 index 0000000..f4e0947 --- /dev/null +++ b/phronCare.API/Helpers/FileUploadOperationFilter.cs @@ -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" } + } + } + } + }; + } +} diff --git a/phronCare.API/Program.cs b/phronCare.API/Program.cs index f7d9f95..53c31c5 100644 --- a/phronCare.API/Program.cs +++ b/phronCare.API/Program.cs @@ -132,6 +132,8 @@ builder.Services.AddSwaggerGen(option => new string[]{} } }); + //option.OperationFilter(); + }); #endregion @@ -265,4 +267,6 @@ static void RepositorysAndServices(WebApplicationBuilder builder) builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + } \ No newline at end of file diff --git a/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json b/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json index 0351352..547faba 100644 --- a/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json +++ b/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json @@ -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", diff --git a/phronCare.API/obj/phronCare.API.csproj.nuget.dgspec.json b/phronCare.API/obj/phronCare.API.csproj.nuget.dgspec.json index f03eee1..9c5d300 100644 --- a/phronCare.API/obj/phronCare.API.csproj.nuget.dgspec.json +++ b/phronCare.API/obj/phronCare.API.csproj.nuget.dgspec.json @@ -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": { diff --git a/phronCare.API/obj/project.assets.json b/phronCare.API/obj/project.assets.json index b152054..173b8a1 100644 --- a/phronCare.API/obj/project.assets.json +++ b/phronCare.API/obj/project.assets.json @@ -3294,6 +3294,7 @@ "type": "project", "framework": ".NETCoreApp,Version=v8.0", "dependencies": { + "Domain": "1.0.0", "EPPlus": "7.7.2", "PuppeteerSharp": "6.0.0" }, diff --git a/phronCare.Test/obj/phronCare.Test.csproj.nuget.dgspec.json b/phronCare.Test/obj/phronCare.Test.csproj.nuget.dgspec.json index 53e3039..4be250c 100644 --- a/phronCare.Test/obj/phronCare.Test.csproj.nuget.dgspec.json +++ b/phronCare.Test/obj/phronCare.Test.csproj.nuget.dgspec.json @@ -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": { diff --git a/phronCare.Test/obj/project.assets.json b/phronCare.Test/obj/project.assets.json index e35d890..fe48f62 100644 --- a/phronCare.Test/obj/project.assets.json +++ b/phronCare.Test/obj/project.assets.json @@ -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", diff --git a/phronCare.UIBlazor/Pages/Stock/ProductImport.razor b/phronCare.UIBlazor/Pages/Stock/ProductImport.razor index fe1b296..9b0b2f4 100644 --- a/phronCare.UIBlazor/Pages/Stock/ProductImport.razor +++ b/phronCare.UIBlazor/Pages/Stock/ProductImport.razor @@ -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) {
- +
+ +
} @@ -64,27 +67,17 @@ -
- - +
+
} @code { private List? PreviewItems; private IBrowserFile? UploadedFile; - - protected override void OnInitialized() - { - PreviewItems = new List - { - 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}"); + } } } diff --git a/phronCare.UIBlazor/Services/Stock/LSProductService.cs b/phronCare.UIBlazor/Services/Stock/LSProductService.cs index 499b3b9..4104b2e 100644 --- a/phronCare.UIBlazor/Services/Stock/LSProductService.cs +++ b/phronCare.UIBlazor/Services/Stock/LSProductService.cs @@ -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> 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>(); + return result ?? new(); + } + + public async Task ConfirmImportAsync(IEnumerable 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(); + } + } } diff --git a/phronCare.UIBlazor/obj/phronCare.UIBlazor.csproj.nuget.dgspec.json b/phronCare.UIBlazor/obj/phronCare.UIBlazor.csproj.nuget.dgspec.json index 526c3ee..32ae6e3 100644 --- a/phronCare.UIBlazor/obj/phronCare.UIBlazor.csproj.nuget.dgspec.json +++ b/phronCare.UIBlazor/obj/phronCare.UIBlazor.csproj.nuget.dgspec.json @@ -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": { diff --git a/phronCare.UIBlazor/obj/project.assets.json b/phronCare.UIBlazor/obj/project.assets.json index 7d98c05..4e2df4f 100644 --- a/phronCare.UIBlazor/obj/project.assets.json +++ b/phronCare.UIBlazor/obj/project.assets.json @@ -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" },