All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m50s
260 lines
12 KiB
Plaintext
260 lines
12 KiB
Plaintext
@page "/stock/products"
|
||
@using Blazored.Typeahead
|
||
@using phronCare.UIBlazor.Services.Stock
|
||
@using phronCare.UIBlazor.Services.Lookups
|
||
@using Domain.Entities
|
||
@using Domain.Generics
|
||
@inject IToastService toastService
|
||
@inject NavigationManager Navigation
|
||
@inject LSProductService productService
|
||
@inject IStockLookUpService lookUpService
|
||
|
||
<div class="card" style="zoom:80%">
|
||
<div class="card-header d-flex justify-content-center align-items-center" style="zoom:80%;">
|
||
<h3 class="card-title m-0">Catálogo de Productos Médicos</h3>
|
||
</div>
|
||
<div class="card-body px-4">
|
||
<!-- FILTROS -->
|
||
<div class="mb-3 row g-2 align-items-end">
|
||
<div class="col-md-1">
|
||
<label for="code">Código</label>
|
||
<input id="code" class="form-control form-control-sm" style="height: 38px;" placeholder="Código interno o externo" @bind="SearchParams.Code" />
|
||
</div>
|
||
<div class="col-sm">
|
||
<label for="description">Nombre o descripción</label>
|
||
<input id="description" class="form-control form-control-sm" style="height: 38px;" placeholder="Nombre o descripción" @bind="SearchParams.Description" />
|
||
</div>
|
||
<div class="col-sm">
|
||
<label for="division">División</label>
|
||
<BlazoredTypeahead id="division" TItem="ELookUpItem" TValue="ELookUpItem"
|
||
SearchMethod="@(filter => lookUpService.GetProductDivisionsAsync(filter).ContinueWith(t => t.Result.AsEnumerable()))"
|
||
Value="_selectedDivision" ValueChanged="OnDivisionSelected"
|
||
ValueExpression="@(() => _selectedDivision)"
|
||
Placeholder="Buscar división..." TextProperty="Nombre" MaximumSuggestions="5"
|
||
class="form-control form-control-sm" style="height: 38px;">
|
||
<ResultTemplate Context="item">@item.Nombre</ResultTemplate>
|
||
<SelectedTemplate Context="item">@item.Nombre</SelectedTemplate>
|
||
</BlazoredTypeahead>
|
||
</div>
|
||
<div class="col-sm-2">
|
||
<label for="unitmeasure">Unidad</label>
|
||
<BlazoredTypeahead id="unitmeasure" TItem="ELookUpItem" TValue="ELookUpItem"
|
||
SearchMethod="@(filter => lookUpService.GetUnitsOfMeasureAsync(filter).ContinueWith(t => t.Result.AsEnumerable()))"
|
||
Value="_selectedUnit" ValueChanged="OnUnitSelected"
|
||
ValueExpression="@(() => _selectedUnit)"
|
||
Placeholder="Buscar unidad..." TextProperty="Nombre" MaximumSuggestions="5"
|
||
class="form-control form-control-sm" style="height: 38px;">
|
||
<ResultTemplate Context="item">@item.Nombre</ResultTemplate>
|
||
<SelectedTemplate Context="item">@item.Nombre</SelectedTemplate>
|
||
</BlazoredTypeahead>
|
||
</div>
|
||
<div class="col-sm-2">
|
||
<label for="type">Tipo</label>
|
||
|
||
<InputSelect id="type" class="form-select form-control-sm" style="height: 38px;" @bind-Value="SearchParams.ProductType">
|
||
<option value="">Tipo</option>
|
||
<option value="1">Implantable</option>
|
||
<option value="2">Instrumental</option>
|
||
<option value="3">Inyectable</option>
|
||
</InputSelect>
|
||
</div>
|
||
<div class="col-sm">
|
||
<label for="traceability">División</label>
|
||
|
||
<InputSelect id="traceability" class="form-select form-control-sm" style="height: 38px;" @bind-Value="SearchParams.TraceabilityType">
|
||
<option value="">Trazabilidad</option>
|
||
<option value="1">No aplica</option>
|
||
<option value="2">Por cantidad</option>
|
||
<option value="3">Por lote y vencimiento</option>
|
||
</InputSelect>
|
||
</div>
|
||
<div class="col-sm">
|
||
<div class="form-check form-switch">
|
||
<input type="checkbox" class="form-check-input me-2" id="plusProcess" @bind="SearchParams.PlusProcess" />
|
||
<label class="form-check-label" for="plusProcess">Esterilizable</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- BOONES DE BUSQUEDA -->
|
||
<div class="mb-3 d-flex justify-content-end gap-2">
|
||
<button class="btn btn-primary rounded-pill" @onclick="Buscar">
|
||
<i class="fas fa-binoculars me-1"></i> Buscar
|
||
</button>
|
||
<button class="btn btn-success rounded-pill" @onclick="NuevoProducto">
|
||
<i class="fas fa-plus me-1"></i> Nuevo
|
||
</button>
|
||
<button class="btn btn-warning rounded-pill" @onclick="ImportarProductos">
|
||
<i class="fas fa-file-upload me-1"></i> Importar
|
||
</button>
|
||
<button class="btn btn-success rounded-pill" @onclick="ExportarExcel">
|
||
<i class="fas fa-file-excel me-1"></i> Excel
|
||
</button>
|
||
<button class="btn btn-secondary rounded-pill" @onclick="Cancelar">
|
||
<i class="fas fa-arrow-left me-1"></i> Volver
|
||
</button>
|
||
</div>
|
||
<hr />
|
||
<div style="zoom:90%;">
|
||
@if (TablaProductos != null && TablaProductos.Any())
|
||
{
|
||
<PhTable Columns="TableColumns"
|
||
Data="TablaProductos"
|
||
SelectionField="Id"
|
||
RowsPerPage="SearchParams.PageSize"
|
||
RenderButtons="true"
|
||
Buttons="botones"
|
||
ShowPageButtons="false"
|
||
ShowQuickSearch="false"
|
||
RenderSelect="false" />
|
||
}
|
||
else
|
||
{
|
||
<p>No hay resultados.</p>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card-footer d-flex justify-content-center align-items-end" style="zoom:80%;">
|
||
<div class="d-flex align-items-center gap-3">
|
||
<button class="btn btn-secondary rounded-pill" @onclick="PrimeraPagina" disabled="@(SearchParams.Page == 1)">« Primera</button>
|
||
<button class="btn btn-secondary rounded-pill" @onclick="AnteriorPagina" disabled="@(!PuedeRetroceder)">‹ Anterior</button>
|
||
<span class="mx-2">Página <strong>@SearchParams.Page</strong> de <strong>@TotalPaginas</strong></span>
|
||
<button class="btn btn-secondary rounded-pill" @onclick="SiguientePagina" disabled="@(!PuedeAvanzar)">Siguiente ›</button>
|
||
<button class="btn btn-secondary rounded-pill" @onclick="UltimaPagina" disabled="@(SearchParams.Page == TotalPaginas)">Última »</button>
|
||
<input type="number" class="form-control form-control-sm rounded ms-3" style="width: 80px;" min="1" max="@TotalPaginas" @bind="PaginaDeseada" />
|
||
<button class="btn btn-outline-primary btn-sm rounded-pill ms-2" @onclick="IrAPagina">Ir</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@code {
|
||
private LSProductSearchParams SearchParams = new() { Page = 1, PageSize = 10 };
|
||
private List<Dictionary<string, object>> TablaProductos = new();
|
||
private PagedResult<ELSProduct>? PagedResult;
|
||
private int PaginaDeseada = 1;
|
||
private ELookUpItem? _selectedDivision;
|
||
private ELookUpItem? _selectedUnit;
|
||
List<PhTable.ButtonOptions> botones = new();
|
||
|
||
private List<string> TableColumns = new()
|
||
{
|
||
"Id", "Código Fábrica", "Código Externo", "Nombre", "Descripción", "División", "Unidad", "Tipo", "Trazabilidad", "Esteriliza"
|
||
};
|
||
|
||
protected override async Task OnInitializedAsync()
|
||
{
|
||
botones = new List<PhTable.ButtonOptions>
|
||
{
|
||
new PhTable.ButtonOptions
|
||
{
|
||
Caption = "<i class='fas fa-pen'></i>",
|
||
ElementClass = "btn btn-success btn-sm",
|
||
UrlAction = "/stock/productform/",
|
||
OnClickAction = async (id) =>
|
||
{
|
||
if (int.TryParse(id, out var pid))
|
||
Navigation.NavigateTo($"/stock/productform/{pid}");
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
private async Task Buscar()
|
||
{
|
||
SearchParams.PageSize = 13;
|
||
SearchParams.Page = 1;
|
||
await CargarPaginaActual();
|
||
}
|
||
|
||
private async Task CargarPaginaActual()
|
||
{
|
||
SearchParams.DivisionId = _selectedDivision?.Id;
|
||
SearchParams.UnitId = _selectedUnit?.Id;
|
||
PagedResult = await productService.SearchAsync(SearchParams);
|
||
TablaProductos = PagedResult?.Items.Select(p => new Dictionary<string, object>
|
||
{
|
||
{ "Id", p.Id },
|
||
{ "Código Fábrica", p.FactoryCode },
|
||
{ "Código Externo", p.ExternalCode?? string.Empty },
|
||
{ "Nombre", p.Name?? string.Empty },
|
||
{ "Descripción", p.Descripcion },
|
||
{ "División", p.Division?.Name ?? "" },
|
||
{ "Unidad", p.Unit?.Name ?? "" },
|
||
{ "Tipo", ObtenerTipoProducto(p.ProductType) },
|
||
{ "Trazabilidad", ObtenerTipoTrazabilidad(p.TraceabilityType) },
|
||
{ "Esteriliza", p.PlusProcess ? "Sí" : "No" }
|
||
}).ToList() ?? [];
|
||
}
|
||
|
||
private void OnDivisionSelected(ELookUpItem item) => _selectedDivision = item;
|
||
private void OnUnitSelected(ELookUpItem item) => _selectedUnit = item;
|
||
|
||
private string ObtenerTipoProducto(int? tipo) => tipo switch
|
||
{
|
||
1 => "Implantable",
|
||
2 => "Instrumental",
|
||
3 => "Inyectable",
|
||
_ => ""
|
||
};
|
||
|
||
private string ObtenerTipoTrazabilidad(int? tipo) => tipo switch
|
||
{
|
||
1 => "No aplica",
|
||
2 => "Por cantidad",
|
||
3 => "Por lote/vencimiento",
|
||
_ => ""
|
||
};
|
||
|
||
private async Task PrimeraPagina() { SearchParams.Page = 1; await CargarPaginaActual(); }
|
||
private async Task UltimaPagina() { SearchParams.Page = TotalPaginas; await CargarPaginaActual(); }
|
||
private async Task SiguientePagina() => await CambiarPagina(1);
|
||
private async Task AnteriorPagina() => await CambiarPagina(-1);
|
||
private async Task CambiarPagina(int delta)
|
||
{
|
||
var nueva = SearchParams.Page + delta;
|
||
if (nueva >= 1 && nueva <= TotalPaginas)
|
||
{
|
||
SearchParams.Page = nueva;
|
||
await CargarPaginaActual();
|
||
}
|
||
}
|
||
|
||
private async Task IrAPagina()
|
||
{
|
||
if (PaginaDeseada >= 1 && PaginaDeseada <= TotalPaginas)
|
||
{
|
||
SearchParams.Page = PaginaDeseada;
|
||
await CargarPaginaActual();
|
||
}
|
||
else
|
||
{
|
||
toastService.ShowWarning("Página fuera de rango.");
|
||
}
|
||
}
|
||
|
||
private async Task ExportarExcel()
|
||
{
|
||
SearchParams.Page = 1;
|
||
SearchParams.PageSize = int.MaxValue; // Exportar todos los resultados
|
||
try
|
||
{
|
||
await productService.ExportFilteredAsync(SearchParams);
|
||
toastService.ShowSuccess("Exportación completada.");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
toastService.ShowError($"Error: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void NuevoProducto() => Navigation.NavigateTo("/stock/productform");
|
||
|
||
private void ImportarProductos() => Navigation.NavigateTo("/stock/productimport");
|
||
|
||
private void Cancelar() => Navigation.NavigateTo("/DashboardPanel");
|
||
|
||
private int TotalPaginas => PagedResult is null ? 1 : (int)Math.Ceiling(PagedResult.TotalItems / (double)SearchParams.PageSize);
|
||
private bool PuedeRetroceder => SearchParams.Page > 1;
|
||
private bool PuedeAvanzar => PagedResult != null && SearchParams.Page < TotalPaginas;
|
||
}
|