leandro a837eb41fe
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 10m11s
feat(ui): add sales document backoffice foundation
close #66
2026-06-03 21:04:49 -03:00

251 lines
11 KiB
Plaintext

@page "/salesdocuments"
@using Domain.Constants
@using Domain.Dtos.Sales
@using Domain.Generics
@using phronCare.UIBlazor.Services.Sales.SalesDocuments
@inject NavigationManager Navigation
@inject ISalesDocumentService SalesDocumentService
@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 Sales Documents</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="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="quoteId">Presupuesto ID</label>
<InputNumber id="quoteId" @bind-Value="Filters.QuoteId" class="form-control form-control-sm" />
</div>
<div class="col-sm">
<label for="documentType">Tipo</label>
<select id="documentType" @bind="Filters.DocumentType" class="form-select form-select-sm">
<option value="">— Todos —</option>
@foreach (var item in DocumentTypeOptions)
{
<option value="@item.Value">@item.Label</option>
}
</select>
</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>
@foreach (var item in StatusOptions)
{
<option value="@item.Value">@item.Label</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>
</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>Documento</th>
<th>Emisión</th>
<th>Cliente</th>
<th>Facturar a</th>
<th>Presupuesto</th>
<th>Tipo</th>
<th>Estado</th>
<th>Moneda</th>
<th>Total</th>
<th style="width:80px;">Acciones</th>
</tr>
</thead>
<tbody>
@if (PagedSalesDocuments?.Items?.Any() == true)
{
@foreach (var document in PagedSalesDocuments.Items)
{
<tr class="text-center">
<td>@(string.IsNullOrWhiteSpace(document.InternalDocumentNumber) ? $"#{document.Id}" : document.InternalDocumentNumber)</td>
<td>@FormatDate(document.IssueDate)</td>
<td>@document.CustomerName</td>
<td>@document.BillToCustomerName</td>
<td>@(document.QuoteId?.ToString() ?? "—")</td>
<td>@GetDocumentTypeLabel(document.DocumentType)</td>
<td><span class="badge @GetStatusBadge(document.Status)">@GetStatusLabel(document.Status)</span></td>
<td>@document.Currency</td>
<td>@document.TotalAmount.ToString("N2")</td>
<td class="text-center align-middle">
<button class="btn btn-link btn-lg p-0 text-primary ms-2" title="Ver detalle" @onclick="() => Detail(document.Id)"><i class="fas fa-eye"></i></button>
</td>
</tr>
}
}
else if (IsLoading)
{
<tr><td colspan="10" class="text-center text-muted py-4">Cargando...</td></tr>
}
else
{
<tr><td colspan="10" 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 SalesDocumentSearchParams Filters = new() { PageSize = 10 };
private PagedResult<SalesDocumentSummaryDto>? PagedSalesDocuments;
private bool IsLoading;
private int PaginaDeseada = 1;
private static readonly List<SelectOption> DocumentTypeOptions = Enum.GetValues<SalesDocumentType>()
.Select(x => new SelectOption((int)x, GetDocumentTypeLabel((int)x)))
.ToList();
private static readonly List<SelectOption> StatusOptions = Enum.GetValues<SalesDocumentStatus>()
.Select(x => new SelectOption((int)x, GetStatusLabel((int)x)))
.ToList();
protected override async Task OnInitializedAsync()
{
await Search();
}
private async Task Search()
{
try
{
IsLoading = true;
PagedSalesDocuments = await SalesDocumentService.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 => PagedSalesDocuments != null && Filters.Page > 1;
private bool PuedeAvanzar => PagedSalesDocuments != null && Filters.Page < TotalPaginas;
private int TotalPaginas => PagedSalesDocuments is null ? 1 : Math.Max(1, PagedSalesDocuments.TotalPages);
private void Create() => Navigation.NavigateTo("/salesdocuments/create");
private void Detail(int id) => Navigation.NavigateTo($"/salesdocuments/{id}");
private void OnClear()
{
Filters = new SalesDocumentSearchParams { PageSize = 10 };
PagedSalesDocuments = null;
PaginaDeseada = 1;
}
private static string FormatDate(DateTime? value) => value.HasValue ? value.Value.ToString("dd/MM/yyyy") : "—";
private static string GetDocumentTypeLabel(int value) => Enum.IsDefined(typeof(SalesDocumentType), value)
? ((SalesDocumentType)value) switch
{
SalesDocumentType.Invoice => "Factura",
SalesDocumentType.DebitNote => "Nota de débito",
SalesDocumentType.CreditNote => "Nota de crédito",
SalesDocumentType.CreditInvoice => "Factura crédito",
SalesDocumentType.CreditDebitNote => "N/D crédito",
SalesDocumentType.CreditCreditNote => "N/C crédito",
_ => value.ToString()
}
: value.ToString();
private static string GetStatusLabel(int value) => Enum.IsDefined(typeof(SalesDocumentStatus), value)
? ((SalesDocumentStatus)value) switch
{
SalesDocumentStatus.Draft => "Borrador",
SalesDocumentStatus.Validated => "Validado",
SalesDocumentStatus.Issued => "Emitido",
SalesDocumentStatus.Cancelled => "Anulado",
_ => value.ToString()
}
: value.ToString();
private static string GetStatusBadge(int value) => value switch
{
(int)SalesDocumentStatus.Draft => "bg-secondary text-white",
(int)SalesDocumentStatus.Validated => "bg-info text-dark",
(int)SalesDocumentStatus.Issued => "bg-primary text-white",
(int)SalesDocumentStatus.Cancelled => "bg-danger text-white",
_ => "bg-light text-dark"
};
private sealed record SelectOption(int Value, string Label);
}