feat(sales): implement delivery note persistence on issue #38
@ -62,7 +62,7 @@ namespace Core.Services
|
||||
return _deliveryNoteRepository.GetDtosByQuoteIdAsync(quoteId);
|
||||
}
|
||||
|
||||
public Task<DeliveryNoteCreateResponse> CreateAndIssueDeliveryNoteAsync(DeliveryNoteCreateRequest request)
|
||||
public async Task<DeliveryNoteCreateResponse> CreateAndIssueDeliveryNoteAsync(DeliveryNoteCreateRequest request)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
@ -84,21 +84,46 @@ namespace Core.Services
|
||||
if (request.Items.Any(i => string.IsNullOrWhiteSpace(i.Description)))
|
||||
throw new InvalidOperationException("Todos los ítems deben incluir descripción.");
|
||||
|
||||
var deliveryNoteNumber = request.DeliveryNoteNumber.Trim();
|
||||
|
||||
if (await _deliveryNoteRepository.ExistsByDeliveryNoteNumberAsync(deliveryNoteNumber))
|
||||
throw new InvalidOperationException($"Ya existe un remito con el número '{deliveryNoteNumber}'.");
|
||||
|
||||
var now = DateTime.Now;
|
||||
|
||||
var entity = new EDeliveryNote
|
||||
{
|
||||
Deliverynotenumber = request.DeliveryNoteNumber.Trim(),
|
||||
Deliverynotenumber = deliveryNoteNumber,
|
||||
QuoteId = request.QuoteId,
|
||||
Issuedate = request.IssueDate,
|
||||
CustomerId = request.CustomerId,
|
||||
Observations = request.Observations,
|
||||
ExtrainfoJson = request.ExtraInfoJson
|
||||
Status = "Emitido",
|
||||
Observations = string.IsNullOrWhiteSpace(request.Observations) ? null : request.Observations.Trim(),
|
||||
ExtrainfoJson = string.IsNullOrWhiteSpace(request.ExtraInfoJson) ? null : request.ExtraInfoJson.Trim(),
|
||||
Printcount = 0,
|
||||
Createdat = now,
|
||||
PhSDeliveryNoteDetails = request.Items
|
||||
.Select((item, index) => new EDeliveryNoteDetail
|
||||
{
|
||||
LineNumber = index + 1,
|
||||
OriginType = item.OriginType,
|
||||
OriginId = item.OriginId,
|
||||
QuoteDetailId = item.QuoteDetailId,
|
||||
Description = item.Description.Trim(),
|
||||
Quantity = item.Quantity,
|
||||
Notes = string.IsNullOrWhiteSpace(item.Notes) ? string.Empty : item.Notes.Trim(),
|
||||
Createdat = now
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
|
||||
return Task.FromResult(new DeliveryNoteCreateResponse
|
||||
var created = await _deliveryNoteRepository.CreateAsync(entity);
|
||||
|
||||
return new DeliveryNoteCreateResponse
|
||||
{
|
||||
Id = entity.Id,
|
||||
DeliveryNoteNumber = entity.Deliverynotenumber
|
||||
});
|
||||
Id = created.Id,
|
||||
DeliveryNoteNumber = created.Deliverynotenumber
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,304 @@
|
||||
@page "/deliverynotes/create"
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Blazored.Typeahead
|
||||
@using Domain.Constants
|
||||
@using Domain.Dtos.Sales
|
||||
@using phronCare.UIBlazor.Services.Lookups
|
||||
@using phronCare.UIBlazor.Services.Sales.DeliveryNotes
|
||||
|
||||
@inject NavigationManager Navigation
|
||||
@inject IDeliveryNoteService DeliveryNoteService
|
||||
@inject ISalesLookupService SalesLookupService
|
||||
@inject IToastService toastService
|
||||
|
||||
<EditForm Model="Model" OnValidSubmit="HandleValidSubmit">
|
||||
<DataAnnotationsValidator />
|
||||
<ValidationSummary />
|
||||
|
||||
<div class="container mt-4" style="zoom:.8;">
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header d-flex justify-content-center align-items-center">
|
||||
<h3 class="mb-0">Emisión de Remito</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label for="deliveryNoteNumber" class="form-label">Número de remito</label>
|
||||
<InputText id="deliveryNoteNumber" class="form-control" @bind-Value="Model.DeliveryNoteNumber" />
|
||||
<ValidationMessage For="@(() => Model.DeliveryNoteNumber)" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="issueDate" class="form-label">Fecha de emisión</label>
|
||||
<InputDate id="issueDate" class="form-control" @bind-Value="Model.IssueDate" />
|
||||
<ValidationMessage For="@(() => Model.IssueDate)" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="quoteLookup" class="form-label">Presupuesto aprobado (opcional)</label>
|
||||
<BlazoredTypeahead id="quoteLookup" TItem="ELookUpItem" TValue="ELookUpItem"
|
||||
SearchMethod="SalesLookupService.SearchApprovedQuotesAsync"
|
||||
Value="SelectedQuote"
|
||||
ValueChanged="OnQuoteSelected"
|
||||
ValueExpression="@(() => SelectedQuote)"
|
||||
MaximumSuggestions="10"
|
||||
Placeholder="Buscar presupuesto aprobado..."
|
||||
TextProperty="Nombre">
|
||||
<ResultTemplate Context="item">@item.Nombre</ResultTemplate>
|
||||
<SelectedTemplate Context="item">@item.Nombre</SelectedTemplate>
|
||||
</BlazoredTypeahead>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="customerLookup" class="form-label">Cliente</label>
|
||||
<BlazoredTypeahead id="customerLookup" TItem="ELookUpItem" TValue="ELookUpItem"
|
||||
SearchMethod="SalesLookupService.SearchCustomersAsync"
|
||||
Value="SelectedCustomer"
|
||||
ValueChanged="OnCustomerSelected"
|
||||
ValueExpression="@(() => SelectedCustomer)"
|
||||
MaximumSuggestions="10"
|
||||
Placeholder="Buscar cliente..."
|
||||
TextProperty="Nombre">
|
||||
<ResultTemplate Context="item">@item.Nombre</ResultTemplate>
|
||||
<SelectedTemplate Context="item">@item.Nombre</SelectedTemplate>
|
||||
</BlazoredTypeahead>
|
||||
<ValidationMessage For="@(() => Model.CustomerId)" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="observations" class="form-label">Observaciones</label>
|
||||
<InputTextArea id="observations" class="form-control" rows="3" @bind-Value="Model.Observations" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (SelectedQuote is not null)
|
||||
{
|
||||
<div class="alert alert-light border mb-0">
|
||||
<strong>Presupuesto vinculado:</strong> @SelectedQuote.Nombre
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Ítems del remito</h5>
|
||||
<button type="button" class="btn btn-outline-success btn-sm rounded-pill" @onclick="AddItem">
|
||||
<i class="fas fa-plus me-1"></i> Agregar ítem
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
@if (Items.Any())
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 60px;" class="text-center">#</th>
|
||||
<th>Descripción</th>
|
||||
<th style="width: 120px;" class="text-center">Cantidad</th>
|
||||
<th style="width: 160px;" class="text-center">Origen</th>
|
||||
<th>Notas</th>
|
||||
<th style="width: 60px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var item in Items)
|
||||
{
|
||||
<tr>
|
||||
<td class="text-center">@item.LineNumber</td>
|
||||
<td>
|
||||
<InputText class="form-control form-control-sm" @bind-Value="item.Description" />
|
||||
</td>
|
||||
<td>
|
||||
<InputNumber class="form-control form-control-sm text-end" @bind-Value="item.Quantity" />
|
||||
</td>
|
||||
<td>
|
||||
<InputSelect class="form-select form-select-sm" @bind-Value="item.OriginType">
|
||||
<option value="@((byte)DeliveryNoteItemOriginType.Manual)">Manual</option>
|
||||
<option value="@((byte)DeliveryNoteItemOriginType.QuoteDetail)">Presupuesto</option>
|
||||
<option value="@((byte)DeliveryNoteItemOriginType.SalesProduct)">Producto venta</option>
|
||||
<option value="@((byte)DeliveryNoteItemOriginType.StockProduct)">Producto stock</option>
|
||||
</InputSelect>
|
||||
</td>
|
||||
<td>
|
||||
<InputText class="form-control form-control-sm" @bind-Value="item.Notes" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button type="button" class="btn btn-link p-0 text-danger" title="Eliminar" @onclick="() => RemoveItem(item)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center text-muted py-4">
|
||||
No hay ítems cargados.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button type="button" class="btn btn-secondary rounded-pill" @onclick="BackToList" disabled="@IsSaving">
|
||||
<i class="fas fa-arrow-left me-1"></i> Volver
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary rounded-pill" disabled="@IsSaving">
|
||||
@if (IsSaving)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||
}
|
||||
<i class="fas fa-save me-1"></i> Emitir remito
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
@code {
|
||||
private DeliveryNoteCreatePageModel Model = new()
|
||||
{
|
||||
IssueDate = DateTime.Today
|
||||
};
|
||||
|
||||
private ELookUpItem? SelectedCustomer;
|
||||
private ELookUpItem? SelectedQuote;
|
||||
private List<DeliveryNoteItemRow> Items = new();
|
||||
|
||||
private bool IsSaving;
|
||||
|
||||
private void AddItem()
|
||||
{
|
||||
Items.Add(new DeliveryNoteItemRow
|
||||
{
|
||||
LineNumber = Items.Count + 1,
|
||||
OriginType = (byte)DeliveryNoteItemOriginType.Manual,
|
||||
Quantity = 1
|
||||
});
|
||||
}
|
||||
|
||||
private void RemoveItem(DeliveryNoteItemRow item)
|
||||
{
|
||||
if (Items.Remove(item))
|
||||
{
|
||||
ReindexItems();
|
||||
}
|
||||
}
|
||||
|
||||
private void ReindexItems()
|
||||
{
|
||||
for (var i = 0; i < Items.Count; i++)
|
||||
{
|
||||
Items[i].LineNumber = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
private Task OnCustomerSelected(ELookUpItem? customer)
|
||||
{
|
||||
SelectedCustomer = customer;
|
||||
Model.CustomerId = customer?.Id;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task OnQuoteSelected(ELookUpItem? quote)
|
||||
{
|
||||
SelectedQuote = quote;
|
||||
Model.QuoteId = quote?.Id;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private string? ValidateBeforeSave()
|
||||
{
|
||||
if (Items.Count == 0)
|
||||
return "Debe incluir al menos un ítem.";
|
||||
|
||||
if (Items.Any(x => string.IsNullOrWhiteSpace(x.Description)))
|
||||
return "Todos los ítems deben tener descripción.";
|
||||
|
||||
if (Items.Any(x => x.Quantity <= 0))
|
||||
return "Todos los ítems deben tener cantidad mayor a cero.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task HandleValidSubmit()
|
||||
{
|
||||
var validationError = ValidateBeforeSave();
|
||||
if (!string.IsNullOrWhiteSpace(validationError))
|
||||
{
|
||||
toastService.ShowError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IsSaving = true;
|
||||
|
||||
var request = new DeliveryNoteCreateRequest
|
||||
{
|
||||
DeliveryNoteNumber = Model.DeliveryNoteNumber.Trim(),
|
||||
IssueDate = Model.IssueDate!.Value,
|
||||
CustomerId = Model.CustomerId!.Value,
|
||||
QuoteId = Model.QuoteId,
|
||||
Observations = Model.Observations,
|
||||
ExtraInfoJson = Model.ExtraInfoJson,
|
||||
Items = Items.Select(x => new DeliveryNoteCreateItemRequest
|
||||
{
|
||||
OriginType = x.OriginType,
|
||||
OriginId = x.OriginId,
|
||||
QuoteDetailId = x.QuoteDetailId,
|
||||
Description = x.Description.Trim(),
|
||||
Quantity = x.Quantity,
|
||||
Notes = string.IsNullOrWhiteSpace(x.Notes) ? null : x.Notes.Trim()
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
var response = await DeliveryNoteService.CreateAndIssueAsync(request);
|
||||
toastService.ShowSuccess($"Remito {response.DeliveryNoteNumber} emitido correctamente.");
|
||||
Navigation.NavigateTo("/deliverynotes");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
toastService.ShowError(ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void BackToList()
|
||||
{
|
||||
Navigation.NavigateTo("/deliverynotes");
|
||||
}
|
||||
|
||||
private sealed class DeliveryNoteCreatePageModel
|
||||
{
|
||||
[Required(ErrorMessage = "El número de remito es obligatorio.")]
|
||||
public string DeliveryNoteNumber { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "La fecha de emisión es obligatoria.")]
|
||||
public DateTime? IssueDate { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "El cliente es obligatorio.")]
|
||||
public int? CustomerId { get; set; }
|
||||
|
||||
public int? QuoteId { get; set; }
|
||||
public string? Observations { get; set; }
|
||||
public string? ExtraInfoJson { get; set; }
|
||||
}
|
||||
|
||||
private sealed class DeliveryNoteItemRow
|
||||
{
|
||||
public int LineNumber { get; set; }
|
||||
public byte OriginType { get; set; }
|
||||
public int? OriginId { get; set; }
|
||||
public int? QuoteDetailId { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public decimal Quantity { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
}
|
||||
@ -201,7 +201,7 @@
|
||||
|
||||
private void Create()
|
||||
{
|
||||
toastService.ShowInfo("La creación de remitos se implementará en una próxima story.");
|
||||
Navigation.NavigateTo("/deliverynotes/create");
|
||||
}
|
||||
|
||||
private void OnClear()
|
||||
|
||||
@ -78,5 +78,21 @@ namespace phronCare.UIBlazor.Services.Sales.DeliveryNotes
|
||||
return Enumerable.Empty<DeliveryNoteDto>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DeliveryNoteCreateResponse> CreateAndIssueAsync(DeliveryNoteCreateRequest request)
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync("/api/deliverynote/issue", request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var serverMessage = await response.Content.ReadAsStringAsync();
|
||||
throw new Exception(string.IsNullOrWhiteSpace(serverMessage)
|
||||
? "No se pudo emitir el remito."
|
||||
: serverMessage);
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<DeliveryNoteCreateResponse>();
|
||||
return result ?? throw new Exception("Respuesta vacía del servidor.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,5 +9,6 @@ namespace phronCare.UIBlazor.Services.Sales.DeliveryNotes
|
||||
Task<DeliveryNoteDto?> GetByIdAsync(int id);
|
||||
Task<DeliveryNoteDto?> GetByDeliveryNoteNumberAsync(string deliveryNoteNumber);
|
||||
Task<IEnumerable<DeliveryNoteDto>> GetByQuoteIdAsync(int quoteId);
|
||||
Task<DeliveryNoteCreateResponse> CreateAndIssueAsync(DeliveryNoteCreateRequest request);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user