phronCare/phronCare.UIBlazor/Pages/Stock/Shared/StockItemSelectorModal.razor
Leandro Hernan Rojas d884970019
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 7m57s
Add StockScanModal
2025-08-24 02:15:54 -03:00

227 lines
7.7 KiB
Plaintext

@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>Serial</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.Serial</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);
// 3) BusinessKey (clave de fusión) y contexto
{
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,
Serial = matchedItem.Serial,
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,
Serial= x.Serial,
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 string Serial { get; set; } = string.Empty;
public DateTime? Expiration { get; set; }
public decimal Available { get; set; }
public decimal Selected { get; set; }
public int LocationId { get; set; }
}
}