Update UI Add ProductForm v1
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 8m54s

This commit is contained in:
Leandro Hernan Rojas 2025-04-19 12:56:35 -03:00
parent e14fecc455
commit 2b456f1e47
8 changed files with 478 additions and 10 deletions

View File

@ -0,0 +1,167 @@
@page "/sales/productform"
@page "/sales/productform/{ProductId:int?}"
@using System.ComponentModel.DataAnnotations
@using phronCare.UIBlazor.Services.Sales
@using phronCare.UIBlazor.Pages.Shared.Modals
@inject ProductService ProductService
@inject ProductCategoryService ProductCategoryService
@inject BusinessUnitService BusinessUnitService
@inject IToastService ToastService
@inject NavigationManager Navigation
@inject IModalService Modal
<div class="card" style="zoom:80%">
<div class="card-header d-flex justify-content-center align-items-center">
<h3 class="card-title m-0">@((ProductId.HasValue ? "Editar producto" : "Nuevo producto"))</h3>
</div>
<div class="card-body">
<EditForm Model="_model" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="row mb-3">
<div class="col-md-6">
<label for="Name">Nombre:</label>
<InputText id="Name" @bind-Value="_model.Name" class="form-control" />
<ValidationMessage For="@(() => _model.Name)" />
</div>
<div class="mb-3 col-md-6">
<label for="Description" class="form-label">Descripción:</label>
<InputTextArea id="Description" @bind-Value="_model.Description" class="form-control" rows="4" />
<ValidationMessage For="@(() => _model.Description)" />
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="Categoryid">Categoría:</label>
<InputSelect id="Categoryid" @bind-Value="_model.Categoryid" class="form-control">
<option value="">-- Seleccionar --</option>
@foreach (var cat in _productCategories)
{
<option value="@cat.Id">@cat.Name</option>
}
</InputSelect>
<ValidationMessage For="@(() => _model.Categoryid)" />
</div>
<div class="col-md-6">
<label for="BusinessunitsId">Unidad de Negocio:</label>
<InputSelect id="BusinessunitsId" @bind-Value="_model.BusinessunitsId" class="form-control">
<option value="">-- Seleccionar --</option>
@foreach (var unit in _businessUnits)
{
<option value="@unit.Id">@unit.Code</option>
}
</InputSelect>
<ValidationMessage For="@(() => _model.BusinessunitsId)" />
</div>
</div>
<div class="row mb-3">
<div class="mb-3 col-md-4">
<label>Origen</label>
<InputSelect class="form-select" @bind-Value="_model.Origin">
<option value="">-- Seleccionar --</option>
<option value="IMPORTADO">IMPORTADO</option>
<option value="NACIONAL">NACIONAL</option>
</InputSelect>
<ValidationMessage For="@(() => _model.Origin)" />
</div>
<div class="col-md-3">
<label for="Baseprice">Precio Base:</label>
<InputNumber id="Baseprice" @bind-Value="_model.Baseprice" class="form-control" />
<ValidationMessage For="@(() => _model.Baseprice)" />
</div>
<div class="col-md-4">
<label>Moneda</label>
<InputSelect class="form-select" @bind-Value="_model.Currency">
<option value="">-- Seleccionar --</option>
<option value="ARS">🇦🇷 ARS - Peso argentino</option>
<option value="USD">🇺🇸 USD - Dólar estadounidense</option>
<option value="EUR">🇪🇺 EUR - Euro</option>
<option value="BRL">🇧🇷 BRL - Real brasileño</option>
<option value="UYU">🇺🇾 UYU - Peso uruguayo</option>
</InputSelect>
<ValidationMessage For="@(() => _model.Currency)" />
</div>
<div class="col-md-1 d-flex align-items-center justify-content-start mt-4">
<div class="form-check form-switch">
<InputCheckbox id="Isactive" @bind-Value="_model.Isactive" class="form-check-input" />
<label class="form-check-label ms-2" for="Isactive">Activo</label>
</div>
</div>
</div>
</EditForm>
</div>
<div class="card-footer">
<div class="d-flex justify-content-end align-items-center py-3">
<button type="submit" class="btn btn-primary me-2">Guardar</button>
<button type="button" class="btn btn-secondary" @onclick="NavigateBack">Cancelar</button>
</div>
</div>
</div>
@code {
[Parameter] public int? ProductId { get; set; }
[Parameter] public string? returnUrl { get; set; } = "/sales/products";
private EProduct _model = new();
private List<EProductCategory> _productCategories = new();
private List<EBusinessUnit> _businessUnits = new();
protected override async Task OnInitializedAsync()
{
_productCategories = await ProductCategoryService.GetAllAsync();
_businessUnits = await BusinessUnitService.GetAllAsync();
if (ProductId.HasValue)
{
_model = await ProductService.GetByIdAsync(ProductId.Value);
}
}
private async Task HandleValidSubmit()
{
var parameters = new ModalParameters();
parameters.Add("Message", "¿Desea guardar los cambios del producto?");
var modal = Modal.Show<ConfirmModal>("Confirmación", parameters);
var result = await modal.Result;
if (result.Cancelled)
return;
try
{
HttpResponseMessage response;
if (_model.Id == 0)
response = await ProductService.CreateAsync(_model);
else
response = await ProductService.UpdateAsync(_model);
if (response.IsSuccessStatusCode)
{
ToastService.ShowSuccess("Producto guardado correctamente.");
NavigateBack();
}
else
{
var error = await response.Content.ReadAsStringAsync();
ToastService.ShowError($"Error: {error}");
}
}
catch (Exception ex)
{
ToastService.ShowError($"Error: {ex.Message}");
}
}
private void NavigateBack()
{
Navigation.NavigateTo(returnUrl ?? "/sales/products");
}
}

View File

@ -0,0 +1,183 @@
@page "/sales/products"
@using phronCare.UIBlazor.Services.Sales
@using phronCare.UIBlazor.Data
@using Domain.Entities
@using Domain.Generics
@inject IToastService toastService
@inject NavigationManager Navigation
@inject ProductService productService
<div class="card">
<div class="card-header d-flex justify-content-center align-items-center" style="zoom:80%;">
<h3 class="card-title m-0">Búsqueda de productos</h3>
</div>
<div class="card-body" style="zoom:80%;">
<div class="mb-4 space-y-2">
<input @bind="SearchParams.Term" placeholder="Nombre y descripcion" class="border rounded p-1 w-full" />
<button class="btn btn-primary rounded-pill" @onclick="BuscarProductos">
<i class="fas fa-binoculars me-1"></i> Buscar
</button>
<button class="btn btn-success rounded-pill" @onclick="NuevoProducto">
<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 (TablaProductos != null && TablaProductos.Any())
{
<PhTable Columns="TableColumns"
Data="TablaProductos"
SelectionField="Id"
RowsPerPage=SearchParams.PageSize
RenderButtons="true" Buttons="botones"
ShowPageButtons="false"
ShowQuickSearch="false"
RenderSelect="false" />
}
else
{
<p>No hay resultados.</p>
}
</div>
</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>
</div>
</div>
</div>
</div>
@code {
private ProductSearchParams SearchParams = new();
private PagedResult<EProduct>? PagedResult;
private List<Dictionary<string, object>> TablaProductos = new();
private List<string> TableColumns = new()
{
"Id", "Nombre", "Descripción", "Precio", "Moneda", "Negocio", "Categoría", "Activo"
};
private int PaginaDeseada = 1;
private List<PhTable.ButtonOptions> botones;
protected override void OnInitialized()
{
botones = new List<PhTable.ButtonOptions>
{
new PhTable.ButtonOptions
{
Caption = "Editar",
ElementClass = "btn btn-primary btn-sm",
UrlAction = "/sales/products/edit/",
OnClickAction = async (id) =>
{
if (int.TryParse(id, out var productId))
{
Navigation.NavigateTo($"/sales/productform/{productId}");
}
}
}
};
}
private async Task BuscarProductos() => await CargarProductos();
private async Task CargarProductos()
{
PagedResult = await productService.SearchProductsAsync(SearchParams);
if (PagedResult?.Items is not null)
{
TablaProductos = PagedResult.Items.Select(p => new Dictionary<string, object>
{
{ "Id", p.Id },
{ "Nombre", p.Name ?? string.Empty },
{ "Descripción", p.Description ?? string.Empty },
{ "Precio", p.Baseprice },
{ "Moneda", p.Currency ?? string.Empty },
{ "Negocio", p.Businessunits?.Code ?? string.Empty },
{ "Categoría", p.Category?.Description ?? string.Empty },
{ "Activo", p.Isactive ? "Sí" : "No" }
}).ToList();
}
}
private async Task PrimeraPagina() { SearchParams.Page = 1; await BuscarProductos(); }
private async Task UltimaPagina() { SearchParams.Page = TotalPaginas; await BuscarProductos(); }
private async Task SiguientePagina() => await CambiarPagina(1);
private async Task AnteriorPagina() => await CambiarPagina(-1);
private async Task CambiarPagina(int delta)
{
var nuevaPagina = SearchParams.Page + delta;
if (nuevaPagina >= 1 && nuevaPagina <= TotalPaginas)
{
SearchParams.Page = nuevaPagina;
await BuscarProductos();
}
}
private async Task IrAPagina()
{
if (PaginaDeseada >= 1 && PaginaDeseada <= TotalPaginas)
{
SearchParams.Page = PaginaDeseada;
await BuscarProductos();
}
else
{
toastService.ShowWarning("Número de página fuera de rango.");
}
}
private async Task ExportarExcel()
{
// Crea el objeto de parámetros para la búsqueda
var searchParams = new ProductSearchParams
{
Term = SearchParams.Term, // Aquí podés obtener los filtros de los campos en el formulario
Page = 1,
PageSize = int.MaxValue // Puedes ajustar el tamaño de la página para exportar todos los registros
};
try
{
await productService.ExportFilteredAsync(searchParams);
toastService.ShowSuccess("Exportación completada exitosamente.");
}
catch (Exception ex)
{
toastService.ShowError($"{ex.Message}");
}
}
private void NuevoProducto() => Navigation.NavigateTo("/sales/productform/");
private void Cancel() => Navigation.NavigateTo("/DashboardPanel");
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);
}

View File

@ -42,6 +42,9 @@ builder.Services.AddScoped<TicketsService>();
builder.Services.AddScoped<CustomerService>(); builder.Services.AddScoped<CustomerService>();
builder.Services.AddScoped<TaxConditionService>(); builder.Services.AddScoped<TaxConditionService>();
builder.Services.AddScoped<AccountTypeService>(); builder.Services.AddScoped<AccountTypeService>();
builder.Services.AddScoped<ProductService>();
builder.Services.AddScoped<BusinessUnitService>();
builder.Services.AddScoped<ProductCategoryService>();
#endregion #endregion
#region UI #region UI
builder.Services.AddBlazoredModal(); builder.Services.AddBlazoredModal();

View File

@ -0,0 +1,21 @@
using Domain.Entities;
using System.Net.Http.Json;
namespace phronCare.UIBlazor.Services.Sales
{
public class BusinessUnitService
{
private readonly HttpClient _http;
public BusinessUnitService(HttpClient http)
{
_http = http;
}
public async Task<List<EBusinessUnit>> GetAllAsync()
{
var result = await _http.GetFromJsonAsync<List<EBusinessUnit>>("/api/BusinessUnit/All");
return result ?? new List<EBusinessUnit>();
}
}
}

View File

@ -6,7 +6,6 @@ using System.Text;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using System.Reflection; using System.Reflection;
namespace phronCare.UIBlazor.Services.Sales namespace phronCare.UIBlazor.Services.Sales
{ {
public class CustomerService public class CustomerService
@ -55,13 +54,4 @@ namespace phronCare.UIBlazor.Services.Sales
} }
} }
} }
//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;
//}
} }

View File

@ -0,0 +1,19 @@
using Domain.Entities;
using System.Net.Http.Json;
namespace phronCare.UIBlazor.Services.Sales
{
public class ProductCategoryService
{
private readonly HttpClient _http;
public ProductCategoryService(HttpClient http)
{
_http = http;
}
public async Task<List<EProductCategory>> GetAllAsync()
{
var result = await _http.GetFromJsonAsync<List<EProductCategory>>("/api/ProductCategory/All");
return result ?? new List<EProductCategory>();
}
}
}

View File

@ -0,0 +1,80 @@
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 ProductService
{
private readonly HttpClient _http;
private readonly IJSRuntime _js;
public ProductService(HttpClient http, IJSRuntime js)
{
_http = http;
_js = js;
}
public async Task<List<EProduct>> GetAllAsync()
{
var result = await _http.GetFromJsonAsync<List<EProduct>>("/api/Product/GetAll");
return result ?? new List<EProduct>();
}
public async Task<EProduct> GetByIdAsync(int id)
{
var result = await _http.GetFromJsonAsync<EProduct>($"/api/Product/{id}");
return result ?? new EProduct();
}
public async Task<HttpResponseMessage> CreateAsync(EProduct product)
{
return await _http.PostAsJsonAsync("/api/Product/Create", product);
}
public async Task<HttpResponseMessage> UpdateAsync(EProduct product)
{
return await _http.PutAsJsonAsync("/api/Product/Update", product);
}
public async Task<PagedResult<EProduct>?> SearchProductsAsync(ProductSearchParams searchParams)
{
var url = $"api/Product/search?" +
$"term={searchParams.Term}&" +
$"page={searchParams.Page}&" +
$"pageSize={searchParams.PageSize}";
return await _http.GetFromJsonAsync<PagedResult<EProduct>>(url);
}
public async Task ExportFilteredAsync(ProductSearchParams searchParams)
{
try
{
var content = new StringContent(JsonSerializer.Serialize(searchParams), Encoding.UTF8, "application/json");
var response = await _http.PostAsync("api/Product/exportfiltered", content);
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}_productos.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);
}
}
}
}

View File

@ -76,6 +76,11 @@
<li aria-hidden="true"></li> Crear <li aria-hidden="true"></li> Crear
</NavLink> </NavLink>
</div> </div>
<div class="nav-item px-1">
<NavLink class="nav-link" href="sales/products/">
<li aria-hidden="true"></li> Productos
</NavLink>
</div>
</ul> </ul>
} }
</div> </div>