All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m18s
Quote Demo v1
374 lines
18 KiB
Plaintext
374 lines
18 KiB
Plaintext
@page "/quote/create"
|
|
@using System.Globalization;
|
|
@using System.Net.Http.Json
|
|
@using Blazored.Typeahead
|
|
@using Services.Lookups
|
|
@using phronCare.UIBlazor.Pages.Sales.Modals
|
|
@inject ISalesLookupService SalesLookupService
|
|
@inject IModalService Modal
|
|
|
|
<EditForm Model="_quoteModel">
|
|
<div class="container mt-4" style="zoom:0.8;">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<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 class="form-label">Cliente *</label>
|
|
<BlazoredTypeahead 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 class="form-label">Vendedor *</label>
|
|
<BlazoredTypeahead 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 class="form-label">Profesional *</label>
|
|
<BlazoredTypeahead 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 class="form-label">Paciente *</label>
|
|
<BlazoredTypeahead 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">+</button>
|
|
</div>
|
|
</div>
|
|
<!-- FILA 3: Institución, Unidad de negocio -->
|
|
<div class="row mb-3">
|
|
<div class="col-md-6 add-wrapper">
|
|
<label class="form-label">Institución *</label>
|
|
<BlazoredTypeahead 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 class="form-label">Unidad de negocio *</label>
|
|
<InputSelect 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 class="form-label">Moneda</label>
|
|
<InputSelect class="form-select" @bind-Value="_quoteModel.Currency">
|
|
<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>
|
|
<InputNumber 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 class="form-label">Instrucciones de despacho</label>
|
|
<InputText class="form-control" @bind-Value="_quoteModel.DispatchInstruction" />
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Observaciones</label>
|
|
<InputText class="form-control" @bind-Value="_quoteModel.Observations" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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: 15%;">Producto</th>
|
|
<th>Descripción</th>
|
|
<th style="width: 10%;">Cantidad</th>
|
|
<th style="width: 15%;">Precio Unitario</th>
|
|
<th style="width: 15%;">Total</th>
|
|
<th style="width: 5%;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (_quoteModel.PhSQuoteDetails.Any())
|
|
{
|
|
foreach (var item in _quoteModel.PhSQuoteDetails)
|
|
{
|
|
<tr>
|
|
<td>@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>
|
|
<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-block">Ajustes comerciales</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>
|
|
}
|
|
|
|
<button class="btn btn-sm btn-outline-success mt-2" @onclick="OpenAdjustmentModal">
|
|
<i class="fas fa-plus me-1"></i> Agregar ajuste
|
|
</button>
|
|
</div>
|
|
<div class="col-md-4">
|
|
</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="submit" class="btn btn-primary">Guardar</button>
|
|
<button type="button" class="btn btn-secondary ms-2">Cancelar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</EditForm>
|
|
|
|
@code {
|
|
private EQuoteHeader _quoteModel = new();
|
|
private ELookUpItem? _selectedCustomer;
|
|
private ELookUpItem? _selectedProfessional;
|
|
private ELookUpItem? _selectedInstitution;
|
|
private ELookUpItem? _selectedPatient;
|
|
private ELookUpItem? _selectedPerson;
|
|
private List<ELookUpItem> _businessUnits = new();
|
|
private decimal _netAmount = 0;
|
|
private decimal _taxAmount = 0;
|
|
private decimal _grandTotal = 0;
|
|
|
|
private Task OnValueChanged<T>(EQuoteDetail item, T value, Action<EQuoteDetail, T> setter)
|
|
{
|
|
setter(item, value);
|
|
RecalculateTotals();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
_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 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 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)
|
|
=> 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);
|
|
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 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 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 void RemoveAdjustment(EQuoteAdjustment adj)
|
|
{
|
|
_quoteModel.PhSQuoteAdjustments.Remove(adj);
|
|
}
|
|
|
|
}
|