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:
2026-07-02 01:44:53 +02:00
parent a2443032c5
commit e80059d34e
2 changed files with 213 additions and 11 deletions

View File

@@ -2,6 +2,7 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
using EnvelopeGenerator.Server.Client.Models; using EnvelopeGenerator.Server.Client.Models;
namespace EnvelopeGenerator.Server.Client.Services; namespace EnvelopeGenerator.Server.Client.Services;
@@ -9,6 +10,7 @@ namespace EnvelopeGenerator.Server.Client.Services;
/// <summary> /// <summary>
/// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver /// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver
/// from <c>GET /api/EnvelopeReceiver/{envelopeKey}</c>. /// from <c>GET /api/EnvelopeReceiver/{envelopeKey}</c>.
/// Also creates new envelopes via <c>POST /api/EnvelopeReceiver</c>.
/// </summary> /// </summary>
public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory) public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
{ {
@@ -37,4 +39,28 @@ public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
return await response.Content.ReadFromJsonAsync<EnvelopeReceiverDto>(_jsonOptions, cancel); 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);
}
} }

View File

@@ -3,6 +3,7 @@
@using DevExpress.Blazor.PdfViewer @using DevExpress.Blazor.PdfViewer
@using DevExpress.Blazor.Reporting.Models @using DevExpress.Blazor.Reporting.Models
@using DevExpress.Blazor @using DevExpress.Blazor
@using EnvelopeGenerator.Application.EnvelopeReceivers.Commands
@using EnvelopeGenerator.Server.Client.Services @using EnvelopeGenerator.Server.Client.Services
@using EnvelopeGenerator.Server.Services @using EnvelopeGenerator.Server.Services
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@@ -12,6 +13,7 @@
@inject AppVersionService AppVersion @inject AppVersionService AppVersion
@inject ILogger<EnvelopeSenderEditorPage> Logger @inject ILogger<EnvelopeSenderEditorPage> Logger
@inject EnvelopeReceiverPageDataService ReceiverPageDataService @inject EnvelopeReceiverPageDataService ReceiverPageDataService
@inject EnvelopeReceiverService EnvelopeReceiverService
@inject IMemoryCache MemoryCache @inject IMemoryCache MemoryCache
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" /> <link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
@@ -143,11 +145,20 @@
@* Save *@ @* Save *@
<button class="pdf-toolbar__btn pdf-toolbar__btn--success" <button class="pdf-toolbar__btn pdf-toolbar__btn--success"
@onclick="SaveAsync" @onclick="SaveAsync"
disabled="@_isSaving"
title="Speichern" 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;"> 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"> <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" /> <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> </svg>
}
Speichern Speichern
</button> </button>
} }
@@ -303,6 +314,79 @@
</FooterContentTemplate> </FooterContentTemplate>
</DxPopup> </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 { @code {
// ── Session query param — persists across SignalR reconnects ── // ── Session query param — persists across SignalR reconnects ──
[SupplyParameterFromQuery(Name = "esid")] [SupplyParameterFromQuery(Name = "esid")]
@@ -330,6 +414,11 @@
List<SignatureFieldDraft> _signatureFields = []; List<SignatureFieldDraft> _signatureFields = [];
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen" ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
// ── Save state ──
bool _isSaving = false;
bool _savePopupVisible = false;
string? _saveErrorMessage = null;
List<ReceiverDraft> _receivers = []; List<ReceiverDraft> _receivers = [];
bool _receiverPopupVisible; bool _receiverPopupVisible;
string _receiverDraftName = string.Empty; string _receiverDraftName = string.Empty;
@@ -523,24 +612,111 @@
void Cancel() => NavigationManager.NavigateTo("/sender"); 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() 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) if (_signatureFields.Count == 0)
{ {
await JSRuntime.InvokeVoidAsync("console.log", _saveErrorMessage = "Bitte platzieren Sie mindestens ein Signaturfeld.";
"[SenderEditor] No signature fields to save."); _savePopupVisible = true;
return; 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", Logger.LogWarning(
$"[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}"); "[SenderEditor] Receivers without signature field: {Names}",
string.Join(", ", receiversWithoutField.Select(r => r.FullName)));
} }
await JSRuntime.InvokeVoidAsync("console.log", _isSaving = true;
$"[SenderEditor] Total fields: {_signatureFields.Count}"); _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 ── // ── Cache persistence ──