Leandro Hernan Rojas 0361d4c978
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m2s
Add Export QUOTES & refactoring
2025-09-11 22:41:46 -03:00

349 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@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 for="number" class="form-label mb-1">Número</label>
<InputText id="number" 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 for="status" class="form-label mb-1">Estado</label>
<InputSelect id="status" 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 for="from" class="form-label mb-1">Fecha desde</label>
<InputDate id="from" 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 for="to" class="form-label mb-1">Fecha hasta</label>
<InputDate id="to" 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 for="location" class="form-label mb-1">Ubicación</label>
<InputNumber id="location" 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 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()))">
<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">
<i class="fas fa-eraser me-1"></i> Limpiar
</button>
<button class="btn btn-success rounded-pill" @onclick="ExportarExcel">
<i class="fas fa-file-excel me-1"></i> Excel
</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 if (IsLoading)
{
<tr><td colspan="9" class="text-center text-muted py-4">Cargando...</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 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);
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()
{
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;
}
}
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;
}
}