phronCare/phronCare.UIBlazor/Pages/Stock/Expeditions/ExpeditionDetailDrawer.razor
Leandro Hernan Rojas be690849fd
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m27s
Update Expediciones UI
2025-09-05 16:31:58 -03:00

371 lines
14 KiB
Plaintext
Raw Permalink 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.

@using System.Linq
@using System.Text.Json
@using Domain.Dtos.Stock
@inject IToastService Toast
@if (Visible && Expedition is not null)
{
<style>
/* Overlay + panel (bootstrap-like offcanvas) */
.drawer-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,.35);
z-index: 1050;
}
.drawer-panel {
position: fixed;
top: 0;
right: 0;
height: 100vh;
width: 560px; /* ajustable: 480640px suele ir bien */
max-width: 95vw;
z-index: 1051;
overflow: auto;
}
</style>
<div class="drawer-overlay" @onclick="() => Close()"></div>
<div class="drawer-panel card shadow-lg">
<div class="card-header py-2">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-2">
<h5 class="mb-0">
Expedición <span class="fw-semibold">@Expedition.Expeditionnumber</span>
</h5>
@if (!string.IsNullOrWhiteSpace(Expedition.StatusLabel))
{
<span class="badge @GetStatusBadgeClass(Expedition.StatusLabel!)">
@Expedition.StatusLabel
</span>
}
</div>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-warning rounded-pill" title="Descargar"
@onclick="RequestExportPdf">
<i class="fas fa-file-pdf me-1"></i> PDF
</button>
<button class="btn btn-sm btn-danger rounded-pill" title="Cerrar"
@onclick="() => Close()">
<i class="fas fa-times me-1"></i> Cerrar
</button>
</div>
</div>
</div>
<div class="card-body pb-2">
<ul class="nav nav-tabs small" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link @(activeTab == 0 ? "active" : "")"
@onclick="() => SetTab(0)" role="tab">
Datos
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @(activeTab == 1 ? "active" : "")"
@onclick="() => SetTab(1)" role="tab">
Ítems (@(Expedition.Items?.Count ?? 0))
</button>
</li>
</ul>
<div class="tab-content pt-3">
@if (activeTab == 0)
{
<div class="tab-pane fade show active" style="zoom: 0.8;">
<div class="row g-2">
<div class="col-12 col-md-6">
<div class="card border-0 bg-light-subtle">
<div class="card-body py-2">
<div class="small text-muted mb-1">Número</div>
<div class="fw-semibold">@Expedition.Expeditionnumber</div>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card border-0 bg-light-subtle">
<div class="card-body py-2">
<div class="small text-muted mb-1">Fecha</div>
<div class="fw-semibold">@Expedition.Issuedate.ToString("yyyy-MM-dd")</div>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card border-0 bg-light-subtle">
<div class="card-body py-2">
<div class="small text-muted mb-1">Ubicación</div>
<div class="fw-semibold">@Expedition.LocationName</div>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card border-0 bg-light-subtle">
<div class="card-body py-2">
<div class="small text-muted mb-1">Estado</div>
<div class="fw-semibold">
<span class="badge @GetStatusBadgeClass(Expedition.StatusLabel)">@Expedition.StatusLabel</span>
</div>
</div>
</div>
</div>
<!-- Datos del snapshot -->
<div class="col-12 col-md-6">
<div class="card border-0 bg-light-subtle">
<div class="card-body py-2">
<div class="small text-muted mb-1">Paciente</div>
<div class="fw-semibold">@GetPatient(Expedition.ExtrainfoJson)</div>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card border-0 bg-light-subtle">
<div class="card-body py-2">
<div class="small text-muted mb-1">Médico</div>
<div class="fw-semibold">@GetProfessional(Expedition.ExtrainfoJson)</div>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card border-0 bg-light-subtle">
<div class="card-body py-2">
<div class="small text-muted mb-1">Hospital</div>
<div class="fw-semibold">@GetInstitution(Expedition.ExtrainfoJson)</div>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card border-0 bg-light-subtle">
<div class="card-body py-2">
<div class="small text-muted mb-1">Fecha de cirugía</div>
<div class="fw-semibold">@GetSurgeryDateShort(Expedition.ExtrainfoJson)</div>
</div>
</div>
</div>
<div class="col-12">
<label class="form-label mb-1 small text-muted">Observaciones</label>
<div class="form-control form-control-sm" style="min-height: 60px;">@Expedition.Observations</div>
</div>
@if (!string.IsNullOrWhiteSpace(Expedition.ExtrainfoJson))
{
<div class="col-12">
<label class="form-label mb-1 small text-muted">Extra info (snapshot)</label>
<pre class="form-control form-control-sm" style="height: 120px; overflow:auto; white-space: pre-wrap;">@Expedition.ExtrainfoJson</pre>
</div>
}
</div>
</div>
}
else
{
<div class="tab-pane fade show active"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:120px">Código</th>
<th>Producto</th>
<th style="width:80px" class="text-center">Cant.</th>
<th style="width:140px">Lote</th>
<th style="width:160px">Serie</th>
<th style="width:120px">Vence</th>
<th style="width:160px">Ubicación</th>
</tr>
</thead>
<tbody>
@if (Loading && Expedition?.Items is null)
{
<tr>
<td colspan="7" class="text-center py-4">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
Cargando ítems...
</td>
</tr>
}
else if (PagedItems?.Any() == true)
{
@foreach (var it in PagedItems!)
{
<tr>
<td class="fw-semibold">@it.FactoryCode</td>
<td class="text-truncate" style="max-width: 360px">@it.ProductName</td>
<td class="text-center">@it.Quantity</td>
<td>@it.Batch</td>
<td>@it.Serial</td>
<td>@(it.Expiration?.ToString("yyyy-MM-dd"))</td>
<td>@it.LocationName</td>
</tr>
}
}
else
{
<tr><td colspan="7" class="text-center text-muted py-4">Sin ítems</td></tr>
}
</tbody>
</table>
</div>
<div class="d-flex justify-content-between align-items-center px-1 py-2 border-top">
<div class="text-muted small">
@if (TotalItems > 0)
{
<span>Página @itemsPage de @ItemsTotalPages — @TotalItems ítem(s)</span>
}
</div>
<div class="d-flex align-items-center gap-2">
<select class="form-select form-select-sm" style="width:auto"
@bind="itemsPageSize"
@bind:after="OnItemsPageSizeChanged">
<option value="10">10</option>
<option value="20">20</option>
<option value="50">50</option>
</select>
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary rounded-pill"
disabled="@(itemsPage<=1)" @onclick="PrevItemsPage">
<i class="fas fa-chevron-left"></i> Anterior
</button>
<button class="btn btn-sm btn-outline-secondary rounded-pill"
disabled="@(itemsPage>=ItemsTotalPages)" @onclick="NextItemsPage">
Siguiente <i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
</div>
</div>
}
</div>
</div>
</div>
}
@code {
// Parámetros
[Parameter] public ExpeditionDto? Expedition { get; set; }
[Parameter] public bool Visible { get; set; }
[Parameter] public EventCallback<bool> VisibleChanged { get; set; }
[Parameter] public EventCallback<(int Id, string Number)> ExportPdfRequested { get; set; }
[Parameter] public bool Loading { get; set; }
// Estado local
private int activeTab = 0;
private int itemsPage = 1;
private int itemsPageSize = 10;
private int? lastExpeditionId;
protected override void OnParametersSet()
{
if (Expedition?.Id != lastExpeditionId)
{
lastExpeditionId = Expedition?.Id;
itemsPage = 1; // reset de página al cambiar de expedición
activeTab = 0; // opcional: volver a "Datos"
}
}
private void SetTab(int tab) => activeTab = tab;
// Paginación de ítems
private int TotalItems => Expedition?.Items?.Count ?? 0;
private int ItemsTotalPages => TotalItems == 0 ? 1 : (int)Math.Ceiling(TotalItems / (double)itemsPageSize);
private IEnumerable<ExpeditionItemDto> PagedItems =>
Expedition?.Items?
.Skip((itemsPage - 1) * itemsPageSize)
.Take(itemsPageSize) ?? Enumerable.Empty<ExpeditionItemDto>();
private void PrevItemsPage()
{
if (itemsPage <= 1) return;
itemsPage--;
}
private void NextItemsPage()
{
if (itemsPage >= ItemsTotalPages) return;
itemsPage++;
}
private void ChangeItemsPageSize(string? value)
{
if (int.TryParse(value, out var newSize) && newSize > 0)
{
itemsPageSize = newSize;
itemsPage = 1;
}
}
// Acciones
private async Task RequestExportPdf()
{
if (Expedition is null) return;
await ExportPdfRequested.InvokeAsync((Expedition.Id, Expedition.Expeditionnumber));
}
private async Task Close()
{
await VisibleChanged.InvokeAsync(false);
}
// Snapshot helpers (case-insensitive y tolerantes a null)
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("yyyy-MM-dd");
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;
}
// UI helpers
private static string GetStatusBadgeClass(string? status)
{
if (string.IsNullOrWhiteSpace(status)) return "bg-secondary";
return status switch
{
"Emitida" => "bg-secondary",
"En tránsito" => "bg-info",
"En destino" => "bg-primary",
"Retorno" => "bg-warning text-dark",
"Cerrada" => "bg-success",
"Anulada" => "bg-danger",
_ => "bg-secondary"
};
}
private void OnItemsPageSizeChanged()
{
if (itemsPageSize <= 0) itemsPageSize = 10; // por las dudas
itemsPage = 1; // resetea a la primera página
// No hace falta nada más: PagedItems se recalcula y Blazor re-renderiza.
}
}