Add envelope creation functionality
Introduced the ability to create envelopes with documents and receivers via a new `CreateAsync` method in `EnvelopeReceiverService`. Integrated this functionality into `EnvelopeSenderEditorPage.razor` with UI updates, including a save button spinner, validation checks, and a result popup for success or error feedback. - Added `CreateAsync` method to handle `POST /api/EnvelopeReceiver` API calls. - Injected `EnvelopeReceiverService` into `EnvelopeSenderEditorPage.razor`. - Implemented save logic with validation for PDF upload, receivers, and signature fields. - Added success and error popups for user feedback. - Improved logging for envelope creation and validation warnings. - Refactored save logic for better readability and maintainability.
This commit is contained in:
@@ -2,6 +2,7 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
|
||||
using EnvelopeGenerator.Server.Client.Models;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
@@ -9,6 +10,7 @@ namespace EnvelopeGenerator.Server.Client.Services;
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver
|
||||
/// from <c>GET /api/EnvelopeReceiver/{envelopeKey}</c>.
|
||||
/// Also creates new envelopes via <c>POST /api/EnvelopeReceiver</c>.
|
||||
/// </summary>
|
||||
public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
@@ -37,4 +39,28 @@ public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<EnvelopeReceiverDto>(_jsonOptions, cancel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new envelope with document and receivers via <c>POST /api/EnvelopeReceiver</c>.
|
||||
/// Requires sender authentication cookie to be present in the request.
|
||||
/// </summary>
|
||||
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
|
||||
public async Task<CreateEnvelopeReceiverResponse?> CreateAsync(
|
||||
CreateEnvelopeReceiverCommand request,
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var response = await http.PostAsJsonAsync("/api/EnvelopeReceiver", request, _jsonOptions, cancel);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(cancel);
|
||||
throw new HttpRequestException(
|
||||
$"Fehler beim Erstellen des Umschlags. Status: {(int)response.StatusCode} – {body}",
|
||||
null,
|
||||
response.StatusCode);
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<CreateEnvelopeReceiverResponse>(_jsonOptions, cancel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@using DevExpress.Blazor.PdfViewer
|
||||
@using DevExpress.Blazor.Reporting.Models
|
||||
@using DevExpress.Blazor
|
||||
@using EnvelopeGenerator.Application.EnvelopeReceivers.Commands
|
||||
@using EnvelopeGenerator.Server.Client.Services
|
||||
@using EnvelopeGenerator.Server.Services
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@@ -12,6 +13,7 @@
|
||||
@inject AppVersionService AppVersion
|
||||
@inject ILogger<EnvelopeSenderEditorPage> Logger
|
||||
@inject EnvelopeReceiverPageDataService ReceiverPageDataService
|
||||
@inject EnvelopeReceiverService EnvelopeReceiverService
|
||||
@inject IMemoryCache MemoryCache
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
@@ -143,11 +145,20 @@
|
||||
@* Save *@
|
||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--success"
|
||||
@onclick="SaveAsync"
|
||||
disabled="@_isSaving"
|
||||
title="Speichern"
|
||||
style="background: linear-gradient(135deg,#059669,#047857); color:#fff; border-radius:6px; padding:0.3rem 0.75rem; font-size:0.75rem; font-weight:600; border:none;">
|
||||
@if (_isSaving)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-1" role="status"
|
||||
style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
||||
</svg>
|
||||
}
|
||||
Speichern
|
||||
</button>
|
||||
}
|
||||
@@ -303,6 +314,79 @@
|
||||
</FooterContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
@* ── Save result popup (success + error) ── *@
|
||||
<DxPopup @bind-Visible="_savePopupVisible"
|
||||
HeaderText="@(_saveErrorMessage is null ? "Umschlag erstellt" : "Fehler")"
|
||||
Width="460px"
|
||||
MaxWidth="95vw"
|
||||
ShowFooter="true"
|
||||
CloseOnOutsideClick="false"
|
||||
ShowCloseButton="false"
|
||||
CloseOnEscape="false">
|
||||
<BodyContentTemplate>
|
||||
@if (_saveErrorMessage is null)
|
||||
{
|
||||
<div style="display:flex;align-items:flex-start;gap:1rem;padding:0.5rem 0;">
|
||||
<div style="flex-shrink:0;width:40px;height:40px;background:#d1fae5;border-radius:50%;
|
||||
display:flex;align-items:center;justify-content:center;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#065f46" viewBox="0 0 16 16">
|
||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p style="margin:0 0 0.4rem;font-weight:600;color:#1f2937;font-size:0.95rem;">
|
||||
Umschlag wurde erfolgreich erstellt.
|
||||
</p>
|
||||
<p style="margin:0;color:#6b7280;font-size:0.85rem;line-height:1.5;">
|
||||
Der Umschlag wurde gespeichert und die Empfänger wurden benachrichtigt.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="display:flex;align-items:flex-start;gap:1rem;padding:0.5rem 0;">
|
||||
<div style="flex-shrink:0;width:40px;height:40px;background:#fee2e2;border-radius:50%;
|
||||
display:flex;align-items:center;justify-content:center;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#991b1b" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p style="margin:0 0 0.4rem;font-weight:600;color:#1f2937;font-size:0.95rem;">
|
||||
Fehler beim Erstellen des Umschlags
|
||||
</p>
|
||||
<p style="margin:0;color:#6b7280;font-size:0.85rem;line-height:1.5;word-break:break-word;">
|
||||
@_saveErrorMessage
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</BodyContentTemplate>
|
||||
<FooterContentTemplate>
|
||||
<div style="display:flex;gap:0.5rem;justify-content:flex-end;width:100%;padding:0.5rem 0;">
|
||||
@if (_saveErrorMessage is null)
|
||||
{
|
||||
<button class="btn btn-primary"
|
||||
@onclick="GoToDashboard"
|
||||
style="background:linear-gradient(135deg,#059669,#047857);border:none;border-radius:6px;
|
||||
padding:0.5rem 1.5rem;font-weight:600;color:#fff;">
|
||||
Zur Übersicht
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-outline-secondary"
|
||||
@onclick="() => _savePopupVisible = false"
|
||||
style="border-radius:6px;padding:0.5rem 1.25rem;font-weight:500;">
|
||||
Schließen
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</FooterContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
@code {
|
||||
// ── Session query param — persists across SignalR reconnects ──
|
||||
[SupplyParameterFromQuery(Name = "esid")]
|
||||
@@ -330,6 +414,11 @@
|
||||
List<SignatureFieldDraft> _signatureFields = [];
|
||||
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
|
||||
|
||||
// ── Save state ──
|
||||
bool _isSaving = false;
|
||||
bool _savePopupVisible = false;
|
||||
string? _saveErrorMessage = null;
|
||||
|
||||
List<ReceiverDraft> _receivers = [];
|
||||
bool _receiverPopupVisible;
|
||||
string _receiverDraftName = string.Empty;
|
||||
@@ -523,24 +612,111 @@
|
||||
|
||||
void Cancel() => NavigationManager.NavigateTo("/sender");
|
||||
|
||||
// ── Save ──
|
||||
void GoToDashboard()
|
||||
{
|
||||
// Clear session from cache on successful save so it is not restored if user returns
|
||||
if (!string.IsNullOrWhiteSpace(Esid))
|
||||
MemoryCache.Remove(SessionKey);
|
||||
|
||||
NavigationManager.NavigateTo("/sender");
|
||||
}
|
||||
|
||||
// ── Save — POST /api/EnvelopeReceiver ──
|
||||
async Task SaveAsync()
|
||||
{
|
||||
// ── Validation ──
|
||||
if (!_pdfLoaded || _originalPdfBytes is null)
|
||||
{
|
||||
_saveErrorMessage = "Bitte laden Sie zuerst ein PDF-Dokument hoch.";
|
||||
_savePopupVisible = true;
|
||||
return;
|
||||
}
|
||||
if (_receivers.Count == 0)
|
||||
{
|
||||
_saveErrorMessage = "Bitte fügen Sie mindestens einen Empfänger hinzu.";
|
||||
_savePopupVisible = true;
|
||||
return;
|
||||
}
|
||||
if (_signatureFields.Count == 0)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("console.log",
|
||||
"[SenderEditor] No signature fields to save.");
|
||||
_saveErrorMessage = "Bitte platzieren Sie mindestens ein Signaturfeld.";
|
||||
_savePopupVisible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var f in _signatureFields)
|
||||
// Warn if any receiver has no signature field, but don't block
|
||||
var receiversWithoutField = _receivers
|
||||
.Where(r => !_signatureFields.Any(f => f.ReceiverName == r.FullName))
|
||||
.ToList();
|
||||
if (receiversWithoutField.Count > 0)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("console.log",
|
||||
$"[SenderEditor] Field: Page={f.Page} X={f.XPt:F2}pt ({f.XPt / 72:F3}in) Y={f.YPt:F2}pt ({f.YPt / 72:F3}in) Receiver={f.ReceiverName}");
|
||||
Logger.LogWarning(
|
||||
"[SenderEditor] Receivers without signature field: {Names}",
|
||||
string.Join(", ", receiversWithoutField.Select(r => r.FullName)));
|
||||
}
|
||||
|
||||
await JSRuntime.InvokeVoidAsync("console.log",
|
||||
$"[SenderEditor] Total fields: {_signatureFields.Count}");
|
||||
_isSaving = true;
|
||||
_saveErrorMessage = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
// ── Build request ──
|
||||
// Document: use the ORIGINAL pdf (without placeholder burn-in) as base64
|
||||
var docBase64 = Convert.ToBase64String(_originalPdfBytes);
|
||||
|
||||
// Build receivers list — each receiver gets their own signature fields
|
||||
// Coordinate conversion: PDF points → inches (DB stores inches)
|
||||
var receiversCmd = _receivers.Select(receiver =>
|
||||
{
|
||||
var fields = _signatureFields
|
||||
.Where(f => f.ReceiverName == receiver.FullName)
|
||||
.Select(f => new DocReceiverElementCreateDto(
|
||||
X: f.XPt / 72.0,
|
||||
Y: f.YPt / 72.0,
|
||||
Page: f.Page))
|
||||
.ToList();
|
||||
|
||||
return new ReceiverGetOrCreateCommand
|
||||
{
|
||||
EmailAddress = receiver.Email,
|
||||
Salution = receiver.FullName,
|
||||
PhoneNumber = string.IsNullOrWhiteSpace(receiver.PhoneNumber)
|
||||
? null
|
||||
: receiver.PhoneNumber,
|
||||
DocReceiverElements = fields,
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
var command = new CreateEnvelopeReceiverCommand
|
||||
{
|
||||
Title = "Neuer Umschlag", // placeholder — dedicated field will be added later
|
||||
Message = "Bitte unterzeichnen Sie das beigefügte Dokument.",
|
||||
TFAEnabled = false,
|
||||
Document = new DocumentCreateCommand { DataAsBase64 = docBase64 },
|
||||
Receivers = receiversCmd,
|
||||
};
|
||||
|
||||
var result = await EnvelopeReceiverService.CreateAsync(command);
|
||||
|
||||
Logger.LogInformation(
|
||||
"[SenderEditor] Envelope created. Id={Id} SentReceivers={Count}",
|
||||
result?.Id, result?.SentReceiver.Count());
|
||||
|
||||
// Success — show popup; GoToDashboard clears cache and navigates
|
||||
_saveErrorMessage = null;
|
||||
_savePopupVisible = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "[SenderEditor] Failed to create envelope");
|
||||
_saveErrorMessage = ex.Message;
|
||||
_savePopupVisible = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cache persistence ──
|
||||
|
||||
Reference in New Issue
Block a user