Add Export QUOTES & refactoring
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m2s
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m2s
This commit is contained in:
parent
4c99757fb4
commit
0361d4c978
@ -11,7 +11,7 @@ namespace Core.Interfaces
|
||||
Task<QuoteDto?> GetDtoByQuoteNumberAsync(string quoteNumber);
|
||||
#endregion
|
||||
#region Exportación
|
||||
//Task<byte[]> ExportFilteredQuotesToExcelAsync(QuoteSearchParams searchParams);
|
||||
Task<byte[]> ExportFilteredToExcelAsync(QuoteSearchParams searchParams);
|
||||
#endregion
|
||||
#region Guardado completo de presupuesto (encabezado + detalles + roles + ajustes + impuestos)
|
||||
Task<(int Id, string Quotenumber)> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
|
||||
|
||||
@ -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<byte[]> 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<QuoteDto?> GetDtoByQuoteNumberAsync(string quoteNumber)
|
||||
//{
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -135,6 +135,21 @@ namespace phronCare.API.Controllers.Sales
|
||||
|
||||
return File(pdfBytes, "application/pdf", $"Presupuesto_{quote.Quotenumber}.pdf");
|
||||
}
|
||||
[HttpPost("exportfiltered")]
|
||||
public async Task<IActionResult> 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")]
|
||||
|
||||
@ -128,8 +128,8 @@ namespace phronCare.API.Controllers.Stock
|
||||
|
||||
return File(pdfBytes, "application/pdf", $"Expedicion_{expedition.Expeditionnumber}.pdf");
|
||||
}
|
||||
[HttpPost("exportfiltered")]
|
||||
|
||||
[HttpPost("exportfiltered")]
|
||||
public async Task<IActionResult> ExportFiltered([FromBody] ExpeditionSearchParams searchParams)
|
||||
{
|
||||
try
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
<div class="card shadow-sm mb-3" style="zoom: 0.8;">
|
||||
@ -14,7 +13,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- BODY -->
|
||||
<div class="card-body">
|
||||
<div class="card-body pt-2 pb-0">
|
||||
<!-- FILTROS -->
|
||||
<div class="mb-3 row g-2 align-items-end">
|
||||
<div class="col-sm">
|
||||
@ -58,21 +57,24 @@
|
||||
<InputDate id="dateto" @bind-Value="Filters.IssueDateTo" class="form-control form-control-sm" />
|
||||
</div>
|
||||
<div class="d-flex justify-content-end gap-2 mt-3">
|
||||
<button class="btn btn-primary btn-sm rounded-pill" @onclick="Search">
|
||||
<button class="btn btn-primary rounded-pill" @onclick="Search">
|
||||
<i class="fas fa-binoculars me-1"></i> Buscar
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm rounded-pill ms-1" @onclick="OnClear">
|
||||
<button class="btn btn-secondary rounded-pill ms-1" @onclick="OnClear">
|
||||
<i class="fas fa-eraser me-1"></i> Limpiar
|
||||
</button>
|
||||
<button class="btn btn-success btn-sm rounded-pill" @onclick="Create">
|
||||
<button class="btn btn-success rounded-pill" @onclick="Create">
|
||||
<i class="fas fa-plus me-1"></i> Nuevo
|
||||
</button>
|
||||
<button class="btn btn-success rounded-pill" @onclick="ExportarExcel">
|
||||
<i class="fas fa-file-excel me-1"></i> Excel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm" style="zoom:0.8;">
|
||||
<div class="table-responsive">
|
||||
<div class="table-responsive" style="zoom:0.8;">
|
||||
<!-- TABLA DE RESULTADOS -->
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
@ -139,8 +141,8 @@
|
||||
</table>
|
||||
</div>
|
||||
<!-- Paginación debajo -->
|
||||
<div class="d-flex justify-content-between align-items-center px-3 py-2 border-top">
|
||||
<div class="btn-group">
|
||||
<div class="d-flex justify-content-center align-items-center px-3 py-2 border-top">
|
||||
<div class="btn-group justify-content-center">
|
||||
<button class="btn btn-outline-secondary btn-sm rounded-pill" @onclick="PrimeraPagina" disabled="@(Filters.Page == 1)">Primera</button>
|
||||
<button class="btn btn-outline-secondary btn-sm rounded-pill" @onclick="AnteriorPagina" disabled="@(!PuedeRetroceder)">Anterior</button>
|
||||
<span class="mx-2">
|
||||
@ -285,9 +287,8 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@code {
|
||||
private QuoteSearchParams Filters = new() { PageSize = 9 };
|
||||
private QuoteSearchParams Filters = new() { PageSize = 10 };
|
||||
private PagedResult<QuoteDto>? 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,13 +22,13 @@
|
||||
<div class="row g-2 align-items-end">
|
||||
<!-- En monitores grandes queda todo en una fila (col-xxl-2 = 6 columnas por fila) -->
|
||||
<div class="col-12 col-sm-6 col-md-4 col-lg-3 col-xxl-2">
|
||||
<label class="form-label mb-1">Número</label>
|
||||
<InputText class="form-control form-control-sm" @bind-Value="filters.Number" />
|
||||
<label for="number" class="form-label mb-1">Número</label>
|
||||
<InputText id="number" class="form-control form-control-sm" @bind-Value="filters.Number" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6 col-md-4 col-lg-3 col-xxl-2">
|
||||
<label class="form-label mb-1">Estado</label>
|
||||
<InputSelect class="form-select form-select-sm" @bind-Value="filters.Status">
|
||||
<label for="status" class="form-label mb-1">Estado</label>
|
||||
<InputSelect id="status" class="form-select form-select-sm" @bind-Value="filters.Status">
|
||||
<option value="">(Todos)</option>
|
||||
<option value="Emitida">Emitida</option>
|
||||
<option value="EnTransito">En tránsito</option>
|
||||
@ -40,24 +40,24 @@
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6 col-md-4 col-lg-3 col-xxl-2">
|
||||
<label class="form-label mb-1">Fecha desde</label>
|
||||
<InputDate class="form-control form-control-sm" @bind-Value="filters.From" />
|
||||
<label for="from" class="form-label mb-1">Fecha desde</label>
|
||||
<InputDate id="from" class="form-control form-control-sm" @bind-Value="filters.From" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6 col-md-4 col-lg-3 col-xxl-2">
|
||||
<label class="form-label mb-1">Fecha hasta</label>
|
||||
<InputDate class="form-control form-control-sm" @bind-Value="filters.To" />
|
||||
<label for="to" class="form-label mb-1">Fecha hasta</label>
|
||||
<InputDate id="to" class="form-control form-control-sm" @bind-Value="filters.To" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6 col-md-4 col-lg-3 col-xxl-2">
|
||||
<label class="form-label mb-1">Ubicación</label>
|
||||
<InputNumber class="form-control form-control-sm" @bind-Value="filters.LocationId" />
|
||||
<label for="location" class="form-label mb-1">Ubicación</label>
|
||||
<InputNumber id="location" class="form-control form-control-sm" @bind-Value="filters.LocationId" />
|
||||
@* TODO: reemplazar por BlazoredTypeahead cuando conectes lookup de ubicaciones *@
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6 col-md-4 col-lg-3 col-xxl-2">
|
||||
<label class="form-label mb-1">Tam. página</label>
|
||||
<InputSelect class="form-select form-select-sm" @bind-Value="pageSize" @onchange="@(e => ChangePageSize(e.Value?.ToString()))">
|
||||
<label for="pageg" class="form-label mb-1">Tam. página</label>
|
||||
<InputSelect id="pageg"class="form-select form-select-sm" @bind-Value="pageSize" @onchange="@(e => ChangePageSize(e.Value?.ToString()))">
|
||||
<option value="10">10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
@ -72,13 +72,13 @@
|
||||
<button class="btn btn-success rounded-pill" @onclick="Create">
|
||||
<i class="fas fa-plus me-1"></i> Nuevo
|
||||
</button>
|
||||
<button class="btn btn-success rounded-pill" @onclick="ExportarExcel">
|
||||
<button class="btn btn-secondary rounded-pill" @onclick="Clear">
|
||||
<i class="fas fa-eraser me-1"></i> Limpiar
|
||||
</button>
|
||||
<button class="btn btn-success rounded-pill" @onclick="ExportarExcel">
|
||||
<i class="fas fa-file-excel me-1"></i> Excel
|
||||
</button>
|
||||
<button class="btn btn-secondary rounded-pill" @onclick="Clear">
|
||||
<i class="fas fa-arrow-left me-1"></i> Volver
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
</div>
|
||||
@ -127,9 +127,13 @@
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
else if (IsLoading)
|
||||
{
|
||||
<tr><td colspan="9" class="text-center text-muted py-4">Cargando...</td></tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
<tr><td colspan="9" class="text-center text-muted py-4">Sin resultados</td></tr>
|
||||
<tr><td colspan="9" class="text-center text-muted py-4">Sin resultados</td></tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -168,11 +172,13 @@
|
||||
@code {
|
||||
private LSProductSearchParams SearchParams = new() { Page = 1, PageSize = 10 };
|
||||
|
||||
private Filters filters = new();
|
||||
private PagedResult<ExpeditionDto>? 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<ExpeditionDto>? 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()
|
||||
{
|
||||
|
||||
@ -54,9 +54,9 @@ static void InjectDependencies(WebAssemblyHostBuilder builder)
|
||||
builder.Services.AddScoped<IExchangeRateService, ExchangeRateService>();
|
||||
builder.Services.AddScoped<IStockScanService, StockScanService>();
|
||||
builder.Services.AddScoped<IExpeditionService, ExpeditionService>();
|
||||
builder.Services.AddScoped<IQuoteService,QuoteService>();
|
||||
|
||||
builder.Services.AddScoped<ExchangeRateService>();
|
||||
builder.Services.AddScoped<QuoteService>();
|
||||
builder.Services.AddScoped<TicketsService>();
|
||||
builder.Services.AddScoped<CustomerService>();
|
||||
builder.Services.AddScoped<TaxConditionService>();
|
||||
|
||||
@ -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<CreateQuoteResult> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
|
||||
// Aquí podrías agregar otros métodos: GetById, Search, etc.
|
||||
Task<bool> AuthorizeQuoteAsync(QuoteAuthorizationRequest request);
|
||||
Task<CreateQuoteResult> CreateFullQuoteAsync(EQuoteHeader quote, int formSeriesId);
|
||||
Task ExportFilteredAsync(QuoteSearchParams searchParams);
|
||||
Task ExportPdfAsync(int quoteId, string quoteNumber);
|
||||
Task<QuoteDto?> GetDtoByIdAsync(int id);
|
||||
Task<PagedResult<QuoteDto>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,6 +159,32 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user