From 1c4c241266fc67b2a7fca3ca5879fc9dfc08d22e Mon Sep 17 00:00:00 2001 From: Leandro Hernan Rojas Date: Mon, 18 Aug 2025 00:47:37 -0300 Subject: [PATCH] Add StockItemModal v1 --- Core/Interfaces/ILookUpDom.cs | 2 + Core/Interfaces/IQuoteDom.cs | 6 +- Core/Interfaces/Stock/ILSStockScanDom.cs | 12 + Core/Services/LookupService .cs | 3 +- Core/Services/QuoteService.cs | 10 +- Core/Services/Stock/LSProductService.cs | 1 + Core/Services/Stock/LSStockScanService.cs | 45 +++ Core/obj/Core.csproj.nuget.g.props | 2 +- Domain/Dtos/QuoteDto.cs | 29 +- Domain/Dtos/Stock/Gs1ScanResult.cs | 12 + Domain/Dtos/Stock/ProductImportPreviewDto.cs | 1 + Domain/Dtos/Stock/ProductSetItemDto.cs | 32 ++ Domain/Dtos/Stock/StockItemScanResultDto.cs | 80 +++++ Domain/Dtos/Stock/StockItemSelectionDto.cs | 51 ++++ Domain/Entities/ELSExpeditionDetail.cs | 69 +++++ Domain/Entities/ELSExpeditionHeader.cs | 82 ++++++ Domain/Entities/ELSProduct.cs | 5 + Domain/Entities/ELSProductSet..cs | 43 +++ Domain/Entities/ELSProductSetItem..cs | 34 +++ Domain/Entities/ELSStockEntry.cs | 77 +++++ Domain/Entities/ELSStockItem.cs | 69 +++++ Domain/Entities/ELSStockLocation.cs | 22 ++ Domain/Entities/ELSStockOut.cs | 82 ++++++ .../Generics/StockItemParsedSearchParams.cs | 13 + Domain/Generics/StockItemSearchParams.cs | 16 + Domain/obj/Domain.csproj.nuget.dgspec.json | 2 +- Domain/obj/Domain.csproj.nuget.g.props | 2 +- Domain/obj/project.assets.json | 2 +- .../Interfaces/IPhLSMStockItemRepository.cs | 28 ++ Models/Interfaces/IPhSLookUpRepository.cs | 1 + .../Interfaces/IPhSQuoteHeaderRepository.cs | 4 +- Models/Interfaces/IQuoteRepository.cs | 1 + Models/Models/PhLsmExpeditionDetail.cs | 5 + Models/Models/PhLsmProduct.cs | 5 + Models/Models/PhLsmStockItem.cs | 5 + .../Models/PhronCareOperationsHubContext.cs | 40 ++- Models/Repositories/PhSLookUpRepository.cs | 17 ++ .../Repositories/PhSQuoteHeaderRepository.cs | 16 +- Models/Repositories/PhSQuoteRepository.cs | 44 +++ .../Stock/PhLSMProductRepository.cs | 1 + .../Stock/PhLSMStockItemRepository.cs | 213 ++++++++++++++ Models/obj/Models.csproj.nuget.dgspec.json | 4 +- Models/obj/Models.csproj.nuget.g.props | 2 +- Models/obj/project.assets.json | 2 +- .../obj/Services.csproj.nuget.dgspec.json | 2 +- Services/obj/Services.csproj.nuget.g.props | 2 +- Services/obj/project.assets.json | 2 +- Transversal/Services/Gs1CodeParser.cs | 181 ++++++++++++ .../obj/Transversal.csproj.nuget.g.props | 2 +- .../Controllers/Sales/LookUpController.cs | 4 + .../Controllers/Sales/QuoteController.cs | 23 ++ .../Stock/LSStockScanController.cs | 47 +++ phronCare.API/Program.cs | 2 + .../Templates/Stock/plantilla_productos.xlsx | Bin 5245 -> 8906 bytes .../obj/Debug/net8.0/ApiEndpoints.json | 104 +++++++ .../phronCare.API.csproj.nuget.dgspec.json | 14 +- .../obj/phronCare.API.csproj.nuget.g.props | 2 +- phronCare.API/obj/project.assets.json | 2 +- .../phronCare.Test.csproj.nuget.dgspec.json | 6 +- .../obj/phronCare.Test.csproj.nuget.g.props | 2 +- phronCare.Test/obj/project.assets.json | 2 +- phronCare.UIBlazor/Layout/NavMenu.razor | 2 +- .../Pages/Authorization/LoginPage.razor | 2 +- .../Stock/Expeditions/ExpeditionCreate.razor | 276 ++++++++++++++++++ .../Pages/Stock/LSProduct.razor | 3 +- .../Pages/Stock/LSProductForm.razor | 8 +- .../Stock/Shared/StockItemSelectorModal.razor | 221 ++++++++++++++ .../Pages/Stock/Shared/StockScanModal.razor | 123 ++++++++ phronCare.UIBlazor/Program.cs | 3 + .../Services/Lookups/ISalesLookupService .cs | 1 + .../Services/Lookups/SalesLookupService.cs | 4 +- .../Services/Sales/Quotes/IQuoteService.cs | 2 +- .../Services/Sales/Quotes/QuoteService.cs | 5 + .../Stock/Expeditions/ExpeditionService.cs | 34 +++ .../Stock/Expeditions/IExpeditionService.cs | 6 + .../Services/Stock/IStockScanService.cs | 7 + .../Services/Stock/MockStockScanService.cs | 30 ++ .../Services/Stock/StockScanService.cs | 110 +++++++ ...hronCare.UIBlazor.csproj.nuget.dgspec.json | 12 +- .../phronCare.UIBlazor.csproj.nuget.g.props | 10 +- .../phronCare.UIBlazor.csproj.nuget.g.targets | 2 +- phronCare.UIBlazor/obj/project.assets.json | 36 +-- 82 files changed, 2394 insertions(+), 92 deletions(-) create mode 100644 Core/Interfaces/Stock/ILSStockScanDom.cs create mode 100644 Core/Services/Stock/LSStockScanService.cs create mode 100644 Domain/Dtos/Stock/Gs1ScanResult.cs create mode 100644 Domain/Dtos/Stock/ProductSetItemDto.cs create mode 100644 Domain/Dtos/Stock/StockItemScanResultDto.cs create mode 100644 Domain/Dtos/Stock/StockItemSelectionDto.cs create mode 100644 Domain/Entities/ELSExpeditionDetail.cs create mode 100644 Domain/Entities/ELSExpeditionHeader.cs create mode 100644 Domain/Entities/ELSProductSet..cs create mode 100644 Domain/Entities/ELSProductSetItem..cs create mode 100644 Domain/Entities/ELSStockEntry.cs create mode 100644 Domain/Entities/ELSStockItem.cs create mode 100644 Domain/Entities/ELSStockLocation.cs create mode 100644 Domain/Entities/ELSStockOut.cs create mode 100644 Domain/Generics/StockItemParsedSearchParams.cs create mode 100644 Domain/Generics/StockItemSearchParams.cs create mode 100644 Models/Interfaces/IPhLSMStockItemRepository.cs create mode 100644 Models/Repositories/Stock/PhLSMStockItemRepository.cs create mode 100644 Transversal/Services/Gs1CodeParser.cs create mode 100644 phronCare.API/Controllers/Stock/LSStockScanController.cs create mode 100644 phronCare.UIBlazor/Pages/Stock/Expeditions/ExpeditionCreate.razor create mode 100644 phronCare.UIBlazor/Pages/Stock/Shared/StockItemSelectorModal.razor create mode 100644 phronCare.UIBlazor/Pages/Stock/Shared/StockScanModal.razor create mode 100644 phronCare.UIBlazor/Services/Stock/Expeditions/ExpeditionService.cs create mode 100644 phronCare.UIBlazor/Services/Stock/Expeditions/IExpeditionService.cs create mode 100644 phronCare.UIBlazor/Services/Stock/IStockScanService.cs create mode 100644 phronCare.UIBlazor/Services/Stock/MockStockScanService.cs create mode 100644 phronCare.UIBlazor/Services/Stock/StockScanService.cs diff --git a/Core/Interfaces/ILookUpDom.cs b/Core/Interfaces/ILookUpDom.cs index 222b8aa..d746dda 100644 --- a/Core/Interfaces/ILookUpDom.cs +++ b/Core/Interfaces/ILookUpDom.cs @@ -12,5 +12,7 @@ namespace Core.Interfaces Task> BussinessUnitsListAsync(string filter, int limit = 10); Task> ProductsListAsync(string filter, int limit = 10); Task> PaymentTermsListAsync(string filter, int limit = 10); + Task> ApprovedQuotesListAsync(string filter, int limit = 10); + } } diff --git a/Core/Interfaces/IQuoteDom.cs b/Core/Interfaces/IQuoteDom.cs index cd748f5..93966de 100644 --- a/Core/Interfaces/IQuoteDom.cs +++ b/Core/Interfaces/IQuoteDom.cs @@ -7,12 +7,8 @@ namespace Models.Interfaces public interface IQuoteDom { #region Presupuestos - //Task> GetAllQuotesAsync(int page = 1, int pageSize = 50); - //Task GetQuoteByIdAsync(int id); - //Task> GetQuotesByCustomerAsync(int customerId); Task> SearchAsync(int? customerId, string? customerText, string? quoteNumber, int? professionalId, string? professionalText, int? institutionId, string? institutionText, int? patientId, string? patientText, DateTime? issueDateFrom, DateTime? issueDateTo, string? status, int page = 1, int pageSize = 50); - //Task UpdateQuoteAsync(EQuoteHeader quote); - //Task DeleteQuoteAsync(int id); + Task GetDtoByQuoteNumberAsync(string quoteNumber); #endregion #region Exportación //Task ExportFilteredQuotesToExcelAsync(QuoteSearchParams searchParams); diff --git a/Core/Interfaces/Stock/ILSStockScanDom.cs b/Core/Interfaces/Stock/ILSStockScanDom.cs new file mode 100644 index 0000000..c53bb24 --- /dev/null +++ b/Core/Interfaces/Stock/ILSStockScanDom.cs @@ -0,0 +1,12 @@ +using Domain.Dtos.Stock; +using Domain.Generics; + +namespace Core.Interfaces.Stock +{ + public interface ILSStockScanDom + { + Task> SearchAsync(StockItemSearchParams p); + Task> SearchParsedAsync(StockItemParsedSearchParams p, CancellationToken ct = default); + + } +} \ No newline at end of file diff --git a/Core/Services/LookupService .cs b/Core/Services/LookupService .cs index cec140c..e8a3fa7 100644 --- a/Core/Services/LookupService .cs +++ b/Core/Services/LookupService .cs @@ -24,7 +24,8 @@ namespace Core.Services _repository.ProductsListAsync(filter); public Task> PaymentTermsListAsync(string filter, int limit = 10) => _repository.PaymentTermsListAsync(filter, limit); - + public Task> ApprovedQuotesListAsync(string filter, int limit = 10) => + _repository.ApprovedQuotesListAsync(filter, limit); #endregion } } diff --git a/Core/Services/QuoteService.cs b/Core/Services/QuoteService.cs index 76fbb4d..ec03d76 100644 --- a/Core/Services/QuoteService.cs +++ b/Core/Services/QuoteService.cs @@ -37,6 +37,11 @@ namespace Core.Services { return await _quoteRepository.GetDtoByIdAsync(id); } + + //public async Task GetDtoByQuoteNumberAsync(string quoteNumber) + //{ + // return await _quoteRepository.GetDtoByIdAsync(quoteNumber); + //} #endregion #region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos) @@ -74,7 +79,6 @@ namespace Core.Services return await _quoteRepository.AuthorizeQuoteAsync(quoteId, approvedDetails); } - #region Validaciones QuoteCreate private void ValidateQuote(EQuoteHeader quote) { @@ -125,6 +129,10 @@ namespace Core.Services throw new ArgumentException("El total del presupuesto no puede ser negativo."); } + public async Task GetDtoByQuoteNumberAsync(string quoteNumber) + { + return await _quoteRepository.GetDtoByQuoteNumberAsync(quoteNumber); + } #endregion } } \ No newline at end of file diff --git a/Core/Services/Stock/LSProductService.cs b/Core/Services/Stock/LSProductService.cs index d651a3b..614a397 100644 --- a/Core/Services/Stock/LSProductService.cs +++ b/Core/Services/Stock/LSProductService.cs @@ -68,6 +68,7 @@ namespace Core.Services p.Id, p.FactoryCode, p.ExternalCode, + p.RegulatoryCode, p.Name, p.Descripcion, Tipo = p.ProductType == 1 ? "Implantable" : diff --git a/Core/Services/Stock/LSStockScanService.cs b/Core/Services/Stock/LSStockScanService.cs new file mode 100644 index 0000000..89b0179 --- /dev/null +++ b/Core/Services/Stock/LSStockScanService.cs @@ -0,0 +1,45 @@ +using Core.Interfaces.Stock; +using Domain.Dtos.Stock; +using Domain.Generics; +using Models.Interfaces; + +namespace Core.Services.Stock +{ + public class LSStockScanService : ILSStockScanDom + { + #region Declaraciones y Constructor + private readonly IPhLSMStockItemRepository _repo; + // Constructor injection for repository dependency + public LSStockScanService(IPhLSMStockItemRepository repo) => _repo = repo; + #endregion + #region Metodos + public Task> SearchAsync(StockItemSearchParams p) + => _repo.SearchStockItemsAsync( + p.CodeOrText, + p.Batch, + p.LocationId, + p.ProductType, + p.TraceabilityType, + p.PlusProcess, + p.Page, + p.PageSize); + public Task> SearchParsedAsync( + StockItemParsedSearchParams p, + CancellationToken ct = default) + { + var page = p.Page <= 0 ? 1 : p.Page; + var take = p.PageSize <= 0 ? 20 : p.PageSize; + + return _repo.SearchStockItemsParsedAsync( + gtin: string.IsNullOrWhiteSpace(p.Gtin) ? null : p.Gtin!.Trim(), + batch: string.IsNullOrWhiteSpace(p.Batch) ? null : p.Batch!.Trim(), + expiration: p.Expiration, + serial: string.IsNullOrWhiteSpace(p.Serial) ? null : p.Serial!.Trim(), + locationId: p.LocationId, + page: page, + take: take + ); + } + #endregion + } +} \ No newline at end of file diff --git a/Core/obj/Core.csproj.nuget.g.props b/Core/obj/Core.csproj.nuget.g.props index bb4fdbc..f58dcab 100644 --- a/Core/obj/Core.csproj.nuget.g.props +++ b/Core/obj/Core.csproj.nuget.g.props @@ -7,7 +7,7 @@ $(UserProfile)\.nuget\packages\ C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages PackageReference - 6.14.0 + 6.14.1 diff --git a/Domain/Dtos/QuoteDto.cs b/Domain/Dtos/QuoteDto.cs index 4f09d69..0a8e522 100644 --- a/Domain/Dtos/QuoteDto.cs +++ b/Domain/Dtos/QuoteDto.cs @@ -10,7 +10,12 @@ /// /// Número de presupuesto (ej. "Q-00000001"). /// - public string Quotenumber { get; set; } = String.Empty; + public string Quotenumber { get; set; } = string.Empty; + + /// + /// Relación con Tickets + /// + public Guid? TicketId { get; set; } /// /// Fecha de emisión del presupuesto. @@ -25,22 +30,22 @@ /// /// Nombre completo del cliente asociado. /// - public string CustomerName { get; set; } = String.Empty; + public string CustomerName { get; set; } = string.Empty; /// /// Nombre completo del médico responsable. /// - public string ProfessionalName { get; set; } = String.Empty; + public string ProfessionalName { get; set; } = string.Empty; /// /// Nombre de la institución u hospital. /// - public string InstitutionName { get; set; } = String.Empty; + public string InstitutionName { get; set; } = string.Empty; /// /// Nombre completo del paciente. /// - public string PatientName { get; set; } = String.Empty; + public string PatientName { get; set; } = string.Empty; /// /// Días de validez del presupuesto (desde la fecha de emisión). @@ -50,18 +55,18 @@ /// /// Descripción de la condición de pago (ej. "Contado", "30 días"). /// - public string? PaymentTermDescription { get; set; } = String.Empty; + public string? PaymentTermDescription { get; set; } = string.Empty; /// /// Nombre de la unidad de negocio. /// - public string BusinessUnitName { get; set; } = String.Empty; + public string BusinessUnitName { get; set; } = string.Empty; /// /// Moneda del presupuesto (ej. "ARS", "USD"). /// - public string Currency { get; set; } = String.Empty; + public string Currency { get; set; } = string.Empty; /// /// Importe total final (incluye impuestos y ajustes). @@ -71,16 +76,16 @@ /// /// Estado actual del presupuesto ("Pendiente", "Aprobado", etc.). /// - public string Status { get; set; } = String.Empty; + public string Status { get; set; } = string.Empty; /// /// Nombre del vendedor o ejecutivo de ventas. /// - public string SalespersonName { get; set; } = String.Empty; + public string SalespersonName { get; set; } = string.Empty; /// /// Nombre del vendedor o ejecutivo de ventas. /// - public string Observations { get; set; } = String.Empty; + public string Observations { get; set; } = string.Empty; /// /// Detalle de los ítems o productos cotizados. @@ -105,6 +110,6 @@ /// /// Logo de la compañia. /// - public string LogoBase64 { get; set; } = ""; + public string LogoBase64 { get; set; } = string.Empty; } } \ No newline at end of file diff --git a/Domain/Dtos/Stock/Gs1ScanResult.cs b/Domain/Dtos/Stock/Gs1ScanResult.cs new file mode 100644 index 0000000..6eb98cd --- /dev/null +++ b/Domain/Dtos/Stock/Gs1ScanResult.cs @@ -0,0 +1,12 @@ +namespace Domain.Dtos.Stock +{ + public class Gs1ScanResult + { + public string? Gtin { get; set; } + public string? Lot { get; set; } + public DateTime? ExpirationDate { get; set; } + public string? Serial { get; set; } + public string? Raw { get; set; } + public string? Variant { get; set; } + } +} diff --git a/Domain/Dtos/Stock/ProductImportPreviewDto.cs b/Domain/Dtos/Stock/ProductImportPreviewDto.cs index 0239da8..22ed32c 100644 --- a/Domain/Dtos/Stock/ProductImportPreviewDto.cs +++ b/Domain/Dtos/Stock/ProductImportPreviewDto.cs @@ -11,6 +11,7 @@ public string UnitCode { get; set; } = string.Empty; public bool PlusProcess { get; set; } public string ExternalCode { get; set; } = string.Empty; + public string RegulatoryCode { get; set; } = string.Empty; public string? ErrorMessage { get; set; } public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage); } diff --git a/Domain/Dtos/Stock/ProductSetItemDto.cs b/Domain/Dtos/Stock/ProductSetItemDto.cs new file mode 100644 index 0000000..549148f --- /dev/null +++ b/Domain/Dtos/Stock/ProductSetItemDto.cs @@ -0,0 +1,32 @@ +namespace Domain.Dtos.Stock +{ + /// + /// Representa un producto que forma parte de un set quirúrgico predefinido. + /// Este DTO se utiliza en la UI para sugerir qué productos incluir al seleccionar un set. + /// + public class ProductSetItemDto + { + /// + /// ID del producto asociado al set. + /// + public int ProductId { get; set; } + + /// + /// Nombre corto o técnico del producto (para mostrar en UI). + /// + public string ProductName { get; set; } = string.Empty; + + /// + /// Cantidad sugerida a utilizar para este producto dentro del set. + /// Se usa como valor por defecto al cargar el selector de stock. + /// Puede incluir fracciones (ej: 0.5, 1.25), por eso es decimal. + /// + public decimal DefaultQuantity { get; set; } + + /// + /// Indica si este producto es obligatorio dentro del set quirúrgico. + /// Si es true, se espera que el usuario no lo omita al cargar los ítems. + /// + public bool Mandatory { get; set; } + } +} diff --git a/Domain/Dtos/Stock/StockItemScanResultDto.cs b/Domain/Dtos/Stock/StockItemScanResultDto.cs new file mode 100644 index 0000000..0d12090 --- /dev/null +++ b/Domain/Dtos/Stock/StockItemScanResultDto.cs @@ -0,0 +1,80 @@ +namespace Domain.Dtos.Stock +{ + /// + /// Representa un ítem de stock disponible encontrado mediante búsqueda o escaneo. + /// Este DTO se utiliza como resultado en la UI para que el usuario elija qué ítem tomar. + /// + public class StockItemScanResultDto + { + /// + /// Identificador único del ítem en la tabla PhLSM_StockItem. + /// + public int StockItemId { get; set; } + + /// + /// Identificador del producto asociado. + /// + public int ProductId { get; set; } + + /// + /// Código de fábrica del producto. + /// + public string FactoryCode { get; set; } = string.Empty; + + /// + /// Código externo o alternativo (si existe). + /// + public string? ExternalCode { get; set; } + + /// + /// Nombre formal o técnico del producto. + /// + public string ProductName { get; set; } = string.Empty; + + /// + /// Descripción comercial o de uso. + /// + public string? Description { get; set; } + + /// + /// Identificador de la ubicación física (depósito, sala, etc.). + /// + public int? LocationId { get; set; } + + /// + /// Nombre descriptivo de la ubicación. + /// + public string? LocationName { get; set; } + + /// + /// Número de lote (si aplica). + /// + public string? Batch { get; set; } + + /// + /// Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante. + /// + public string? Serial { get; set; } + + /// + /// Fecha de vencimiento (si aplica). + /// + public DateOnly? Expiration { get; set; } + + /// + /// Tipo de trazabilidad del producto: + /// 1 = No aplica, 2 = Por cantidad, 3 = Por lote y vencimiento. + /// + public int TraceabilityType { get; set; } + + /// + /// Cantidad disponible actualmente. + /// + public decimal AvailableQty { get; set; } + + /// + /// Indica si requiere proceso adicional (por ejemplo, esterilización). + /// + public bool PlusProcess { get; set; } + } +} diff --git a/Domain/Dtos/Stock/StockItemSelectionDto.cs b/Domain/Dtos/Stock/StockItemSelectionDto.cs new file mode 100644 index 0000000..1b92b5a --- /dev/null +++ b/Domain/Dtos/Stock/StockItemSelectionDto.cs @@ -0,0 +1,51 @@ +namespace Domain.Dtos.Stock +{ + /// + /// Representa un ítem seleccionado desde el stock real para operaciones como + /// expediciones, consumo quirúrgico, ventas directas o devoluciones. + /// Este DTO se usa únicamente en la capa de UI y se transforma a entidades de persistencia al guardar. + /// + public class StockItemSelectionDto + { + /// + /// Identificador del ítem de stock seleccionado (PhLSM_StockItem.id) + /// + public int StockItemId { get; set; } + + /// + /// Identificador del producto asociado + /// + public int ProductId { get; set; } + + /// + /// Nombre o descripción corta del producto (solo para visualización en UI) + /// + public string ProductName { get; set; } = string.Empty; + + /// + /// Lote del stock seleccionado + /// + public string Batch { get; set; } = string.Empty; + + /// + /// Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante. + /// + public string? Serial { get; set; } + + /// + /// Fecha de vencimiento (si aplica) + /// + public DateTime? Expiration { get; set; } + + + /// + /// Cantidad que el usuario desea usar (puede ser decimal si es por peso o volumen) + /// + public decimal Quantity { get; set; } + + /// + /// Identificador del depósito o ubicación de donde se toma el ítem + /// + public int LocationId { get; set; } + } +} diff --git a/Domain/Entities/ELSExpeditionDetail.cs b/Domain/Entities/ELSExpeditionDetail.cs new file mode 100644 index 0000000..26cfbf4 --- /dev/null +++ b/Domain/Entities/ELSExpeditionDetail.cs @@ -0,0 +1,69 @@ +namespace Domain.Entities +{ + public class ELSExpeditionDetail + { + /// + /// Identificador interno del ítem de expedición + /// + public int Id { get; set; } + + /// + /// Referencia a la cabecera de expedición (PhLSM_ExpeditionHeaders) + /// + public int ExpeditionId { get; set; } + + /// + /// Producto médico a despachar + /// + public int ProductId { get; set; } + + /// + /// Cantidad solicitada del producto + /// + public decimal Quantity { get; set; } + + /// + /// Ubicación específica desde donde se despacha este ítem + /// + public int LocationId { get; set; } + + /// + /// Número de lote (si aplica trazabilidad) + /// + public string? Batch { get; set; } + + /// + /// Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante. + /// + public string? Serial { get; set; } + + /// + /// Fecha de vencimiento del producto (si aplica trazabilidad) + /// + public DateOnly? Expiration { get; set; } + + /// + /// Descripción libre del ítem (uso interno o impresión) + /// + public string? Description { get; set; } + + /// + /// Precio estimado unitario del producto (sin efecto contable) + /// + public decimal? EstimatedUnitprice { get; set; } + + /// + /// Moneda del precio estimado (ej: ARS, USD) + /// + public string? EstimatedCurrency { get; set; } + + /// + /// Tipo de cambio aplicado al precio estimado + /// + public decimal? EstimatedExchangerate { get; set; } + + //public virtual PhLsmExpeditionHeader Expedition { get; set; } = null!; + + //public virtual PhLsmProduct Product { get; set; } = null!; + } +} diff --git a/Domain/Entities/ELSExpeditionHeader.cs b/Domain/Entities/ELSExpeditionHeader.cs new file mode 100644 index 0000000..29acb31 --- /dev/null +++ b/Domain/Entities/ELSExpeditionHeader.cs @@ -0,0 +1,82 @@ +namespace Domain.Entities +{ + public class ELSExpeditionHeader + { + /// + /// Identificador interno de la expedición + /// + public int Id { get; set; } + + /// + /// Referencia al ticket quirúrgico (si aplica) + /// + public Guid? TicketId { get; set; } + + /// + /// Número de expedición (formato EX-00000001) + /// + public string Expeditionnumber { get; set; } = null!; + + /// + /// Ubicación (depósito) desde donde se despacha + /// + public int LocationId { get; set; } + + /// + /// Fecha de emisión de la expedición + /// + public DateTime Issuedate { get; set; } + + /// + /// Estado de la expedición (1=Borrador, 2=Confirmada, etc.) + /// + public int Status { get; set; } + + /// + /// Nombre del destinatario visible en la impresión + /// + public string? RecipientName { get; set; } + + /// + /// Número o referencia externa asociada + /// + public string? ReferenceNumber { get; set; } + + /// + /// Tipo de origen externo (ej: surgery, demo, préstamo) + /// + public string? OriginType { get; set; } + + /// + /// ID externo relacionado a otro módulo (ej: ticket, orden) + /// + public string? ExternalReference { get; set; } + + /// + /// Observaciones generales de la expedición + /// + public string? Observations { get; set; } + + /// + /// Información adicional en formato JSON (ej: paciente, médico, etc.) + /// + public string? ExtrainfoJson { get; set; } + + /// + /// Cantidad de veces que se imprimió la nota de expedición + /// + public int Printcount { get; set; } + + /// + /// Fecha de creación del registro + /// + public DateTime Createdat { get; set; } + + /// + /// Fecha de última modificación del registro + /// + public DateTime? Modifiedat { get; set; } + + public virtual ICollection PhLsmExpeditionDetails { get; set; } = new List(); + } +} diff --git a/Domain/Entities/ELSProduct.cs b/Domain/Entities/ELSProduct.cs index 3ef56ac..abda3a0 100644 --- a/Domain/Entities/ELSProduct.cs +++ b/Domain/Entities/ELSProduct.cs @@ -42,6 +42,11 @@ /// public string? ExternalCode { get; set; } = string.Empty; + /// + /// Código regulatorio (PM) o registro sanitario oficial del producto. Registro otorgado por ANMAT u ente regulador. + /// + public string? RegulatoryCode { get; set; } = string.Empty; + /// /// División o familia técnica del producto (ej: columna, trauma, descartables, etc.) /// diff --git a/Domain/Entities/ELSProductSet..cs b/Domain/Entities/ELSProductSet..cs new file mode 100644 index 0000000..33f90f2 --- /dev/null +++ b/Domain/Entities/ELSProductSet..cs @@ -0,0 +1,43 @@ +namespace Domain.Entities +{ + public class ELSProductSet + { + /// + /// Identificador único del set de productos + /// + public int Id { get; set; } + + /// + /// Código del set (ej: CMF1.2, TRAUMA2.0) + /// + public string Code { get; set; } = null!; + + /// + /// Nombre comercial o técnico del set + /// + public string Name { get; set; } = null!; + + /// + /// Descripción extendida del set de productos + /// + public string? Descripcion { get; set; } + + /// + /// Indica si el set requiere un proceso adicional (ej: esterilización) + /// + public bool PlusProcess { get; set; } + + /// + /// Fecha de creación del registro + /// + public DateTime Createdat { get; set; } + + /// + /// Fecha de última modificación del registro + /// + public DateTime? Modifiedat { get; set; } + + public virtual ICollection PhLsmProductSetItems { get; set; } = new List(); + } + +} diff --git a/Domain/Entities/ELSProductSetItem..cs b/Domain/Entities/ELSProductSetItem..cs new file mode 100644 index 0000000..1dd82d5 --- /dev/null +++ b/Domain/Entities/ELSProductSetItem..cs @@ -0,0 +1,34 @@ +namespace Domain.Entities +{ + public class ELSProductSetItem + { + /// + /// Identificador único del ítem dentro del set + /// + public int Id { get; set; } + + /// + /// ID del set al que pertenece este ítem + /// + public int ProductsetId { get; set; } + + /// + /// Producto incluido en el set + /// + public int ProductId { get; set; } + + /// + /// Cantidad estándar del producto en el set + /// + public decimal DefaultQuantity { get; set; } + + /// + /// Indica si este ítem es obligatorio en la composición del set + /// + public bool Mandatory { get; set; } + + //public virtual PhLsmProduct Product { get; set; } = null!; + + //public virtual PhLsmProductSet Productset { get; set; } = null!; + } +} diff --git a/Domain/Entities/ELSStockEntry.cs b/Domain/Entities/ELSStockEntry.cs new file mode 100644 index 0000000..96a374d --- /dev/null +++ b/Domain/Entities/ELSStockEntry.cs @@ -0,0 +1,77 @@ +namespace Domain.Entities +{ + public class ELSStockEntry + { + /// + /// Identificador único del ingreso de stock + /// + public int Id { get; set; } + + /// + /// Producto ingresado al stock + /// + public int ProductId { get; set; } + + /// + /// Cantidad ingresada del producto + /// + public decimal Quantity { get; set; } + + /// + /// Precio unitario usado para valorizar el ingreso + /// + public decimal 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 decimal Exchangerate { get; set; } + + /// + /// Fecha del ingreso de stock + /// + public DateTime Entrydate { get; set; } + + /// + /// Referencia visible del movimiento (ej: factura, orden de compra) + /// + public string? Reference { get; set; } + + /// + /// Tipo de origen del ingreso (purchase, return, manual, etc.) + /// + public string? Sourcetype { get; set; } + + /// + /// ID de la entidad que generó el ingreso (ej: orden de compra) + /// + public int? SourceId { get; set; } + + /// + /// Ubicación física donde se depositó el producto + /// + public int LocationId { get; set; } + + /// + /// Lote del producto ingresado (si aplica trazabilidad) + /// + public string? Batch { get; set; } + + /// + /// Fecha de vencimiento del producto ingresado (si aplica) + /// + public DateOnly? Expiration { get; set; } + + /// + /// Fecha de creación del registro + /// + public DateTime Createdat { get; set; } + + //public virtual PhLsmProduct Product { get; set; } = null!; + } +} diff --git a/Domain/Entities/ELSStockItem.cs b/Domain/Entities/ELSStockItem.cs new file mode 100644 index 0000000..569be73 --- /dev/null +++ b/Domain/Entities/ELSStockItem.cs @@ -0,0 +1,69 @@ +namespace Domain.Entities +{ + public class ELSStockItem + { + /// + /// 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 decimal Quantity { get; set; } + + /// + /// Cantidad comprometida o reservada para expediciones futuras + /// + public decimal ReservedQuantity { get; set; } + + /// + /// Código de lote (si aplica) + /// + public string? Batch { get; set; } + + /// + /// Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante. + /// + public string? Serial { 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; } + + /// + /// Fecha de creación del registro + /// + public DateTime Createdat { get; set; } + + /// + /// Fecha de última modificación del registro + /// + public DateTime? Modifiedat { get; set; } + + //public virtual PhLsmStockLocation Location { get; set; } = null!; + + //public virtual PhLsmProduct Product { get; set; } = null!; + } +} diff --git a/Domain/Entities/ELSStockLocation.cs b/Domain/Entities/ELSStockLocation.cs new file mode 100644 index 0000000..269280e --- /dev/null +++ b/Domain/Entities/ELSStockLocation.cs @@ -0,0 +1,22 @@ +namespace Domain.Entities +{ + public class ELSStockLocation + { + /// + /// 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/Domain/Entities/ELSStockOut.cs b/Domain/Entities/ELSStockOut.cs new file mode 100644 index 0000000..f3e7cb3 --- /dev/null +++ b/Domain/Entities/ELSStockOut.cs @@ -0,0 +1,82 @@ +namespace Domain.Entities +{ + public class ELSStockOut + { + /// + /// 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 decimal Quantity { get; set; } + + /// + /// Precio unitario usado para valorizar el egreso + /// + public decimal 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 decimal 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; } + + /// + /// Identificador del caso quirúrgico asociado al egreso + /// + public Guid? TicketId { get; set; } + + /// + /// Ubicación física desde donde se retiró el producto + /// + public int? LocationId { get; set; } + + /// + /// Lote del producto egresado (si aplica trazabilidad) + /// + public string? Batch { get; set; } + + /// + /// Fecha de vencimiento del producto egresado (si aplica) + /// + public DateOnly? Expiration { get; set; } + + /// + /// Fecha de creación del registro + /// + public DateTime Createdat { get; set; } + + //public virtual PhLsmProduct Product { get; set; } = null!; + } +} diff --git a/Domain/Generics/StockItemParsedSearchParams.cs b/Domain/Generics/StockItemParsedSearchParams.cs new file mode 100644 index 0000000..58d7207 --- /dev/null +++ b/Domain/Generics/StockItemParsedSearchParams.cs @@ -0,0 +1,13 @@ +namespace Domain.Generics +{ + public class StockItemParsedSearchParams + { + public string? Gtin { get; set; } + public string? Batch { get; set; } + public DateOnly? Expiration { get; set; } + public string? Serial { get; set; } + public int? LocationId { get; set; } + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 20; + } +} diff --git a/Domain/Generics/StockItemSearchParams.cs b/Domain/Generics/StockItemSearchParams.cs new file mode 100644 index 0000000..ecef010 --- /dev/null +++ b/Domain/Generics/StockItemSearchParams.cs @@ -0,0 +1,16 @@ +namespace Domain.Generics +{ + public class StockItemSearchParams + { + public string? CodeOrText { get; set; } + public string? Batch { get; set; } + public int? LocationId { get; set; } + public int? ProductType { get; set; } + public int? TraceabilityType { get; set; } + public bool? PlusProcess { get; set; } + + // paginación + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 20; + } +} \ No newline at end of file diff --git a/Domain/obj/Domain.csproj.nuget.dgspec.json b/Domain/obj/Domain.csproj.nuget.dgspec.json index 6f68fd4..307aa15 100644 --- a/Domain/obj/Domain.csproj.nuget.dgspec.json +++ b/Domain/obj/Domain.csproj.nuget.dgspec.json @@ -66,7 +66,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" } } } diff --git a/Domain/obj/Domain.csproj.nuget.g.props b/Domain/obj/Domain.csproj.nuget.g.props index c25da7f..ca369a5 100644 --- a/Domain/obj/Domain.csproj.nuget.g.props +++ b/Domain/obj/Domain.csproj.nuget.g.props @@ -7,7 +7,7 @@ $(UserProfile)\.nuget\packages\ C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages PackageReference - 6.14.0 + 6.14.1 diff --git a/Domain/obj/project.assets.json b/Domain/obj/project.assets.json index 8b55ef3..a7696fe 100644 --- a/Domain/obj/project.assets.json +++ b/Domain/obj/project.assets.json @@ -73,7 +73,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" } } } diff --git a/Models/Interfaces/IPhLSMStockItemRepository.cs b/Models/Interfaces/IPhLSMStockItemRepository.cs new file mode 100644 index 0000000..0842e30 --- /dev/null +++ b/Models/Interfaces/IPhLSMStockItemRepository.cs @@ -0,0 +1,28 @@ +using Domain.Dtos.Stock; +using Domain.Generics; + +namespace Models.Interfaces +{ + public interface IPhLSMStockItemRepository + { + Task> SearchStockItemsAsync( + string? codeOrText, + string? batch, + int? locationId, + int? productType, + int? traceabilityType, + bool? plusProcess, + int page, + int take + ); + // Búsqueda con datos parseados (GS1/DataMatrix) + Task> SearchStockItemsParsedAsync( + string? gtin, + string? batch, + DateOnly? expiration, + string? serial, // reservado (si agregás columna Serial en StockItem) + int? locationId, + int page, + int take); + } +} diff --git a/Models/Interfaces/IPhSLookUpRepository.cs b/Models/Interfaces/IPhSLookUpRepository.cs index 708b4ec..559e8ae 100644 --- a/Models/Interfaces/IPhSLookUpRepository.cs +++ b/Models/Interfaces/IPhSLookUpRepository.cs @@ -12,5 +12,6 @@ namespace Models.Interfaces Task> BussinessUnitsListAsync(string filter, int limit = 10); Task> ProductsListAsync(string filter, int limit = 10); Task> PaymentTermsListAsync(string filter, int limit = 10); + Task> ApprovedQuotesListAsync(string filter, int limit = 10); } } \ No newline at end of file diff --git a/Models/Interfaces/IPhSQuoteHeaderRepository.cs b/Models/Interfaces/IPhSQuoteHeaderRepository.cs index e4767a1..2559da6 100644 --- a/Models/Interfaces/IPhSQuoteHeaderRepository.cs +++ b/Models/Interfaces/IPhSQuoteHeaderRepository.cs @@ -1,4 +1,5 @@ -using Domain.Entities; +using Domain.Dtos; +using Domain.Entities; using Domain.Generics; namespace Models.Interfaces @@ -7,6 +8,7 @@ namespace Models.Interfaces { Task> GetAllAsync(int page = 1, int pageSize = 50); Task GetByIdAsync(int id); + Task GetByQuoteNumberAsync(string quoteNumber); Task> GetByCustomerIdAsync(int customerId); Task UpdateAsync(EQuoteHeader quoteHeader); Task DeleteAsync(int id); diff --git a/Models/Interfaces/IQuoteRepository.cs b/Models/Interfaces/IQuoteRepository.cs index f5bf922..452ace7 100644 --- a/Models/Interfaces/IQuoteRepository.cs +++ b/Models/Interfaces/IQuoteRepository.cs @@ -12,6 +12,7 @@ namespace Models.Interfaces #region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos) Task<(int Id, string Quotenumber)> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId); Task GetDtoByIdAsync(int id); + Task GetDtoByQuoteNumberAsync(string quoteNumber); Task AuthorizeQuoteAsync(int quoteId, List approvedItems); #endregion } diff --git a/Models/Models/PhLsmExpeditionDetail.cs b/Models/Models/PhLsmExpeditionDetail.cs index 7c73696..eb5c16b 100644 --- a/Models/Models/PhLsmExpeditionDetail.cs +++ b/Models/Models/PhLsmExpeditionDetail.cs @@ -35,6 +35,11 @@ public partial class PhLsmExpeditionDetail /// public string? Batch { get; set; } + /// + /// Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante. + /// + public string? Serial { get; set; } + /// /// Fecha de vencimiento del producto (si aplica trazabilidad) /// diff --git a/Models/Models/PhLsmProduct.cs b/Models/Models/PhLsmProduct.cs index a11c9d7..0de2625 100644 --- a/Models/Models/PhLsmProduct.cs +++ b/Models/Models/PhLsmProduct.cs @@ -45,6 +45,11 @@ public partial class PhLsmProduct /// public string? ExternalCode { get; set; } + /// + /// Código regulatorio (PM) o registro sanitario oficial del producto. Registro otorgado por ANMAT u ente regulador. + /// + public string? RegulatoryCode { get; set; } + /// /// División o familia técnica del producto (ej: columna, trauma, descartables, etc.) /// diff --git a/Models/Models/PhLsmStockItem.cs b/Models/Models/PhLsmStockItem.cs index e7b044c..bbc9f60 100644 --- a/Models/Models/PhLsmStockItem.cs +++ b/Models/Models/PhLsmStockItem.cs @@ -35,6 +35,11 @@ public partial class PhLsmStockItem /// public string? Batch { get; set; } + /// + /// Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante. + /// + public string? Serial { get; set; } + /// /// Fecha de vencimiento (si aplica) /// diff --git a/Models/Models/PhronCareOperationsHubContext.cs b/Models/Models/PhronCareOperationsHubContext.cs index 95d6ceb..d7a8df1 100644 --- a/Models/Models/PhronCareOperationsHubContext.cs +++ b/Models/Models/PhronCareOperationsHubContext.cs @@ -96,8 +96,15 @@ 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) { @@ -151,6 +158,10 @@ public partial class PhronCareOperationsHubContext : DbContext .HasComment("Cantidad solicitada del producto") .HasColumnType("decimal(18, 4)") .HasColumnName("quantity"); + entity.Property(e => e.Serial) + .HasMaxLength(100) + .HasComment("Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante.") + .HasColumnName("serial"); entity.HasOne(d => d.Expedition).WithMany(p => p.PhLsmExpeditionDetails) .HasForeignKey(d => d.ExpeditionId) @@ -267,6 +278,10 @@ public partial class PhronCareOperationsHubContext : DbContext entity.Property(e => e.ProductType) .HasComment("Tipo de producto: 1=Implantable, 2=Instrumental, 3=Inyectable, etc.") .HasColumnName("product_type"); + entity.Property(e => e.RegulatoryCode) + .HasMaxLength(50) + .HasComment("Código regulatorio (PM) o registro sanitario oficial del producto. Registro otorgado por ANMAT u ente regulador.") + .HasColumnName("regulatory_code"); entity.Property(e => e.TraceabilityType) .HasComment("Tipo de trazabilidad: 1=No aplica, 2=Por cantidad, 3=Por lote y vencimiento") .HasColumnName("traceability_type"); @@ -459,7 +474,21 @@ public partial class PhronCareOperationsHubContext : DbContext entity.ToTable("PhLSM_StockItem"); - entity.HasIndex(e => new { e.ProductId, e.LocationId, e.Batch, e.Expiration }, "IX_PhLSM_StockItem_UniqueTraceability").IsUnique(); + entity.HasIndex(e => new { e.ProductId, e.LocationId, e.Batch }, "UQ_PhLSM_StockItem_ProdLoc_BatchOnly") + .IsUnique() + .HasFilter("([serial] IS NULL AND [batch] IS NOT NULL AND [expiration] IS NULL)"); + + entity.HasIndex(e => new { e.ProductId, e.LocationId, e.Batch, e.Expiration }, "UQ_PhLSM_StockItem_ProdLoc_Batch_Exp") + .IsUnique() + .HasFilter("([serial] IS NULL AND [batch] IS NOT NULL AND [expiration] IS NOT NULL)"); + + entity.HasIndex(e => new { e.ProductId, e.LocationId }, "UQ_PhLSM_StockItem_ProdLoc_None") + .IsUnique() + .HasFilter("([serial] IS NULL AND [batch] IS NULL AND [expiration] IS NULL)"); + + entity.HasIndex(e => new { e.ProductId, e.Serial }, "UQ_PhLSM_StockItem_Product_Serial") + .IsUnique() + .HasFilter("([serial] IS NOT NULL)"); entity.Property(e => e.Id) .HasComment("Identificador único del ítem de stock físico") @@ -500,6 +529,11 @@ public partial class PhronCareOperationsHubContext : DbContext .HasComment("Cantidad comprometida o reservada para expediciones futuras") .HasColumnType("decimal(18, 4)") .HasColumnName("reserved_quantity"); + entity.Property(e => e.Serial) + .HasMaxLength(100) + .IsUnicode(false) + .HasComment("Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante.") + .HasColumnName("serial"); entity.Property(e => e.Status) .HasComment("Estado del ítem (1=Disponible, 2=Reservado, 3=Vencido, etc.)") .HasColumnName("status"); diff --git a/Models/Repositories/PhSLookUpRepository.cs b/Models/Repositories/PhSLookUpRepository.cs index 415b066..5402c84 100644 --- a/Models/Repositories/PhSLookUpRepository.cs +++ b/Models/Repositories/PhSLookUpRepository.cs @@ -92,6 +92,23 @@ namespace Models.Repositories .Take(limit) .ToListAsync(); } + public async Task> ApprovedQuotesListAsync(string filter, int limit = 10) + { + return await ( + from q in _context.PhSQuoteHeaders + join c in _context.PhSCustomers on q.CustomerId equals c.Id + where q.Status == "Emitido" && + (q.Quotenumber.Contains(filter) || c.Name.Contains(filter)) + orderby q.Issuedate descending + select new ELookUpItem + { + Id = q.Id, + Nombre = q.Quotenumber + " - " + c.Name + } + ) + .Take(limit) + .ToListAsync(); + } } } diff --git a/Models/Repositories/PhSQuoteHeaderRepository.cs b/Models/Repositories/PhSQuoteHeaderRepository.cs index fcd661f..21c039a 100644 --- a/Models/Repositories/PhSQuoteHeaderRepository.cs +++ b/Models/Repositories/PhSQuoteHeaderRepository.cs @@ -1,16 +1,16 @@ -using Microsoft.EntityFrameworkCore; -using Models.Interfaces; -using Models.Helpers; -using Models.Models; +using Domain.Dtos; using Domain.Entities; using Domain.Generics; +using Microsoft.EntityFrameworkCore; +using Models.Helpers; +using Models.Interfaces; +using Models.Models; namespace Models.Repositories { public class PhSQuoteHeaderRepository(PhronCareOperationsHubContext context) : IPhSQuoteHeaderRepository { private readonly PhronCareOperationsHubContext _context = context; - //private readonly IPhSFormSeriesRepository _formSeriesRepository = formSeriesRepository; #region Metodos public async Task> GetAllAsync(int page = 1, int pageSize = 50) { @@ -42,6 +42,12 @@ namespace Models.Repositories return entity != null ? EntityMapper.MapEntity(entity) : null; } + public async Task GetByQuoteNumberAsync(string quoteNumber) + { + var entity= await _context.PhSQuoteHeaders + .FirstOrDefaultAsync(q => q.Quotenumber == quoteNumber); + return entity != null ? EntityMapper.MapEntity(entity) : null; + } public async Task> GetByCustomerIdAsync(int customerId) { var entities = await _context.PhSQuoteHeaders diff --git a/Models/Repositories/PhSQuoteRepository.cs b/Models/Repositories/PhSQuoteRepository.cs index f1af0f6..8b68526 100644 --- a/Models/Repositories/PhSQuoteRepository.cs +++ b/Models/Repositories/PhSQuoteRepository.cs @@ -344,7 +344,51 @@ namespace Models.Repositories return dto; } + public async Task GetDtoByQuoteNumberAsync(string quoteNumber) + { + var header = await _context.PhSQuoteHeaders + .Include(q => q.PhSQuoteRoles) + .FirstOrDefaultAsync(q => q.Quotenumber == quoteNumber); + if (header == null) + return null; + + var customer = await _context.PhSCustomers + .FirstOrDefaultAsync(c => c.Id == header.CustomerId); + + return new QuoteDto + { + Id = header.Id, + Quotenumber = header.Quotenumber, + EstimatedDate = header.Estimateddate, + Observations = header.DispatchInstruction ?? "", + CustomerName = customer?.Name ?? "", + + ProfessionalName = header.PhSQuoteRoles + .Where(r => r.Entitytype == PhSEntityTypes.Professional) + .Select(r => _context.PhSProfessionals + .Where(p => p.Id == r.EntityId) + .Select(p => p.Fullname) + .FirstOrDefault()) + .FirstOrDefault() ?? "", + + InstitutionName = header.PhSQuoteRoles + .Where(r => r.Entitytype == PhSEntityTypes.Institution) + .Select(r => _context.PhSInstitutions + .Where(i => i.Id == r.EntityId) + .Select(i => i.Name) + .FirstOrDefault()) + .FirstOrDefault() ?? "", + + PatientName = header.PhSQuoteRoles + .Where(r => r.Entitytype == PhSEntityTypes.Patient) + .Select(r => _context.PhSPatients + .Where(pt => pt.Id == r.EntityId) + .Select(pt => (pt.Firstname + " " + pt.Lastname).Trim()) + .FirstOrDefault()) + .FirstOrDefault() ?? "" + }; + } #endregion #region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos) /// diff --git a/Models/Repositories/Stock/PhLSMProductRepository.cs b/Models/Repositories/Stock/PhLSMProductRepository.cs index 077d50c..5f8eff5 100644 --- a/Models/Repositories/Stock/PhLSMProductRepository.cs +++ b/Models/Repositories/Stock/PhLSMProductRepository.cs @@ -144,6 +144,7 @@ namespace Models.Repositories UnitId = unitId, PlusProcess = item.PlusProcess, ExternalCode = item.ExternalCode, + RegulatoryCode = item.RegulatoryCode // otros campos... }; diff --git a/Models/Repositories/Stock/PhLSMStockItemRepository.cs b/Models/Repositories/Stock/PhLSMStockItemRepository.cs new file mode 100644 index 0000000..bf82ac1 --- /dev/null +++ b/Models/Repositories/Stock/PhLSMStockItemRepository.cs @@ -0,0 +1,213 @@ +using Domain.Dtos.Stock; // StockItemScanResultDto +using Domain.Generics; // PagedResult +using Microsoft.EntityFrameworkCore; +using Models.Helpers; // ToPagedResultAsync +using Models.Interfaces; // IPhLSMStockItemRepository +using Models.Models; // PhronCareOperationsHubContext + +namespace Models.Repositories.Stock +{ + public class PhLSMStockItemRepository(PhronCareOperationsHubContext context) : IPhLSMStockItemRepository + { + private readonly PhronCareOperationsHubContext _context = context; + + public async Task> SearchStockItemsAsync( + string? codeOrText, + string? batch, + int? locationId, + int? productType, + int? traceabilityType, + bool? plusProcess, + int page, + int take) + { + // Base: stock disponible (>0) + joins necesarios + var baseQuery = + from si in _context.PhLsmStockItems.AsNoTracking() + join p in _context.PhLsmProducts.AsNoTracking() on si.ProductId equals p.Id + join l in _context.PhLsmStockLocations.AsNoTracking() on si.LocationId equals l.Id into _loc + from loc in _loc.DefaultIfEmpty() + where si.Quantity > 0 + select new { si, p, loc }; + + // ---- Filtros (case-insensitive donde aplica) ---- + if (!string.IsNullOrWhiteSpace(codeOrText)) + { + var t = codeOrText.Trim().ToLower(); + baseQuery = baseQuery.Where(x => + (!string.IsNullOrEmpty(x.p.FactoryCode) && x.p.FactoryCode.ToLower().Contains(t)) || + (!string.IsNullOrEmpty(x.p.ExternalCode) && x.p.ExternalCode.ToLower().Contains(t)) || + (!string.IsNullOrEmpty(x.p.Name) && x.p.Name.ToLower().Contains(t)) || + (!string.IsNullOrEmpty(x.p.Descripcion) && x.p.Descripcion.ToLower().Contains(t)) || + (!string.IsNullOrEmpty(x.si.Batch) && x.si.Batch.ToLower().Contains(t)) + ); + } + + if (!string.IsNullOrWhiteSpace(batch)) + { + var b = batch.Trim().ToLower(); + baseQuery = baseQuery.Where(x => x.si.Batch != null && x.si.Batch.ToLower().Contains(b)); + } + + if (locationId.HasValue) + baseQuery = baseQuery.Where(x => x.si.LocationId == locationId.Value); + + if (productType.HasValue) + baseQuery = baseQuery.Where(x => x.p.ProductType == productType.Value); + + if (traceabilityType.HasValue) + baseQuery = baseQuery.Where(x => x.p.TraceabilityType == traceabilityType.Value); + + if (plusProcess.HasValue) + baseQuery = baseQuery.Where(x => x.p.PlusProcess == plusProcess.Value); + + // Orden lógico + baseQuery = baseQuery + .OrderBy(x => x.si.Expiration) + .ThenBy(x => x.p.Name); + + // Proyección final a DTO (IQueryable) + var dtoQuery = baseQuery.Select(x => new StockItemScanResultDto + { + StockItemId = x.si.Id, + ProductId = x.p.Id, + FactoryCode = x.p.FactoryCode ?? string.Empty, + ExternalCode = x.p.ExternalCode, + ProductName = x.p.Name ?? string.Empty, + Description = x.p.Descripcion, + LocationId = x.si.LocationId, + LocationName = x.loc != null ? x.loc.Descripcion : null, + Batch = x.si.Batch, + Expiration = x.si.Expiration, + TraceabilityType = x.p.TraceabilityType, + AvailableQty = x.si.Quantity, + PlusProcess = x.p.PlusProcess + }); + + page = page <= 0 ? 1 : page; + take = take <= 0 ? 20 : take; + + // Paginado con tu helper (manteniendo el mismo patrón que ProductRepository) + var paged = await dtoQuery.ToPagedResultAsync(page, take); + + return new PagedResult + { + Items = paged.Items, + TotalItems = paged.TotalItems, + Page = paged.Page, + PageSize = paged.PageSize + }; + } + + // -------- Búsqueda PARSEADA (GS1/DataMatrix) ---------- + public async Task> SearchStockItemsParsedAsync( + string? gtin, + string? batch, + DateOnly? expiration, + string? serial, + int? locationId, + int page, + int take) + { + // 0) Si no hay NINGÚN dato parseado, no traigas todo + if (string.IsNullOrWhiteSpace(gtin) && + string.IsNullOrWhiteSpace(batch) && + !expiration.HasValue && + string.IsNullOrWhiteSpace(serial)) + { + return new PagedResult + { Items = [], TotalItems = 0, Page = page <= 0 ? 1 : page, PageSize = take <= 0 ? 20 : take }; + } + + // 1) Resolver productos por GTIN/Factory/Regulatory si vino GTIN + var productIds = new List(); + if (!string.IsNullOrWhiteSpace(gtin)) + { + var g = gtin.Trim(); + productIds = await _context.PhLsmProducts.AsNoTracking() + .Where(p => p.ExternalCode == g || p.FactoryCode == g || p.RegulatoryCode == g) + .Select(p => p.Id).ToListAsync(); + + // Si se pidió GTIN y no matchea ningún producto, devolvé vacío + if (productIds.Count == 0) + { + return new PagedResult + { Items = [], TotalItems = 0, Page = page <= 0 ? 1 : page, PageSize = take <= 0 ? 20 : take }; + } + } + + // 2) Base query (stock disponible) + var baseQuery = + from si in _context.PhLsmStockItems.AsNoTracking() + join p in _context.PhLsmProducts.AsNoTracking() on si.ProductId equals p.Id + join l in _context.PhLsmStockLocations.AsNoTracking() on si.LocationId equals l.Id into _loc + from loc in _loc.DefaultIfEmpty() + where si.Quantity > 0 + select new { si, p, loc }; + + if (productIds.Count > 0) + baseQuery = baseQuery.Where(x => productIds.Contains(x.p.Id)); + + if (locationId.HasValue) + baseQuery = baseQuery.Where(x => x.si.LocationId == locationId.Value); + + // 3) Reglas por tipo de trazabilidad (sin fuzzy) + baseQuery = baseQuery.Where(x => + // None: solo producto + ubicación + (x.p.TraceabilityType == 1) + // BatchOnly: requiere batch exacto + || (x.p.TraceabilityType == 2 + && !string.IsNullOrWhiteSpace(batch) + && x.si.Batch == batch!.Trim()) + // Batch+Exp: requiere batch + expiration exactos + || (x.p.TraceabilityType == 3 + && !string.IsNullOrWhiteSpace(batch) && expiration.HasValue + && x.si.Batch == batch!.Trim() + && x.si.Expiration.HasValue && x.si.Expiration.Value == expiration.Value) + // SerialUnit: requiere serial exacto (ignora expiration) + || (x.p.TraceabilityType == 4 + && !string.IsNullOrWhiteSpace(serial) + && x.si.Serial != null && x.si.Serial == serial!.Trim()) + // Serial+Exp: requiere serial + expiration exactos + || (x.p.TraceabilityType == 5 + && !string.IsNullOrWhiteSpace(serial) && expiration.HasValue + && x.si.Serial != null && x.si.Serial == serial!.Trim() + && x.si.Expiration.HasValue && x.si.Expiration.Value == expiration.Value) + ); + + // 4) Orden y proyección + baseQuery = baseQuery.OrderBy(x => x.si.Expiration).ThenBy(x => x.p.Name); + + var dtoQuery = baseQuery.Select(x => new StockItemScanResultDto + { + StockItemId = x.si.Id, + ProductId = x.p.Id, + FactoryCode = x.p.FactoryCode ?? string.Empty, + ExternalCode = x.p.ExternalCode, + ProductName = x.p.Name ?? string.Empty, + Description = x.p.Descripcion, + LocationId = x.si.LocationId, + LocationName = x.loc != null ? x.loc.Descripcion : null, + Batch = x.si.Batch, + Expiration = x.si.Expiration, // DateOnly? + Serial = x.si.Serial, // si aplica + TraceabilityType = x.p.TraceabilityType, + AvailableQty = x.si.Quantity, + PlusProcess = x.p.PlusProcess + }); + + page = page <= 0 ? 1 : page; + take = take <= 0 ? 20 : take; + + var paged = await dtoQuery.ToPagedResultAsync(page, take); + + return new PagedResult + { + Items = paged.Items, + TotalItems = paged.TotalItems, + Page = paged.Page, + PageSize = paged.PageSize + }; + } + } +} diff --git a/Models/obj/Models.csproj.nuget.dgspec.json b/Models/obj/Models.csproj.nuget.dgspec.json index 923675c..f603076 100644 --- a/Models/obj/Models.csproj.nuget.dgspec.json +++ b/Models/obj/Models.csproj.nuget.dgspec.json @@ -66,7 +66,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" } } }, @@ -148,7 +148,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" } } } diff --git a/Models/obj/Models.csproj.nuget.g.props b/Models/obj/Models.csproj.nuget.g.props index 9c53f59..fb03bea 100644 --- a/Models/obj/Models.csproj.nuget.g.props +++ b/Models/obj/Models.csproj.nuget.g.props @@ -7,7 +7,7 @@ $(UserProfile)\.nuget\packages\ C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages PackageReference - 6.14.0 + 6.14.1 diff --git a/Models/obj/project.assets.json b/Models/obj/project.assets.json index 13a1f78..8532a64 100644 --- a/Models/obj/project.assets.json +++ b/Models/obj/project.assets.json @@ -4098,7 +4098,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" } } } diff --git a/Services/obj/Services.csproj.nuget.dgspec.json b/Services/obj/Services.csproj.nuget.dgspec.json index 4a3b29b..c01f6c6 100644 --- a/Services/obj/Services.csproj.nuget.dgspec.json +++ b/Services/obj/Services.csproj.nuget.dgspec.json @@ -80,7 +80,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" } } } diff --git a/Services/obj/Services.csproj.nuget.g.props b/Services/obj/Services.csproj.nuget.g.props index c25da7f..ca369a5 100644 --- a/Services/obj/Services.csproj.nuget.g.props +++ b/Services/obj/Services.csproj.nuget.g.props @@ -7,7 +7,7 @@ $(UserProfile)\.nuget\packages\ C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages PackageReference - 6.14.0 + 6.14.1 diff --git a/Services/obj/project.assets.json b/Services/obj/project.assets.json index 1a8ada1..52a4f70 100644 --- a/Services/obj/project.assets.json +++ b/Services/obj/project.assets.json @@ -1318,7 +1318,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" } } } diff --git a/Transversal/Services/Gs1CodeParser.cs b/Transversal/Services/Gs1CodeParser.cs new file mode 100644 index 0000000..ace6321 --- /dev/null +++ b/Transversal/Services/Gs1CodeParser.cs @@ -0,0 +1,181 @@ +using Domain.Dtos.Stock; + +namespace Transversal.Services +{ + public static class Gs1CodeParser + { + // GS1 FNC1 (ASCII 29) + private const char FNC1 = (char)29; + + /// + /// Parsea un string GS1 (GS1-128/EAN-128/DataMatrix) con AIs en cualquier orden. + /// Soporta separadores FNC1 (ASCII 29), '$' y espacios. AIs: (01),(10),(11),(17),(21),(22). + /// + public static Gs1ScanResult Parse(string raw) + { + var result = new Gs1ScanResult + { + Raw = raw?.Trim() ?? string.Empty + }; + + if (string.IsNullOrWhiteSpace(raw)) + return result; + + // 1) Normalizar: $ y espacios -> FNC1; quitar saltos + var s = raw.Trim() + .Replace("\r", "") + .Replace("\n", "") + .Replace("$", FNC1.ToString()); + + // algunos escáneres meten espacios sueltos entre AIs/valores + while (s.Contains(" ")) s = s.Replace(" ", " "); + s = s.Replace(" ", string.Empty); + + int i = 0; + while (i < s.Length) + { + if (s[i] == FNC1) { i++; continue; } + if (i + 2 > s.Length) break; + + // Intentar leer AI de dos dígitos (los que usamos acá) + var ai = s.Substring(i, 2); + i += 2; + + switch (ai) + { + case "01": // GTIN (14 fijos) + if (TryTakeFixed(s, ref i, 14, out var gtin)) + result.Gtin = gtin; + break; + + case "17": // Expiración YYMMDD (6 fijos) + if (TryTakeFixed(s, ref i, 6, out var yymmdd) + && TryParseYyMmDdToDateTime(yymmdd, out var expDt)) + { + result.ExpirationDate = expDt; + } + break; + + case "11": // Fabricación YYMMDD (6 fijos) -> opcional guardar + if (TryTakeFixed(s, ref i, 6, out var mfgYymmdd) + && TryParseYyMmDdToDateTime(mfgYymmdd, out var mfgDt)) + { + // Si querés guardar: agregá ManufacturingDate en tu DTO + // result.ManufacturingDate = mfgDt; + } + break; + + case "10": // Lote (var-length) + result.Lot = TakeVariable(s, ref i); + break; + + case "21": // Serie (var-length) + result.Serial = TakeVariable(s, ref i); + break; + + case "22": // Variante (var-length) + result.Variant = TakeVariable(s, ref i); + break; + + default: + // Heurística: si el char previo a este AI no era FNC1, podría ser que no era AI real. + // Retrocede 1 y avanza de a 1 hasta próximo FNC1 o final. + i -= 1; + SkipUnknownUntilFnc1(s, ref i); + break; + } + } + + // Heurística: 17 seguido de 10 sin FNC1 (p.ej. ...17YYMMDD10LOTE) + if (result.ExpirationDate.HasValue && string.IsNullOrEmpty(result.Lot)) + { + var idx17 = s.IndexOf("17", StringComparison.Ordinal); + if (idx17 >= 0 && idx17 + 8 < s.Length) // "17" + 6 chars de fecha = +8 + { + var after17 = s.Substring(idx17 + 8); + var idx10 = after17.IndexOf("10", StringComparison.Ordinal); + if (idx10 >= 0) + { + var start = idx17 + 8 + idx10 + 2; + var lot = ReadUntilFnc1OrEnd(s, start); + if (!string.IsNullOrEmpty(lot)) + result.Lot = lot; + } + } + } + + return result; + } + + // === Helpers === + + private static bool TryTakeFixed(string s, ref int i, int length, out string value) + { + value = string.Empty; + if (i + length > s.Length) return false; + value = s.Substring(i, length); + i += length; + return true; + } + + /// + /// Extrae un valor de longitud variable (por ejemplo, Batch o Serial) del código GS1. + /// Cortamos únicamente cuando encontramos un separador FNC1. + /// No se intenta detectar un posible AI dentro del valor, ya que hay casos donde el valor + /// legítimamente contiene secuencias como "22" o "17" que podrían confundirse con un AI. + /// Ejemplo: un lote "52360227" no debe cortarse en "52360" al detectar "22". + /// + private static string TakeVariable(string s, ref int i) + { + int start = i; + while (i < s.Length && s[i] != FNC1) i++; + return s.Substring(start, i - start); + } + + private static bool LooksLikeNextAi(string s, int index) + { + if (index + 1 >= s.Length) return false; + // AIs que nos interesan acá arrancan con 0–2 y son de 2 dígitos (01,10,11,17,21,22) + return char.IsDigit(s[index]) && char.IsDigit(s[index + 1]) && + (s[index] == '0' || s[index] == '1' || s[index] == '2') && + (s.Substring(index, 2) is "01" or "10" or "11" or "17" or "21" or "22"); + } + + private static void SkipUnknownUntilFnc1(string s, ref int i) + { + while (i < s.Length && s[i] != FNC1) i++; + if (i < s.Length && s[i] == FNC1) i++; // consumir FNC1 si lo hay + } + + private static bool TryParseYyMmDdToDateTime(string yymmdd, out DateTime dt) + { + dt = default; + if (yymmdd?.Length != 6) return false; + + int yy = int.Parse(yymmdd.Substring(0, 2)); + int mm = int.Parse(yymmdd.Substring(2, 2)); + int dd = int.Parse(yymmdd.Substring(4, 2)); + + // GS1 usa 00–99; se suele mapear a 2000–2099. Ajustá si necesitás 1990–1999 para 90–99. + int year = 2000 + yy; + + try + { + dt = new DateTime(year, mm, dd); + return true; + } + catch + { + return false; + } + } + + private static string ReadUntilFnc1OrEnd(string s, int start) + { + int i = start; + while (i < s.Length && s[i] != FNC1) i++; + return s.Substring(start, i - start); + } + + } +} diff --git a/Transversal/obj/Transversal.csproj.nuget.g.props b/Transversal/obj/Transversal.csproj.nuget.g.props index 3763305..f7a31ae 100644 --- a/Transversal/obj/Transversal.csproj.nuget.g.props +++ b/Transversal/obj/Transversal.csproj.nuget.g.props @@ -7,7 +7,7 @@ $(UserProfile)\.nuget\packages\ C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages PackageReference - 6.14.0 + 6.14.1 diff --git a/phronCare.API/Controllers/Sales/LookUpController.cs b/phronCare.API/Controllers/Sales/LookUpController.cs index eae4f69..02a9f40 100644 --- a/phronCare.API/Controllers/Sales/LookUpController.cs +++ b/phronCare.API/Controllers/Sales/LookUpController.cs @@ -37,5 +37,9 @@ namespace phronCare.API.Controllers.Sales [HttpGet("paymentterms")] public Task> PaymentTerms() => _lookup.PaymentTermsListAsync(""); // o sin parámetro si lo hacés opcional + + [HttpGet("approvedquotes")] + public Task> ApprovedQuotes([FromQuery] string q) + => _lookup.ApprovedQuotesListAsync(q); } } diff --git a/phronCare.API/Controllers/Sales/QuoteController.cs b/phronCare.API/Controllers/Sales/QuoteController.cs index 07fbdc7..b007daa 100644 --- a/phronCare.API/Controllers/Sales/QuoteController.cs +++ b/phronCare.API/Controllers/Sales/QuoteController.cs @@ -94,7 +94,30 @@ namespace phronCare.API.Controllers.Sales } } + /// + /// Devuelve un presupuesto DTO con información básica a partir del número de presupuesto. + /// + [HttpGet("summary/{quoteNumber}")] + public async Task> GetHeaderDtoByQuoteNumber(string quoteNumber) + { + try + { + var dto = await _quoteService.GetDtoByQuoteNumberAsync(quoteNumber); + if (dto == null) + return NotFound($"No se encontró un presupuesto con el número: {quoteNumber}"); + return Ok(dto); + } + catch (Exception ex) + { + var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + return StatusCode(500, $"{methodName} Message: {ex.Message}"); + } + } + + /// + /// Genera y devuelve un archivo PDF correspondiente al presupuesto especificado por su ID. + /// [HttpGet("{id}/pdf")] public async Task GetQuotePdf(int id) { diff --git a/phronCare.API/Controllers/Stock/LSStockScanController.cs b/phronCare.API/Controllers/Stock/LSStockScanController.cs new file mode 100644 index 0000000..14f6721 --- /dev/null +++ b/phronCare.API/Controllers/Stock/LSStockScanController.cs @@ -0,0 +1,47 @@ +using Core.Interfaces.Stock; // ILSStockScanDom +using Domain.Dtos.Stock; // StockItemSearchParams, StockItemScanResultDto +using Domain.Generics; // PagedResult +using Microsoft.AspNetCore.Mvc; + +namespace API.Controllers.Stock +{ + [Route("api/[controller]")] + [ApiController] + public class LSStockScanController : ControllerBase + { + private readonly ILSStockScanDom _service; + + public LSStockScanController(ILSStockScanDom service) + { + _service = service; + } + + /// + /// Búsqueda paginada de ítems de stock por código/texto, lote y filtros opcionales. + /// + [HttpPost("search")] + public async Task>> Search([FromBody] StockItemSearchParams searchParams) + { + var result = await _service.SearchAsync(searchParams); + return Ok(result); + } + + /// Realiza una búsqueda paginada de ítems de stock utilizando datos ya parseados + /// (por ejemplo, provenientes de un código GS1 escaneado). + /// + /// + /// Parámetros de búsqueda ya procesados y listos para filtrar en base de datos, + /// incluyendo código de producto, lote, fecha de vencimiento, ubicación, etc. + /// + /// + /// Lista paginada de ítems de stock que cumplen con los filtros especificados. + /// + + [HttpPost("search-parsed")] + public async Task>> SearchParsed([FromBody] StockItemParsedSearchParams searchParams) + { + var result = await _service.SearchParsedAsync(searchParams); + return Ok(result); + } + } +} diff --git a/phronCare.API/Program.cs b/phronCare.API/Program.cs index 0fd06db..75f9f81 100644 --- a/phronCare.API/Program.cs +++ b/phronCare.API/Program.cs @@ -271,5 +271,7 @@ static void RepositorysAndServices(WebApplicationBuilder builder) 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/Resources/Templates/Stock/plantilla_productos.xlsx b/phronCare.API/Resources/Templates/Stock/plantilla_productos.xlsx index 7019c442b260493859a3cccdb53382a401789fad..3cd35a9a2bb2991898fbaad60af7668be4994d50 100644 GIT binary patch literal 8906 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 F`ad7>TOa@c literal 5245 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 z7NM;P150NVu$(UserProfile)\.nuget\packages\ C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages PackageReference - 6.14.0 + 6.14.1 diff --git a/phronCare.API/obj/project.assets.json b/phronCare.API/obj/project.assets.json index 7dd1735..b6403c5 100644 --- a/phronCare.API/obj/project.assets.json +++ b/phronCare.API/obj/project.assets.json @@ -9811,7 +9811,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" } } } diff --git a/phronCare.Test/obj/phronCare.Test.csproj.nuget.dgspec.json b/phronCare.Test/obj/phronCare.Test.csproj.nuget.dgspec.json index b6ad43e..c0efb3d 100644 --- a/phronCare.Test/obj/phronCare.Test.csproj.nuget.dgspec.json +++ b/phronCare.Test/obj/phronCare.Test.csproj.nuget.dgspec.json @@ -66,7 +66,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" } } }, @@ -166,7 +166,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" } } }, @@ -246,7 +246,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" } } } diff --git a/phronCare.Test/obj/phronCare.Test.csproj.nuget.g.props b/phronCare.Test/obj/phronCare.Test.csproj.nuget.g.props index d20173c..db018e3 100644 --- a/phronCare.Test/obj/phronCare.Test.csproj.nuget.g.props +++ b/phronCare.Test/obj/phronCare.Test.csproj.nuget.g.props @@ -7,7 +7,7 @@ $(UserProfile)\.nuget\packages\ C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages PackageReference - 6.14.0 + 6.14.1 diff --git a/phronCare.Test/obj/project.assets.json b/phronCare.Test/obj/project.assets.json index bcd1d21..e5f6580 100644 --- a/phronCare.Test/obj/project.assets.json +++ b/phronCare.Test/obj/project.assets.json @@ -2503,7 +2503,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" } } } diff --git a/phronCare.UIBlazor/Layout/NavMenu.razor b/phronCare.UIBlazor/Layout/NavMenu.razor index 73376ca..ffaf995 100644 --- a/phronCare.UIBlazor/Layout/NavMenu.razor +++ b/phronCare.UIBlazor/Layout/NavMenu.razor @@ -92,7 +92,7 @@ diff --git a/phronCare.UIBlazor/Pages/Authorization/LoginPage.razor b/phronCare.UIBlazor/Pages/Authorization/LoginPage.razor index fafe97f..d4a518a 100644 --- a/phronCare.UIBlazor/Pages/Authorization/LoginPage.razor +++ b/phronCare.UIBlazor/Pages/Authorization/LoginPage.razor @@ -73,7 +73,7 @@
-
diff --git a/phronCare.UIBlazor/Pages/Stock/Expeditions/ExpeditionCreate.razor b/phronCare.UIBlazor/Pages/Stock/Expeditions/ExpeditionCreate.razor new file mode 100644 index 0000000..a02977c --- /dev/null +++ b/phronCare.UIBlazor/Pages/Stock/Expeditions/ExpeditionCreate.razor @@ -0,0 +1,276 @@ +@page "/stock/expeditions/create" +@using Blazored.Typeahead +@using Domain.Dtos.Stock +@using Domain.Entities +@using Services.Lookups +@using Services.Stock.Expeditions +@using phronCare.UIBlazor.Pages.Stock.Shared + +@* @using static phronCare.UIBlazor.Pages.Stock.Shared.StockItemSelectorModal + *@ +@inject NavigationManager Navigation +@inject ExpeditionService expeditionService +@* @inject Lookup lookUpService *@ +@inject ISalesLookupService lookUpService +@inject IToastService toastService +@inject IModalService Modal + + + + + + + +
+
+

Nueva Expedición

+
+ +
+
+
+ + + + @item.Nombre + + + @item.Nombre + + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + @if (!string.IsNullOrWhiteSpace(DispatchInstruction)) + { +
+ Instrucciones desde presupuesto:
+ @DispatchInstruction +
+ } +
+
+ +
+
+
Productos a Expedir
+
+ + + +
+
+ +
+ @if (Details.Any()) + { + + + + + + + + + + + + + + @foreach (var item in Details) + { + + + + + + + + + + } + +
ProductoCant.LoteSerialVencimientoUbicación
@item.ProductId@item.Quantity@item.Batch@item.Serial@item.Expiration?.ToString("yyyy-MM-dd")@item.LocationId + +
+ } + else + { +

No hay productos agregados.

+ } +
+
+ +
+ +
+
+ +@code { + private ELSExpeditionHeader Model = new(); + private ExtraInfoModel ExtraInfo = new(); + private string DispatchInstruction = string.Empty; + private ELookUpItem? SelectedQuote; + private List Details = new(); + + private List ProductSetItems = new(); + + + private string ticketIdString + { + get => Model.TicketId?.ToString() ?? string.Empty; + set => Model.TicketId = Guid.TryParse(value, out var guid) ? guid : null; + } + + private async Task> SearchQuotes(string filter) + { + return await lookUpService.SearchApprovedQuotesAsync(filter); + } + + private async Task OnQuoteSelected(ELookUpItem? selected) + { + if (selected is null || string.IsNullOrWhiteSpace(selected.Nombre)) + { + SelectedQuote = null; + ExtraInfo = new(); // Limpiar datos cargados + DispatchInstruction = ""; + return; + } + + SelectedQuote = selected; + + var quoteNumber = selected.Nombre.Split(" - ")[0]; + var quote = await expeditionService.GetQuoteByNumberAsync(quoteNumber); + + if (quote is null) + { + toastService.ShowError("No se pudo cargar el presupuesto."); + return; + } + + ExtraInfo.Professional = quote.ProfessionalName; + ExtraInfo.Institution = quote.InstitutionName; + ExtraInfo.Patient = quote.PatientName; + ExtraInfo.SurgeryDate = quote.EstimatedDate; + DispatchInstruction = quote.Observations ?? ""; + } + + private void AddProduct() + { + // TODO: abrir modal de producto individual + } + + private void AddSet() + { + // TODO: abrir modal de set + } + + private void ScanProduct() + { + // TODO: activar input de escáner + } + + private void RemoveItem(ELSExpeditionDetail item) + { + Details.Remove(item); + } + + private async Task HandleValidSubmit() + { + // TODO: Lógica de guardado de la expedición completa + } + private async Task OpenStockItemSelectorModal() + { + + var parameters = new ModalParameters(); + parameters.Add(nameof(StockItemSelectorModal.SetItems), ProductSetItems); // o null + //parameters.Add(nameof(StockItemSelectorModal.LocationId), SelectedLocationId); + + var options = new ModalOptions() + { + Size = ModalSize.Large, + HideHeader = true + }; + var modal = Modal.Show("", parameters, options); + + var result = await modal.Result; + + if (!result.Cancelled && result.Data is List selectedItems) + { + foreach (var s in selectedItems) + { + var detail = new ELSExpeditionDetail + { + ProductId = s.ProductId, + Quantity = s.Quantity, // si es Serial*, probablemente 1 + Batch = s.Batch, + Expiration = s.Expiration.HasValue + ? DateOnly.FromDateTime(s.Expiration.Value) + : (DateOnly?)null, + Serial = s.Serial, // si es Serial*, probablemente null + LocationId = s.LocationId // si tu detalle lo maneja + }; + + Details.Add(detail); + } + + StateHasChanged(); + toastService.ShowSuccess($"{selectedItems.Count} item(s) agregados a la expedición."); + } + + } + + + private class ExtraInfoModel + { + public string? Professional { get; set; } + public string? Institution { get; set; } + public string? Patient { get; set; } + public DateTime? SurgeryDate { get; set; } + } +} diff --git a/phronCare.UIBlazor/Pages/Stock/LSProduct.razor b/phronCare.UIBlazor/Pages/Stock/LSProduct.razor index f6a5bd7..82addbb 100644 --- a/phronCare.UIBlazor/Pages/Stock/LSProduct.razor +++ b/phronCare.UIBlazor/Pages/Stock/LSProduct.razor @@ -138,7 +138,7 @@ private List TableColumns = new() { - "Id", "Código Fábrica", "Código Externo", "Nombre", "Descripción", "División", "Unidad", "Tipo", "Trazabilidad", "Esteriliza" + "Id", "Código Fábrica", "Código Externo", "Código Regulatorio", "Nombre", "Descripción", "División", "Unidad", "Tipo", "Trazabilidad", "Esteriliza" }; protected override async Task OnInitializedAsync() @@ -176,6 +176,7 @@ { "Id", p.Id }, { "Código Fábrica", p.FactoryCode }, { "Código Externo", p.ExternalCode?? string.Empty }, + { "Código Regulatorio", p.RegulatoryCode?? string.Empty }, { "Nombre", p.Name?? string.Empty }, { "Descripción", p.Descripcion }, { "División", p.Division?.Name ?? "" }, diff --git a/phronCare.UIBlazor/Pages/Stock/LSProductForm.razor b/phronCare.UIBlazor/Pages/Stock/LSProductForm.razor index fe74f98..cbda891 100644 --- a/phronCare.UIBlazor/Pages/Stock/LSProductForm.razor +++ b/phronCare.UIBlazor/Pages/Stock/LSProductForm.razor @@ -24,14 +24,18 @@
-
+
-
+
+
+ + +
diff --git a/phronCare.UIBlazor/Pages/Stock/Shared/StockItemSelectorModal.razor b/phronCare.UIBlazor/Pages/Stock/Shared/StockItemSelectorModal.razor new file mode 100644 index 0000000..4ea29d9 --- /dev/null +++ b/phronCare.UIBlazor/Pages/Stock/Shared/StockItemSelectorModal.razor @@ -0,0 +1,221 @@ +@using Blazored.Modal +@using Blazored.Modal.Services +@using Domain.Dtos.Stock +@using Microsoft.AspNetCore.Components.Web + +@inject IToastService toastService +@inject IStockScanService stockScanService +@inject IModalService Modal + +@inherits LayoutComponentBase + + + + + + + +@code { + [CascadingParameter] BlazoredModalInstance ModalInstance { get; set; } = default!; + + [Parameter] public int? ProductId { get; set; } + [Parameter] public int? LocationId { get; set; } + [Parameter] public List? SetItems { get; set; } + + private string InputCode { get; set; } = string.Empty; + private ElementReference scanInput; + + private readonly List SelectedItems = new(); + private List StockList = new(); + + protected override async Task OnInitializedAsync() + { + await LoadMockStock(); + } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + await scanInput.FocusAsync(); + } + private async Task OnKeyDown(KeyboardEventArgs e) + { + if (e.Key == "Enter") + await HandleScan(); + } + private async Task HandleKeyDown(KeyboardEventArgs e) + { + if (e.Key is "Enter" or "NumpadEnter") + { + await HandleScan(); + await scanInput.FocusAsync(); + } + } + private async Task HandleScan() + { + var code = (InputCode ?? string.Empty).Trim(); // limpia CR/LF del lector + if (string.IsNullOrWhiteSpace(code)) + { + toastService.ShowWarning("Ingrese un código válido."); + await Refocus(); + return; + } + + try + { + var matchedItem = await stockScanService.ParseAndMatchAsync(code, LocationId ?? 1); + + if (matchedItem is null) + { + toastService.ShowWarning("No se encontró el producto en stock."); + await Refocus(); + return; + } + + if (StockList.Any(s => s.StockItemId == matchedItem.StockItemId)) + { + toastService.ShowInfo("Este ítem ya está listado."); + await Refocus(); + return; + } + + StockList.Insert(0, new StockDisplayRow + { + StockItemId = matchedItem.StockItemId, + ProductId = matchedItem.ProductId, + ProductName = matchedItem.ProductName, + Batch = matchedItem.Batch, + Expiration = matchedItem.Expiration, + Available = matchedItem.Quantity, + Selected = 1, + LocationId = matchedItem.LocationId + }); + } + catch (Exception ex) + { + toastService.ShowError($"Error en escaneo: {ex.Message}"); + } + finally + { + InputCode = string.Empty; + await Refocus(); + StateHasChanged(); + } + } + + private async Task Refocus() + { + await Task.Yield(); // asegura que el DOM está listo + await scanInput.FocusAsync(); // deja el cursor listo para el próximo escaneo + } + + private Task Cancel() => ModalInstance.CancelAsync(); + + private async Task Confirm() + { + var selected = StockList + .Where(x => x.Selected > 0) + .Select(x => new StockItemSelectionDto + { + StockItemId = x.StockItemId, + ProductId = x.ProductId, + ProductName = x.ProductName, + Batch = x.Batch, + Expiration = x.Expiration, + Quantity = x.Selected, + LocationId = x.LocationId + }) + .ToList(); + + if (!selected.Any()) + { + toastService.ShowWarning("No se seleccionó ningún producto."); + return; + } + + await ModalInstance.CloseAsync(ModalResult.Ok(selected)); + } + + private async Task LoadMockStock() + { + StockList = new List + { + new StockDisplayRow { StockItemId = 101, ProductId = 1, ProductName = "Tornillo 4mm x 20mm", Batch = "LOTE001", Expiration = DateTime.Today.AddMonths(12), Available = 5, Selected = 0, LocationId = LocationId ?? 1 }, + new StockDisplayRow { StockItemId = 102, ProductId = 1, ProductName = "Tornillo 4mm x 20mm", Batch = "LOTE002", Expiration = DateTime.Today.AddMonths(18), Available = 10, Selected = 0, LocationId = LocationId ?? 1 }, + new StockDisplayRow { StockItemId = 103, ProductId = 2, ProductName = "Placa LCP 6 orificios", Batch = "PL001-A", Expiration = DateTime.Today.AddYears(2), Available = 3, Selected = 0, LocationId = LocationId ?? 1 } + }; + await Task.CompletedTask; + } + + public class StockDisplayRow + { + public int StockItemId { get; set; } + public int ProductId { get; set; } + public string ProductName { get; set; } = string.Empty; + public string Batch { get; set; } = string.Empty; + public DateTime? Expiration { get; set; } + public decimal Available { get; set; } + public decimal Selected { get; set; } + public int LocationId { get; set; } + } +} diff --git a/phronCare.UIBlazor/Pages/Stock/Shared/StockScanModal.razor b/phronCare.UIBlazor/Pages/Stock/Shared/StockScanModal.razor new file mode 100644 index 0000000..7594a9e --- /dev/null +++ b/phronCare.UIBlazor/Pages/Stock/Shared/StockScanModal.razor @@ -0,0 +1,123 @@ +@using Blazored.Modal +@using Blazored.Modal.Services +@using Domain.Dtos.Stock +@inject IStockScanService StockScanService + +@inherits LayoutComponentBase + + + + + + + +@code { + [CascadingParameter] BlazoredModalInstance ModalInstance { get; set; } + + [Parameter] public int? LocationId { get; set; } + private string SearchAddress { get; set; } = string.Empty; + + private string ScanInput { get; set; } = string.Empty; + private string ErrorMessage { get; set; } = string.Empty; + + private List ScanResults { get; set; } = new(); + + private async Task HandleScan() + { + ErrorMessage = string.Empty; + ScanResults.Clear(); + + if (string.IsNullOrWhiteSpace(ScanInput)) + { + ErrorMessage = "Ingrese un código válido."; + return; + } + + if (LocationId is null) + { + ErrorMessage = "Falta el depósito para escanear correctamente."; + return; + } + + try + { + var result = await StockScanService.ParseAndMatchAsync(ScanInput, LocationId.Value); + + if (result is not null) + { + ScanResults.Add(result); + } + else + { + ErrorMessage = "No se encontró stock coincidente."; + } + } + catch (Exception ex) + { + ErrorMessage = $"Error: {ex.Message}"; + } + } + + private void Cancel() => ModalInstance.CancelAsync(); + + private void ConfirmSelection() + { + var selected = ScanResults.Where(r => r.Quantity > 0).ToList(); + ModalInstance.CloseAsync(ModalResult.Ok(selected)); + } +} diff --git a/phronCare.UIBlazor/Program.cs b/phronCare.UIBlazor/Program.cs index 7b82878..342ef1c 100644 --- a/phronCare.UIBlazor/Program.cs +++ b/phronCare.UIBlazor/Program.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Blazored.Modal; using Blazored.Toast; using phronCare.UIBlazor.Services.Stock; +using phronCare.UIBlazor.Services.Stock.Expeditions; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); builder.RootComponents.Add("head::after"); @@ -48,6 +49,7 @@ static void InjectDependencies(WebAssemblyHostBuilder builder) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -67,6 +69,7 @@ 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/ISalesLookupService .cs b/phronCare.UIBlazor/Services/Lookups/ISalesLookupService .cs index 5e50c93..cc35f07 100644 --- a/phronCare.UIBlazor/Services/Lookups/ISalesLookupService .cs +++ b/phronCare.UIBlazor/Services/Lookups/ISalesLookupService .cs @@ -14,5 +14,6 @@ namespace phronCare.UIBlazor.Services.Lookups Task> GetAdjustmentReasonsAsync(); Task> GetTaxTypesAsync(); Task> GetPaymentTermsAsync(); + Task> SearchApprovedQuotesAsync(string filtro); } } diff --git a/phronCare.UIBlazor/Services/Lookups/SalesLookupService.cs b/phronCare.UIBlazor/Services/Lookups/SalesLookupService.cs index 59edd2f..2d614bf 100644 --- a/phronCare.UIBlazor/Services/Lookups/SalesLookupService.cs +++ b/phronCare.UIBlazor/Services/Lookups/SalesLookupService.cs @@ -35,7 +35,6 @@ namespace phronCare.UIBlazor.Services.Lookups var items = await _http.GetFromJsonAsync(url); return items ?? Array.Empty(); } - public Task> SearchProductsAsync(string filtro) => FetchProductsAsync($"api/lookup/products?q={Uri.EscapeDataString(filtro)}"); public async Task> GetAdjustmentReasonsAsync() @@ -43,7 +42,6 @@ namespace phronCare.UIBlazor.Services.Lookups var items = await _http.GetFromJsonAsync("api/adjustmentreason/getall"); return items ?? Array.Empty(); } - private async Task> FetchProductsAsync(string url) { var items = await _http.GetFromJsonAsync(url); @@ -59,5 +57,7 @@ namespace phronCare.UIBlazor.Services.Lookups var items = await _http.GetFromJsonAsync("api/lookup/paymentterms?q="); return items ?? Array.Empty(); } + public Task> SearchApprovedQuotesAsync(string filtro) + => FetchAsync($"api/lookup/approvedquotes?q={Uri.EscapeDataString(filtro)}", filtro); } } diff --git a/phronCare.UIBlazor/Services/Sales/Quotes/IQuoteService.cs b/phronCare.UIBlazor/Services/Sales/Quotes/IQuoteService.cs index 93578f1..8944e1f 100644 --- a/phronCare.UIBlazor/Services/Sales/Quotes/IQuoteService.cs +++ b/phronCare.UIBlazor/Services/Sales/Quotes/IQuoteService.cs @@ -5,7 +5,7 @@ namespace Services.Sales.Quotes { public interface IQuoteService { - Task CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId); + //Task CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId); // Aquí podrías agregar otros métodos: GetById, Search, etc. } } diff --git a/phronCare.UIBlazor/Services/Sales/Quotes/QuoteService.cs b/phronCare.UIBlazor/Services/Sales/Quotes/QuoteService.cs index edd5378..ea62446 100644 --- a/phronCare.UIBlazor/Services/Sales/Quotes/QuoteService.cs +++ b/phronCare.UIBlazor/Services/Sales/Quotes/QuoteService.cs @@ -124,6 +124,9 @@ namespace phronCare.UIBlazor.Services.Sales.Quotes throw new Exception($"ExportPdfAsync: {message}", ex); } } + /// + /// Envía una solicitud al backend para autorizar un presupuesto específico. + /// public async Task AuthorizeQuoteAsync(QuoteAuthorizationRequest request) { var response = await _http.PostAsJsonAsync("/api/quote/authorize", request); @@ -136,6 +139,7 @@ namespace phronCare.UIBlazor.Services.Sales.Quotes return true; } + /// /// Obtiene un presupuesto completo por ID para su visualización y autorización. /// @@ -152,6 +156,7 @@ namespace phronCare.UIBlazor.Services.Sales.Quotes return null; } } + } public class CreateQuoteResult diff --git a/phronCare.UIBlazor/Services/Stock/Expeditions/ExpeditionService.cs b/phronCare.UIBlazor/Services/Stock/Expeditions/ExpeditionService.cs new file mode 100644 index 0000000..ba8849a --- /dev/null +++ b/phronCare.UIBlazor/Services/Stock/Expeditions/ExpeditionService.cs @@ -0,0 +1,34 @@ +using Domain.Dtos; +using Microsoft.JSInterop; +using System.Net.Http.Json; + +namespace phronCare.UIBlazor.Services.Stock.Expeditions +{ + public class ExpeditionService + { + private readonly IJSRuntime _js; + private readonly HttpClient _http; + public ExpeditionService(HttpClient http, IJSRuntime js) + { + _js = js; + _http = http; + } + /// + /// Obtiene un presupuesto por QuoteNumber. + /// + public async Task GetQuoteByNumberAsync(string quoteNumber) + { + try + { + var result = await _http.GetFromJsonAsync($"api/quote/summary/{quoteNumber}"); + return result; + } + catch (Exception ex) + { + Console.WriteLine($"Error al obtener QuoteDto por QuoteNumber: {ex.Message}"); + return null; + } + } + + } +} diff --git a/phronCare.UIBlazor/Services/Stock/Expeditions/IExpeditionService.cs b/phronCare.UIBlazor/Services/Stock/Expeditions/IExpeditionService.cs new file mode 100644 index 0000000..f298102 --- /dev/null +++ b/phronCare.UIBlazor/Services/Stock/Expeditions/IExpeditionService.cs @@ -0,0 +1,6 @@ +namespace phronCare.UIBlazor.Services.Stock.Expeditions +{ + public interface IExpeditionService + { + } +} diff --git a/phronCare.UIBlazor/Services/Stock/IStockScanService.cs b/phronCare.UIBlazor/Services/Stock/IStockScanService.cs new file mode 100644 index 0000000..747fed9 --- /dev/null +++ b/phronCare.UIBlazor/Services/Stock/IStockScanService.cs @@ -0,0 +1,7 @@ +using Domain.Dtos.Stock; + +public interface IStockScanService +{ + Task ParseAndMatchAsync(string rawInput, int locationId); +} + diff --git a/phronCare.UIBlazor/Services/Stock/MockStockScanService.cs b/phronCare.UIBlazor/Services/Stock/MockStockScanService.cs new file mode 100644 index 0000000..7f31248 --- /dev/null +++ b/phronCare.UIBlazor/Services/Stock/MockStockScanService.cs @@ -0,0 +1,30 @@ +using Domain.Dtos.Stock; + +public class MockStockScanService : IStockScanService +{ + public async Task ParseAndMatchAsync(string rawInput, int locationId) + { + // Simula lógica de parseo de código escaneado + await Task.Delay(100); // simula delay + + if (string.IsNullOrWhiteSpace(rawInput)) + return null; + + // Simulación: si empieza con 0108... devolvemos un producto de prueba + if (rawInput.StartsWith("0108")) + { + return new StockItemSelectionDto + { + StockItemId = 999, + ProductId = 88, + ProductName = "Tornillo 6x30 mm", + Batch = "LOTE-MOCK", + Expiration = DateTime.Today.AddMonths(10), + Quantity = 10, + LocationId = locationId + }; + } + + return null; + } +} diff --git a/phronCare.UIBlazor/Services/Stock/StockScanService.cs b/phronCare.UIBlazor/Services/Stock/StockScanService.cs new file mode 100644 index 0000000..fd5d2e8 --- /dev/null +++ b/phronCare.UIBlazor/Services/Stock/StockScanService.cs @@ -0,0 +1,110 @@ +using System.Net.Http.Json; +using Domain.Dtos.Stock; +using Domain.Generics; +using Transversal.Services; + +public class StockScanService : IStockScanService +{ + private readonly HttpClient _http; + + public StockScanService(HttpClient http) + { + _http = http; + } + + public async Task ParseAndMatchAsync(string rawInput, int locationId) + { + if (string.IsNullOrWhiteSpace(rawInput)) + return null; + + try + { + //var parsed = Gs1CodeParser.Parse(rawInput); + //var raw = rawInput.Trim(); + + //// usar raw como GTIN solo si NO hay AIs parseados + //bool hasParsedAis = !string.IsNullOrWhiteSpace(parsed.Lot) + // || parsed.ExpirationDate.HasValue + // || !string.IsNullOrWhiteSpace(parsed.Serial); + + //string? gtinToSend = parsed.Gtin; + //if (gtinToSend is null && !hasParsedAis && IsPlainCode(raw)) + // gtinToSend = raw; // ej: factory_code tipeado ("336005") + var parsed = Gs1CodeParser.Parse(rawInput); + var raw = rawInput.Trim(); + + bool hasParsedAis = !string.IsNullOrWhiteSpace(parsed.Lot) + || parsed.ExpirationDate.HasValue + || !string.IsNullOrWhiteSpace(parsed.Serial) + || !string.IsNullOrWhiteSpace(parsed.Variant); // incluir (22) + + string? gtinToSend = parsed.Gtin ?? parsed.Variant; // (22) como fallback + if (gtinToSend is null && !hasParsedAis && IsPlainCode(raw)) + gtinToSend = raw; // código plano tipeado (factory/regulatory) + + + // 3. Armar parámetros de búsqueda + var sp = new StockItemParsedSearchParams + { + Gtin = gtinToSend, + Batch = string.IsNullOrWhiteSpace(parsed.Lot) ? null : parsed.Lot, + Expiration = parsed.ExpirationDate.HasValue + ? DateOnly.FromDateTime(parsed.ExpirationDate.Value) + : null, + Serial = string.IsNullOrWhiteSpace(parsed.Serial) ? null : parsed.Serial, + LocationId = locationId, + Page = 1, + PageSize = 10 + }; + + // 4. Log para depuración (quitar en producción) + Console.WriteLine($"[ParseAndMatchAsync] Gtin={sp.Gtin}, Batch={sp.Batch}, Exp={sp.Expiration}, Serial={sp.Serial}, Loc={sp.LocationId}"); + + // 5. Llamar a la API + var resp = await _http.PostAsJsonAsync("/api/lsstockscan/search-parsed", sp); + if (!resp.IsSuccessStatusCode) + { + var err = await resp.Content.ReadAsStringAsync(); + Console.WriteLine($"[ParseAndMatchAsync] API devolvió error {resp.StatusCode}: {err}"); + return null; + } + + // 6. Leer resultado + var pr = await resp.Content.ReadFromJsonAsync>(); + var first = pr?.Items?.FirstOrDefault(); + if (first == null) + { + Console.WriteLine("[ParseAndMatchAsync] No se encontró ningún ítem que coincida."); + return null; + } + + // 7. Mapear a DTO de selección + return new StockItemSelectionDto + { + StockItemId = first.StockItemId, + ProductId = first.ProductId, + ProductName = first.ProductName, + Batch = first.Batch ?? string.Empty, + Expiration = first.Expiration?.ToDateTime(TimeOnly.MinValue), + Quantity = first.AvailableQty, + LocationId = first.LocationId ?? 0 + }; + } + catch (Exception ex) + { + Console.WriteLine($"[ParseAndMatchAsync] Error inesperado: {ex}"); + throw; + } + + } + private static bool IsPlainCode(string s) + { + // sin FNC1 ($), sin espacios y sin prefijos AI típicos + if (s.Contains('$') || s.Contains((char)29) || s.Contains(' ')) return false; + // evita raws que empiezan como AIs "01","10","17","21","22" + var prefix = s.Length >= 2 ? s[..2] : s; + if (prefix is "01" or "10" or "11" or "17" or "21" or "22") return false; + // permite letras/dígitos y algunos separadores comunes + return s.All(c => char.IsLetterOrDigit(c) || c is '-' or '/' or '_'); + } +} diff --git a/phronCare.UIBlazor/obj/phronCare.UIBlazor.csproj.nuget.dgspec.json b/phronCare.UIBlazor/obj/phronCare.UIBlazor.csproj.nuget.dgspec.json index de18641..b2f80cc 100644 --- a/phronCare.UIBlazor/obj/phronCare.UIBlazor.csproj.nuget.dgspec.json +++ b/phronCare.UIBlazor/obj/phronCare.UIBlazor.csproj.nuget.dgspec.json @@ -66,7 +66,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" } } }, @@ -152,13 +152,13 @@ "Microsoft.NET.ILLink.Tasks": { "suppressParent": "All", "target": "Package", - "version": "[8.0.18, )", + "version": "[8.0.19, )", "autoReferenced": true }, "Microsoft.NET.Sdk.WebAssembly.Pack": { "suppressParent": "All", "target": "Package", - "version": "[9.0.7, )", + "version": "[9.0.8, )", "autoReferenced": true }, "PSC.Blazor.Components.Chartjs": { @@ -180,7 +180,7 @@ "downloadDependencies": [ { "name": "Microsoft.NETCore.App.Runtime.Mono.browser-wasm", - "version": "[8.0.18, 8.0.18]" + "version": "[8.0.19, 8.0.19]" } ], "frameworkReferences": { @@ -188,7 +188,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" } }, "runtimes": { @@ -273,7 +273,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" } } } diff --git a/phronCare.UIBlazor/obj/phronCare.UIBlazor.csproj.nuget.g.props b/phronCare.UIBlazor/obj/phronCare.UIBlazor.csproj.nuget.g.props index 2d56482..917a0b5 100644 --- a/phronCare.UIBlazor/obj/phronCare.UIBlazor.csproj.nuget.g.props +++ b/phronCare.UIBlazor/obj/phronCare.UIBlazor.csproj.nuget.g.props @@ -7,7 +7,7 @@ $(UserProfile)\.nuget\packages\ C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages PackageReference - 6.14.0 + 6.14.1 @@ -33,8 +33,8 @@ - - + + @@ -42,8 +42,8 @@ C:\Users\maski\.nuget\packages\newtonsoft.json\10.0.3 - C:\Users\maski\.nuget\packages\microsoft.net.sdk.webassembly.pack\9.0.7 - C:\Users\maski\.nuget\packages\microsoft.net.illink.tasks\8.0.18 + C:\Users\maski\.nuget\packages\microsoft.net.sdk.webassembly.pack\9.0.8 + C:\Users\maski\.nuget\packages\microsoft.net.illink.tasks\8.0.19 C:\Users\maski\.nuget\packages\microsoft.aspnetcore.components.webassembly.devserver\8.0.6 \ No newline at end of file diff --git a/phronCare.UIBlazor/obj/phronCare.UIBlazor.csproj.nuget.g.targets b/phronCare.UIBlazor/obj/phronCare.UIBlazor.csproj.nuget.g.targets index bc860cd..fe11ce9 100644 --- a/phronCare.UIBlazor/obj/phronCare.UIBlazor.csproj.nuget.g.targets +++ b/phronCare.UIBlazor/obj/phronCare.UIBlazor.csproj.nuget.g.targets @@ -4,7 +4,7 @@ - + diff --git a/phronCare.UIBlazor/obj/project.assets.json b/phronCare.UIBlazor/obj/project.assets.json index ce4db3d..ccfde59 100644 --- a/phronCare.UIBlazor/obj/project.assets.json +++ b/phronCare.UIBlazor/obj/project.assets.json @@ -628,13 +628,13 @@ } } }, - "Microsoft.NET.ILLink.Tasks/8.0.18": { + "Microsoft.NET.ILLink.Tasks/8.0.19": { "type": "package", "build": { "build/Microsoft.NET.ILLink.Tasks.props": {} } }, - "Microsoft.NET.Sdk.WebAssembly.Pack/9.0.7": { + "Microsoft.NET.Sdk.WebAssembly.Pack/9.0.8": { "type": "package", "build": { "build/Microsoft.NET.Sdk.WebAssembly.Pack.props": {}, @@ -2962,13 +2962,13 @@ } } }, - "Microsoft.NET.ILLink.Tasks/8.0.18": { + "Microsoft.NET.ILLink.Tasks/8.0.19": { "type": "package", "build": { "build/Microsoft.NET.ILLink.Tasks.props": {} } }, - "Microsoft.NET.Sdk.WebAssembly.Pack/9.0.7": { + "Microsoft.NET.Sdk.WebAssembly.Pack/9.0.8": { "type": "package", "build": { "build/Microsoft.NET.Sdk.WebAssembly.Pack.props": {}, @@ -5752,10 +5752,10 @@ "microsoft.net.http.headers.nuspec" ] }, - "Microsoft.NET.ILLink.Tasks/8.0.18": { - "sha512": "OiXqr2YIBEV9dsAWEtasK470ALyJ0VxJ9k4MotOxlWV6HeEgrJKYMW4HHj1OCCXvqE0/A25wEKPkpfiBARgDZA==", + "Microsoft.NET.ILLink.Tasks/8.0.19": { + "sha512": "IhHf+zeZiaE5EXRyxILd4qM+Hj9cxV3sa8MpzZgeEhpvaG3a1VEGF6UCaPFLO44Kua3JkLKluE0SWVamS50PlA==", "type": "package", - "path": "microsoft.net.illink.tasks/8.0.18", + "path": "microsoft.net.illink.tasks/8.0.19", "hasTools": true, "files": [ ".nupkg.metadata", @@ -5769,7 +5769,7 @@ "build/Microsoft.NET.ILLink.Analyzers.props", "build/Microsoft.NET.ILLink.Tasks.props", "build/Microsoft.NET.ILLink.targets", - "microsoft.net.illink.tasks.8.0.18.nupkg.sha512", + "microsoft.net.illink.tasks.8.0.19.nupkg.sha512", "microsoft.net.illink.tasks.nuspec", "tools/net472/ILLink.Tasks.dll", "tools/net472/ILLink.Tasks.dll.config", @@ -5803,10 +5803,10 @@ "useSharedDesignerContext.txt" ] }, - "Microsoft.NET.Sdk.WebAssembly.Pack/9.0.7": { - "sha512": "5ehgbqGUERh0JVhTUPwFizw4hIoAglkFk/WMs45djePp16YHP11Vnmx44rOQ3gLW8/aDYN1j+pKdAtcEp4QOcw==", + "Microsoft.NET.Sdk.WebAssembly.Pack/9.0.8": { + "sha512": "+JJYRyS8YoLMzLquskLfdF8RFp2PhN5v+lGCGDLHsywj3JHfSV/Zqo8+T0Fl0Xvoc9Js8YkrAWTnm7M2/3CniA==", "type": "package", - "path": "microsoft.net.sdk.webassembly.pack/9.0.7", + "path": "microsoft.net.sdk.webassembly.pack/9.0.8", "hasTools": true, "files": [ ".nupkg.metadata", @@ -5837,7 +5837,7 @@ "build/Microsoft.NET.Sdk.WebAssembly.Pack.targets", "build/Wasm.web.config", "build/browser.runtimeconfig.template.json", - "microsoft.net.sdk.webassembly.pack.9.0.7.nupkg.sha512", + "microsoft.net.sdk.webassembly.pack.9.0.8.nupkg.sha512", "microsoft.net.sdk.webassembly.pack.nuspec", "tools/net472/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.dll", "tools/net472/Microsoft.NET.WebAssembly.Webcil.dll", @@ -10559,8 +10559,8 @@ "Microsoft.AspNetCore.Components.Authorization >= 8.0.6", "Microsoft.AspNetCore.Components.WebAssembly >= 8.0.6", "Microsoft.AspNetCore.Components.WebAssembly.DevServer >= 8.0.6", - "Microsoft.NET.ILLink.Tasks >= 8.0.18", - "Microsoft.NET.Sdk.WebAssembly.Pack >= 9.0.7", + "Microsoft.NET.ILLink.Tasks >= 8.0.19", + "Microsoft.NET.Sdk.WebAssembly.Pack >= 9.0.8", "PSC.Blazor.Components.Chartjs >= 8.0.8", "Transversal >= 1.0.0" ] @@ -10651,13 +10651,13 @@ "Microsoft.NET.ILLink.Tasks": { "suppressParent": "All", "target": "Package", - "version": "[8.0.18, )", + "version": "[8.0.19, )", "autoReferenced": true }, "Microsoft.NET.Sdk.WebAssembly.Pack": { "suppressParent": "All", "target": "Package", - "version": "[9.0.7, )", + "version": "[9.0.8, )", "autoReferenced": true }, "PSC.Blazor.Components.Chartjs": { @@ -10679,7 +10679,7 @@ "downloadDependencies": [ { "name": "Microsoft.NETCore.App.Runtime.Mono.browser-wasm", - "version": "[8.0.18, 8.0.18]" + "version": "[8.0.19, 8.0.19]" } ], "frameworkReferences": { @@ -10687,7 +10687,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" } }, "runtimes": {