UpClean de Codigo
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m5s
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m5s
This commit is contained in:
parent
a38bd4570f
commit
8303751ab7
@ -1,10 +1,8 @@
|
||||
@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
|
||||
|
||||
@inject NavigationManager Navigation
|
||||
@ -44,7 +42,7 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Ticket ID</label>
|
||||
<InputText class="form-control" @bind-Value="ticketIdString" @bind-Value:event="oninput" />
|
||||
<InputText class="form-control" @bind-Value="ticketIdString" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -118,26 +116,23 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (int i = 0; i < 50; i++)
|
||||
{
|
||||
@foreach (var item in Details)
|
||||
{
|
||||
<tr>
|
||||
<td class="text-center align-middle">@item.ProductId</td>
|
||||
<td>@item.ProductName</td>
|
||||
<td class="text-center align-middle">@item.Quantity</td>
|
||||
<td class="text-center align-middle">@item.Batch</td>
|
||||
<td class="text-center align-middle">@item.Serial</td>
|
||||
<td class="text-center align-middle">@item.Expiration?.ToString("yyyy-MM-dd")</td>
|
||||
<td class ="text-center align-middle">@item.LocationId</td>
|
||||
<td class="text-center align-middle">
|
||||
<button class="btn btn-link p-0" style="color: red;" title="Eliminar" @onclick="() => RemoveItem(item)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
@foreach (var item in Details)
|
||||
{
|
||||
<tr>
|
||||
<td class="text-center align-middle">@item.ProductId</td>
|
||||
<td>@item.ProductName</td>
|
||||
<td class="text-center align-middle">@item.Quantity</td>
|
||||
<td class="text-center align-middle">@item.Batch</td>
|
||||
<td class="text-center align-middle">@item.Serial</td>
|
||||
<td class="text-center align-middle">@item.Expiration?.ToString("yyyy-MM-dd")</td>
|
||||
<td class ="text-center align-middle">@item.LocationId</td>
|
||||
<td class="text-center align-middle">
|
||||
<button class="btn btn-link p-0" style="color: red;" title="Eliminar" @onclick="() => RemoveItem(item)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -164,11 +159,7 @@
|
||||
|
||||
private string DispatchInstruction = string.Empty;
|
||||
|
||||
private string ticketIdString
|
||||
{
|
||||
get => Model.TicketId?.ToString() ?? string.Empty;
|
||||
set => Model.TicketId = Guid.TryParse(value, out var guid) ? guid : null;
|
||||
}
|
||||
private string ticketIdString = string.Empty;
|
||||
|
||||
private async Task<IEnumerable<ELookUpItem>> SearchQuotes(string filter)
|
||||
{
|
||||
@ -247,11 +238,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Fusiona la lista de ítems seleccionados en el modal con la lista principal <c>Details</c>.
|
||||
///
|
||||
/// Reglas principales:
|
||||
/// - Se construye una clave de negocio (ProductId, LocationId, Batch, Expiration, Serial) para identificar ítems únicos.
|
||||
/// - Si el ítem ya existe en <c>Details</c>:
|
||||
/// • Si la cantidad recibida es > 0 → se actualiza la fila existente (SET exacto, no suma).
|
||||
/// • Si la cantidad recibida es 0 y la fila tenía cantidad previa > 0 → se interpreta como
|
||||
/// "snapshot sin intención de borrar" y se conserva la fila (no se elimina).
|
||||
/// • Si la cantidad recibida es 0 y no había cantidad previa → se elimina la fila.
|
||||
/// - Si el ítem no existe en <c>Details</c> y la cantidad recibida es > 0 → se agrega como nueva fila.
|
||||
/// - Si el ítem no existe y la cantidad es 0 → no se hace nada.
|
||||
/// - Si el ítem tiene número de serie → la cantidad se fuerza siempre a 1.
|
||||
///
|
||||
/// Esta lógica evita el bug en el que, al reabrir el modal con un snapshot, los ítems volvían con
|
||||
/// cantidad = 0 y eran eliminados indebidamente de la grilla principal.
|
||||
/// </summary>
|
||||
private void MergeSelectionsByBusinessKey(List<StockItemSelectionDto> selected)
|
||||
{
|
||||
foreach (var s in selected)
|
||||
{
|
||||
var exp = s.Expiration.HasValue ? DateOnly.FromDateTime(s.Expiration.Value) : (DateOnly?)null;
|
||||
var exp = s.Expiration.HasValue
|
||||
? DateOnly.FromDateTime(s.Expiration.Value)
|
||||
: (DateOnly?)null;
|
||||
|
||||
var key = StockKeys.BuildBusinessKey(
|
||||
s.ProductId,
|
||||
@ -265,9 +274,10 @@
|
||||
StockKeys.BuildBusinessKey(d.ProductId, d.LocationId, d.Batch ?? string.Empty, d.Expiration, d.Serial ?? string.Empty) == key
|
||||
);
|
||||
|
||||
// Normalización de quantity (no negativa)
|
||||
var newQty = s.Quantity < 0 ? 0 : s.Quantity;
|
||||
|
||||
// Serial ⇒ siempre 1
|
||||
// Serial ⇒ siempre 1 (ignora lo que venga del modal)
|
||||
if (!string.IsNullOrWhiteSpace(s.Serial))
|
||||
newQty = 1;
|
||||
|
||||
@ -275,26 +285,26 @@
|
||||
{
|
||||
if (newQty == 0)
|
||||
{
|
||||
// ⚠️ Si ya había cantidad y el modal devolvió 0,
|
||||
// lo tratamos como "snapshot sin intención de borrar" → no remover.
|
||||
if (existing.Quantity > 0)
|
||||
{
|
||||
// ⚠️ Caso snapshot con 0 → ignorar, mantener la fila
|
||||
continue;
|
||||
}
|
||||
|
||||
// 0 explícito válido → borrar
|
||||
// 0 explícito válido (no había cantidad previa): eliminar.
|
||||
Details.Remove(existing);
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// SET: exactamente lo que vino del modal
|
||||
existing.Quantity = newQty;
|
||||
existing.ProductName = s.ProductName;
|
||||
existing.Batch = s.Batch;
|
||||
existing.Serial = s.Serial;
|
||||
existing.Expiration = exp;
|
||||
existing.LocationId = s.LocationId;
|
||||
existing.TraceabilityType = s.TraceabilityType;
|
||||
}
|
||||
|
||||
// SET exacto (no sumar): mantener coherencia con el modal
|
||||
existing.Quantity = newQty;
|
||||
|
||||
// Actualizaciones mínimas necesarias (evitar sobreescrituras innecesarias):
|
||||
if (!string.IsNullOrWhiteSpace(s.ProductName)) existing.ProductName = s.ProductName;
|
||||
if (s.Batch is not null) existing.Batch = s.Batch;
|
||||
if (s.Serial is not null) existing.Serial = s.Serial;
|
||||
existing.Expiration = exp;
|
||||
existing.LocationId = s.LocationId;
|
||||
existing.TraceabilityType = s.TraceabilityType; // UI only
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -307,23 +317,16 @@
|
||||
Quantity = newQty,
|
||||
Batch = s.Batch,
|
||||
Expiration = exp,
|
||||
TraceabilityType = s.TraceabilityType,
|
||||
TraceabilityType = s.TraceabilityType, // UI only (no DB)
|
||||
Serial = s.Serial,
|
||||
LocationId = s.LocationId
|
||||
});
|
||||
}
|
||||
// si newQty == 0 y no existía, no hacemos nada
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ExtraInfoModel
|
||||
{
|
||||
public string? Professional { get; set; }
|
||||
public string? Institution { get; set; }
|
||||
public string? Patient { get; set; }
|
||||
public DateTime? SurgeryDate { get; set; }
|
||||
}
|
||||
|
||||
private List<StockSnapshotItem> BuildSnapshotFromDetails()
|
||||
{
|
||||
return Details.Select(d =>
|
||||
@ -350,4 +353,12 @@
|
||||
};
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private class ExtraInfoModel
|
||||
{
|
||||
public string? Professional { get; set; }
|
||||
public string? Institution { get; set; }
|
||||
public string? Patient { get; set; }
|
||||
public DateTime? SurgeryDate { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,123 +0,0 @@
|
||||
@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));
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ namespace phronCare.UIBlazor.Services.Stock.Expeditions
|
||||
{
|
||||
public class ExpeditionService
|
||||
{
|
||||
private readonly IJSRuntime _js;
|
||||
private readonly IJSRuntime _js; //Todavia no se utiliza pero eventualmente para exportaciones seguramente./
|
||||
private readonly HttpClient _http;
|
||||
public ExpeditionService(HttpClient http, IJSRuntime js)
|
||||
{
|
||||
@ -29,6 +29,5 @@ namespace phronCare.UIBlazor.Services.Stock.Expeditions
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -38,7 +38,7 @@ public class StockScanService : IStockScanService
|
||||
Quantity = first.AvailableQty,
|
||||
LocationId = first.LocationId ?? locationId,
|
||||
TraceabilityType = first.TraceabilityType,
|
||||
Serial = first.Serial // si lo devolvés en el DTO de scan
|
||||
Serial = first.Serial ?? string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user