Add Authorization Quote
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 7m16s
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 7m16s
This commit is contained in:
parent
69ea0a6a98
commit
e276e9672c
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
10
Domain/Dtos/QuoteAuthorizationDto.cs
Normal file
10
Domain/Dtos/QuoteAuthorizationDto.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Domain/Dtos/QuoteAuthorizationRequest.cs
Normal file
7
Domain/Dtos/QuoteAuthorizationRequest.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
using Domain.Dtos;
|
||||||
|
|
||||||
|
public class QuoteAuthorizationRequest
|
||||||
|
{
|
||||||
|
public int QuoteId { get; set; }
|
||||||
|
public List<QuoteAuthorizationDto>? Items { get; set; } = new();
|
||||||
|
}
|
||||||
@ -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.
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Descripción modificable del producto (puede diferir del original)
|
/// Descripción modificable del producto (puede diferir del original)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ProductDescription { get; set; }=String.Empty;
|
public string? ProductDescription { get; set; } = String.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cantidad
|
/// Cantidad
|
||||||
@ -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!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.)
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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.)
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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",
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
227
phronCare.UIBlazor/Pages/Sales/Quotes/QuoteAuthorize.razor
Normal file
227
phronCare.UIBlazor/Pages/Sales/Quotes/QuoteAuthorize.razor
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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}");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user