From 6419ac8843e8503993938c3351c2076a90cc60ab Mon Sep 17 00:00:00 2001 From: leandro Date: Sat, 14 Mar 2026 21:23:06 -0300 Subject: [PATCH] =?UTF-8?q?feat(expeditions):=20permitir=20transici=C3=B3n?= =?UTF-8?q?=20Emitida=20=E2=86=92=20EnTransito=20desde=20la=20consulta=20c?= =?UTF-8?q?loses=20#7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Core/Interfaces/Stock/IExpeditionDom.cs | 1 + Core/Services/Stock/ExpeditionService.cs | 7 + Models/Interfaces/IExpeditionRepository.cs | 2 + .../Stock/PhLSMExpeditionRepository.cs | 15 ++ Services/Services.csproj | 2 +- .../Controllers/Stock/ExpeditionController.cs | 26 ++++ phronCare.API/phronCare.API.csproj | 1 + phronCare.Test/PdfGeneratorServiceTests.cs | 142 ------------------ .../Pages/Stock/Expeditions/Expeditions.razor | 72 ++++++++- .../Stock/Expeditions/ExpeditionService.cs | 17 +++ .../Stock/Expeditions/IExpeditionService.cs | 1 + 11 files changed, 136 insertions(+), 150 deletions(-) delete mode 100644 phronCare.Test/PdfGeneratorServiceTests.cs diff --git a/Core/Interfaces/Stock/IExpeditionDom.cs b/Core/Interfaces/Stock/IExpeditionDom.cs index 4f4618e..fcb5800 100644 --- a/Core/Interfaces/Stock/IExpeditionDom.cs +++ b/Core/Interfaces/Stock/IExpeditionDom.cs @@ -24,5 +24,6 @@ namespace Core.Interfaces.Stock IEnumerable details, int formSeriesId); Task ExportFilteredToExcelAsync(ExpeditionSearchParams searchParams); + Task MarkInTransitAsync(int expeditionId); } } diff --git a/Core/Services/Stock/ExpeditionService.cs b/Core/Services/Stock/ExpeditionService.cs index 85708be..1b6f153 100644 --- a/Core/Services/Stock/ExpeditionService.cs +++ b/Core/Services/Stock/ExpeditionService.cs @@ -220,5 +220,12 @@ namespace Core.Services.Stock throw new Exception($"{ex.Message}", ex); } } + public async Task MarkInTransitAsync(int expeditionId) + { + if (expeditionId <= 0) + throw new ArgumentException("El identificador de la expedición no es válido."); + + await _repo.MarkInTransitAsync(expeditionId); + } } } diff --git a/Models/Interfaces/IExpeditionRepository.cs b/Models/Interfaces/IExpeditionRepository.cs index 5270ce2..b1862bb 100644 --- a/Models/Interfaces/IExpeditionRepository.cs +++ b/Models/Interfaces/IExpeditionRepository.cs @@ -23,6 +23,8 @@ 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); + Task MarkInTransitAsync(int expeditionId); + } } diff --git a/Models/Repositories/Stock/PhLSMExpeditionRepository.cs b/Models/Repositories/Stock/PhLSMExpeditionRepository.cs index a58835b..6d27217 100644 --- a/Models/Repositories/Stock/PhLSMExpeditionRepository.cs +++ b/Models/Repositories/Stock/PhLSMExpeditionRepository.cs @@ -404,7 +404,22 @@ namespace Models.Repositories.Stock sb.Append(ch); return sb.ToString().Normalize(NormalizationForm.FormC).Replace(" ", ""); } + public async Task MarkInTransitAsync(int expeditionId) + { + var header = await _context.PhLsmExpeditionHeaders + .FirstOrDefaultAsync(x => x.Id == expeditionId); + if (header == null) + throw new KeyNotFoundException($"No se encontró la expedición con ID {expeditionId}."); + + if (header.Status != (int)ExpeditionStatus.Emitida) + throw new InvalidOperationException("Solo las expediciones en estado 'Emitida' pueden pasar a 'En tránsito'."); + + header.Status = (int)ExpeditionStatus.EnTransito; + header.Modifiedat = DateTime.Now; + + await _context.SaveChangesAsync(); + } } } diff --git a/Services/Services.csproj b/Services/Services.csproj index 5ecc797..e586b62 100644 --- a/Services/Services.csproj +++ b/Services/Services.csproj @@ -8,7 +8,7 @@ - + diff --git a/phronCare.API/Controllers/Stock/ExpeditionController.cs b/phronCare.API/Controllers/Stock/ExpeditionController.cs index 2cb1419..5a5db98 100644 --- a/phronCare.API/Controllers/Stock/ExpeditionController.cs +++ b/phronCare.API/Controllers/Stock/ExpeditionController.cs @@ -144,6 +144,32 @@ namespace phronCare.API.Controllers.Stock return BadRequest(ex.Message); } } + + [HttpPost("{id:int}/mark-in-transit")] + public async Task MarkInTransit(int id) + { + try + { + await _expeditionService.MarkInTransitAsync(id); + return Ok(new { Success = true, Message = "La expedición pasó a estado 'En tránsito' correctamente." }); + } + catch (KeyNotFoundException ex) + { + return NotFound(ex.Message); + } + catch (InvalidOperationException ex) + { + return BadRequest(ex.Message); + } + catch (ArgumentException ex) + { + return BadRequest(ex.Message); + } + catch (Exception ex) + { + return StatusCode(500, $"Ocurrió un error interno: {ex.Message}"); + } + } } public class CreateFullExpeditionRequest { diff --git a/phronCare.API/phronCare.API.csproj b/phronCare.API/phronCare.API.csproj index 7cb5363..5d59202 100644 --- a/phronCare.API/phronCare.API.csproj +++ b/phronCare.API/phronCare.API.csproj @@ -37,6 +37,7 @@ + all diff --git a/phronCare.Test/PdfGeneratorServiceTests.cs b/phronCare.Test/PdfGeneratorServiceTests.cs deleted file mode 100644 index b5c7914..0000000 --- a/phronCare.Test/PdfGeneratorServiceTests.cs +++ /dev/null @@ -1,142 +0,0 @@ -using Transversal.Services; -using Transversal.Models; - -namespace phronCare.Test -{ - [TestFixture] - public class PdfGeneratorServiceTests - { - private PuppeteerPdfGeneratorService _pdfService; - - [OneTimeSetUp] - public void Setup() - { - // Instancia real del servicio (recomendado para test manual/local) - _pdfService = new PuppeteerPdfGeneratorService(); - } - - [OneTimeTearDown] - public async Task TearDown() - { - // Liberar Chromium al finalizar los tests - if (_pdfService != null) - await _pdfService.DisposeAsync(); - } - - [Test] - public async Task GeneratePdfFromHtml_ShouldCreatePdfFile() - { - // Arrange - - string html = @" - - - - - -
-

PhronCare Ortopedia

-

Presupuesto Médico

-
- -
- - - - - - - - - -
Cliente: Juan PérezPresupuesto N°: 000123
Fecha: 13/05/2025Profesional: Dr. Carlos López
-
- -
- - - - - - - - - - - - - - - - - - - -
CantidadProductoPrecio UnitarioSubtotal
2Rodillera ortopédica$5.000$10.000
1Férula de inmovilización$8.000$8.000
-
- -
- - - - - - - - - - - - - -
Subtotal$18.000
IVA (21%)$3.780
Total$21.780
-
- - - - "; - string outputFolder = @"C:\temp"; - if (!Directory.Exists(outputFolder)) - Directory.CreateDirectory(outputFolder); - - string outputPath = Path.Combine(outputFolder, "DemoTest_Puppeteer.pdf"); - - // Opcional: podés probar pasando o no opciones - var options = new PdfGenerationOptions - { - Format = PuppeteerSharp.Media.PaperFormat.A4, - Landscape = false, - PrintBackground = true, - Scale = 1.0m, - HeaderTemplate = "
Presupuesto
", - FooterTemplate = "
Página de
" - }; - - // Act - byte[] pdfBytes = await _pdfService.GeneratePdfFromHtmlAsync(html, options); - await File.WriteAllBytesAsync(outputPath, pdfBytes); - - // Assert - Assert.IsTrue(File.Exists(outputPath)); - Assert.IsTrue(new FileInfo(outputPath).Length > 0); - TestContext.WriteLine($"PDF generado correctamente en: {outputPath}"); - } - } -} diff --git a/phronCare.UIBlazor/Pages/Stock/Expeditions/Expeditions.razor b/phronCare.UIBlazor/Pages/Stock/Expeditions/Expeditions.razor index 1f0ff4f..ebabf58 100644 --- a/phronCare.UIBlazor/Pages/Stock/Expeditions/Expeditions.razor +++ b/phronCare.UIBlazor/Pages/Stock/Expeditions/Expeditions.razor @@ -3,12 +3,13 @@ @using Domain.Dtos.Stock @using Domain.Generics @using System.Text.Json - +@using phronCare.UIBlazor.Shared.Modals @using phronCare.UIBlazor.Services.Stock.Expeditions @inject IExpeditionService expeditionService @inject NavigationManager Nav @inject IToastService Toast +@inject IModalService Modal
@@ -115,13 +116,21 @@ @e.Observations
- - + + + @if (CanMarkInTransit(e)) + { + + }
@@ -187,6 +196,7 @@ private async Task OpenDetailAsync(ExpeditionDto dto) { // Abro el drawer con lo que ya tengo (encabezado) + loadingDetail = true; drawerOpen = true; selected = dto; StateHasChanged(); @@ -345,4 +355,52 @@ if (DateTime.TryParse(s, out var dt)) return dt; return null; } + // private bool CanMarkInTransit(ExpeditionDto expedition) + // { + // if (expedition == null) + // return false; + + // if (!string.IsNullOrWhiteSpace(expedition.StatusLabel)) + // return string.Equals(expedition.StatusLabel, "Emitida", StringComparison.OrdinalIgnoreCase); + + // return expedition.Status == 1; + // } + private bool CanMarkInTransit(ExpeditionDto expedition) + { + return expedition is not null && expedition.Status == 1; + } + private async Task ConfirmMarkInTransitAsync(ExpeditionDto expedition) + { + var parameters = new ModalParameters(); + parameters.Add(nameof(ConfirmModal.Title), "Confirmar cambio de estado"); + parameters.Add(nameof(ConfirmModal.Message), + $"La expedición '{expedition.Expeditionnumber}' pasará a estado 'En tránsito'. ¿Desea continuar?"); + + var modal = Modal.Show("Confirmar", parameters); + var resultModal = await modal.Result; + + if (resultModal.Cancelled) + return; + + try + { + await expeditionService.MarkInTransitAsync(expedition.Id); + + Toast.ShowSuccess($"La expedición '{expedition.Expeditionnumber}' pasó a 'En tránsito' correctamente."); + + await Search(); + + if (selected is not null && selected.Id == expedition.Id) + { + selected = await expeditionService.GetDtoByIdAsync(expedition.Id); + StateHasChanged(); + } + } + catch (Exception ex) + { + Toast.ShowError(string.IsNullOrWhiteSpace(ex.Message) + ? "No se pudo pasar la expedición a 'En tránsito'." + : ex.Message); + } + } } diff --git a/phronCare.UIBlazor/Services/Stock/Expeditions/ExpeditionService.cs b/phronCare.UIBlazor/Services/Stock/Expeditions/ExpeditionService.cs index 410d8ea..d82dd5f 100644 --- a/phronCare.UIBlazor/Services/Stock/Expeditions/ExpeditionService.cs +++ b/phronCare.UIBlazor/Services/Stock/Expeditions/ExpeditionService.cs @@ -179,6 +179,23 @@ namespace phronCare.UIBlazor.Services.Stock.Expeditions throw new Exception($"{message}", ex); } } + public async Task MarkInTransitAsync(int expeditionId) + { + if (expeditionId <= 0) + throw new ArgumentException("El identificador de la expedición no es válido."); + + var response = await _http.PostAsync($"api/expedition/{expeditionId}/mark-in-transit", null); + + if (!response.IsSuccessStatusCode) + { + var message = await response.Content.ReadAsStringAsync(); + + if (string.IsNullOrWhiteSpace(message)) + message = "No se pudo pasar la expedición a 'En tránsito'."; + + throw new Exception(message); + } + } } /// diff --git a/phronCare.UIBlazor/Services/Stock/Expeditions/IExpeditionService.cs b/phronCare.UIBlazor/Services/Stock/Expeditions/IExpeditionService.cs index 5fdfd19..7a6d03a 100644 --- a/phronCare.UIBlazor/Services/Stock/Expeditions/IExpeditionService.cs +++ b/phronCare.UIBlazor/Services/Stock/Expeditions/IExpeditionService.cs @@ -11,5 +11,6 @@ namespace phronCare.UIBlazor.Services.Stock.Expeditions Task GetQuoteByNumberAsync(string quoteNumber); Task> SearchAsync(string? expeditionNumber = null, string? status = null, DateTime? issueDateFrom = null, DateTime? issueDateTo = null, int? locationId = null, int page = 1, int pageSize = 10); Task ExportFilteredAsync(LSProductSearchParams searchParams); + Task MarkInTransitAsync(int expeditionId); } }