All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m30s
331 lines
11 KiB
Plaintext
331 lines
11 KiB
Plaintext
@page "/expeditions"
|
||
@using Domain.Dtos
|
||
@using Domain.Dtos.Stock
|
||
@using Domain.Generics
|
||
@using System.Text.Json
|
||
|
||
@using phronCare.UIBlazor.Services.Stock.Expeditions
|
||
@inject IExpeditionService expeditionService
|
||
|
||
@inject NavigationManager Nav
|
||
@inject IToastService Toast
|
||
|
||
<div class="card shadow-sm mb-3" style="zoom: 0.8;">
|
||
<div class="card-header py-2">
|
||
<div class="d-flex justify-content-center align-items-center">
|
||
<h3 class="card-title m-0">Consulta de Expediciones</h3>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card-body">
|
||
<EditForm Model="@filters" OnValidSubmit="@Search">
|
||
<div class="row g-2 align-items-end">
|
||
<!-- En monitores grandes queda todo en una fila (col-xxl-2 = 6 columnas por fila) -->
|
||
<div class="col-12 col-sm-6 col-md-4 col-lg-3 col-xxl-2">
|
||
<label class="form-label mb-1">Número</label>
|
||
<InputText class="form-control form-control-sm" @bind-Value="filters.Number" />
|
||
</div>
|
||
|
||
<div class="col-12 col-sm-6 col-md-4 col-lg-3 col-xxl-2">
|
||
<label class="form-label mb-1">Estado</label>
|
||
<InputSelect class="form-select form-select-sm" @bind-Value="filters.Status">
|
||
<option value="">(Todos)</option>
|
||
<option value="Emitida">Emitida</option>
|
||
<option value="EnTransito">En tránsito</option>
|
||
<option value="EnDestino">En destino</option>
|
||
<option value="Retorno">Retorno</option>
|
||
<option value="Cerrada">Cerrada</option>
|
||
<option value="Anulada">Anulada</option>
|
||
</InputSelect>
|
||
</div>
|
||
|
||
<div class="col-12 col-sm-6 col-md-4 col-lg-3 col-xxl-2">
|
||
<label class="form-label mb-1">Fecha desde</label>
|
||
<InputDate class="form-control form-control-sm" @bind-Value="filters.From" />
|
||
</div>
|
||
|
||
<div class="col-12 col-sm-6 col-md-4 col-lg-3 col-xxl-2">
|
||
<label class="form-label mb-1">Fecha hasta</label>
|
||
<InputDate class="form-control form-control-sm" @bind-Value="filters.To" />
|
||
</div>
|
||
|
||
<div class="col-12 col-sm-6 col-md-4 col-lg-3 col-xxl-2">
|
||
<label class="form-label mb-1">Ubicación</label>
|
||
<InputNumber class="form-control form-control-sm" @bind-Value="filters.LocationId" />
|
||
@* TODO: reemplazar por BlazoredTypeahead cuando conectes lookup de ubicaciones *@
|
||
</div>
|
||
|
||
<div class="col-12 col-sm-6 col-md-4 col-lg-3 col-xxl-2">
|
||
<label class="form-label mb-1">Tam. página</label>
|
||
<InputSelect class="form-select form-select-sm" @bind-Value="pageSize" @onchange="@(e => ChangePageSize(e.Value?.ToString()))">
|
||
<option value="10">10</option>
|
||
<option value="20">20</option>
|
||
<option value="50">50</option>
|
||
</InputSelect>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="d-flex justify-content-end gap-2 mt-3">
|
||
<button class="btn btn-primary rounded-pill">
|
||
<i class="fas fa-search me-1"></i> Buscar
|
||
</button>
|
||
<button class="btn btn-success rounded-pill" @onclick="Create">
|
||
<i class="fas fa-plus me-1"></i> Nuevo
|
||
</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="Clear">
|
||
<i class="fas fa-arrow-left me-1"></i> Volver
|
||
</button>
|
||
</div>
|
||
</EditForm>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card shadow-sm" style="zoom:0.8;">
|
||
<div class="table-responsive">
|
||
<table class="table table-sm align-middle mb-0">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th style="width:160px">Nº</th>
|
||
<th style="width:120px">Fecha</th>
|
||
<th style="width:140px">Estado</th>
|
||
<th style="width:200px">Referencia Ext.</th>
|
||
<th style="width:220px">Paciente</th>
|
||
<th style="width:220px">Médico</th>
|
||
<th style="width:220px">Hospital</th>
|
||
<th>Observaciones</th>
|
||
<th style="width:180px" class="text-end">Acciones</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@if (result?.Items?.Any() == true)
|
||
{
|
||
@foreach (var e in result!.Items!)
|
||
{
|
||
<tr>
|
||
<td class="fw-semibold">@e.Expeditionnumber</td>
|
||
<td>@e.Issuedate.ToString("yyyy-MM-dd")</td>
|
||
<td>@e.Status</td>
|
||
<td>@e.ExternalReference</td>
|
||
<td class="text-truncate" style="max-width: 220px">@GetPatient(e.ExtrainfoJson)</td>
|
||
<td class="text-truncate" style="max-width: 220px">@GetProfessional(e.ExtrainfoJson)</td>
|
||
<td class="text-truncate" style="max-width: 220px">@GetInstitution(e.ExtrainfoJson)</td>
|
||
<td class="text-truncate" style="max-width: 420px">@e.Observations</td>
|
||
<td class="text-end">
|
||
<div class="btn-group btn-group-sm">
|
||
<button class="btn btn-outline-primary rounded-pill" title="PDF" @onclick="() => ViewPdf(e.Id, e.Expeditionnumber)">
|
||
<i class="fas fa-file-pdf"></i>
|
||
</button>
|
||
<button class="btn btn-outline-secondary rounded-pill" title="Ver" @onclick="() => OpenDetailAsync(e)">
|
||
<i class="fas fa-eye"></i>
|
||
</button>
|
||
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
}
|
||
}
|
||
else
|
||
{
|
||
<tr><td colspan="9" class="text-center text-muted py-4">Sin resultados</td></tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Paginación debajo -->
|
||
<div class="d-flex justify-content-between align-items-center px-3 py-2 border-top">
|
||
<div class="small text-muted">
|
||
@if (result is not null && result.TotalItems > 0)
|
||
{
|
||
var from = ((page - 1) * pageSize) + 1;
|
||
var to = Math.Min(page * pageSize, result.TotalItems);
|
||
<text>Mostrando @from–@to de @result.TotalItems</text>
|
||
}
|
||
</div>
|
||
<div class="btn-group">
|
||
<button class="btn btn-outline-secondary btn-sm rounded-pill"
|
||
disabled="@(page<=1)" @onclick="PrevPage">
|
||
<i class="fas fa-chevron-left me-1"></i> Anterior
|
||
</button>
|
||
<button class="btn btn-outline-secondary btn-sm rounded-pill"
|
||
disabled="@(result is null || page>=TotalPages)" @onclick="NextPage">
|
||
Siguiente <i class="fas fa-chevron-right ms-1"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Drawer de detalle -->
|
||
<ExpeditionDetailDrawer Expedition="@selected"
|
||
Visible="@drawerOpen"
|
||
VisibleChanged="@(v => drawerOpen = v)"
|
||
ExportPdfRequested="OnExportPdfRequested"
|
||
Loading="@loadingDetail" />
|
||
|
||
@code {
|
||
private LSProductSearchParams SearchParams = new() { Page = 1, PageSize = 10 };
|
||
|
||
private Filters filters = new();
|
||
private PagedResult<ExpeditionDto>? result;
|
||
private int page = 1;
|
||
private int pageSize = 10;
|
||
private int TotalPages => result is null ? 1 : (int)Math.Ceiling((double)result.TotalItems / result.PageSize);
|
||
|
||
private ExpeditionDto? selected;
|
||
private bool drawerOpen;
|
||
private bool loadingDetail;
|
||
|
||
private async Task OpenDetailAsync(ExpeditionDto dto)
|
||
{
|
||
// Abro el drawer con lo que ya tengo (encabezado)
|
||
drawerOpen = true;
|
||
selected = dto;
|
||
StateHasChanged();
|
||
|
||
// Traigo el DTO completo (incluye Items)
|
||
var full = await expeditionService.GetDtoByIdAsync(dto.Id);
|
||
if (full is not null)
|
||
{
|
||
selected = full; // ahora sí con Items
|
||
}
|
||
else
|
||
{
|
||
Toast.ShowError("No se pudo cargar el detalle de la expedición.");
|
||
}
|
||
|
||
// opcional: si querés mostrar un spinner en el drawer mientras carga
|
||
loadingDetail = false;
|
||
StateHasChanged();
|
||
}
|
||
|
||
private async Task Search()
|
||
{
|
||
result = await expeditionService.SearchAsync(
|
||
expeditionNumber: filters.Number,
|
||
status: filters.Status,
|
||
issueDateFrom: filters.From,
|
||
issueDateTo: filters.To,
|
||
locationId: filters.LocationId,
|
||
page: page,
|
||
pageSize: pageSize);
|
||
StateHasChanged();
|
||
}
|
||
|
||
private void Clear()
|
||
{
|
||
filters = new();
|
||
page = 1;
|
||
result = null;
|
||
}
|
||
|
||
private void Create()
|
||
{
|
||
Nav.NavigateTo("/expeditions/create");
|
||
}
|
||
|
||
private async Task ExportarExcel()
|
||
{
|
||
SearchParams.Page = 1;
|
||
SearchParams.PageSize = int.MaxValue; // Exportar todos los resultados
|
||
try
|
||
{
|
||
await expeditionService.ExportFilteredAsync(SearchParams);
|
||
Toast.ShowSuccess("Exportación completada.");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Toast.ShowError($"Error: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// private async Task ExportCurrent()
|
||
// {
|
||
// // Opcional: /api/lsm/expeditions/export?{filtros}
|
||
// Toast.ShowInfo("Export en preparación.");
|
||
// }
|
||
|
||
private async Task ViewPdf(int id, string number)
|
||
{
|
||
await expeditionService.ExportPdfAsync(id, number);
|
||
}
|
||
|
||
private void OpenDetail(ExpeditionDto dto)
|
||
{
|
||
selected = dto;
|
||
drawerOpen = true;
|
||
}
|
||
|
||
private async Task OnExportPdfRequested((int Id, string Number) payload)
|
||
{
|
||
await expeditionService.ExportPdfAsync(payload.Id, payload.Number);
|
||
}
|
||
|
||
private async Task PrevPage()
|
||
{
|
||
if (page <= 1) return;
|
||
page--;
|
||
await Search();
|
||
}
|
||
|
||
private async Task NextPage()
|
||
{
|
||
if (result is null || page >= TotalPages) return;
|
||
page++;
|
||
await Search();
|
||
}
|
||
|
||
private void ChangePageSize(string? value)
|
||
{
|
||
if (int.TryParse(value, out var newSize) && newSize > 0)
|
||
{
|
||
pageSize = newSize;
|
||
page = 1;
|
||
_ = Search();
|
||
}
|
||
}
|
||
|
||
public class Filters
|
||
{
|
||
public string? Number { get; set; }
|
||
public string? Status { get; set; }
|
||
public DateTime? From { get; set; }
|
||
public DateTime? To { get; set; }
|
||
public int? LocationId { get; set; }
|
||
}
|
||
private static string? GetProfessional(string? json) => TryGetString(json, "Professional");
|
||
private static string? GetInstitution(string? json) => TryGetString(json, "Institution");
|
||
private static string? GetPatient(string? json) => TryGetString(json, "Patient");
|
||
private static string? GetSurgeryDateShort(string? json)
|
||
=> TryGetDate(json, "SurgeryDate")?.ToString("dd/MM/yyyy");
|
||
|
||
private static string? TryGetString(string? json, string propName)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(json)) return null;
|
||
try
|
||
{
|
||
using var doc = JsonDocument.Parse(json);
|
||
var root = doc.RootElement;
|
||
if (root.ValueKind != JsonValueKind.Object) return null;
|
||
|
||
foreach (var p in root.EnumerateObject())
|
||
if (string.Equals(p.Name, propName, StringComparison.OrdinalIgnoreCase)
|
||
&& p.Value.ValueKind == JsonValueKind.String)
|
||
return p.Value.GetString();
|
||
|
||
return null;
|
||
}
|
||
catch { return null; }
|
||
}
|
||
|
||
private static DateTime? TryGetDate(string? json, string propName)
|
||
{
|
||
var s = TryGetString(json, propName);
|
||
if (string.IsNullOrWhiteSpace(s)) return null;
|
||
if (DateTime.TryParse(s, out var dt)) return dt;
|
||
return null;
|
||
}
|
||
}
|