From 0361d4c97893d69d4d4b98d7c1f95326a72a0f9c Mon Sep 17 00:00:00 2001 From: Leandro Hernan Rojas Date: Thu, 11 Sep 2025 22:41:46 -0300 Subject: [PATCH] Add Export QUOTES & refactoring --- Core/Interfaces/IQuoteDom.cs | 2 +- Core/Services/QuoteService.cs | 62 +++++++++++++- Core/Services/Stock/ExpeditionService.cs | 2 +- .../Controllers/Sales/QuoteController.cs | 15 ++++ .../Controllers/Stock/ExpeditionController.cs | 2 +- .../obj/Debug/net8.0/ApiEndpoints.json | 16 ++++ .../Pages/Sales/Quotes/Quotes.razor | 38 ++++++--- .../Pages/Stock/Expeditions/Expeditions.razor | 84 +++++++++++-------- phronCare.UIBlazor/Program.cs | 2 +- .../Services/Sales/Quotes/IQuoteService.cs | 15 ++-- .../Services/Sales/Quotes/QuoteService.cs | 33 +++++++- 11 files changed, 213 insertions(+), 58 deletions(-) diff --git a/Core/Interfaces/IQuoteDom.cs b/Core/Interfaces/IQuoteDom.cs index 7827437..51f3195 100644 --- a/Core/Interfaces/IQuoteDom.cs +++ b/Core/Interfaces/IQuoteDom.cs @@ -11,7 +11,7 @@ namespace Core.Interfaces Task GetDtoByQuoteNumberAsync(string quoteNumber); #endregion #region Exportación - //Task ExportFilteredQuotesToExcelAsync(QuoteSearchParams searchParams); + Task ExportFilteredToExcelAsync(QuoteSearchParams searchParams); #endregion #region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos) Task<(int Id, string Quotenumber)> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId); diff --git a/Core/Services/QuoteService.cs b/Core/Services/QuoteService.cs index 1d5dcaa..fcd3bd6 100644 --- a/Core/Services/QuoteService.cs +++ b/Core/Services/QuoteService.cs @@ -1,9 +1,12 @@ -using Domain.Dtos; +using Core.Interfaces; using Domain.Constants; +using Domain.Dtos; using Domain.Entities; using Domain.Generics; using Models.Interfaces; -using Core.Interfaces; +using System.Drawing.Printing; +using System.Reflection; +using Transversal.Services; namespace Core.Services { @@ -38,6 +41,61 @@ namespace Core.Services { return await _quoteRepository.GetDtoByIdAsync(id); } + public async Task ExportFilteredToExcelAsync(QuoteSearchParams searchParams) + { + try + { + // Realiza la búsqueda de clientes con los parámetros proporcionados + var searchResult = await _quoteRepository.SearchAsync( + searchParams.CustomerId, + searchParams.CustomerText, + searchParams.QuoteNumber, + searchParams.ProfessionalId, + searchParams.ProfessionalText, + searchParams.InstitutionId, + searchParams.InstitutionText, + searchParams.PatientId, + searchParams.PatientText, + searchParams.IssueDateFrom, + searchParams.IssueDateTo, + searchParams.Status, + searchParams.Page, + searchParams.PageSize + ); + // Verifica que se hayan encontrado resultados + if (searchResult?.Items is null || !searchResult.Items.Any()) + { + throw new Exception("No se encontraron clientes para exportar."); + } + // Llamamos a un método que exporta los datos a Excel + var stream = new XLSXExportBase(); + // Convertimos los resultados de la búsqueda a un formato adecuado para el exportador + var items = searchResult.Items.Select(c => new + { + c.Quotenumber, + Issuedate = c.IssueDate.ToString("dd/MM/yyyy"), // ← string + EstimatedDate = c.EstimatedDate?.ToString("dd/MM/yyyy HH:mm"), // ← string + c.Status, + c.CustomerName, + c.ProfessionalName, + c.InstitutionName, + c.PatientName, + c.BusinessUnitName, + c.SalespersonName, + c.Observations, + c.Total + }).ToList(); + // Genera el archivo Excel + var excelFile = stream.ExportExcel(items); + // Devuelve el archivo Excel como un array de bytes + return excelFile; + } + catch (Exception ex) + { + var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + throw new Exception($"{ex.Message}", ex); + } + } //public async Task GetDtoByQuoteNumberAsync(string quoteNumber) //{ diff --git a/Core/Services/Stock/ExpeditionService.cs b/Core/Services/Stock/ExpeditionService.cs index ddba93a..3769e81 100644 --- a/Core/Services/Stock/ExpeditionService.cs +++ b/Core/Services/Stock/ExpeditionService.cs @@ -55,7 +55,7 @@ namespace Core.Services.Stock try { // Realiza la búsqueda de clientes con los parámetros proporcionados - var searchResult = await SearchAsync( + var searchResult = await _repo.SearchAsync( searchParams.Number, searchParams.Status, searchParams.From, diff --git a/phronCare.API/Controllers/Sales/QuoteController.cs b/phronCare.API/Controllers/Sales/QuoteController.cs index ddfda12..381b517 100644 --- a/phronCare.API/Controllers/Sales/QuoteController.cs +++ b/phronCare.API/Controllers/Sales/QuoteController.cs @@ -135,6 +135,21 @@ namespace phronCare.API.Controllers.Sales return File(pdfBytes, "application/pdf", $"Presupuesto_{quote.Quotenumber}.pdf"); } + [HttpPost("exportfiltered")] + public async Task ExportFiltered([FromBody] QuoteSearchParams searchParams) + { + try + { + var file = await _quoteService.ExportFilteredToExcelAsync(searchParams); + return File(file, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "Expediciones.xlsx"); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } #region Endpoint de emision de presupuesto (encabezado + detalles + roles + ajustes + impuestos) [HttpPost("createfull")] diff --git a/phronCare.API/Controllers/Stock/ExpeditionController.cs b/phronCare.API/Controllers/Stock/ExpeditionController.cs index 581e5a1..2cb1419 100644 --- a/phronCare.API/Controllers/Stock/ExpeditionController.cs +++ b/phronCare.API/Controllers/Stock/ExpeditionController.cs @@ -128,8 +128,8 @@ namespace phronCare.API.Controllers.Stock return File(pdfBytes, "application/pdf", $"Expedicion_{expedition.Expeditionnumber}.pdf"); } + [HttpPost("exportfiltered")] - public async Task ExportFiltered([FromBody] ExpeditionSearchParams searchParams) { try diff --git a/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json b/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json index 7da4172..063e378 100644 --- a/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json +++ b/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json @@ -2383,6 +2383,22 @@ ], "ReturnTypes": [] }, + { + "ContainingType": "phronCare.API.Controllers.Sales.QuoteController", + "Method": "ExportFiltered", + "RelativePath": "api/Quote/exportfiltered", + "HttpMethod": "POST", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "searchParams", + "Type": "Domain.Generics.QuoteSearchParams", + "IsRequired": true + } + ], + "ReturnTypes": [] + }, { "ContainingType": "phronCare.API.Controllers.Sales.QuoteController", "Method": "Search", diff --git a/phronCare.UIBlazor/Pages/Sales/Quotes/Quotes.razor b/phronCare.UIBlazor/Pages/Sales/Quotes/Quotes.razor index f477271..7cc8348 100644 --- a/phronCare.UIBlazor/Pages/Sales/Quotes/Quotes.razor +++ b/phronCare.UIBlazor/Pages/Sales/Quotes/Quotes.razor @@ -1,10 +1,9 @@ @page "/quotes" - @using Domain.Dtos @using Domain.Generics @using phronCare.UIBlazor.Services.Sales.Quotes @inject NavigationManager Navigation -@inject QuoteService quoteService +@inject IQuoteService quoteService @inject IToastService toastService
@@ -14,7 +13,7 @@
-
+
@@ -58,21 +57,24 @@
- - - +
-
+
@@ -139,8 +141,8 @@
-
-
+
+
@@ -285,9 +287,8 @@
} - @code { - private QuoteSearchParams Filters = new() { PageSize = 9 }; + private QuoteSearchParams Filters = new() { PageSize = 10 }; private PagedResult? PagedQuotes; private QuoteDto? SelectedQuote { get; set; } private bool IsLoading; @@ -399,5 +400,18 @@ { Navigation.NavigateTo($"/quotes/authorize/{id}"); } - + private async Task ExportarExcel() + { + Filters.Page = 1; + Filters.PageSize = int.MaxValue; // Exportar todos los resultados + try + { + await quoteService.ExportFilteredAsync(Filters); + toastService.ShowSuccess("Exportación completada."); + } + catch (Exception ex) + { + toastService.ShowError($"Error: {ex.Message}"); + } + } } diff --git a/phronCare.UIBlazor/Pages/Stock/Expeditions/Expeditions.razor b/phronCare.UIBlazor/Pages/Stock/Expeditions/Expeditions.razor index 8536f43..1f0ff4f 100644 --- a/phronCare.UIBlazor/Pages/Stock/Expeditions/Expeditions.razor +++ b/phronCare.UIBlazor/Pages/Stock/Expeditions/Expeditions.razor @@ -22,13 +22,13 @@
- - + +
- - + + @@ -40,24 +40,24 @@
- - + +
- - + +
- - + + @* TODO: reemplazar por BlazoredTypeahead cuando conectes lookup de ubicaciones *@
- - + + @@ -72,13 +72,13 @@ - + - -
+
@@ -127,9 +127,13 @@ } } + else if (IsLoading) + { + Cargando... + } else { - Sin resultados + Sin resultados } @@ -168,11 +172,13 @@ @code { private LSProductSearchParams SearchParams = new() { Page = 1, PageSize = 10 }; - private Filters filters = new(); - private PagedResult? result; - private int page = 1; - private int pageSize = 10; - private int TotalPages => result is null ? 1 : (int)Math.Ceiling((double)result.TotalItems / result.PageSize); + private Filters filters = new(); + private PagedResult? result; + private bool IsLoading; + + private int page = 1; + private int pageSize = 10; + private int TotalPages => result is null ? 1 : (int)Math.Ceiling((double)result.TotalItems / result.PageSize); private ExpeditionDto? selected; private bool drawerOpen; @@ -203,16 +209,28 @@ private async Task Search() { - result = await expeditionService.SearchAsync( - expeditionNumber: filters.Number, - status: filters.Status, - issueDateFrom: filters.From, - issueDateTo: filters.To, - locationId: filters.LocationId, - page: page, - pageSize: pageSize); - StateHasChanged(); - } + try + { + IsLoading = true; + result = await expeditionService.SearchAsync( + expeditionNumber: filters.Number, + status: filters.Status, + issueDateFrom: filters.From, + issueDateTo: filters.To, + locationId: filters.LocationId, + page: page, + pageSize: pageSize); + StateHasChanged(); + } + catch (Exception ex) + { + Toast.ShowError(ex.Message); + } + finally + { + IsLoading = false; + } + } private void Clear() { diff --git a/phronCare.UIBlazor/Program.cs b/phronCare.UIBlazor/Program.cs index 066ba96..8fbb26d 100644 --- a/phronCare.UIBlazor/Program.cs +++ b/phronCare.UIBlazor/Program.cs @@ -54,9 +54,9 @@ static void InjectDependencies(WebAssemblyHostBuilder builder) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); - builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/phronCare.UIBlazor/Services/Sales/Quotes/IQuoteService.cs b/phronCare.UIBlazor/Services/Sales/Quotes/IQuoteService.cs index 8944e1f..7666b2b 100644 --- a/phronCare.UIBlazor/Services/Sales/Quotes/IQuoteService.cs +++ b/phronCare.UIBlazor/Services/Sales/Quotes/IQuoteService.cs @@ -1,11 +1,16 @@ -using Domain.Entities; -using phronCare.UIBlazor.Services.Sales.Quotes; +using Domain.Dtos; +using Domain.Entities; +using Domain.Generics; -namespace Services.Sales.Quotes +namespace phronCare.UIBlazor.Services.Sales.Quotes { public interface IQuoteService { - //Task CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId); - // Aquí podrías agregar otros métodos: GetById, Search, etc. + Task AuthorizeQuoteAsync(QuoteAuthorizationRequest request); + Task CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId); + Task ExportFilteredAsync(QuoteSearchParams searchParams); + Task ExportPdfAsync(int quoteId, string quoteNumber); + Task GetDtoByIdAsync(int id); + Task> SearchAsync(int? customerId = null, string? customerText = null, string? quoteNumber = null, int? professionalId = null, string? professionalText = null, int? institutionId = null, string? institutionText = null, int? patientId = null, string? patientText = null, string? status = null, DateTime? issueDateFrom = null, DateTime? issueDateTo = null, int page = 1, int pageSize = 10); } } diff --git a/phronCare.UIBlazor/Services/Sales/Quotes/QuoteService.cs b/phronCare.UIBlazor/Services/Sales/Quotes/QuoteService.cs index ea62446..6598a51 100644 --- a/phronCare.UIBlazor/Services/Sales/Quotes/QuoteService.cs +++ b/phronCare.UIBlazor/Services/Sales/Quotes/QuoteService.cs @@ -3,10 +3,13 @@ using Domain.Entities; using Domain.Generics; using Microsoft.JSInterop; using System.Net.Http.Json; +using System.Reflection; +using System.Text; +using System.Text.Json; namespace phronCare.UIBlazor.Services.Sales.Quotes { - public class QuoteService + public class QuoteService:IQuoteService { private readonly IJSRuntime _js; private readonly HttpClient _http; @@ -156,7 +159,33 @@ namespace phronCare.UIBlazor.Services.Sales.Quotes return null; } } - + public async Task ExportFilteredAsync(QuoteSearchParams searchParams) + { + try + { + var content = new StringContent(JsonSerializer.Serialize(searchParams), Encoding.UTF8, "application/json"); + var response = await _http.PostAsync("api/quote/exportfiltered", content); + + //response.EnsureSuccessStatusCode(); + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(); + throw new Exception(errorContent); + } + var bytes = await response.Content.ReadAsByteArrayAsync(); + var base64 = Convert.ToBase64String(bytes); + var timestamp = DateTime.Now.ToString("yyyyMMddHHmm"); + var fileName = $"{timestamp}_quotes.xlsx"; + await _js.InvokeVoidAsync("saveAsFile", fileName, base64); + } + catch (Exception ex) + { + var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + var message = ex.Message ?? "No message provided"; + throw new Exception($"{message}", ex); + } + } + } public class CreateQuoteResult