Add UnitOfMeasure Search and Form
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m50s

This commit is contained in:
Leandro Hernan Rojas 2025-07-15 12:42:30 -03:00
parent 20dd5d6943
commit 6e592bd034
16 changed files with 539 additions and 12 deletions

View File

@ -0,0 +1,16 @@
using Domain.Entities;
using Domain.Generics;
namespace Core.Interfaces.Stock
{
public interface ILSUnitOfMeasureDom
{
Task<PagedResult<ELSUnitOfMeasure>> SearchAsync(string? text, int page = 1, int pageSize = 100);
Task<ELSUnitOfMeasure?> GetByIdAsync(int id);
Task<int> AddAsync(ELSUnitOfMeasure model);
Task UpdateAsync(ELSUnitOfMeasure model);
Task<bool> ExistsByCodeAsync(string code);
Task<List<string>> GetAllCodesAsync();
}
}

View File

@ -0,0 +1,35 @@
using Core.Interfaces.Stock;
using Domain.Entities;
using Domain.Generics;
using Models.Interfaces;
namespace Core.Services.Stock
{
public class LSUnitOfMeasureService : ILSUnitOfMeasureDom
{
private readonly IPhLSMUnitOfMeasureRepository _repo;
public LSUnitOfMeasureService(IPhLSMUnitOfMeasureRepository repo)
{
_repo = repo;
}
public Task<PagedResult<ELSUnitOfMeasure>> SearchAsync(string? text, int page = 1, int pageSize = 100)
=> _repo.SearchAsync(text, page, pageSize);
public Task<ELSUnitOfMeasure?> GetByIdAsync(int id)
=> _repo.GetByIdAsync(id);
public Task<int> AddAsync(ELSUnitOfMeasure model)
=> _repo.AddAsync(model);
public Task UpdateAsync(ELSUnitOfMeasure model)
=> _repo.UpdateAsync(model);
public Task<bool> ExistsByCodeAsync(string code)
=> _repo.ExistsByCodeAsync(code);
public Task<List<string>> GetAllCodesAsync()
=> _repo.GetAllCodesAsync();
}
}

View File

@ -1,6 +1,6 @@
namespace Domain.Generics namespace Domain.Generics
{ {
public class ProductDivisionSearchParams : PagedRequest public class DivisionUnitSearchParams : PagedRequest
{ {
public string? Term { get; set; } public string? Term { get; set; }
} }

View File

@ -12,5 +12,9 @@ namespace Models.Interfaces
/// </summary> /// </summary>
Task<bool> ExistsByCodeAsync(string code); Task<bool> ExistsByCodeAsync(string code);
Task<List<string>> GetAllCodesAsync(); Task<List<string>> GetAllCodesAsync();
Task<ELSUnitOfMeasure?> GetByIdAsync(int id);
Task<int> AddAsync(ELSUnitOfMeasure model);
Task UpdateAsync(ELSUnitOfMeasure model);
Task<PagedResult<ELSUnitOfMeasure>> SearchAsync(string? text, int page = 1, int pageSize = 100);
} }
} }

View File

@ -23,6 +23,13 @@ namespace Models.Repositories.Stock
PageSize = paged.PageSize PageSize = paged.PageSize
}; };
} }
public async Task<ELSUnitOfMeasure?> GetByIdAsync(int id)
{
var entity = await _context.PhLsmUnitOfMeasures.FindAsync(id);
return entity != null
? EntityMapper.MapEntity<PhLsmUnitOfMeasure, ELSUnitOfMeasure>(entity)
: null;
}
public async Task<bool> ExistsByCodeAsync(string code) public async Task<bool> ExistsByCodeAsync(string code)
{ {
if (string.IsNullOrWhiteSpace(code)) if (string.IsNullOrWhiteSpace(code))
@ -37,5 +44,52 @@ namespace Models.Repositories.Stock
.Select(x => x.Code) .Select(x => x.Code)
.ToListAsync(); .ToListAsync();
} }
public async Task<int> AddAsync(ELSUnitOfMeasure model)
{
var entity = new PhLsmUnitOfMeasure
{
Name = model.Name,
Description = model.Description,
Code = model.Code // si lo tenés
};
_context.PhLsmUnitOfMeasures.Add(entity);
await _context.SaveChangesAsync();
return entity.Id;
}
public async Task UpdateAsync(ELSUnitOfMeasure model)
{
var entity = await _context.PhLsmUnitOfMeasures.FindAsync(model.Id);
if (entity == null) throw new Exception("Unidad no encontrada");
entity.Name = model.Name;
entity.Description = model.Description;
entity.Code = model.Code;
await _context.SaveChangesAsync();
}
public async Task<PagedResult<ELSUnitOfMeasure>> SearchAsync(string? text, int page = 1, int pageSize = 100)
{
var query = _context.PhLsmUnitOfMeasures.AsQueryable();
if (!string.IsNullOrWhiteSpace(text))
{
var lowered = text.ToLower();
query = query.Where(x =>
x.Name.ToLower().Contains(lowered) ||
x.Description.ToLower().Contains(lowered));
}
var paged = await query.ToPagedResultAsync(page, pageSize);
return new PagedResult<ELSUnitOfMeasure>
{
Items = paged.Items.Select(EntityMapper.MapEntity<PhLsmUnitOfMeasure, ELSUnitOfMeasure>),
TotalItems = paged.TotalItems,
Page = paged.Page,
PageSize = paged.PageSize
};
}
} }
} }

View File

@ -0,0 +1,60 @@
using Core.Interfaces.Stock;
using Domain.Entities;
using Microsoft.AspNetCore.Mvc;
namespace phronCare.API.Controllers.Stock
{
[Route("api/[controller]")]
[ApiController]
public class LSUnitOfMeasureController : ControllerBase
{
private readonly ILSUnitOfMeasureDom _service;
public LSUnitOfMeasureController(ILSUnitOfMeasureDom service)
{
_service = service ?? throw new ArgumentNullException(nameof(service));
}
[HttpGet("Search")]
public async Task<IActionResult> Search([FromQuery] string? term, [FromQuery] int page = 1, [FromQuery] int pageSize = 50)
{
var result = await _service.SearchAsync(term, page, pageSize);
return Ok(result);
}
[HttpGet("GetById/{id}")]
public async Task<IActionResult> GetById(int id)
{
var result = await _service.GetByIdAsync(id);
return result is null ? NotFound() : Ok(result);
}
[HttpPost("Create")]
public async Task<IActionResult> Create([FromBody] ELSUnitOfMeasure model)
{
try
{
var newId = await _service.AddAsync(model);
return CreatedAtAction(nameof(GetById), new { id = newId }, model);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
[HttpPut("Update")]
public async Task<IActionResult> Update([FromBody] ELSUnitOfMeasure model)
{
try
{
await _service.UpdateAsync(model);
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
}

View File

@ -265,6 +265,9 @@ static void RepositorysAndServices(WebApplicationBuilder builder)
builder.Services.AddScoped<ILSProductDom, LSProductService>(); builder.Services.AddScoped<ILSProductDom, LSProductService>();
builder.Services.AddScoped<IPhLSMProductRepository, PhLSMProductRepository>(); builder.Services.AddScoped<IPhLSMProductRepository, PhLSMProductRepository>();
builder.Services.AddScoped<ILSUnitOfMeasureDom, LSUnitOfMeasureService>();
builder.Services.AddScoped<IPhLSMUnitOfMeasureRepository, PhLSMUnitOfMeasureRepository>();
builder.Services.AddScoped<IPhLSMLookUpRepository, PhLSMLookUpRepository>(); builder.Services.AddScoped<IPhLSMLookUpRepository, PhLSMLookUpRepository>();
builder.Services.AddScoped<ILSMLookUpDom, LSMLookUpService>(); builder.Services.AddScoped<ILSMLookUpDom, LSMLookUpService>();
builder.Services.AddScoped<IPhLSMUnitOfMeasureRepository, PhLSMUnitOfMeasureRepository>(); builder.Services.AddScoped<IPhLSMUnitOfMeasureRepository, PhLSMUnitOfMeasureRepository>();

View File

@ -1200,6 +1200,80 @@
], ],
"ReturnTypes": [] "ReturnTypes": []
}, },
{
"ContainingType": "phronCare.API.Controllers.Stock.LSUnitOfMeasureController",
"Method": "Create",
"RelativePath": "api/LSUnitOfMeasure/Create",
"HttpMethod": "POST",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "model",
"Type": "Domain.Entities.ELSUnitOfMeasure",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Stock.LSUnitOfMeasureController",
"Method": "GetById",
"RelativePath": "api/LSUnitOfMeasure/GetById/{id}",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "id",
"Type": "System.Int32",
"IsRequired": true
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Stock.LSUnitOfMeasureController",
"Method": "Search",
"RelativePath": "api/LSUnitOfMeasure/Search",
"HttpMethod": "GET",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "term",
"Type": "System.String",
"IsRequired": false
},
{
"Name": "page",
"Type": "System.Int32",
"IsRequired": false
},
{
"Name": "pageSize",
"Type": "System.Int32",
"IsRequired": false
}
],
"ReturnTypes": []
},
{
"ContainingType": "phronCare.API.Controllers.Stock.LSUnitOfMeasureController",
"Method": "Update",
"RelativePath": "api/LSUnitOfMeasure/Update",
"HttpMethod": "PUT",
"IsController": true,
"Order": 0,
"Parameters": [
{
"Name": "model",
"Type": "Domain.Entities.ELSUnitOfMeasure",
"IsRequired": true
}
],
"ReturnTypes": []
},
{ {
"ContainingType": "phronCare.API.Controllers.Sales.PatientController", "ContainingType": "phronCare.API.Controllers.Sales.PatientController",
"Method": "GetById", "Method": "GetById",

View File

@ -147,8 +147,8 @@
{ {
new PhTable.ButtonOptions new PhTable.ButtonOptions
{ {
Caption = "Editar", Caption = "<i class='fas fa-pen'></i>",
ElementClass = "btn btn-primary btn-sm", ElementClass = "btn btn-success btn-sm",
UrlAction = "/stock/productform/", UrlAction = "/stock/productform/",
OnClickAction = async (id) => OnClickAction = async (id) =>
{ {
@ -157,8 +157,6 @@
} }
} }
}; };
// await Buscar();
} }
private async Task Buscar() private async Task Buscar()
@ -177,8 +175,8 @@
{ {
{ "Id", p.Id }, { "Id", p.Id },
{ "Código Fábrica", p.FactoryCode }, { "Código Fábrica", p.FactoryCode },
{ "Código Externo", p.ExternalCode }, { "Código Externo", p.ExternalCode?? string.Empty },
{ "Nombre", p.Name }, { "Nombre", p.Name?? string.Empty },
{ "Descripción", p.Descripcion }, { "Descripción", p.Descripcion },
{ "División", p.Division?.Name ?? "" }, { "División", p.Division?.Name ?? "" },
{ "Unidad", p.Unit?.Name ?? "" }, { "Unidad", p.Unit?.Name ?? "" },

View File

@ -0,0 +1,156 @@
@page "/stock/units"
@using Domain.Generics
@using phronCare.UIBlazor.Services.Stock
@inject LSUnitOfMeasureService unitService
@inject IToastService toastService
@inject NavigationManager Navigation
<div class="card">
<div class="card-header d-flex justify-content-center align-items-center" style="zoom:80%;">
<h3 class="card-title m-0">Unidades de medida</h3>
</div>
<div class="card-body" style="zoom:80%;">
<div class="mb-4 space-y-2">
<input @bind="SearchParams.Term" placeholder="Nombre o descripción..." class="border rounded p-1 w-full" />
<button class="btn btn-primary rounded-pill" @onclick="Buscar">
<i class="fas fa-search me-1"></i> Buscar
</button>
<button class="btn btn-success rounded-pill" @onclick="Nuevo">
<i class="fas fa-plus me-1"></i> Nuevo
</button>
<button class="btn btn-secondary rounded-pill" @onclick="Volver">
<i class="fas fa-arrow-left me-1"></i> Volver
</button>
</div>
<hr />
<div style="zoom:90%;">
@if (Tabla != null && Tabla.Any())
{
<PhTable Columns="Columnas"
Data="Tabla"
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 PagedResult<ELSUnitOfMeasure>? Resultado;
private List<Dictionary<string, object>> Tabla = new();
private List<string> Columnas = new() { "Id", "Nombre", "Descripción" };
private DivisionUnitSearchParams SearchParams = new() { PageSize = 10 };
private List<PhTable.ButtonOptions> Botones = new();
private int PaginaDeseada = 1;
protected override void OnInitialized()
{
Botones = new List<PhTable.ButtonOptions>
{
new PhTable.ButtonOptions
{
Caption = "<i class='fas fa-pen'></i>",
ElementClass = "btn btn-success",
UrlAction = "/stock/unitform/",
OnClickAction = async (id) =>
{
Navigation.NavigateTo($"/stock/unitform/{id}");
}
}
};
}
private async Task Buscar()
{
SearchParams.Page = 1;
await CargarPaginaActual();
}
private async Task CargarPaginaActual()
{
Resultado = await unitService.SearchAsync(SearchParams.Term, SearchParams.Page, SearchParams.PageSize);
if (Resultado?.Items != null)
{
Tabla = Resultado.Items.Select(u => new Dictionary<string, object>
{
{ "Id", u.Id },
{ "Nombre", u.Name ?? string.Empty },
{ "Descripción", u.Description ?? string.Empty }
}).ToList();
}
}
private async Task PrimeraPagina() { SearchParams.Page = 1; await CargarPaginaActual(); }
private async Task UltimaPagina() { SearchParams.Page = TotalPaginas; await CargarPaginaActual(); }
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 CargarPaginaActual();
}
}
private async Task IrAPagina()
{
if (PaginaDeseada >= 1 && PaginaDeseada <= TotalPaginas)
{
SearchParams.Page = PaginaDeseada;
await Buscar();
}
else
{
toastService.ShowWarning("Número de página fuera de rango.");
}
}
private void Nuevo() => Navigation.NavigateTo("/stock/unitform/");
private void Volver() => Navigation.NavigateTo("/DashboardPanel");
private int TotalPaginas => Resultado == null || Resultado.TotalItems == 0
? 1
: (int)Math.Ceiling((double)(Resultado.TotalItems) / SearchParams.PageSize);
private bool PuedeRetroceder => Resultado != null && SearchParams.Page > 1;
private bool PuedeAvanzar => Resultado != null && SearchParams.Page < TotalPaginas;
}

View File

@ -0,0 +1,84 @@
@page "/stock/unitform/"
@page "/stock/unitform/{Id:int?}"
@using phronCare.UIBlazor.Services.Stock
@inject LSUnitOfMeasureService unitService
@inject NavigationManager Navigation
@inject IToastService toastService
<EditForm Model="model" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="card mt-4" style="zoom:80%">
<div class="card-header d-flex justify-content-center align-items-center">
<h3 class="card-title m-0">@((model.Id == 0) ? "Nueva Unidad de Medida" : "Editar Unidad de Medida")</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4 mb-3">
<label for="Name" class="form-label">Nombre</label>
<InputText id="Name" class="form-control" @bind-Value="model.Name" />
<ValidationMessage For="@(() => model.Name)" />
</div>
<div class="col-md-8 mb-3">
<label for="Descripcion" class="form-label">Descripción</label>
<InputText id="Descripcion" class="form-control" @bind-Value="model.Description" />
<ValidationMessage For="@(() => model.Description)" />
</div>
</div>
</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="Cancel">Cancelar</button>
</div>
</div>
</div>
</EditForm>
@code {
[Parameter]
public int? Id { get; set; }
private ELSUnitOfMeasure model = new();
protected override async Task OnInitializedAsync()
{
if (Id.HasValue && Id > 0)
{
var result = await unitService.GetByIdAsync(Id.Value);
if (result != null)
model = result;
else
toastService.ShowError("No se pudo cargar la unidad de medida.");
}
}
private async Task HandleValidSubmit()
{
try
{
if (model.Id == 0)
{
await unitService.CreateAsync(model);
}
else
{
await unitService.UpdateAsync(model);
}
toastService.ShowSuccess("Unidad guardada correctamente.");
Navigation.NavigateTo("/stock/units");
}
catch (Exception ex)
{
toastService.ShowError($"Error al guardar: {ex.Message}");
}
}
private void Cancel() => Navigation.NavigateTo("/stock/units");
}

View File

@ -74,7 +74,7 @@
private PagedResult<ELSProductDivision>? Resultado; private PagedResult<ELSProductDivision>? Resultado;
private List<Dictionary<string, object>> Tabla = new(); private List<Dictionary<string, object>> Tabla = new();
private List<string> Columnas = new() { "Id", "Codigo", "Nombre", "Descripción" }; private List<string> Columnas = new() { "Id", "Codigo", "Nombre", "Descripción" };
private ProductDivisionSearchParams SearchParams = new() { PageSize = 10 }; private DivisionUnitSearchParams SearchParams = new() { PageSize = 10 };
private List<PhTable.ButtonOptions> Botones; private List<PhTable.ButtonOptions> Botones;
private int PaginaDeseada = 1; private int PaginaDeseada = 1;
@ -84,8 +84,8 @@
{ {
new PhTable.ButtonOptions new PhTable.ButtonOptions
{ {
Caption = "Editar", Caption = "<i class='fas fa-pen'></i>",
ElementClass = "btn btn-primary btn-sm", ElementClass = "btn btn-success",
UrlAction = "/stock/productdivisionform/", UrlAction = "/stock/productdivisionform/",
OnClickAction = async (id) => OnClickAction = async (id) =>
{ {

View File

@ -66,5 +66,7 @@ static void InjectDependencies(WebAssemblyHostBuilder builder)
builder.Services.AddScoped<PatientService>(); builder.Services.AddScoped<PatientService>();
builder.Services.AddScoped<ProductDivisionService>(); builder.Services.AddScoped<ProductDivisionService>();
builder.Services.AddScoped<LSProductService>(); builder.Services.AddScoped<LSProductService>();
builder.Services.AddScoped<LSUnitOfMeasureService>();
} }

View File

@ -0,0 +1,41 @@
using Domain.Entities;
using Domain.Generics;
using System.Net.Http.Json;
namespace phronCare.UIBlazor.Services.Stock
{
public class LSUnitOfMeasureService
{
private readonly HttpClient _http;
public LSUnitOfMeasureService(HttpClient http)
{
_http = http;
}
public async Task<PagedResult<ELSUnitOfMeasure>> SearchAsync(string? term, int page = 1, int pageSize = 50)
{
var response = await _http.GetFromJsonAsync<PagedResult<ELSUnitOfMeasure>>(
$"api/LSUnitOfMeasure/Search?term={term}&page={page}&pageSize={pageSize}");
return response!;
}
public async Task<ELSUnitOfMeasure?> GetByIdAsync(int id)
{
return await _http.GetFromJsonAsync<ELSUnitOfMeasure>($"api/LSUnitOfMeasure/GetById/{id}");
}
public async Task<int> CreateAsync(ELSUnitOfMeasure model)
{
var response = await _http.PostAsJsonAsync("api/LSUnitOfMeasure/Create", model);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<int>();
}
public async Task UpdateAsync(ELSUnitOfMeasure model)
{
var response = await _http.PutAsJsonAsync("api/LSUnitOfMeasure/Update", model);
response.EnsureSuccessStatusCode();
}
}
}

View File

@ -43,7 +43,7 @@ namespace phronCare.UIBlazor.Services.Stock
return await _http.DeleteAsync($"/api/ProductDivision/Delete/{id}"); return await _http.DeleteAsync($"/api/ProductDivision/Delete/{id}");
} }
public async Task<PagedResult<ELSProductDivision>?> SearchAsync(ProductDivisionSearchParams searchParams) public async Task<PagedResult<ELSProductDivision>?> SearchAsync(DivisionUnitSearchParams searchParams)
{ {
var url = $"/api/ProductDivision/Search?" + var url = $"/api/ProductDivision/Search?" +
$"term={searchParams.Term}&" + $"term={searchParams.Term}&" +

View File

@ -178,7 +178,7 @@
<a class="@button.ElementClass" <a class="@button.ElementClass"
@onclick='async () => button?.OnClickAction(item[SelectionField]?.ToString() ?? "")' @onclick='async () => button?.OnClickAction(item[SelectionField]?.ToString() ?? "")'
style="zoom:@button.ElementZoom"> style="zoom:@button.ElementZoom">
@button.Caption @((MarkupString)button.Caption)
</a> </a>
} }
} }