feat(expeditions): permitir transición Emitida → EnTransito desde la consulta
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 34m41s
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 34m41s
closes #7
This commit is contained in:
parent
d99f1c34d2
commit
6419ac8843
@ -24,5 +24,6 @@ namespace Core.Interfaces.Stock
|
||||
IEnumerable<ELSExpeditionDetail> details,
|
||||
int formSeriesId);
|
||||
Task<byte[]> ExportFilteredToExcelAsync(ExpeditionSearchParams searchParams);
|
||||
Task MarkInTransitAsync(int expeditionId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,8 @@ namespace Models.Interfaces
|
||||
Task<(int Id, string Expeditionnumber)> CreateFullExpeditionAsync(ELSExpeditionHeader expedition, int formSeriesId);
|
||||
Task<ExpeditionDto?> GetDtoByIdAsync(int id);
|
||||
Task<PagedResult<ExpeditionDto>> SearchAsync(string? expeditionNumber, string? status, DateTime? issueDateFrom, DateTime? issueDateTo, int? locationId, int page, int pageSize);
|
||||
Task MarkInTransitAsync(int expeditionId);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EPPlus" Version="7.5.2" />
|
||||
<PackageReference Include="MimeKit" Version="4.8.0" />
|
||||
<PackageReference Include="MimeKit" Version="4.15.1" />
|
||||
<PackageReference Include="NETCore.MailKit" Version="2.1.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -144,6 +144,32 @@ namespace phronCare.API.Controllers.Stock
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{id:int}/mark-in-transit")]
|
||||
public async Task<IActionResult> 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
|
||||
{
|
||||
|
||||
@ -37,6 +37,7 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.15" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.Bcl.Memory" Version="9.0.14" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@ -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 = @"
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; font-size: 12px; margin: 20px; }
|
||||
.header { text-align: center; border-bottom: 2px solid #4CAF50; padding-bottom: 10px; margin-bottom: 20px; }
|
||||
.header h1 { color: #4CAF50; }
|
||||
.info { margin-bottom: 20px; }
|
||||
.info table { width: 100%; }
|
||||
.info td { padding: 5px; }
|
||||
.details table { width: 100%; border-collapse: collapse; }
|
||||
.details th, .details td { border: 1px solid #dddddd; text-align: center; padding: 8px; }
|
||||
.details th { background-color: #4CAF50; color: white; }
|
||||
.totals { margin-top: 20px; float: right; width: 300px; }
|
||||
.totals table { width: 100%; border-collapse: collapse; }
|
||||
.totals th, .totals td { border: 1px solid #dddddd; text-align: right; padding: 8px; }
|
||||
.totals th { background-color: #4CAF50; color: white; }
|
||||
.footer { position: fixed; bottom: 20px; left: 0; right: 0; text-align: center; font-size: 10px; color: gray; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class='header'>
|
||||
<h1>PhronCare Ortopedia</h1>
|
||||
<p>Presupuesto Médico</p>
|
||||
</div>
|
||||
|
||||
<div class='info'>
|
||||
<table>
|
||||
<tr>
|
||||
<td><strong>Cliente:</strong> Juan Pérez</td>
|
||||
<td><strong>Presupuesto N°:</strong> 000123</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Fecha:</strong> 13/05/2025</td>
|
||||
<td><strong>Profesional:</strong> Dr. Carlos López</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class='details'>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Cantidad</th>
|
||||
<th>Producto</th>
|
||||
<th>Precio Unitario</th>
|
||||
<th>Subtotal</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>Rodillera ortopédica</td>
|
||||
<td>$5.000</td>
|
||||
<td>$10.000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>Férula de inmovilización</td>
|
||||
<td>$8.000</td>
|
||||
<td>$8.000</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class='totals'>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Subtotal</th>
|
||||
<td>$18.000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>IVA (21%)</th>
|
||||
<td>$3.780</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Total</th>
|
||||
<td><strong>$21.780</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class='footer'>
|
||||
Presupuesto generado automáticamente por PhronCare - No válido como factura.
|
||||
</div>
|
||||
</body>
|
||||
</html>";
|
||||
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 = "<div style='font-size:10px; text-align:center;'>Presupuesto</div>",
|
||||
FooterTemplate = "<div style='font-size:10px; text-align:center;'>Página <span class='pageNumber'></span> de <span class='totalPages'></span></div>"
|
||||
};
|
||||
|
||||
// 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
<div class="card shadow-sm mb-3" style="zoom: 0.8;">
|
||||
<div class="card-header py-2">
|
||||
@ -115,13 +116,21 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@if (CanMarkInTransit(e))
|
||||
{
|
||||
<button class="btn btn-outline-warning rounded-pill"
|
||||
title="Pasar a En tránsito"
|
||||
@onclick="() => ConfirmMarkInTransitAsync(e)">
|
||||
<i class="fas fa-truck"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -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<ConfirmModal>("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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -11,5 +11,6 @@ namespace phronCare.UIBlazor.Services.Stock.Expeditions
|
||||
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);
|
||||
Task ExportFilteredAsync(LSProductSearchParams searchParams);
|
||||
Task MarkInTransitAsync(int expeditionId);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user