Update UI Add ProductForm v1
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 8m54s
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 8m54s
This commit is contained in:
parent
e14fecc455
commit
2b456f1e47
167
phronCare.UIBlazor/Pages/Sales/ProductForm.razor
Normal file
167
phronCare.UIBlazor/Pages/Sales/ProductForm.razor
Normal 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");
|
||||
}
|
||||
}
|
||||
183
phronCare.UIBlazor/Pages/Sales/Products.razor
Normal file
183
phronCare.UIBlazor/Pages/Sales/Products.razor
Normal 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);
|
||||
}
|
||||
@ -42,6 +42,9 @@ builder.Services.AddScoped<TicketsService>();
|
||||
builder.Services.AddScoped<CustomerService>();
|
||||
builder.Services.AddScoped<TaxConditionService>();
|
||||
builder.Services.AddScoped<AccountTypeService>();
|
||||
builder.Services.AddScoped<ProductService>();
|
||||
builder.Services.AddScoped<BusinessUnitService>();
|
||||
builder.Services.AddScoped<ProductCategoryService>();
|
||||
#endregion
|
||||
#region UI
|
||||
builder.Services.AddBlazoredModal();
|
||||
|
||||
21
phronCare.UIBlazor/Services/Sales/BusinessUnitService.cs
Normal file
21
phronCare.UIBlazor/Services/Sales/BusinessUnitService.cs
Normal 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>();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,6 @@ using System.Text;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Reflection;
|
||||
|
||||
|
||||
namespace phronCare.UIBlazor.Services.Sales
|
||||
{
|
||||
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;
|
||||
//}
|
||||
}
|
||||
|
||||
19
phronCare.UIBlazor/Services/Sales/ProductCategoryService.cs
Normal file
19
phronCare.UIBlazor/Services/Sales/ProductCategoryService.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
80
phronCare.UIBlazor/Services/Sales/ProductService.cs
Normal file
80
phronCare.UIBlazor/Services/Sales/ProductService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -76,6 +76,11 @@
|
||||
<li aria-hidden="true"></li> Crear
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-1">
|
||||
<NavLink class="nav-link" href="sales/products/">
|
||||
<li aria-hidden="true"></li> Productos
|
||||
</NavLink>
|
||||
</div>
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user