Leandro Hernan Rojas ee013c952c
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 14m58s
Add Patient QuickAdd
2025-05-22 09:28:06 -03:00

524 lines
25 KiB
Plaintext

@page "/quote/create"
@using System.Globalization;
@using System.Net.Http.Json
@using Blazored.Typeahead
@using Pages.Sales.Modals
@using Services.Lookups
@using Services.Integrations
@using Services.Sales.Quotes
@using Blazored.Toast.Services
@using Blazored.Toast.Configuration
@inject NavigationManager Navigation
@inject ISalesLookupService SalesLookupService
@inject IExchangeRateService ExchangeRateService
@inject QuoteService QuoteService
@inject IToastService toastService
@inject IModalService Modal
<EditForm Model="_quoteModel" >
<div class="container mt-4" style="zoom:.8;">
<div class="card">
<div class="card-header d-flex justify-content-center align-items-center">
<h3 class="mb-0">Emisión de Presupuesto</h3>
</div>
<div class="card-body">
<!-- FILA 1: Cliente, Vendedor -->
<div class="row mb-3">
<div class="col-md-6 add-wrapper">
<label for="clientes">Cliente *</label>
<BlazoredTypeahead id="clientes" TItem="ELookUpItem" TValue="ELookUpItem"
SearchMethod="SalesLookupService.SearchCustomersAsync"
Value="_selectedCustomer"
ValueChanged="OnCustomerSelected"
ValueExpression="@(() => _selectedCustomer)"
MaximumSuggestions="5" Placeholder="Buscar cliente..."
TextProperty="Nombre">
<ResultTemplate Context="customer">@customer.Nombre</ResultTemplate>
<SelectedTemplate Context="customer">@customer.Nombre</SelectedTemplate>
</BlazoredTypeahead>
<button type="button" class="add-btn" title="Agregar nuevo">+</button>
</div>
<div class="col-md-6 add-wrapper">
<label for="vendedores">Vendedor *</label>
<BlazoredTypeahead id="vendedores" TItem="ELookUpItem" TValue="ELookUpItem"
SearchMethod="SalesLookupService.SearchPeopleAsync"
Value="_selectedPerson"
ValueChanged="OnPersonSelected"
ValueExpression="@(() => _selectedPerson)"
MaximumSuggestions="5" Placeholder="Buscar vendedor..."
TextProperty="Nombre">
<ResultTemplate Context="item">@item.Nombre</ResultTemplate>
<SelectedTemplate Context="item">@item.Nombre</SelectedTemplate>
</BlazoredTypeahead>
<button type="button" class="add-btn" title="Agregar vendedor">+</button>
</div>
</div>
<!-- FILA 2: Profesional, Paciente -->
<div class="row mb-3">
<div class="col-md-6 add-wrapper">
<label for="profesionales">Profesional *</label>
<BlazoredTypeahead id="profesionales" TItem="ELookUpItem" TValue="ELookUpItem"
SearchMethod="SalesLookupService.SearchProfessionalsAsync"
Value="_selectedProfessional"
ValueChanged="OnProfessionalSelected"
ValueExpression="@(() => _selectedProfessional)"
MaximumSuggestions="5" Placeholder="Buscar profesional..."
TextProperty="Nombre">
<ResultTemplate Context="item">@item.Nombre</ResultTemplate>
<SelectedTemplate Context="item">@item.Nombre</SelectedTemplate>
</BlazoredTypeahead>
<button type="button" class="add-btn" title="Agregar profesional">+</button>
<button type="button" class="add-btn" title="Agregar profesional" @onclick="AddNewProfessional">+</button>
</div>
<div class="col-md-6 add-wrapper">
<label for="pacientes">Paciente *</label>
<BlazoredTypeahead id="pacientes" TItem="ELookUpItem" TValue="ELookUpItem"
SearchMethod="SalesLookupService.SearchPatientsAsync"
Value="_selectedPatient"
ValueChanged="OnPatientSelected"
ValueExpression="@(() => _selectedPatient)"
MaximumSuggestions="5" Placeholder="Buscar paciente..."
TextProperty="Nombre">
<ResultTemplate Context="item">@item.Nombre</ResultTemplate>
<SelectedTemplate Context="item">@item.Nombre</SelectedTemplate>
</BlazoredTypeahead>
<button type="button" class="add-btn" title="Agregar paciente" @onclick="AddNewPatient">+</button>
</div>
</div>
<!-- FILA 3: Institución, Unidad de negocio -->
<div class="row mb-3">
<div class="col-md-6 add-wrapper">
<label for="instituciones">Institución *</label>
<BlazoredTypeahead id="instituciones" TItem="ELookUpItem" TValue="ELookUpItem"
SearchMethod="SalesLookupService.SearchInstitutionsAsync"
Value="_selectedInstitution"
ValueChanged="OnInstitutionSelected"
ValueExpression="@(() => _selectedInstitution)"
MaximumSuggestions="5" Placeholder="Buscar institución..."
TextProperty="Nombre">
<ResultTemplate Context="item">@item.Nombre</ResultTemplate>
<SelectedTemplate Context="item">@item.Nombre</SelectedTemplate>
</BlazoredTypeahead>
<button type="button" class="add-btn" title="Agregar institución">+</button>
</div>
<div class="col-md-6">
<label for="unidad">Unidad de negocio *</label>
<InputSelect id="unidad" class="form-select" @bind-Value="_quoteModel.BusinessunitId">
<option disabled selected value="">Seleccione...</option>
@foreach (var unidad in _businessUnits)
{
<option value="@unidad.Id">@unidad.Nombre</option>
}
</InputSelect>
</div>
</div>
<!-- FILA 4: Moneda, Cambio, OutOfTown -->
<div class="row mb-3">
<div class="col-md-4">
<label id="moneda">Moneda</label>
<InputSelect id="moneda" 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 id="cambio">Tipo de cambio</label>
<InputNumber id="cambio" class="form-control" @bind-Value="_quoteModel.Exchangerate"/>
</div>
<div class="col-md-4 d-flex align-items-end">
<div class="form-check form-switch">
<InputCheckbox id="OutOfTown" class="form-check-input" @bind-Value="_quoteModel.OutOfTown" />
<label class="form-check-label ms-2" for="OutOfTown">¿Fuera de localidad?</label>
</div>
</div>
</div>
<!-- FILA 5: Instrucciones y Observaciones -->
<div class="row mb-3">
<div class="col-md-6">
<label id="despacho">Instrucciones de despacho</label>
<InputText id="despacho" class="form-control" @bind-Value="_quoteModel.DispatchInstruction" />
</div>
<div class="col-md-6">
<label id="observaciones">Observaciones</label>
<InputText id="observaciones" class="form-control" @bind-Value="_quoteModel.Observations" />
</div>
</div>
<!-- TABLA DETALLE: Productos Cotizados -->
<hr />
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-3">Productos Cotizados</h5>
<button type="button" class="btn btn-outline-success mb-2" @onclick="AddNewProduct">
<i class="fas fa-cart-plus me-1"></i> Agregar producto
</button>
</div>
<div class="table-responsive">
<table class="table table-bordered table-hover align-middle">
<thead class="table-light">
<tr>
<th style="width: 5%; align-items:center">Producto</th>
<th>Descripción</th>
<th style="width: 8%; text-align:center;">Cantidad</th>
<th style="width: 15%;text-align:center;">Precio Unitario</th>
<th style="width: 10%;text-align:center;">Total</th>
<th style="width: 5%;"></th>
</tr>
</thead>
<tbody>
@if (_quoteModel.PhSQuoteDetails.Any())
{
foreach (var item in _quoteModel.PhSQuoteDetails)
{
<tr>
<td style="text-align:center">@item.ProductId</td>
<td>
<InputTextArea class="form-control"
style="resize: vertical;"
@bind-Value="item.ProductDescription"
Rows="3" @oninput="RecalculateTotals" />
</td>
<td>
<InputNumber class="form-control"
Value="item.Quantity"
ValueChanged="(int val) => OnValueChanged(item, (int)val, (i, v) => i.Quantity = v)"
ValueExpression="@(() => item.Quantity)" />
</td>
<td>
<InputNumber class="form-control"
Value="item.Unitprice"
ValueChanged="(decimal val) =>OnValueChanged(item, (decimal)val, (i, v) => i.Unitprice = v)"
ValueExpression="@(() => item.Unitprice)" />
</td>
<td>$ @($"{item.Quantity * item.Unitprice:0.00}")</td>
<td style="text-align:center;">
<button class="btn btn-sm btn-danger" @onclick="() => RemoveDetail(item)">🗑</button>
</td>
</tr>
}
}
else
{
<tr>
<td colspan="5"><em>No hay productos agregados.</em></td>
</tr>
}
</tbody>
</table>
</div>
<!-- Totales + Ajustes -->
<div class="row justify-content-end mt-3" >
<div class="col-md-4">
<label class="form-label d-flex justify-content-between align-items-center">
Ajustes comerciales
<button class="btn btn-sm btn-outline-success mt-2" @onclick="OpenAdjustmentModal">
<i class="fas fa-plus me-1"></i> Agregar
</button>
</label>
@if (_quoteModel.PhSQuoteAdjustments.Any())
{
<div class="mb-2">
@foreach (var adj in _quoteModel.PhSQuoteAdjustments)
{
<span class="adjustment-tag">
@adj.ReasonCode
<button type="button" class="remove-btn" title="Eliminar" @onclick="() => RemoveAdjustment(adj)">
<i class="fas fa-trash-alt"></i>
</button>
</span>
}
</div>
}
else
{
<p class="text-muted">Sin ajustes.</p>
}
</div>
<!-- Impuestos -->
<div class="col-md-4">
<label class="form-label d-flex justify-content-between align-items-center">
Impuestos
<button class="btn btn-sm btn-outline-success mt-2" @onclick="AddNewTax">
<i class="fas fa-percentage me-1"></i> Agregar
</button>
</label>
@if (_quoteModel.PhSQuoteTaxes.Any())
{
<div class="table-responsive">
<table class="table table-sm table-bordered mb-0">
<thead class="table-light">
<tr>
<th>Nombre</th>
<th class="text-end">%</th>
<th class="text-end">Importe</th>
<th style="width: 30px;"></th>
</tr>
</thead>
<tbody>
@foreach (var tax in _quoteModel.PhSQuoteTaxes)
{
<tr>
<td>@tax.Taxname</td>
<td class="text-end">@tax.Taxrate.ToString("0.##")%</td>
<td class="text-end">$@tax.Taxamount.ToString("N2")</td>
<td class="text-center">
<button class="btn btn-sm btn-danger" @onclick="() => RemoveTax(tax)">
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<p class="text-muted"><em>No hay impuestos aplicados</em></p>
}
</div>
<div class="col-md-4">
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between">
<span>Subtotal</span>
<strong>$ @_netAmount.ToString("N2")</strong>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Taxes</span>
<strong>$ @_taxAmount.ToString("N2")</strong>
</li>
<li class="list-group-item d-flex justify-content-between">
<span>Total</span>
<strong>$ @_grandTotal.ToString("N2")</strong>
</li>
</ul>
</div>
</div>
</div>
<div class="card-footer text-end">
<button type="button" class="btn btn-primary" @onclick="HandleValidSubmit">Guardar</button>
<button type="button" class="btn btn-secondary ms-2" @onclick="Cancel">Cancelar</button>
</div>
</div>
</div>
</EditForm>
@code {
private EQuoteHeader _quoteModel = new();
private ELookUpItem? _selectedCustomer, _selectedProfessional, _selectedInstitution, _selectedPatient, _selectedPerson;
private List<ELookUpItem> _businessUnits = new();
private EExchangeRateHistory? YesterdayRate;
private string returnUrl = "quotes/";
private decimal _netAmount = 0, _taxAmount = 0, _grandTotal = 0;
public const int QuoteSeriesId = 1; // Serie de comprobante para presupuestos (talonario Q).
protected override async Task OnInitializedAsync()
{
// 1. Levantamos la cotización de ayer
try
{
YesterdayRate = await ExchangeRateService.GetYesterdayRateAsync();
}
catch (Exception ex)
{
// manejar error (toast, log, etc.)
Console.Error.WriteLine(ex.Message);
}
// 2. Asignamos al modelo si aún no tiene valor
if (YesterdayRate != null && _quoteModel.Exchangerate == 0)
{
_quoteModel.Exchangerate = YesterdayRate.Salerate;
}
_businessUnits = (await SalesLookupService.SearchBussinessUnitsAsync(string.Empty)).ToList();
}
private async Task AddNewProduct()
{
var options = new ModalOptions()
{
Size = ModalSize.Large,
HideHeader = true
};
var modal = Modal.Show<ProductSelectorModal>("", options);
var result = await modal.Result;
if (!result.Cancelled && result.Data is EProductLookupItem selected)
{
var newDetail = new EQuoteDetail
{
ProductId = selected.Id,
ProductDescription = selected.Description,
Quantity = 1,
Unitprice = selected.UnitPrice,
Approved = false,
Createdat = DateTime.Now
};
_quoteModel.PhSQuoteDetails.Add(newDetail);
}
}
private async Task AddNewPatient()
{
var options = new ModalOptions()
{
Size = ModalSize.Large,
HideHeader = true
};
var modal = Modal.Show<PatientQuickAddModal>(options);
var result = await modal.Result;
if (!result.Cancelled && result.Data is ELookUpItem nuevo)
{
_selectedPatient = nuevo;
}
}
private async Task AddNewProfessional()
{
var options = new ModalOptions()
{
Size = ModalSize.Large,
HideHeader = true
};
var modal = Modal.Show<ProfessionalQuickAddModal>(options);
var result = await modal.Result;
if (!result.Cancelled && result.Data is ELookUpItem nuevo)
{
_selectedProfessional = nuevo;
// No hay ProfessionalId en el modelo base, pero si lo usás, actualizalo aquí.
// _quoteModel.ProfessionalId = nuevo.Id;
}
}
private async Task OpenAdjustmentModal()
{
var modal = Modal.Show<QuoteAdjustmentQuickAddModal>("Agregar Ajuste", new ModalOptions { HideHeader = true });
var result = await modal.Result;
if (!result.Cancelled && result.Data is QuoteAdjustmentQuickAddModal.QuoteAdjustmentDto dto)
{
_quoteModel.PhSQuoteAdjustments.Add(new EQuoteAdjustment
{
ReasonCode = dto.ReasonCode,
Amount = dto.Amount
});
}
}
private async Task HandleValidSubmit()
{
var result = await QuoteService.CreateFullQuoteAsync(_quoteModel, QuoteSeriesId);
if (!result.Success)
{
toastService.ShowError(result.ErrorMessage);
return;
}
ToastParameters _parameters = new();
_parameters.Add(nameof(PhLinkToast.Comprobante), result.QuoteNumber);
_parameters.Add(nameof(PhLinkToast.Style), "success");
_parameters.Add(nameof(PhLinkToast.OnLinkClick), EventCallback.Factory.Create(this, () =>
QuoteService.ExportPdfAsync(result.Id, result.QuoteNumber)));
toastService.ShowToast<PhLinkToast>(_parameters);
Navigation.NavigateTo(returnUrl);
}
private async Task AddNewTax()
{
var parameters = new ModalParameters();
parameters.Add(nameof(QuoteTaxQuickAddModal.NetAmount), _netAmount);
var options = new ModalOptions
{
HideHeader = true,
Size = ModalSize.Small
};
var modal = Modal.Show<QuoteTaxQuickAddModal>("", parameters, options);
var result = await modal.Result;
if (!result.Cancelled && result.Data is EQuoteTax newTax)
{
_quoteModel.PhSQuoteTaxes.Add(newTax);
RecalculateTotals();
}
}
private Task OnCustomerSelected(ELookUpItem item)
=> 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)
{
_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;
}
private Task OnValueChanged<T>(EQuoteDetail item, T value, Action<EQuoteDetail, T> setter)
{
setter(item, value);
RecalculateTotals();
return Task.CompletedTask;
}
private Task SetLookupSelection(ELookUpItem? item, Action<ELookUpItem?> setSelected, Action<int>? setModelId = null)
{
setSelected(item);
if (item != null && setModelId != null) setModelId(item.Id);
return Task.CompletedTask;
}
private void OnDetailChanged(EQuoteDetail item) { }
private void RemoveDetail(EQuoteDetail item)
=> _quoteModel.PhSQuoteDetails.Remove(item);
private void RemoveAdjustment(EQuoteAdjustment adj)
{
_quoteModel.PhSQuoteAdjustments.Remove(adj);
}
private void RemoveTax(EQuoteTax tax)
{
_quoteModel.PhSQuoteTaxes.Remove(tax);
RecalculateTotals();
}
private void RecalculateTotals()
{
_netAmount = _quoteModel.PhSQuoteDetails.Sum(d => d.Quantity * d.Unitprice);
_taxAmount = _quoteModel.PhSQuoteTaxes.Sum(t => t.Taxamount);
_grandTotal = _netAmount + _taxAmount;
_quoteModel.Netamount = _netAmount;
_quoteModel.Total = _grandTotal;
}
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
{
Entitytype = entityType,
EntityId = entityId,
Role = roleName
});
}
}
private void Cancel()
{
Navigation.NavigateTo(returnUrl);
}
}