Compare commits

..

2 Commits

Author SHA1 Message Date
9342447598 Merge pull request 'feat(sales): pantalla principal de consulta Delivery Note alineada a Quotes (#29)' (#30) from feature/leandro/29-deliverynote-consulta-principal into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m6s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/30
2026-03-21 15:54:28 +00:00
af635eadda feat(sales): pantalla principal de consulta Delivery Note alineada a Quotes (#29)
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 6m37s
- Se incorpora /deliverynotes con patrón visual idéntico a Quotes
- Se implementan filtros, tabla y paginación completa
- Se integra búsqueda con endpoint /api/deliverynote/search
- Se utiliza DeliveryNoteSearchParams desde Domain.Generics (sin duplicaciones)
- Se agregan placeholders para Nuevo, Excel y Ver
- Se incorpora navegación en menú

Closes #29
2026-03-21 12:53:53 -03:00
4 changed files with 259 additions and 0 deletions

View File

@ -129,6 +129,11 @@
<i class="bi bi-receipt me-2 text-white"></i> Presupuesto <i class="bi bi-receipt me-2 text-white"></i> Presupuesto
</NavLink> </NavLink>
</div> </div>
<div class="nav-item ps-4 py-0 border-start border-2 border-white">
<NavLink class="nav-link small py-0 px-2 text-start d-flex align-items-center" href="deliverynotes" activeClass="bg-secondary text-white fw-semibold">
<i class="bi bi-truck me-2 text-warning"></i> Remitos
</NavLink>
</div>
<div class="nav-item ps-4 py-0 border-start border-2 border-white"> <div class="nav-item ps-4 py-0 border-start border-2 border-white">
<NavLink class="nav-link small py-0 px-2 text-start d-flex align-items-center" href="sales/institutions/" activeClass="bg-secondary text-white fw-semibold"> <NavLink class="nav-link small py-0 px-2 text-start d-flex align-items-center" href="sales/institutions/" activeClass="bg-secondary text-white fw-semibold">
<i class="bi bi-building me-2 text-info"></i> Instituciones <i class="bi bi-building me-2 text-info"></i> Instituciones

View File

@ -0,0 +1,221 @@
@page "/deliverynotes"
@using Domain.Dtos.Sales
@using Domain.Generics
@using phronCare.UIBlazor.Services.Sales.DeliveryNotes
@inject NavigationManager Navigation
@inject IDeliveryNoteService deliveryNoteService
@inject IToastService toastService
<div class="card shadow-sm mb-3" style="zoom: 0.8;">
<div class="card-header py-2">
<div class="d-flex justify-content-center align-items-center">
<h3 class="card-title m-0">Consulta de Remitos</h3>
</div>
</div>
<div class="card-body pt-2 pb-0">
<div class="mb-3 row g-2 align-items-end">
<div class="col-sm">
<label for="deliveryNoteNumber">Remito</label>
<input id="deliveryNoteNumber" @bind="Filters.DeliveryNoteNumber" class="form-control form-control-sm" placeholder="DN-0000000X" />
</div>
<div class="col-sm">
<label for="customer">Cliente</label>
<input id="customer" @bind="Filters.CustomerText" class="form-control form-control-sm" placeholder="Nombre o código" />
</div>
<div class="col-sm">
<label for="quoteNumber">Presupuesto</label>
<input id="quoteNumber" @bind="Filters.QuoteNumber" class="form-control form-control-sm" placeholder="Q-0000000X" />
</div>
<div class="col-sm">
<label for="status">Estado</label>
<select id="status" @bind="Filters.Status" class="form-select form-select-sm">
<option value="">— Todos —</option>
<option value="Emitido">Emitido</option>
<option value="Anulado">Anulado</option>
<option value="Cerrado">Cerrado</option>
</select>
</div>
<div class="col-sm">
<label for="datefrom">Desde</label>
<InputDate id="datefrom" @bind-Value="Filters.IssueDateFrom" class="form-control form-control-sm" />
</div>
<div class="col-sm">
<label for="dateto">Hasta</label>
<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 rounded-pill" @onclick="Search">
<i class="fas fa-binoculars me-1"></i> Buscar
</button>
<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 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" style="zoom:0.8;">
<table class="table table-sm align-middle mb-0">
<thead class="table-light">
<tr>
<th>Remito</th>
<th>Emisión</th>
<th>Presupuesto</th>
<th>Cliente</th>
<th>Estado</th>
<th>Observaciones</th>
<th>Reimpresiones</th>
<th style="width:80px;">Acciones</th>
</tr>
</thead>
<tbody>
@if (PagedDeliveryNotes?.Items?.Any() == true)
{
@foreach (var deliveryNote in PagedDeliveryNotes.Items)
{
<tr class="text-center">
<td>@deliveryNote.DeliveryNoteNumber</td>
<td>@deliveryNote.IssueDate.ToString("dd/MM/yyyy")</td>
<td>@(string.IsNullOrWhiteSpace(deliveryNote.QuoteNumber) ? "—" : deliveryNote.QuoteNumber)</td>
<td>@deliveryNote.CustomerName</td>
<td>
<span class="badge @GetStatusBadge(deliveryNote.Status)">@deliveryNote.Status</span>
</td>
<td>@(string.IsNullOrWhiteSpace(deliveryNote.Observations) ? "—" : deliveryNote.Observations)</td>
<td>@deliveryNote.PrintCount</td>
<td class="text-center align-middle">
<button class="btn btn-link btn-lg p-0 text-primary ms-2" title="Ver detalle" @onclick="() => ViewDetail(deliveryNote.Id)"><i class="fas fa-eye"></i></button>
</td>
</tr>
}
}
else if (IsLoading)
{
<tr><td colspan="8" class="text-center text-muted py-4">Cargando...</td></tr>
}
else
{
<tr><td colspan="8" class="text-center text-muted py-4">Sin resultados</td></tr>
}
</tbody>
</table>
</div>
<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">
Página <strong>@Filters.Page</strong> de <strong>@TotalPaginas</strong>
</span>
<button class="btn btn-outline-secondary btn-sm rounded-pill" @onclick="SiguientePagina" disabled="@(!PuedeAvanzar)">Siguiente</button>
<button class="btn btn-outline-secondary btn-sm rounded-pill" @onclick="UltimaPagina" disabled="@(Filters.Page == TotalPaginas)">Última</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" />
</div>
<button class="btn btn-outline-secondary btn-sm rounded-pill" @onclick="IrAPagina">
<i class="fas fa-arrow-right-to-bracket me-1"></i> Ir
</button>
</div>
</div>
</div>
@code {
private DeliveryNoteSearchParams Filters = new() { PageSize = 10 };
private PagedResult<DeliveryNoteSummaryDto>? PagedDeliveryNotes;
private bool IsLoading;
private int PaginaDeseada = 1;
private async Task Search()
{
try
{
IsLoading = true;
PagedDeliveryNotes = await deliveryNoteService.SearchAsync(Filters);
PaginaDeseada = Filters.Page;
}
catch (Exception ex)
{
toastService.ShowError(ex.Message);
}
finally
{
IsLoading = false;
}
}
private async Task PrimeraPagina() { Filters.Page = 1; await Search(); }
private async Task UltimaPagina() { Filters.Page = TotalPaginas; await Search(); }
private async Task SiguientePagina() => await CambiarPagina(1);
private async Task AnteriorPagina() => await CambiarPagina(-1);
private async Task CambiarPagina(int delta)
{
var nuevaPagina = Filters.Page + delta;
if (nuevaPagina >= 1 && nuevaPagina <= TotalPaginas)
{
Filters.Page = nuevaPagina;
await Search();
}
}
private async Task IrAPagina()
{
if (PaginaDeseada >= 1 && PaginaDeseada <= TotalPaginas)
{
Filters.Page = PaginaDeseada;
await Search();
}
else
{
toastService.ShowWarning("Número de página fuera de rango.");
}
}
private bool PuedeRetroceder => PagedDeliveryNotes != null && Filters.Page > 1;
private bool PuedeAvanzar => PagedDeliveryNotes != null && Filters.Page < TotalPaginas;
private int TotalPaginas => PagedDeliveryNotes is null ? 1 :
(int)Math.Ceiling((double)(PagedDeliveryNotes.TotalItems) / Filters.PageSize);
private void Create()
{
toastService.ShowInfo("La creación de remitos se implementará en una próxima story.");
}
private void OnClear()
{
Filters = new DeliveryNoteSearchParams { PageSize = 10 };
PagedDeliveryNotes = null;
PaginaDeseada = 1;
}
private string GetStatusBadge(string status) => status switch
{
"Anulado" => "bg-danger text-white",
"Emitido" => "bg-primary text-white",
"Aprobado" => "bg-success",
"Cerrado" => "bg-dark text-white",
_ => "bg-light text-dark"
};
private void ViewDetail(int id)
{
toastService.ShowInfo($"El detalle del remito {id} se implementará en una próxima story.");
}
private void ExportarExcel()
{
toastService.ShowInfo("La exportación a Excel se implementará en una próxima story.");
}
}

View File

@ -1,4 +1,5 @@
using Domain.Dtos.Sales; using Domain.Dtos.Sales;
using Domain.Generics;
using System.Net.Http.Json; using System.Net.Http.Json;
namespace phronCare.UIBlazor.Services.Sales.DeliveryNotes namespace phronCare.UIBlazor.Services.Sales.DeliveryNotes
@ -12,6 +13,36 @@ namespace phronCare.UIBlazor.Services.Sales.DeliveryNotes
_http = http; _http = http;
} }
public async Task<PagedResult<DeliveryNoteSummaryDto>> SearchAsync(DeliveryNoteSearchParams searchParams)
{
var queryParams = new List<string>();
void AddParam(string key, string? value)
{
if (!string.IsNullOrWhiteSpace(value))
queryParams.Add($"{key}={Uri.EscapeDataString(value!)}");
}
AddParam("customerId", searchParams.CustomerId?.ToString());
AddParam("customerText", searchParams.CustomerText);
AddParam("deliveryNoteNumber", searchParams.DeliveryNoteNumber);
AddParam("quoteId", searchParams.QuoteId?.ToString());
AddParam("quoteNumber", searchParams.QuoteNumber);
AddParam("issueDateFrom", searchParams.IssueDateFrom?.ToString("o"));
AddParam("issueDateTo", searchParams.IssueDateTo?.ToString("o"));
AddParam("status", searchParams.Status);
AddParam("page", searchParams.Page.ToString());
AddParam("pageSize", searchParams.PageSize.ToString());
var url = "/api/deliverynote/search";
if (queryParams.Any())
url += "?" + string.Join("&", queryParams);
var result = await _http.GetFromJsonAsync<PagedResult<DeliveryNoteSummaryDto>>(url);
return result ?? new PagedResult<DeliveryNoteSummaryDto>();
}
public async Task<DeliveryNoteDto?> GetByIdAsync(int id) public async Task<DeliveryNoteDto?> GetByIdAsync(int id)
{ {
try try

View File

@ -1,9 +1,11 @@
using Domain.Dtos.Sales; using Domain.Dtos.Sales;
using Domain.Generics;
namespace phronCare.UIBlazor.Services.Sales.DeliveryNotes namespace phronCare.UIBlazor.Services.Sales.DeliveryNotes
{ {
public interface IDeliveryNoteService public interface IDeliveryNoteService
{ {
Task<PagedResult<DeliveryNoteSummaryDto>> SearchAsync(DeliveryNoteSearchParams searchParams);
Task<DeliveryNoteDto?> GetByIdAsync(int id); Task<DeliveryNoteDto?> GetByIdAsync(int id);
Task<DeliveryNoteDto?> GetByDeliveryNoteNumberAsync(string deliveryNoteNumber); Task<DeliveryNoteDto?> GetByDeliveryNoteNumberAsync(string deliveryNoteNumber);
Task<IEnumerable<DeliveryNoteDto>> GetByQuoteIdAsync(int quoteId); Task<IEnumerable<DeliveryNoteDto>> GetByQuoteIdAsync(int quoteId);