All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m27s
371 lines
14 KiB
Plaintext
371 lines
14 KiB
Plaintext
@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: 480–640px 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.
|
||
}
|
||
}
|
||
|
||
|