From af91f6be5c10c80c6d0a4b0044628eb90da58793 Mon Sep 17 00:00:00 2001 From: leandro Date: Tue, 24 Mar 2026 16:34:38 -0300 Subject: [PATCH] feat(sales): implement delivery note persistence on issue closes #37 --- Core/Services/DeliveryNoteService.cs | 41 ++- .../DeliveryNotes/DeliveryNoteCreate.razor | 304 ++++++++++++++++++ .../Sales/DeliveryNotes/DeliveryNotes.razor | 2 +- .../DeliveryNotes/DeliveryNoteService.cs | 18 +- .../DeliveryNotes/IDeliveryNoteService.cs | 1 + 5 files changed, 356 insertions(+), 10 deletions(-) create mode 100644 phronCare.UIBlazor/Pages/Sales/DeliveryNotes/DeliveryNoteCreate.razor diff --git a/Core/Services/DeliveryNoteService.cs b/Core/Services/DeliveryNoteService.cs index c046236..e5e45b8 100644 --- a/Core/Services/DeliveryNoteService.cs +++ b/Core/Services/DeliveryNoteService.cs @@ -62,7 +62,7 @@ namespace Core.Services return _deliveryNoteRepository.GetDtosByQuoteIdAsync(quoteId); } - public Task CreateAndIssueDeliveryNoteAsync(DeliveryNoteCreateRequest request) + public async Task 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 + }; } } } diff --git a/phronCare.UIBlazor/Pages/Sales/DeliveryNotes/DeliveryNoteCreate.razor b/phronCare.UIBlazor/Pages/Sales/DeliveryNotes/DeliveryNoteCreate.razor new file mode 100644 index 0000000..b17c84b --- /dev/null +++ b/phronCare.UIBlazor/Pages/Sales/DeliveryNotes/DeliveryNoteCreate.razor @@ -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 + + + + + +
+
+
+

Emisión de Remito

+
+
+
+
+ + + +
+
+ + + +
+
+ + + @item.Nombre + @item.Nombre + +
+
+ +
+
+ + + @item.Nombre + @item.Nombre + + +
+
+ + +
+
+ + @if (SelectedQuote is not null) + { +
+ Presupuesto vinculado: @SelectedQuote.Nombre +
+ } +
+
+ +
+
+
Ítems del remito
+ +
+
+ @if (Items.Any()) + { +
+ + + + + + + + + + + + + @foreach (var item in Items) + { + + + + + + + + + } + +
#DescripciónCantidadOrigenNotas
@item.LineNumber + + + + + + + + + + + + + + +
+
+ } + else + { +
+ No hay ítems cargados. +
+ } +
+
+ +
+ + +
+
+
+ +@code { + private DeliveryNoteCreatePageModel Model = new() + { + IssueDate = DateTime.Today + }; + + private ELookUpItem? SelectedCustomer; + private ELookUpItem? SelectedQuote; + private List 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; } + } +} diff --git a/phronCare.UIBlazor/Pages/Sales/DeliveryNotes/DeliveryNotes.razor b/phronCare.UIBlazor/Pages/Sales/DeliveryNotes/DeliveryNotes.razor index 1642223..31e333e 100644 --- a/phronCare.UIBlazor/Pages/Sales/DeliveryNotes/DeliveryNotes.razor +++ b/phronCare.UIBlazor/Pages/Sales/DeliveryNotes/DeliveryNotes.razor @@ -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() diff --git a/phronCare.UIBlazor/Services/Sales/DeliveryNotes/DeliveryNoteService.cs b/phronCare.UIBlazor/Services/Sales/DeliveryNotes/DeliveryNoteService.cs index 4b89ad2..c3c779f 100644 --- a/phronCare.UIBlazor/Services/Sales/DeliveryNotes/DeliveryNoteService.cs +++ b/phronCare.UIBlazor/Services/Sales/DeliveryNotes/DeliveryNoteService.cs @@ -78,5 +78,21 @@ namespace phronCare.UIBlazor.Services.Sales.DeliveryNotes return Enumerable.Empty(); } } + + public async Task 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(); + return result ?? throw new Exception("Respuesta vacía del servidor."); + } } -} \ No newline at end of file +} diff --git a/phronCare.UIBlazor/Services/Sales/DeliveryNotes/IDeliveryNoteService.cs b/phronCare.UIBlazor/Services/Sales/DeliveryNotes/IDeliveryNoteService.cs index 43d2567..f26b1ca 100644 --- a/phronCare.UIBlazor/Services/Sales/DeliveryNotes/IDeliveryNoteService.cs +++ b/phronCare.UIBlazor/Services/Sales/DeliveryNotes/IDeliveryNoteService.cs @@ -9,5 +9,6 @@ namespace phronCare.UIBlazor.Services.Sales.DeliveryNotes Task GetByIdAsync(int id); Task GetByDeliveryNoteNumberAsync(string deliveryNoteNumber); Task> GetByQuoteIdAsync(int quoteId); + Task CreateAndIssueAsync(DeliveryNoteCreateRequest request); } }