Files
EnvelopeGenerator/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/Sender/CreateEnvelopeWizardPopup.razor
TekH 94658b0d9b Simplify wizard flow and improve document upload UX
Reduced wizard steps from 5 to 4 by merging document upload into step 3. Added a new localized hint (`SenderWizard_FinalUploadHint`) to guide users during the final document upload. Removed redundant document preview functionality and associated UI elements.

Refactored the document upload area to always remain visible on the right side of the wizard, using the `PdfFileDropZone` component. Removed the "Skip" button for optional steps and cleaned up unused methods, properties, and CSS classes.

Updated step titles and descriptions to reflect the new flow. General code cleanup to simplify logic and improve maintainability.
2026-11-11 17:24:39 +01:00

1175 lines
63 KiB
Plaintext

@using EnvelopeGenerator.Application.Common.Dto
@using EnvelopeGenerator.Application.Common.Dto.Receiver
@using EnvelopeGenerator.Application.Resources
@using EnvelopeGenerator.Domain.Constants
@using EnvelopeGenerator.Domain.Resources
@using EnvelopeGenerator.Server.Client.Components.Sender.ReceiverSuggestionFilter
@using EnvelopeGenerator.Server.Client.Services
@using MudBlazor
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Localization
@inject IStringLocalizer<Model> ModelLocalizer
@inject IStringLocalizer<Resource> UiLocalizer
@inject ReceiverService ReceiverService
<DxPopup Visible="@_isVisible"
VisibleChanged="OnPopupVisibleChangedAsync"
HeaderText="@L("SenderWizard_Header", "Neuen Umschlag erstellen")"
Width="90vw"
MaxWidth="90vw"
ShowFooter="true"
CloseOnOutsideClick="false"
ShowCloseButton="true"
CloseOnEscape="true"
ShowHeader="true"
CssClass="sender-create-wizard-popup">
<BodyContentTemplate>
<div class="sender-create-wizard"
tabindex="0"
@onkeydown="OnWizardKeyDownAsync">
<div class="sender-create-wizard__body-layout">
<MudPaper Class="sender-create-wizard__stepper-shell" Elevation="0">
<MudStepper Vertical="true"
NonLinear="false"
ShowResetButton="false"
CurrentStepColor="Color.Info"
CompletedStepColor="Color.Success"
ErrorStepColor="Color.Error"
StepCompleteIcon="@Icons.Material.Filled.CheckCircle"
ActiveIndex="@_currentStep"
Class="sender-create-wizard__stepper">
<ChildContent>
@for (var step = 0; step < TotalSteps; step++)
{
var stepIndex = step;
<MudStep Title="@GetStepTitle(stepIndex)"
Completed="@_completedSteps.Contains(stepIndex)"
Skippable="false">
<div class="sender-create-wizard__stepper-description">@GetStepDescription(stepIndex)</div>
@if (stepIndex == _currentStep)
{
<div class="sender-create-wizard__step-content">
<div class="sender-create-wizard__step-layout">
<div class="sender-create-wizard__step-main">
@if (_currentStep == 0)
{
<div class="sender-create-wizard__form-grid">
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label sender-create-wizard__label-row">
<span>@L("Title", "Titel") <span class="sender-create-wizard__required">*</span></span>
<MudTooltip Text='@L("SenderWizard_Help_Title", "Ein kurzer, eindeutiger Titel hilft beim schnellen Wiederfinden des Umschlags.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_Title", "Hilfe zum Feld Titel")' />
</MudTooltip>
</label>
<DxTextBox Text="@_title"
TextChanged="OnTitleChanged"
BindValueMode="BindValueMode.OnInput"
NullText='@L("SenderEditor_Title_Placeholder", "z.B. Arbeitsvertrag 2025")'
CssClass="w-100" />
</div>
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label sender-create-wizard__label-row">
<span>@L("EnvelopeType", "Umschlagtyp") <span class="sender-create-wizard__required">*</span></span>
<MudTooltip Text='@L("SenderWizard_Help_EnvelopeType", "Der Umschlagtyp kann Standardwerte wie Sprache, Erinnerungen und Sicherheit vorbelegen.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_EnvelopeType", "Hilfe zum Feld Umschlagtyp")' />
</MudTooltip>
</label>
<select value="@(_selectedEnvelopeTypeId?.ToString() ?? string.Empty)"
@onchange="OnEnvelopeTypeChanged"
class="sender-create-wizard__select">
@foreach (var type in EnvelopeTypes)
{
<option value="@type.Id">@type.Title</option>
}
</select>
</div>
</div>
}
else if (_currentStep == 3)
{
<div class="sender-create-wizard__form-grid">
<div class="sender-create-wizard__field sender-create-wizard__field--full">
<label class="sender-create-wizard__label sender-create-wizard__label-row">
<span>@L("Message", "Nachricht") <span class="sender-create-wizard__optional">(@L("Optional", "optional"))</span></span>
<MudTooltip Text='@L("SenderWizard_Help_Message", "Diese Nachricht wird den Empfaengern in der E-Mail und im Signaturprozess angezeigt.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_Message", "Hilfe zum Feld Nachricht")' />
</MudTooltip>
</label>
<DxMemo Text="@_message"
TextChanged="OnMessageChanged"
Rows="5"
MaxLength="500"
NullText='@L("SenderEditor_Message_Placeholder", "Bitte unterzeichnen Sie das beigefügte Dokument.")'
CssClass="w-100" />
</div>
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label sender-create-wizard__label-row">
<span>@L("Language", "Sprache")</span>
<MudTooltip Text='@L("SenderWizard_Help_Language", "Legt die Sprache fuer Hinweise, E-Mails und Oberflaechentexte fest.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_Language", "Hilfe zum Feld Sprache")' />
</MudTooltip>
</label>
<select @bind="_language" class="sender-create-wizard__select">
<option value="de-DE">Deutsch (de-DE)</option>
<option value="en-US">English (en-US)</option>
<option value="de">Deutsch (de)</option>
<option value="en">English (en)</option>
</select>
</div>
<div class="sender-create-wizard__field sender-create-wizard__field--full">
<div class="sender-create-wizard__stage">
<p>@L("SenderWizard_FinalUploadHint", "Letzte Moeglichkeit im Wizard: Bitte laden Sie jetzt Ihr PDF ueber den rechten Upload-Bereich hoch. Im Editor koennen Sie spaeter ebenfalls ein Dokument hochladen oder ersetzen.")</p>
</div>
</div>
</div>
}
else if (_currentStep == 2)
{
<div class="sender-create-wizard__settings-grid">
<div class="sender-create-wizard__settings-group">
<div class="sender-create-wizard__settings-title">
@L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen")
</div>
<div class="sender-create-wizard__form-grid">
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label sender-create-wizard__label-row">
<span>@L("CertificationType", "Zertifizierung")</span>
<MudTooltip Text='@L("SenderWizard_Help_CertificationType", "Bestimmt das Signaturniveau fuer diesen Umschlag.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_CertificationType", "Hilfe zum Feld Zertifizierung")' />
</MudTooltip>
</label>
<select @bind="_certificationType" @bind:after="OnCertificationTypeChanged" class="sender-create-wizard__select">
<option value="@CertificationType.AdvancedElectronicSignature">@L("AdvancedElectronicSignature", "Advanced Electronic Signature")</option>
<option value="@CertificationType.ElectronicSignature">@L("ElectronicSignature", "Electronic Signature")</option>
</select>
</div>
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label sender-create-wizard__label-row">
<span>@L("FinalEmailToCreator", "Abgeschlossenes Dokument an Ersteller")</span>
<MudTooltip Text='@L("SenderWizard_Help_FinalEmailToCreator", "Steuert, ob der Ersteller nach Abschluss eine finale E-Mail erhaelt und ob das Dokument angehaengt wird.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_FinalEmailToCreator", "Hilfe zum Feld Abgeschlossenes Dokument an Ersteller")' />
</MudTooltip>
</label>
<select @bind="_finalEmailToCreator" class="sender-create-wizard__select">
<option value="0">@L("No", "Nein")</option>
<option value="1">@L("Yes", "Ja")</option>
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
</select>
</div>
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label sender-create-wizard__label-row">
<span>@L("FinalEmailToReceivers", "Abgeschlossenes Dokument an Empfänger")</span>
<MudTooltip Text='@L("SenderWizard_Help_FinalEmailToReceivers", "Steuert, ob Empfaenger nach Abschluss eine finale E-Mail erhalten und ob das Dokument angehaengt wird.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_FinalEmailToReceivers", "Hilfe zum Feld Abgeschlossenes Dokument an Empfaenger")' />
</MudTooltip>
</label>
<select @bind="_finalEmailToReceivers" class="sender-create-wizard__select">
<option value="0">@L("No", "Nein")</option>
<option value="1">@L("Yes", "Ja")</option>
<option value="2">@L("YesWithAttachment", "Ja, mit Anhang")</option>
</select>
</div>
<div class="sender-create-wizard__field sender-create-wizard__field--full">
<div class="sender-create-wizard__settings-toggles">
<label class="sender-create-wizard__settings-toggle">
<input type="checkbox" checked="@_tfaEnabled" @onchange="OnTfaChanged" disabled="@IsFesSelected" />
<span>@L("TFAEnabled", "2FA aktiviert")</span>
<MudTooltip Text='@L("SenderWizard_Help_TfaEnabled", "Wenn aktiviert, ist die Signaturfreigabe nur mit zusaetzlicher Verifizierung moeglich.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_TfaEnabled", "Hilfe zur Option 2FA")' />
</MudTooltip>
</label>
<label class="sender-create-wizard__settings-toggle">
<input type="checkbox" checked="@_useAccessCode" @onchange="OnUseAccessCodeChanged" disabled="@_tfaEnabled" />
<span>@L("UseAccessCode", "Zugriffscode verwenden")</span>
<MudTooltip Text='@L("SenderWizard_Help_UseAccessCode", "Empfaenger muessen einen Zugriffscode eingeben. Bei aktivem 2FA bleibt diese Option immer aktiv.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_UseAccessCode", "Hilfe zur Option Zugriffscode verwenden")' />
</MudTooltip>
</label>
</div>
</div>
</div>
</div>
<div class="sender-create-wizard__settings-group">
<div class="sender-create-wizard__settings-title">
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
</div>
<div class="sender-create-wizard__form-grid">
<div class="sender-create-wizard__field sender-create-wizard__field--full">
<label class="sender-create-wizard__settings-toggle">
<input type="checkbox" checked="@_sendReminderEmails" @onchange="OnSendReminderEmailsChanged" />
<span>@L("SendReminderEmails", "Erinnerungs-E-Mails senden")</span>
<MudTooltip Text='@L("SenderWizard_Help_SendReminderEmails", "Aktiviert automatische Erinnerungsmails fuer noch offene Signaturen.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_SendReminderEmails", "Hilfe zur Option Erinnerungs-E-Mails senden")' />
</MudTooltip>
</label>
</div>
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label sender-create-wizard__label-row">
<span>@L("FirstReminderDays", "Erste Erinnerung (Tage)")</span>
<MudTooltip Text='@L("SenderWizard_Help_FirstReminderDays", "Anzahl Tage nach Versand bis zur ersten Erinnerung.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_FirstReminderDays", "Hilfe zum Feld Erste Erinnerung")' />
</MudTooltip>
</label>
<InputNumber @bind-Value="_firstReminderDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
</div>
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label sender-create-wizard__label-row">
<span>@L("ReminderIntervalDays", "Erinnerungsintervall (Tage)")</span>
<MudTooltip Text='@L("SenderWizard_Help_ReminderIntervalDays", "Abstand in Tagen zwischen den weiteren Erinnerungen.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_ReminderIntervalDays", "Hilfe zum Feld Erinnerungsintervall")' />
</MudTooltip>
</label>
<InputNumber @bind-Value="_reminderIntervalDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
</div>
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label sender-create-wizard__label-row">
<span>@L("ExpiresWhenDays", "Laeuft ab nach (Tage)")</span>
<MudTooltip Text='@L("SenderWizard_Help_ExpiresWhenDays", "Legt fest, nach wie vielen Tagen der Umschlag automatisch ablaeuft.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_ExpiresWhenDays", "Hilfe zum Feld Laeuft ab nach")' />
</MudTooltip>
</label>
<InputNumber @bind-Value="_expiresWhenDays" class="form-control form-control-sm" />
</div>
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label sender-create-wizard__label-row">
<span>@L("ExpiresWarningWhenDays", "Ablaufwarnung (Tage)")</span>
<MudTooltip Text='@L("SenderWizard_Help_ExpiresWarningWhenDays", "Tage vor Ablauf, an denen eine Warnung versendet wird.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_ExpiresWarningWhenDays", "Hilfe zum Feld Ablaufwarnung")' />
</MudTooltip>
</label>
<InputNumber @bind-Value="_expiresWarningWhenDays" class="form-control form-control-sm" />
</div>
</div>
</div>
</div>
}
else if (_currentStep == 1)
{
<div class="sender-create-wizard__suggestions">
<DxTextBox Text="@_receiverSuggestionFilterText"
TextChanged="OnReceiverSuggestionFilterChanged"
BindValueMode="BindValueMode.OnInput"
NullText='@L("SenderWizard_FilterRecentReceivers_Placeholder", "Empfänger in Vorschlägen filtern...")'
CssClass="w-100" />
@if (_isLoadingRecentSuggestions)
{
<div class="sender-create-wizard__empty-list">@L("Loading", "Lädt...")</div>
}
else if (_recentReceiverSuggestions.Count > 0)
{
<div class="sender-create-wizard__suggestion-list" role="listbox" aria-label="Recent receivers">
@foreach (var suggestion in FilteredRecentSuggestions)
{
<button type="button"
class="sender-create-wizard__suggestion-item"
@onclick="() => ApplyRecentSuggestionAndAddAsync(suggestion)"
disabled="@IsActionLocked">
<span class="sender-create-wizard__suggestion-name">@suggestion.FullName</span>
<span class="sender-create-wizard__suggestion-email">@suggestion.EmailAddress</span>
</button>
}
</div>
}
</div>
<div class="sender-create-wizard__receiver-tagbox-wrap">
<div class="sender-create-wizard__receiver-list" role="table" aria-label="Added receivers">
<div class="sender-create-wizard__receiver-existing-list">
@for (var i = 0; i < _receivers.Count; i++)
{
var rowIndex = i;
var receiver = _receivers[rowIndex];
<div class="sender-create-wizard__receiver-row" role="row">
<div class="sender-create-wizard__receiver-fields-stack sender-create-wizard__field--full">
<div class="sender-create-wizard__field">
<DxTextBox Text="@receiver.FullName"
TextChanged="@(value => OnReceiverListNameChanged(rowIndex, value))"
BindValueMode="BindValueMode.OnInput"
NullText='@L("FullName", "Vor- und Nachname")'
CssClass="w-100"
Enabled="@(!IsActionLocked)" />
</div>
<div class="sender-create-wizard__field">
<DxTextBox Text="@receiver.EmailAddress"
TextChanged="@(value => OnReceiverListEmailChanged(rowIndex, value))"
BindValueMode="BindValueMode.OnInput"
NullText='@L("EmailAddress", "E-Mail-Adresse")'
CssClass="w-100"
Enabled="@(!IsActionLocked)" />
</div>
</div>
<button type="button"
class="sender-create-wizard__receiver-remove-btn"
@onclick="() => RemoveReceiver(receiver)"
disabled="@IsActionLocked"
title='@L("Remove", "Entfernen")'
aria-label='@L("Remove", "Entfernen")'>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z" />
</svg>
</button>
</div>
}
<div class="sender-create-wizard__receiver-row sender-create-wizard__receiver-row--new" role="row">
<div class="sender-create-wizard__receiver-fields-stack sender-create-wizard__field--full">
<div class="sender-create-wizard__field">
<DxTextBox Text="@_receiverName"
TextChanged="OnReceiverNameChanged"
BindValueMode="BindValueMode.OnInput"
NullText="Max Mustermann"
CssClass="w-100"
Enabled="@(!IsActionLocked)" />
</div>
<div class="sender-create-wizard__field">
<DxTextBox Text="@_receiverEmail"
TextChanged="OnReceiverEmailChanged"
BindValueMode="BindValueMode.OnInput"
NullText="name@beispiel.de"
CssClass="w-100"
Enabled="@(!IsActionLocked)" />
</div>
</div>
<button type="button"
class="sender-create-wizard__receiver-add-inline-btn"
@onclick="AddReceiver"
disabled="@IsActionLocked"
title='@L("AddReceiver", "Empfänger hinzufügen")'
aria-label='@L("AddReceiver", "Empfänger hinzufügen")'>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
<path d="M8 1a.5.5 0 0 1 .5.5V7.5h6a.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>
</button>
</div>
</div>
</div>
</div>
}
</div>
</div>
</div>
}
</MudStep>
}
</ChildContent>
</MudStepper>
</MudPaper>
<aside class="sender-create-wizard__wizard-side">
<PdfFileDropZone Disabled="@IsActionLocked"
HasDocument="@(_documentBytes is { Length: > 0 })"
DocumentName="@_documentFileName"
DocumentSizeText="@_documentSizeText"
UploadText='@L("SenderWizard_DocumentUploadSimpleHint", "Dokument hinzufuegen: klicken oder per Drag-and-Drop ablegen")'
ReplaceText='@L("SenderWizard_DocumentUploadSimpleHint", "Dokument hinzufuegen: klicken oder per Drag-and-Drop ablegen")'
UploadedHintText='@L("SenderWizard_DocumentReplaceHint", "Zum Ersetzen klicken oder eine Datei auf diese Fläche ziehen.")'
FileSelected="OnDocumentSelectedAsync" />
</aside>
</div>
@if (!string.IsNullOrWhiteSpace(_validationMessage))
{
<div class="sender-create-wizard__validation">@_validationMessage</div>
}
</div>
</BodyContentTemplate>
<FooterContentTemplate>
<div class="sender-create-wizard__footer">
<DxButton Text="@L("Cancel", "Abbrechen")"
RenderStyle="ButtonRenderStyle.Secondary"
Click="CloseAsync"
Enabled="@(!IsActionLocked)" />
<DxButton Text="@L("Back", "Zurück")"
RenderStyle="ButtonRenderStyle.Secondary"
Click="MoveBack"
Enabled="@(!IsFirstStep && !IsActionLocked)" />
@if (!IsLastStep)
{
<DxButton Text="@L("Next", "Weiter")"
RenderStyle="ButtonRenderStyle.Primary"
Click="MoveNextAsync"
Enabled="@(!IsActionLocked)" />
}
else
{
<DxButton Text="@L("Start", "Editor öffnen")"
RenderStyle="ButtonRenderStyle.Primary"
Click="FinishAsync"
Enabled="@(!IsActionLocked)" />
}
</div>
</FooterContentTemplate>
</DxPopup>
@code {
[Parameter]
public bool Visible { get; set; }
[Parameter]
public EventCallback<bool> VisibleChanged { get; set; }
[Parameter]
public EventCallback<CreateEnvelopeWizardResult> StartEditorRequested { get; set; }
[Parameter]
public IReadOnlyList<EnvelopeTypeDto> EnvelopeTypes { get; set; } = [];
private const int TotalSteps = 4;
private static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute EmailValidator = new();
private bool _isVisible;
private bool _previousVisible;
private int _currentStep;
private readonly HashSet<int> _completedSteps = [];
private string _title = string.Empty;
private int? _selectedEnvelopeTypeId;
private string _message = string.Empty;
private string _language = "de-DE";
private bool _useAccessCode;
private bool _tfaEnabled;
private CertificationType _certificationType = CertificationType.AdvancedElectronicSignature;
private int _finalEmailToCreator = (int)FinalEmailType.No;
private int _finalEmailToReceivers = (int)FinalEmailType.No;
private bool _sendReminderEmails;
private int _firstReminderDays;
private int _reminderIntervalDays;
private int _expiresWhenDays;
private int _expiresWarningWhenDays;
private string _receiverName = string.Empty;
private string _receiverEmail = string.Empty;
private string _receiverSuggestionFilterText = string.Empty;
private readonly List<CreateEnvelopeWizardReceiver> _receivers = [];
private readonly List<RecentReceiverSuggestionDto> _recentReceiverSuggestions = [];
private bool _isLoadingRecentSuggestions;
private bool _recentSuggestionsLoaded;
private const int SuggestionLimit = 20;
private byte[]? _documentBytes;
private string? _documentFileName;
private string _documentSizeText = string.Empty;
private bool _isDocumentUploading;
private bool _isStepActionRunning;
private bool _isFinishing;
private string? _validationMessage;
private bool IsActionLocked => _isDocumentUploading || _isStepActionRunning || _isFinishing;
private bool IsFesSelected => _certificationType == CertificationType.AdvancedElectronicSignature;
private bool IsFirstStep => _currentStep <= 0;
private bool IsLastStep => _currentStep >= TotalSteps - 1;
private IReadOnlyList<RecentReceiverSuggestionDto> FilteredRecentSuggestions =>
BuildFilteredRecentSuggestions();
private string GetStepTitle(int step) => step switch
{
0 => L("SenderWizard_Step_Start", "Start"),
1 => L("SenderWizard_Step_Receivers", "Empfänger"),
2 => L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen"),
3 => L("SenderWizard_Step_Message", "Nachricht und letzter Dokument-Upload"),
_ => L("SenderWizard_Step_Start", "Start")
};
private string GetStepDescription(int step) => step switch
{
0 => L("SenderWizard_Step_Basics_Desc", "Starten Sie mit den Pflichtangaben für den Umschlag."),
1 => L("SenderWizard_Step_Receivers_Desc", "Definieren Sie die Empfänger für den Signaturprozess."),
2 => L("SenderWizard_Step_Settings_Desc", "Konfigurieren Sie Sicherheits-, Erinnerungs- und Ablaufoptionen."),
3 => L("SenderWizard_Step_Message_Desc", "Fuegen Sie optionale Nachricht hinzu und nutzen Sie rechts die letzte Upload-Moeglichkeit im Wizard."),
_ => L("SenderWizard_Step_Basics_Desc", "Starten Sie mit den Pflichtangaben für den Umschlag.")
};
protected override void OnParametersSet()
{
_isVisible = Visible;
if (_selectedEnvelopeTypeId is null && EnvelopeTypes.Count > 0)
ApplyDefaultEnvelopeType();
if (Visible && !_previousVisible)
ResetWizardState();
_previousVisible = Visible;
}
private Task OnTitleChanged(string? value)
{
_title = value ?? string.Empty;
_validationMessage = null;
return Task.CompletedTask;
}
private Task OnMessageChanged(string? value)
{
_message = value ?? string.Empty;
_validationMessage = null;
return Task.CompletedTask;
}
private Task OnReceiverNameChanged(string? value)
{
_receiverName = value ?? string.Empty;
_validationMessage = null;
return Task.CompletedTask;
}
private Task OnReceiverEmailChanged(string? value)
{
_receiverEmail = value ?? string.Empty;
_validationMessage = null;
return Task.CompletedTask;
}
private Task OnReceiverSuggestionFilterChanged(string? value)
{
_receiverSuggestionFilterText = value ?? string.Empty;
_validationMessage = null;
return Task.CompletedTask;
}
private Task OnEnvelopeTypeChanged(ChangeEventArgs args)
{
_validationMessage = null;
var raw = args.Value?.ToString();
if (int.TryParse(raw, out var id) && id > 0)
{
_selectedEnvelopeTypeId = id;
var selectedType = EnvelopeTypes.FirstOrDefault(x => x.Id == id);
if (selectedType is not null)
ApplyEnvelopeTypeDefaults(selectedType);
return Task.CompletedTask;
}
_selectedEnvelopeTypeId = null;
return Task.CompletedTask;
}
private Task AddReceiver()
{
if (IsActionLocked)
return Task.CompletedTask;
var fullName = _receiverName.Trim();
var email = _receiverEmail.Trim();
if (string.IsNullOrWhiteSpace(fullName))
{
_validationMessage = L("SenderEditor_Validation_FullNameRequired", "Bitte geben Sie einen Vor- und Nachnamen ein.");
return Task.CompletedTask;
}
if (string.IsNullOrWhiteSpace(email))
{
_validationMessage = L("SenderEditor_Validation_EmailRequired", "Bitte geben Sie eine E-Mail-Adresse ein.");
return Task.CompletedTask;
}
if (!EmailValidator.IsValid(email))
{
_validationMessage = L("SenderEditor_Validation_EmailInvalid", "Bitte geben Sie eine gültige E-Mail-Adresse ein.");
return Task.CompletedTask;
}
if (_receivers.Any(x => string.Equals(x.EmailAddress, email, StringComparison.OrdinalIgnoreCase)))
{
_validationMessage = L("SenderEditor_Validation_EmailDuplicate", "Diese E-Mail-Adresse wurde bereits hinzugefügt.");
return Task.CompletedTask;
}
_receivers.Add(new CreateEnvelopeWizardReceiver(fullName, email));
_receiverName = string.Empty;
_receiverEmail = string.Empty;
_validationMessage = null;
return Task.CompletedTask;
}
private Task RemoveReceiver(CreateEnvelopeWizardReceiver receiver)
{
if (IsActionLocked)
return Task.CompletedTask;
_receivers.Remove(receiver);
_validationMessage = null;
return Task.CompletedTask;
}
private Task OnReceiverListNameChanged(int index, string? value)
{
if (IsActionLocked || index < 0 || index >= _receivers.Count)
return Task.CompletedTask;
var current = _receivers[index];
_receivers[index] = current with { FullName = value ?? string.Empty };
_validationMessage = null;
return Task.CompletedTask;
}
private Task OnReceiverListEmailChanged(int index, string? value)
{
if (IsActionLocked || index < 0 || index >= _receivers.Count)
return Task.CompletedTask;
var current = _receivers[index];
_receivers[index] = current with { EmailAddress = value ?? string.Empty };
_validationMessage = null;
return Task.CompletedTask;
}
private IReadOnlyList<RecentReceiverSuggestionDto> BuildFilteredRecentSuggestions()
{
if (_recentReceiverSuggestions.Count == 0)
return [];
var handler = new ReceiverSuggestionFilterHandler(_recentReceiverSuggestions);
var filterText = _receiverSuggestionFilterText.Trim();
var filtered = string.IsNullOrWhiteSpace(filterText)
? _recentReceiverSuggestions
: handler.Filter(filterText);
return filtered
.GroupBy(x => x.EmailAddress, StringComparer.OrdinalIgnoreCase)
.Select(g => g.OrderByDescending(x => x.LastUsedAt).First())
.OrderByDescending(x => x.LastUsedAt)
.ThenBy(x => x.EmailAddress, StringComparer.OrdinalIgnoreCase)
.Take(SuggestionLimit)
.ToList();
}
private async Task OnDocumentSelectedAsync(InputFileChangeEventArgs args)
{
if (IsActionLocked)
return;
_validationMessage = null;
var file = args.File;
if (file is null)
return;
if (!file.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
{
_validationMessage = L("SenderEditor_SelectPdf", "Bitte wählen Sie eine PDF-Datei aus.");
return;
}
try
{
_isDocumentUploading = true;
const long maxBytes = 50 * 1024 * 1024;
await using var stream = file.OpenReadStream(maxBytes);
using var ms = new System.IO.MemoryStream();
await stream.CopyToAsync(ms);
_documentBytes = ms.ToArray();
_documentFileName = file.Name;
_documentSizeText = FormatSize(_documentBytes.Length);
}
finally
{
_isDocumentUploading = false;
}
}
private Task MoveBack()
{
if (IsActionLocked)
return Task.CompletedTask;
_validationMessage = null;
if (!IsFirstStep)
_currentStep--;
return Task.CompletedTask;
}
private async Task MoveNextAsync()
{
if (IsActionLocked)
return;
_isStepActionRunning = true;
try
{
if (!ValidateCurrentStep())
return;
_validationMessage = null;
if (!IsLastStep)
{
_completedSteps.Add(_currentStep);
_currentStep++;
if (_currentStep == 1)
await EnsureRecentSuggestionsLoadedAsync();
}
}
finally
{
_isStepActionRunning = false;
}
await Task.CompletedTask;
}
private async Task CloseAsync()
{
if (IsActionLocked)
return;
await CloseCoreAsync();
}
private async Task CloseCoreAsync()
{
ResetWizardState();
await OnPopupVisibleChangedAsync(false);
}
private async Task FinishAsync()
{
if (IsActionLocked)
return;
_isFinishing = true;
try
{
if (!ValidateCurrentStep())
return;
var result = new CreateEnvelopeWizardResult(
Title: _title.Trim(),
Message: _message.Trim(),
EnvelopeTypeId: _selectedEnvelopeTypeId,
Language: string.IsNullOrWhiteSpace(_language) ? "de-DE" : _language,
UseAccessCode: _useAccessCode,
TFAEnabled: _tfaEnabled,
CertificationType: _certificationType,
FinalEmailToCreator: _finalEmailToCreator,
FinalEmailToReceivers: _finalEmailToReceivers,
SendReminderEmails: _sendReminderEmails,
FirstReminderDays: _firstReminderDays,
ReminderIntervalDays: _reminderIntervalDays,
ExpiresWhenDays: _expiresWhenDays,
ExpiresWarningWhenDays: _expiresWarningWhenDays,
Receivers: _receivers.ToList(),
DocumentBytes: _documentBytes,
DocumentFileName: _documentFileName);
await CloseCoreAsync();
if (StartEditorRequested.HasDelegate)
await StartEditorRequested.InvokeAsync(result);
}
finally
{
_isFinishing = false;
}
}
private async Task OnWizardKeyDownAsync(KeyboardEventArgs args)
{
if (!_isVisible)
return;
if (string.Equals(args.Key, "Escape", StringComparison.OrdinalIgnoreCase))
{
await CloseAsync();
return;
}
if (!string.Equals(args.Key, "Enter", StringComparison.OrdinalIgnoreCase))
return;
if (_currentStep == 3)
return;
if (_currentStep == 1 && (!string.IsNullOrWhiteSpace(_receiverName) || !string.IsNullOrWhiteSpace(_receiverEmail)))
{
await AddReceiver();
return;
}
if (IsLastStep)
{
await FinishAsync();
return;
}
await MoveNextAsync();
}
private async Task OnPopupVisibleChangedAsync(bool value)
{
_isVisible = value;
if (!value)
ResetWizardState();
if (VisibleChanged.HasDelegate)
await VisibleChanged.InvokeAsync(value);
}
private async Task EnsureRecentSuggestionsLoadedAsync()
{
if (_recentSuggestionsLoaded || _isLoadingRecentSuggestions)
return;
_isLoadingRecentSuggestions = true;
try
{
var suggestions = await ReceiverService.GetRecentReceiverSuggestionsAsync(SuggestionLimit);
_recentReceiverSuggestions.Clear();
_recentReceiverSuggestions.AddRange(suggestions);
_recentSuggestionsLoaded = true;
}
catch
{
_recentReceiverSuggestions.Clear();
_recentSuggestionsLoaded = true;
}
finally
{
_isLoadingRecentSuggestions = false;
await InvokeAsync(StateHasChanged);
}
}
private async Task ApplyRecentSuggestionAndAddAsync(RecentReceiverSuggestionDto suggestion)
{
if (IsActionLocked)
return;
_receiverName = suggestion.FullName;
_receiverEmail = suggestion.EmailAddress;
await AddReceiver();
}
private bool ValidateCurrentStep()
{
if (_currentStep == 0)
{
if (_selectedEnvelopeTypeId is null && EnvelopeTypes.Count > 0)
_selectedEnvelopeTypeId = GetDefaultEnvelopeTypeId();
if (string.IsNullOrWhiteSpace(_title))
{
_validationMessage = L("SenderEditor_Validation_TitleRequired", "Bitte geben Sie einen Titel ein.");
return false;
}
if (!_selectedEnvelopeTypeId.HasValue || _selectedEnvelopeTypeId.Value <= 0)
{
_validationMessage = L("SenderWizard_Validation_EnvelopeTypeRequired", "Bitte wählen Sie einen Umschlagtyp aus.");
return false;
}
return true;
}
if (_currentStep == 2)
{
if (!ValidateSettings(out var settingsError))
{
_validationMessage = settingsError;
return false;
}
return true;
}
if (_currentStep == 1)
{
if (_receivers.Count == 0)
{
_validationMessage = L("SenderEditor_Validation_ReceiverRequired", "Bitte fügen Sie mindestens einen Empfänger hinzu.");
return false;
}
for (var i = 0; i < _receivers.Count; i++)
{
var receiver = _receivers[i];
var fullName = receiver.FullName.Trim();
var email = receiver.EmailAddress.Trim();
if (string.IsNullOrWhiteSpace(fullName))
{
_validationMessage = L("SenderEditor_Validation_FullNameRequired", "Bitte geben Sie einen Vor- und Nachnamen ein.");
return false;
}
if (string.IsNullOrWhiteSpace(email))
{
_validationMessage = L("SenderEditor_Validation_EmailRequired", "Bitte geben Sie eine E-Mail-Adresse ein.");
return false;
}
if (!EmailValidator.IsValid(email))
{
_validationMessage = L("SenderEditor_Validation_EmailInvalid", "Bitte geben Sie eine gültige E-Mail-Adresse ein.");
return false;
}
}
var duplicateEmailExists = _receivers
.GroupBy(x => x.EmailAddress.Trim(), StringComparer.OrdinalIgnoreCase)
.Any(group => group.Count() > 1);
if (duplicateEmailExists)
{
_validationMessage = L("SenderEditor_Validation_EmailDuplicate", "Diese E-Mail-Adresse wurde bereits hinzugefügt.");
return false;
}
return true;
}
return true;
}
private static string FormatSize(long bytes)
{
const double kb = 1024;
const double mb = kb * 1024;
if (bytes >= mb)
return $"{bytes / mb:0.##} MB";
if (bytes >= kb)
return $"{bytes / kb:0.##} KB";
return $"{bytes} B";
}
private void OnTfaChanged(ChangeEventArgs args)
{
_validationMessage = null;
if (IsFesSelected)
{
_tfaEnabled = true;
_useAccessCode = true;
return;
}
_tfaEnabled = ParseCheckboxValue(args);
if (_tfaEnabled)
_useAccessCode = true;
}
private Task OnCertificationTypeChanged()
{
_validationMessage = null;
NormalizeSettings();
return Task.CompletedTask;
}
private void OnUseAccessCodeChanged(ChangeEventArgs args)
{
_validationMessage = null;
if (_tfaEnabled)
{
_useAccessCode = true;
return;
}
_useAccessCode = ParseCheckboxValue(args);
}
private void OnSendReminderEmailsChanged(ChangeEventArgs args)
{
_validationMessage = null;
_sendReminderEmails = ParseCheckboxValue(args);
}
private static bool ParseCheckboxValue(ChangeEventArgs args)
{
if (args.Value is bool boolValue)
return boolValue;
return bool.TryParse(args.Value?.ToString(), out var parsed) && parsed;
}
private void ApplyEnvelopeTypeDefaults(EnvelopeTypeDto envelopeType)
{
if (!string.IsNullOrWhiteSpace(envelopeType.Language))
_language = envelopeType.Language;
if (envelopeType.UseAccessCode.HasValue)
_useAccessCode = envelopeType.UseAccessCode.Value;
if (_tfaEnabled)
_useAccessCode = true;
if (envelopeType.CertificationType.HasValue)
_certificationType = envelopeType.CertificationType.Value;
if (envelopeType.FinalEmailToCreator.HasValue)
_finalEmailToCreator = envelopeType.FinalEmailToCreator.Value;
if (envelopeType.FinalEmailToReceivers.HasValue)
_finalEmailToReceivers = envelopeType.FinalEmailToReceivers.Value;
if (envelopeType.SendReminderEmails.HasValue)
_sendReminderEmails = envelopeType.SendReminderEmails.Value;
if (envelopeType.FirstReminderDays.HasValue)
_firstReminderDays = envelopeType.FirstReminderDays.Value;
if (envelopeType.ReminderIntervalDays.HasValue)
_reminderIntervalDays = envelopeType.ReminderIntervalDays.Value;
if (envelopeType.ExpiresDays.HasValue)
_expiresWhenDays = envelopeType.ExpiresDays.Value;
if (envelopeType.ExpiresWarningDays.HasValue)
_expiresWarningWhenDays = envelopeType.ExpiresWarningDays.Value;
NormalizeSettings();
}
private void NormalizeSettings()
{
_language = string.IsNullOrWhiteSpace(_language) ? "de-DE" : _language.Trim();
if (IsFesSelected)
_tfaEnabled = true;
if (_tfaEnabled)
_useAccessCode = true;
_firstReminderDays = Math.Max(0, _firstReminderDays);
_reminderIntervalDays = Math.Max(0, _reminderIntervalDays);
_expiresWhenDays = Math.Max(0, _expiresWhenDays);
_expiresWarningWhenDays = Math.Max(0, _expiresWarningWhenDays);
if (_expiresWhenDays > 0 && _expiresWarningWhenDays > _expiresWhenDays)
_expiresWarningWhenDays = _expiresWhenDays;
}
private bool ValidateSettings(out string? message)
{
NormalizeSettings();
if (_sendReminderEmails && _reminderIntervalDays <= 0)
{
message = L("SenderEditor_Validation_ReminderInterval", "Bitte geben Sie ein Erinnerungsintervall groesser als 0 Tage ein.");
return false;
}
if (_sendReminderEmails && _firstReminderDays < 0)
{
message = L("SenderEditor_Validation_FirstReminder", "Bitte geben Sie gueltige Tage fuer die erste Erinnerung ein.");
return false;
}
if (_expiresWhenDays > 0 && _expiresWarningWhenDays > _expiresWhenDays)
{
message = L("SenderEditor_Validation_ExpiryWarning", "Die Ablaufwarnung darf nicht nach dem Ablaufdatum liegen.");
return false;
}
message = null;
return true;
}
private void ApplyDefaultEnvelopeType()
{
_selectedEnvelopeTypeId = GetDefaultEnvelopeTypeId();
if (!_selectedEnvelopeTypeId.HasValue)
return;
var selectedType = EnvelopeTypes.FirstOrDefault(x => x.Id == _selectedEnvelopeTypeId.Value);
if (selectedType is not null)
ApplyEnvelopeTypeDefaults(selectedType);
}
private void ResetWizardState()
{
_currentStep = 0;
_completedSteps.Clear();
_validationMessage = null;
_title = string.Empty;
_selectedEnvelopeTypeId = null;
_message = string.Empty;
_language = "de-DE";
_useAccessCode = false;
_tfaEnabled = false;
_certificationType = CertificationType.AdvancedElectronicSignature;
_finalEmailToCreator = (int)FinalEmailType.No;
_finalEmailToReceivers = (int)FinalEmailType.No;
_sendReminderEmails = false;
_firstReminderDays = 0;
_reminderIntervalDays = 0;
_expiresWhenDays = 0;
_expiresWarningWhenDays = 0;
_receiverName = string.Empty;
_receiverEmail = string.Empty;
_receiverSuggestionFilterText = string.Empty;
_receivers.Clear();
_recentReceiverSuggestions.Clear();
_isLoadingRecentSuggestions = false;
_recentSuggestionsLoaded = false;
_documentBytes = null;
_documentFileName = null;
_documentSizeText = string.Empty;
ApplyDefaultEnvelopeType();
NormalizeSettings();
}
private int? GetDefaultEnvelopeTypeId()
{
if (EnvelopeTypes.Count == 0)
return null;
var handwrittenType = EnvelopeTypes.FirstOrDefault(x =>
!string.IsNullOrWhiteSpace(x.Title)
&& x.Title.Contains("Handschriftliche Unterschrift", StringComparison.OrdinalIgnoreCase));
return handwrittenType?.Id ?? EnvelopeTypes[0].Id;
}
private string L(string key, string fallback)
{
var modelValue = ModelLocalizer[key];
if (!modelValue.ResourceNotFound)
return modelValue.Value;
var uiValue = UiLocalizer[key];
return uiValue.ResourceNotFound ? fallback : uiValue.Value;
}
}