Leandro Hernan Rojas 345bc817c4
Some checks failed
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Failing after 1m2s
Arreglo Fix1 SelectorScam
2025-08-24 02:31:12 -03:00

386 lines
14 KiB
Plaintext

@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
@inject ExpeditionService expeditionService
@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 ELookUpItem? SelectedQuote;
private List<ELSExpeditionDetail> Details = new();
private List<ProductSetItemDto> ProductSetItems = new();
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 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,
// 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);
parameters.Add(nameof(StockItemSelectorModal.Snapshot), BuildSnapshotFromDetails()); // ← clave
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)
{
MergeSelectionsByBusinessKey(selectedItems); // ← usar merge (ver abajo)
StateHasChanged();
toastService.ShowSuccess($"{selectedItems.Count} item(s) agregados/actualizados.");
}
}
private void MergeSelectionsByBusinessKey(List<StockItemSelectionDto> selected)
{
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,
ProductName = s.ProductName,
Quantity = newQty,
Batch = s.Batch,
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
}
}
}
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 =>
{
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();
}
}