Add Authorization Quote
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 7m16s

This commit is contained in:
Leandro Hernan Rojas 2025-05-29 19:21:57 -03:00
parent 69ea0a6a98
commit e276e9672c
19 changed files with 494 additions and 22 deletions

View File

@ -21,5 +21,9 @@ namespace Models.Interfaces
Task<(int Id, string Quotenumber)> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId); Task<(int Id, string Quotenumber)> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
Task<QuoteDto?> GetDtoByIdAsync(int id); Task<QuoteDto?> GetDtoByIdAsync(int id);
#endregion #endregion
#region Autorización
Task<bool> AuthorizeQuoteAsync(int quoteId, List<QuoteAuthorizationDto> items);
#endregion
} }
} }

View File

@ -48,6 +48,19 @@ namespace Core.Services
} }
#endregion #endregion
public async Task<bool> AuthorizeQuoteAsync(int quoteId, List<QuoteAuthorizationDto> items)
{
var approvedDetails = items.Select(i => new EQuoteDetail
{
Id = i.Id,
Approved = i.Approved,
Approvedquantity = i.ApprovedQuantity,
Approvedunitprice = i.ApprovedUnitPrice
}).ToList();
return await _quoteRepository.AuthorizeQuoteAsync(quoteId, approvedDetails);
}
#region Validaciones QuoteCreate #region Validaciones QuoteCreate
private void ValidateQuote(EQuoteHeader quote) private void ValidateQuote(EQuoteHeader quote)
{ {

View File

@ -0,0 +1,10 @@
namespace Domain.Dtos
{
public class QuoteAuthorizationDto
{
public int Id { get; set; } // Id del detalle
public bool Approved { get; set; } // ¿Aprobado?
public int? ApprovedQuantity { get; set; } // Cantidad aprobada
public decimal? ApprovedUnitPrice { get; set; } // Precio aprobado
}
}

View File

@ -0,0 +1,7 @@
using Domain.Dtos;
public class QuoteAuthorizationRequest
{
public int QuoteId { get; set; }
public List<QuoteAuthorizationDto>? Items { get; set; } = new();
}

View File

@ -2,10 +2,15 @@
{ {
public class QuoteItemDto public class QuoteItemDto
{ {
/// <summary>
/// Identificador único del ítem dentro del presupuesto.
/// </summary>
public int Id { get; set; }
/// <summary> /// <summary>
/// Descripción del producto o servicio cotizado. /// Descripción del producto o servicio cotizado.
/// </summary> /// </summary>
public string Description { get; set; } = ""; public string Description { get; set; } = string.Empty;
/// <summary> /// <summary>
/// Cantidad de unidades cotizadas. /// Cantidad de unidades cotizadas.

View File

@ -37,6 +37,16 @@
/// </summary> /// </summary>
public bool Approved { get; set; } public bool Approved { get; set; }
/// <summary>
/// Cantidad aprobada para este ítem del presupuesto.
/// </summary>
public int? Approvedquantity { get; set; }
/// <summary>
/// Precio unitario aprobado (puede diferir del original).
/// </summary>
public decimal? Approvedunitprice { get; set; }
/// <summary> /// <summary>
/// Importe aprobado final /// Importe aprobado final
/// </summary> /// </summary>
@ -52,8 +62,8 @@
/// </summary> /// </summary>
public DateTime? Modifiedat { get; set; } public DateTime? Modifiedat { get; set; }
//public virtual EProduct Product { get; set; } = null!; //public virtual PhSProduct Product { get; set; } = null!;
//public virtual EQuoteHeader PhSQuoteheader { get; set; } = null!; //public virtual PhSQuoteHeader Quoteheader { get; set; } = null!;
} }
} }

View File

@ -44,7 +44,7 @@
/// <summary> /// <summary>
/// Fecha de aprobación /// Fecha de aprobación
/// </summary> /// </summary>
public DateOnly? Approvaldate { get; set; } public DateTime? Approvaldate { get; set; }
/// <summary> /// <summary>
/// Fecha tentativa (de cirugía por ej.) /// Fecha tentativa (de cirugía por ej.)

View File

@ -12,6 +12,7 @@ namespace Models.Interfaces
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos) #region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
Task<(int Id, string Quotenumber)> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId); Task<(int Id, string Quotenumber)> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
Task<QuoteDto?> GetDtoByIdAsync(int id); Task<QuoteDto?> GetDtoByIdAsync(int id);
Task<bool> AuthorizeQuoteAsync(int quoteId, List<EQuoteDetail> approvedItems);
#endregion #endregion
} }
} }

View File

@ -43,6 +43,16 @@ public partial class PhSQuoteDetail
/// </summary> /// </summary>
public bool Approved { get; set; } public bool Approved { get; set; }
/// <summary>
/// Cantidad aprobada para este ítem del presupuesto.
/// </summary>
public int? Approvedquantity { get; set; }
/// <summary>
/// Precio unitario aprobado (puede diferir del original).
/// </summary>
public decimal? Approvedunitprice { get; set; }
/// <summary> /// <summary>
/// Importe aprobado final /// Importe aprobado final
/// </summary> /// </summary>

View File

@ -46,7 +46,7 @@ public partial class PhSQuoteHeader
/// <summary> /// <summary>
/// Fecha de aprobación /// Fecha de aprobación
/// </summary> /// </summary>
public DateOnly? Approvaldate { get; set; } public DateTime? Approvaldate { get; set; }
/// <summary> /// <summary>
/// Fecha tentativa (de cirugía por ej.) /// Fecha tentativa (de cirugía por ej.)

View File

@ -74,15 +74,8 @@ public partial class PhronCareOperationsHubContext : DbContext
public virtual DbSet<PhSQuoteTaxis> PhSQuoteTaxes { get; set; } public virtual DbSet<PhSQuoteTaxis> PhSQuoteTaxes { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#region VERSION DOCKER #warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
{ => optionsBuilder.UseSqlServer("data source=srv01.saludlab.com.ar,39458;initial catalog=phronCare_OperationsHub;User ID=sa;Password=HS|s[~xxQzTo/n>9jO;encrypt=False;trustServerCertificate=True;MultipleActiveResultSets=True");
if (!optionsBuilder.IsConfigured)
{
// Dejarlo vacío para usar la configuración externa desde Program.cs o Startup.cs
}
}
#endregion
//=> optionsBuilder.UseSqlServer("data source=srv01.saludlab.com.ar,39458;initial catalog=phronCare_OperationsHub;User ID=sa;Password=HS|s[~xxQzTo/n>9jO;encrypt=False;trustServerCertificate=True;MultipleActiveResultSets=True");
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@ -888,6 +881,13 @@ public partial class PhronCareOperationsHubContext : DbContext
.HasComment("Importe aprobado final") .HasComment("Importe aprobado final")
.HasColumnType("decimal(18, 2)") .HasColumnType("decimal(18, 2)")
.HasColumnName("approvedamount"); .HasColumnName("approvedamount");
entity.Property(e => e.Approvedquantity)
.HasComment("Cantidad aprobada para este ítem del presupuesto.")
.HasColumnName("approvedquantity");
entity.Property(e => e.Approvedunitprice)
.HasComment("Precio unitario aprobado (puede diferir del original).")
.HasColumnType("decimal(18, 2)")
.HasColumnName("approvedunitprice");
entity.Property(e => e.Createdat) entity.Property(e => e.Createdat)
.HasDefaultValueSql("(getdate())") .HasDefaultValueSql("(getdate())")
.HasComment("Fecha de creación del registro") .HasComment("Fecha de creación del registro")

View File

@ -296,6 +296,7 @@ namespace Models.Repositories
return new QuoteItemDto return new QuoteItemDto
{ {
Id = d.Id,
Description = d.ProductDescription, Description = d.ProductDescription,
Quantity = d.Quantity, Quantity = d.Quantity,
UnitPrice = d.Unitprice, UnitPrice = d.Unitprice,
@ -378,5 +379,56 @@ namespace Models.Repositories
} }
} }
#endregion #endregion
#region Autorización de presupuesto (aprobar/rechazar ítems)
public async Task<bool> AuthorizeQuoteAsync(int quoteId, List<EQuoteDetail> approvedItems)
{
var header = await _context.PhSQuoteHeaders
.Include(h => h.PhSQuoteDetails)
.FirstOrDefaultAsync(h => h.Id == quoteId);
if (header == null)
return false;
bool anyApproved = false;
foreach (var item in approvedItems)
{
var detail = header.PhSQuoteDetails.FirstOrDefault(d => d.Id == item.Id);
if (detail == null)
continue;
detail.Approved = item.Approved;
detail.Approvedquantity = item.Approved ? item.Approvedquantity : null;
detail.Approvedunitprice = item.Approved ? item.Approvedunitprice : null;
detail.Approvedamount = item.Approved && item.Approvedquantity.HasValue && item.Approvedunitprice.HasValue
? item.Approvedquantity.Value * item.Approvedunitprice.Value
: null;
if (item.Approved)
anyApproved = true;
detail.Modifiedat = DateTime.Now;
}
if (anyApproved)
{
header.Status = "Aprobado";
header.Approvaldate = DateTime.Now;
header.Approvedamount = header.PhSQuoteDetails
.Where(d => d.Approved && d.Approvedamount.HasValue)
.Sum(d => d.Approvedamount.Value);
}
else
{
header.Status = "Anulado";
header.Approvaldate = DateTime.Now;
header.Approvedamount = 0;
}
header.Modifiedat = DateTime.Now;
await _context.SaveChangesAsync();
return true;
}
#endregion
} }
} }

View File

@ -72,6 +72,29 @@ namespace phronCare.API.Controllers.Sales
} }
} }
/// <summary>
/// Obtiene un presupuesto completo por su ID.
/// </summary>
[HttpGet("{id}")]
public async Task<ActionResult<QuoteDto>> GetById(int id)
{
try
{
var quote = await _quoteService.GetDtoByIdAsync(id);
if (quote == null)
return NotFound($"Presupuesto con ID {id} no encontrado.");
return Ok(quote);
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpGet("{id}/pdf")] [HttpGet("{id}/pdf")]
public async Task<IActionResult> GetQuotePdf(int id) public async Task<IActionResult> GetQuotePdf(int id)
{ {
@ -130,6 +153,20 @@ namespace phronCare.API.Controllers.Sales
#endregion #endregion
#region Autorizacion de presupuestos
[HttpPost("authorize")]
public async Task<IActionResult> AuthorizeQuote([FromBody] QuoteAuthorizationRequest request)
{
if (request == null || request.Items == null)
return BadRequest("No se recibió información válida para autorizar o anular.");
var result = await _quoteService.AuthorizeQuoteAsync(request.QuoteId, request.Items);
return result
? Ok(new { success = true, message = "Presupuesto procesado correctamente." })
: BadRequest(new { success = false, message = "No se pudo procesar el presupuesto." });
}
#endregion
} }
} }

View File

@ -1606,6 +1606,32 @@
], ],
"ReturnTypes": [] "ReturnTypes": []
}, },
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "GetById",
"RelativePath": "api/Quote/{id}",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "id",
"Type": "System.Int32",
"IsRequired": true
}
],
"ReturnTypes": [
{
"Type": "Domain.Dtos.QuoteDto",
"MediaTypes": [
"text/plain",
"application/json",
"text/json"
],
"StatusCode": 200
}
]
},
{ {
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController", "ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "GetQuotePdf", "Method": "GetQuotePdf",
@ -1622,6 +1648,22 @@
], ],
"ReturnTypes": [] "ReturnTypes": []
}, },
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "AuthorizeQuote",
"RelativePath": "api/Quote/authorize",
"HttpMethod": "POST",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "request",
"Type": "QuoteAuthorizationRequest",
"IsRequired": true
}
],
"ReturnTypes": []
},
{ {
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController", "ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "CreateFullQuote", "Method": "CreateFullQuote",

View File

@ -0,0 +1,13 @@
using Domain.Dtos;
namespace phronCare.UIBlazor.Models.Sales
{
public class QuoteAuthorizationViewItem : QuoteAuthorizationDto
{
public string ProductDescription { get; set; } = string.Empty;
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal Subtotal => Quantity * UnitPrice;
}
}

View File

@ -0,0 +1,227 @@
@page "/quotes/authorize/{QuoteId:int}"
@using Domain.Dtos
@using phronCare.UIBlazor.Models.Sales
@using phronCare.UIBlazor.Services.Sales.Quotes
@using Blazored.Modal
@using Blazored.Modal.Services
@inject QuoteService quoteService
@inject NavigationManager nav
@inject IToastService toast
@inject IModalService Modal
<div class="card" style="zoom: 80%">
<div class="card-header d-flex justify-content-center align-items-center">
<h3 class="card-title m-0">
<i class="fas fa-file-signature text-secondary me-2"></i> Autorización de Presupuesto
</h3>
</div>
<div class="card-body">
@if (IsLoading)
{
<p>Cargando información del presupuesto...</p>
}
else if (LoadError)
{
<div class="alert alert-danger">
Ocurrió un error al intentar cargar el presupuesto.
</div>
}
else
{
<div class="mb-4">
<div class="row">
<div class="col"><strong>N°:</strong> @QuoteHeader.Quotenumber</div>
<div class="col"><strong>Fecha:</strong> @QuoteHeader.IssueDate.ToShortDateString()</div>
<div class="col"><strong>Cliente:</strong> @QuoteHeader.CustomerName</div>
</div>
<div class="row mt-1">
<div class="col"><strong>Médico:</strong> @QuoteHeader.ProfessionalName</div>
<div class="col"><strong>Hospital:</strong> @QuoteHeader.InstitutionName</div>
<div class="col"><strong>Paciente:</strong> @QuoteHeader.PatientName</div>
</div>
<div class="row mt-1">
<div class="col"><strong>Cirugía estimada:</strong> @QuoteHeader.EstimatedDate?.ToShortDateString()</div>
<div class="col"><strong>Importe total:</strong> @QuoteHeader.Total.ToString("C")</div>
<div class="col">
<strong>Estado:</strong>
<span class="badge @GetStatusColor(QuoteHeader.Status)">
@QuoteHeader.Status
</span>
</div>
</div>
</div>
<EditForm Model="FormModel" OnValidSubmit="OpenConfirmation">
<div class="text-end mb-2">
@if (FormModel.Items.Any(i => i.Approved))
{
<span class="badge bg-success px-3 py-2"><i class="fas fa-check-circle me-1"></i> Al menos un ítem será autorizado</span>
}
else
{
<span class="badge bg-danger px-3 py-2"><i class="fas fa-ban me-1"></i> Todos los ítems serán anulados</span>
}
</div>
<table class="table table-sm table-bordered">
<thead class="table-light text-center">
<tr>
<th style="width: 40px;">¿Aprobar?</th>
<th style="width: 60%;">Descripción</th>
<th>Cant.</th>
<th>Precio U.</th>
<th>Subtotal</th>
<th>Cant. Aprob.</th>
<th>Precio Aprob.</th>
</tr>
</thead>
<tbody>
@foreach (var item in FormModel.Items)
{
<tr>
<td class="text-center">
<input type="checkbox" class="form-check-input"
@bind="item.Approved" />
</td>
<td>@item.ProductDescription</td>
<td class="text-center">@item.Quantity</td>
<td class="text-end">@item.UnitPrice.ToString("N2")</td>
<td class="text-end">@item.Subtotal.ToString("N2")</td>
<td class="text-center">
<InputNumber @bind-Value="item.ApprovedQuantity"
class="form-control form-control-sm text-center"
disabled="@(item.Approved == false)" />
</td>
<td class="text-end">
<InputNumber @bind-Value="item.ApprovedUnitPrice"
class="form-control form-control-sm text-end"
disabled="@(item.Approved == false)" />
</td>
</tr>
}
</tbody>
</table>
<div class="d-flex justify-content-end mt-3">
<button type="submit" class="btn text-white me-2 @(FormModel.Items.Any(i => i.Approved) ? "btn-success" : "btn-danger")">
@(FormModel.Items.Any(i => i.Approved) ? "Confirmar Autorización" : "Confirmar Anulación")
</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">Cancelar
</button>
</div>
</EditForm>
}
</div>
</div>
@code {
[Parameter] public int QuoteId { get; set; }
private QuoteDto? QuoteHeader;
private QuoteAuthorizationFormModel FormModel = new();
private bool IsLoading = true;
private bool LoadError = false;
protected override async Task OnInitializedAsync()
{
try
{
var quote = await quoteService.GetDtoByIdAsync(QuoteId);
if (quote?.Items == null || !quote.Items.Any())
{
LoadError = true;
return;
}
QuoteHeader = quote;
FormModel.Items = quote.Items.Select(x => new QuoteAuthorizationViewItem
{
Id = x.Id,
ProductDescription = x.Description,
Quantity = x.Quantity,
UnitPrice = x.UnitPrice,
Approved = false,
ApprovedQuantity = null,
ApprovedUnitPrice = null
}).ToList();
}
catch
{
LoadError = true;
}
finally
{
IsLoading = false;
}
}
private async Task OpenConfirmation()
{
var options = new ModalOptions()
{
HideHeader = true
};
var parameters = new ModalParameters();
parameters.Add("Message", "¿Desea continuar con esta operación?");
var modal = Modal.Show<Shared.Modals.ConfirmModal>("Confirmación", parameters,options);
var result = await modal.Result;
if (!result.Cancelled)
{
await AuthorizeQuote();
}
}
private async Task AuthorizeQuote()
{
var approvedItems = FormModel.Items
.Where(i => i.Approved)
.Select(i => new QuoteAuthorizationDto
{
Id = i.Id,
Approved = true,
ApprovedQuantity = i.ApprovedQuantity,
ApprovedUnitPrice = i.ApprovedUnitPrice
})
.ToList();
var payload = new QuoteAuthorizationRequest
{
QuoteId = QuoteId,
Items = approvedItems
};
var success = await quoteService.AuthorizeQuoteAsync(payload);
if (success)
{
if (!approvedItems.Any())
toast.ShowInfo("Presupuesto anulado correctamente.");
else
toast.ShowSuccess("Presupuesto autorizado con éxito.");
nav.NavigateTo("/quotes");
}
else
{
toast.ShowError("No se pudo procesar el presupuesto.");
}
}
private void Cancel() => nav.NavigateTo("/quotes");
private string GetStatusColor(string status) => status.ToLower() switch
{
"Anulado" => "bg-danger text-white",
"Emitido" => "bg-primary text-white",
"Aprobado" => "bg-success",
"Despacho" => "bg-info text-white",
"SinConsumo" => "bg-warning text-dark",
"Transito" => "bg-secondary text-white",
"Cerrado" => "bg-dark text-white",
_ => "bg-light text-dark"
};
public class QuoteAuthorizationFormModel
{
public List<QuoteAuthorizationViewItem> Items { get; set; } = new();
}
}

View File

@ -1,4 +1,4 @@
@page "/quote/create" @page "/quotes/create"
@using System.Globalization; @using System.Globalization;
@using System.Net.Http.Json @using System.Net.Http.Json
@using Blazored.Typeahead @using Blazored.Typeahead

View File

@ -87,13 +87,13 @@
<th>Total</th> <th>Total</th>
<th>Estado</th> <th>Estado</th>
<th>Vendedor</th> <th>Vendedor</th>
<th>Acciones</th> <th style="width:80px;">Acciones</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var quote in PagedQuotes.Items) @foreach (var quote in PagedQuotes.Items)
{ {
<tr> <tr class="text-center">
<td>@quote.Quotenumber</td> <td>@quote.Quotenumber</td>
<td>@quote.IssueDate.ToString("dd/MM/yyyy")</td> <td>@quote.IssueDate.ToString("dd/MM/yyyy")</td>
<td>@(quote.EstimatedDate.HasValue ? quote.EstimatedDate.Value.ToString("dd/MM/yyyy") : "—")</td> <td>@(quote.EstimatedDate.HasValue ? quote.EstimatedDate.Value.ToString("dd/MM/yyyy") : "—")</td>
@ -108,9 +108,17 @@
<span class="badge @GetStatusBadge(quote.Status)">@quote.Status</span> <span class="badge @GetStatusBadge(quote.Status)">@quote.Status</span>
</td> </td>
<td>@quote.SalespersonName</td> <td>@quote.SalespersonName</td>
<td> <td class="text-center align-middle">
<button class="btn btn-link btn-lg p-0 text-success ms-2" @onclick="() => ToggleDetail(quote)"><i class="fas fa-eye"></i></button> <button class="btn btn-link btn-lg p-0 text-primary ms-2" title="Ver detalle" @onclick="() => ToggleDetail(quote)"><i class="fas fa-eye"></i></button>
<button class="btn btn-link btn-lg p-0 text-primary ms-2" @onclick="() => PrintPdf(quote.Id,quote.Quotenumber)"><i class="fas fa-print"></i></button> <button class="btn btn-link btn-lg p-0 text-success ms-2" title="Generar PDF" @onclick="() => PrintPdf(quote.Id, quote.Quotenumber)"><i class="fas fa-print"></i></button>
@if (quote.Status?.ToLower() == "emitido")
{
<button class="btn btn-link btn-lg p-0 text-danger ms-2"
title="Autorizar presupuesto"
@onclick="() => Autorizar(quote.Id)">
<i class="fas fa-check-circle"></i>
</button>
}
</td> </td>
</tr> </tr>
} }
@ -359,7 +367,7 @@
private void Create() private void Create()
{ {
Navigation.NavigateTo("/quote/create/"); Navigation.NavigateTo("/quotes/create/");
} }
private void OnClear() private void OnClear()
{ {
@ -369,6 +377,7 @@
} }
private string GetStatusBadge(string status) => status switch private string GetStatusBadge(string status) => status switch
{ {
"Anulado" => "bg-danger text-white",
"Emitido" => "bg-primary text-white", "Emitido" => "bg-primary text-white",
"Aprobado" => "bg-success", "Aprobado" => "bg-success",
"Despacho" => "bg-info text-white", "Despacho" => "bg-info text-white",
@ -389,4 +398,9 @@
toastService.ShowError(ex.Message); toastService.ShowError(ex.Message);
} }
} }
private void Autorizar(int id)
{
Navigation.NavigateTo($"/quotes/authorize/{id}");
}
} }

View File

@ -124,7 +124,34 @@ namespace phronCare.UIBlazor.Services.Sales.Quotes
throw new Exception($"ExportPdfAsync: {message}", ex); throw new Exception($"ExportPdfAsync: {message}", ex);
} }
} }
public async Task<bool> AuthorizeQuoteAsync(QuoteAuthorizationRequest request)
{
var response = await _http.PostAsJsonAsync("/api/quote/authorize", request);
if (!response.IsSuccessStatusCode)
{
var serverMessage = await response.Content.ReadAsStringAsync();
throw new Exception($"Error al autorizar el presupuesto: {serverMessage}");
}
return true;
}
/// <summary>
/// Obtiene un presupuesto completo por ID para su visualización y autorización.
/// </summary>
public async Task<QuoteDto?> GetDtoByIdAsync(int id)
{
try
{
var result = await _http.GetFromJsonAsync<QuoteDto>($"/api/quote/{id}");
return result;
}
catch (Exception ex)
{
Console.WriteLine($"Error al obtener QuoteDto por ID: {ex.Message}");
return null;
}
}
} }
public class CreateQuoteResult public class CreateQuoteResult