Add Update API UI Customers CRUD
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 5m16s

This commit is contained in:
Leandro Hernan Rojas 2025-04-14 17:50:25 -03:00
parent 0177366fc9
commit d5722495ae
18 changed files with 266 additions and 173 deletions

View File

@ -1,21 +1,17 @@
using Domain.Entities;
using Domain.Generics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.Interfaces
{
public interface ICustomerDom
{
Task<ECustomer> CreateAsync(ECustomer entity);
Task<bool> DeleteAsync(int id);
Task<bool> UpdateAsync(ECustomer entity);
Task<PagedResult<ECustomer>> GetAllAsync(int page = 1, int pageSize = 50);
Task<ECustomer?> GetByIdAsync(int id);
Task<PagedResult<ECustomer>> SearchAsync(string? name, string? email, string? document, int page = 1, int pageSize = 50);
Task<bool> UpdateAsync(ECustomer entity);
Task<byte[]> ExportFilteredCustomersToExcelAsync(CustomerSearchParams searchParams);
Task<bool> DeleteAsync(int id);
}
}

View File

@ -1,14 +1,9 @@
using System.Drawing.Printing;
using System.Reflection;
using Azure;
using System.Reflection.Metadata;
using Core.Interfaces;
using Core.Interfaces;
using Domain.Entities;
using Domain.Generics;
using Microsoft.EntityFrameworkCore;
using Models.Helpers;
using Models.Interfaces;
using Models.Models;
using System.Reflection;
using Transversal.Services;
namespace Core.Services
{
@ -21,6 +16,7 @@ namespace Core.Services
_repository = customerRepository ?? throw new ArgumentNullException(nameof(customerRepository));
}
#endregion
#region Metodos
public async Task<PagedResult<ECustomer>> GetAllAsync(int page = 1, int pageSize = 50)
{
try
@ -72,7 +68,6 @@ namespace Core.Services
throw new Exception($"{methodName} Message: {ex.Message}", ex);
}
}
public async Task<bool> UpdateAsync(ECustomer entity)
{
if (entity is null)
@ -90,5 +85,49 @@ namespace Core.Services
{
throw new NotImplementedException();
}
public async Task<byte[]> ExportFilteredCustomersToExcelAsync(CustomerSearchParams searchParams)
{
try
{
// Realiza la búsqueda de clientes con los parámetros proporcionados
var searchResult = await SearchAsync(
searchParams.Name,
searchParams.Email,
searchParams.Document,
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 customersData = searchResult.Items.Select(c => new
{
c.Id,
c.Name,
c.BusinessName,
c.Active,
c.HasCreditAccount,
c.CreditLimit,
Address = c.PhSCustomerAddresses.FirstOrDefault()?.Streetaddress1,
Email = c.PhSCustomerAddresses.FirstOrDefault()?.Email,
Document = c.PhSCustomerDocuments.FirstOrDefault()?.DocumentNumber
}).ToList();
// Genera el archivo Excel
var excelFile = stream.ExportExcel(customersData);
// 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);
}
}
#endregion
}
}

View File

@ -1,23 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Domain.Entities
namespace Domain.Entities
{
public class EAccountType
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public string? Description { get; set; }
public decimal? CreditLimit { get; set; }
public DateTime? CreationDate { get; set; }
//public virtual ICollection<ECustomer> Customers { get; set; } = new List<ECustomer>();
}
}

View File

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
namespace Domain.Entities
{
public class ECustomer
@ -18,20 +12,12 @@ namespace Domain.Entities
public int? AccounttypesId { get; set; }
[Required(ErrorMessage = "Debe seleccionar un condicion.")]
public int? TaxConditionId { get; set; }
public bool HasCreditAccount { get; set; }
public decimal CreditLimit { get; set; }
public bool Active { get; set; }
public string? ExternalCode { get; set; }
public virtual EAccountType? Accounttypes { get; set; }
public virtual ICollection<ECustomerAddress> PhSCustomerAddresses { get; set; } = new List<ECustomerAddress>();
public virtual ICollection<ECustomerDocument> PhSCustomerDocuments { get; set; } = new List<ECustomerDocument>();
}
}

View File

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
namespace Domain.Entities
{
public class ECustomerAddress
@ -26,5 +20,4 @@ namespace Domain.Entities
public string? Email { get; set; }
public string? Notes { get; set; }
}
}

View File

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
namespace Domain.Entities
{
public class ECustomerDocument
@ -15,9 +9,7 @@ namespace Domain.Entities
public int DocumenttypesId { get; set; }
[Required(ErrorMessage = "Debe seleccionar un numero documento.")]
public string DocumentNumber { get; set; } = null!;
public DateOnly? IssueDate { get; set; }
public DateOnly? ExpiryDate { get; set; }
}
}

View File

@ -3,11 +3,8 @@
public class EDocumentType
{
public int Id { get; set; }
public string? Code { get; set; }
public string Name { get; set; } = null!;
public string? Description { get; set; }
}
}

View File

@ -1,5 +1,4 @@

namespace Domain.Entities
namespace Domain.Entities
{
public class ETicket
{

View File

@ -0,0 +1,9 @@
namespace Domain.Generics
{
public class CustomerSearchParams : PagedRequest
{
public string? Name { get; set; }
public string? Email { get; set; }
public string? Document { get; set; }
}
}

View File

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Domain.Generics
namespace Domain.Generics
{
public class PagedRequest
{

View File

@ -6,7 +6,6 @@
public int TotalItems { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalPages => (int)Math.Ceiling((double)TotalItems / PageSize);
}
}

View File

@ -15,7 +15,6 @@ namespace Models.Helpers
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new PagedResult<T>
{
Items = items,

View File

@ -1,11 +1,5 @@
using Domain.Entities;
using Domain.Generics;
using Models.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Models.Interfaces
{

View File

@ -1,6 +1,7 @@
using Core.Interfaces;
using Domain.Entities;
using Microsoft.AspNetCore.Http;
using Domain.Generics;
using Microsoft.AspNetCore.Mvc;
using System.Reflection;
@ -50,6 +51,7 @@ namespace phronCare.API.Controllers.Sales
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpGet("{id:int}")]
public async Task<ActionResult<ECustomer>> GetById(int id)
{
@ -66,6 +68,7 @@ namespace phronCare.API.Controllers.Sales
return StatusCode(500, $"Error: {ex.Message}");
}
}
[HttpPost("create")]
public async Task<IActionResult> Create([FromBody] ECustomer customer)
{
@ -91,6 +94,7 @@ namespace phronCare.API.Controllers.Sales
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpPut("update")]
public async Task<IActionResult> Update([FromBody] ECustomer customer)
{
@ -112,5 +116,20 @@ namespace phronCare.API.Controllers.Sales
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpPost("exportfiltered")]
public async Task<IActionResult> ExportFiltered([FromBody] CustomerSearchParams searchParams)
{
try {
var file = await _customerService.ExportFilteredCustomersToExcelAsync(searchParams);
return File(file,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Clientes.xlsx");
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
}

View File

@ -367,6 +367,22 @@
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.CustomerController",
"Method": "ExportFiltered",
"RelativePath": "api/Customer/exportfiltered",
"HttpMethod": "POST",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "searchParams",
"Type": "Domain.Generics.CustomerSearchParams",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Sales.CustomerController",
"Method": "Search",

View File

@ -4,16 +4,16 @@
@using phronCare.UIBlazor.Pages.Shared.Modals
@using phronCare.UIBlazor.Services.Sales
@inject IModalService Modal
@inject IToastService toastService
@inject HttpClient _httpClient
@inject NavigationManager Navigation
@inject IToastService toastService
@inject AuthenticationStateProvider authenticationStateProvider
@inject AccountTypeService accountTypeService
@inject TaxConditionService taxConditionService
<div class="card " style="zoom:70%">
<div class="card-header">
<h3 class="card-title">Formulario: cliente</h3>
<div class="card" style="zoom:80%">
<div class="card-header d-flex justify-content-center align-items-center">
<h3 class="card-title m-0">Información del cliente</h3>
</div>
<div class="card-body">
<EditForm Model="@customer" OnValidSubmit="@HandleValidSubmit">
@ -231,21 +231,17 @@
@code {
[Parameter]
public int? CustomerId { get; set; }
private ECustomer customer { get; set; } = new();
private List<EAccountType> accountTypes = new();
private List<ETaxCondition> taxConditions = new();
private List<EDocumentType> documentTypes = new();
private ECustomerDocument documentFormModel = new();
private bool isSaving = false;
private string returnUrl = "/sales/customers";
private List<string> countries = new() {
"Argentina", "Brasil", "Chile", "Uruguay", "Paraguay", "Estados Unidos", "Canadá",
"México", "Alemania", "Reino Unido", "Francia", "Italia", "España"
};
private Dictionary<string, List<string>> provincesByCountry = new()
{
{
@ -258,17 +254,14 @@
}
}
};
private ECustomerAddress editingAddress = new();
private int editingIndex = -1;
private void OnCountryChanged(ChangeEventArgs e)
{
var selectedCountry = e.Value?.ToString();
editingAddress.Country = selectedCountry;
editingAddress.Stateprovince = string.Empty; // Resetear la provincia si cambia el país
}
private void AddOrUpdateAddress()
{
var context = new ValidationContext(editingAddress, null, null);
@ -292,7 +285,6 @@
{
// Actualizar dirección existente
var existing = customer.PhSCustomerAddresses.ElementAt(editingIndex);
existing.BusinessName = editingAddress.BusinessName;
existing.Streetaddress1 = editingAddress.Streetaddress1;
existing.Streetaddress2 = editingAddress.Streetaddress2;
@ -306,10 +298,8 @@
existing.Email = editingAddress.Email;
existing.Notes = editingAddress.Notes;
}
ResetAddressForm();
}
private void EditAddress(int index)
{
var addr = customer.PhSCustomerAddresses.ElementAt(index);
@ -333,26 +323,21 @@
editingIndex = index;
}
private void RemoveAddress(int index)
{
var itemToRemove = customer.PhSCustomerAddresses.ElementAt(index);
customer.PhSCustomerAddresses.Remove(itemToRemove);
ResetAddressForm();
}
private void CancelAddressEdit()
{
ResetAddressForm();
}
private void ResetAddressForm()
{
editingAddress = new();
editingIndex = -1;
}
protected override async Task OnInitializedAsync()
{
await LoadAccountTypes();
@ -365,7 +350,6 @@
customer = await _httpClient.GetFromJsonAsync<ECustomer>($"/api/Customer/{CustomerId.Value}") ?? new();
}
}
private async Task LoadAccountTypes()
{
accountTypes = await accountTypeService.GetAllAsync();
@ -379,7 +363,6 @@
var result = await _httpClient.GetFromJsonAsync<List<EDocumentType>>("/api/DocumentType/GetAll");
documentTypes = result ?? new();
}
private void AddCustomerDocument()
{
if (!string.IsNullOrWhiteSpace(documentFormModel.DocumentNumber))
@ -396,21 +379,17 @@
{
customer.PhSCustomerAddresses.Remove(address);
}
private async Task HandleValidSubmit()
{
var parameters = new ModalParameters();
parameters.Add("Message", "¿Desea guardar los cambios del cliente?");
var modal = Modal.Show<ConfirmModal>("Confirmacion", parameters);
var result = await modal.Result;
if (result.Cancelled)
return;
try
{
HttpResponseMessage response;
if (CustomerId.HasValue)
{
response = await _httpClient.PutAsJsonAsync("/api/Customer/Update", customer);
@ -435,7 +414,6 @@
toastService.ShowError($"Error: {ex.Message}");
}
}
private void Cancel()
{
Navigation.NavigateTo(returnUrl);

View File

@ -3,21 +3,34 @@
@using phronCare.UIBlazor.Data
@using Domain.Entities
@using Domain.Generics
@inject IToastService toastService
@inject NavigationManager Navigation
@inject CustomerService customerService
<div class="card " style="zoom:90%">
<div class="card-header">
<h3 class="card-title">Listado de clientes</h3> @* wtf? *@
<div class="card">
<div class="card-header d-flex justify-content-center align-items-center" style="zoom:80%;">
<h3 class="card-title m-0">Administración de clientes</h3> @* wtf? *@
</div>
<div class="card-body" style="zoom:70%;">
<div class="card-body" style="zoom:80%;">
<div class="mb-4 space-y-2" >
<input @bind="SearchParams.Name" placeholder="Nombre" class="border rounded p-1 w-full" />
<input @bind="SearchParams.Email" placeholder="Email" class="border rounded p-1 w-full" />
<input @bind="SearchParams.Document" placeholder="Documento" class="border rounded p-1 w-full" />
<button class="btn btn-primary" @onclick="BuscarClientes">Buscar</button>
<button class="btn btn-primary rounded-pill" @onclick="BuscarClientes">
<i class="fas fa-binoculars me-1"></i> Buscar
</button>
<button class="btn btn-success rounded-pill" @onclick="NuevoCliente">
<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>
<button class="btn btn-secondary rounded-pill" @onclick="Cancel">
<i class="fas fa-arrow-left me-1"></i> Volver
</button>
</div>
<hr />
<div>
@if (TablaClientes != null && TablaClientes.Any())
{
<PhTable Columns="TableColumns"
@ -30,11 +43,6 @@
RenderSelect="false"
/>
<div class="mt-4 flex justify-between items-center">
<button class="btn btn-secondary" @onclick="AnteriorPagina" disabled="@(!PuedeRetroceder)">Anterior</button>
<span> Página @SearchParams.Page de @TotalPaginas </span>
<button class="btn btn-secondary" @onclick="SiguientePagina" disabled="@(!PuedeAvanzar)">Siguiente</button>
</div>
}
else
{
@ -42,18 +50,55 @@
}
</div>
<div class="card-footer">
<div class="row">
<div class="col">
<button type="button" class="btn btn-success" @onclick="XSLXExportar">
<i class="fas fa-file-excel"></i> Exportar a Excel
</div>
<div class="card-footer d-flex justify-content-center align-items-center" style="zoom:80%;">
<div class="d-flex align-items-center gap-3">
<button class="btn btn-secondary rounded-pill" @onclick="PrimeraPagina" disabled="@(SearchParams.Page == 1)">
<i class="fas fa-angle-double-left me-1"></i> Primera
</button>
<button class="btn btn-secondary rounded-pill" @onclick="AnteriorPagina" disabled="@(!PuedeRetroceder)">
<i class="fas fa-chevron-left me-1"></i> Anterior
</button>
<span class="mx-2">
Página <strong>@SearchParams.Page</strong> de <strong>@TotalPaginas</strong>
</span>
<button class="btn btn-secondary rounded-pill" @onclick="SiguientePagina" disabled="@(!PuedeAvanzar)">
Siguiente <i class="fas fa-chevron-right ms-1"></i>
</button>
<button class="btn btn-secondary rounded-pill" @onclick="UltimaPagina" disabled="@(SearchParams.Page == TotalPaginas)">
Última <i class="fas fa-angle-double-right ms-1"></i>
</button>
<div class="d-flex align-items-center ms-3">
<input type="number" class="form-control form-control-sm rounded" style="width: 80px;" min="1" max="@TotalPaginas" @bind="PaginaDeseada" />
<button class="btn btn-outline-primary btn-sm ms-2 rounded-pill" @onclick="IrAPagina">
<i class="fas fa-arrow-right-to-bracket me-1"></i> Ir
</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">Volver</button>
</div>
</div>
</div>
</div>
@code {
protected override void OnInitialized()
{
botones = new List<PhTable.ButtonOptions>
{
new PhTable.ButtonOptions
{
Caption = "Editar",
ElementClass = "btn btn-primary btn-sm",
UrlAction = "/sales/customers/edit/",
OnClickAction = async (id) =>
{
if (int.TryParse(id, out var customerId))
{
Navigation.NavigateTo($"/sales/customerform/{customerId}");
}
}
}
};
}
private CustomerSearchParams SearchParams = new();
private PagedResult<ECustomer>? PagedResult;
private List<Dictionary<string, object>> TablaClientes = new();
@ -62,13 +107,34 @@
"Id", "Nombre", "Activo", "Crédito", "Límite",
"Email", "Teléfono", "Dirección", "Documento"
};
private int PaginaDeseada = 1;
private async Task BuscarClientes()
private async Task PrimeraPagina()
{
SearchParams.Page = 1;
await BuscarClientes();
}
private async Task UltimaPagina()
{
SearchParams.Page = TotalPaginas;
await BuscarClientes();
}
private async Task IrAPagina()
{
if (PaginaDeseada >= 1 && PaginaDeseada <= TotalPaginas)
{
SearchParams.Page = PaginaDeseada;
await BuscarClientes();
}
else
{
toastService.ShowWarning("Número de página fuera de rango.");
}
}
private async Task BuscarClientes()
{
await CargarClientes();
}
private async Task CargarClientes()
{
PagedResult = await customerService.SearchCustomersAsync(SearchParams);
@ -107,45 +173,39 @@
await CargarClientes();
}
}
private async Task XSLXExportar()
private async Task ExportarExcel()
{
// string endpoint = "/api/Ticket/ExportDashboardDetail";
// var response = await _httpClient.PostAsJsonAsync(endpoint, new { Param1 = Group, Param2 = "ASC" });
// response.EnsureSuccessStatusCode();
// var fileBytes = await response.Content.ReadAsByteArrayAsync();
// var currentDate = DateTime.Now.ToString("ddMMyyyyhhmmss");
// var filename = $"Tickets_{Group}_{currentDate}.xlsx";
// await js.InvokeAsync<object>("saveAsFile", filename, Convert.ToBase64String(fileBytes));
}
List<PhTable.ButtonOptions> botones = new();
protected override void OnInitialized()
// Crea el objeto de parámetros para la búsqueda
var searchParams = new CustomerSearchParams
{
botones = new List<PhTable.ButtonOptions>
{
new PhTable.ButtonOptions
{
Caption = "Editar",
ElementClass = "btn btn-primary btn-sm",
UrlAction = "/sales/customers/edit/",
OnClickAction = async (id) =>
{
if (int.TryParse(id, out var customerId))
{
Navigation.NavigateTo($"/sales/customerform/{customerId}");
}
}
}
Name = SearchParams.Name, // Aquí podés obtener los filtros de los campos en el formulario
Email = SearchParams.Email,
Document = SearchParams.Document,
Page = 1,
PageSize = int.MaxValue // Puedes ajustar el tamaño de la página para exportar todos los registros
};
try
{
await customerService.ExportFilteredAsync(searchParams);
toastService.ShowSuccess("Exportación completada exitosamente.");
}
catch (Exception ex)
{
toastService.ShowError($"{ex.Message}");
}
}
private bool PuedeRetroceder => PagedResult != null && SearchParams.Page > 1;
private bool PuedeAvanzar => PagedResult != null && SearchParams.Page < TotalPaginas;
private int TotalPaginas => PagedResult is null ? 1 :
(int)Math.Ceiling((double)(PagedResult.TotalItems) / SearchParams.PageSize);
private bool PuedeAvanzar => PagedResult != null && SearchParams.Page < TotalPaginas;
private bool PuedeRetroceder => PagedResult != null && SearchParams.Page > 1;
private void NuevoCliente()
{
Navigation.NavigateTo("/sales/customerform/");
}
public void Cancel()
{
Navigation.NavigateTo("/DashboardPanel");
}
List<PhTable.ButtonOptions> botones = new();
}

View File

@ -1,14 +1,22 @@
using Domain.Entities;
using Domain.Generics;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text;
using Microsoft.JSInterop;
using System.Reflection;
namespace phronCare.UIBlazor.Services.Sales
{
public class CustomerService
{
private readonly HttpClient _http;
public CustomerService(HttpClient http)
private readonly IJSRuntime _js;
public CustomerService(HttpClient http, IJSRuntime js)
{
_http = http;
_js = js;
}
public async Task<PagedResult<ECustomer>?> SearchCustomersAsync(CustomerSearchParams searchParams)
{
@ -20,13 +28,40 @@ namespace phronCare.UIBlazor.Services.Sales
$"pageSize={searchParams.PageSize}";
return await _http.GetFromJsonAsync<PagedResult<ECustomer>>(url);
}
}
public class CustomerSearchParams
public async Task ExportFilteredAsync(CustomerSearchParams searchParams)
{
public string? Name { get; set; }
public string? Email { get; set; }
public string? Document { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 10;
try
{
var content = new StringContent(JsonSerializer.Serialize(searchParams), Encoding.UTF8, "application/json");
var response = await _http.PostAsync("api/Customer/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}_clientes.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 CustomerSearchParams
//{
// public string? Name { get; set; }
// public string? Email { get; set; }
// public string? Document { get; set; }
// public int Page { get; set; } = 1;
// public int PageSize { get; set; } = 10;
//}
}