diff --git a/Core/Interfaces/Stock/ILSMLookUpDom.cs b/Core/Interfaces/Stock/ILSMLookUpDom.cs new file mode 100644 index 0000000..01e54b7 --- /dev/null +++ b/Core/Interfaces/Stock/ILSMLookUpDom.cs @@ -0,0 +1,10 @@ +using Domain.Entities; + +namespace Core.Interfaces +{ + public interface ILSMLookUpDom + { + Task> ProductDivisionsListAsync(string filter, int limit = 10); + Task> UnitsOfMeasureListAsync(string filter, int limit = 10); + } +} diff --git a/Core/Interfaces/Stock/ILSProductDivisionDom.cs b/Core/Interfaces/Stock/ILSProductDivisionDom.cs new file mode 100644 index 0000000..86b37c9 --- /dev/null +++ b/Core/Interfaces/Stock/ILSProductDivisionDom.cs @@ -0,0 +1,15 @@ +using Domain.Entities; +using Domain.Generics; + +namespace Core.Interfaces.Stock +{ + public interface ILSProductDivisionDom + { + Task> GetAllAsync(int page = 1, int pageSize = 50); + Task GetByIdAsync(int id); + Task> SearchAsync(string? term, int page = 1, int pageSize = 50); + Task CreateAsync(ELSProductDivision entity); + Task UpdateAsync(ELSProductDivision entity); + Task DeleteAsync(int id); + } +} diff --git a/Core/Interfaces/Stock/ILSProductDom.cs b/Core/Interfaces/Stock/ILSProductDom.cs new file mode 100644 index 0000000..bba90cd --- /dev/null +++ b/Core/Interfaces/Stock/ILSProductDom.cs @@ -0,0 +1,15 @@ +using Domain.Entities; +using Domain.Generics; + +namespace Core.Interfaces +{ + public interface ILSProductDom + { + Task> SearchAsync(LSProductSearchParams searchParams); + Task GetByIdAsync(int id); + Task CreateAsync(ELSProduct entity); + Task UpdateAsync(ELSProduct entity); + Task DeleteAsync(int id); + Task ExportToExcelAsync(LSProductSearchParams searchParams); + } +} \ No newline at end of file diff --git a/Core/Interfaces/Stock/IProductDivisionDom.cs b/Core/Interfaces/Stock/IProductDivisionDom.cs deleted file mode 100644 index 2142a04..0000000 --- a/Core/Interfaces/Stock/IProductDivisionDom.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Domain.Entities; -using Domain.Generics; - -namespace Core.Interfaces.Stock -{ - public interface IProductDivisionDom - { - Task> GetAllAsync(int page = 1, int pageSize = 50); - Task GetByIdAsync(int id); - Task> SearchAsync(string? term, int page = 1, int pageSize = 50); - Task CreateAsync(EProductDivision entity); - Task UpdateAsync(EProductDivision entity); - Task DeleteAsync(int id); - } -} diff --git a/Core/Services/Stock/LSMLookUpService.cs b/Core/Services/Stock/LSMLookUpService.cs new file mode 100644 index 0000000..92493a5 --- /dev/null +++ b/Core/Services/Stock/LSMLookUpService.cs @@ -0,0 +1,26 @@ +using Core.Interfaces; +using Domain.Entities; +using Models.Interfaces; + +namespace Core.Services +{ + public class LSMLookUpService : ILSMLookUpDom + { + #region Declaraciones y Constructor + private readonly IPhLSMLookUpRepository _repository; + + public LSMLookUpService(IPhLSMLookUpRepository repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + #endregion + + #region Métodos de clase + public Task> ProductDivisionsListAsync(string filter, int limit = 10) + => _repository.ProductDivisionsAsync(filter, limit); + + public Task> UnitsOfMeasureListAsync(string filter, int limit = 10) + => _repository.UnitsOfMeasureAsync(filter, limit); + #endregion + } +} \ No newline at end of file diff --git a/Core/Services/Stock/ProductDivisionService.cs b/Core/Services/Stock/LSProductDivisionService.cs similarity index 81% rename from Core/Services/Stock/ProductDivisionService.cs rename to Core/Services/Stock/LSProductDivisionService.cs index 97bc1cc..c50fbab 100644 --- a/Core/Services/Stock/ProductDivisionService.cs +++ b/Core/Services/Stock/LSProductDivisionService.cs @@ -7,18 +7,18 @@ using System.Reflection; namespace Core.Services.Stock { - public class ProductDivisionService : IProductDivisionDom + public class LSProductDivisionService : ILSProductDivisionDom { #region Declaraciones y Constructor private readonly IPhLSMProductDivisionRepository _repository; - public ProductDivisionService(IPhLSMProductDivisionRepository repository) + public LSProductDivisionService(IPhLSMProductDivisionRepository repository) { _repository = repository ?? throw new ArgumentNullException(nameof(repository)); } #endregion #region Métodos de clase - public async Task> GetAllAsync(int page = 1, int pageSize = 50) + public async Task> GetAllAsync(int page = 1, int pageSize = 50) { try { @@ -31,7 +31,7 @@ namespace Core.Services.Stock } } - public async Task GetByIdAsync(int id) + public async Task GetByIdAsync(int id) { try { @@ -44,7 +44,7 @@ namespace Core.Services.Stock } } - public async Task> SearchAsync(string? term, int page = 1, int pageSize = 50) + public async Task> SearchAsync(string? term, int page = 1, int pageSize = 50) { try { @@ -57,7 +57,7 @@ namespace Core.Services.Stock } } - public async Task CreateAsync(EProductDivision entity) + public async Task CreateAsync(ELSProductDivision entity) { try { @@ -70,7 +70,7 @@ namespace Core.Services.Stock } } - public async Task UpdateAsync(EProductDivision entity) + public async Task UpdateAsync(ELSProductDivision entity) { try { diff --git a/Core/Services/Stock/LSProductService.cs b/Core/Services/Stock/LSProductService.cs new file mode 100644 index 0000000..335456b --- /dev/null +++ b/Core/Services/Stock/LSProductService.cs @@ -0,0 +1,81 @@ +using Core.Interfaces; +using Domain.Entities; +using Domain.Generics; +using Models.Interfaces; +using System.Reflection; +using Transversal.Services; + +namespace Core.Services +{ + public class LSProductService : ILSProductDom + { + private readonly IPhLSMProductRepository _repository; + + public LSProductService(IPhLSMProductRepository repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + public async Task> SearchAsync(LSProductSearchParams searchParams) + { + return await _repository.SearchAsync(searchParams); + } + + public async Task GetByIdAsync(int id) + { + return await _repository.GetByIdAsync(id); + } + + public async Task CreateAsync(ELSProduct entity) + { + return await _repository.CreateAsync(entity); + } + + public async Task UpdateAsync(ELSProduct entity) + { + return await _repository.UpdateAsync(entity); + } + + public async Task DeleteAsync(int id) + { + return await _repository.DeleteAsync(id); + } + + public async Task ExportToExcelAsync(LSProductSearchParams searchParams) + { + try + { + var result = await _repository.SearchAsync(searchParams); + if (result?.Items == null || !result.Items.Any()) + throw new Exception("No se encontraron productos para exportar."); + + var exportador = new XLSXExportBase(); + + var exportData = result.Items.Select(p => new + { + p.Id, + p.FactoryCode, + p.ExternalCode, + p.Name, + p.Descripcion, + Tipo = p.ProductType == 1 ? "Implantable" : + p.ProductType == 2 ? "Instrumental" : + p.ProductType == 3 ? "Inyectable" : "Otro", + Trazabilidad = p.TraceabilityType == 1 ? "No aplica" : + p.TraceabilityType == 2 ? "Por cantidad" : + p.TraceabilityType == 3 ? "Por lote y vencimiento" : "Otro", + PlusProcess = p.PlusProcess ? "Sí" : "No", + División = p.Division?.Name ?? "-", + Unidad = p.Unit?.Name ?? "-" + }).ToList(); + + return exportador.ExportExcel(exportData); + } + catch (Exception ex) + { + var method = MethodBase.GetCurrentMethod()?.Name ?? "ExportToExcel"; + throw new Exception($"Error en {method}: {ex.Message}", ex); + } + } + } +} diff --git a/Domain/Entities/ELSProduct.cs b/Domain/Entities/ELSProduct.cs new file mode 100644 index 0000000..3ef56ac --- /dev/null +++ b/Domain/Entities/ELSProduct.cs @@ -0,0 +1,59 @@ +namespace Domain.Entities +{ + public partial class ELSProduct + { + /// + /// Identificador único del producto médico o industrial + /// + public int Id { get; set; } + + /// + /// Código de producto definido por la fábrica o fabricante. Puede variar según proveedor, presentación o país de origen. + /// + public string FactoryCode { get; set; } = string.Empty; + + /// + /// Nombre técnico o estandarizado del producto (ej: ficha técnica, fabricante) + /// + public string? Name { get; set; } = string.Empty; + + /// + /// Descripción comercial o práctica del producto (impresión logística, uso cotidiano) + /// + public string Descripcion { get; set; } = string.Empty; + + /// + /// Tipo de producto: 1=Implantable, 2=Instrumental, 3=Inyectable, etc. + /// + public int ProductType { get; set; } + + /// + /// Tipo de trazabilidad: 1=No aplica, 2=Por cantidad, 3=Por lote y vencimiento + /// + public int TraceabilityType { get; set; } + + /// + /// Indica si el producto requiere un proceso adicional previo a su uso (ej: esterilización, calibración, limpieza, inspección, etc.) + /// + public bool PlusProcess { get; set; } + + /// + /// Código externo estándar del producto (ej: GTIN, código de proveedor, catálogo EAN, etc.) + /// + public string? ExternalCode { get; set; } = string.Empty; + + /// + /// División o familia técnica del producto (ej: columna, trauma, descartables, etc.) + /// + public int? DivisionId { get; set; } + + /// + /// Unidad de medida base del producto (ej: unidad, mililitro, metro) + /// + public int UnitId { get; set; } + + public virtual ELSProductDivision? Division { get; set; } + + public virtual ELSUnitOfMeasure Unit { get; set; } = null!; + } +} \ No newline at end of file diff --git a/Domain/Entities/EProductDivision.cs b/Domain/Entities/ELSProductDivision.cs similarity index 95% rename from Domain/Entities/EProductDivision.cs rename to Domain/Entities/ELSProductDivision.cs index 1115591..593b797 100644 --- a/Domain/Entities/EProductDivision.cs +++ b/Domain/Entities/ELSProductDivision.cs @@ -1,6 +1,6 @@ namespace Domain.Entities { - public class EProductDivision + public class ELSProductDivision { /// /// Identificador único de la división de productos diff --git a/Domain/Entities/ELSUnitOfMeasure.cs b/Domain/Entities/ELSUnitOfMeasure.cs new file mode 100644 index 0000000..1292246 --- /dev/null +++ b/Domain/Entities/ELSUnitOfMeasure.cs @@ -0,0 +1,23 @@ +namespace Domain.Entities +{ + public class ELSUnitOfMeasure + { + public int Id { get; set; } + + /// + /// Código abreviado de unidad de medida (ej: UN, ML, MT) + /// + public string Code { get; set; } = null!; + + /// + /// Nombre descriptivo de la unidad de medida + /// + public string Name { get; set; } = null!; + + /// + /// Descripción extendida o notas adicionales sobre la unidad + /// + public string? Description { get; set; } + + } +} diff --git a/Domain/Generics/LSProductSearchParams.cs b/Domain/Generics/LSProductSearchParams.cs new file mode 100644 index 0000000..8c20821 --- /dev/null +++ b/Domain/Generics/LSProductSearchParams.cs @@ -0,0 +1,41 @@ +namespace Domain.Generics +{ + public class LSProductSearchParams : PagedRequest + { + /// + /// Código interno o externo del producto (factory_code o external_code) + /// + public string? Code { get; set; } + + /// + /// Nombre técnico o descripción comercial (name o descripcion) + /// + public string? Description { get; set; } + + /// + /// Tipo de producto: 1=Implantable, 2=Instrumental, 3=Inyectable + /// + public int? ProductType { get; set; } + + /// + /// Tipo de trazabilidad: 1=No aplica, 2=Por cantidad, 3=Por lote y vencimiento + /// + public int? TraceabilityType { get; set; } + + /// + /// División técnica del producto (FK a ProductDivision) + /// + public int? DivisionId { get; set; } + + /// + /// Unidad de medida base del producto (FK a UnitOfMeasure) + /// + public int? UnitId { get; set; } + + /// + /// Indica si el producto requiere un proceso adicional como esterilización + /// + public bool? PlusProcess { get; set; } + } +} + diff --git a/Models/Interfaces/IPhLSMLookUpRepository.cs b/Models/Interfaces/IPhLSMLookUpRepository.cs new file mode 100644 index 0000000..294619d --- /dev/null +++ b/Models/Interfaces/IPhLSMLookUpRepository.cs @@ -0,0 +1,10 @@ +using Domain.Entities; + +namespace Models.Interfaces +{ + public interface IPhLSMLookUpRepository + { + Task> ProductDivisionsAsync(string filter, int limit = 10); + Task> UnitsOfMeasureAsync(string filter, int limit = 10); + } +} diff --git a/Models/Interfaces/IPhLSMProductDivisionRepository.cs b/Models/Interfaces/IPhLSMProductDivisionRepository.cs index 4c5a886..b2a0169 100644 --- a/Models/Interfaces/IPhLSMProductDivisionRepository.cs +++ b/Models/Interfaces/IPhLSMProductDivisionRepository.cs @@ -5,11 +5,11 @@ namespace Models.Interfaces { public interface IPhLSMProductDivisionRepository { - Task CreateAsync(EProductDivision entity); + Task CreateAsync(ELSProductDivision entity); Task DeleteAsync(int id); - Task> GetAllAsync(int page = 1, int pageSize = 50); - Task GetByIdAsync(int id); - Task> SearchAsync(string? term, int page = 1, int pageSize = 50); - Task UpdateAsync(EProductDivision entity); + Task> GetAllAsync(int page = 1, int pageSize = 50); + 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 new file mode 100644 index 0000000..f104811 --- /dev/null +++ b/Models/Interfaces/IPhLSMProductRepository.cs @@ -0,0 +1,15 @@ +using Domain.Entities; +using Domain.Generics; +using Models.Models; + +namespace Models.Interfaces +{ + public interface IPhLSMProductRepository + { + Task> SearchAsync(LSProductSearchParams searchParams); + Task GetByIdAsync(int id); + Task CreateAsync(ELSProduct entity); + Task UpdateAsync(ELSProduct entity); + Task DeleteAsync(int id); + } +} diff --git a/Models/Repositories/Stock/PhLSMLookUpRepository.cs b/Models/Repositories/Stock/PhLSMLookUpRepository.cs new file mode 100644 index 0000000..569ebbe --- /dev/null +++ b/Models/Repositories/Stock/PhLSMLookUpRepository.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore; +using Domain.Entities; +using Models.Interfaces; +using Models.Models; + +namespace Models.Repositories +{ + public class PhLSMLookUpRepository(PhronCareOperationsHubContext context) : IPhLSMLookUpRepository + { + private readonly PhronCareOperationsHubContext _context = context; + + public async Task> ProductDivisionsAsync(string filter, int limit = 10) + => await _context.PhLsmProductDivisions + .Where(d => + d.Code.Contains(filter) || + d.Name.Contains(filter) || + d.Description.Contains(filter)) + .OrderBy(d => d.Code) + .Select(d => new ELookUpItem + { + Id = d.Id, + Nombre = d.Code + " | " + d.Name + }) + .Take(limit) + .ToListAsync(); + + public async Task> UnitsOfMeasureAsync(string filter, int limit = 10) + => await _context.PhLsmUnitOfMeasures + .Where(u => u.Code.Contains(filter) || u.Name.Contains(filter)) + .OrderBy(u => u.Code) + .Select(u => new ELookUpItem + { + Id = u.Id, + Nombre = u.Code + " | " + u.Name + }) + .Take(limit) + .ToListAsync(); + } +} diff --git a/Models/Repositories/Stock/PhLSMProductDivisionRepository.cs b/Models/Repositories/Stock/PhLSMProductDivisionRepository.cs index 059b99f..3b9b247 100644 --- a/Models/Repositories/Stock/PhLSMProductDivisionRepository.cs +++ b/Models/Repositories/Stock/PhLSMProductDivisionRepository.cs @@ -11,28 +11,28 @@ namespace Models.Repositories.Stock { private readonly PhronCareOperationsHubContext _context = context; - public async Task> GetAllAsync(int page = 1, int pageSize = 50) + public async Task> GetAllAsync(int page = 1, int pageSize = 50) { var query = _context.PhLsmProductDivisions.AsQueryable(); var pagedEntities = await query.ToPagedResultAsync(page, pageSize); - return new PagedResult + return new PagedResult { - Items = pagedEntities.Items.Select(EntityMapper.MapEntity), + Items = pagedEntities.Items.Select(EntityMapper.MapEntity), TotalItems = pagedEntities.TotalItems, Page = pagedEntities.Page, PageSize = pagedEntities.PageSize }; } - public async Task GetByIdAsync(int id) + public async Task GetByIdAsync(int id) { var entity = await _context.PhLsmProductDivisions.FirstOrDefaultAsync(x => x.Id == id); - return entity is null ? null : EntityMapper.MapEntity(entity); + return entity is null ? null : EntityMapper.MapEntity(entity); } - public async Task> SearchAsync(string? term, int page = 1, int pageSize = 50) + public async Task> SearchAsync(string? term, int page = 1, int pageSize = 50) { var query = _context.PhLsmProductDivisions.AsQueryable(); @@ -47,26 +47,26 @@ namespace Models.Repositories.Stock } var pagedEntities = await query.ToPagedResultAsync(page, pageSize); - return new PagedResult + return new PagedResult { - Items = pagedEntities.Items.Select(EntityMapper.MapEntity), + Items = pagedEntities.Items.Select(EntityMapper.MapEntity), TotalItems = pagedEntities.TotalItems, Page = pagedEntities.Page, PageSize = pagedEntities.PageSize }; } - public async Task CreateAsync(EProductDivision entity) + public async Task CreateAsync(ELSProductDivision entity) { if (entity == null) throw new ArgumentNullException(nameof(entity)); try { - var mapped = EntityMapper.MapEntity(entity); + var mapped = EntityMapper.MapEntity(entity); _context.PhLsmProductDivisions.Add(mapped); await _context.SaveChangesAsync(); - return EntityMapper.MapEntity(mapped); + return EntityMapper.MapEntity(mapped); } catch (DbUpdateException dbEx) { @@ -78,7 +78,7 @@ namespace Models.Repositories.Stock } } - public async Task UpdateAsync(EProductDivision entity) + public async Task UpdateAsync(ELSProductDivision entity) { if (entity == null) throw new ArgumentNullException(nameof(entity)); diff --git a/Models/Repositories/Stock/PhLSMProductRepository.cs b/Models/Repositories/Stock/PhLSMProductRepository.cs new file mode 100644 index 0000000..b54203a --- /dev/null +++ b/Models/Repositories/Stock/PhLSMProductRepository.cs @@ -0,0 +1,101 @@ +using Domain.Entities; +using Domain.Generics; +using Microsoft.EntityFrameworkCore; +using Models.Helpers; +using Models.Interfaces; +using Models.Models; + +namespace Models.Repositories +{ + public class PhLSMProductRepository(PhronCareOperationsHubContext context) : IPhLSMProductRepository + { + private readonly PhronCareOperationsHubContext _context = context; + + public async Task> SearchAsync(LSProductSearchParams searchParams) + { + var query = _context.PhLsmProducts + .Include(p => p.Division) + .Include(p => p.Unit) + .AsQueryable(); + + if (!string.IsNullOrWhiteSpace(searchParams.Code)) + { + var lowered = searchParams.Code.ToLower(); + query = query.Where(p => + (!string.IsNullOrEmpty(p.FactoryCode) && p.FactoryCode.ToLower().Contains(lowered)) || + (!string.IsNullOrEmpty(p.ExternalCode) && p.ExternalCode.ToLower().Contains(lowered))); + } + + if (!string.IsNullOrWhiteSpace(searchParams.Description)) + { + var lowered = searchParams.Description.ToLower(); + query = query.Where(p => + (!string.IsNullOrEmpty(p.Name) && p.Name.ToLower().Contains(lowered)) || + (!string.IsNullOrEmpty(p.Descripcion) && p.Descripcion.ToLower().Contains(lowered))); + } + + if (searchParams.ProductType.HasValue) + query = query.Where(p => p.ProductType == searchParams.ProductType); + + if (searchParams.TraceabilityType.HasValue) + query = query.Where(p => p.TraceabilityType == searchParams.TraceabilityType); + + if (searchParams.DivisionId.HasValue) + query = query.Where(p => p.DivisionId == searchParams.DivisionId); + + if (searchParams.UnitId.HasValue) + query = query.Where(p => p.UnitId == searchParams.UnitId); + + if (searchParams.PlusProcess.HasValue) + query = query.Where(p => p.PlusProcess == searchParams.PlusProcess); + + var paged = await query.ToPagedResultAsync(searchParams.Page, searchParams.PageSize); + + return new PagedResult + { + Items = paged.Items.Select(EntityMapper.MapEntity), + TotalItems = paged.TotalItems, + Page = paged.Page, + PageSize = paged.PageSize + }; + } + + public async Task GetByIdAsync(int id) + { + var entity = await _context.PhLsmProducts + .Include(p => p.Division) + .Include(p => p.Unit) + .FirstOrDefaultAsync(p => p.Id == id); + + return entity != null ? EntityMapper.MapEntity(entity) : null; + } + + public async Task CreateAsync(ELSProduct entity) + { + var mapped = EntityMapper.MapEntity(entity); + _context.PhLsmProducts.Add(mapped); + await _context.SaveChangesAsync(); + return EntityMapper.MapEntity(mapped); + } + + public async Task UpdateAsync(ELSProduct entity) + { + var existing = await _context.PhLsmProducts.FindAsync(entity.Id); + if (existing == null) return false; + + EntityMapper.MapEntityToExisting(entity, existing); + await _context.SaveChangesAsync(); + return true; + } + + public async Task DeleteAsync(int id) + { + var entity = await _context.PhLsmProducts.FindAsync(id); + if (entity == null) return false; + + _context.PhLsmProducts.Remove(entity); + await _context.SaveChangesAsync(); + return true; + } + } +} diff --git a/phronCare.API/Controllers/Stock/LSMLookUpController.cs b/phronCare.API/Controllers/Stock/LSMLookUpController.cs new file mode 100644 index 0000000..7497dc6 --- /dev/null +++ b/phronCare.API/Controllers/Stock/LSMLookUpController.cs @@ -0,0 +1,22 @@ +using Core.Interfaces; +using Domain.Entities; +using Microsoft.AspNetCore.Mvc; + +namespace phronCare.API.Controllers.Stock +{ + [Route("api/[controller]")] + [ApiController] + public class LSMLookUpController : ControllerBase + { + private readonly ILSMLookUpDom _lookup; + public LSMLookUpController(ILSMLookUpDom lookup) => _lookup = lookup; + + [HttpGet("productdivisions")] + public Task> ProductDivisions([FromQuery] string q = "") + => _lookup.ProductDivisionsListAsync(q); + + [HttpGet("units")] + public Task> Units([FromQuery] string q = "") + => _lookup.UnitsOfMeasureListAsync(q); + } +} diff --git a/phronCare.API/Controllers/Stock/LSProductController.cs b/phronCare.API/Controllers/Stock/LSProductController.cs new file mode 100644 index 0000000..ccf762d --- /dev/null +++ b/phronCare.API/Controllers/Stock/LSProductController.cs @@ -0,0 +1,68 @@ +using Core.Interfaces; +using Domain.Entities; +using Domain.Generics; +using Microsoft.AspNetCore.Mvc; + +namespace API.Controllers.Stock +{ + [Route("api/[controller]")] + [ApiController] + public class LSProductController : ControllerBase + { + private readonly ILSProductDom _service; + + public LSProductController(ILSProductDom service) + { + _service = service; + } + + [HttpPost("search")] + public async Task>> Search([FromBody] LSProductSearchParams searchParams) + { + var result = await _service.SearchAsync(searchParams); + return Ok(result); + } + + [HttpGet("{id}")] + public async Task> GetById(int id) + { + var product = await _service.GetByIdAsync(id); + if (product == null) + return NotFound($"No se encontró el producto con ID {id}."); + return Ok(product); + } + + [HttpPost("create")] + public async Task> Create([FromBody] ELSProduct model) + { + var created = await _service.CreateAsync(model); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + + [HttpPut("update")] + public async Task Update([FromBody] ELSProduct model) + { + var success = await _service.UpdateAsync(model); + if (!success) + return NotFound("No se pudo actualizar el producto."); + return Ok(); + } + + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var success = await _service.DeleteAsync(id); + if (!success) + return NotFound("No se pudo eliminar el producto."); + return Ok(); + } + + [HttpPost("exportfiltered")] + public async Task ExportFiltered([FromBody] LSProductSearchParams searchParams) + { + var content = await _service.ExportToExcelAsync(searchParams); + var fileName = $"productos_{DateTime.Now:yyyyMMddHHmm}.xlsx"; + return File(content, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName); + } + } +} diff --git a/phronCare.API/Controllers/Stock/ProductDivisionController.cs b/phronCare.API/Controllers/Stock/ProductDivisionController.cs index 41d8685..c8f1fa7 100644 --- a/phronCare.API/Controllers/Stock/ProductDivisionController.cs +++ b/phronCare.API/Controllers/Stock/ProductDivisionController.cs @@ -8,9 +8,9 @@ namespace phronCare.API.Controllers.Stock [ApiController] public class ProductDivisionController : ControllerBase { - private readonly IProductDivisionDom _service; + private readonly ILSProductDivisionDom _service; - public ProductDivisionController(IProductDivisionDom service) + public ProductDivisionController(ILSProductDivisionDom service) { _service = service ?? throw new ArgumentNullException(nameof(service)); } @@ -54,7 +54,7 @@ namespace phronCare.API.Controllers.Stock } [HttpPost("Create")] - public async Task Create([FromBody] EProductDivision division) + public async Task Create([FromBody] ELSProductDivision division) { try { @@ -68,7 +68,7 @@ namespace phronCare.API.Controllers.Stock } [HttpPut("Update")] - public async Task Update([FromBody] EProductDivision division) + public async Task Update([FromBody] ELSProductDivision division) { try { diff --git a/phronCare.API/Program.cs b/phronCare.API/Program.cs index cda0872..f7d9f95 100644 --- a/phronCare.API/Program.cs +++ b/phronCare.API/Program.cs @@ -257,7 +257,12 @@ static void RepositorysAndServices(WebApplicationBuilder builder) client.BaseAddress = new Uri("https://api.bcra.gob.ar/"); }); //Core de Divisiones de Productos - builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + 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 beef607..89e1f28 100644 --- a/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json +++ b/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json @@ -960,6 +960,184 @@ } ] }, + { + "ContainingType": "phronCare.API.Controllers.Stock.LSMLookUpController", + "Method": "ProductDivisions", + "RelativePath": "api/LSMLookUp/productdivisions", + "HttpMethod": "GET", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "q", + "Type": "System.String", + "IsRequired": false + } + ], + "ReturnTypes": [ + { + "Type": "System.Collections.Generic.IEnumerable\u00601[[Domain.Entities.ELookUpItem, Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]", + "MediaTypes": [ + "text/plain", + "application/json", + "text/json" + ], + "StatusCode": 200 + } + ] + }, + { + "ContainingType": "phronCare.API.Controllers.Stock.LSMLookUpController", + "Method": "Units", + "RelativePath": "api/LSMLookUp/units", + "HttpMethod": "GET", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "q", + "Type": "System.String", + "IsRequired": false + } + ], + "ReturnTypes": [ + { + "Type": "System.Collections.Generic.IEnumerable\u00601[[Domain.Entities.ELookUpItem, 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": "GetById", + "RelativePath": "api/LSProduct/{id}", + "HttpMethod": "GET", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "id", + "Type": "System.Int32", + "IsRequired": true + } + ], + "ReturnTypes": [ + { + "Type": "Domain.Entities.ELSProduct", + "MediaTypes": [ + "text/plain", + "application/json", + "text/json" + ], + "StatusCode": 200 + } + ] + }, + { + "ContainingType": "API.Controllers.Stock.LSProductController", + "Method": "Delete", + "RelativePath": "api/LSProduct/{id}", + "HttpMethod": "DELETE", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "id", + "Type": "System.Int32", + "IsRequired": true + } + ], + "ReturnTypes": [] + }, + { + "ContainingType": "API.Controllers.Stock.LSProductController", + "Method": "Create", + "RelativePath": "api/LSProduct/create", + "HttpMethod": "POST", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "model", + "Type": "Domain.Entities.ELSProduct", + "IsRequired": true + } + ], + "ReturnTypes": [ + { + "Type": "Domain.Entities.ELSProduct", + "MediaTypes": [ + "text/plain", + "application/json", + "text/json" + ], + "StatusCode": 200 + } + ] + }, + { + "ContainingType": "API.Controllers.Stock.LSProductController", + "Method": "ExportFiltered", + "RelativePath": "api/LSProduct/exportfiltered", + "HttpMethod": "POST", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "searchParams", + "Type": "Domain.Generics.LSProductSearchParams", + "IsRequired": true + } + ], + "ReturnTypes": [] + }, + { + "ContainingType": "API.Controllers.Stock.LSProductController", + "Method": "Search", + "RelativePath": "api/LSProduct/search", + "HttpMethod": "POST", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "searchParams", + "Type": "Domain.Generics.LSProductSearchParams", + "IsRequired": true + } + ], + "ReturnTypes": [ + { + "Type": "Domain.Generics.PagedResult\u00601[[Domain.Entities.ELSProduct, 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": "Update", + "RelativePath": "api/LSProduct/update", + "HttpMethod": "PUT", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "model", + "Type": "Domain.Entities.ELSProduct", + "IsRequired": true + } + ], + "ReturnTypes": [] + }, { "ContainingType": "phronCare.API.Controllers.Sales.PatientController", "Method": "GetById", @@ -1459,7 +1637,7 @@ "Parameters": [ { "Name": "division", - "Type": "Domain.Entities.EProductDivision", + "Type": "Domain.Entities.ELSProductDivision", "IsRequired": true } ], @@ -1554,7 +1732,7 @@ "Parameters": [ { "Name": "division", - "Type": "Domain.Entities.EProductDivision", + "Type": "Domain.Entities.ELSProductDivision", "IsRequired": true } ], diff --git a/phronCare.UIBlazor/Pages/Stock/LSProduct.razor b/phronCare.UIBlazor/Pages/Stock/LSProduct.razor new file mode 100644 index 0000000..6f14979 --- /dev/null +++ b/phronCare.UIBlazor/Pages/Stock/LSProduct.razor @@ -0,0 +1,255 @@ +@page "/stock/products" +@using Blazored.Typeahead +@using phronCare.UIBlazor.Services.Stock +@using phronCare.UIBlazor.Services.Lookups +@using Domain.Entities +@using Domain.Generics +@inject IToastService toastService +@inject NavigationManager Navigation +@inject LSProductService productService +@inject IStockLookUpService lookUpService + +
+
+

Catálogo de Productos Médicos

+
+
+ +
+
+ + +
+
+ + +
+
+ + + @item.Nombre + @item.Nombre + +
+
+ + + @item.Nombre + @item.Nombre + +
+
+ + + + + + + + +
+
+ + + + + + + + +
+
+
+ + +
+
+
+ +
+ + + + +
+
+
+ @if (TablaProductos != null && TablaProductos.Any()) + { + + } + else + { +

No hay resultados.

+ } +
+
+ + +
+ +@code { + private LSProductSearchParams SearchParams = new() { Page = 1, PageSize = 10 }; + private List> TablaProductos = new(); + private PagedResult? PagedResult; + private int PaginaDeseada = 1; + private ELookUpItem? _selectedDivision; + private ELookUpItem? _selectedUnit; + List botones = new(); + + private List TableColumns = new() + { + "Id", "Código Fábrica", "Código Externo", "Nombre", "Descripción", "División", "Unidad", "Tipo", "Trazabilidad", "Esteriliza" + }; + + protected override async Task OnInitializedAsync() + { + botones = new List + { + new PhTable.ButtonOptions + { + Caption = "Editar", + ElementClass = "btn btn-primary btn-sm", + UrlAction = "/stock/productform/", + OnClickAction = async (id) => + { + if (int.TryParse(id, out var pid)) + Navigation.NavigateTo($"/stock/productform/{pid}"); + } + } + }; + + // await Buscar(); + } + + private async Task Buscar() + { + SearchParams.PageSize = 13; + SearchParams.Page = 1; + await CargarPaginaActual(); + } + + private async Task CargarPaginaActual() + { + SearchParams.DivisionId = _selectedDivision?.Id; + SearchParams.UnitId = _selectedUnit?.Id; + PagedResult = await productService.SearchAsync(SearchParams); + TablaProductos = PagedResult?.Items.Select(p => new Dictionary + { + { "Id", p.Id }, + { "Código Fábrica", p.FactoryCode }, + { "Código Externo", p.ExternalCode }, + { "Nombre", p.Name }, + { "Descripción", p.Descripcion }, + { "División", p.Division?.Name ?? "" }, + { "Unidad", p.Unit?.Name ?? "" }, + { "Tipo", ObtenerTipoProducto(p.ProductType) }, + { "Trazabilidad", ObtenerTipoTrazabilidad(p.TraceabilityType) }, + { "Esteriliza", p.PlusProcess ? "Sí" : "No" } + }).ToList() ?? []; + } + + private void OnDivisionSelected(ELookUpItem item) => _selectedDivision = item; + private void OnUnitSelected(ELookUpItem item) => _selectedUnit = item; + + private string ObtenerTipoProducto(int? tipo) => tipo switch + { + 1 => "Implantable", + 2 => "Instrumental", + 3 => "Inyectable", + _ => "" + }; + + private string ObtenerTipoTrazabilidad(int? tipo) => tipo switch + { + 1 => "No aplica", + 2 => "Por cantidad", + 3 => "Por lote/vencimiento", + _ => "" + }; + + private async Task PrimeraPagina() { SearchParams.Page = 1; await CargarPaginaActual(); } + private async Task UltimaPagina() { SearchParams.Page = TotalPaginas; await CargarPaginaActual(); } + private async Task SiguientePagina() => await CambiarPagina(1); + private async Task AnteriorPagina() => await CambiarPagina(-1); + private async Task CambiarPagina(int delta) + { + var nueva = SearchParams.Page + delta; + if (nueva >= 1 && nueva <= TotalPaginas) + { + SearchParams.Page = nueva; + await CargarPaginaActual(); + } + } + + private async Task IrAPagina() + { + if (PaginaDeseada >= 1 && PaginaDeseada <= TotalPaginas) + { + SearchParams.Page = PaginaDeseada; + await CargarPaginaActual(); + } + else + { + toastService.ShowWarning("Página fuera de rango."); + } + } + + private async Task ExportarExcel() + { + SearchParams.Page = 1; + SearchParams.PageSize = int.MaxValue; // Exportar todos los resultados + try + { + await productService.ExportFilteredAsync(SearchParams); + toastService.ShowSuccess("Exportación completada."); + } + catch (Exception ex) + { + toastService.ShowError($"Error: {ex.Message}"); + } + } + + private void Nuevo() => Navigation.NavigateTo("/stock/productform/"); + private void Cancelar() => Navigation.NavigateTo("/DashboardPanel"); + + private int TotalPaginas => PagedResult is null ? 1 : (int)Math.Ceiling(PagedResult.TotalItems / (double)SearchParams.PageSize); + private bool PuedeRetroceder => SearchParams.Page > 1; + private bool PuedeAvanzar => PagedResult != null && SearchParams.Page < TotalPaginas; +} diff --git a/phronCare.UIBlazor/Pages/Stock/ProductDivision.razor b/phronCare.UIBlazor/Pages/Stock/ProductDivision.razor index 09369db..e8c6517 100644 --- a/phronCare.UIBlazor/Pages/Stock/ProductDivision.razor +++ b/phronCare.UIBlazor/Pages/Stock/ProductDivision.razor @@ -71,7 +71,7 @@ @code { - private PagedResult? Resultado; + private PagedResult? Resultado; private List> Tabla = new(); private List Columnas = new() { "Id", "Codigo", "Nombre", "Descripción" }; private ProductDivisionSearchParams SearchParams = new() { PageSize = 10 }; diff --git a/phronCare.UIBlazor/Pages/Stock/ProductDivisionForm.razor b/phronCare.UIBlazor/Pages/Stock/ProductDivisionForm.razor index 2328acc..854e99c 100644 --- a/phronCare.UIBlazor/Pages/Stock/ProductDivisionForm.razor +++ b/phronCare.UIBlazor/Pages/Stock/ProductDivisionForm.razor @@ -50,7 +50,7 @@ [Parameter] public int? Id { get; set; } - private EProductDivision model = new(); + private ELSProductDivision model = new(); protected override async Task OnInitializedAsync() { diff --git a/phronCare.UIBlazor/Program.cs b/phronCare.UIBlazor/Program.cs index da8993b..32fff4b 100644 --- a/phronCare.UIBlazor/Program.cs +++ b/phronCare.UIBlazor/Program.cs @@ -46,6 +46,7 @@ await builder.Build().RunAsync(); static void InjectDependencies(WebAssemblyHostBuilder builder) { builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -64,4 +65,6 @@ static void InjectDependencies(WebAssemblyHostBuilder builder) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + } \ No newline at end of file diff --git a/phronCare.UIBlazor/Services/Lookups/IStockLookUpService.cs b/phronCare.UIBlazor/Services/Lookups/IStockLookUpService.cs new file mode 100644 index 0000000..44a38b0 --- /dev/null +++ b/phronCare.UIBlazor/Services/Lookups/IStockLookUpService.cs @@ -0,0 +1,10 @@ +using Domain.Entities; + +namespace phronCare.UIBlazor.Services.Lookups +{ + public interface IStockLookUpService + { + Task> GetProductDivisionsAsync(string filter = ""); + Task> GetUnitsOfMeasureAsync(string filter = ""); + } +} diff --git a/phronCare.UIBlazor/Services/Lookups/StockLookUpService.cs b/phronCare.UIBlazor/Services/Lookups/StockLookUpService.cs new file mode 100644 index 0000000..1425da9 --- /dev/null +++ b/phronCare.UIBlazor/Services/Lookups/StockLookUpService.cs @@ -0,0 +1,28 @@ +using Domain.Entities; +using phronCare.UIBlazor.Services.Lookups; +using System.Net.Http.Json; + +public class StockLookUpService: IStockLookUpService +{ + private readonly HttpClient _http; + + public StockLookUpService(HttpClient http) + { + _http = http; + } + + public async Task> GetProductDivisionsAsync(string filter = "") + { + string url = string.IsNullOrWhiteSpace(filter) + ? "/api/LSMLookUp/productdivisions" + : $"/api/LSMLookUp/productdivisions?q={Uri.EscapeDataString(filter)}"; + + var result = await _http.GetFromJsonAsync>(url); + return result ?? new List(); + } + public async Task> GetUnitsOfMeasureAsync(string filter = "") + { + var result = await _http.GetFromJsonAsync>($"/api/LSMLookUp/units?q={filter}"); + return result ?? []; + } +} diff --git a/phronCare.UIBlazor/Services/Stock/LSProductService.cs b/phronCare.UIBlazor/Services/Stock/LSProductService.cs new file mode 100644 index 0000000..62fd7f7 --- /dev/null +++ b/phronCare.UIBlazor/Services/Stock/LSProductService.cs @@ -0,0 +1,77 @@ +using Domain.Entities; +using Domain.Generics; +using Microsoft.JSInterop; +using System.Net.Http.Json; +using System.Reflection; +using System.Text; +using System.Text.Json; + +namespace phronCare.UIBlazor.Services.Stock +{ + public class LSProductService + { + private readonly HttpClient _http; + private readonly IJSRuntime _js; + + public LSProductService(HttpClient http, IJSRuntime js) + { + _http = http; + _js = js; + } + + public async Task?> SearchAsync(LSProductSearchParams searchParams) + { + return await _http.PostAsJsonAsync("/api/LSProduct/Search", searchParams) + .ContinueWith(async t => + await t.Result.Content.ReadFromJsonAsync>()) + .Unwrap(); + } + + public async Task GetByIdAsync(int id) + { + return await _http.GetFromJsonAsync($"/api/LSProduct/{id}"); + } + + public async Task CreateAsync(ELSProduct product) + { + return await _http.PostAsJsonAsync("/api/LSProduct", product); + } + + public async Task UpdateAsync(ELSProduct product) + { + return await _http.PutAsJsonAsync("/api/LSProduct", product); + } + + public async Task DeleteAsync(int id) + { + return await _http.DeleteAsync($"/api/LSProduct/{id}"); + } + + public async Task ExportFilteredAsync(LSProductSearchParams searchParams) + { + try + { + var content = new StringContent(JsonSerializer.Serialize(searchParams), Encoding.UTF8, "application/json"); + var response = await _http.PostAsync("/api/LSProduct/exportfiltered", content); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + throw new Exception(errorContent); + } + var bytes = await response.Content.ReadAsByteArrayAsync(); + var base64 = Convert.ToBase64String(bytes); + var timestamp = DateTime.Now.ToString("yyyyMMddHHmm"); + + var fileName = $"productos_{DateTime.Now:yyyyMMddHHmmss}.xlsx"; + await _js.InvokeVoidAsync("saveAsFile", fileName, base64); + } + catch(Exception ex) + { + var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + var message = ex.Message ?? "No message provided"; + throw new Exception($"{message}", ex); + } + } + } +} diff --git a/phronCare.UIBlazor/Services/Stock/ProductDivisionService.cs b/phronCare.UIBlazor/Services/Stock/ProductDivisionService.cs index 6dfe0e9..db67d19 100644 --- a/phronCare.UIBlazor/Services/Stock/ProductDivisionService.cs +++ b/phronCare.UIBlazor/Services/Stock/ProductDivisionService.cs @@ -16,24 +16,24 @@ namespace phronCare.UIBlazor.Services.Stock _js = js; } - public async Task> GetAllAsync() + public async Task> GetAllAsync() { - var result = await _http.GetFromJsonAsync>("/api/ProductDivision/GetAll"); - return result ?? new List(); + var result = await _http.GetFromJsonAsync>("/api/ProductDivision/GetAll"); + return result ?? new List(); } - public async Task GetByIdAsync(int id) + public async Task GetByIdAsync(int id) { - var result = await _http.GetFromJsonAsync($"/api/ProductDivision/GetById/{id}"); + var result = await _http.GetFromJsonAsync($"/api/ProductDivision/GetById/{id}"); return result; } - public async Task CreateAsync(EProductDivision division) + public async Task CreateAsync(ELSProductDivision division) { return await _http.PostAsJsonAsync("/api/ProductDivision/Create", division); } - public async Task UpdateAsync(EProductDivision division) + public async Task UpdateAsync(ELSProductDivision division) { return await _http.PutAsJsonAsync("/api/ProductDivision/Update", division); } @@ -43,13 +43,13 @@ namespace phronCare.UIBlazor.Services.Stock return await _http.DeleteAsync($"/api/ProductDivision/Delete/{id}"); } - public async Task?> SearchAsync(ProductDivisionSearchParams searchParams) + public async Task?> SearchAsync(ProductDivisionSearchParams searchParams) { var url = $"/api/ProductDivision/Search?" + $"term={searchParams.Term}&" + $"page={searchParams.Page}&" + $"pageSize={searchParams.PageSize}"; - return await _http.GetFromJsonAsync>(url); + return await _http.GetFromJsonAsync>(url); } } }