From be690849fd917c48919ca177ffc443f602737b26 Mon Sep 17 00:00:00 2001 From: Leandro Hernan Rojas Date: Fri, 5 Sep 2025 16:31:58 -0300 Subject: [PATCH] Update Expediciones UI --- Core/Interfaces/Stock/IExpeditionDom.cs | 17 +- Core/Services/Stock/ExpeditionService.cs | 20 +- Models/Interfaces/IExpeditionRepository.cs | 2 + .../Stock/PhLSMExpeditionRepository.cs | 163 ++++++++ .../Controllers/Stock/ExpeditionController.cs | 38 ++ .../obj/Debug/net8.0/ApiEndpoints.json | 56 +++ phronCare.UIBlazor/Layout/NavMenu.razor | 14 +- .../Stock/Expeditions/ExpeditionCreate.razor | 2 +- .../Expeditions/ExpeditionDetailDrawer.razor | 370 ++++++++++++++++++ .../Pages/Stock/Expeditions/Expeditions.razor | 314 +++++++++++++++ .../Stock/Expeditions/ExpeditionService.cs | 68 ++-- .../Stock/Expeditions/IExpeditionService.cs | 10 +- 12 files changed, 1034 insertions(+), 40 deletions(-) create mode 100644 phronCare.UIBlazor/Pages/Stock/Expeditions/ExpeditionDetailDrawer.razor create mode 100644 phronCare.UIBlazor/Pages/Stock/Expeditions/Expeditions.razor diff --git a/Core/Interfaces/Stock/IExpeditionDom.cs b/Core/Interfaces/Stock/IExpeditionDom.cs index 44e0d47..e308276 100644 --- a/Core/Interfaces/Stock/IExpeditionDom.cs +++ b/Core/Interfaces/Stock/IExpeditionDom.cs @@ -1,12 +1,27 @@ using Domain.Dtos.Stock; using Domain.Entities; + using Domain.Generics; namespace Core.Interfaces.Stock { // 1.2 Domain (Core) public interface IExpeditionDom { - Task<(int Id, string ExpeditionNumber)> CreateAndIssueAsync(ELSExpeditionHeader header, IEnumerable details, int formSeriesId); + Task> SearchAsync( + string? expeditionNumber, + string? status, + DateTime? issueDateFrom, + DateTime? issueDateTo, + int? locationId, + int page, + int pageSize); + Task GetDtoByIdAsync(int id); + Task GetDtoByExpeditionNumberAsync(string expeditionNumber); + + Task<(int Id, string ExpeditionNumber)> CreateAndIssueAsync( + ELSExpeditionHeader header, + IEnumerable details, + int formSeriesId); } } diff --git a/Core/Services/Stock/ExpeditionService.cs b/Core/Services/Stock/ExpeditionService.cs index b88a4ee..5451ec9 100644 --- a/Core/Services/Stock/ExpeditionService.cs +++ b/Core/Services/Stock/ExpeditionService.cs @@ -1,6 +1,7 @@ using Core.Interfaces.Stock; using Domain.Dtos.Stock; using Domain.Entities; +using Domain.Generics; using Models.Interfaces; namespace Core.Services.Stock @@ -28,10 +29,23 @@ namespace Core.Services.Stock return await _repo.CreateFullExpeditionAsync(header, formSeriesId); } - #endregion - public async Task GetDtoByIdAsync(int id) + + public Task GetDtoByExpeditionNumberAsync(string expeditionNumber) { - return await _repo.GetDtoByIdAsync(id); + throw new NotImplementedException(); } + #endregion + + public Task> 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 GetDtoByIdAsync(int id) + => _repo.GetDtoByIdAsync(id); } } diff --git a/Models/Interfaces/IExpeditionRepository.cs b/Models/Interfaces/IExpeditionRepository.cs index a5c807d..3975de9 100644 --- a/Models/Interfaces/IExpeditionRepository.cs +++ b/Models/Interfaces/IExpeditionRepository.cs @@ -1,5 +1,6 @@ using Domain.Dtos.Stock; using Domain.Entities; +using Domain.Generics; namespace Models.Interfaces { @@ -11,6 +12,7 @@ namespace Models.Interfaces /// Task<(int Id, string Expeditionnumber)> CreateFullExpeditionAsync(ELSExpeditionHeader expedition, int formSeriesId); Task GetDtoByIdAsync(int id); + Task> SearchAsync(string? expeditionNumber, string? status, DateTime? issueDateFrom, DateTime? issueDateTo, int? locationId, int page, int pageSize); } } diff --git a/Models/Repositories/Stock/PhLSMExpeditionRepository.cs b/Models/Repositories/Stock/PhLSMExpeditionRepository.cs index 82908be..c0d6651 100644 --- a/Models/Repositories/Stock/PhLSMExpeditionRepository.cs +++ b/Models/Repositories/Stock/PhLSMExpeditionRepository.cs @@ -1,10 +1,13 @@ using Domain.Constants; using Domain.Dtos.Stock; using Domain.Entities; +using Domain.Generics; using Microsoft.EntityFrameworkCore; using Models.Helpers; using Models.Interfaces; using Models.Models; +using System.Globalization; +using System.Text; namespace Models.Repositories.Stock { @@ -177,6 +180,166 @@ namespace Models.Repositories.Stock 6 => "Anulada", _ => $"Desconocido ({status})" }; + // =========================== + // BÚSQUEDA + PAGINACIÓN + // =========================== + public async Task> 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 + { + 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(" ", ""); + } + } } diff --git a/phronCare.API/Controllers/Stock/ExpeditionController.cs b/phronCare.API/Controllers/Stock/ExpeditionController.cs index b1697ea..8367e22 100644 --- a/phronCare.API/Controllers/Stock/ExpeditionController.cs +++ b/phronCare.API/Controllers/Stock/ExpeditionController.cs @@ -1,8 +1,11 @@ using Core.Interfaces.Stock; using Documents.Interfaces; using Documents.Models; +using Domain.Dtos.Stock; using Domain.Entities; +using Domain.Generics; using Microsoft.AspNetCore.Mvc; +using System.Reflection; namespace phronCare.API.Controllers.Stock { @@ -22,6 +25,40 @@ namespace phronCare.API.Controllers.Stock _expeditionService = expeditionService ?? throw new ArgumentNullException(nameof(expeditionService)); } + /// + /// Busca expediciones con filtros y paginación. + /// Filtros: número, estado, fechas de emisión, ubicación (opcional). + /// + [HttpGet("search")] + public async Task>> 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) [HttpPost("createfull")] @@ -71,6 +108,7 @@ namespace phronCare.API.Controllers.Stock return File(pdfBytes, "application/pdf", $"Expedicion_{expedition.Expeditionnumber}.pdf"); } + } public class CreateFullExpeditionRequest { diff --git a/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json b/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json index ce212f5..47e7f6f 100644 --- a/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json +++ b/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json @@ -659,6 +659,62 @@ ], "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", "Method": "GetById", diff --git a/phronCare.UIBlazor/Layout/NavMenu.razor b/phronCare.UIBlazor/Layout/NavMenu.razor index ffaf995..14ca6d7 100644 --- a/phronCare.UIBlazor/Layout/NavMenu.razor +++ b/phronCare.UIBlazor/Layout/NavMenu.razor @@ -58,6 +58,13 @@ @if (expStock) {