Arreglo Fix1 SelectorScam
Some checks failed
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Failing after 1m2s
Some checks failed
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Failing after 1m2s
This commit is contained in:
parent
d884970019
commit
345bc817c4
@ -20,7 +20,7 @@
|
||||
/// <summary>
|
||||
/// Nombre o descripción corta del producto (solo para visualización en UI)
|
||||
/// </summary>
|
||||
public string ProductName { get; set; } = string.Empty;
|
||||
public string? ProductName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Lote del stock seleccionado
|
||||
@ -37,6 +37,10 @@
|
||||
/// </summary>
|
||||
public DateTime? Expiration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tipo de trazabilidad: 1=No aplica, 2=Por cantidad, 3=Por lote y vencimiento
|
||||
/// </summary>
|
||||
public int TraceabilityType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cantidad que el usuario desea usar (puede ser decimal si es por peso o volumen)
|
||||
|
||||
44
Domain/Dtos/Stock/StockSnapshotItem.cs
Normal file
44
Domain/Dtos/Stock/StockSnapshotItem.cs
Normal file
@ -0,0 +1,44 @@
|
||||
namespace Domain.Dtos.Stock
|
||||
{
|
||||
/// <summary>
|
||||
/// Contexto de lo ya agregado en la expedición (no se persiste).
|
||||
/// Sirve para evitar duplicados y calcular disponible efectivo.
|
||||
/// </summary>
|
||||
public class StockSnapshotItem
|
||||
{
|
||||
public int ProductId { get; set; }
|
||||
public string? ProductName { get; set; } = string.Empty;
|
||||
public int LocationId { get; set; }
|
||||
public string Batch { get; set; } = string.Empty;
|
||||
public DateOnly? Expiration { get; set; }
|
||||
public string Serial { get; set; } = string.Empty;
|
||||
|
||||
/// <summary> 1=No aplica, 2=Por cantidad, 3=Por lote y vencimiento (solo UI/validación; no se persiste). </summary>
|
||||
public int TraceabilityType { get; set; }
|
||||
|
||||
/// <summary>Cantidad ya agregada en la expedición para esta clave de negocio.</summary>
|
||||
public decimal Quantity { get; set; }
|
||||
|
||||
/// <summary>Clave de fusión para idempotencia (no se persiste).</summary>
|
||||
public string BusinessKey { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public static class StockKeys
|
||||
{
|
||||
public static string BuildBusinessKey(int productId, int locationId, string batch, DateOnly? expiration, string serial)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(serial))
|
||||
return $"P{productId}-S{serial.Trim()}";
|
||||
|
||||
batch = batch?.Trim() ?? string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(batch) && expiration.HasValue)
|
||||
return $"P{productId}-L{locationId}-B{batch}-E{expiration.Value:yyyyMMdd}";
|
||||
|
||||
if (!string.IsNullOrEmpty(batch))
|
||||
return $"P{productId}-L{locationId}-B{batch}";
|
||||
|
||||
return $"P{productId}-L{locationId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -47,6 +47,19 @@
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tipo de trazabilidad: 1=No aplica, 2=Por cantidad, 3=Por lote y vencimiento
|
||||
/// ATENCION: Campo auxiliar de UI/servicio para aplicar reglas de trazabilidad.
|
||||
/// No se persiste: la DB no tiene esta columna.
|
||||
/// </summary>
|
||||
public int TraceabilityType { get; set; }
|
||||
/// <summary>
|
||||
/// Nombre del producto: Nombre descriptivo de la tabla Products
|
||||
/// ATENCION: Campo auxiliar de UI/servicio para aplicar reglas de trazabilidad.
|
||||
/// No se persiste: la DB no tiene esta columna.
|
||||
/// </summary>
|
||||
public string? ProductName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Precio estimado unitario del producto (sin efecto contable)
|
||||
/// </summary>
|
||||
|
||||
@ -6,17 +6,12 @@
|
||||
@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 />
|
||||
@ -154,12 +149,12 @@
|
||||
@code {
|
||||
private ELSExpeditionHeader Model = new();
|
||||
private ExtraInfoModel ExtraInfo = new();
|
||||
private string DispatchInstruction = string.Empty;
|
||||
private ELookUpItem? SelectedQuote;
|
||||
private List<ELSExpeditionDetail> Details = new();
|
||||
|
||||
private List<ELSExpeditionDetail> Details = new();
|
||||
private List<ProductSetItemDto> ProductSetItems = new();
|
||||
|
||||
private string DispatchInstruction = string.Empty;
|
||||
|
||||
private string ticketIdString
|
||||
{
|
||||
@ -224,47 +219,133 @@
|
||||
{
|
||||
// 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,
|
||||
// TraceabilityType=s.TraceabilityType, //agregado al model pero no es registrable en la entidad
|
||||
// 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 async Task OpenStockItemSelectorModal()
|
||||
{
|
||||
|
||||
var parameters = new ModalParameters();
|
||||
parameters.Add(nameof(StockItemSelectorModal.SetItems), ProductSetItems); // o null
|
||||
//parameters.Add(nameof(StockItemSelectorModal.LocationId), SelectedLocationId);
|
||||
parameters.Add(nameof(StockItemSelectorModal.SetItems), ProductSetItems);
|
||||
parameters.Add(nameof(StockItemSelectorModal.Snapshot), BuildSnapshotFromDetails()); // ← clave
|
||||
|
||||
var options = new ModalOptions()
|
||||
{
|
||||
Size = ModalSize.Large,
|
||||
HideHeader = true
|
||||
};
|
||||
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)
|
||||
MergeSelectionsByBusinessKey(selectedItems); // ← usar merge (ver abajo)
|
||||
StateHasChanged();
|
||||
toastService.ShowSuccess($"{selectedItems.Count} item(s) agregados/actualizados.");
|
||||
}
|
||||
}
|
||||
|
||||
private void MergeSelectionsByBusinessKey(List<StockItemSelectionDto> selected)
|
||||
{
|
||||
var detail = new ELSExpeditionDetail
|
||||
foreach (var s in selected)
|
||||
{
|
||||
var exp = s.Expiration.HasValue ? DateOnly.FromDateTime(s.Expiration.Value) : (DateOnly?)null;
|
||||
|
||||
var key = StockKeys.BuildBusinessKey(
|
||||
s.ProductId,
|
||||
s.LocationId,
|
||||
s.Batch ?? string.Empty,
|
||||
exp,
|
||||
s.Serial ?? string.Empty
|
||||
);
|
||||
|
||||
var existing = Details.FirstOrDefault(d =>
|
||||
StockKeys.BuildBusinessKey(d.ProductId, d.LocationId, d.Batch ?? string.Empty, d.Expiration, d.Serial ?? string.Empty) == key
|
||||
);
|
||||
|
||||
// Normalizo cantidad pedida desde el modal (total final)
|
||||
var newQty = s.Quantity < 0 ? 0 : s.Quantity;
|
||||
|
||||
// Serial ⇒ siempre 1 (ignora lo que venga)
|
||||
if (!string.IsNullOrWhiteSpace(s.Serial))
|
||||
newQty = 1;
|
||||
|
||||
if (existing is not null)
|
||||
{
|
||||
if (newQty == 0)
|
||||
{
|
||||
// Si el modal dejó en 0, se elimina la fila
|
||||
Details.Remove(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
// SET (no sumar): que quede exactamente como en el modal
|
||||
existing.Quantity = newQty;
|
||||
existing.ProductName = s.ProductName; // opcional, por si viene actualizado
|
||||
existing.Batch = s.Batch;
|
||||
existing.Serial = s.Serial;
|
||||
existing.Expiration = exp;
|
||||
existing.LocationId = s.LocationId;
|
||||
existing.TraceabilityType = s.TraceabilityType; // UI only
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Si no existía y la cantidad es > 0, crear nueva fila
|
||||
if (newQty > 0)
|
||||
{
|
||||
Details.Add(new ELSExpeditionDetail
|
||||
{
|
||||
ProductId = s.ProductId,
|
||||
Quantity = s.Quantity, // si es Serial*, probablemente 1
|
||||
ProductName = s.ProductName,
|
||||
Quantity = newQty,
|
||||
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);
|
||||
Expiration = exp,
|
||||
TraceabilityType = s.TraceabilityType, // UI only (no DB)
|
||||
Serial = s.Serial,
|
||||
LocationId = s.LocationId
|
||||
});
|
||||
}
|
||||
// Si newQty == 0 y no existía, no hay nada que hacer
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
toastService.ShowSuccess($"{selectedItems.Count} item(s) agregados a la expedición.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private class ExtraInfoModel
|
||||
{
|
||||
@ -273,4 +354,32 @@
|
||||
public string? Patient { get; set; }
|
||||
public DateTime? SurgeryDate { get; set; }
|
||||
}
|
||||
|
||||
private List<StockSnapshotItem> BuildSnapshotFromDetails()
|
||||
{
|
||||
return Details.Select(d =>
|
||||
{
|
||||
var key = StockKeys.BuildBusinessKey(
|
||||
d.ProductId,
|
||||
d.LocationId,
|
||||
d.Batch ?? string.Empty,
|
||||
d.Expiration,
|
||||
d.Serial ?? string.Empty
|
||||
);
|
||||
|
||||
return new StockSnapshotItem
|
||||
{
|
||||
ProductId = d.ProductId,
|
||||
ProductName = d.ProductName,
|
||||
LocationId = d.LocationId,
|
||||
Batch = d.Batch ?? string.Empty,
|
||||
Expiration = d.Expiration,
|
||||
Serial = d.Serial ?? string.Empty,
|
||||
TraceabilityType = d.TraceabilityType, // UI only (no DB)
|
||||
Quantity = d.Quantity,
|
||||
BusinessKey = key
|
||||
};
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
@using Blazored.Modal.Services
|
||||
@using Domain.Dtos.Stock
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using System.Globalization
|
||||
|
||||
@inject IToastService toastService
|
||||
@inject IStockScanService stockScanService
|
||||
@ -36,14 +37,13 @@
|
||||
<button type="button" class="btn btn-secondary mt-2" @onclick="HandleScan">Buscar</button>
|
||||
</div>
|
||||
|
||||
@if (StockList.Any())
|
||||
@if (HasLastScan)
|
||||
{
|
||||
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>
|
||||
@ -63,9 +63,14 @@
|
||||
<td>@item.Batch</td>
|
||||
<td>@item.Serial</td>
|
||||
<td>@item.Expiration?.ToShortDateString()</td>
|
||||
<td>@item.Available</td>
|
||||
<td>@item.Available.ToString("N2", CultureInfo.CurrentCulture)</td>
|
||||
<td>
|
||||
<InputNumber @bind-Value="item.Selected" class="form-control form-control-sm" min="0" max="@item.Available" />
|
||||
<input type="number"
|
||||
value="@(item.Selected.ToString("G29"))"
|
||||
@onchange="@(n => item.Selected = decimal.Parse(n.Value.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture))"
|
||||
min="0"
|
||||
max="@(item.Available.ToString("G29"))"
|
||||
class="form-control form-control-sm" />
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@ -83,28 +88,53 @@
|
||||
|
||||
[Parameter] public int? ProductId { get; set; }
|
||||
[Parameter] public int? LocationId { get; set; }
|
||||
[Parameter] public List<ProductSetItemDto>? SetItems { get; set; }
|
||||
//[Parameter] public List<ProductSetItemDto>? SetItems { get; set; }
|
||||
[Parameter] public List<StockSnapshotItem> Snapshot { get; set; } = new();
|
||||
|
||||
// Mapa: BusinessKey -> Cantidad ya en la expedición
|
||||
private Dictionary<string, decimal> _alreadyInExpedition = new();
|
||||
private bool HasLastScan { get; set; }
|
||||
|
||||
//private readonly List<StockItemSelectionDto> SelectedItems = new();
|
||||
private List<StockDisplayRow> StockList = new();
|
||||
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()
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
await LoadMockStock();
|
||||
HasLastScan = false;
|
||||
_alreadyInExpedition = Snapshot
|
||||
.GroupBy(x => x.BusinessKey)
|
||||
.ToDictionary(g => g.Key, g => g.Sum(x => x.Quantity));
|
||||
|
||||
// Precargar la tabla del modal con lo que ya estaba en la expedición
|
||||
StockList = Snapshot.Select(s => new StockDisplayRow
|
||||
{
|
||||
ProductId = s.ProductId,
|
||||
ProductName = s.ProductName ?? "<Producto sin descripción>",
|
||||
Batch = s.Batch,
|
||||
Serial = s.Serial,
|
||||
Expiration = s.Expiration?.ToDateTime(TimeOnly.MinValue),
|
||||
LocationId = s.LocationId,
|
||||
TraceabilityType = s.TraceabilityType,
|
||||
BusinessKey = s.BusinessKey,
|
||||
Selected = s.Quantity, // cantidad actual en la expedición
|
||||
Available = 0 // opcional: si querés recalcular, traer de BD
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
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")
|
||||
@ -113,56 +143,136 @@
|
||||
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);
|
||||
var raw = InputCode?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
return;
|
||||
|
||||
// 3) BusinessKey (clave de fusión) y contexto
|
||||
// Parsear y resolver candidato desde backend
|
||||
var s = await stockScanService.ParseAndMatchAsync(raw, LocationId ?? 1);
|
||||
|
||||
// limpiar input y devolver foco
|
||||
InputCode = string.Empty;
|
||||
//wait Refocus();
|
||||
|
||||
if (s is null)
|
||||
{
|
||||
toastService.ShowWarning("No se encontró el producto en stock.");
|
||||
toastService.ShowWarning("No se encontró un artículo para ese código.");
|
||||
return;
|
||||
}
|
||||
HasLastScan = true;
|
||||
|
||||
// Mapear a fila del modal
|
||||
var row = new StockDisplayRow
|
||||
{
|
||||
StockItemId = s.StockItemId,
|
||||
ProductId = s.ProductId,
|
||||
ProductName = s.ProductName ?? string.Empty,
|
||||
Batch = s.Batch ?? string.Empty,
|
||||
Serial = s.Serial ?? string.Empty,
|
||||
Expiration = s.Expiration,
|
||||
LocationId = s.LocationId,
|
||||
Available = s.Quantity,
|
||||
TraceabilityType = s.TraceabilityType,
|
||||
Selected = 0
|
||||
};
|
||||
|
||||
// Clave de negocio + contexto del snapshot
|
||||
row.BusinessKey = BuildKeyFromRow(row);
|
||||
|
||||
var alreadyInExpedition = _alreadyInExpedition.TryGetValue(row.BusinessKey, out var q)
|
||||
? q
|
||||
: 0m;
|
||||
|
||||
var selectedInModal = StockList
|
||||
.Where(x => x.BusinessKey == row.BusinessKey)
|
||||
.Sum(x => x.Selected);
|
||||
|
||||
var effectiveAvailable = row.Available - selectedInModal;
|
||||
|
||||
// Reglas por trazabilidad
|
||||
// SERIAL*: nunca duplicar ni incrementar
|
||||
if (!string.IsNullOrWhiteSpace(row.Serial))
|
||||
{
|
||||
// buscar si ya existe la misma BusinessKey en la tabla del modal
|
||||
var existingSerial = StockList.FirstOrDefault(x => x.BusinessKey == row.BusinessKey);
|
||||
|
||||
// 🔄 REFRESH siempre que tengamos la fila (si existe), antes de cualquier return
|
||||
if (existingSerial is not null)
|
||||
{
|
||||
existingSerial.Available = row.Available ; // actualizar disponible con el del re-escaneo
|
||||
}
|
||||
|
||||
var existsInModal = existingSerial is not null;
|
||||
|
||||
//var existsInModal = StockList.Any(x => x.BusinessKey == row.BusinessKey);
|
||||
if (alreadyInExpedition > 0 || existsInModal)
|
||||
{
|
||||
toastService.ShowWarning("Este serial ya está agregado.");
|
||||
await Refocus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (StockList.Any(s => s.StockItemId == matchedItem.StockItemId))
|
||||
if (effectiveAvailable <= 0)
|
||||
{
|
||||
toastService.ShowInfo("Este ítem ya está listado.");
|
||||
toastService.ShowWarning("No hay disponible para este serial.");
|
||||
await Refocus();
|
||||
return;
|
||||
}
|
||||
|
||||
StockList.Insert(0, new StockDisplayRow
|
||||
row.Selected = 1; // serial siempre = 1
|
||||
StockList.Insert(0, row);
|
||||
await Refocus();
|
||||
return;
|
||||
}
|
||||
|
||||
// BATCH / NONE: sumar sin pasar el disponible efectivo
|
||||
if (effectiveAvailable <= 0)
|
||||
{
|
||||
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
|
||||
});
|
||||
var existingSerial = StockList.FirstOrDefault(x => x.BusinessKey == row.BusinessKey);
|
||||
|
||||
// 🔄 REFRESH siempre que tengamos la fila (si existe), antes de cualquier return
|
||||
if (existingSerial is not null)
|
||||
{
|
||||
existingSerial.Available = row.Available; // actualizar disponible con el del re-escaneo
|
||||
}
|
||||
toastService.ShowWarning("No hay más disponible para esta combinación.");
|
||||
await Refocus();
|
||||
return;
|
||||
}
|
||||
|
||||
var existingBN = StockList.FirstOrDefault(x => x.BusinessKey == row.BusinessKey);
|
||||
if (existingBN is not null)
|
||||
{
|
||||
existingBN.Available = s.Quantity;
|
||||
// suma +1 cap al disponible efectivo
|
||||
var add = 1m;
|
||||
var maxAdd = Math.Min(add, effectiveAvailable);
|
||||
if (maxAdd <= 0)
|
||||
{
|
||||
toastService.ShowWarning("Se alcanzó el máximo disponible.");
|
||||
await Refocus();
|
||||
return;
|
||||
}
|
||||
|
||||
existingBN.Selected += maxAdd;
|
||||
await Refocus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Nueva fila (otra ubicación/lote/etc.)
|
||||
row.Selected = 1;
|
||||
StockList.Insert(0, row);
|
||||
await Refocus();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
toastService.ShowError($"Error en escaneo: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
InputCode = string.Empty;
|
||||
toastService.ShowError($"Error al procesar el escaneo: {ex.Message}");
|
||||
await Refocus();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,8 +282,6 @@
|
||||
await scanInput.FocusAsync(); // deja el cursor listo para el próximo escaneo
|
||||
}
|
||||
|
||||
private Task Cancel() => ModalInstance.CancelAsync();
|
||||
|
||||
private async Task Confirm()
|
||||
{
|
||||
var selected = StockList
|
||||
@ -186,6 +294,7 @@
|
||||
Batch = x.Batch,
|
||||
Serial= x.Serial,
|
||||
Expiration = x.Expiration,
|
||||
TraceabilityType = x.TraceabilityType,
|
||||
Quantity = x.Selected,
|
||||
LocationId = x.LocationId
|
||||
})
|
||||
@ -196,20 +305,10 @@
|
||||
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;
|
||||
}
|
||||
private Task Cancel() => ModalInstance.CancelAsync();
|
||||
|
||||
public class StockDisplayRow
|
||||
{
|
||||
@ -222,5 +321,20 @@
|
||||
public decimal Available { get; set; }
|
||||
public decimal Selected { get; set; }
|
||||
public int LocationId { get; set; }
|
||||
public int TraceabilityType { get; set; }
|
||||
|
||||
/// <summary>Clave de fusión (no se persiste).</summary>
|
||||
public string BusinessKey { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private static string BuildKeyFromRow(StockDisplayRow r)
|
||||
{
|
||||
return Domain.Dtos.Stock.StockKeys.BuildBusinessKey(
|
||||
r.ProductId,
|
||||
r.LocationId,
|
||||
r.Batch,
|
||||
r.Expiration.HasValue ? DateOnly.FromDateTime(r.Expiration.Value) : (DateOnly?)null,
|
||||
r.Serial
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,23 +1,26 @@
|
||||
using phronCare.UIBlazor;
|
||||
using phronCare.UIBlazor.Services.UI;
|
||||
using phronCare.UIBlazor.Services.Sales;
|
||||
using phronCare.UIBlazor.Services.Lookups;
|
||||
using phronCare.UIBlazor.Services.Tickets;
|
||||
using phronCare.UIBlazor.Services.Authorization;
|
||||
using phronCare.UIBlazor.Services.Sales.Quotes;
|
||||
using phronCare.UIBlazor.Services.Integrations;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
|
||||
using Blazored.Modal;
|
||||
using Blazored.Toast;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using phronCare.UIBlazor;
|
||||
using phronCare.UIBlazor.Services.Authorization;
|
||||
using phronCare.UIBlazor.Services.Integrations;
|
||||
using phronCare.UIBlazor.Services.Lookups;
|
||||
using phronCare.UIBlazor.Services.Sales;
|
||||
using phronCare.UIBlazor.Services.Sales.Quotes;
|
||||
using phronCare.UIBlazor.Services.Stock;
|
||||
using phronCare.UIBlazor.Services.Stock.Expeditions;
|
||||
using phronCare.UIBlazor.Services.Tickets;
|
||||
using phronCare.UIBlazor.Services.UI;
|
||||
using System.Globalization;
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
var culture = new CultureInfo("es-AR");
|
||||
CultureInfo.DefaultThreadCurrentCulture = culture;
|
||||
CultureInfo.DefaultThreadCurrentUICulture = culture;
|
||||
#region Proveedor de Autorizacion
|
||||
builder.Services.AddScoped<CustomAuthorizationProvider>();
|
||||
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthorizationProvider>(p => p.GetRequiredService<CustomAuthorizationProvider>());
|
||||
@ -70,6 +73,4 @@ static void InjectDependencies(WebAssemblyHostBuilder builder)
|
||||
builder.Services.AddScoped<LSProductService>();
|
||||
builder.Services.AddScoped<LSUnitOfMeasureService>();
|
||||
builder.Services.AddScoped<ExpeditionService>();
|
||||
|
||||
|
||||
}
|
||||
@ -37,6 +37,7 @@ public class StockScanService : IStockScanService
|
||||
Expiration = first.Expiration?.ToDateTime(TimeOnly.MinValue),
|
||||
Quantity = first.AvailableQty,
|
||||
LocationId = first.LocationId ?? locationId,
|
||||
TraceabilityType = first.TraceabilityType,
|
||||
Serial = first.Serial // si lo devolvés en el DTO de scan
|
||||
};
|
||||
}
|
||||
|
||||
@ -5,6 +5,11 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Habilita datos de globalización (ICU) en WASM -->
|
||||
<InvariantGlobalization>false</InvariantGlobalization>
|
||||
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="wwwroot\css\fontawesome\**" />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user