From 85fedf79af7eb08067fe50824ee92aa834af5bb3 Mon Sep 17 00:00:00 2001 From: Leandro Hernan Rojas Date: Fri, 27 Jun 2025 13:52:22 -0300 Subject: [PATCH] Add Stock Module MVP Divisiones --- Core/Interfaces/Stock/IProductDivisionDom.cs | 15 + Core/Services/Stock/ProductDivisionService.cs | 100 +++++++ Domain/Entities/EProductDivision.cs | 25 ++ .../Generics/ProductDivisionSearchParams.cs | 7 + .../IPhLSMProductDivisionRepository.cs | 15 + Models/Models/PhLsmProduct.cs | 67 +++++ Models/Models/PhLsmProductDivision.cs | 29 ++ Models/Models/PhLsmStockEntry.cs | 62 ++++ Models/Models/PhLsmStockItem.cs | 51 ++++ Models/Models/PhLsmStockLocation.cs | 24 ++ Models/Models/PhLsmStockOut.cs | 59 ++++ Models/Models/PhLsmUnitOfMeasure.cs | 26 ++ .../Models/PhronCareOperationsHubContext.cs | 283 +++++++++++++++++- .../Stock/PhLSMProductDivisionRepository.cs | 108 +++++++ .../Stock/ProductDivisionController.cs | 98 ++++++ phronCare.API/Program.cs | 30 +- .../obj/Debug/net8.0/ApiEndpoints.json | 111 +++++++ phronCare.UIBlazor/Layout/NavMenu.razor | 59 ++++ .../Pages/Stock/ProductDivision.razor | 149 +++++++++ .../Pages/Stock/ProductDivisionForm.razor | 86 ++++++ phronCare.UIBlazor/Program.cs | 3 +- .../Services/Stock/ProductDivisionService.cs | 55 ++++ 22 files changed, 1447 insertions(+), 15 deletions(-) create mode 100644 Core/Interfaces/Stock/IProductDivisionDom.cs create mode 100644 Core/Services/Stock/ProductDivisionService.cs create mode 100644 Domain/Entities/EProductDivision.cs create mode 100644 Domain/Generics/ProductDivisionSearchParams.cs create mode 100644 Models/Interfaces/IPhLSMProductDivisionRepository.cs create mode 100644 Models/Models/PhLsmProduct.cs create mode 100644 Models/Models/PhLsmProductDivision.cs create mode 100644 Models/Models/PhLsmStockEntry.cs create mode 100644 Models/Models/PhLsmStockItem.cs create mode 100644 Models/Models/PhLsmStockLocation.cs create mode 100644 Models/Models/PhLsmStockOut.cs create mode 100644 Models/Models/PhLsmUnitOfMeasure.cs create mode 100644 Models/Repositories/Stock/PhLSMProductDivisionRepository.cs create mode 100644 phronCare.API/Controllers/Stock/ProductDivisionController.cs create mode 100644 phronCare.UIBlazor/Pages/Stock/ProductDivision.razor create mode 100644 phronCare.UIBlazor/Pages/Stock/ProductDivisionForm.razor create mode 100644 phronCare.UIBlazor/Services/Stock/ProductDivisionService.cs diff --git a/Core/Interfaces/Stock/IProductDivisionDom.cs b/Core/Interfaces/Stock/IProductDivisionDom.cs new file mode 100644 index 0000000..2142a04 --- /dev/null +++ b/Core/Interfaces/Stock/IProductDivisionDom.cs @@ -0,0 +1,15 @@ +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/ProductDivisionService.cs b/Core/Services/Stock/ProductDivisionService.cs new file mode 100644 index 0000000..97bc1cc --- /dev/null +++ b/Core/Services/Stock/ProductDivisionService.cs @@ -0,0 +1,100 @@ +using Core.Interfaces; +using Core.Interfaces.Stock; +using Domain.Entities; +using Domain.Generics; +using Models.Interfaces; +using System.Reflection; + +namespace Core.Services.Stock +{ + public class ProductDivisionService : IProductDivisionDom + { + #region Declaraciones y Constructor + private readonly IPhLSMProductDivisionRepository _repository; + public ProductDivisionService(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) + { + try + { + return await _repository.GetAllAsync(page, pageSize); + } + catch (Exception ex) + { + var method = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + throw new Exception($"{method} Message: {ex.Message}", ex); + } + } + + public async Task GetByIdAsync(int id) + { + try + { + return await _repository.GetByIdAsync(id); + } + catch (Exception ex) + { + var method = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + throw new Exception($"{method} Message: {ex.Message}", ex); + } + } + + public async Task> SearchAsync(string? term, int page = 1, int pageSize = 50) + { + try + { + return await _repository.SearchAsync(term, page, pageSize); + } + catch (Exception ex) + { + var method = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + throw new Exception($"{method} Message: {ex.Message}", ex); + } + } + + public async Task CreateAsync(EProductDivision entity) + { + try + { + return await _repository.CreateAsync(entity); + } + catch (Exception ex) + { + var method = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + throw new Exception($"{method} Message: {ex.Message}", ex); + } + } + + public async Task UpdateAsync(EProductDivision entity) + { + try + { + return await _repository.UpdateAsync(entity); + } + catch (Exception ex) + { + var method = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + throw new Exception($"{method} Message: {ex.Message}", ex); + } + } + + public async Task DeleteAsync(int id) + { + try + { + return await _repository.DeleteAsync(id); + } + catch (Exception ex) + { + var method = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + throw new Exception($"{method} Message: {ex.Message}", ex); + } + } + #endregion + } +} diff --git a/Domain/Entities/EProductDivision.cs b/Domain/Entities/EProductDivision.cs new file mode 100644 index 0000000..1115591 --- /dev/null +++ b/Domain/Entities/EProductDivision.cs @@ -0,0 +1,25 @@ +namespace Domain.Entities +{ + public class EProductDivision + { + /// + /// Identificador único de la división de productos + /// + public int Id { get; set; } + + /// + /// Código breve de la división, coincide con la línea original (ej: TCLO, CMF_J) + /// + public string Code { get; set; } = string.Empty; + + /// + /// Nombre de la división o familia técnica de productos (ej: columna, trauma, descartables, etc.) + /// + public string Name { get; set; } = string.Empty; + + /// + /// Descripción adicional de la división de productos (opcional) + /// + public string? Description { get; set; } = string.Empty; + } +} diff --git a/Domain/Generics/ProductDivisionSearchParams.cs b/Domain/Generics/ProductDivisionSearchParams.cs new file mode 100644 index 0000000..545ea47 --- /dev/null +++ b/Domain/Generics/ProductDivisionSearchParams.cs @@ -0,0 +1,7 @@ +namespace Domain.Generics +{ + public class ProductDivisionSearchParams : PagedRequest + { + public string? Term { get; set; } + } +} \ No newline at end of file diff --git a/Models/Interfaces/IPhLSMProductDivisionRepository.cs b/Models/Interfaces/IPhLSMProductDivisionRepository.cs new file mode 100644 index 0000000..4c5a886 --- /dev/null +++ b/Models/Interfaces/IPhLSMProductDivisionRepository.cs @@ -0,0 +1,15 @@ +using Domain.Entities; +using Domain.Generics; + +namespace Models.Interfaces +{ + public interface IPhLSMProductDivisionRepository + { + Task CreateAsync(EProductDivision 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); + } +} diff --git a/Models/Models/PhLsmProduct.cs b/Models/Models/PhLsmProduct.cs new file mode 100644 index 0000000..fc04ca5 --- /dev/null +++ b/Models/Models/PhLsmProduct.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; + +namespace Models.Models; + +public partial class PhLsmProduct +{ + /// + /// 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; } = null!; + + /// + /// Nombre técnico o estandarizado del producto (ej: ficha técnica, fabricante) + /// + public string? Name { get; set; } + + /// + /// Descripción comercial o práctica del producto (impresión logística, uso cotidiano) + /// + public string Descripcion { get; set; } = null!; + + /// + /// 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; } + + /// + /// 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 PhLsmProductDivision? Division { get; set; } + + public virtual ICollection PhLsmStockEntries { get; set; } = new List(); + + public virtual ICollection PhLsmStockItems { get; set; } = new List(); + + public virtual ICollection PhLsmStockOuts { get; set; } = new List(); + + public virtual PhLsmUnitOfMeasure Unit { get; set; } = null!; +} diff --git a/Models/Models/PhLsmProductDivision.cs b/Models/Models/PhLsmProductDivision.cs new file mode 100644 index 0000000..19b8ba3 --- /dev/null +++ b/Models/Models/PhLsmProductDivision.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +namespace Models.Models; + +public partial class PhLsmProductDivision +{ + /// + /// Identificador único de la división de productos + /// + public int Id { get; set; } + + /// + /// Código breve de la división, coincide con la línea original (ej: TCLO, CMF_J) + /// + public string Code { get; set; } = null!; + + /// + /// Nombre de la división o familia técnica de productos (ej: columna, trauma, descartables, etc.) + /// + public string Name { get; set; } = null!; + + /// + /// Descripción adicional de la división de productos (opcional) + /// + public string? Description { get; set; } + + public virtual ICollection PhLsmProducts { get; set; } = new List(); +} diff --git a/Models/Models/PhLsmStockEntry.cs b/Models/Models/PhLsmStockEntry.cs new file mode 100644 index 0000000..e91719d --- /dev/null +++ b/Models/Models/PhLsmStockEntry.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; + +namespace Models.Models; + +/// +/// Registro individual de ingreso de stock con valor y trazabilidad +/// +public partial class PhLsmStockEntry +{ + /// + /// Identificador único del ingreso de stock + /// + public int Id { get; set; } + + /// + /// Referencia al producto ingresado + /// + public int ProductId { get; set; } + + /// + /// Cantidad ingresada en la unidad correspondiente del producto + /// + public double Quantity { get; set; } + + /// + /// Precio unitario del producto en la moneda indicada + /// + public double Unitprice { get; set; } + + /// + /// Moneda del ingreso (ej: ars, usd, eur) + /// + public string Currency { get; set; } = null!; + + /// + /// Tipo de cambio aplicado respecto a ARS + /// + public double Exchangerate { get; set; } + + /// + /// Fecha y hora del ingreso físico al stock + /// + public DateTime Entrydate { get; set; } + + /// + /// Referencia externa: número de remito, factura o documento de ingreso + /// + public string? Reference { get; set; } + + /// + /// Tipo de origen del ingreso (ej: compra, devolución, ajuste) + /// + public string? Sourcetype { get; set; } + + /// + /// Identificador interno del documento de origen (opcional) + /// + public int? SourceId { get; set; } + + public virtual PhLsmProduct Product { get; set; } = null!; +} diff --git a/Models/Models/PhLsmStockItem.cs b/Models/Models/PhLsmStockItem.cs new file mode 100644 index 0000000..fd237c1 --- /dev/null +++ b/Models/Models/PhLsmStockItem.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + +namespace Models.Models; + +public partial class PhLsmStockItem +{ + /// + /// Identificador único del ítem de stock físico + /// + public int Id { get; set; } + + /// + /// Producto vinculado al ítem de stock + /// + public int ProductId { get; set; } + + /// + /// Ubicación física del stock (depósito, valija, etc.) + /// + public int LocationId { get; set; } + + /// + /// Cantidad actual disponible en esta unidad de stock + /// + public double Quantity { get; set; } + + /// + /// Código de lote (si aplica) + /// + public string? Batch { get; set; } + + /// + /// Fecha de vencimiento (si aplica) + /// + public DateOnly? Expiration { get; set; } + + /// + /// Estado del ítem (1=Disponible, 2=Reservado, 3=Vencido, etc.) + /// + public int Status { get; set; } + + /// + /// Comentario libre u observación sobre este ítem de stock + /// + public string? Description { get; set; } + + public virtual PhLsmStockLocation Location { get; set; } = null!; + + public virtual PhLsmProduct Product { get; set; } = null!; +} diff --git a/Models/Models/PhLsmStockLocation.cs b/Models/Models/PhLsmStockLocation.cs new file mode 100644 index 0000000..4c8e201 --- /dev/null +++ b/Models/Models/PhLsmStockLocation.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace Models.Models; + +public partial class PhLsmStockLocation +{ + /// + /// Identificador único de la ubicación de stock + /// + public int Id { get; set; } + + /// + /// Nombre visible de la ubicación (ej: Depósito Central, Cuarentena, Caja A1) + /// + public string Nombre { get; set; } = null!; + + /// + /// Descripción o comentario adicional sobre la ubicación + /// + public string? Descripcion { get; set; } + + public virtual ICollection PhLsmStockItems { get; set; } = new List(); +} diff --git a/Models/Models/PhLsmStockOut.cs b/Models/Models/PhLsmStockOut.cs new file mode 100644 index 0000000..b70b465 --- /dev/null +++ b/Models/Models/PhLsmStockOut.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; + +namespace Models.Models; + +public partial class PhLsmStockOut +{ + /// + /// Identificador único del egreso de stock + /// + public int Id { get; set; } + + /// + /// Producto retirado del stock + /// + public int ProductId { get; set; } + + /// + /// Cantidad retirada del producto + /// + public double Quantity { get; set; } + + /// + /// Precio unitario usado para valorizar el egreso + /// + public double Unitprice { get; set; } + + /// + /// Moneda utilizada en la valorización (ars, usd, eur) + /// + public string Currency { get; set; } = null!; + + /// + /// Tasa de conversión de la moneda a ARS + /// + public double Exchangerate { get; set; } + + /// + /// Fecha del egreso de stock + /// + public DateTime Outdate { get; set; } + + /// + /// Referencia visible del movimiento (NE, devolución, cirugía) + /// + public string? Reference { get; set; } + + /// + /// Tipo de origen del egreso (surgery, expiration, manual, etc.) + /// + public string? Sourcetype { get; set; } + + /// + /// ID de la entidad que generó el egreso (ej: nota de expedición) + /// + public int? SourceId { get; set; } + + public virtual PhLsmProduct Product { get; set; } = null!; +} diff --git a/Models/Models/PhLsmUnitOfMeasure.cs b/Models/Models/PhLsmUnitOfMeasure.cs new file mode 100644 index 0000000..af9221a --- /dev/null +++ b/Models/Models/PhLsmUnitOfMeasure.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace Models.Models; + +public partial class PhLsmUnitOfMeasure +{ + 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; } + + public virtual ICollection PhLsmProducts { get; set; } = new List(); +} diff --git a/Models/Models/PhronCareOperationsHubContext.cs b/Models/Models/PhronCareOperationsHubContext.cs index 7be3409..73e7a7d 100644 --- a/Models/Models/PhronCareOperationsHubContext.cs +++ b/Models/Models/PhronCareOperationsHubContext.cs @@ -15,6 +15,20 @@ public partial class PhronCareOperationsHubContext : DbContext { } + public virtual DbSet PhLsmProducts { get; set; } + + public virtual DbSet PhLsmProductDivisions { get; set; } + + public virtual DbSet PhLsmStockEntries { get; set; } + + public virtual DbSet PhLsmStockItems { get; set; } + + public virtual DbSet PhLsmStockLocations { get; set; } + + public virtual DbSet PhLsmStockOuts { get; set; } + + public virtual DbSet PhLsmUnitOfMeasures { get; set; } + public virtual DbSet PhOhArcadocumentTypes { get; set; } public virtual DbSet PhOhArcataxTypes { get; set; } @@ -74,13 +88,277 @@ public partial class PhronCareOperationsHubContext : DbContext public virtual DbSet PhSQuoteTaxes { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) -#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263. - => optionsBuilder.UseSqlServer("data source=srv01.saludlab.com.ar,39458;initial catalog=phronCare_OperationsHub;User ID=sa;Password=HS|s[~xxQzTo/n>9jO;encrypt=False;trustServerCertificate=True;MultipleActiveResultSets=True"); + #region VERSION DOCKER + { + if (!optionsBuilder.IsConfigured) + { + // Dejarlo vacío para usar la configuración externa desde Program.cs o Startup.cs + } + } + #endregion + //=> optionsBuilder.UseSqlServer("data source=srv01.saludlab.com.ar,39458;initial catalog=phronCare_OperationsHub;User ID=sa;Password=HS|s[~xxQzTo/n>9jO;encrypt=False;trustServerCertificate=True;MultipleActiveResultSets=True"); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.UseCollation("Modern_Spanish_CI_AS"); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("PK__PhLSM_Pr__3213E83F874510C5"); + + entity.ToTable("PhLSM_Product"); + + entity.Property(e => e.Id) + .HasComment("Identificador único del producto médico o industrial") + .HasColumnName("id"); + entity.Property(e => e.Descripcion) + .HasMaxLength(255) + .HasComment("Descripción comercial o práctica del producto (impresión logística, uso cotidiano)") + .HasColumnName("descripcion"); + entity.Property(e => e.DivisionId) + .HasComment("División o familia técnica del producto (ej: columna, trauma, descartables, etc.)") + .HasColumnName("division_id"); + entity.Property(e => e.ExternalCode) + .HasMaxLength(50) + .HasComment("Código externo estándar del producto (ej: GTIN, código de proveedor, catálogo EAN, etc.)") + .HasColumnName("external_code"); + entity.Property(e => e.FactoryCode) + .HasMaxLength(50) + .HasComment("Código de producto definido por la fábrica o fabricante. Puede variar según proveedor, presentación o país de origen.") + .HasColumnName("factory_code"); + entity.Property(e => e.Name) + .HasMaxLength(255) + .HasComment("Nombre técnico o estandarizado del producto (ej: ficha técnica, fabricante)") + .HasColumnName("name"); + entity.Property(e => e.PlusProcess) + .HasComment("Indica si el producto requiere un proceso adicional previo a su uso (ej: esterilización, calibración, limpieza, inspección, etc.)") + .HasColumnName("plus_process"); + entity.Property(e => e.ProductType) + .HasComment("Tipo de producto: 1=Implantable, 2=Instrumental, 3=Inyectable, etc.") + .HasColumnName("product_type"); + entity.Property(e => e.TraceabilityType) + .HasComment("Tipo de trazabilidad: 1=No aplica, 2=Por cantidad, 3=Por lote y vencimiento") + .HasColumnName("traceability_type"); + entity.Property(e => e.UnitId) + .HasComment("Unidad de medida base del producto (ej: unidad, mililitro, metro)") + .HasColumnName("unit_id"); + + entity.HasOne(d => d.Division).WithMany(p => p.PhLsmProducts) + .HasForeignKey(d => d.DivisionId) + .HasConstraintName("FK__PhLSM_Pro__divis__2EFAF1E2"); + + entity.HasOne(d => d.Unit).WithMany(p => p.PhLsmProducts) + .HasForeignKey(d => d.UnitId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK_PhLSM_Product_Unit"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("PK__PhLSM_Pr__3213E83F27FDF540"); + + entity.ToTable("PhLSM_ProductDivision"); + + entity.HasIndex(e => e.Code, "UQ_PhLSM_ProductDivision_code").IsUnique(); + + entity.Property(e => e.Id) + .HasComment("Identificador único de la división de productos") + .HasColumnName("id"); + entity.Property(e => e.Code) + .HasMaxLength(50) + .HasDefaultValue("") + .HasComment("Código breve de la división, coincide con la línea original (ej: TCLO, CMF_J)") + .HasColumnName("code"); + entity.Property(e => e.Description) + .HasMaxLength(255) + .HasComment("Descripción adicional de la división de productos (opcional)") + .HasColumnName("description"); + entity.Property(e => e.Name) + .HasMaxLength(100) + .HasComment("Nombre de la división o familia técnica de productos (ej: columna, trauma, descartables, etc.)") + .HasColumnName("name"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("PK__PhLSM_St__3213E83FCFAF3A63"); + + entity.ToTable("PhLSM_StockEntry", tb => tb.HasComment("Registro individual de ingreso de stock con valor y trazabilidad")); + + entity.Property(e => e.Id) + .HasComment("Identificador único del ingreso de stock") + .HasColumnName("id"); + entity.Property(e => e.Currency) + .HasMaxLength(3) + .HasComment("Moneda del ingreso (ej: ars, usd, eur)") + .HasColumnName("currency"); + entity.Property(e => e.Entrydate) + .HasComment("Fecha y hora del ingreso físico al stock") + .HasColumnType("datetime") + .HasColumnName("entrydate"); + entity.Property(e => e.Exchangerate) + .HasDefaultValue(1.0) + .HasComment("Tipo de cambio aplicado respecto a ARS") + .HasColumnName("exchangerate"); + entity.Property(e => e.ProductId) + .HasComment("Referencia al producto ingresado") + .HasColumnName("product_id"); + entity.Property(e => e.Quantity) + .HasComment("Cantidad ingresada en la unidad correspondiente del producto") + .HasColumnName("quantity"); + entity.Property(e => e.Reference) + .HasMaxLength(100) + .HasComment("Referencia externa: número de remito, factura o documento de ingreso") + .HasColumnName("reference"); + entity.Property(e => e.SourceId) + .HasComment("Identificador interno del documento de origen (opcional)") + .HasColumnName("source_id"); + entity.Property(e => e.Sourcetype) + .HasMaxLength(50) + .HasComment("Tipo de origen del ingreso (ej: compra, devolución, ajuste)") + .HasColumnName("sourcetype"); + entity.Property(e => e.Unitprice) + .HasComment("Precio unitario del producto en la moneda indicada") + .HasColumnName("unitprice"); + + entity.HasOne(d => d.Product).WithMany(p => p.PhLsmStockEntries) + .HasForeignKey(d => d.ProductId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK__PhLSM_Sto__produ__40257DE4"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("PK__PhLSM_St__3213E83F7C7F442D"); + + entity.ToTable("PhLSM_StockItem"); + + entity.Property(e => e.Id) + .HasComment("Identificador único del ítem de stock físico") + .HasColumnName("id"); + entity.Property(e => e.Batch) + .HasMaxLength(100) + .HasComment("Código de lote (si aplica)") + .HasColumnName("batch"); + entity.Property(e => e.Description) + .HasMaxLength(255) + .HasComment("Comentario libre u observación sobre este ítem de stock") + .HasColumnName("description"); + entity.Property(e => e.Expiration) + .HasComment("Fecha de vencimiento (si aplica)") + .HasColumnName("expiration"); + entity.Property(e => e.LocationId) + .HasComment("Ubicación física del stock (depósito, valija, etc.)") + .HasColumnName("location_id"); + entity.Property(e => e.ProductId) + .HasComment("Producto vinculado al ítem de stock") + .HasColumnName("product_id"); + entity.Property(e => e.Quantity) + .HasComment("Cantidad actual disponible en esta unidad de stock") + .HasColumnName("quantity"); + entity.Property(e => e.Status) + .HasComment("Estado del ítem (1=Disponible, 2=Reservado, 3=Vencido, etc.)") + .HasColumnName("status"); + + entity.HasOne(d => d.Location).WithMany(p => p.PhLsmStockItems) + .HasForeignKey(d => d.LocationId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK__PhLSM_Sto__locat__3C54ED00"); + + entity.HasOne(d => d.Product).WithMany(p => p.PhLsmStockItems) + .HasForeignKey(d => d.ProductId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK__PhLSM_Sto__produ__3B60C8C7"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("PK__PhLSM_St__3213E83F68EA6A9A"); + + entity.ToTable("PhLSM_StockLocation"); + + entity.Property(e => e.Id) + .HasComment("Identificador único de la ubicación de stock") + .HasColumnName("id"); + entity.Property(e => e.Descripcion) + .HasMaxLength(255) + .HasComment("Descripción o comentario adicional sobre la ubicación") + .HasColumnName("descripcion"); + entity.Property(e => e.Nombre) + .HasMaxLength(100) + .HasComment("Nombre visible de la ubicación (ej: Depósito Central, Cuarentena, Caja A1)") + .HasColumnName("nombre"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("PK__PhLSM_St__3213E83F96B2D858"); + + entity.ToTable("PhLSM_StockOut"); + + entity.Property(e => e.Id) + .HasComment("Identificador único del egreso de stock") + .HasColumnName("id"); + entity.Property(e => e.Currency) + .HasMaxLength(3) + .HasComment("Moneda utilizada en la valorización (ars, usd, eur)") + .HasColumnName("currency"); + entity.Property(e => e.Exchangerate) + .HasDefaultValue(1.0) + .HasComment("Tasa de conversión de la moneda a ARS") + .HasColumnName("exchangerate"); + entity.Property(e => e.Outdate) + .HasComment("Fecha del egreso de stock") + .HasColumnType("datetime") + .HasColumnName("outdate"); + entity.Property(e => e.ProductId) + .HasComment("Producto retirado del stock") + .HasColumnName("product_id"); + entity.Property(e => e.Quantity) + .HasComment("Cantidad retirada del producto") + .HasColumnName("quantity"); + entity.Property(e => e.Reference) + .HasMaxLength(100) + .HasComment("Referencia visible del movimiento (NE, devolución, cirugía)") + .HasColumnName("reference"); + entity.Property(e => e.SourceId) + .HasComment("ID de la entidad que generó el egreso (ej: nota de expedición)") + .HasColumnName("source_id"); + entity.Property(e => e.Sourcetype) + .HasMaxLength(50) + .HasComment("Tipo de origen del egreso (surgery, expiration, manual, etc.)") + .HasColumnName("sourcetype"); + entity.Property(e => e.Unitprice) + .HasComment("Precio unitario usado para valorizar el egreso") + .HasColumnName("unitprice"); + + entity.HasOne(d => d.Product).WithMany(p => p.PhLsmStockOuts) + .HasForeignKey(d => d.ProductId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK__PhLSM_Sto__produ__43F60EC8"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("PK__PhLSM_Un__3213E83FD70349B6"); + + entity.ToTable("PhLSM_UnitOfMeasure"); + + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.Code) + .HasMaxLength(10) + .HasComment("Código abreviado de unidad de medida (ej: UN, ML, MT)") + .HasColumnName("code"); + entity.Property(e => e.Description) + .HasMaxLength(255) + .HasComment("Descripción extendida o notas adicionales sobre la unidad") + .HasColumnName("description"); + entity.Property(e => e.Name) + .HasMaxLength(50) + .HasComment("Nombre descriptivo de la unidad de medida") + .HasColumnName("name"); + }); + modelBuilder.Entity(entity => { entity.HasKey(e => e.Id).HasName("PK__PhOH_ARC__3213E83FF8940395"); @@ -939,6 +1217,7 @@ public partial class PhronCareOperationsHubContext : DbContext .HasColumnName("id"); entity.Property(e => e.Approvaldate) .HasComment("Fecha de aprobación") + .HasColumnType("datetime") .HasColumnName("approvaldate"); entity.Property(e => e.Approvedamount) .HasComment("Importe aprobado") diff --git a/Models/Repositories/Stock/PhLSMProductDivisionRepository.cs b/Models/Repositories/Stock/PhLSMProductDivisionRepository.cs new file mode 100644 index 0000000..8a78556 --- /dev/null +++ b/Models/Repositories/Stock/PhLSMProductDivisionRepository.cs @@ -0,0 +1,108 @@ +using Domain.Entities; +using Domain.Generics; +using Microsoft.EntityFrameworkCore; +using Models.Helpers; +using Models.Interfaces; +using Models.Models; + +namespace Models.Repositories.Stock +{ + public class PhLSMProductDivisionRepository(PhronCareOperationsHubContext context) : IPhLSMProductDivisionRepository + { + private readonly PhronCareOperationsHubContext _context = context; + + 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 + { + Items = pagedEntities.Items.Select(EntityMapper.MapEntity), + TotalItems = pagedEntities.TotalItems, + Page = pagedEntities.Page, + PageSize = pagedEntities.PageSize + }; + } + + public async Task GetByIdAsync(int id) + { + var entity = await _context.PhLsmProductDivisions.FirstOrDefaultAsync(x => x.Id == id); + return entity is null ? null : EntityMapper.MapEntity(entity); + } + + public async Task> SearchAsync(string? term, int page = 1, int pageSize = 50) + { + var query = _context.PhLsmProductDivisions.AsQueryable(); + + if (!string.IsNullOrWhiteSpace(term)) + { + term = term.ToLower(); + query = query.Where(x => x.Description.ToLower().Contains(term)); + } + + var pagedEntities = await query.ToPagedResultAsync(page, pageSize); + + return new PagedResult + { + Items = pagedEntities.Items.Select(EntityMapper.MapEntity), + TotalItems = pagedEntities.TotalItems, + Page = pagedEntities.Page, + PageSize = pagedEntities.PageSize + }; + } + + public async Task CreateAsync(EProductDivision entity) + { + if (entity == null) + throw new ArgumentNullException(nameof(entity)); + + try + { + var mapped = EntityMapper.MapEntity(entity); + _context.PhLsmProductDivisions.Add(mapped); + await _context.SaveChangesAsync(); + return EntityMapper.MapEntity(mapped); + } + catch (DbUpdateException dbEx) + { + throw new Exception("Error al guardar la división de producto. Verificá integridad de datos.", dbEx); + } + catch (Exception ex) + { + throw new Exception("Error inesperado al crear la división: " + ex.Message, ex); + } + } + + public async Task UpdateAsync(EProductDivision entity) + { + if (entity == null) + throw new ArgumentNullException(nameof(entity)); + + try + { + var existing = await _context.PhLsmProductDivisions.FirstOrDefaultAsync(x => x.Id == entity.Id); + if (existing == null) return false; + + EntityMapper.MapEntityToExisting(entity, existing); + await _context.SaveChangesAsync(); + return true; + } + catch + { + return false; + } + } + + public async Task DeleteAsync(int id) + { + var existing = await _context.PhLsmProductDivisions.FindAsync(id); + if (existing == null) return false; + + _context.PhLsmProductDivisions.Remove(existing); + await _context.SaveChangesAsync(); + return true; + } + } +} diff --git a/phronCare.API/Controllers/Stock/ProductDivisionController.cs b/phronCare.API/Controllers/Stock/ProductDivisionController.cs new file mode 100644 index 0000000..41d8685 --- /dev/null +++ b/phronCare.API/Controllers/Stock/ProductDivisionController.cs @@ -0,0 +1,98 @@ +using Core.Interfaces.Stock; +using Domain.Entities; +using Microsoft.AspNetCore.Mvc; + +namespace phronCare.API.Controllers.Stock +{ + [Route("api/[controller]")] + [ApiController] + public class ProductDivisionController : ControllerBase + { + private readonly IProductDivisionDom _service; + + public ProductDivisionController(IProductDivisionDom service) + { + _service = service ?? throw new ArgumentNullException(nameof(service)); + } + + [HttpGet("GetAll")] + public async Task GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + try + { + var result = await _service.GetAllAsync(page, pageSize); + return Ok(result); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpGet("GetById/{id}")] + public async Task GetById(int id) + { + var result = await _service.GetByIdAsync(id); + if (result == null) + return NotFound($"No se encontró una división con ID {id}."); + + return Ok(result); + } + + [HttpGet("Search")] + public async Task Search([FromQuery] string? term, [FromQuery] int page = 1, [FromQuery] int pageSize = 50) + { + try + { + var result = await _service.SearchAsync(term, page, pageSize); + return Ok(result); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpPost("Create")] + public async Task Create([FromBody] EProductDivision division) + { + try + { + var created = await _service.CreateAsync(division); + return CreatedAtAction(nameof(GetById), new { id = created.Id }, created); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpPut("Update")] + public async Task Update([FromBody] EProductDivision division) + { + try + { + var updated = await _service.UpdateAsync(division); + return updated ? Ok() : NotFound($"No se encontró una división con ID {division.Id}."); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpDelete("Delete/{id}")] + public async Task Delete(int id) + { + try + { + var deleted = await _service.DeleteAsync(id); + return deleted ? Ok() : NotFound($"No se encontró una división con ID {id}."); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + } +} diff --git a/phronCare.API/Program.cs b/phronCare.API/Program.cs index cd62a38..cda0872 100644 --- a/phronCare.API/Program.cs +++ b/phronCare.API/Program.cs @@ -1,25 +1,28 @@ +using Core.Interfaces; +using Core.Interfaces.Stock; +using Core.Services; +using Core.Services.Stock; +using Documents.Interfaces; +using Documents.Services; using Google.Authenticator; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using Models.Interfaces; -using Services.Interfaces; -using Services.Services; -using Services.Models; -using System.Text; -using phronCare.API.Models; -using Core.Interfaces; -using Core.Services; -using phronCare.API.Models.Security; -using Models.Repositories; using Models.Models; +using Models.Repositories; +using Models.Repositories.Stock; +using phronCare.API.Models; +using phronCare.API.Models.Security; +using Services.Interfaces; +using Services.Models; +using Services.Services; +using System.Text; using Transversal.Interfaces; using Transversal.Services; -using Microsoft.AspNetCore.Components.Web; -using Documents.Interfaces; -using Documents.Services; var builder = WebApplication.CreateBuilder(args); @@ -253,5 +256,8 @@ 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(); } \ 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 688d77c..beef607 100644 --- a/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json +++ b/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json @@ -1449,6 +1449,117 @@ ], "ReturnTypes": [] }, + { + "ContainingType": "phronCare.API.Controllers.Stock.ProductDivisionController", + "Method": "Create", + "RelativePath": "api/ProductDivision/Create", + "HttpMethod": "POST", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "division", + "Type": "Domain.Entities.EProductDivision", + "IsRequired": true + } + ], + "ReturnTypes": [] + }, + { + "ContainingType": "phronCare.API.Controllers.Stock.ProductDivisionController", + "Method": "Delete", + "RelativePath": "api/ProductDivision/Delete/{id}", + "HttpMethod": "DELETE", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "id", + "Type": "System.Int32", + "IsRequired": true + } + ], + "ReturnTypes": [] + }, + { + "ContainingType": "phronCare.API.Controllers.Stock.ProductDivisionController", + "Method": "GetAll", + "RelativePath": "api/ProductDivision/GetAll", + "HttpMethod": "GET", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "page", + "Type": "System.Int32", + "IsRequired": false + }, + { + "Name": "pageSize", + "Type": "System.Int32", + "IsRequired": false + } + ], + "ReturnTypes": [] + }, + { + "ContainingType": "phronCare.API.Controllers.Stock.ProductDivisionController", + "Method": "GetById", + "RelativePath": "api/ProductDivision/GetById/{id}", + "HttpMethod": "GET", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "id", + "Type": "System.Int32", + "IsRequired": true + } + ], + "ReturnTypes": [] + }, + { + "ContainingType": "phronCare.API.Controllers.Stock.ProductDivisionController", + "Method": "Search", + "RelativePath": "api/ProductDivision/Search", + "HttpMethod": "GET", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "term", + "Type": "System.String", + "IsRequired": false + }, + { + "Name": "page", + "Type": "System.Int32", + "IsRequired": false + }, + { + "Name": "pageSize", + "Type": "System.Int32", + "IsRequired": false + } + ], + "ReturnTypes": [] + }, + { + "ContainingType": "phronCare.API.Controllers.Stock.ProductDivisionController", + "Method": "Update", + "RelativePath": "api/ProductDivision/Update", + "HttpMethod": "PUT", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "division", + "Type": "Domain.Entities.EProductDivision", + "IsRequired": true + } + ], + "ReturnTypes": [] + }, { "ContainingType": "phronCare.API.Controllers.Sales.ProfessionalController", "Method": "GetById", diff --git a/phronCare.UIBlazor/Layout/NavMenu.razor b/phronCare.UIBlazor/Layout/NavMenu.razor index 955e23b..73376ca 100644 --- a/phronCare.UIBlazor/Layout/NavMenu.razor +++ b/phronCare.UIBlazor/Layout/NavMenu.razor @@ -46,6 +46,64 @@ +