407 lines
14 KiB
Plaintext
Raw Normal View History

2025-09-05 16:31:58 -03:00
@page "/expeditions"
@using Domain.Dtos
@using Domain.Dtos.Stock
@using Domain.Generics
@using System.Text.Json
@using phronCare.UIBlazor.Shared.Modals
2025-09-05 16:31:58 -03:00
@using phronCare.UIBlazor.Services.Stock.Expeditions
2025-09-09 23:54:11 -03:00
@inject IExpeditionService expeditionService
2025-09-05 16:31:58 -03:00
@inject NavigationManager Nav
@inject IToastService Toast
@inject IModalService Modal
2025-09-05 16:31:58 -03:00
<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">
2025-09-11 22:41:46 -03:00
<label for="number" class="form-label mb-1">Número</label>
<InputText id="number" class="form-control form-control-sm" @bind-Value="filters.Number" />
2025-09-05 16:31:58 -03:00
</div>
<div class="col-12 col-sm-6 col-md-4 col-lg-3 col-xxl-2">
2025-09-11 22:41:46 -03:00
<label for="status" class="form-label mb-1">Estado</label>
<InputSelect id="status" class="form-select form-select-sm" @bind-Value="filters.Status">
2025-09-05 16:31:58 -03:00
<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">
2025-09-11 22:41:46 -03:00
<label for="from" class="form-label mb-1">Fecha desde</label>
<InputDate id="from" class="form-control form-control-sm" @bind-Value="filters.From" />
2025-09-05 16:31:58 -03:00
</div>
<div class="col-12 col-sm-6 col-md-4 col-lg-3 col-xxl-2">
2025-09-11 22:41:46 -03:00
<label for="to" class="form-label mb-1">Fecha hasta</label>
<InputDate id="to" class="form-control form-control-sm" @bind-Value="filters.To" />
2025-09-05 16:31:58 -03:00
</div>
<div class="col-12 col-sm-6 col-md-4 col-lg-3 col-xxl-2">
2025-09-11 22:41:46 -03:00
<label for="location" class="form-label mb-1">Ubicación</label>
<InputNumber id="location" class="form-control form-control-sm" @bind-Value="filters.LocationId" />
2025-09-05 16:31:58 -03:00
@* 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">
2025-09-11 22:41:46 -03:00
<label for="pageg" class="form-label mb-1">Tam. página</label>
<InputSelect id="pageg"class="form-select form-select-sm" @bind-Value="pageSize" @onchange="@(e => ChangePageSize(e.Value?.ToString()))">
2025-09-05 16:31:58 -03:00
<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-secondary rounded-pill" @onclick="Clear">
2025-09-11 22:41:46 -03:00
<i class="fas fa-eraser me-1"></i> Limpiar
2025-09-05 16:31:58 -03:00
</button>
2025-09-11 22:41:46 -03:00
<button class="btn btn-success rounded-pill" @onclick="ExportarExcel">
<i class="fas fa-file-excel me-1"></i> Excel
</button>
</div>
2025-09-05 16:31:58 -03:00
</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>
@if (CanMarkInTransit(e))
{
<button class="btn btn-outline-warning rounded-pill"
title="Pasar a En tránsito"
@onclick="() => ConfirmMarkInTransitAsync(e)">
<i class="fas fa-truck"></i>
</button>
}
2025-09-05 16:31:58 -03:00
</div>
</td>
</tr>
}
}
2025-09-11 22:41:46 -03:00
else if (IsLoading)
{
<tr><td colspan="9" class="text-center text-muted py-4">Cargando...</td></tr>
}
2025-09-05 16:31:58 -03:00
else
{
2025-09-11 22:41:46 -03:00
<tr><td colspan="9" class="text-center text-muted py-4">Sin resultados</td></tr>
2025-09-05 16:31:58 -03:00
}
</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 {
2025-09-09 23:54:11 -03:00
private LSProductSearchParams SearchParams = new() { Page = 1, PageSize = 10 };
2025-09-11 22:41:46 -03:00
private Filters filters = new();
private PagedResult<ExpeditionDto>? result;
private bool IsLoading;
private int page = 1;
private int pageSize = 10;
private int TotalPages => result is null ? 1 : (int)Math.Ceiling((double)result.TotalItems / result.PageSize);
2025-09-05 16:31:58 -03:00
private ExpeditionDto? selected;
private bool drawerOpen;
private bool loadingDetail;
private async Task OpenDetailAsync(ExpeditionDto dto)
{
// Abro el drawer con lo que ya tengo (encabezado)
loadingDetail = true;
2025-09-05 16:31:58 -03:00
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()
{
2025-09-11 22:41:46 -03:00
try
{
IsLoading = true;
result = await expeditionService.SearchAsync(
expeditionNumber: filters.Number,
status: filters.Status,
issueDateFrom: filters.From,
issueDateTo: filters.To,
locationId: filters.LocationId,
page: page,
pageSize: pageSize);
StateHasChanged();
}
catch (Exception ex)
{
Toast.ShowError(ex.Message);
}
finally
{
IsLoading = false;
}
}
2025-09-05 16:31:58 -03:00
private void Clear()
{
filters = new();
page = 1;
result = null;
}
private void Create()
{
Nav.NavigateTo("/expeditions/create");
}
2025-09-09 23:54:11 -03:00
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.");
// }
2025-09-05 16:31:58 -03:00
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;
}
// private bool CanMarkInTransit(ExpeditionDto expedition)
// {
// if (expedition == null)
// return false;
// if (!string.IsNullOrWhiteSpace(expedition.StatusLabel))
// return string.Equals(expedition.StatusLabel, "Emitida", StringComparison.OrdinalIgnoreCase);
// return expedition.Status == 1;
// }
private bool CanMarkInTransit(ExpeditionDto expedition)
{
return expedition is not null && expedition.Status == 1;
}
private async Task ConfirmMarkInTransitAsync(ExpeditionDto expedition)
{
var parameters = new ModalParameters();
parameters.Add(nameof(ConfirmModal.Title), "Confirmar cambio de estado");
parameters.Add(nameof(ConfirmModal.Message),
$"La expedición '{expedition.Expeditionnumber}' pasará a estado 'En tránsito'. ¿Desea continuar?");
var modal = Modal.Show<ConfirmModal>("Confirmar", parameters);
var resultModal = await modal.Result;
if (resultModal.Cancelled)
return;
try
{
await expeditionService.MarkInTransitAsync(expedition.Id);
Toast.ShowSuccess($"La expedición '{expedition.Expeditionnumber}' pasó a 'En tránsito' correctamente.");
await Search();
if (selected is not null && selected.Id == expedition.Id)
{
selected = await expeditionService.GetDtoByIdAsync(expedition.Id);
StateHasChanged();
}
}
catch (Exception ex)
{
Toast.ShowError(string.IsNullOrWhiteSpace(ex.Message)
? "No se pudo pasar la expedición a 'En tránsito'."
: ex.Message);
}
}
2025-09-05 16:31:58 -03:00
}