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;
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
@if (_isSaving)
|
||||||
<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>
|
<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
|
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 ──
|
||||||
|
|||||||
Reference in New Issue
Block a user