All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 26m7s
445 lines
20 KiB
Plaintext
445 lines
20 KiB
Plaintext
@page "/salesdocuments/{Id:int}/review"
|
|
@using Domain.Constants
|
|
@using Domain.Dtos.Sales
|
|
@using phronCare.UIBlazor.Services.Sales.SalesDocuments
|
|
@inject NavigationManager Navigation
|
|
@inject ISalesDocumentService SalesDocumentService
|
|
@inject IToastService toastService
|
|
|
|
<div class="sales-document-review container-fluid" style="zoom:.8;">
|
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
|
<div>
|
|
<h3 class="mb-1">Revision administrativa</h3>
|
|
<div class="text-muted">Sales Document Draft Review & Validation</div>
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<button type="button" class="btn btn-secondary rounded-pill" @onclick="BackToList">
|
|
<i class="fas fa-arrow-left me-1"></i> Volver
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary rounded-pill" @onclick="ViewDetail" disabled="@(Document is null)">
|
|
<i class="fas fa-eye me-1"></i> Detalle
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
@if (IsLoading)
|
|
{
|
|
<div class="card shadow-sm">
|
|
<div class="card-body text-center text-muted py-4">Cargando...</div>
|
|
</div>
|
|
}
|
|
else if (Document is null)
|
|
{
|
|
<div class="alert alert-warning">No se pudo cargar el Sales Document.</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="alert @(IsDraft ? "alert-info" : "alert-secondary")">
|
|
@if (IsDraft)
|
|
{
|
|
<span>Este documento esta en Draft y puede revisarse visualmente. Guardar y validar requieren los endpoints de backend fuera del alcance UI-only.</span>
|
|
}
|
|
else
|
|
{
|
|
<span>Este documento no esta en Draft. La revision queda en modo solo lectura.</span>
|
|
}
|
|
</div>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-xl-8">
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">Cabecera del documento</h5>
|
|
<span class="badge @GetStatusBadge(Document.Status)">@GetStatusLabel(Document.Status)</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-3">
|
|
<label class="form-label fw-semibold mb-1">Documento</label>
|
|
<div class="form-control bg-white">@DocumentNumber</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label fw-semibold mb-1">Tipo</label>
|
|
<div class="form-control bg-white">@GetDocumentTypeLabel(Document.DocumentType)</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label fw-semibold mb-1">Emision</label>
|
|
<div class="form-control bg-white">@FormatDate(Document.IssueDate)</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label fw-semibold mb-1">Moneda</label>
|
|
<div class="form-control bg-white">@Document.Currency</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-semibold mb-1">Cliente</label>
|
|
<div class="form-control bg-white">@Document.CustomerName</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-semibold mb-1">Bill To Customer</label>
|
|
<div class="form-control bg-white">@Document.BillToCustomerName</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-semibold mb-1">Presupuesto</label>
|
|
<div class="form-control bg-white">@(Document.QuoteId?.ToString() ?? "-")</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-semibold mb-1">Periodo desde</label>
|
|
<div class="form-control bg-white">@FormatDate(Document.PeriodFrom)</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-semibold mb-1">Periodo hasta</label>
|
|
<div class="form-control bg-white">@FormatDate(Document.PeriodTo)</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label fw-semibold mb-1">Cotizacion</label>
|
|
<div class="form-control bg-white">@Document.ExchangeRate.ToString("N4")</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label fw-semibold mb-1">Observaciones</label>
|
|
<textarea class="form-control" rows="4" @bind="ReviewObservations" disabled="@(!IsDraft)"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">Remitos origen</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
@if (OriginDeliveryNotes.Any())
|
|
{
|
|
<div class="row g-2">
|
|
@foreach (var item in OriginDeliveryNotes)
|
|
{
|
|
<div class="col-md-6">
|
|
<div class="origin-card">
|
|
<strong>@item.DeliveryNoteNumber</strong>
|
|
<small>Remito ID @item.Id</small>
|
|
<span>@FormatDate(item.IssueDate)</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="text-muted">No se detectaron remitos origen en el snapshot del documento.</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">Items</h5>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-bordered mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Origen</th>
|
|
<th>Descripcion</th>
|
|
<th class="text-end">Cantidad</th>
|
|
<th class="text-end">Unitario</th>
|
|
<th class="text-end">Neto</th>
|
|
<th class="text-end">Impuesto</th>
|
|
<th class="text-end">Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (Document.Details.Any())
|
|
{
|
|
@foreach (var item in Document.Details.OrderBy(x => x.LineNumber))
|
|
{
|
|
<tr>
|
|
<td class="text-center">@item.LineNumber</td>
|
|
<td>@GetOriginTypeLabel(item.OriginType)</td>
|
|
<td>@item.Description</td>
|
|
<td class="text-end">@item.Quantity.ToString("N2")</td>
|
|
<td class="text-end">@item.UnitPrice.ToString("N2")</td>
|
|
<td class="text-end">@item.NetAmount.ToString("N2")</td>
|
|
<td class="text-end">@item.TaxAmount.ToString("N2")</td>
|
|
<td class="text-end">@item.TotalAmount.ToString("N2")</td>
|
|
</tr>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<tr><td colspan="8" class="text-center text-muted py-4">Sin items.</td></tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow-sm">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">Coverage</h5>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-bordered mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Tipo</th>
|
|
<th>Presupuesto</th>
|
|
<th>Quote Detail</th>
|
|
<th class="text-end">Porcentaje</th>
|
|
<th class="text-end">Importe</th>
|
|
<th>Desde</th>
|
|
<th>Hasta</th>
|
|
<th>Notas</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (Document.Coverage.Any())
|
|
{
|
|
@foreach (var coverage in Document.Coverage)
|
|
{
|
|
<tr>
|
|
<td>@GetCoverageTypeLabel(coverage.CoverageType)</td>
|
|
<td>@coverage.QuoteId</td>
|
|
<td>@(coverage.QuoteDetailId?.ToString() ?? "-")</td>
|
|
<td class="text-end">@(coverage.CoveragePercentage?.ToString("N2") ?? "-")</td>
|
|
<td class="text-end">@(coverage.CoverageAmount?.ToString("N2") ?? "-")</td>
|
|
<td>@FormatDate(coverage.PeriodFrom)</td>
|
|
<td>@FormatDate(coverage.PeriodTo)</td>
|
|
<td>@(string.IsNullOrWhiteSpace(coverage.Notes) ? "-" : coverage.Notes)</td>
|
|
</tr>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<tr><td colspan="8" class="text-center text-muted py-4">Sin coverage informado.</td></tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-4">
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">Validacion del Draft</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<ul class="validation-list">
|
|
@foreach (var item in ValidationItems)
|
|
{
|
|
<li class="@(item.IsValid ? "valid" : "invalid")">
|
|
<i class="fas @(item.IsValid ? "fa-check-circle" : "fa-circle-exclamation")"></i>
|
|
<span>@item.Message</span>
|
|
</li>
|
|
}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">Importes</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="amount-row">
|
|
<span>Neto</span>
|
|
<strong>@Document.NetAmount.ToString("N2")</strong>
|
|
</div>
|
|
<div class="amount-row">
|
|
<span>Impuestos</span>
|
|
<strong>@Document.TaxAmount.ToString("N2")</strong>
|
|
</div>
|
|
<div class="amount-row total">
|
|
<span>Total</span>
|
|
<strong>@Document.Currency @Document.TotalAmount.ToString("N2")</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow-sm">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">Acciones</h5>
|
|
</div>
|
|
<div class="card-body d-grid gap-2">
|
|
<button type="button" class="btn btn-primary rounded-pill" @onclick="SaveReviewAsync" disabled="@(!IsDraft || IsSaving)">
|
|
<i class="fas fa-save me-1"></i> Guardar revision
|
|
</button>
|
|
<button type="button" class="btn btn-success rounded-pill" @onclick="ValidateDraftAsync" disabled="@(!CanValidate || IsSaving)">
|
|
<i class="fas fa-check-circle me-1"></i> Validar documento
|
|
</button>
|
|
<div class="small text-muted mt-2">
|
|
Guardar revision persiste los campos editables del Draft. Validar cambia el estado a Validated.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@code {
|
|
[Parameter] public int Id { get; set; }
|
|
|
|
private SalesDocumentDto? Document;
|
|
private bool IsLoading;
|
|
private bool IsSaving;
|
|
private string? ReviewObservations;
|
|
private List<DeliveryNoteSummaryDto> OriginDeliveryNotes = new();
|
|
private SalesDocumentDraftValidationDto DraftValidation = new();
|
|
|
|
private bool IsDraft => Document?.Status == (int)SalesDocumentStatus.Draft;
|
|
private bool CanValidate => IsDraft && ValidationItems.All(x => x.IsValid);
|
|
private string DocumentNumber => string.IsNullOrWhiteSpace(Document?.InternalDocumentNumber) ? $"#{Document?.Id}" : Document.InternalDocumentNumber;
|
|
|
|
private List<ValidationItem> ValidationItems =>
|
|
[
|
|
new(DraftValidation.HasDetails, "Tiene detalles"),
|
|
new(DraftValidation.HasValidAmounts, "Tiene importes validos"),
|
|
new(DraftValidation.HasCustomer, "Posee cliente asignado"),
|
|
new(DraftValidation.IsDraft, "Permanece en estado Draft")
|
|
];
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadAsync();
|
|
}
|
|
|
|
private async Task LoadAsync()
|
|
{
|
|
try
|
|
{
|
|
IsLoading = true;
|
|
var preview = await SalesDocumentService.GetDraftPreviewAsync(Id);
|
|
ApplyPreview(preview);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
toastService.ShowError(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
IsLoading = false;
|
|
}
|
|
}
|
|
|
|
private async Task SaveReviewAsync()
|
|
{
|
|
if (Document is null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
IsSaving = true;
|
|
var preview = await SalesDocumentService.UpdateDraftReviewAsync(Id, new SalesDocumentDraftReviewDto
|
|
{
|
|
IssueDate = Document.IssueDate,
|
|
AssociatedDocumentType = Document.AssociatedDocumentType,
|
|
AssociatedDocumentNumber = Document.AssociatedDocumentNumber,
|
|
AssociatedDocumentDate = Document.AssociatedDocumentDate,
|
|
Observations = ReviewObservations,
|
|
PeriodFrom = Document.PeriodFrom,
|
|
PeriodTo = Document.PeriodTo
|
|
});
|
|
|
|
ApplyPreview(preview);
|
|
toastService.ShowSuccess("Revision guardada correctamente.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
toastService.ShowError(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
IsSaving = false;
|
|
}
|
|
}
|
|
|
|
private async Task ValidateDraftAsync()
|
|
{
|
|
try
|
|
{
|
|
IsSaving = true;
|
|
var preview = await SalesDocumentService.ValidateDraftAsync(Id);
|
|
ApplyPreview(preview);
|
|
toastService.ShowSuccess("Sales Document validado correctamente.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
toastService.ShowError(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
IsSaving = false;
|
|
}
|
|
}
|
|
|
|
private void BackToList() => Navigation.NavigateTo("/salesdocuments");
|
|
private void ViewDetail() => Navigation.NavigateTo($"/salesdocuments/{Id}");
|
|
|
|
private void ApplyPreview(SalesDocumentDraftPreviewDto? preview)
|
|
{
|
|
Document = preview?.Document;
|
|
ReviewObservations = Document?.Observations;
|
|
OriginDeliveryNotes = preview?.OriginDeliveryNotes ?? new List<DeliveryNoteSummaryDto>();
|
|
DraftValidation = preview?.Validation ?? new SalesDocumentDraftValidationDto();
|
|
}
|
|
|
|
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 debito",
|
|
SalesDocumentType.CreditNote => "Nota de credito",
|
|
SalesDocumentType.CreditInvoice => "Factura credito",
|
|
SalesDocumentType.CreditDebitNote => "N/D credito",
|
|
SalesDocumentType.CreditCreditNote => "N/C credito",
|
|
_ => 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 GetCoverageTypeLabel(int value) => Enum.IsDefined(typeof(SalesDocumentCoverageType), value)
|
|
? ((SalesDocumentCoverageType)value) switch
|
|
{
|
|
SalesDocumentCoverageType.Direct => "Directa",
|
|
SalesDocumentCoverageType.Capita => "Capita",
|
|
SalesDocumentCoverageType.Adjustment => "Ajuste",
|
|
SalesDocumentCoverageType.Manual => "Manual",
|
|
_ => value.ToString()
|
|
}
|
|
: value.ToString();
|
|
|
|
private static string GetOriginTypeLabel(string value) => value switch
|
|
{
|
|
"MANUAL" => "Manual",
|
|
"QUOTE" => "Presupuesto",
|
|
"ADJUSTMENT" => "Ajuste",
|
|
"CAPITA" => "Capita",
|
|
"DELIVERY_NOTE" => "Remito",
|
|
_ => string.IsNullOrWhiteSpace(value) ? "-" : value
|
|
};
|
|
|
|
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 ValidationItem(bool IsValid, string Message);
|
|
}
|