Add StockItemModal v1
Some checks failed
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Failing after 15m47s

This commit is contained in:
Leandro Hernan Rojas 2025-08-18 00:47:37 -03:00
parent 626f57aab3
commit 1c4c241266
82 changed files with 2394 additions and 92 deletions

View File

@ -12,5 +12,7 @@ namespace Core.Interfaces
Task<IEnumerable<ELookUpItem>> BussinessUnitsListAsync(string filter, int limit = 10); Task<IEnumerable<ELookUpItem>> BussinessUnitsListAsync(string filter, int limit = 10);
Task<IEnumerable<EProductLookupItem>> ProductsListAsync(string filter, int limit = 10); Task<IEnumerable<EProductLookupItem>> ProductsListAsync(string filter, int limit = 10);
Task<IEnumerable<ELookUpItem>> PaymentTermsListAsync(string filter, int limit = 10); Task<IEnumerable<ELookUpItem>> PaymentTermsListAsync(string filter, int limit = 10);
Task<IEnumerable<ELookUpItem>> ApprovedQuotesListAsync(string filter, int limit = 10);
} }
} }

View File

@ -7,12 +7,8 @@ namespace Models.Interfaces
public interface IQuoteDom public interface IQuoteDom
{ {
#region Presupuestos #region Presupuestos
//Task<PagedResult<EQuoteHeader>> GetAllQuotesAsync(int page = 1, int pageSize = 50);
//Task<EQuoteHeader?> GetQuoteByIdAsync(int id);
//Task<IEnumerable<EQuoteHeader>> GetQuotesByCustomerAsync(int customerId);
Task<PagedResult<QuoteDto>> 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<PagedResult<QuoteDto>> 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<QuoteDto?> GetDtoByQuoteNumberAsync(string quoteNumber);
//Task DeleteQuoteAsync(int id);
#endregion #endregion
#region Exportación #region Exportación
//Task<byte[]> ExportFilteredQuotesToExcelAsync(QuoteSearchParams searchParams); //Task<byte[]> ExportFilteredQuotesToExcelAsync(QuoteSearchParams searchParams);

View File

@ -0,0 +1,12 @@
using Domain.Dtos.Stock;
using Domain.Generics;
namespace Core.Interfaces.Stock
{
public interface ILSStockScanDom
{
Task<PagedResult<StockItemScanResultDto>> SearchAsync(StockItemSearchParams p);
Task<PagedResult<StockItemScanResultDto>> SearchParsedAsync(StockItemParsedSearchParams p, CancellationToken ct = default);
}
}

View File

@ -24,7 +24,8 @@ namespace Core.Services
_repository.ProductsListAsync(filter); _repository.ProductsListAsync(filter);
public Task<IEnumerable<ELookUpItem>> PaymentTermsListAsync(string filter, int limit = 10) public Task<IEnumerable<ELookUpItem>> PaymentTermsListAsync(string filter, int limit = 10)
=> _repository.PaymentTermsListAsync(filter, limit); => _repository.PaymentTermsListAsync(filter, limit);
public Task<IEnumerable<ELookUpItem>> ApprovedQuotesListAsync(string filter, int limit = 10) =>
_repository.ApprovedQuotesListAsync(filter, limit);
#endregion #endregion
} }
} }

View File

@ -37,6 +37,11 @@ namespace Core.Services
{ {
return await _quoteRepository.GetDtoByIdAsync(id); return await _quoteRepository.GetDtoByIdAsync(id);
} }
//public async Task<QuoteDto?> GetDtoByQuoteNumberAsync(string quoteNumber)
//{
// return await _quoteRepository.GetDtoByIdAsync(quoteNumber);
//}
#endregion #endregion
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos) #region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
@ -74,7 +79,6 @@ namespace Core.Services
return await _quoteRepository.AuthorizeQuoteAsync(quoteId, approvedDetails); return await _quoteRepository.AuthorizeQuoteAsync(quoteId, approvedDetails);
} }
#region Validaciones QuoteCreate #region Validaciones QuoteCreate
private void ValidateQuote(EQuoteHeader quote) private void ValidateQuote(EQuoteHeader quote)
{ {
@ -125,6 +129,10 @@ namespace Core.Services
throw new ArgumentException("El total del presupuesto no puede ser negativo."); throw new ArgumentException("El total del presupuesto no puede ser negativo.");
} }
public async Task<QuoteDto?> GetDtoByQuoteNumberAsync(string quoteNumber)
{
return await _quoteRepository.GetDtoByQuoteNumberAsync(quoteNumber);
}
#endregion #endregion
} }
} }

View File

@ -68,6 +68,7 @@ namespace Core.Services
p.Id, p.Id,
p.FactoryCode, p.FactoryCode,
p.ExternalCode, p.ExternalCode,
p.RegulatoryCode,
p.Name, p.Name,
p.Descripcion, p.Descripcion,
Tipo = p.ProductType == 1 ? "Implantable" : Tipo = p.ProductType == 1 ? "Implantable" :

View File

@ -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<PagedResult<StockItemScanResultDto>> SearchAsync(StockItemSearchParams p)
=> _repo.SearchStockItemsAsync(
p.CodeOrText,
p.Batch,
p.LocationId,
p.ProductType,
p.TraceabilityType,
p.PlusProcess,
p.Page,
p.PageSize);
public Task<PagedResult<StockItemScanResultDto>> 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
}
}

View File

@ -7,7 +7,7 @@
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot> <NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders> <NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle> <NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.0</NuGetToolVersion> <NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.1</NuGetToolVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' "> <ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\maski\.nuget\packages\" /> <SourceRoot Include="C:\Users\maski\.nuget\packages\" />

View File

@ -10,7 +10,12 @@
/// <summary> /// <summary>
/// Número de presupuesto (ej. "Q-00000001"). /// Número de presupuesto (ej. "Q-00000001").
/// </summary> /// </summary>
public string Quotenumber { get; set; } = String.Empty; public string Quotenumber { get; set; } = string.Empty;
/// <summary>
/// Relación con Tickets
/// </summary>
public Guid? TicketId { get; set; }
/// <summary> /// <summary>
/// Fecha de emisión del presupuesto. /// Fecha de emisión del presupuesto.
@ -25,22 +30,22 @@
/// <summary> /// <summary>
/// Nombre completo del cliente asociado. /// Nombre completo del cliente asociado.
/// </summary> /// </summary>
public string CustomerName { get; set; } = String.Empty; public string CustomerName { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Nombre completo del médico responsable. /// Nombre completo del médico responsable.
/// </summary> /// </summary>
public string ProfessionalName { get; set; } = String.Empty; public string ProfessionalName { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Nombre de la institución u hospital. /// Nombre de la institución u hospital.
/// </summary> /// </summary>
public string InstitutionName { get; set; } = String.Empty; public string InstitutionName { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Nombre completo del paciente. /// Nombre completo del paciente.
/// </summary> /// </summary>
public string PatientName { get; set; } = String.Empty; public string PatientName { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Días de validez del presupuesto (desde la fecha de emisión). /// Días de validez del presupuesto (desde la fecha de emisión).
@ -50,18 +55,18 @@
/// <summary> /// <summary>
/// Descripción de la condición de pago (ej. "Contado", "30 días"). /// Descripción de la condición de pago (ej. "Contado", "30 días").
/// </summary> /// </summary>
public string? PaymentTermDescription { get; set; } = String.Empty; public string? PaymentTermDescription { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Nombre de la unidad de negocio. /// Nombre de la unidad de negocio.
/// </summary> /// </summary>
public string BusinessUnitName { get; set; } = String.Empty; public string BusinessUnitName { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Moneda del presupuesto (ej. "ARS", "USD"). /// Moneda del presupuesto (ej. "ARS", "USD").
/// </summary> /// </summary>
public string Currency { get; set; } = String.Empty; public string Currency { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Importe total final (incluye impuestos y ajustes). /// Importe total final (incluye impuestos y ajustes).
@ -71,16 +76,16 @@
/// <summary> /// <summary>
/// Estado actual del presupuesto ("Pendiente", "Aprobado", etc.). /// Estado actual del presupuesto ("Pendiente", "Aprobado", etc.).
/// </summary> /// </summary>
public string Status { get; set; } = String.Empty; public string Status { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Nombre del vendedor o ejecutivo de ventas. /// Nombre del vendedor o ejecutivo de ventas.
/// </summary> /// </summary>
public string SalespersonName { get; set; } = String.Empty; public string SalespersonName { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Nombre del vendedor o ejecutivo de ventas. /// Nombre del vendedor o ejecutivo de ventas.
/// </summary> /// </summary>
public string Observations { get; set; } = String.Empty; public string Observations { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Detalle de los ítems o productos cotizados. /// Detalle de los ítems o productos cotizados.
@ -105,6 +110,6 @@
/// <summary> /// <summary>
/// Logo de la compañia. /// Logo de la compañia.
/// </summary> /// </summary>
public string LogoBase64 { get; set; } = ""; public string LogoBase64 { get; set; } = string.Empty;
} }
} }

View File

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

View File

@ -11,6 +11,7 @@
public string UnitCode { get; set; } = string.Empty; public string UnitCode { get; set; } = string.Empty;
public bool PlusProcess { get; set; } public bool PlusProcess { get; set; }
public string ExternalCode { get; set; } = string.Empty; public string ExternalCode { get; set; } = string.Empty;
public string RegulatoryCode { get; set; } = string.Empty;
public string? ErrorMessage { get; set; } public string? ErrorMessage { get; set; }
public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage); public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage);
} }

View File

@ -0,0 +1,32 @@
namespace Domain.Dtos.Stock
{
/// <summary>
/// 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.
/// </summary>
public class ProductSetItemDto
{
/// <summary>
/// ID del producto asociado al set.
/// </summary>
public int ProductId { get; set; }
/// <summary>
/// Nombre corto o técnico del producto (para mostrar en UI).
/// </summary>
public string ProductName { get; set; } = string.Empty;
/// <summary>
/// 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.
/// </summary>
public decimal DefaultQuantity { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool Mandatory { get; set; }
}
}

View File

@ -0,0 +1,80 @@
namespace Domain.Dtos.Stock
{
/// <summary>
/// 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.
/// </summary>
public class StockItemScanResultDto
{
/// <summary>
/// Identificador único del ítem en la tabla PhLSM_StockItem.
/// </summary>
public int StockItemId { get; set; }
/// <summary>
/// Identificador del producto asociado.
/// </summary>
public int ProductId { get; set; }
/// <summary>
/// Código de fábrica del producto.
/// </summary>
public string FactoryCode { get; set; } = string.Empty;
/// <summary>
/// Código externo o alternativo (si existe).
/// </summary>
public string? ExternalCode { get; set; }
/// <summary>
/// Nombre formal o técnico del producto.
/// </summary>
public string ProductName { get; set; } = string.Empty;
/// <summary>
/// Descripción comercial o de uso.
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Identificador de la ubicación física (depósito, sala, etc.).
/// </summary>
public int? LocationId { get; set; }
/// <summary>
/// Nombre descriptivo de la ubicación.
/// </summary>
public string? LocationName { get; set; }
/// <summary>
/// Número de lote (si aplica).
/// </summary>
public string? Batch { get; set; }
/// <summary>
/// Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante.
/// </summary>
public string? Serial { get; set; }
/// <summary>
/// Fecha de vencimiento (si aplica).
/// </summary>
public DateOnly? Expiration { get; set; }
/// <summary>
/// Tipo de trazabilidad del producto:
/// 1 = No aplica, 2 = Por cantidad, 3 = Por lote y vencimiento.
/// </summary>
public int TraceabilityType { get; set; }
/// <summary>
/// Cantidad disponible actualmente.
/// </summary>
public decimal AvailableQty { get; set; }
/// <summary>
/// Indica si requiere proceso adicional (por ejemplo, esterilización).
/// </summary>
public bool PlusProcess { get; set; }
}
}

View File

@ -0,0 +1,51 @@
namespace Domain.Dtos.Stock
{
/// <summary>
/// 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.
/// </summary>
public class StockItemSelectionDto
{
/// <summary>
/// Identificador del ítem de stock seleccionado (PhLSM_StockItem.id)
/// </summary>
public int StockItemId { get; set; }
/// <summary>
/// Identificador del producto asociado
/// </summary>
public int ProductId { get; set; }
/// <summary>
/// Nombre o descripción corta del producto (solo para visualización en UI)
/// </summary>
public string ProductName { get; set; } = string.Empty;
/// <summary>
/// Lote del stock seleccionado
/// </summary>
public string Batch { get; set; } = string.Empty;
/// <summary>
/// Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante.
/// </summary>
public string? Serial { get; set; }
/// <summary>
/// Fecha de vencimiento (si aplica)
/// </summary>
public DateTime? Expiration { get; set; }
/// <summary>
/// Cantidad que el usuario desea usar (puede ser decimal si es por peso o volumen)
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// Identificador del depósito o ubicación de donde se toma el ítem
/// </summary>
public int LocationId { get; set; }
}
}

View File

@ -0,0 +1,69 @@
namespace Domain.Entities
{
public class ELSExpeditionDetail
{
/// <summary>
/// Identificador interno del ítem de expedición
/// </summary>
public int Id { get; set; }
/// <summary>
/// Referencia a la cabecera de expedición (PhLSM_ExpeditionHeaders)
/// </summary>
public int ExpeditionId { get; set; }
/// <summary>
/// Producto médico a despachar
/// </summary>
public int ProductId { get; set; }
/// <summary>
/// Cantidad solicitada del producto
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// Ubicación específica desde donde se despacha este ítem
/// </summary>
public int LocationId { get; set; }
/// <summary>
/// Número de lote (si aplica trazabilidad)
/// </summary>
public string? Batch { get; set; }
/// <summary>
/// Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante.
/// </summary>
public string? Serial { get; set; }
/// <summary>
/// Fecha de vencimiento del producto (si aplica trazabilidad)
/// </summary>
public DateOnly? Expiration { get; set; }
/// <summary>
/// Descripción libre del ítem (uso interno o impresión)
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Precio estimado unitario del producto (sin efecto contable)
/// </summary>
public decimal? EstimatedUnitprice { get; set; }
/// <summary>
/// Moneda del precio estimado (ej: ARS, USD)
/// </summary>
public string? EstimatedCurrency { get; set; }
/// <summary>
/// Tipo de cambio aplicado al precio estimado
/// </summary>
public decimal? EstimatedExchangerate { get; set; }
//public virtual PhLsmExpeditionHeader Expedition { get; set; } = null!;
//public virtual PhLsmProduct Product { get; set; } = null!;
}
}

View File

@ -0,0 +1,82 @@
namespace Domain.Entities
{
public class ELSExpeditionHeader
{
/// <summary>
/// Identificador interno de la expedición
/// </summary>
public int Id { get; set; }
/// <summary>
/// Referencia al ticket quirúrgico (si aplica)
/// </summary>
public Guid? TicketId { get; set; }
/// <summary>
/// Número de expedición (formato EX-00000001)
/// </summary>
public string Expeditionnumber { get; set; } = null!;
/// <summary>
/// Ubicación (depósito) desde donde se despacha
/// </summary>
public int LocationId { get; set; }
/// <summary>
/// Fecha de emisión de la expedición
/// </summary>
public DateTime Issuedate { get; set; }
/// <summary>
/// Estado de la expedición (1=Borrador, 2=Confirmada, etc.)
/// </summary>
public int Status { get; set; }
/// <summary>
/// Nombre del destinatario visible en la impresión
/// </summary>
public string? RecipientName { get; set; }
/// <summary>
/// Número o referencia externa asociada
/// </summary>
public string? ReferenceNumber { get; set; }
/// <summary>
/// Tipo de origen externo (ej: surgery, demo, préstamo)
/// </summary>
public string? OriginType { get; set; }
/// <summary>
/// ID externo relacionado a otro módulo (ej: ticket, orden)
/// </summary>
public string? ExternalReference { get; set; }
/// <summary>
/// Observaciones generales de la expedición
/// </summary>
public string? Observations { get; set; }
/// <summary>
/// Información adicional en formato JSON (ej: paciente, médico, etc.)
/// </summary>
public string? ExtrainfoJson { get; set; }
/// <summary>
/// Cantidad de veces que se imprimió la nota de expedición
/// </summary>
public int Printcount { get; set; }
/// <summary>
/// Fecha de creación del registro
/// </summary>
public DateTime Createdat { get; set; }
/// <summary>
/// Fecha de última modificación del registro
/// </summary>
public DateTime? Modifiedat { get; set; }
public virtual ICollection<ELSExpeditionDetail> PhLsmExpeditionDetails { get; set; } = new List<ELSExpeditionDetail>();
}
}

View File

@ -42,6 +42,11 @@
/// </summary> /// </summary>
public string? ExternalCode { get; set; } = string.Empty; public string? ExternalCode { get; set; } = string.Empty;
/// <summary>
/// Código regulatorio (PM) o registro sanitario oficial del producto. Registro otorgado por ANMAT u ente regulador.
/// </summary>
public string? RegulatoryCode { get; set; } = string.Empty;
/// <summary> /// <summary>
/// División o familia técnica del producto (ej: columna, trauma, descartables, etc.) /// División o familia técnica del producto (ej: columna, trauma, descartables, etc.)
/// </summary> /// </summary>

View File

@ -0,0 +1,43 @@
namespace Domain.Entities
{
public class ELSProductSet
{
/// <summary>
/// Identificador único del set de productos
/// </summary>
public int Id { get; set; }
/// <summary>
/// Código del set (ej: CMF1.2, TRAUMA2.0)
/// </summary>
public string Code { get; set; } = null!;
/// <summary>
/// Nombre comercial o técnico del set
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// Descripción extendida del set de productos
/// </summary>
public string? Descripcion { get; set; }
/// <summary>
/// Indica si el set requiere un proceso adicional (ej: esterilización)
/// </summary>
public bool PlusProcess { get; set; }
/// <summary>
/// Fecha de creación del registro
/// </summary>
public DateTime Createdat { get; set; }
/// <summary>
/// Fecha de última modificación del registro
/// </summary>
public DateTime? Modifiedat { get; set; }
public virtual ICollection<ELSProductSetItem> PhLsmProductSetItems { get; set; } = new List<ELSProductSetItem>();
}
}

View File

@ -0,0 +1,34 @@
namespace Domain.Entities
{
public class ELSProductSetItem
{
/// <summary>
/// Identificador único del ítem dentro del set
/// </summary>
public int Id { get; set; }
/// <summary>
/// ID del set al que pertenece este ítem
/// </summary>
public int ProductsetId { get; set; }
/// <summary>
/// Producto incluido en el set
/// </summary>
public int ProductId { get; set; }
/// <summary>
/// Cantidad estándar del producto en el set
/// </summary>
public decimal DefaultQuantity { get; set; }
/// <summary>
/// Indica si este ítem es obligatorio en la composición del set
/// </summary>
public bool Mandatory { get; set; }
//public virtual PhLsmProduct Product { get; set; } = null!;
//public virtual PhLsmProductSet Productset { get; set; } = null!;
}
}

View File

@ -0,0 +1,77 @@
namespace Domain.Entities
{
public class ELSStockEntry
{
/// <summary>
/// Identificador único del ingreso de stock
/// </summary>
public int Id { get; set; }
/// <summary>
/// Producto ingresado al stock
/// </summary>
public int ProductId { get; set; }
/// <summary>
/// Cantidad ingresada del producto
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// Precio unitario usado para valorizar el ingreso
/// </summary>
public decimal Unitprice { get; set; }
/// <summary>
/// Moneda utilizada en la valorización (ARS, USD, EUR)
/// </summary>
public string Currency { get; set; } = null!;
/// <summary>
/// Tasa de conversión de la moneda a ARS
/// </summary>
public decimal Exchangerate { get; set; }
/// <summary>
/// Fecha del ingreso de stock
/// </summary>
public DateTime Entrydate { get; set; }
/// <summary>
/// Referencia visible del movimiento (ej: factura, orden de compra)
/// </summary>
public string? Reference { get; set; }
/// <summary>
/// Tipo de origen del ingreso (purchase, return, manual, etc.)
/// </summary>
public string? Sourcetype { get; set; }
/// <summary>
/// ID de la entidad que generó el ingreso (ej: orden de compra)
/// </summary>
public int? SourceId { get; set; }
/// <summary>
/// Ubicación física donde se depositó el producto
/// </summary>
public int LocationId { get; set; }
/// <summary>
/// Lote del producto ingresado (si aplica trazabilidad)
/// </summary>
public string? Batch { get; set; }
/// <summary>
/// Fecha de vencimiento del producto ingresado (si aplica)
/// </summary>
public DateOnly? Expiration { get; set; }
/// <summary>
/// Fecha de creación del registro
/// </summary>
public DateTime Createdat { get; set; }
//public virtual PhLsmProduct Product { get; set; } = null!;
}
}

View File

@ -0,0 +1,69 @@
namespace Domain.Entities
{
public class ELSStockItem
{
/// <summary>
/// Identificador único del ítem de stock físico
/// </summary>
public int Id { get; set; }
/// <summary>
/// Producto vinculado al ítem de stock
/// </summary>
public int ProductId { get; set; }
/// <summary>
/// Ubicación física del stock (depósito, valija, etc.)
/// </summary>
public int LocationId { get; set; }
/// <summary>
/// Cantidad actual disponible en esta unidad de stock
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// Cantidad comprometida o reservada para expediciones futuras
/// </summary>
public decimal ReservedQuantity { get; set; }
/// <summary>
/// Código de lote (si aplica)
/// </summary>
public string? Batch { get; set; }
/// <summary>
/// Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante.
/// </summary>
public string? Serial { get; set; }
/// <summary>
/// Fecha de vencimiento (si aplica)
/// </summary>
public DateOnly? Expiration { get; set; }
/// <summary>
/// Estado del ítem (1=Disponible, 2=Reservado, 3=Vencido, etc.)
/// </summary>
public int Status { get; set; }
/// <summary>
/// Comentario libre u observación sobre este ítem de stock
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Fecha de creación del registro
/// </summary>
public DateTime Createdat { get; set; }
/// <summary>
/// Fecha de última modificación del registro
/// </summary>
public DateTime? Modifiedat { get; set; }
//public virtual PhLsmStockLocation Location { get; set; } = null!;
//public virtual PhLsmProduct Product { get; set; } = null!;
}
}

View File

@ -0,0 +1,22 @@
namespace Domain.Entities
{
public class ELSStockLocation
{
/// <summary>
/// Identificador único de la ubicación de stock
/// </summary>
public int Id { get; set; }
/// <summary>
/// Nombre visible de la ubicación (ej: Depósito Central, Cuarentena, Caja A1)
/// </summary>
public string Nombre { get; set; } = null!;
/// <summary>
/// Descripción o comentario adicional sobre la ubicación
/// </summary>
public string? Descripcion { get; set; }
//public virtual ICollection<PhLsmStockItem> PhLsmStockItems { get; set; } = new List<PhLsmStockItem>();
}
}

View File

@ -0,0 +1,82 @@
namespace Domain.Entities
{
public class ELSStockOut
{
/// <summary>
/// Identificador único del egreso de stock
/// </summary>
public int Id { get; set; }
/// <summary>
/// Producto retirado del stock
/// </summary>
public int ProductId { get; set; }
/// <summary>
/// Cantidad retirada del producto
/// </summary>
public decimal Quantity { get; set; }
/// <summary>
/// Precio unitario usado para valorizar el egreso
/// </summary>
public decimal Unitprice { get; set; }
/// <summary>
/// Moneda utilizada en la valorización (ARS, USD, EUR)
/// </summary>
public string Currency { get; set; } = null!;
/// <summary>
/// Tasa de conversión de la moneda a ARS
/// </summary>
public decimal Exchangerate { get; set; }
/// <summary>
/// Fecha del egreso de stock
/// </summary>
public DateTime Outdate { get; set; }
/// <summary>
/// Referencia visible del movimiento (NE, devolución, cirugía)
/// </summary>
public string? Reference { get; set; }
/// <summary>
/// Tipo de origen del egreso (surgery, expiration, manual, etc.)
/// </summary>
public string? Sourcetype { get; set; }
/// <summary>
/// ID de la entidad que generó el egreso (ej: nota de expedición)
/// </summary>
public int? SourceId { get; set; }
/// <summary>
/// Identificador del caso quirúrgico asociado al egreso
/// </summary>
public Guid? TicketId { get; set; }
/// <summary>
/// Ubicación física desde donde se retiró el producto
/// </summary>
public int? LocationId { get; set; }
/// <summary>
/// Lote del producto egresado (si aplica trazabilidad)
/// </summary>
public string? Batch { get; set; }
/// <summary>
/// Fecha de vencimiento del producto egresado (si aplica)
/// </summary>
public DateOnly? Expiration { get; set; }
/// <summary>
/// Fecha de creación del registro
/// </summary>
public DateTime Createdat { get; set; }
//public virtual PhLsmProduct Product { get; set; } = null!;
}
}

View File

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

View File

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

View File

@ -66,7 +66,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
} }

View File

@ -7,7 +7,7 @@
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot> <NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders> <NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle> <NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.0</NuGetToolVersion> <NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.1</NuGetToolVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' "> <ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\maski\.nuget\packages\" /> <SourceRoot Include="C:\Users\maski\.nuget\packages\" />

View File

@ -73,7 +73,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
} }

View File

@ -0,0 +1,28 @@
using Domain.Dtos.Stock;
using Domain.Generics;
namespace Models.Interfaces
{
public interface IPhLSMStockItemRepository
{
Task<PagedResult<StockItemScanResultDto>> 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<PagedResult<StockItemScanResultDto>> SearchStockItemsParsedAsync(
string? gtin,
string? batch,
DateOnly? expiration,
string? serial, // reservado (si agregás columna Serial en StockItem)
int? locationId,
int page,
int take);
}
}

View File

@ -12,5 +12,6 @@ namespace Models.Interfaces
Task<IEnumerable<ELookUpItem>> BussinessUnitsListAsync(string filter, int limit = 10); Task<IEnumerable<ELookUpItem>> BussinessUnitsListAsync(string filter, int limit = 10);
Task<IEnumerable<EProductLookupItem>> ProductsListAsync(string filter, int limit = 10); Task<IEnumerable<EProductLookupItem>> ProductsListAsync(string filter, int limit = 10);
Task<IEnumerable<ELookUpItem>> PaymentTermsListAsync(string filter, int limit = 10); Task<IEnumerable<ELookUpItem>> PaymentTermsListAsync(string filter, int limit = 10);
Task<IEnumerable<ELookUpItem>> ApprovedQuotesListAsync(string filter, int limit = 10);
} }
} }

View File

@ -1,4 +1,5 @@
using Domain.Entities; using Domain.Dtos;
using Domain.Entities;
using Domain.Generics; using Domain.Generics;
namespace Models.Interfaces namespace Models.Interfaces
@ -7,6 +8,7 @@ namespace Models.Interfaces
{ {
Task<PagedResult<EQuoteHeader>> GetAllAsync(int page = 1, int pageSize = 50); Task<PagedResult<EQuoteHeader>> GetAllAsync(int page = 1, int pageSize = 50);
Task<EQuoteHeader?> GetByIdAsync(int id); Task<EQuoteHeader?> GetByIdAsync(int id);
Task<EQuoteHeader?> GetByQuoteNumberAsync(string quoteNumber);
Task<IEnumerable<EQuoteHeader>> GetByCustomerIdAsync(int customerId); Task<IEnumerable<EQuoteHeader>> GetByCustomerIdAsync(int customerId);
Task UpdateAsync(EQuoteHeader quoteHeader); Task UpdateAsync(EQuoteHeader quoteHeader);
Task DeleteAsync(int id); Task DeleteAsync(int id);

View File

@ -12,6 +12,7 @@ namespace Models.Interfaces
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos) #region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
Task<(int Id, string Quotenumber)> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId); Task<(int Id, string Quotenumber)> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
Task<QuoteDto?> GetDtoByIdAsync(int id); Task<QuoteDto?> GetDtoByIdAsync(int id);
Task<QuoteDto?> GetDtoByQuoteNumberAsync(string quoteNumber);
Task<bool> AuthorizeQuoteAsync(int quoteId, List<EQuoteDetail> approvedItems); Task<bool> AuthorizeQuoteAsync(int quoteId, List<EQuoteDetail> approvedItems);
#endregion #endregion
} }

View File

@ -35,6 +35,11 @@ public partial class PhLsmExpeditionDetail
/// </summary> /// </summary>
public string? Batch { get; set; } public string? Batch { get; set; }
/// <summary>
/// Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante.
/// </summary>
public string? Serial { get; set; }
/// <summary> /// <summary>
/// Fecha de vencimiento del producto (si aplica trazabilidad) /// Fecha de vencimiento del producto (si aplica trazabilidad)
/// </summary> /// </summary>

View File

@ -45,6 +45,11 @@ public partial class PhLsmProduct
/// </summary> /// </summary>
public string? ExternalCode { get; set; } public string? ExternalCode { get; set; }
/// <summary>
/// Código regulatorio (PM) o registro sanitario oficial del producto. Registro otorgado por ANMAT u ente regulador.
/// </summary>
public string? RegulatoryCode { get; set; }
/// <summary> /// <summary>
/// División o familia técnica del producto (ej: columna, trauma, descartables, etc.) /// División o familia técnica del producto (ej: columna, trauma, descartables, etc.)
/// </summary> /// </summary>

View File

@ -35,6 +35,11 @@ public partial class PhLsmStockItem
/// </summary> /// </summary>
public string? Batch { get; set; } public string? Batch { get; set; }
/// <summary>
/// Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante.
/// </summary>
public string? Serial { get; set; }
/// <summary> /// <summary>
/// Fecha de vencimiento (si aplica) /// Fecha de vencimiento (si aplica)
/// </summary> /// </summary>

View File

@ -96,8 +96,15 @@ public partial class PhronCareOperationsHubContext : DbContext
public virtual DbSet<PhSQuoteTaxis> PhSQuoteTaxes { get; set; } public virtual DbSet<PhSQuoteTaxis> PhSQuoteTaxes { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 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. #region VERSION DOCKER
=> 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"); {
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) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@ -151,6 +158,10 @@ public partial class PhronCareOperationsHubContext : DbContext
.HasComment("Cantidad solicitada del producto") .HasComment("Cantidad solicitada del producto")
.HasColumnType("decimal(18, 4)") .HasColumnType("decimal(18, 4)")
.HasColumnName("quantity"); .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) entity.HasOne(d => d.Expedition).WithMany(p => p.PhLsmExpeditionDetails)
.HasForeignKey(d => d.ExpeditionId) .HasForeignKey(d => d.ExpeditionId)
@ -267,6 +278,10 @@ public partial class PhronCareOperationsHubContext : DbContext
entity.Property(e => e.ProductType) entity.Property(e => e.ProductType)
.HasComment("Tipo de producto: 1=Implantable, 2=Instrumental, 3=Inyectable, etc.") .HasComment("Tipo de producto: 1=Implantable, 2=Instrumental, 3=Inyectable, etc.")
.HasColumnName("product_type"); .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) entity.Property(e => e.TraceabilityType)
.HasComment("Tipo de trazabilidad: 1=No aplica, 2=Por cantidad, 3=Por lote y vencimiento") .HasComment("Tipo de trazabilidad: 1=No aplica, 2=Por cantidad, 3=Por lote y vencimiento")
.HasColumnName("traceability_type"); .HasColumnName("traceability_type");
@ -459,7 +474,21 @@ public partial class PhronCareOperationsHubContext : DbContext
entity.ToTable("PhLSM_StockItem"); 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) entity.Property(e => e.Id)
.HasComment("Identificador único del ítem de stock físico") .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") .HasComment("Cantidad comprometida o reservada para expediciones futuras")
.HasColumnType("decimal(18, 4)") .HasColumnType("decimal(18, 4)")
.HasColumnName("reserved_quantity"); .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) entity.Property(e => e.Status)
.HasComment("Estado del ítem (1=Disponible, 2=Reservado, 3=Vencido, etc.)") .HasComment("Estado del ítem (1=Disponible, 2=Reservado, 3=Vencido, etc.)")
.HasColumnName("status"); .HasColumnName("status");

View File

@ -92,6 +92,23 @@ namespace Models.Repositories
.Take(limit) .Take(limit)
.ToListAsync(); .ToListAsync();
} }
public async Task<IEnumerable<ELookUpItem>> 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();
}
} }
} }

View File

@ -1,16 +1,16 @@
using Microsoft.EntityFrameworkCore; using Domain.Dtos;
using Models.Interfaces;
using Models.Helpers;
using Models.Models;
using Domain.Entities; using Domain.Entities;
using Domain.Generics; using Domain.Generics;
using Microsoft.EntityFrameworkCore;
using Models.Helpers;
using Models.Interfaces;
using Models.Models;
namespace Models.Repositories namespace Models.Repositories
{ {
public class PhSQuoteHeaderRepository(PhronCareOperationsHubContext context) : IPhSQuoteHeaderRepository public class PhSQuoteHeaderRepository(PhronCareOperationsHubContext context) : IPhSQuoteHeaderRepository
{ {
private readonly PhronCareOperationsHubContext _context = context; private readonly PhronCareOperationsHubContext _context = context;
//private readonly IPhSFormSeriesRepository _formSeriesRepository = formSeriesRepository;
#region Metodos #region Metodos
public async Task<PagedResult<EQuoteHeader>> GetAllAsync(int page = 1, int pageSize = 50) public async Task<PagedResult<EQuoteHeader>> GetAllAsync(int page = 1, int pageSize = 50)
{ {
@ -42,6 +42,12 @@ namespace Models.Repositories
return entity != null ? EntityMapper.MapEntity<PhSQuoteHeader, EQuoteHeader>(entity) : null; return entity != null ? EntityMapper.MapEntity<PhSQuoteHeader, EQuoteHeader>(entity) : null;
} }
public async Task<EQuoteHeader?> GetByQuoteNumberAsync(string quoteNumber)
{
var entity= await _context.PhSQuoteHeaders
.FirstOrDefaultAsync(q => q.Quotenumber == quoteNumber);
return entity != null ? EntityMapper.MapEntity<PhSQuoteHeader, EQuoteHeader>(entity) : null;
}
public async Task<IEnumerable<EQuoteHeader>> GetByCustomerIdAsync(int customerId) public async Task<IEnumerable<EQuoteHeader>> GetByCustomerIdAsync(int customerId)
{ {
var entities = await _context.PhSQuoteHeaders var entities = await _context.PhSQuoteHeaders

View File

@ -344,7 +344,51 @@ namespace Models.Repositories
return dto; return dto;
} }
public async Task<QuoteDto?> 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 #endregion
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos) #region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
/// <summary> /// <summary>

View File

@ -144,6 +144,7 @@ namespace Models.Repositories
UnitId = unitId, UnitId = unitId,
PlusProcess = item.PlusProcess, PlusProcess = item.PlusProcess,
ExternalCode = item.ExternalCode, ExternalCode = item.ExternalCode,
RegulatoryCode = item.RegulatoryCode
// otros campos... // otros campos...
}; };

View File

@ -0,0 +1,213 @@
using Domain.Dtos.Stock; // StockItemScanResultDto
using Domain.Generics; // PagedResult<T>
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<PagedResult<StockItemScanResultDto>> 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<StockItemScanResultDto>)
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<StockItemScanResultDto>
{
Items = paged.Items,
TotalItems = paged.TotalItems,
Page = paged.Page,
PageSize = paged.PageSize
};
}
// -------- Búsqueda PARSEADA (GS1/DataMatrix) ----------
public async Task<PagedResult<StockItemScanResultDto>> 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<StockItemScanResultDto>
{ 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<int>();
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<StockItemScanResultDto>
{ 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<StockItemScanResultDto>
{
Items = paged.Items,
TotalItems = paged.TotalItems,
Page = paged.Page,
PageSize = paged.PageSize
};
}
}
}

View File

@ -66,7 +66,7 @@
"privateAssets": "all" "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" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
} }

View File

@ -7,7 +7,7 @@
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot> <NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders> <NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle> <NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.0</NuGetToolVersion> <NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.1</NuGetToolVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' "> <ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\maski\.nuget\packages\" /> <SourceRoot Include="C:\Users\maski\.nuget\packages\" />

View File

@ -4098,7 +4098,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
} }

View File

@ -80,7 +80,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
} }

View File

@ -7,7 +7,7 @@
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot> <NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders> <NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle> <NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.0</NuGetToolVersion> <NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.1</NuGetToolVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' "> <ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\maski\.nuget\packages\" /> <SourceRoot Include="C:\Users\maski\.nuget\packages\" />

View File

@ -1318,7 +1318,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
} }

View File

@ -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;
/// <summary>
/// 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).
/// </summary>
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;
}
/// <summary>
/// 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".
/// </summary>
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 02 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 0099; se suele mapear a 20002099. Ajustá si necesitás 19901999 para 9099.
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);
}
}
}

View File

@ -7,7 +7,7 @@
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot> <NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders> <NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle> <NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.0</NuGetToolVersion> <NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.1</NuGetToolVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' "> <ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\maski\.nuget\packages\" /> <SourceRoot Include="C:\Users\maski\.nuget\packages\" />

View File

@ -37,5 +37,9 @@ namespace phronCare.API.Controllers.Sales
[HttpGet("paymentterms")] [HttpGet("paymentterms")]
public Task<IEnumerable<ELookUpItem>> PaymentTerms() public Task<IEnumerable<ELookUpItem>> PaymentTerms()
=> _lookup.PaymentTermsListAsync(""); // o sin parámetro si lo hacés opcional => _lookup.PaymentTermsListAsync(""); // o sin parámetro si lo hacés opcional
[HttpGet("approvedquotes")]
public Task<IEnumerable<ELookUpItem>> ApprovedQuotes([FromQuery] string q)
=> _lookup.ApprovedQuotesListAsync(q);
} }
} }

View File

@ -94,7 +94,30 @@ namespace phronCare.API.Controllers.Sales
} }
} }
/// <summary>
/// Devuelve un presupuesto DTO con información básica a partir del número de presupuesto.
/// </summary>
[HttpGet("summary/{quoteNumber}")]
public async Task<ActionResult<QuoteDto>> 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}");
}
}
/// <summary>
/// Genera y devuelve un archivo PDF correspondiente al presupuesto especificado por su ID.
/// </summary>
[HttpGet("{id}/pdf")] [HttpGet("{id}/pdf")]
public async Task<IActionResult> GetQuotePdf(int id) public async Task<IActionResult> GetQuotePdf(int id)
{ {

View File

@ -0,0 +1,47 @@
using Core.Interfaces.Stock; // ILSStockScanDom
using Domain.Dtos.Stock; // StockItemSearchParams, StockItemScanResultDto
using Domain.Generics; // PagedResult<T>
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;
}
/// <summary>
/// Búsqueda paginada de ítems de stock por código/texto, lote y filtros opcionales.
/// </summary>
[HttpPost("search")]
public async Task<ActionResult<PagedResult<StockItemScanResultDto>>> 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).
/// </summary>
/// <param name="searchParams">
/// 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.
/// </param>
/// <returns>
/// Lista paginada de ítems de stock que cumplen con los filtros especificados.
/// </returns>
[HttpPost("search-parsed")]
public async Task<ActionResult<PagedResult<StockItemScanResultDto>>> SearchParsed([FromBody] StockItemParsedSearchParams searchParams)
{
var result = await _service.SearchParsedAsync(searchParams);
return Ok(result);
}
}
}

View File

@ -271,5 +271,7 @@ static void RepositorysAndServices(WebApplicationBuilder builder)
builder.Services.AddScoped<IPhLSMLookUpRepository, PhLSMLookUpRepository>(); builder.Services.AddScoped<IPhLSMLookUpRepository, PhLSMLookUpRepository>();
builder.Services.AddScoped<ILSMLookUpDom, LSMLookUpService>(); builder.Services.AddScoped<ILSMLookUpDom, LSMLookUpService>();
builder.Services.AddScoped<IPhLSMUnitOfMeasureRepository, PhLSMUnitOfMeasureRepository>(); builder.Services.AddScoped<IPhLSMUnitOfMeasureRepository, PhLSMUnitOfMeasureRepository>();
builder.Services.AddScoped<ILSStockScanDom, LSStockScanService>();
builder.Services.AddScoped<IPhLSMStockItemRepository, PhLSMStockItemRepository>();
} }

View File

@ -758,6 +758,32 @@
], ],
"ReturnTypes": [] "ReturnTypes": []
}, },
{
"ContainingType": "phronCare.API.Controllers.Sales.LookUpController",
"Method": "ApprovedQuotes",
"RelativePath": "api/LookUp/approvedquotes",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "q",
"Type": "System.String",
"IsRequired": false
}
],
"ReturnTypes": [
{
"Type": "System.Collections.Generic.IEnumerable\u00601[[Domain.Entities.ELookUpItem, Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]",
"MediaTypes": [
"text/plain",
"application/json",
"text/json"
],
"StatusCode": 200
}
]
},
{ {
"ContainingType": "phronCare.API.Controllers.Sales.LookUpController", "ContainingType": "phronCare.API.Controllers.Sales.LookUpController",
"Method": "BussinessUnits", "Method": "BussinessUnits",
@ -1200,6 +1226,58 @@
], ],
"ReturnTypes": [] "ReturnTypes": []
}, },
{
"ContainingType": "API.Controllers.Stock.LSStockScanController",
"Method": "Search",
"RelativePath": "api/LSStockScan/search",
"HttpMethod": "POST",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "searchParams",
"Type": "Domain.Generics.StockItemSearchParams",
"IsRequired": true
}
],
"ReturnTypes": [
{
"Type": "Domain.Generics.PagedResult\u00601[[Domain.Dtos.Stock.StockItemScanResultDto, Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]",
"MediaTypes": [
"text/plain",
"application/json",
"text/json"
],
"StatusCode": 200
}
]
},
{
"ContainingType": "API.Controllers.Stock.LSStockScanController",
"Method": "SearchParsed",
"RelativePath": "api/LSStockScan/search-parsed",
"HttpMethod": "POST",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "searchParams",
"Type": "Domain.Generics.StockItemParsedSearchParams",
"IsRequired": true
}
],
"ReturnTypes": [
{
"Type": "Domain.Generics.PagedResult\u00601[[Domain.Dtos.Stock.StockItemScanResultDto, Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]",
"MediaTypes": [
"text/plain",
"application/json",
"text/json"
],
"StatusCode": 200
}
]
},
{ {
"ContainingType": "phronCare.API.Controllers.Stock.LSUnitOfMeasureController", "ContainingType": "phronCare.API.Controllers.Stock.LSUnitOfMeasureController",
"Method": "Create", "Method": "Create",
@ -2196,6 +2274,32 @@
} }
] ]
}, },
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "GetHeaderDtoByQuoteNumber",
"RelativePath": "api/Quote/summary/{quoteNumber}",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "quoteNumber",
"Type": "System.String",
"IsRequired": true
}
],
"ReturnTypes": [
{
"Type": "Domain.Dtos.QuoteDto",
"MediaTypes": [
"text/plain",
"application/json",
"text/json"
],
"StatusCode": 200
}
]
},
{ {
"ContainingType": "phronCare.API.Controllers.Sales.TaxConditionController", "ContainingType": "phronCare.API.Controllers.Sales.TaxConditionController",
"Method": "GetAll", "Method": "GetAll",

View File

@ -76,7 +76,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
}, },
@ -163,7 +163,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
}, },
@ -229,7 +229,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
}, },
@ -311,7 +311,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
}, },
@ -479,7 +479,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
}, },
@ -559,7 +559,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
}, },
@ -639,7 +639,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
} }

View File

@ -7,7 +7,7 @@
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot> <NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders> <NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle> <NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.0</NuGetToolVersion> <NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.1</NuGetToolVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' "> <ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\maski\.nuget\packages\" /> <SourceRoot Include="C:\Users\maski\.nuget\packages\" />

View File

@ -9811,7 +9811,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
} }

View File

@ -66,7 +66,7 @@
"privateAssets": "all" "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" "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" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
} }

View File

@ -7,7 +7,7 @@
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot> <NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders> <NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle> <NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.0</NuGetToolVersion> <NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.1</NuGetToolVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' "> <ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\maski\.nuget\packages\" /> <SourceRoot Include="C:\Users\maski\.nuget\packages\" />

View File

@ -2503,7 +2503,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
} }

View File

@ -92,7 +92,7 @@
</NavLink> </NavLink>
</div> </div>
<div class="nav-item ps-4 py-0 border-start border-2 border-white"> <div class="nav-item ps-4 py-0 border-start border-2 border-white">
<NavLink class="nav-link small py-0 px-2 text-start d-flex align-items-center" href="stock/expeditions" activeClass="bg-secondary text-white fw-semibold"> <NavLink class="nav-link small py-0 px-2 text-start d-flex align-items-center" href="stock/expeditions/create" activeClass="bg-secondary text-white fw-semibold">
<span class="oi oi-data-transfer-download me-2 text-danger"></span> Expediciones <span class="oi oi-data-transfer-download me-2 text-danger"></span> Expediciones
</NavLink> </NavLink>
</div> </div>

View File

@ -73,7 +73,7 @@
<br/> <br/>
<div class="col-12" style="text-align:center;"> <div class="col-12" style="text-align:center;">
<button type="submit" class="btn btn-primary"><span class="fa fa-vault"></span> Restablecer</button> <button type="submit" class="btn btn-primary"><span class="fa fa-vault"></span> Restablecer</button>
<button class="btn btn-warning btn-circle" style="fData.Login.Loginght;" @olick="ToggleIsSecret"> <button class="btn btn-warning btn-circle" style="fData.Login.Loginght;" @onclick="ToggleIsSecret">
<span class="fa fa-circle-left"></span> <span class="fa fa-circle-left"></span>
</button> </button>
</div> </div>

View File

@ -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
<EditForm Model="Model" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="card mt-4" style="zoom:80%">
<div class="card-header d-flex justify-content-center align-items-center">
<h3 class="card-title m-0">Nueva Expedición</h3>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Presupuesto aprobado</label>
<BlazoredTypeahead id="quotes" TItem="ELookUpItem" TValue="ELookUpItem"
SearchMethod="SearchQuotes"
Value="SelectedQuote"
ValueChanged="OnQuoteSelected"
ValueExpression="@(() => SelectedQuote)"
MaximumSuggestions="10"
Placeholder="Buscar presupuesto aprobado..."
TextProperty="Nombre">
<ResultTemplate Context="item">
@item.Nombre
</ResultTemplate>
<SelectedTemplate Context="item">
@item.Nombre
</SelectedTemplate>
</BlazoredTypeahead>
</div>
<div class="col-md-6">
<label class="form-label">Ticket ID</label>
<InputText class="form-control"
@bind-Value="ticketIdString"
@bind-Value:event="oninput" />
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<label class="form-label">Profesional</label>
<InputText class="form-control" @bind-Value="ExtraInfo.Professional" />
</div>
<div class="col-md-4">
<label class="form-label">Institución</label>
<InputText class="form-control" @bind-Value="ExtraInfo.Institution" />
</div>
<div class="col-md-4">
<label class="form-label">Paciente</label>
<InputText class="form-control" @bind-Value="ExtraInfo.Patient" />
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<label class="form-label">Fecha de Cirugía</label>
<InputDate class="form-control" @bind-Value="ExtraInfo.SurgeryDate" />
</div>
<div class="col-md-8">
<label class="form-label">Observaciones</label>
<InputTextArea class="form-control" @bind-Value="Model.Observations" />
</div>
</div>
@if (!string.IsNullOrWhiteSpace(DispatchInstruction))
{
<div class="alert alert-info">
<strong>Instrucciones desde presupuesto:</strong><br />
@DispatchInstruction
</div>
}
</div>
</div>
<div class="card mt-3" style="zoom:90%">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title m-0">Productos a Expedir</h5>
<div class="btn-group">
<button class="btn btn-dark btn-sm" title="Agregar producto" @onclick="OpenStockItemSelectorModal">
<i class="fas fa-box"></i> Producto
</button>
<button class="btn btn-dark btn-sm" title="Agregar set">
<i class="fas fa-th-large"></i> Set-Box
</button>
<button class="btn btn-dark btn-sm" title="Escanear producto">
<i class="fas fa-barcode"></i> Scanner
</button>
</div>
</div>
<div class="card-body p-2">
@if (Details.Any())
{
<table class="table table-sm table-bordered">
<thead class="table-light">
<tr>
<th>Producto</th>
<th>Cant.</th>
<th>Lote</th>
<th>Serial</th>
<th>Vencimiento</th>
<th>Ubicación</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Details)
{
<tr>
<td>@item.ProductId</td>
<td>@item.Quantity</td>
<td>@item.Batch</td>
<td>@item.Serial</td>
<td>@item.Expiration?.ToString("yyyy-MM-dd")</td>
<td>@item.LocationId</td>
<td>
<button class="btn btn-sm btn-danger" @onclick="() => RemoveItem(item)">✕</button>
</td>
</tr>
}
</tbody>
</table>
}
else
{
<p class="text-muted">No hay productos agregados.</p>
}
</div>
</div>
<div class="mt-3 d-flex justify-content-end">
<button type="submit" class="btn btn-primary">Guardar Expedición</button>
</div>
</EditForm>
@code {
private ELSExpeditionHeader Model = new();
private ExtraInfoModel ExtraInfo = new();
private string DispatchInstruction = string.Empty;
private ELookUpItem? SelectedQuote;
private List<ELSExpeditionDetail> Details = new();
private List<ProductSetItemDto> 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<IEnumerable<ELookUpItem>> 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<StockItemSelectorModal>("", parameters, options);
var result = await modal.Result;
if (!result.Cancelled && result.Data is List<StockItemSelectionDto> 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; }
}
}

View File

@ -138,7 +138,7 @@
private List<string> TableColumns = new() private List<string> 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() protected override async Task OnInitializedAsync()
@ -176,6 +176,7 @@
{ "Id", p.Id }, { "Id", p.Id },
{ "Código Fábrica", p.FactoryCode }, { "Código Fábrica", p.FactoryCode },
{ "Código Externo", p.ExternalCode?? string.Empty }, { "Código Externo", p.ExternalCode?? string.Empty },
{ "Código Regulatorio", p.RegulatoryCode?? string.Empty },
{ "Nombre", p.Name?? string.Empty }, { "Nombre", p.Name?? string.Empty },
{ "Descripción", p.Descripcion }, { "Descripción", p.Descripcion },
{ "División", p.Division?.Name ?? "" }, { "División", p.Division?.Name ?? "" },

View File

@ -24,14 +24,18 @@
<ValidationSummary /> <ValidationSummary />
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-4">
<label>Código Fábrica *</label> <label>Código Fábrica *</label>
<InputText class="form-control" @bind-Value="product.FactoryCode" /> <InputText class="form-control" @bind-Value="product.FactoryCode" />
</div> </div>
<div class="col-md-6"> <div class="col-md-4">
<label>Código Externo</label> <label>Código Externo</label>
<InputText class="form-control" @bind-Value="product.ExternalCode" /> <InputText class="form-control" @bind-Value="product.ExternalCode" />
</div> </div>
<div class="col-md-4">
<label>Código Regulatorio</label>
<InputText class="form-control" @bind-Value="product.RegulatoryCode" />
</div>
</div> </div>
<div class="row mt-2"> <div class="row mt-2">

View File

@ -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
<div class="modal-header bg-dark text-white">
<h5 class="modal-title">Seleccionar artículos de stock</h5>
<button type="button" class="btn-close" aria-label="Close" @onclick="Cancel"></button>
</div>
<div class="modal-body" style="zoom:0.8;">
<div class="mb-3">
<label for="scan" class="form-label">Escanear o ingresar código</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-qrcode"></i>
</span>
<!-- Input nativo: @ref es ElementReference; capturamos Enter -->
<input class="form-control"
id="scan"
@bind="InputCode"
@bind:event="oninput"
@onkeydown="HandleKeyDown"
autocomplete="off"
spellcheck="false"
@ref="scanInput" />
</div>
<button type="button" class="btn btn-secondary mt-2" @onclick="HandleScan">Buscar</button>
</div>
@if (StockList.Any())
{
var last = StockList.First();
<div class="alert alert-info small">
Último escaneado: <strong>@last.ProductName</strong> | Lote <strong>@last.Batch</strong> | Venc: <strong>@last.Expiration?.ToShortDateString()</strong>
</div>
}
<table class="table table-sm table-bordered">
<thead class="table-light">
<tr>
<th>Producto</th>
<th>Lote</th>
<th>Vencimiento</th>
<th>Disponible</th>
<th>Cantidad a usar</th>
</tr>
</thead>
<tbody>
@foreach (var item in StockList)
{
<tr>
<td>@item.ProductName</td>
<td>@item.Batch</td>
<td>@item.Expiration?.ToShortDateString()</td>
<td>@item.Available</td>
<td>
<InputNumber @bind-Value="item.Selected" class="form-control form-control-sm" min="0" max="@item.Available" />
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @onclick="Cancel">Cancelar</button>
<button class="btn btn-primary" @onclick="Confirm">Agregar a lista</button>
</div>
@code {
[CascadingParameter] BlazoredModalInstance ModalInstance { get; set; } = default!;
[Parameter] public int? ProductId { get; set; }
[Parameter] public int? LocationId { get; set; }
[Parameter] public List<ProductSetItemDto>? SetItems { get; set; }
private string InputCode { get; set; } = string.Empty;
private ElementReference scanInput;
private readonly List<StockItemSelectionDto> SelectedItems = new();
private List<StockDisplayRow> 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<StockDisplayRow>
{
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; }
}
}

View File

@ -0,0 +1,123 @@
@using Blazored.Modal
@using Blazored.Modal.Services
@using Domain.Dtos.Stock
@inject IStockScanService StockScanService
@inherits LayoutComponentBase
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">Escaneo de producto</h5>
<button type="button" class="btn-close" @onclick="Cancel"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="scan" class="form-label">Escanear o ingresar código</label>
@* <InputText id="scan"
class="form-control"
@bind-Value="ScanInput" /> *@
<input @bind="ScanInput"
@bind:event="oninput"
class="form-control form-control-sm"
placeholder="Scannear..."
style="width: 250px;" />
@* <button class="btn btn-secondary mt-2" @onclick="HandleScan">Buscar</button> *@
</div>
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
{
<div class="alert alert-danger">@ErrorMessage</div>
}
@if (ScanResults.Any())
{
<table class="table table-sm table-bordered">
<thead class="table-light">
<tr>
<th>Producto</th>
<th>Lote</th>
<th>Vencimiento</th>
<th>Disponible</th>
<th>Cantidad a usar</th>
</tr>
</thead>
<tbody>
@foreach (var item in ScanResults)
{
<tr>
<td>@item.ProductName</td>
<td>@item.Batch</td>
<td>@item.Expiration?.ToShortDateString()</td>
<td>@item.Quantity</td>
<td>
<InputNumber @bind-Value="item.Quantity"
class="form-control form-control-sm"
min="0" />
</td>
</tr>
}
</tbody>
</table>
}
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @onclick="Cancel">Cancelar</button>
<button class="btn btn-primary" @onclick="ConfirmSelection" disabled="@(!ScanResults.Any(r => r.Quantity > 0))">Agregar</button>
</div>
@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<StockItemSelectionDto> 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));
}
}

View File

@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Blazored.Modal; using Blazored.Modal;
using Blazored.Toast; using Blazored.Toast;
using phronCare.UIBlazor.Services.Stock; using phronCare.UIBlazor.Services.Stock;
using phronCare.UIBlazor.Services.Stock.Expeditions;
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app"); builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after"); builder.RootComponents.Add<HeadOutlet>("head::after");
@ -48,6 +49,7 @@ static void InjectDependencies(WebAssemblyHostBuilder builder)
builder.Services.AddScoped<ISalesLookupService, SalesLookupService>(); builder.Services.AddScoped<ISalesLookupService, SalesLookupService>();
builder.Services.AddScoped<IStockLookUpService, StockLookUpService>(); builder.Services.AddScoped<IStockLookUpService, StockLookUpService>();
builder.Services.AddScoped<IExchangeRateService, ExchangeRateService>(); builder.Services.AddScoped<IExchangeRateService, ExchangeRateService>();
builder.Services.AddScoped<IStockScanService, StockScanService>();
builder.Services.AddScoped<ExchangeRateService>(); builder.Services.AddScoped<ExchangeRateService>();
builder.Services.AddScoped<QuoteService>(); builder.Services.AddScoped<QuoteService>();
@ -67,6 +69,7 @@ static void InjectDependencies(WebAssemblyHostBuilder builder)
builder.Services.AddScoped<ProductDivisionService>(); builder.Services.AddScoped<ProductDivisionService>();
builder.Services.AddScoped<LSProductService>(); builder.Services.AddScoped<LSProductService>();
builder.Services.AddScoped<LSUnitOfMeasureService>(); builder.Services.AddScoped<LSUnitOfMeasureService>();
builder.Services.AddScoped<ExpeditionService>();
} }

View File

@ -14,5 +14,6 @@ namespace phronCare.UIBlazor.Services.Lookups
Task<IEnumerable<EAdjustmentReason>> GetAdjustmentReasonsAsync(); Task<IEnumerable<EAdjustmentReason>> GetAdjustmentReasonsAsync();
Task<IEnumerable<ETaxType>> GetTaxTypesAsync(); Task<IEnumerable<ETaxType>> GetTaxTypesAsync();
Task<IEnumerable<ELookUpItem>> GetPaymentTermsAsync(); Task<IEnumerable<ELookUpItem>> GetPaymentTermsAsync();
Task<IEnumerable<ELookUpItem>> SearchApprovedQuotesAsync(string filtro);
} }
} }

View File

@ -35,7 +35,6 @@ namespace phronCare.UIBlazor.Services.Lookups
var items = await _http.GetFromJsonAsync<ELookUpItem[]>(url); var items = await _http.GetFromJsonAsync<ELookUpItem[]>(url);
return items ?? Array.Empty<ELookUpItem>(); return items ?? Array.Empty<ELookUpItem>();
} }
public Task<IEnumerable<EProductLookupItem>> SearchProductsAsync(string filtro) public Task<IEnumerable<EProductLookupItem>> SearchProductsAsync(string filtro)
=> FetchProductsAsync($"api/lookup/products?q={Uri.EscapeDataString(filtro)}"); => FetchProductsAsync($"api/lookup/products?q={Uri.EscapeDataString(filtro)}");
public async Task<IEnumerable<EAdjustmentReason>> GetAdjustmentReasonsAsync() public async Task<IEnumerable<EAdjustmentReason>> GetAdjustmentReasonsAsync()
@ -43,7 +42,6 @@ namespace phronCare.UIBlazor.Services.Lookups
var items = await _http.GetFromJsonAsync<EAdjustmentReason[]>("api/adjustmentreason/getall"); var items = await _http.GetFromJsonAsync<EAdjustmentReason[]>("api/adjustmentreason/getall");
return items ?? Array.Empty<EAdjustmentReason>(); return items ?? Array.Empty<EAdjustmentReason>();
} }
private async Task<IEnumerable<EProductLookupItem>> FetchProductsAsync(string url) private async Task<IEnumerable<EProductLookupItem>> FetchProductsAsync(string url)
{ {
var items = await _http.GetFromJsonAsync<EProductLookupItem[]>(url); var items = await _http.GetFromJsonAsync<EProductLookupItem[]>(url);
@ -59,5 +57,7 @@ namespace phronCare.UIBlazor.Services.Lookups
var items = await _http.GetFromJsonAsync<ELookUpItem[]>("api/lookup/paymentterms?q="); var items = await _http.GetFromJsonAsync<ELookUpItem[]>("api/lookup/paymentterms?q=");
return items ?? Array.Empty<ELookUpItem>(); return items ?? Array.Empty<ELookUpItem>();
} }
public Task<IEnumerable<ELookUpItem>> SearchApprovedQuotesAsync(string filtro)
=> FetchAsync($"api/lookup/approvedquotes?q={Uri.EscapeDataString(filtro)}", filtro);
} }
} }

View File

@ -5,7 +5,7 @@ namespace Services.Sales.Quotes
{ {
public interface IQuoteService public interface IQuoteService
{ {
Task<CreateQuoteResult> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId); //Task<CreateQuoteResult> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
// Aquí podrías agregar otros métodos: GetById, Search, etc. // Aquí podrías agregar otros métodos: GetById, Search, etc.
} }
} }

View File

@ -124,6 +124,9 @@ namespace phronCare.UIBlazor.Services.Sales.Quotes
throw new Exception($"ExportPdfAsync: {message}", ex); throw new Exception($"ExportPdfAsync: {message}", ex);
} }
} }
/// <summary>
/// Envía una solicitud al backend para autorizar un presupuesto específico.
/// </summary>
public async Task<bool> AuthorizeQuoteAsync(QuoteAuthorizationRequest request) public async Task<bool> AuthorizeQuoteAsync(QuoteAuthorizationRequest request)
{ {
var response = await _http.PostAsJsonAsync("/api/quote/authorize", request); var response = await _http.PostAsJsonAsync("/api/quote/authorize", request);
@ -136,6 +139,7 @@ namespace phronCare.UIBlazor.Services.Sales.Quotes
return true; return true;
} }
/// <summary> /// <summary>
/// Obtiene un presupuesto completo por ID para su visualización y autorización. /// Obtiene un presupuesto completo por ID para su visualización y autorización.
/// </summary> /// </summary>
@ -152,6 +156,7 @@ namespace phronCare.UIBlazor.Services.Sales.Quotes
return null; return null;
} }
} }
} }
public class CreateQuoteResult public class CreateQuoteResult

View File

@ -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;
}
/// <summary>
/// Obtiene un presupuesto por QuoteNumber.
/// </summary>
public async Task<QuoteDto?> GetQuoteByNumberAsync(string quoteNumber)
{
try
{
var result = await _http.GetFromJsonAsync<QuoteDto?>($"api/quote/summary/{quoteNumber}");
return result;
}
catch (Exception ex)
{
Console.WriteLine($"Error al obtener QuoteDto por QuoteNumber: {ex.Message}");
return null;
}
}
}
}

View File

@ -0,0 +1,6 @@
namespace phronCare.UIBlazor.Services.Stock.Expeditions
{
public interface IExpeditionService
{
}
}

View File

@ -0,0 +1,7 @@
using Domain.Dtos.Stock;
public interface IStockScanService
{
Task<StockItemSelectionDto?> ParseAndMatchAsync(string rawInput, int locationId);
}

View File

@ -0,0 +1,30 @@
using Domain.Dtos.Stock;
public class MockStockScanService : IStockScanService
{
public async Task<StockItemSelectionDto?> 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;
}
}

View File

@ -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<StockItemSelectionDto?> 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<PagedResult<StockItemScanResultDto>>();
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 '_');
}
}

View File

@ -66,7 +66,7 @@
"privateAssets": "all" "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": { "Microsoft.NET.ILLink.Tasks": {
"suppressParent": "All", "suppressParent": "All",
"target": "Package", "target": "Package",
"version": "[8.0.18, )", "version": "[8.0.19, )",
"autoReferenced": true "autoReferenced": true
}, },
"Microsoft.NET.Sdk.WebAssembly.Pack": { "Microsoft.NET.Sdk.WebAssembly.Pack": {
"suppressParent": "All", "suppressParent": "All",
"target": "Package", "target": "Package",
"version": "[9.0.7, )", "version": "[9.0.8, )",
"autoReferenced": true "autoReferenced": true
}, },
"PSC.Blazor.Components.Chartjs": { "PSC.Blazor.Components.Chartjs": {
@ -180,7 +180,7 @@
"downloadDependencies": [ "downloadDependencies": [
{ {
"name": "Microsoft.NETCore.App.Runtime.Mono.browser-wasm", "name": "Microsoft.NETCore.App.Runtime.Mono.browser-wasm",
"version": "[8.0.18, 8.0.18]" "version": "[8.0.19, 8.0.19]"
} }
], ],
"frameworkReferences": { "frameworkReferences": {
@ -188,7 +188,7 @@
"privateAssets": "all" "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": { "runtimes": {
@ -273,7 +273,7 @@
"privateAssets": "all" "privateAssets": "all"
} }
}, },
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.302/PortableRuntimeIdentifierGraph.json" "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
} }
} }
} }

View File

@ -7,7 +7,7 @@
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot> <NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders> <NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\maski\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle> <NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.0</NuGetToolVersion> <NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.1</NuGetToolVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' "> <ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\maski\.nuget\packages\" /> <SourceRoot Include="C:\Users\maski\.nuget\packages\" />
@ -33,8 +33,8 @@
</ItemGroup> </ItemGroup>
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' "> <ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)psc.blazor.components.chartjs\8.0.8\buildTransitive\PSC.Blazor.Components.Chartjs.props" Condition="Exists('$(NuGetPackageRoot)psc.blazor.components.chartjs\8.0.8\buildTransitive\PSC.Blazor.Components.Chartjs.props')" /> <Import Project="$(NuGetPackageRoot)psc.blazor.components.chartjs\8.0.8\buildTransitive\PSC.Blazor.Components.Chartjs.props" Condition="Exists('$(NuGetPackageRoot)psc.blazor.components.chartjs\8.0.8\buildTransitive\PSC.Blazor.Components.Chartjs.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.net.sdk.webassembly.pack\9.0.7\build\Microsoft.NET.Sdk.WebAssembly.Pack.props" Condition="Exists('$(NuGetPackageRoot)microsoft.net.sdk.webassembly.pack\9.0.7\build\Microsoft.NET.Sdk.WebAssembly.Pack.props')" /> <Import Project="$(NuGetPackageRoot)microsoft.net.sdk.webassembly.pack\9.0.8\build\Microsoft.NET.Sdk.WebAssembly.Pack.props" Condition="Exists('$(NuGetPackageRoot)microsoft.net.sdk.webassembly.pack\9.0.8\build\Microsoft.NET.Sdk.WebAssembly.Pack.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.net.illink.tasks\8.0.18\build\Microsoft.NET.ILLink.Tasks.props" Condition="Exists('$(NuGetPackageRoot)microsoft.net.illink.tasks\8.0.18\build\Microsoft.NET.ILLink.Tasks.props')" /> <Import Project="$(NuGetPackageRoot)microsoft.net.illink.tasks\8.0.19\build\Microsoft.NET.ILLink.Tasks.props" Condition="Exists('$(NuGetPackageRoot)microsoft.net.illink.tasks\8.0.19\build\Microsoft.NET.ILLink.Tasks.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.aspnetcore.components.webassembly\8.0.6\build\net8.0\Microsoft.AspNetCore.Components.WebAssembly.props" Condition="Exists('$(NuGetPackageRoot)microsoft.aspnetcore.components.webassembly\8.0.6\build\net8.0\Microsoft.AspNetCore.Components.WebAssembly.props')" /> <Import Project="$(NuGetPackageRoot)microsoft.aspnetcore.components.webassembly\8.0.6\build\net8.0\Microsoft.AspNetCore.Components.WebAssembly.props" Condition="Exists('$(NuGetPackageRoot)microsoft.aspnetcore.components.webassembly\8.0.6\build\net8.0\Microsoft.AspNetCore.Components.WebAssembly.props')" />
<Import Project="$(NuGetPackageRoot)blazored.typeahead\4.7.0\buildTransitive\Blazored.Typeahead.props" Condition="Exists('$(NuGetPackageRoot)blazored.typeahead\4.7.0\buildTransitive\Blazored.Typeahead.props')" /> <Import Project="$(NuGetPackageRoot)blazored.typeahead\4.7.0\buildTransitive\Blazored.Typeahead.props" Condition="Exists('$(NuGetPackageRoot)blazored.typeahead\4.7.0\buildTransitive\Blazored.Typeahead.props')" />
<Import Project="$(NuGetPackageRoot)blazored.toast\4.2.1\buildTransitive\Blazored.Toast.props" Condition="Exists('$(NuGetPackageRoot)blazored.toast\4.2.1\buildTransitive\Blazored.Toast.props')" /> <Import Project="$(NuGetPackageRoot)blazored.toast\4.2.1\buildTransitive\Blazored.Toast.props" Condition="Exists('$(NuGetPackageRoot)blazored.toast\4.2.1\buildTransitive\Blazored.Toast.props')" />
@ -42,8 +42,8 @@
</ImportGroup> </ImportGroup>
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' "> <PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<PkgNewtonsoft_Json Condition=" '$(PkgNewtonsoft_Json)' == '' ">C:\Users\maski\.nuget\packages\newtonsoft.json\10.0.3</PkgNewtonsoft_Json> <PkgNewtonsoft_Json Condition=" '$(PkgNewtonsoft_Json)' == '' ">C:\Users\maski\.nuget\packages\newtonsoft.json\10.0.3</PkgNewtonsoft_Json>
<PkgMicrosoft_NET_Sdk_WebAssembly_Pack Condition=" '$(PkgMicrosoft_NET_Sdk_WebAssembly_Pack)' == '' ">C:\Users\maski\.nuget\packages\microsoft.net.sdk.webassembly.pack\9.0.7</PkgMicrosoft_NET_Sdk_WebAssembly_Pack> <PkgMicrosoft_NET_Sdk_WebAssembly_Pack Condition=" '$(PkgMicrosoft_NET_Sdk_WebAssembly_Pack)' == '' ">C:\Users\maski\.nuget\packages\microsoft.net.sdk.webassembly.pack\9.0.8</PkgMicrosoft_NET_Sdk_WebAssembly_Pack>
<PkgMicrosoft_NET_ILLink_Tasks Condition=" '$(PkgMicrosoft_NET_ILLink_Tasks)' == '' ">C:\Users\maski\.nuget\packages\microsoft.net.illink.tasks\8.0.18</PkgMicrosoft_NET_ILLink_Tasks> <PkgMicrosoft_NET_ILLink_Tasks Condition=" '$(PkgMicrosoft_NET_ILLink_Tasks)' == '' ">C:\Users\maski\.nuget\packages\microsoft.net.illink.tasks\8.0.19</PkgMicrosoft_NET_ILLink_Tasks>
<PkgMicrosoft_AspNetCore_Components_WebAssembly_DevServer Condition=" '$(PkgMicrosoft_AspNetCore_Components_WebAssembly_DevServer)' == '' ">C:\Users\maski\.nuget\packages\microsoft.aspnetcore.components.webassembly.devserver\8.0.6</PkgMicrosoft_AspNetCore_Components_WebAssembly_DevServer> <PkgMicrosoft_AspNetCore_Components_WebAssembly_DevServer Condition=" '$(PkgMicrosoft_AspNetCore_Components_WebAssembly_DevServer)' == '' ">C:\Users\maski\.nuget\packages\microsoft.aspnetcore.components.webassembly.devserver\8.0.6</PkgMicrosoft_AspNetCore_Components_WebAssembly_DevServer>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -4,7 +4,7 @@
<Import Project="$(NuGetPackageRoot)microsoft.extensions.options\8.0.2\buildTransitive\net6.0\Microsoft.Extensions.Options.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.options\8.0.2\buildTransitive\net6.0\Microsoft.Extensions.Options.targets')" /> <Import Project="$(NuGetPackageRoot)microsoft.extensions.options\8.0.2\buildTransitive\net6.0\Microsoft.Extensions.Options.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.options\8.0.2\buildTransitive\net6.0\Microsoft.Extensions.Options.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.extensions.logging.abstractions\8.0.1\buildTransitive\net6.0\Microsoft.Extensions.Logging.Abstractions.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.logging.abstractions\8.0.1\buildTransitive\net6.0\Microsoft.Extensions.Logging.Abstractions.targets')" /> <Import Project="$(NuGetPackageRoot)microsoft.extensions.logging.abstractions\8.0.1\buildTransitive\net6.0\Microsoft.Extensions.Logging.Abstractions.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.logging.abstractions\8.0.1\buildTransitive\net6.0\Microsoft.Extensions.Logging.Abstractions.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.aspnetcore.components.analyzers\8.0.6\buildTransitive\netstandard2.0\Microsoft.AspNetCore.Components.Analyzers.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.aspnetcore.components.analyzers\8.0.6\buildTransitive\netstandard2.0\Microsoft.AspNetCore.Components.Analyzers.targets')" /> <Import Project="$(NuGetPackageRoot)microsoft.aspnetcore.components.analyzers\8.0.6\buildTransitive\netstandard2.0\Microsoft.AspNetCore.Components.Analyzers.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.aspnetcore.components.analyzers\8.0.6\buildTransitive\netstandard2.0\Microsoft.AspNetCore.Components.Analyzers.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.net.sdk.webassembly.pack\9.0.7\build\Microsoft.NET.Sdk.WebAssembly.Pack.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.net.sdk.webassembly.pack\9.0.7\build\Microsoft.NET.Sdk.WebAssembly.Pack.targets')" /> <Import Project="$(NuGetPackageRoot)microsoft.net.sdk.webassembly.pack\9.0.8\build\Microsoft.NET.Sdk.WebAssembly.Pack.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.net.sdk.webassembly.pack\9.0.8\build\Microsoft.NET.Sdk.WebAssembly.Pack.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.extensions.configuration.binder\8.0.1\buildTransitive\netstandard2.0\Microsoft.Extensions.Configuration.Binder.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.configuration.binder\8.0.1\buildTransitive\netstandard2.0\Microsoft.Extensions.Configuration.Binder.targets')" /> <Import Project="$(NuGetPackageRoot)microsoft.extensions.configuration.binder\8.0.1\buildTransitive\netstandard2.0\Microsoft.Extensions.Configuration.Binder.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.configuration.binder\8.0.1\buildTransitive\netstandard2.0\Microsoft.Extensions.Configuration.Binder.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.aspnetcore.components.webassembly.devserver\8.0.6\build\Microsoft.AspNetCore.Components.WebAssembly.DevServer.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.aspnetcore.components.webassembly.devserver\8.0.6\build\Microsoft.AspNetCore.Components.WebAssembly.DevServer.targets')" /> <Import Project="$(NuGetPackageRoot)microsoft.aspnetcore.components.webassembly.devserver\8.0.6\build\Microsoft.AspNetCore.Components.WebAssembly.DevServer.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.aspnetcore.components.webassembly.devserver\8.0.6\build\Microsoft.AspNetCore.Components.WebAssembly.DevServer.targets')" />
</ImportGroup> </ImportGroup>

View File

@ -628,13 +628,13 @@
} }
} }
}, },
"Microsoft.NET.ILLink.Tasks/8.0.18": { "Microsoft.NET.ILLink.Tasks/8.0.19": {
"type": "package", "type": "package",
"build": { "build": {
"build/Microsoft.NET.ILLink.Tasks.props": {} "build/Microsoft.NET.ILLink.Tasks.props": {}
} }
}, },
"Microsoft.NET.Sdk.WebAssembly.Pack/9.0.7": { "Microsoft.NET.Sdk.WebAssembly.Pack/9.0.8": {
"type": "package", "type": "package",
"build": { "build": {
"build/Microsoft.NET.Sdk.WebAssembly.Pack.props": {}, "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", "type": "package",
"build": { "build": {
"build/Microsoft.NET.ILLink.Tasks.props": {} "build/Microsoft.NET.ILLink.Tasks.props": {}
} }
}, },
"Microsoft.NET.Sdk.WebAssembly.Pack/9.0.7": { "Microsoft.NET.Sdk.WebAssembly.Pack/9.0.8": {
"type": "package", "type": "package",
"build": { "build": {
"build/Microsoft.NET.Sdk.WebAssembly.Pack.props": {}, "build/Microsoft.NET.Sdk.WebAssembly.Pack.props": {},
@ -5752,10 +5752,10 @@
"microsoft.net.http.headers.nuspec" "microsoft.net.http.headers.nuspec"
] ]
}, },
"Microsoft.NET.ILLink.Tasks/8.0.18": { "Microsoft.NET.ILLink.Tasks/8.0.19": {
"sha512": "OiXqr2YIBEV9dsAWEtasK470ALyJ0VxJ9k4MotOxlWV6HeEgrJKYMW4HHj1OCCXvqE0/A25wEKPkpfiBARgDZA==", "sha512": "IhHf+zeZiaE5EXRyxILd4qM+Hj9cxV3sa8MpzZgeEhpvaG3a1VEGF6UCaPFLO44Kua3JkLKluE0SWVamS50PlA==",
"type": "package", "type": "package",
"path": "microsoft.net.illink.tasks/8.0.18", "path": "microsoft.net.illink.tasks/8.0.19",
"hasTools": true, "hasTools": true,
"files": [ "files": [
".nupkg.metadata", ".nupkg.metadata",
@ -5769,7 +5769,7 @@
"build/Microsoft.NET.ILLink.Analyzers.props", "build/Microsoft.NET.ILLink.Analyzers.props",
"build/Microsoft.NET.ILLink.Tasks.props", "build/Microsoft.NET.ILLink.Tasks.props",
"build/Microsoft.NET.ILLink.targets", "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", "microsoft.net.illink.tasks.nuspec",
"tools/net472/ILLink.Tasks.dll", "tools/net472/ILLink.Tasks.dll",
"tools/net472/ILLink.Tasks.dll.config", "tools/net472/ILLink.Tasks.dll.config",
@ -5803,10 +5803,10 @@
"useSharedDesignerContext.txt" "useSharedDesignerContext.txt"
] ]
}, },
"Microsoft.NET.Sdk.WebAssembly.Pack/9.0.7": { "Microsoft.NET.Sdk.WebAssembly.Pack/9.0.8": {
"sha512": "5ehgbqGUERh0JVhTUPwFizw4hIoAglkFk/WMs45djePp16YHP11Vnmx44rOQ3gLW8/aDYN1j+pKdAtcEp4QOcw==", "sha512": "+JJYRyS8YoLMzLquskLfdF8RFp2PhN5v+lGCGDLHsywj3JHfSV/Zqo8+T0Fl0Xvoc9Js8YkrAWTnm7M2/3CniA==",
"type": "package", "type": "package",
"path": "microsoft.net.sdk.webassembly.pack/9.0.7", "path": "microsoft.net.sdk.webassembly.pack/9.0.8",
"hasTools": true, "hasTools": true,
"files": [ "files": [
".nupkg.metadata", ".nupkg.metadata",
@ -5837,7 +5837,7 @@
"build/Microsoft.NET.Sdk.WebAssembly.Pack.targets", "build/Microsoft.NET.Sdk.WebAssembly.Pack.targets",
"build/Wasm.web.config", "build/Wasm.web.config",
"build/browser.runtimeconfig.template.json", "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", "microsoft.net.sdk.webassembly.pack.nuspec",
"tools/net472/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.dll", "tools/net472/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.dll",
"tools/net472/Microsoft.NET.WebAssembly.Webcil.dll", "tools/net472/Microsoft.NET.WebAssembly.Webcil.dll",
@ -10559,8 +10559,8 @@
"Microsoft.AspNetCore.Components.Authorization >= 8.0.6", "Microsoft.AspNetCore.Components.Authorization >= 8.0.6",
"Microsoft.AspNetCore.Components.WebAssembly >= 8.0.6", "Microsoft.AspNetCore.Components.WebAssembly >= 8.0.6",
"Microsoft.AspNetCore.Components.WebAssembly.DevServer >= 8.0.6", "Microsoft.AspNetCore.Components.WebAssembly.DevServer >= 8.0.6",
"Microsoft.NET.ILLink.Tasks >= 8.0.18", "Microsoft.NET.ILLink.Tasks >= 8.0.19",
"Microsoft.NET.Sdk.WebAssembly.Pack >= 9.0.7", "Microsoft.NET.Sdk.WebAssembly.Pack >= 9.0.8",
"PSC.Blazor.Components.Chartjs >= 8.0.8", "PSC.Blazor.Components.Chartjs >= 8.0.8",
"Transversal >= 1.0.0" "Transversal >= 1.0.0"
] ]
@ -10651,13 +10651,13 @@
"Microsoft.NET.ILLink.Tasks": { "Microsoft.NET.ILLink.Tasks": {
"suppressParent": "All", "suppressParent": "All",
"target": "Package", "target": "Package",
"version": "[8.0.18, )", "version": "[8.0.19, )",
"autoReferenced": true "autoReferenced": true
}, },
"Microsoft.NET.Sdk.WebAssembly.Pack": { "Microsoft.NET.Sdk.WebAssembly.Pack": {
"suppressParent": "All", "suppressParent": "All",
"target": "Package", "target": "Package",
"version": "[9.0.7, )", "version": "[9.0.8, )",
"autoReferenced": true "autoReferenced": true
}, },
"PSC.Blazor.Components.Chartjs": { "PSC.Blazor.Components.Chartjs": {
@ -10679,7 +10679,7 @@
"downloadDependencies": [ "downloadDependencies": [
{ {
"name": "Microsoft.NETCore.App.Runtime.Mono.browser-wasm", "name": "Microsoft.NETCore.App.Runtime.Mono.browser-wasm",
"version": "[8.0.18, 8.0.18]" "version": "[8.0.19, 8.0.19]"
} }
], ],
"frameworkReferences": { "frameworkReferences": {
@ -10687,7 +10687,7 @@
"privateAssets": "all" "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": { "runtimes": {