Update Expediciones UI
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m27s
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m27s
This commit is contained in:
parent
6e61b7b598
commit
be690849fd
@ -1,12 +1,27 @@
|
|||||||
using Domain.Dtos.Stock;
|
using Domain.Dtos.Stock;
|
||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
|
using Domain.Generics;
|
||||||
|
|
||||||
namespace Core.Interfaces.Stock
|
namespace Core.Interfaces.Stock
|
||||||
{
|
{
|
||||||
// 1.2 Domain (Core)
|
// 1.2 Domain (Core)
|
||||||
public interface IExpeditionDom
|
public interface IExpeditionDom
|
||||||
{
|
{
|
||||||
Task<(int Id, string ExpeditionNumber)> CreateAndIssueAsync(ELSExpeditionHeader header, IEnumerable<ELSExpeditionDetail> details, int formSeriesId);
|
Task<PagedResult<ExpeditionDto>> SearchAsync(
|
||||||
|
string? expeditionNumber,
|
||||||
|
string? status,
|
||||||
|
DateTime? issueDateFrom,
|
||||||
|
DateTime? issueDateTo,
|
||||||
|
int? locationId,
|
||||||
|
int page,
|
||||||
|
int pageSize);
|
||||||
|
|
||||||
Task<ExpeditionDto?> GetDtoByIdAsync(int id);
|
Task<ExpeditionDto?> GetDtoByIdAsync(int id);
|
||||||
|
Task<ExpeditionDto?> GetDtoByExpeditionNumberAsync(string expeditionNumber);
|
||||||
|
|
||||||
|
Task<(int Id, string ExpeditionNumber)> CreateAndIssueAsync(
|
||||||
|
ELSExpeditionHeader header,
|
||||||
|
IEnumerable<ELSExpeditionDetail> details,
|
||||||
|
int formSeriesId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using Core.Interfaces.Stock;
|
using Core.Interfaces.Stock;
|
||||||
using Domain.Dtos.Stock;
|
using Domain.Dtos.Stock;
|
||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
|
using Domain.Generics;
|
||||||
using Models.Interfaces;
|
using Models.Interfaces;
|
||||||
|
|
||||||
namespace Core.Services.Stock
|
namespace Core.Services.Stock
|
||||||
@ -28,10 +29,23 @@ namespace Core.Services.Stock
|
|||||||
|
|
||||||
return await _repo.CreateFullExpeditionAsync(header, formSeriesId);
|
return await _repo.CreateFullExpeditionAsync(header, formSeriesId);
|
||||||
}
|
}
|
||||||
#endregion
|
|
||||||
public async Task<ExpeditionDto?> GetDtoByIdAsync(int id)
|
public Task<ExpeditionDto?> GetDtoByExpeditionNumberAsync(string expeditionNumber)
|
||||||
{
|
{
|
||||||
return await _repo.GetDtoByIdAsync(id);
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public Task<PagedResult<ExpeditionDto>> SearchAsync(
|
||||||
|
string? expeditionNumber,
|
||||||
|
string? status,
|
||||||
|
DateTime? issueDateFrom,
|
||||||
|
DateTime? issueDateTo,
|
||||||
|
int? locationId,
|
||||||
|
int page,
|
||||||
|
int pageSize)
|
||||||
|
=> _repo.SearchAsync(expeditionNumber, status, issueDateFrom, issueDateTo, locationId, page, pageSize);
|
||||||
|
public Task<ExpeditionDto?> GetDtoByIdAsync(int id)
|
||||||
|
=> _repo.GetDtoByIdAsync(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using Domain.Dtos.Stock;
|
using Domain.Dtos.Stock;
|
||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
|
using Domain.Generics;
|
||||||
|
|
||||||
namespace Models.Interfaces
|
namespace Models.Interfaces
|
||||||
{
|
{
|
||||||
@ -11,6 +12,7 @@ namespace Models.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task<(int Id, string Expeditionnumber)> CreateFullExpeditionAsync(ELSExpeditionHeader expedition, int formSeriesId);
|
Task<(int Id, string Expeditionnumber)> CreateFullExpeditionAsync(ELSExpeditionHeader expedition, int formSeriesId);
|
||||||
Task<ExpeditionDto?> GetDtoByIdAsync(int id);
|
Task<ExpeditionDto?> GetDtoByIdAsync(int id);
|
||||||
|
Task<PagedResult<ExpeditionDto>> SearchAsync(string? expeditionNumber, string? status, DateTime? issueDateFrom, DateTime? issueDateTo, int? locationId, int page, int pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
using Domain.Constants;
|
using Domain.Constants;
|
||||||
using Domain.Dtos.Stock;
|
using Domain.Dtos.Stock;
|
||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
|
using Domain.Generics;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Models.Helpers;
|
using Models.Helpers;
|
||||||
using Models.Interfaces;
|
using Models.Interfaces;
|
||||||
using Models.Models;
|
using Models.Models;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace Models.Repositories.Stock
|
namespace Models.Repositories.Stock
|
||||||
{
|
{
|
||||||
@ -177,6 +180,166 @@ namespace Models.Repositories.Stock
|
|||||||
6 => "Anulada",
|
6 => "Anulada",
|
||||||
_ => $"Desconocido ({status})"
|
_ => $"Desconocido ({status})"
|
||||||
};
|
};
|
||||||
|
// ===========================
|
||||||
|
// BÚSQUEDA + PAGINACIÓN
|
||||||
|
// ===========================
|
||||||
|
public async Task<PagedResult<ExpeditionDto>> SearchAsync(
|
||||||
|
string? expeditionNumber,
|
||||||
|
string? status,
|
||||||
|
DateTime? issueDateFrom,
|
||||||
|
DateTime? issueDateTo,
|
||||||
|
int? locationId,
|
||||||
|
int page,
|
||||||
|
int pageSize)
|
||||||
|
{
|
||||||
|
if (page <= 0) page = 1;
|
||||||
|
if (pageSize <= 0) pageSize = 10;
|
||||||
|
|
||||||
|
// NOTE: ajustá el DbSet si tu entidad se llama distinto
|
||||||
|
var q = _context.PhLsmExpeditionHeaders
|
||||||
|
.AsNoTracking()
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
// Número
|
||||||
|
if (!string.IsNullOrWhiteSpace(expeditionNumber))
|
||||||
|
{
|
||||||
|
var num = expeditionNumber.Trim();
|
||||||
|
q = q.Where(h => EF.Functions.Like(h.Expeditionnumber!, $"%{num}%"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estado (acepta etiqueta o número)
|
||||||
|
if (!string.IsNullOrWhiteSpace(status))
|
||||||
|
{
|
||||||
|
var st = MapStatusLabelToInt(status);
|
||||||
|
if (st.HasValue) q = q.Where(h => h.Status == st.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fechas (inclusivo)
|
||||||
|
if (issueDateFrom.HasValue)
|
||||||
|
{
|
||||||
|
var from = issueDateFrom.Value.Date;
|
||||||
|
q = q.Where(h => h.Issuedate >= from);
|
||||||
|
}
|
||||||
|
if (issueDateTo.HasValue)
|
||||||
|
{
|
||||||
|
var toExclusive = issueDateTo.Value.Date.AddDays(1);
|
||||||
|
q = q.Where(h => h.Issuedate < toExclusive);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ubicación (si la cabecera no la tiene, filtramos por detalles)
|
||||||
|
if (locationId.HasValue)
|
||||||
|
{
|
||||||
|
q = q.Where(h => h.PhLsmExpeditionDetails.Any(d => d.LocationId == locationId.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
var total = await q.CountAsync();
|
||||||
|
|
||||||
|
// Página (más recientes primero)
|
||||||
|
var headers = await q
|
||||||
|
.OrderByDescending(h => h.Issuedate)
|
||||||
|
.ThenByDescending(h => h.Id)
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.Select(h => new ExpeditionDto
|
||||||
|
{
|
||||||
|
Id = h.Id,
|
||||||
|
Expeditionnumber = h.Expeditionnumber!,
|
||||||
|
Issuedate = h.Issuedate, // no-nullable
|
||||||
|
Status = h.Status,
|
||||||
|
StatusLabel = MapStatus(h.Status),
|
||||||
|
ExternalReference=h.ExternalReference,
|
||||||
|
ExtrainfoJson = h.ExtrainfoJson,
|
||||||
|
Observations = h.Observations,
|
||||||
|
Printcount = h.Printcount, // ← ajustá si tu entidad usa otro nombre
|
||||||
|
Createdat = h.Createdat, // ← idem
|
||||||
|
Modifiedat = h.Modifiedat, // ← idem
|
||||||
|
LocationId = 0, // por defecto (se resuelve si todos los ítems comparten)
|
||||||
|
LocationName = null
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (headers.Count > 0)
|
||||||
|
{
|
||||||
|
var headerIds = headers.Select(x => x.Id).ToList();
|
||||||
|
|
||||||
|
// Distintas ubicaciones por header (en base a detalles)
|
||||||
|
var locsPerHeader = await _context.PhLsmExpeditionDetails
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(d => headerIds.Contains(d.ExpeditionId)) // ← ajustá si es HeaderId
|
||||||
|
.GroupBy(d => d.ExpeditionId)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
HeaderId = g.Key,
|
||||||
|
DistinctLocs = g.Select(x => x.LocationId).Distinct().ToList()
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Diccionario de nombres de ubicación
|
||||||
|
//var allLocIds = locsPerHeader.SelectMany(x => x.DistinctLocs).Distinct().ToList();
|
||||||
|
//var locNames = await _context.PhLsmStockLocations
|
||||||
|
// .AsNoTracking()
|
||||||
|
// .Where(l => allLocIds.Contains(l.Id))
|
||||||
|
// .Select(l => new { l.Id, l.Name })
|
||||||
|
// .ToDictionaryAsync(x => x.Id, x => x.Name);
|
||||||
|
|
||||||
|
//// Asignar LocationId/Name sólo si TODOS los items comparten la misma
|
||||||
|
//foreach (var h in headers)
|
||||||
|
//{
|
||||||
|
// var entry = locsPerHeader.FirstOrDefault(x => x.HeaderId == h.Id);
|
||||||
|
// if (entry is null) continue;
|
||||||
|
|
||||||
|
// if (entry.DistinctLocs.Count == 1)
|
||||||
|
// {
|
||||||
|
// var lid = entry.DistinctLocs[0];
|
||||||
|
// h.LocationId = lid;
|
||||||
|
// if (locNames.TryGetValue(lid, out var lname))
|
||||||
|
// h.LocationName = lname;
|
||||||
|
// }
|
||||||
|
// else
|
||||||
|
// {
|
||||||
|
// // varias ubicaciones: dejamos LocationId=0 y LocationName=null
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PagedResult<ExpeditionDto>
|
||||||
|
{
|
||||||
|
Items = headers,
|
||||||
|
TotalItems = total,
|
||||||
|
Page = page,
|
||||||
|
PageSize = pageSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
private static int? MapStatusLabelToInt(string labelOrNumber)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(labelOrNumber)) return null;
|
||||||
|
|
||||||
|
if (int.TryParse(labelOrNumber, out var n) && n is >= 1 and <= 6)
|
||||||
|
return n;
|
||||||
|
|
||||||
|
var key = NormalizeKey(labelOrNumber);
|
||||||
|
return key switch
|
||||||
|
{
|
||||||
|
"emitida" => 1,
|
||||||
|
"entransito" => 2, // cubre "en transito" y "en tránsito"
|
||||||
|
"endestino" => 3,
|
||||||
|
"retorno" => 4,
|
||||||
|
"cerrada" => 5,
|
||||||
|
"anulada" => 6,
|
||||||
|
_ => (int?)null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeKey(string s)
|
||||||
|
{
|
||||||
|
var norm = s.Trim().ToLowerInvariant().Normalize(NormalizationForm.FormD);
|
||||||
|
var sb = new StringBuilder(norm.Length);
|
||||||
|
foreach (var ch in norm)
|
||||||
|
if (CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark)
|
||||||
|
sb.Append(ch);
|
||||||
|
return sb.ToString().Normalize(NormalizationForm.FormC).Replace(" ", "");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
using Core.Interfaces.Stock;
|
using Core.Interfaces.Stock;
|
||||||
using Documents.Interfaces;
|
using Documents.Interfaces;
|
||||||
using Documents.Models;
|
using Documents.Models;
|
||||||
|
using Domain.Dtos.Stock;
|
||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
|
using Domain.Generics;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
namespace phronCare.API.Controllers.Stock
|
namespace phronCare.API.Controllers.Stock
|
||||||
{
|
{
|
||||||
@ -22,6 +25,40 @@ namespace phronCare.API.Controllers.Stock
|
|||||||
_expeditionService = expeditionService
|
_expeditionService = expeditionService
|
||||||
?? throw new ArgumentNullException(nameof(expeditionService));
|
?? throw new ArgumentNullException(nameof(expeditionService));
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Busca expediciones con filtros y paginación.
|
||||||
|
/// Filtros: número, estado, fechas de emisión, ubicación (opcional).
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("search")]
|
||||||
|
public async Task<ActionResult<PagedResult<ExpeditionDto>>> Search(
|
||||||
|
[FromQuery] string? expeditionNumber,
|
||||||
|
[FromQuery] string? status,
|
||||||
|
[FromQuery] DateTime? issueDateFrom,
|
||||||
|
[FromQuery] DateTime? issueDateTo,
|
||||||
|
[FromQuery] int? locationId,
|
||||||
|
[FromQuery] int page = 1,
|
||||||
|
[FromQuery] int pageSize = 50)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _expeditionService.SearchAsync(
|
||||||
|
expeditionNumber,
|
||||||
|
status,
|
||||||
|
issueDateFrom,
|
||||||
|
issueDateTo,
|
||||||
|
locationId,
|
||||||
|
page,
|
||||||
|
pageSize
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
|
||||||
|
return StatusCode(500, $"{methodName} Message: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#region Endpoint de emision de expedicion (encabezado + detalles)
|
#region Endpoint de emision de expedicion (encabezado + detalles)
|
||||||
[HttpPost("createfull")]
|
[HttpPost("createfull")]
|
||||||
@ -71,6 +108,7 @@ namespace phronCare.API.Controllers.Stock
|
|||||||
|
|
||||||
return File(pdfBytes, "application/pdf", $"Expedicion_{expedition.Expeditionnumber}.pdf");
|
return File(pdfBytes, "application/pdf", $"Expedicion_{expedition.Expeditionnumber}.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
public class CreateFullExpeditionRequest
|
public class CreateFullExpeditionRequest
|
||||||
{
|
{
|
||||||
|
|||||||
@ -659,6 +659,62 @@
|
|||||||
],
|
],
|
||||||
"ReturnTypes": []
|
"ReturnTypes": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ContainingType": "phronCare.API.Controllers.Stock.ExpeditionController",
|
||||||
|
"Method": "Search",
|
||||||
|
"RelativePath": "api/Expedition/search",
|
||||||
|
"HttpMethod": "GET",
|
||||||
|
"IsController": true,
|
||||||
|
"Order": 0,
|
||||||
|
"Parameters": [
|
||||||
|
{
|
||||||
|
"Name": "expeditionNumber",
|
||||||
|
"Type": "System.String",
|
||||||
|
"IsRequired": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "status",
|
||||||
|
"Type": "System.String",
|
||||||
|
"IsRequired": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "issueDateFrom",
|
||||||
|
"Type": "System.Nullable\u00601[[System.DateTime, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]",
|
||||||
|
"IsRequired": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "issueDateTo",
|
||||||
|
"Type": "System.Nullable\u00601[[System.DateTime, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]",
|
||||||
|
"IsRequired": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "locationId",
|
||||||
|
"Type": "System.Nullable\u00601[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]",
|
||||||
|
"IsRequired": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "page",
|
||||||
|
"Type": "System.Int32",
|
||||||
|
"IsRequired": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "pageSize",
|
||||||
|
"Type": "System.Int32",
|
||||||
|
"IsRequired": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ReturnTypes": [
|
||||||
|
{
|
||||||
|
"Type": "Domain.Generics.PagedResult\u00601[[Domain.Dtos.Stock.ExpeditionDto, Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]",
|
||||||
|
"MediaTypes": [
|
||||||
|
"text/plain",
|
||||||
|
"application/json",
|
||||||
|
"text/json"
|
||||||
|
],
|
||||||
|
"StatusCode": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ContainingType": "phronCare.API.Controllers.Sales.InstitutionController",
|
"ContainingType": "phronCare.API.Controllers.Sales.InstitutionController",
|
||||||
"Method": "GetById",
|
"Method": "GetById",
|
||||||
|
|||||||
@ -58,6 +58,13 @@
|
|||||||
@if (expStock)
|
@if (expStock)
|
||||||
{
|
{
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
|
<!-- Expediciones -->
|
||||||
|
<div class="nav-item ps-4 py-0 border-start border-2 border-white">
|
||||||
|
<NavLink class="nav-link small py-0 px-2 text-start d-flex align-items-center" href="expeditions" activeClass="bg-secondary text-white fw-semibold">
|
||||||
|
<span class="oi oi-data-transfer-download me-2 text-danger"></span> Expediciones
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Productos -->
|
<!-- Productos -->
|
||||||
<div class="nav-item ps-4 py-0 border-start border-2 border-white">
|
<div class="nav-item ps-4 py-0 border-start border-2 border-white">
|
||||||
<NavLink class="nav-link small py-0 px-2 text-start d-flex align-items-center" href="stock/products" activeClass="bg-secondary text-white fw-semibold">
|
<NavLink class="nav-link small py-0 px-2 text-start d-flex align-items-center" href="stock/products" activeClass="bg-secondary text-white fw-semibold">
|
||||||
@ -92,12 +99,7 @@
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item ps-4 py-0 border-start border-2 border-white">
|
<div class="nav-item ps-4 py-0 border-start border-2 border-white">
|
||||||
<NavLink class="nav-link small py-0 px-2 text-start d-flex align-items-center" href="stock/expeditions/create" activeClass="bg-secondary text-white fw-semibold">
|
<NavLink class="nav-link small py-0 px-2 text-start d-flex align-items-center" href="stock/consumes" activeClass="bg-secondary text-white fw-semibold">
|
||||||
<span class="oi oi-data-transfer-download me-2 text-danger"></span> Expediciones
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item ps-4 py-0 border-start border-2 border-white">
|
|
||||||
<NavLink class="nav-link small py-0 px-2 text-start d-flex align-items-center" href="stock/consumptions" activeClass="bg-secondary text-white fw-semibold">
|
|
||||||
<span class="oi oi-check me-2 text-secondary"></span> Consumos
|
<span class="oi oi-check me-2 text-secondary"></span> Consumos
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
@page "/stock/expeditions/create"
|
@page "/expeditions/create"
|
||||||
@using Blazored.Typeahead
|
@using Blazored.Typeahead
|
||||||
@using Domain.Dtos.Stock
|
@using Domain.Dtos.Stock
|
||||||
@using Services.Lookups
|
@using Services.Lookups
|
||||||
|
|||||||
@ -0,0 +1,370 @@
|
|||||||
|
@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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
314
phronCare.UIBlazor/Pages/Stock/Expeditions/Expeditions.razor
Normal file
314
phronCare.UIBlazor/Pages/Stock/Expeditions/Expeditions.razor
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
@page "/expeditions"
|
||||||
|
@using Domain.Dtos
|
||||||
|
@using Domain.Dtos.Stock
|
||||||
|
@using Domain.Generics
|
||||||
|
@using System.Text.Json
|
||||||
|
|
||||||
|
@using phronCare.UIBlazor.Services.Stock.Expeditions
|
||||||
|
@inject ExpeditionService 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-secondary rounded-pill" @onclick="Clear">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i> Volver
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-success rounded-pill" @onclick="ExportCurrent">
|
||||||
|
<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
|
||||||
|
{
|
||||||
|
<tr><td colspan="6" 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 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 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,11 +1,13 @@
|
|||||||
using Domain.Dtos;
|
using Domain.Dtos;
|
||||||
|
using Domain.Dtos.Stock;
|
||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
|
using Domain.Generics;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
namespace phronCare.UIBlazor.Services.Stock.Expeditions
|
namespace phronCare.UIBlazor.Services.Stock.Expeditions
|
||||||
{
|
{
|
||||||
public class ExpeditionService
|
public class ExpeditionService: IExpeditionService
|
||||||
{
|
{
|
||||||
private readonly IJSRuntime _js; //Todavia no se utiliza pero eventualmente para exportaciones seguramente./
|
private readonly IJSRuntime _js; //Todavia no se utiliza pero eventualmente para exportaciones seguramente./
|
||||||
private readonly HttpClient _http;
|
private readonly HttpClient _http;
|
||||||
@ -14,7 +16,43 @@ namespace phronCare.UIBlazor.Services.Stock.Expeditions
|
|||||||
_js = js;
|
_js = js;
|
||||||
_http = http;
|
_http = http;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Búsqueda de expediciones con filtros y paginación.
|
||||||
|
/// Filtros típicos: número, estado, fechas, ubicación (opcional).
|
||||||
|
/// </summary>
|
||||||
|
/// <summary>Buscar expediciones (simétrico a QuoteService.SearchAsync)</summary>
|
||||||
|
public async Task<PagedResult<ExpeditionDto>> SearchAsync(
|
||||||
|
string? expeditionNumber = null,
|
||||||
|
string? status = null,
|
||||||
|
DateTime? issueDateFrom = null,
|
||||||
|
DateTime? issueDateTo = null,
|
||||||
|
int? locationId = null,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 10)
|
||||||
|
{
|
||||||
|
var query = new List<string>();
|
||||||
|
void Add(string key, string? val)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(val))
|
||||||
|
query.Add($"{key}={Uri.EscapeDataString(val)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Add("expeditionNumber", expeditionNumber);
|
||||||
|
Add("status", status);
|
||||||
|
Add("issueDateFrom", issueDateFrom?.ToString("o"));
|
||||||
|
Add("issueDateTo", issueDateTo?.ToString("o"));
|
||||||
|
Add("locationId", locationId?.ToString());
|
||||||
|
Add("page", page.ToString());
|
||||||
|
Add("pageSize", pageSize.ToString());
|
||||||
|
|
||||||
|
// FIX: controller es singular => /api/expedition/search
|
||||||
|
var url = "/api/expedition/search";
|
||||||
|
if (query.Any()) url += "?" + string.Join("&", query);
|
||||||
|
|
||||||
|
var result = await _http.GetFromJsonAsync<PagedResult<ExpeditionDto>>(url);
|
||||||
|
return result!;
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Obtiene un presupuesto por QuoteNumber.
|
/// Obtiene un presupuesto por QuoteNumber.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -133,31 +171,5 @@ namespace phronCare.UIBlazor.Services.Stock.Expeditions
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Ajustar namespace real si es distinto
|
// TODO: Ajustar namespace real si es distinto
|
||||||
public class ExpeditionDto
|
|
||||||
{
|
|
||||||
// Estructura mínima para compilar si aún no referenciás el DTO real.
|
|
||||||
// Reemplazar por el DTO definitivo de Domain.Dtos.
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string ExpeditionNumber { get; set; } = string.Empty;
|
|
||||||
public string Status { get; set; } = string.Empty;
|
|
||||||
public DateTime IssueDate { get; set; }
|
|
||||||
public string? CustomerName { get; set; }
|
|
||||||
public string? ProfessionalName { get; set; }
|
|
||||||
public string? InstitutionName { get; set; }
|
|
||||||
public string? PatientName { get; set; }
|
|
||||||
public List<ExpeditionItemDto> Items { get; set; } = new();
|
|
||||||
public string? Observations { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ExpeditionItemDto
|
|
||||||
{
|
|
||||||
public int ProductId { get; set; }
|
|
||||||
public string ProductName { get; set; } = string.Empty;
|
|
||||||
public decimal Quantity { get; set; }
|
|
||||||
public string? Batch { get; set; }
|
|
||||||
public string? Serial { get; set; }
|
|
||||||
public DateOnly? Expiration { get; set; }
|
|
||||||
public int? LocationId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,6 +1,14 @@
|
|||||||
namespace phronCare.UIBlazor.Services.Stock.Expeditions
|
using Domain.Dtos;
|
||||||
|
using Domain.Dtos.Stock;
|
||||||
|
using Domain.Generics;
|
||||||
|
|
||||||
|
namespace phronCare.UIBlazor.Services.Stock.Expeditions
|
||||||
{
|
{
|
||||||
public interface IExpeditionService
|
public interface IExpeditionService
|
||||||
{
|
{
|
||||||
|
Task ExportPdfAsync(int expeditionId, string expeditionNumber);
|
||||||
|
Task<ExpeditionDto?> GetDtoByIdAsync(int id);
|
||||||
|
Task<QuoteDto?> GetQuoteByNumberAsync(string quoteNumber);
|
||||||
|
Task<PagedResult<ExpeditionDto>> SearchAsync(string? expeditionNumber = null, string? status = null, DateTime? issueDateFrom = null, DateTime? issueDateTo = null, int? locationId = null, int page = 1, int pageSize = 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user