Add Save Cascade Quote Insert
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 5m39s

with Validations
This commit is contained in:
Leandro Hernan Rojas 2025-05-07 18:35:49 -03:00
parent 09090bef21
commit 3d6af27e34
20 changed files with 549 additions and 720 deletions

View File

@ -20,29 +20,14 @@ namespace Models.Interfaces
string? status,
int page = 1,
int pageSize = 50);
Task<EQuoteHeader> CreateQuoteAsync(EQuoteHeader quote, int formSeriesId);
Task UpdateQuoteAsync(EQuoteHeader quote);
Task DeleteQuoteAsync(int id);
#endregion
#region Ajustes
Task<IEnumerable<EQuoteAdjustment>> GetAdjustmentsByQuoteIdAsync(int quoteId);
Task<EQuoteAdjustment> AddAdjustmentAsync(EQuoteAdjustment adjustment);
Task UpdateAdjustmentAsync(EQuoteAdjustment adjustment);
Task DeleteAdjustmentAsync(int adjustmentId);
#endregion
#region Impuestos
Task<IEnumerable<EQuoteTax>> GetTaxesByQuoteIdAsync(int quoteId);
Task<EQuoteTax> AddTaxAsync(EQuoteTax tax);
Task UpdateTaxAsync(EQuoteTax tax);
Task DeleteTaxAsync(int taxId);
#endregion
#region Exportación
Task<byte[]> ExportFilteredQuotesToExcelAsync(QuoteSearchParams searchParams);
#endregion
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
#endregion
}
}

View File

@ -9,16 +9,12 @@ namespace PhronCare.Core.Services.Sales
{
public class QuoteService(
IPhSQuoteHeaderRepository quoteHeaderRepository,
IPhSQuoteDetailRepository quoteDetailRepository,
IPhSQuoteRoleRepository quoteRoleRepository,
IPhSFormSeriesRepository formSeriesRepository
) : IQuoteDom
IPhSQuoteRepository quoteRepository
) : IQuoteDom
{
#region Declaraciones
private readonly IPhSQuoteHeaderRepository _quoteHeaderRepository = quoteHeaderRepository;
private readonly IPhSQuoteDetailRepository _quoteDetailRepository = quoteDetailRepository;
private readonly IPhSQuoteRoleRepository _quoteRoleRepository = quoteRoleRepository;
private readonly IPhSFormSeriesRepository _formSeriesRepository = formSeriesRepository;
private readonly IPhSQuoteRepository _quoteRepository = quoteRepository;
#endregion
#region Presupuestos
@ -26,17 +22,14 @@ namespace PhronCare.Core.Services.Sales
{
return await _quoteHeaderRepository.GetAllAsync(page, pageSize);
}
public async Task<EQuoteHeader?> GetQuoteByIdAsync(int id)
{
return await _quoteHeaderRepository.GetByIdAsync(id);
}
public async Task<IEnumerable<EQuoteHeader>> GetQuotesByCustomerAsync(int customerId)
{
return await _quoteHeaderRepository.GetByCustomerIdAsync(customerId);
}
public async Task<PagedResult<EQuoteHeader>> SearchQuotesAsync(
int? customerId,
string? quoteNumber,
@ -61,94 +54,16 @@ namespace PhronCare.Core.Services.Sales
page,
pageSize);
}
public async Task<EQuoteHeader> CreateQuoteAsync(EQuoteHeader quote, int formSeriesId)
{
// Obtener el próximo número de documento
var nextNumber = await _formSeriesRepository.GetNextInternalNumberAsync(formSeriesId);
quote.Quotenumber = nextNumber.ToString();
// Crear encabezado
var newQuote = await _quoteHeaderRepository.AddAsync(quote);
// Crear detalles asociados
if (quote.PhSQuoteDetails != null)
{
foreach (var detail in quote.PhSQuoteDetails)
{
detail.QuoteheaderId = newQuote.Id;
await _quoteDetailRepository.AddAsync(detail);
}
}
// Crear roles asociados
if (quote.PhSQuoteRoles != null)
{
foreach (var role in quote.PhSQuoteRoles)
{
role.QuoteheaderId = newQuote.Id;
await _quoteRoleRepository.AddAsync(role);
}
}
return newQuote;
}
public async Task UpdateQuoteAsync(EQuoteHeader quote)
{
await _quoteHeaderRepository.UpdateAsync(quote);
}
public async Task DeleteQuoteAsync(int id)
{
await _quoteHeaderRepository.DeleteAsync(id);
}
#endregion
#region Ajustes
public async Task<IEnumerable<EQuoteAdjustment>> GetAdjustmentsByQuoteIdAsync(int quoteId)
{
return await _quoteHeaderRepository.GetAdjustmentsByQuoteIdAsync(quoteId);
}
public async Task<EQuoteAdjustment> AddAdjustmentAsync(EQuoteAdjustment adjustment)
{
return await _quoteHeaderRepository.AddAdjustmentAsync(adjustment);
}
public async Task UpdateAdjustmentAsync(EQuoteAdjustment adjustment)
{
await _quoteHeaderRepository.UpdateAdjustmentAsync(adjustment);
}
public async Task DeleteAdjustmentAsync(int adjustmentId)
{
await _quoteHeaderRepository.DeleteAdjustmentAsync(adjustmentId);
}
#endregion
#region Impuestos
public async Task<IEnumerable<EQuoteTax>> GetTaxesByQuoteIdAsync(int quoteId)
{
return await _quoteHeaderRepository.GetTaxesByQuoteIdAsync(quoteId);
}
public async Task<EQuoteTax> AddTaxAsync(EQuoteTax tax)
{
return await _quoteHeaderRepository.AddTaxAsync(tax);
}
public async Task UpdateTaxAsync(EQuoteTax tax)
{
await _quoteHeaderRepository.UpdateTaxAsync(tax);
}
public async Task DeleteTaxAsync(int taxId)
{
await _quoteHeaderRepository.DeleteTaxAsync(taxId);
}
#endregion
#region Exportación
public async Task<byte[]> ExportFilteredQuotesToExcelAsync(QuoteSearchParams searchParams)
{
@ -198,66 +113,65 @@ namespace PhronCare.Core.Services.Sales
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
public async Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId)
{
using var transaction = await _quoteHeaderRepository.BeginTransactionAsync();
try
{
// Obtener el próximo número de presupuesto desde la serie
var nextNumber = await _formSeriesRepository.GetNextInternalNumberAsync(formSeriesId);
quote.Quotenumber = nextNumber.ToString();
// Crear encabezado principal
var headerEntity = await _quoteHeaderRepository.AddAsync(quote);
// Crear detalles
if (quote.PhSQuoteDetails?.Any() == true)
{
foreach (var detail in quote.PhSQuoteDetails)
{
detail.QuoteheaderId = headerEntity.Id;
await _quoteDetailRepository.AddAsync(detail);
}
}
// Crear roles
if (quote.PhSQuoteRoles?.Any() == true)
{
foreach (var role in quote.PhSQuoteRoles)
{
role.QuoteheaderId = headerEntity.Id;
await _quoteRoleRepository.AddAsync(role);
}
}
// Crear ajustes (rebajas)
if (quote.PhSQuoteAdjustments?.Any() == true)
{
foreach (var adj in quote.PhSQuoteAdjustments)
{
adj.QuoteheaderId = headerEntity.Id;
await _quoteHeaderRepository.AddAdjustmentAsync(adj);
}
}
// Crear impuestos
if (quote.PhSQuoteTaxes?.Any() == true)
{
foreach (var tax in quote.PhSQuoteTaxes)
{
tax.QuoteheaderId = headerEntity.Id;
await _quoteHeaderRepository.AddTaxAsync(tax);
}
}
await transaction.CommitAsync();
return headerEntity.Quotenumber;
}
catch
{
await transaction.RollbackAsync();
throw;
}
// 1. Validaciones antes de iniciar transacción
ValidateQuote(quote);
return await _quoteRepository.CreateFullQuoteAsync(quote, formSeriesId);
}
#endregion
#region Validaciones QuoteCreate
private void ValidateQuote(EQuoteHeader quote)
{
if (quote == null)
throw new ArgumentNullException(nameof(quote), "El presupuesto no puede ser nulo.");
if (quote.CustomerId <= 0)
throw new ArgumentException("Debe seleccionar un cliente.");
if (quote.PeopleId <= 0)
throw new ArgumentException("Debe seleccionar un vendedor.");
if (string.IsNullOrWhiteSpace(quote.Currency))
throw new ArgumentException("La moneda es obligatoria.");
if (quote.PhSQuoteDetails == null || !quote.PhSQuoteDetails.Any())
throw new InvalidOperationException("Debe incluir al menos un producto.");
foreach (var detail in quote.PhSQuoteDetails)
{
if (detail.Quantity <= 0)
throw new ArgumentException($"La cantidad para el producto {detail.ProductId} debe ser mayor a cero.");
if (detail.Unitprice < 0)
throw new ArgumentException($"El precio unitario del producto {detail.ProductId} no puede ser negativo.");
}
if (quote.PhSQuoteRoles == null || !quote.PhSQuoteRoles.Any())
throw new InvalidOperationException("Debe asignar al menos un rol (profesional, paciente o institución).");
var hasProfessional = quote.PhSQuoteRoles.Any(r => r.Entitytype == PhSEntityTypes.Professional);
var hasPatient = quote.PhSQuoteRoles.Any(r => r.Entitytype == PhSEntityTypes.Patient);
var hasInstitution = quote.PhSQuoteRoles.Any(r => r.Entitytype == PhSEntityTypes.Institution);
if (!hasProfessional)
throw new InvalidOperationException("Debe asignar un profesional.");
if (!hasInstitution)
throw new InvalidOperationException("Debe asignar un paciente.");
if (!hasPatient)
throw new InvalidOperationException("Debe asignar un paciente.");
if (quote.PhSQuoteTaxes != null)
{
foreach (var tax in quote.PhSQuoteTaxes)
{
if (tax.Taxrate < 0 || tax.Taxrate > 100)
throw new ArgumentException($"La alícuota del impuesto '{tax.Taxname}' no es válida.");
}
}
if (quote.Total < 0)
throw new ArgumentException("El total del presupuesto no puede ser negativo.");
}
#endregion
}
}

View File

@ -18,7 +18,7 @@
/// <summary>
/// Código del motivo de ajuste (FK a PhS_AdjustmentReasons)
/// </summary>
public string ReasonCode { get; set; } = null!;
public string ReasonCode { get; set; } = String.Empty;
/// <summary>
/// Importe del ajuste realizado (positivo o nulo)
@ -28,7 +28,7 @@
/// <summary>
/// Descripción adicional del ajuste
/// </summary>
public string? Description { get; set; }
public string? Description { get; set; } = String.Empty;
/// <summary>
/// Fecha de registro del ajuste

View File

@ -20,7 +20,7 @@
/// <summary>
/// Descripción modificable del producto (puede diferir del original)
/// </summary>
public string? ProductDescription { get; set; }
public string? ProductDescription { get; set; }=String.Empty;
/// <summary>
/// Cantidad
@ -52,8 +52,8 @@
/// </summary>
public DateTime? Modifiedat { get; set; }
public virtual EProduct Product { get; set; } = null!;
//public virtual EProduct Product { get; set; } = null!;
public virtual EQuoteHeader PhSQuoteheader { get; set; } = null!;
//public virtual EQuoteHeader PhSQuoteheader { get; set; } = null!;
}
}

View File

@ -1,4 +1,6 @@
namespace Domain.Entities
using System.Text.Json.Serialization;
namespace Domain.Entities
{
/// <summary>
/// Tabla de cabeceras de presupuestos
@ -19,7 +21,8 @@
/// <summary>
/// Número visible del presupuesto
/// </summary>
public string Quotenumber { get; set; } = null!;
public string Quotenumber { get; set; } = String.Empty;
/// <summary>
/// Cliente asociado
@ -54,17 +57,17 @@
/// <summary>
/// Código de moneda pactada (ISO 4217). Ej: ARS, USD
/// </summary>
public string Currency { get; set; } = null!;
public string Currency { get; set; }= String.Empty;
/// <summary>
/// Tipo de cambio pactado para conversión a pesos argentinos
/// </summary>
public decimal? Exchangerate { get; set; }
public decimal Exchangerate { get; set; }
/// <summary>
/// Importe neto antes de aplicar impuestos, expresado en la moneda pactada del presupuesto
/// </summary>
public decimal? Netamount { get; set; }
public decimal Netamount { get; set; }
/// <summary>
/// Importe total del presupuesto expresado en la moneda pactada (extranjera), incluyendo impuestos y ajustes comerciales
@ -84,7 +87,7 @@
/// <summary>
/// Estado: E (Emitido), A (Aprobado), AC (Aprobado para cirugia), etc.
/// </summary>
public string Status { get; set; } = null!;
public string Status { get; set; } = String.Empty;
/// <summary>
/// Indica si la cirugía se realizará fuera de la ciudad/localidad habitual (“out of town”)
@ -94,7 +97,7 @@
/// <summary>
/// Instrucción dirigida al área de logística para detallar qué debe prepararse o despacharse (ej: “CMF 1.5 + INSTRUMENTAL”)
/// </summary>
public string? DispatchInstruction { get; set; }
public string? DispatchInstruction { get; set; } = String.Empty;
/// <summary>
/// Cantidad de impresiones
@ -104,7 +107,7 @@
/// <summary>
/// Observaciones internas
/// </summary>
public string? Observations { get; set; }
public string? Observations { get; set; } = String.Empty;
/// <summary>
/// Fecha de creación
@ -115,13 +118,9 @@
/// Fecha de modificación
/// </summary>
public DateTime? Modifiedat { get; set; }
public virtual ICollection<EQuoteAdjustment> PhSQuoteAdjustments { get; set; } = new List<EQuoteAdjustment>();
public virtual ICollection<EQuoteDetail> PhSQuoteDetails { get; set; } = new List<EQuoteDetail>();
public virtual ICollection<EQuoteRole> PhSQuoteRoles { get; set; } = new List<EQuoteRole>();
public virtual ICollection<EQuoteTax> PhSQuoteTaxes { get; set; } = new List<EQuoteTax>();
}
}

View File

@ -21,7 +21,7 @@ namespace Domain.Entities
/// <summary>
/// Tipo de entidad asociada (Ej: PhS_Professionals, PhS_Institutions, PhS_Patients)
/// </summary>
public string Entitytype { get; set; } = null!;
public string Entitytype { get; set; } = String.Empty;
/// <summary>
/// ID de la entidad asociada

View File

@ -15,12 +15,12 @@
/// <summary>
/// Nombre descriptivo del impuesto
/// </summary>
public string Taxname { get; set; } = null!;
public string Taxname { get; set; } = String.Empty;
/// <summary>
/// Código o identificador oficial del impuesto (ej. AFIP)
/// </summary>
public string? Taxcode { get; set; }
public string? Taxcode { get; set; } = String.Empty;
/// <summary>
/// Base imponible del impuesto (importe gravado)

View File

@ -1,7 +1,10 @@
namespace Models.Interfaces
using Models.Models;
namespace Models.Interfaces
{
public interface IPhSFormSeriesRepository
{
Task<PhSFormSeries?> GetByIdAsync(int formSeriesId);
Task<int> GetNextInternalNumberAsync(int formSeriesId);
}
}

View File

@ -1,6 +1,5 @@
using Domain.Entities;
using Domain.Generics;
using Microsoft.EntityFrameworkCore.Storage;
namespace Models.Interfaces
{
@ -12,36 +11,34 @@ namespace Models.Interfaces
Task<PagedResult<EQuoteHeader>> SearchAsync(int? customerId, string? quoteNumber, int? professionalId,
int? institutionId, int? patientId, DateTime? issueDateFrom, DateTime? issueDateTo, string? status,
int page = 1, int pageSize = 50);
Task<EQuoteHeader> AddAsync(EQuoteHeader quoteHeader);
Task UpdateAsync(EQuoteHeader quoteHeader);
Task DeleteAsync(int id);
// Ajustes
Task<IEnumerable<EQuoteAdjustment>> GetAdjustmentsByQuoteIdAsync(int quoteId);
Task<EQuoteAdjustment> AddAdjustmentAsync(EQuoteAdjustment adjustment);
Task UpdateAdjustmentAsync(EQuoteAdjustment adjustment);
Task DeleteAdjustmentAsync(int adjustmentId);
/// <summary>
/// Obtiene todos los impuestos asociados a un presupuesto dado por su ID.
/// </summary>
Task<IEnumerable<EQuoteTax>> GetTaxesByQuoteIdAsync(int quoteId);
//Task<IEnumerable<EQuoteAdjustment>> GetAdjustmentsByQuoteIdAsync(int quoteId);
//Task<EQuoteAdjustment> AddAdjustmentAsync(EQuoteAdjustment adjustment);
//Task UpdateAdjustmentAsync(EQuoteAdjustment adjustment);
//Task DeleteAdjustmentAsync(int adjustmentId);
///// <summary>
///// Obtiene todos los impuestos asociados a un presupuesto dado por su ID.
///// </summary>
//Task<IEnumerable<EQuoteTax>> GetTaxesByQuoteIdAsync(int quoteId);
/// <summary>
/// Agrega un nuevo impuesto al presupuesto correspondiente.
/// </summary>
Task<EQuoteTax> AddTaxAsync(EQuoteTax tax);
///// <summary>
///// Agrega un nuevo impuesto al presupuesto correspondiente.
///// </summary>
//Task<EQuoteTax> AddTaxAsync(EQuoteTax tax);
/// <summary>
/// Actualiza los datos de un impuesto existente en un presupuesto.
/// </summary>
Task UpdateTaxAsync(EQuoteTax tax);
///// <summary>
///// Actualiza los datos de un impuesto existente en un presupuesto.
///// </summary>
//Task UpdateTaxAsync(EQuoteTax tax);
/// <summary>
/// Elimina un impuesto asociado a un presupuesto a partir de su ID.
/// </summary>
Task DeleteTaxAsync(int taxId);
Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
Task<IDbContextTransaction> BeginTransactionAsync();
///// <summary>
///// Elimina un impuesto asociado a un presupuesto a partir de su ID.
///// </summary>
//Task DeleteTaxAsync(int taxId);
//Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
//Task<IDbContextTransaction> BeginTransactionAsync();
}
}

View File

@ -0,0 +1,9 @@
using Domain.Entities;
namespace Models.Interfaces
{
public interface IPhSQuoteRepository
{
Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
}
}

View File

@ -29,7 +29,12 @@ namespace PhronCare.Core.Data.Repositories.Sales
return (int)nextNumberParam.Value;
}
public async Task<PhSFormSeries?> GetByIdAsync(int formSeriesId)
{
return await _context.PhSFormSeries
.AsNoTracking()
.FirstOrDefaultAsync(s => s.Id == formSeriesId);
}
#endregion
}
}

View File

@ -1,17 +1,17 @@
using Domain.Entities;
using Domain.Generics;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Models.Helpers;
using Microsoft.EntityFrameworkCore;
using Models.Interfaces;
using Models.Helpers;
using Models.Models;
using Domain.Entities;
using Domain.Generics;
namespace PhronCare.Core.Data.Repositories.Sales
{
public class PhSQuoteHeaderRepository(PhronCareOperationsHubContext context, IPhSFormSeriesRepository formSeriesRepository) : IPhSQuoteHeaderRepository
public class PhSQuoteHeaderRepository(PhronCareOperationsHubContext context) : IPhSQuoteHeaderRepository
{
private readonly PhronCareOperationsHubContext _context = context;
private readonly IPhSFormSeriesRepository _formSeriesRepository = formSeriesRepository;
//private readonly IPhSFormSeriesRepository _formSeriesRepository = formSeriesRepository;
#region Metodos
public async Task<PagedResult<EQuoteHeader>> GetAllAsync(int page = 1, int pageSize = 50)
{
var query = _context.PhSQuoteHeaders
@ -100,13 +100,7 @@ namespace PhronCare.Core.Data.Repositories.Sales
PageSize = pagedEntities.PageSize
};
}
public async Task<EQuoteHeader> AddAsync(EQuoteHeader quoteHeader)
{
var dbEntity = EntityMapper.MapEntity<EQuoteHeader, PhSQuoteHeader>(quoteHeader);
_context.PhSQuoteHeaders.Add(dbEntity);
await _context.SaveChangesAsync();
return EntityMapper.MapEntity<PhSQuoteHeader, EQuoteHeader>(dbEntity);
}
public async Task UpdateAsync(EQuoteHeader quoteHeader)
{
var dbEntity = EntityMapper.MapEntity<EQuoteHeader, PhSQuoteHeader>(quoteHeader);
@ -122,172 +116,173 @@ namespace PhronCare.Core.Data.Repositories.Sales
await _context.SaveChangesAsync();
}
}
#endregion
#region <DECRECATED>
// ----------------------------
// Métodos para Ajustes
// ----------------------------
public async Task<IEnumerable<EQuoteAdjustment>> GetAdjustmentsByQuoteIdAsync(int quoteId)
{
var adjustments = await _context.PhSQuoteAdjustments
.Where(a => a.QuoteheaderId == quoteId)
.ToListAsync();
//public async Task<IEnumerable<EQuoteAdjustment>> GetAdjustmentsByQuoteIdAsync(int quoteId)
//{
// var adjustments = await _context.PhSQuoteAdjustments
// .Where(a => a.QuoteheaderId == quoteId)
// .ToListAsync();
return adjustments.Select(EntityMapper.MapEntity<PhSQuoteAdjustment, EQuoteAdjustment>);
}
public async Task<EQuoteAdjustment> AddAdjustmentAsync(EQuoteAdjustment adjustment)
{
var dbEntity = EntityMapper.MapEntity<EQuoteAdjustment, PhSQuoteAdjustment>(adjustment);
_context.PhSQuoteAdjustments.Add(dbEntity);
await _context.SaveChangesAsync();
return EntityMapper.MapEntity<PhSQuoteAdjustment, EQuoteAdjustment>(dbEntity);
}
public async Task UpdateAdjustmentAsync(EQuoteAdjustment adjustment)
{
var dbEntity = EntityMapper.MapEntity<EQuoteAdjustment, PhSQuoteAdjustment>(adjustment);
_context.PhSQuoteAdjustments.Update(dbEntity);
await _context.SaveChangesAsync();
}
public async Task DeleteAdjustmentAsync(int adjustmentId)
{
var entity = await _context.PhSQuoteAdjustments.FindAsync(adjustmentId);
if (entity != null)
{
_context.PhSQuoteAdjustments.Remove(entity);
await _context.SaveChangesAsync();
}
}
// return adjustments.Select(EntityMapper.MapEntity<PhSQuoteAdjustment, EQuoteAdjustment>);
//}
//public async Task<EQuoteAdjustment> AddAdjustmentAsync(EQuoteAdjustment adjustment)
//{
// var dbEntity = EntityMapper.MapEntity<EQuoteAdjustment, PhSQuoteAdjustment>(adjustment);
// _context.PhSQuoteAdjustments.Add(dbEntity);
// await _context.SaveChangesAsync();
// return EntityMapper.MapEntity<PhSQuoteAdjustment, EQuoteAdjustment>(dbEntity);
//}
//public async Task UpdateAdjustmentAsync(EQuoteAdjustment adjustment)
//{
// var dbEntity = EntityMapper.MapEntity<EQuoteAdjustment, PhSQuoteAdjustment>(adjustment);
// _context.PhSQuoteAdjustments.Update(dbEntity);
// await _context.SaveChangesAsync();
//}
//public async Task DeleteAdjustmentAsync(int adjustmentId)
//{
// var entity = await _context.PhSQuoteAdjustments.FindAsync(adjustmentId);
// if (entity != null)
// {
// _context.PhSQuoteAdjustments.Remove(entity);
// await _context.SaveChangesAsync();
// }
//}
// ----------------------------
// Métodos para Impuestos
// ----------------------------
//// ----------------------------
//// Métodos para Impuestos
//// ----------------------------
/// <summary>
/// Obtiene todos los impuestos asociados a un presupuesto dado por su ID.
/// </summary>
public async Task<IEnumerable<EQuoteTax>> GetTaxesByQuoteIdAsync(int quoteId)
{
var taxes = await _context.PhSQuoteTaxes
.Where(t => t.QuoteheaderId == quoteId)
.ToListAsync();
///// <summary>
///// Obtiene todos los impuestos asociados a un presupuesto dado por su ID.
///// </summary>
//public async Task<IEnumerable<EQuoteTax>> GetTaxesByQuoteIdAsync(int quoteId)
//{
// var taxes = await _context.PhSQuoteTaxes
// .Where(t => t.QuoteheaderId == quoteId)
// .ToListAsync();
return taxes.Select(EntityMapper.MapEntity<PhSQuoteTaxis, EQuoteTax>);
}
// return taxes.Select(EntityMapper.MapEntity<PhSQuoteTaxis, EQuoteTax>);
//}
/// <summary>
/// Agrega un nuevo impuesto al presupuesto correspondiente.
/// </summary>
public async Task<EQuoteTax> AddTaxAsync(EQuoteTax tax)
{
var dbEntity = EntityMapper.MapEntity<EQuoteTax, PhSQuoteTaxis>(tax);
_context.PhSQuoteTaxes.Add(dbEntity);
await _context.SaveChangesAsync();
return EntityMapper.MapEntity<PhSQuoteTaxis, EQuoteTax>(dbEntity);
}
///// <summary>
///// Agrega un nuevo impuesto al presupuesto correspondiente.
///// </summary>
//public async Task<EQuoteTax> AddTaxAsync(EQuoteTax tax)
//{
// var dbEntity = EntityMapper.MapEntity<EQuoteTax, PhSQuoteTaxis>(tax);
// _context.PhSQuoteTaxes.Add(dbEntity);
// await _context.SaveChangesAsync();
// return EntityMapper.MapEntity<PhSQuoteTaxis, EQuoteTax>(dbEntity);
//}
/// <summary>
/// Actualiza los datos de un impuesto existente en un presupuesto.
/// </summary>
public async Task UpdateTaxAsync(EQuoteTax tax)
{
var dbEntity = EntityMapper.MapEntity<EQuoteTax, PhSQuoteTaxis>(tax);
_context.PhSQuoteTaxes.Update(dbEntity);
await _context.SaveChangesAsync();
}
///// <summary>
///// Actualiza los datos de un impuesto existente en un presupuesto.
///// </summary>
//public async Task UpdateTaxAsync(EQuoteTax tax)
//{
// var dbEntity = EntityMapper.MapEntity<EQuoteTax, PhSQuoteTaxis>(tax);
// _context.PhSQuoteTaxes.Update(dbEntity);
// await _context.SaveChangesAsync();
//}
/// <summary>
/// Elimina un impuesto asociado a un presupuesto a partir de su ID.
/// </summary>
public async Task DeleteTaxAsync(int taxId)
{
var entity = await _context.PhSQuoteTaxes.FindAsync(taxId);
if (entity != null)
{
_context.PhSQuoteTaxes.Remove(entity);
await _context.SaveChangesAsync();
}
}
///// <summary>
///// Elimina un impuesto asociado a un presupuesto a partir de su ID.
///// </summary>
//public async Task DeleteTaxAsync(int taxId)
//{
// var entity = await _context.PhSQuoteTaxes.FindAsync(taxId);
// if (entity != null)
// {
// _context.PhSQuoteTaxes.Remove(entity);
// await _context.SaveChangesAsync();
// }
//}
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
/// <summary>
/// Crea un nuevo presupuesto, incluyendo encabezado, detalles, roles, ajustes e impuestos asociados.
/// Genera automáticamente el número de presupuesto en base a la serie indicada.
/// </summary>
/// <param name="quote">Presupuesto a registrar, incluyendo entidades relacionadas.</param>
/// <param name="formSeriesId">Identificador de la serie de numeración a utilizar.</param>
/// <returns>Cadena con el número generado del presupuesto.</returns>
public async Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId)
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Obtener el próximo número de presupuesto desde SP
var nextNumber = await _formSeriesRepository.GetNextInternalNumberAsync(formSeriesId);
quote.Quotenumber = nextNumber.ToString();
//#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
///// <summary>
///// Crea un nuevo presupuesto, incluyendo encabezado, detalles, roles, ajustes e impuestos asociados.
///// Genera automáticamente el número de presupuesto en base a la serie indicada.
///// </summary>
///// <param name="quote">Presupuesto a registrar, incluyendo entidades relacionadas.</param>
///// <param name="formSeriesId">Identificador de la serie de numeración a utilizar.</param>
///// <returns>Cadena con el número generado del presupuesto.</returns>
//public async Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId)
//{
// using var transaction = await _context.Database.BeginTransactionAsync();
// try
// {
// // Obtener el próximo número de presupuesto desde SP
// var nextNumber = await _formSeriesRepository.GetNextInternalNumberAsync(formSeriesId);
// quote.Quotenumber = nextNumber.ToString();
// Map y guardado de Header
var headerEntity = EntityMapper.MapEntity<EQuoteHeader, PhSQuoteHeader>(quote);
_context.PhSQuoteHeaders.Add(headerEntity);
await _context.SaveChangesAsync();
// // Map y guardado de Header
// var headerEntity = EntityMapper.MapEntity<EQuoteHeader, PhSQuoteHeader>(quote);
// _context.PhSQuoteHeaders.Add(headerEntity);
// await _context.SaveChangesAsync();
// Guardado de Detalles
if (quote.PhSQuoteDetails?.Any() == true)
{
foreach (var detail in quote.PhSQuoteDetails)
{
detail.QuoteheaderId = headerEntity.Id;
var dbDetail = EntityMapper.MapEntity<EQuoteDetail, PhSQuoteDetail>(detail);
_context.PhSQuoteDetails.Add(dbDetail);
}
}
// // Guardado de Detalles
// if (quote.PhSQuoteDetails?.Any() == true)
// {
// foreach (var detail in quote.PhSQuoteDetails)
// {
// detail.QuoteheaderId = headerEntity.Id;
// var dbDetail = EntityMapper.MapEntity<EQuoteDetail, PhSQuoteDetail>(detail);
// _context.PhSQuoteDetails.Add(dbDetail);
// }
// }
// Guardado de Roles
if (quote.PhSQuoteRoles?.Any() == true)
{
foreach (var role in quote.PhSQuoteRoles)
{
role.QuoteheaderId = headerEntity.Id;
var dbRole = EntityMapper.MapEntity<EQuoteRole, PhSQuoteRole>(role);
_context.PhSQuoteRoles.Add(dbRole);
}
}
// // Guardado de Roles
// if (quote.PhSQuoteRoles?.Any() == true)
// {
// foreach (var role in quote.PhSQuoteRoles)
// {
// role.QuoteheaderId = headerEntity.Id;
// var dbRole = EntityMapper.MapEntity<EQuoteRole, PhSQuoteRole>(role);
// _context.PhSQuoteRoles.Add(dbRole);
// }
// }
// Guardado de Ajustes
if (quote.PhSQuoteAdjustments?.Any() == true)
{
foreach (var adj in quote.PhSQuoteAdjustments)
{
adj.QuoteheaderId = headerEntity.Id;
var dbAdj = EntityMapper.MapEntity<EQuoteAdjustment, PhSQuoteAdjustment>(adj);
_context.PhSQuoteAdjustments.Add(dbAdj);
}
}
// // Guardado de Ajustes
// if (quote.PhSQuoteAdjustments?.Any() == true)
// {
// foreach (var adj in quote.PhSQuoteAdjustments)
// {
// adj.QuoteheaderId = headerEntity.Id;
// var dbAdj = EntityMapper.MapEntity<EQuoteAdjustment, PhSQuoteAdjustment>(adj);
// _context.PhSQuoteAdjustments.Add(dbAdj);
// }
// }
// Guardado de Impuestos
if (quote.PhSQuoteTaxes?.Any() == true)
{
foreach (var tax in quote.PhSQuoteTaxes)
{
tax.QuoteheaderId = headerEntity.Id;
var dbTax = EntityMapper.MapEntity<EQuoteTax, PhSQuoteTaxis>(tax);
_context.PhSQuoteTaxes.Add(dbTax);
}
}
// // Guardado de Impuestos
// if (quote.PhSQuoteTaxes?.Any() == true)
// {
// foreach (var tax in quote.PhSQuoteTaxes)
// {
// tax.QuoteheaderId = headerEntity.Id;
// var dbTax = EntityMapper.MapEntity<EQuoteTax, PhSQuoteTaxis>(tax);
// _context.PhSQuoteTaxes.Add(dbTax);
// }
// }
await _context.SaveChangesAsync();
await transaction.CommitAsync();
// await _context.SaveChangesAsync();
// await transaction.CommitAsync();
return headerEntity.Quotenumber;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
public async Task<IDbContextTransaction> BeginTransactionAsync()
{
return await _context.Database.BeginTransactionAsync();
}
// return headerEntity.Quotenumber;
// }
// catch
// {
// await transaction.RollbackAsync();
// throw;
// }
//}
//public async Task<IDbContextTransaction> BeginTransactionAsync()
//{
// return await _context.Database.BeginTransactionAsync();
//}
#endregion
}
}

View File

@ -0,0 +1,95 @@
using Domain.Entities;
using Models.Helpers;
using Models.Interfaces;
using Models.Models;
namespace Models.Repositories
{
public class PhSQuoteRepository(PhronCareOperationsHubContext context,
IPhSFormSeriesRepository formSeriesRepository) : IPhSQuoteRepository
{
private readonly PhronCareOperationsHubContext _context = context;
private readonly IPhSFormSeriesRepository _formSeriesRepository = formSeriesRepository;
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
/// <summary>
/// Crea un nuevo presupuesto, incluyendo encabezado, detalles, roles, ajustes e impuestos asociados.
/// Genera automáticamente el número de presupuesto en base a la serie indicada.
/// <param name="quote">Presupuesto a registrar, incluyendo entidades relacionadas.</param>
/// <param name="formSeriesId">Identificador de la serie de numeración a utilizar.</param>
/// <returns>Cadena con el número generado del presupuesto.</returns>
/// </summary>
public async Task<string> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId)
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var nextNumber = await _formSeriesRepository.GetNextInternalNumberAsync(formSeriesId);
var series = await _formSeriesRepository.GetByIdAsync(formSeriesId)
?? throw new InvalidOperationException("Serie no encontrada");
int padding = 8; /* format "00000000" */
quote.Quotenumber = $"{series.Letter}-{nextNumber.ToString($"D{padding}")}";
var headerEntity = EntityMapper.MapEntity<EQuoteHeader, PhSQuoteHeader>(quote);
_context.PhSQuoteHeaders.Add(headerEntity);
#region Nota: Esta seccion queda para futura modificacion en caso de cambiar CASCADE INSERT de las entidades relacionadas
//// Guardado de Detalles
//if (quote.PhSQuoteDetails?.Any() == true)
//{
// foreach (var detail in quote.PhSQuoteDetails)
// {
// detail.QuoteheaderId = headerEntity.Id;
// var dbDetail = EntityMapper.MapEntity<EQuoteDetail, PhSQuoteDetail>(detail);
// _context.PhSQuoteDetails.Add(dbDetail);
// }
//}
//// Guardado de Roles
//if (quote.PhSQuoteRoles?.Any() == true)
//{
// foreach (var role in quote.PhSQuoteRoles)
// {
// role.QuoteheaderId = headerEntity.Id;
// var dbRole = EntityMapper.MapEntity<EQuoteRole, PhSQuoteRole>(role);
// _context.PhSQuoteRoles.Add(dbRole);
// }
//}
//// Guardado de Ajustes
//if (quote.PhSQuoteAdjustments?.Any() == true)
//{
// foreach (var adj in quote.PhSQuoteAdjustments)
// {
// adj.QuoteheaderId = headerEntity.Id;
// var dbAdj = EntityMapper.MapEntity<EQuoteAdjustment, PhSQuoteAdjustment>(adj);
// _context.PhSQuoteAdjustments.Add(dbAdj);
// }
//}
//// Guardado de Impuestos
//if (quote.PhSQuoteTaxes?.Any() == true)
//{
// foreach (var tax in quote.PhSQuoteTaxes)
// {
// tax.QuoteheaderId = headerEntity.Id;
// var dbTax = EntityMapper.MapEntity<EQuoteTax, PhSQuoteTaxis>(tax);
// _context.PhSQuoteTaxes.Add(dbTax);
// }
//}
#endregion
await _context.SaveChangesAsync();
await transaction.CommitAsync();
return headerEntity.Quotenumber;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
#endregion
}
}

View File

@ -91,32 +91,6 @@ namespace phronCare.API.Controllers.Sales
#region Crear / Actualizar / Eliminar
[HttpPost("create")]
public async Task<IActionResult> Create([FromBody] EQuoteHeader quote, [FromQuery] int formSeriesId)
{
try
{
if (quote == null)
return BadRequest("El presupuesto no puede ser nulo.");
var result = await _quoteService.CreateQuoteAsync(quote, formSeriesId);
return Ok(result);
}
catch (ArgumentNullException ex)
{
return BadRequest($"Validación fallida: {ex.Message}");
}
catch (InvalidOperationException ex)
{
return BadRequest($"Error de negocio: {ex.Message}");
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpPut("update")]
public async Task<IActionResult> Update([FromBody] EQuoteHeader quote)
{
@ -152,145 +126,6 @@ namespace phronCare.API.Controllers.Sales
#endregion
#region Impuestos (QuoteTaxes)
[HttpGet("{quoteId:int}/taxes")]
public async Task<IActionResult> GetTaxes(int quoteId)
{
try
{
var taxes = await _quoteService.GetTaxesByQuoteIdAsync(quoteId);
return Ok(taxes);
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpPost("{quoteId:int}/taxes")]
public async Task<IActionResult> AddTax(int quoteId, [FromBody] EQuoteTax tax)
{
try
{
if (tax == null || quoteId != tax.QuoteheaderId)
return BadRequest("Datos inválidos para el impuesto.");
var result = await _quoteService.AddTaxAsync(tax);
return Ok(result);
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpPut("taxes")]
public async Task<IActionResult> UpdateTax([FromBody] EQuoteTax tax)
{
try
{
if (tax == null || tax.Id <= 0)
return BadRequest("Datos inválidos para actualizar el impuesto.");
await _quoteService.UpdateTaxAsync(tax);
return Ok("Impuesto actualizado correctamente.");
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpDelete("taxes/{taxId:int}")]
public async Task<IActionResult> DeleteTax(int taxId)
{
try
{
await _quoteService.DeleteTaxAsync(taxId);
return Ok("Impuesto eliminado correctamente.");
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
#endregion
#region Ajustes Comerciales (QuoteAdjustments)
[HttpGet("{quoteId:int}/adjustments")]
public async Task<IActionResult> GetAdjustments(int quoteId)
{
try
{
var adjustments = await _quoteService.GetAdjustmentsByQuoteIdAsync(quoteId);
return Ok(adjustments);
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpPost("{quoteId:int}/adjustments")]
public async Task<IActionResult> AddAdjustment(int quoteId, [FromBody] EQuoteAdjustment adjustment)
{
try
{
if (adjustment == null || quoteId != adjustment.QuoteheaderId)
return BadRequest("Datos inválidos para el ajuste.");
var result = await _quoteService.AddAdjustmentAsync(adjustment);
return Ok(result);
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpPut("adjustments")]
public async Task<IActionResult> UpdateAdjustment([FromBody] EQuoteAdjustment adjustment)
{
try
{
if (adjustment == null || adjustment.Id <= 0)
return BadRequest("Datos inválidos para actualizar el ajuste.");
await _quoteService.UpdateAdjustmentAsync(adjustment);
return Ok("Ajuste actualizado correctamente.");
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpDelete("adjustments/{adjustmentId:int}")]
public async Task<IActionResult> DeleteAdjustment(int adjustmentId)
{
try
{
await _quoteService.DeleteAdjustmentAsync(adjustmentId);
return Ok("Ajuste eliminado correctamente.");
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
#endregion
#region Exportación
@ -314,15 +149,23 @@ namespace phronCare.API.Controllers.Sales
#region Endpoint de emision de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
[HttpPost("createfull")]
public async Task<IActionResult> CreateFullQuote([FromBody] EQuoteHeader quote, [FromQuery] int formSeriesId)
public async Task<IActionResult> CreateFullQuote([FromBody] CreateFullQuoteRequest request)
{
try
{
if (quote == null)
return BadRequest("El presupuesto no puede ser nulo.");
// Validamos que el request y el objeto Quote no sean nulos
if (request == null || request.Quote == null)
return BadRequest("El payload no puede contener elementos nulos.");
// Desempaquetamos los datos
var quote = request.Quote;
var formSeriesId = request.FormSeriesId;
// Llamada al servicio de negocio
var quoteNumber = await _quoteService.CreateFullQuoteAsync(quote, formSeriesId);
return Ok(new { QuoteNumber = quoteNumber });
// Devolvemos el número generado
return Ok(new { Success = true, QuoteNumber = quoteNumber });
}
catch (ArgumentNullException ex)
{
@ -334,10 +177,16 @@ namespace phronCare.API.Controllers.Sales
}
catch (Exception ex)
{
var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod";
return StatusCode(500, $"{methodName} Message: {ex.Message}");
return StatusCode(500, $"Ocurrió un error interno: {ex.Message}");
}
}
public class CreateFullQuoteRequest
{
public EQuoteHeader Quote { get; set; } = default!;
public int FormSeriesId { get; set; }
}
#endregion
}
}

View File

@ -244,8 +244,8 @@ static void RepositorysAndServices(WebApplicationBuilder builder)
builder.Services.AddScoped<IQuoteDom, QuoteService>();
builder.Services.AddScoped<IPhSQuoteHeaderRepository, PhSQuoteHeaderRepository>();
builder.Services.AddScoped<IPhSQuoteDetailRepository, PhSQuoteDetailRepository>();
builder.Services.AddScoped<IPhSQuoteRoleRepository, PhSQuoteRoleRepository>();
builder.Services.AddScoped<IPhSQuoteRepository, PhSQuoteRepository>();
//builder.Services.AddScoped<IPhSQuoteRoleRepository, PhSQuoteRoleRepository>();
builder.Services.AddScoped<IPhSFormSeriesRepository, PhSFormSeriesRepository>();
// Registrar el service de lookup

View File

@ -1556,112 +1556,6 @@
}
]
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "GetAdjustments",
"RelativePath": "api/Quote/{quoteId}/adjustments",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "quoteId",
"Type": "System.Int32",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "AddAdjustment",
"RelativePath": "api/Quote/{quoteId}/adjustments",
"HttpMethod": "POST",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "quoteId",
"Type": "System.Int32",
"IsRequired": true
},
{
"Name": "adjustment",
"Type": "Domain.Entities.EQuoteAdjustment",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "GetTaxes",
"RelativePath": "api/Quote/{quoteId}/taxes",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "quoteId",
"Type": "System.Int32",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "AddTax",
"RelativePath": "api/Quote/{quoteId}/taxes",
"HttpMethod": "POST",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "quoteId",
"Type": "System.Int32",
"IsRequired": true
},
{
"Name": "tax",
"Type": "Domain.Entities.EQuoteTax",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "UpdateAdjustment",
"RelativePath": "api/Quote/adjustments",
"HttpMethod": "PUT",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "adjustment",
"Type": "Domain.Entities.EQuoteAdjustment",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "DeleteAdjustment",
"RelativePath": "api/Quote/adjustments/{adjustmentId}",
"HttpMethod": "DELETE",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "adjustmentId",
"Type": "System.Int32",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "GetAll",
@ -1683,27 +1577,6 @@
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "Create",
"RelativePath": "api/Quote/create",
"HttpMethod": "POST",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "quote",
"Type": "Domain.Entities.EQuoteHeader",
"IsRequired": true
},
{
"Name": "formSeriesId",
"Type": "System.Int32",
"IsRequired": false
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "CreateFullQuote",
@ -1713,14 +1586,9 @@
"Order": 0,
"Parameters": [
{
"Name": "quote",
"Type": "Domain.Entities.EQuoteHeader",
"Name": "request",
"Type": "phronCare.API.Controllers.Sales.QuoteController\u002BCreateFullQuoteRequest",
"IsRequired": true
},
{
"Name": "formSeriesId",
"Type": "System.Int32",
"IsRequired": false
}
],
"ReturnTypes": []
@ -1818,38 +1686,6 @@
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "UpdateTax",
"RelativePath": "api/Quote/taxes",
"HttpMethod": "PUT",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "tax",
"Type": "Domain.Entities.EQuoteTax",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "DeleteTax",
"RelativePath": "api/Quote/taxes/{taxId}",
"HttpMethod": "DELETE",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "taxId",
"Type": "System.Int32",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.QuoteController",
"Method": "Update",

View File

@ -4,10 +4,14 @@
@using Blazored.Typeahead
@using Services.Lookups
@using phronCare.UIBlazor.Pages.Sales.Modals
@using phronCare.UIBlazor.Services.Sales.Quotes
@inject ISalesLookupService SalesLookupService
@inject IQuoteService QuoteService
@inject IToastService toastService
@inject NavigationManager Navigation
@inject IModalService Modal
<EditForm Model="_quoteModel">
<EditForm Model="_quoteModel" >
<div class="container mt-4" style="zoom:0.8;">
<div class="card">
<div class="card-header">
@ -110,9 +114,11 @@
<div class="col-md-4">
<label class="form-label">Moneda</label>
<InputSelect class="form-select" @bind-Value="_quoteModel.Currency">
<option value="">-- Seleccionar moneda --</option>
<option value="ARS">ARS</option>
<option value="USD">USD</option>
</InputSelect>
</div>
<div class="col-md-4">
<label class="form-label">Tipo de cambio</label>
@ -296,7 +302,7 @@
</div>
<div class="card-footer text-end">
<button type="submit" class="btn btn-primary">Guardar</button>
<button type="button" class="btn btn-primary" @onclick="HandleValidSubmit">Guardar</button>
<button type="button" class="btn btn-secondary ms-2">Cancelar</button>
</div>
</div>
@ -369,12 +375,12 @@
=> SetLookupSelection(item, sel => _selectedCustomer = sel, id => _quoteModel.CustomerId = id);
private Task OnPersonSelected(ELookUpItem item)
=> SetLookupSelection(item, sel => _selectedPerson = sel, id => _quoteModel.PeopleId = id);
private Task OnProfessionalSelected(ELookUpItem item)
=> SetLookupSelection(item, sel => _selectedProfessional = sel);
private Task OnInstitutionSelected(ELookUpItem item)
=> SetLookupSelection(item, sel => _selectedInstitution = sel);
private Task OnPatientSelected(ELookUpItem item)
=> SetLookupSelection(item, sel => _selectedPatient = sel);
// private Task OnProfessionalSelected(ELookUpItem item)
// => SetLookupSelection(item, sel => _selectedProfessional = sel);
// private Task OnInstitutionSelected(ELookUpItem item)
// => SetLookupSelection(item, sel => _selectedInstitution = sel);
// private Task OnPatientSelected(ELookUpItem item)
// => SetLookupSelection(item, sel => _selectedPatient = sel);
private Task SetLookupSelection(ELookUpItem? item, Action<ELookUpItem?> setSelected, Action<int>? setModelId = null)
{
setSelected(item);
@ -439,4 +445,65 @@
_quoteModel.PhSQuoteTaxes.Remove(tax);
RecalculateTotals();
}
private async Task HandleValidSubmit()
{
// Si necesitas validar algo extra antes de llamar al servicio, hazlo aquí
int selectedSeriesId = 1/* obtenlo de donde corresponda, p.ej. de un dropdown */;
var result = await QuoteService.CreateFullQuoteAsync(_quoteModel, selectedSeriesId);
if (!result.Success)
{
toastService.ShowError(result.ErrorMessage);
return;
}
toastService.ShowSuccess($"Presupuesto creado: {result.QuoteNumber}");
//Navigation.NavigateTo($"/sales/quotes/details/{result.QuoteNumber}");
}
/// <summary>
/// Agrega o actualiza un rol dentro de _quoteModel.PhSQuoteRoles
/// </summary>
private void AddOrUpdateRole(string entityType, int entityId, string roleName)
{
var existing = _quoteModel.PhSQuoteRoles
.FirstOrDefault(r => r.Entitytype == entityType);
if (existing != null)
{
existing.EntityId = entityId;
existing.Role = roleName;
}
else
{
_quoteModel.PhSQuoteRoles.Add(new EQuoteRole
{
// QuoteheaderId lo llenará EF en el servidor
Entitytype = entityType,
EntityId = entityId,
Role = roleName
});
}
}
private Task OnProfessionalSelected(ELookUpItem item)
{
_selectedProfessional = item;
AddOrUpdateRole("PhS_Professionals", item.Id, "Medico");
return Task.CompletedTask;
}
private Task OnInstitutionSelected(ELookUpItem item)
{
_selectedInstitution = item;
AddOrUpdateRole("PhS_Institutions", item.Id, "Hospital");
return Task.CompletedTask;
}
private Task OnPatientSelected(ELookUpItem item)
{
_selectedPatient = item;
AddOrUpdateRole("PhS_Patients", item.Id, "Paciente");
return Task.CompletedTask;
}
}

View File

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Blazored.Modal;
using Blazored.Toast;
using phronCare.UIBlazor.Services.Sales.Quotes;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
@ -52,6 +53,7 @@ await builder.Build().RunAsync();
static void InjectDependencies(WebAssemblyHostBuilder builder)
{
builder.Services.AddScoped<ISalesLookupService, SalesLookupService>();
builder.Services.AddScoped<IQuoteService,QuoteService>();
builder.Services.AddScoped<TicketsService>();
builder.Services.AddScoped<CustomerService>();
@ -66,4 +68,5 @@ static void InjectDependencies(WebAssemblyHostBuilder builder)
builder.Services.AddScoped<PeopleService>();
builder.Services.AddScoped<ProfessionalSpecialtyService>();
builder.Services.AddScoped<ProductCategoryService>();
}

View File

@ -0,0 +1,10 @@
using Domain.Entities;
namespace phronCare.UIBlazor.Services.Sales.Quotes
{
public interface IQuoteService
{
Task<CreateQuoteResult> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
// Aquí podrías agregar otros métodos: GetById, Search, etc.
}
}

View File

@ -0,0 +1,62 @@
using Domain.Entities;
using System.Net.Http.Json;
namespace phronCare.UIBlazor.Services.Sales.Quotes
{
public class QuoteService : IQuoteService
{
private readonly HttpClient _http;
public QuoteService(HttpClient http)
{
_http = http;
}
/// <summary>
/// Envía el EQuoteHeader junto con el formSeriesId al backend
/// y recibe el número de presupuesto generado o un error.
/// </summary>
public async Task<CreateQuoteResult> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId)
{
// 1) Creamos el DTO request tal cual lo espera el controller
var request = new CreateFullQuoteRequest
{
Quote = quote,
FormSeriesId = formSeriesId
};
// 2) Enviamos ese DTO al endpoint
var response = await _http.PostAsJsonAsync("/api/quote/createfull", request);
//var url = $"/api/quote/createfull?formSeriesId={formSeriesId}";
//var response = await _http.PostAsJsonAsync(url, quote);
if (!response.IsSuccessStatusCode)
{
// Leer mensaje de error del servidor
var serverMessage = await response.Content.ReadAsStringAsync();
return new CreateQuoteResult
{
Success = false,
ErrorMessage = serverMessage
};
}
// Deserializar el resultado esperado
var result = await response.Content.ReadFromJsonAsync<CreateQuoteResult>();
return result!;
}
}
public class CreateQuoteResult
{
public bool Success { get; set; }
public string QuoteNumber { get; set; } = string.Empty;
public string ErrorMessage { get; set; } = string.Empty;
}
public class CreateFullQuoteRequest
{
public EQuoteHeader Quote { get; set; } = default!;
public int FormSeriesId { get; set; }
}
}