UpClean de Codigo
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m5s

This commit is contained in:
Leandro Hernan Rojas 2025-08-27 17:42:48 -03:00
parent a38bd4570f
commit 8303751ab7
5 changed files with 67 additions and 210 deletions

View File

@ -1,10 +1,8 @@
@page "/stock/expeditions/create" @page "/stock/expeditions/create"
@using Blazored.Typeahead @using Blazored.Typeahead
@using Domain.Dtos.Stock @using Domain.Dtos.Stock
@using Domain.Entities
@using Services.Lookups @using Services.Lookups
@using Services.Stock.Expeditions @using Services.Stock.Expeditions
@using phronCare.UIBlazor.Pages.Stock.Shared @using phronCare.UIBlazor.Pages.Stock.Shared
@inject NavigationManager Navigation @inject NavigationManager Navigation
@ -44,7 +42,7 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Ticket ID</label> <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>
</div> </div>
@ -118,8 +116,6 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@for (int i = 0; i < 50; i++)
{
@foreach (var item in Details) @foreach (var item in Details)
{ {
<tr> <tr>
@ -137,7 +133,6 @@
</td> </td>
</tr> </tr>
} }
}
</tbody> </tbody>
</table> </table>
</div> </div>
@ -164,11 +159,7 @@
private string DispatchInstruction = string.Empty; private string DispatchInstruction = string.Empty;
private string ticketIdString private string ticketIdString = string.Empty;
{
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) 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) private void MergeSelectionsByBusinessKey(List<StockItemSelectionDto> selected)
{ {
foreach (var s in 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( var key = StockKeys.BuildBusinessKey(
s.ProductId, s.ProductId,
@ -265,9 +274,10 @@
StockKeys.BuildBusinessKey(d.ProductId, d.LocationId, d.Batch ?? string.Empty, d.Expiration, d.Serial ?? string.Empty) == key 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; 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)) if (!string.IsNullOrWhiteSpace(s.Serial))
newQty = 1; newQty = 1;
@ -275,26 +285,26 @@
{ {
if (newQty == 0) 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) if (existing.Quantity > 0)
{ continue;
// ⚠️ Caso snapshot con 0 → ignorar, mantener la fila
// 0 explícito válido (no había cantidad previa): eliminar.
Details.Remove(existing);
continue; continue;
} }
// 0 explícito válido → borrar // SET exacto (no sumar): mantener coherencia con el modal
Details.Remove(existing);
}
else
{
// SET: exactamente lo que vino del modal
existing.Quantity = newQty; existing.Quantity = newQty;
existing.ProductName = s.ProductName;
existing.Batch = s.Batch; // Actualizaciones mínimas necesarias (evitar sobreescrituras innecesarias):
existing.Serial = s.Serial; 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.Expiration = exp;
existing.LocationId = s.LocationId; existing.LocationId = s.LocationId;
existing.TraceabilityType = s.TraceabilityType; existing.TraceabilityType = s.TraceabilityType; // UI only
}
} }
else else
{ {
@ -307,23 +317,16 @@
Quantity = newQty, Quantity = newQty,
Batch = s.Batch, Batch = s.Batch,
Expiration = exp, Expiration = exp,
TraceabilityType = s.TraceabilityType, TraceabilityType = s.TraceabilityType, // UI only (no DB)
Serial = s.Serial, Serial = s.Serial,
LocationId = s.LocationId 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() private List<StockSnapshotItem> BuildSnapshotFromDetails()
{ {
return Details.Select(d => return Details.Select(d =>
@ -350,4 +353,12 @@
}; };
}).ToList(); }).ToList();
} }
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

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

View File

@ -6,7 +6,7 @@ namespace phronCare.UIBlazor.Services.Stock.Expeditions
{ {
public class ExpeditionService public class ExpeditionService
{ {
private readonly IJSRuntime _js; private readonly IJSRuntime _js; //Todavia no se utiliza pero eventualmente para exportaciones seguramente./
private readonly HttpClient _http; private readonly HttpClient _http;
public ExpeditionService(HttpClient http, IJSRuntime js) public ExpeditionService(HttpClient http, IJSRuntime js)
{ {
@ -29,6 +29,5 @@ namespace phronCare.UIBlazor.Services.Stock.Expeditions
return null; return null;
} }
} }
} }
} }

View File

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

View File

@ -38,7 +38,7 @@ public class StockScanService : IStockScanService
Quantity = first.AvailableQty, Quantity = first.AvailableQty,
LocationId = first.LocationId ?? locationId, LocationId = first.LocationId ?? locationId,
TraceabilityType = first.TraceabilityType, TraceabilityType = first.TraceabilityType,
Serial = first.Serial // si lo devolvés en el DTO de scan Serial = first.Serial ?? string.Empty
}; };
} }
} }