Refactor and optimize envelope handling

Refactored `ReadDocumentDetailsQueryHandler` to remove AutoMapper, improving performance by manually selecting necessary data. Simplified `SenderEditorPage.razor` by removing bootstrap logic and updating the UI for a more compact design. Streamlined envelope creation in `SenderPage.razor` by eliminating the bootstrap token. Updated styles in `envelope-viewer.css` for better alignment and readability.
This commit is contained in:
2026-10-27 21:56:39 +01:00
parent fc5ad7ea3c
commit 74e55e9206
4 changed files with 198 additions and 273 deletions

View File

@@ -10,7 +10,6 @@
@using EnvelopeGenerator.Application.Common.Configurations
@using EnvelopeGenerator.Server.Client.Components.PdfEditor
@using EnvelopeGenerator.Server.Client.Services
@using Microsoft.AspNetCore.WebUtilities
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Localization
@using Microsoft.Extensions.Options
@@ -24,7 +23,6 @@
@inject EnvelopeReceiverService EnvelopeReceiverService
@inject EnvelopeService EnvelopeService
@inject EnvelopeTypeService EnvelopeTypeService
@inject CreateEnvelopeBootstrapStorageService CreateEnvelopeBootstrapStorageService
@inject AuthService AuthService
@inject IStringLocalizer<Resource> UiLocalizer
@inject IStringLocalizer<Model> ModelLocalizer
@@ -35,11 +33,6 @@
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
<script src="@AppVersion.GetVersionedUrl("js/envelope-editor.js")"></script>
<DxLoadingPanel Visible="@IsBootstrapLoading"
IsContentBlocked="true"
ApplyBackgroundShading="true"
IndicatorAreaVisible="true"
Text='@L("SenderEditor_BootstrapLoadingHint", "Der Umschlag wird vorbereitet und die Daten werden uebernommen.")'>
<div class="envelope-viewer-layout">
@* ── Action Bar ── *@
@@ -199,20 +192,43 @@
</div>
<div class="sender-receivers-panel">
<div class="sender-receivers-panel__header">
<div class="sender-receivers-panel__title-wrap">
<span class="sender-receivers-panel__title">@L("Receiver", "Empfänger")</span>
<span class="sender-receivers-panel__count">@_receivers.Count</span>
</div>
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn"
<div class="sender-receivers-list">
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-receivers-list__add-btn"
@onclick="OpenAddReceiverPopup"
title='@L("AddReceiver", "Empfänger hinzufügen")'>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 1a.5.5 0 0 1 .5.5v6h6a.5.5 0 0 1 0 1h-6v6a.5.5 0 0 1-1 0v-6h-6a.5.5 0 0 1 0-1h6v-6A.5.5 0 0 1 8 1z" />
</svg>
<span class="pdf-toolbar__btn-text">@L("AddReceiver", "Empfänger hinzufügen")</span>
<span class="sender-receivers-list__add-text">@L("AddReceiver", "Empfänger hinzufügen")</span>
<span class="badge rounded-pill text-bg-primary sender-receivers-list__count">@_receivers.Count</span>
</button>
@foreach (var receiver in _receivers)
{
<div class="sender-receiver-chip" style="--receiver-color:@receiver.Color;">
<div class="sender-receiver-chip__body">
<div class="sender-receiver-chip__email" title="@receiver.FullName">@receiver.Email</div>
</div>
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset sender-toolbar-action-btn sender-toolbar-action-btn--compact sender-receiver-chip__action sender-receiver-chip__remove-btn"
@onclick="() => RemoveReceiverAsync(receiver)"
title='@L("Delete", "Loeschen")'>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
</button>
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-toolbar-action-btn--compact sender-receiver-chip__action sender-receiver-chip__place-btn
@(_pendingReceiverForPlacement?.Id == receiver.Id ? "sender-receiver-chip__place-btn--active" : "sender-receiver-chip__place-btn--idle")"
@onclick="() => ActivatePlacementForReceiver(receiver)"
style="--receiver-color:@receiver.Color;"
title='@L("SenderEditor_PlaceSignatureField", "Signaturfeld platzieren")'>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10z" />
</svg>
</button>
</div>
}
</div>
@if (_receivers.Count == 0)
@@ -221,43 +237,6 @@
@L("SenderEditor_NoReceiversAdded", "Noch keine Empfänger hinzugefügt.")
</div>
}
else
{
<div class="sender-receivers-list">
@foreach (var receiver in _receivers)
{
<div class="sender-receiver-chip" style="--receiver-color:@receiver.Color;">
<div class="sender-receiver-chip__body">
<div class="sender-receiver-chip__name">@receiver.FullName</div>
<div class="sender-receiver-chip__email">@receiver.Email</div>
@if (!string.IsNullOrWhiteSpace(receiver.PhoneNumber))
{
<div class="sender-receiver-chip__phone">@receiver.PhoneNumber</div>
}
</div>
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset sender-toolbar-action-btn sender-toolbar-action-btn--compact sender-receiver-chip__action sender-receiver-chip__remove-btn"
@onclick="() => RemoveReceiverAsync(receiver)"
title='@L("Delete", "Loeschen")'>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
</button>
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-toolbar-action-btn--compact sender-receiver-chip__place-btn
@(_pendingReceiverForPlacement?.Id == receiver.Id ? "sender-receiver-chip__place-btn--active" : "sender-receiver-chip__place-btn--idle")"
@onclick="() => ActivatePlacementForReceiver(receiver)"
style="--receiver-color:@receiver.Color;"
title='@L("SenderEditor_PlaceSignatureField", "Signaturfeld platzieren")'>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10z" />
</svg>
<span class="pdf-toolbar__btn-text">@L("SenderEditor_AddSignature", "Signatur hinzufügen")</span>
</button>
</div>
}
</div>
}
</div>
</div>
@@ -442,7 +421,6 @@
}
</div>
</div>
</DxLoadingPanel>
<DxPopup @bind-Visible="_receiverPopupVisible"
HeaderText='@L("AddReceiver", "Empfänger hinzufügen")'
@@ -607,9 +585,6 @@
[SupplyParameterFromQuery(Name = "envelopeId")]
public int? EnvelopeId { get; set; }
[SupplyParameterFromQuery(Name = "bootstrapToken")]
public string? BootstrapToken { get; set; }
// ── Signature field size (configurable) ──
double SigWidthInches => SignatureFieldOptions.CurrentValue.Width;
double SigHeightInches => SignatureFieldOptions.CurrentValue.Height;
@@ -629,10 +604,7 @@
int _pageCount;
List<PdfPageImageDto> _pagePreviews = [];
int? _loadedEnvelopeId;
int? _editingEnvelopeId;
bool _isLoadingEnvelopeForEdit;
bool _bootstrapApplied;
string? _pendingBootstrapToken;
List<SignatureFieldDraft> _signatureFields = [];
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
@@ -653,7 +625,6 @@
string _saveSuccessTitle = "Vorgang erfolgreich abgeschlossen.";
string _saveSuccessBody = string.Empty;
bool _saveNavigateToDashboard = false;
bool _saveDraftAfterBootstrap;
// ── Envelope metadata ──
string _envelopeTitle = string.Empty;
@@ -687,9 +658,6 @@
static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute ReceiverEmailValidator = new();
bool IsEditMode => EnvelopeId.HasValue && EnvelopeId.Value > 0;
bool IsBootstrapLoading => !IsEditMode && !_bootstrapApplied;
// ── Lifecycle ──
protected override async Task OnInitializedAsync()
@@ -704,12 +672,6 @@
try
{
_envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList();
if (!IsEditMode && _selectedEnvelopeTypeId is null && _envelopeTypes.Count > 0)
{
_selectedEnvelopeTypeId = _envelopeTypes[0].Id;
ApplyEnvelopeTypeDefaults(_envelopeTypes[0]);
}
}
catch (Exception ex)
{
@@ -720,96 +682,14 @@
protected override async Task OnParametersSetAsync()
{
if (EnvelopeId.HasValue && EnvelopeId.Value > 0)
_editingEnvelopeId = EnvelopeId.Value;
if (IsEditMode && _loadedEnvelopeId != EnvelopeId)
{
await LoadEnvelopeForEditAsync(EnvelopeId!.Value);
return;
}
if (IsEditMode || _bootstrapApplied)
return;
if (string.IsNullOrWhiteSpace(BootstrapToken))
if (!EnvelopeId.HasValue || EnvelopeId.Value <= 0)
{
NavigationManager.NavigateTo("/sender", replace: true);
return;
}
_pendingBootstrapToken = BootstrapToken;
}
async Task TryApplyBootstrapAsync(string token)
{
var payload = await CreateEnvelopeBootstrapStorageService.ConsumeAsync(token);
_bootstrapApplied = true;
if (payload is null)
{
NavigationManager.NavigateTo("/sender", replace: true);
return;
}
_envelopeTitle = payload.Title ?? string.Empty;
_envelopeMessage = payload.Message ?? string.Empty;
_selectedEnvelopeTypeId = payload.EnvelopeTypeId;
_language = string.IsNullOrWhiteSpace(payload.Language) ? "de-DE" : payload.Language;
_useAccessCode = payload.UseAccessCode;
_tfaEnabled = payload.TFAEnabled;
_certificationType = payload.CertificationType;
_finalEmailToCreator = payload.FinalEmailToCreator;
_finalEmailToReceivers = payload.FinalEmailToReceivers;
_sendReminderEmails = payload.SendReminderEmails;
_firstReminderDays = payload.FirstReminderDays;
_reminderIntervalDays = payload.ReminderIntervalDays;
_expiresWhenDays = payload.ExpiresWhenDays;
_expiresWarningWhenDays = payload.ExpiresWarningWhenDays;
NormalizeSettings();
_receivers = payload.Receivers
.Select((receiver, index) => new ReceiverDraft(
Guid.NewGuid(),
null,
receiver.FullName,
receiver.EmailAddress,
string.Empty,
ReceiverPalette[index % ReceiverPalette.Length]))
.ToList();
_originalPdfBytes = payload.DocumentBytes;
_fileName = string.IsNullOrWhiteSpace(payload.DocumentFileName) ? "document.pdf" : payload.DocumentFileName;
_pdfLoaded = _originalPdfBytes is { Length: > 0 };
_signatureFields.Clear();
_pendingReceiverForPlacement = null;
_saveDraftAfterBootstrap = true;
if (_pdfLoaded && _originalPdfBytes is not null)
{
try
{
_pagePreviews = (await PdfRenderService.RenderPagesAsync(_originalPdfBytes))
.OrderBy(p => p.PageNumber)
.ToList();
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to render bootstrap PDF previews.");
_pagePreviews = [];
}
_pageCount = _pagePreviews.Count > 0
? _pagePreviews.Count
: EstimatePdfPageCount(_originalPdfBytes) ?? 1;
}
else
{
_pagePreviews = [];
_pageCount = 0;
}
if (_loadedEnvelopeId != EnvelopeId && !_isLoadingEnvelopeForEdit)
await LoadEnvelopeForEditAsync(EnvelopeId.Value);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -819,26 +699,6 @@
_isPageReady = true;
await InvokeAsync(StateHasChanged);
}
if (!OperatingSystem.IsBrowser())
return;
if (!IsEditMode && !_bootstrapApplied && !string.IsNullOrWhiteSpace(_pendingBootstrapToken))
{
var token = _pendingBootstrapToken;
_pendingBootstrapToken = null;
await TryApplyBootstrapAsync(token);
await InvokeAsync(StateHasChanged);
return;
}
if (_saveDraftAfterBootstrap && !_isSaving)
{
_saveDraftAfterBootstrap = false;
await SaveDraftAndStayOnEditorAsync();
return;
}
}
async Task LoadEnvelopeForEditAsync(int envelopeId)
@@ -960,7 +820,6 @@
_fileName = $"Envelope-{envelopeId}.pdf";
_pendingReceiverForPlacement = null;
_loadedEnvelopeId = envelopeId;
_editingEnvelopeId = envelopeId;
}
catch (Exception ex)
{
@@ -1263,9 +1122,8 @@
try
{
var previousEnvelopeId = EnvelopeId;
var command = BuildCreateEnvelopeCommand(send: false);
Logger.LogInformation("[SenderEditor] Saving draft. EnvelopeId={EnvelopeId} IsEditMode={IsEditMode}", command.EnvelopeId, IsEditMode);
Logger.LogInformation("[SenderEditor] Saving draft. EnvelopeId={EnvelopeId}", command.EnvelopeId);
var result = await EnvelopeService.UpsertAsync(command);
if (result is null)
@@ -1273,16 +1131,9 @@
EnvelopeId = result.Id;
_loadedEnvelopeId = result.Id;
_editingEnvelopeId = result.Id;
if (navigateToDashboard)
{
NavigationManager.NavigateTo("/sender", forceLoad: false);
}
else if (previousEnvelopeId != result.Id)
{
NavigationManager.NavigateTo($"/sender/editor?envelopeId={result.Id}", forceLoad: false);
}
return result.Id;
}
@@ -1300,12 +1151,6 @@
}
}
async Task SaveDraftAndStayOnEditorAsync()
{
Logger.LogInformation("[SenderEditor] Trigger bootstrap draft save.");
await SaveDraftCoreAsync(navigateToDashboard: false);
}
// ── Send envelope ──
async Task SendAsync()
{
@@ -1360,21 +1205,10 @@
try
{
var envelopeId = _editingEnvelopeId
?? EnvelopeId
?? _loadedEnvelopeId
?? GetEnvelopeIdFromCurrentUri();
var envelopeId = EnvelopeId ?? _loadedEnvelopeId;
if (!envelopeId.HasValue || envelopeId.Value <= 0)
{
var saveResult = await EnvelopeService.UpsertAsync(BuildCreateEnvelopeCommand(send: false))
?? throw new InvalidOperationException("Draft save did not return a result.");
envelopeId = saveResult.Id;
EnvelopeId = saveResult.Id;
_loadedEnvelopeId = saveResult.Id;
_editingEnvelopeId = saveResult.Id;
}
throw new InvalidOperationException("Envelope id is missing for send.");
var sendResult = await EnvelopeService.UpsertAsync(BuildCreateEnvelopeCommand(send: true) with { EnvelopeId = envelopeId })
?? throw new InvalidOperationException("Send did not return a result.");
@@ -1405,13 +1239,10 @@
{
NormalizeSettings();
var envelopeId = _editingEnvelopeId
?? EnvelopeId
?? _loadedEnvelopeId
?? GetEnvelopeIdFromCurrentUri();
var envelopeId = EnvelopeId ?? _loadedEnvelopeId;
if (IsEditMode && envelopeId is null)
throw new InvalidOperationException("Edit mode envelope id is missing.");
if (envelopeId is null)
throw new InvalidOperationException("Envelope id is missing.");
var docBase64 = _originalPdfBytes is { Length: > 0 }
? Convert.ToBase64String(_originalPdfBytes)
@@ -1499,28 +1330,6 @@
return true;
}
int? GetEnvelopeIdFromCurrentUri()
{
try
{
var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
if (string.IsNullOrWhiteSpace(uri.Query))
return null;
var query = QueryHelpers.ParseQuery(uri.Query);
if (!query.TryGetValue("envelopeId", out var raw))
return null;
return int.TryParse(raw.ToString(), out var id) && id > 0
? id
: null;
}
catch
{
return null;
}
}
// ── Receiver popup ──
void OpenAddReceiverPopup()
{
@@ -1693,10 +1502,7 @@
if (_isSaving)
return;
var envelopeId = _editingEnvelopeId
?? EnvelopeId
?? _loadedEnvelopeId
?? GetEnvelopeIdFromCurrentUri();
var envelopeId = EnvelopeId ?? _loadedEnvelopeId;
if (envelopeId is int persistedEnvelopeId && persistedEnvelopeId > 0 && receiver.ReceiverId is int persistedReceiverId && persistedReceiverId > 0)
{

View File

@@ -7,6 +7,7 @@
@using EnvelopeGenerator.Server.Client.Services
@using EnvelopeGenerator.Application.Common.Dto
@using EnvelopeGenerator.Application.Common.Dto.History
@using EnvelopeGenerator.Application.Envelopes.Commands
@using EnvelopeGenerator.Application.Resources
@using EnvelopeGenerator.Domain.Resources
@using EnvelopeGenerator.Server.Client.Components.Sender
@@ -21,7 +22,6 @@
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
@inject EnvelopeGenerator.Server.Client.Services.PdfSignatureService PdfSignatureService
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeReceiverService EnvelopeReceiverService
@inject EnvelopeGenerator.Server.Client.Services.CreateEnvelopeBootstrapStorageService CreateEnvelopeBootstrapStorageService
@inject ILogger<SenderPage> Logger
@inject NavigationManager Navigation
@inject IJSRuntime JSRuntime
@@ -744,29 +744,44 @@
async Task StartEditorFromWizardAsync(CreateEnvelopeWizardResult result)
{
var payload = new CreateEnvelopeBootstrapPayload(
Title: result.Title,
Message: result.Message,
EnvelopeTypeId: result.EnvelopeTypeId,
Language: result.Language,
UseAccessCode: result.UseAccessCode,
TFAEnabled: result.TFAEnabled,
CertificationType: result.CertificationType,
FinalEmailToCreator: result.FinalEmailToCreator,
FinalEmailToReceivers: result.FinalEmailToReceivers,
SendReminderEmails: result.SendReminderEmails,
FirstReminderDays: result.FirstReminderDays,
ReminderIntervalDays: result.ReminderIntervalDays,
ExpiresWhenDays: result.ExpiresWhenDays,
ExpiresWarningWhenDays: result.ExpiresWarningWhenDays,
Receivers: result.Receivers
.Select(x => new CreateEnvelopeBootstrapReceiver(x.FullName, x.EmailAddress))
.ToList(),
DocumentBytes: result.DocumentBytes,
DocumentFileName: result.DocumentFileName);
var docBase64 = result.DocumentBytes is { Length: > 0 }
? Convert.ToBase64String(result.DocumentBytes)
: null;
var token = await CreateEnvelopeBootstrapStorageService.StoreAsync(payload);
Navigation.NavigateTo($"/sender/editor?bootstrapToken={token}");
var command = new CreateEnvelopeCommand
{
EnvelopeTypeId = result.EnvelopeTypeId,
Title = result.Title,
Message = result.Message,
Language = string.IsNullOrWhiteSpace(result.Language) ? "de-DE" : result.Language,
UseAccessCode = result.UseAccessCode,
TFAEnabled = result.TFAEnabled,
CertificationType = result.CertificationType,
FinalEmailToCreator = result.FinalEmailToCreator,
FinalEmailToReceivers = result.FinalEmailToReceivers,
SendReminderEmails = result.SendReminderEmails,
FirstReminderDays = result.FirstReminderDays,
ReminderIntervalDays = result.ReminderIntervalDays,
ExpiresWhenDays = result.ExpiresWhenDays,
ExpiresWarningWhenDays = result.ExpiresWarningWhenDays,
Send = false,
Document = string.IsNullOrWhiteSpace(docBase64) ? null : new CreateEnvelopeDocumentDto(docBase64),
Receivers = result.Receivers
.Select(x => new CreateEnvelopeReceiverDto(x.EmailAddress, x.FullName, null, []))
.ToList()
};
try
{
var envelope = await EnvelopeService.UpsertAsync(command)
?? throw new InvalidOperationException("Envelope creation returned no result.");
Navigation.NavigateTo($"/sender/editor?envelopeId={envelope.Id}");
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to create envelope from wizard.");
_errorMessage = ex.Message;
}
}
void EditEnvelope()