Files
EnvelopeGenerator/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/Sender/CreateEnvelopeWizardPopup.razor
TekH 638b734648 Enhance UI with tooltips and MudBlazor integration
Added contextual tooltips (`MudTooltip` and `MudIconButton`) to form fields in `CreateEnvelopeWizardPopup.razor` for improved user guidance. Updated label layouts using the new `sender-create-wizard__label-row` class for better alignment with tooltips. Integrated MudBlazor components (`MudThemeProvider`, `MudPopoverProvider`, `MudDialogProvider`, `MudSnackbarProvider`) in `SenderPage.razor` to enable enhanced UI features.

Updated `_Imports.razor` to include `MudBlazor` for global availability. Enhanced accessibility by adding `aria-label` attributes to help icons. Improved user experience with detailed explanations for fields like "Title," "Envelope Type," and others. Updated checkbox labels and numeric input fields with tooltips for additional context.

Added new CSS styles in `sender-page.css` to ensure proper alignment and sizing of tooltips and icons. These changes improve the overall usability, accessibility, and design consistency of the application.
2026-11-09 11:22:53 +01:00

1254 lines
65 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
@inject PdfRenderService PdfRenderService
<DxPopup Visible="@_isVisible"
VisibleChanged="OnPopupVisibleChangedAsync"
HeaderText="@L("SenderWizard_Header", "Neuen Umschlag erstellen")"
Width="860px"
MaxWidth="96vw"
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">
<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="@(stepIndex == 3)">
<div class="sender-create-wizard__stepper-description">@GetStepDescription(stepIndex)</div>
@if (stepIndex == _currentStep)
{
<div class="sender-create-wizard__step-content">
@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>
}
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" 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", "Finale E-Mail 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 Finale E-Mail 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", "Finale E-Mail 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 Finale E-Mail 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" />
<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">
<div class="sender-create-wizard__suggestions-title">@L("SenderWizard_RecentReceivers", "Zuletzt verwendete Empfänger")</div>
@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="() => ApplyRecentSuggestion(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__form-grid">
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label sender-create-wizard__label-row">
<span>@L("FullName", "Vor- und Nachname") <span class="sender-create-wizard__required">*</span></span>
<MudTooltip Text='@L("SenderWizard_Help_FullName", "Name des Empfaengers wie er in E-Mails und Signaturansicht angezeigt wird.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_FullName", "Hilfe zum Feld Vor- und Nachname")' />
</MudTooltip>
</label>
<DxTextBox Text="@_receiverName"
TextChanged="OnReceiverNameChanged"
BindValueMode="BindValueMode.OnInput"
NullText="Max Mustermann"
CssClass="w-100" />
</div>
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label sender-create-wizard__label-row">
<span>@L("EmailAddress", "E-Mail-Adresse") <span class="sender-create-wizard__required">*</span></span>
<MudTooltip Text='@L("SenderWizard_Help_EmailAddress", "An diese Adresse wird die Signaturanfrage gesendet.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_EmailAddress", "Hilfe zum Feld E-Mail-Adresse")' />
</MudTooltip>
</label>
<DxTextBox Text="@_receiverEmail"
TextChanged="OnReceiverEmailChanged"
BindValueMode="BindValueMode.OnInput"
NullText="name@beispiel.de"
CssClass="w-100" />
</div>
</div>
<div class="sender-create-wizard__receiver-actions">
<button type="button"
class="sender-create-wizard__add-receiver-btn"
@onclick="AddReceiver"
disabled="@IsActionLocked">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
<path d="M8 4a.5.5 0 0 1 .5.5V7.5H11.5a.5.5 0 0 1 0 1H8.5v3a.5.5 0 0 1-1 0v-3H4.5a.5.5 0 0 1 0-1H7.5v-3A.5.5 0 0 1 8 4z" />
</svg>
<span>@L("AddReceiver", "Empfänger hinzufügen")</span>
</button>
</div>
@if (_receivers.Count == 0)
{
<div class="sender-create-wizard__empty-list">
@L("SenderEditor_NoReceiversAdded", "Noch keine Empfänger hinzugefügt.")
</div>
}
else
{
<div class="sender-create-wizard__receiver-tagbox-wrap">
<label class="sender-create-wizard__label sender-create-wizard__label-row" for="wizardReceiversTagBox">
<span>@L("Receiver", "Empfänger")</span>
<MudTooltip Text='@L("SenderWizard_Help_ReceiverList", "Hier sehen und verwalten Sie alle aktuell hinzugefuegten Empfaenger.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_ReceiverList", "Hilfe zur Empfaengerliste")' />
</MudTooltip>
</label>
<DxTagBox Data="@_receivers"
TData="CreateEnvelopeWizardReceiver"
TValue="CreateEnvelopeWizardReceiver"
AllowCustomTags="true"
@bind-Tags="@_receiverTags"
@bind-Tags:after="OnReceiverTagsChanged"
HideSelectedItems="false"
TextFieldName="@nameof(CreateEnvelopeWizardReceiver.EmailAddress)"
ListRenderMode="ListRenderMode.Virtual"
SearchMode="ListSearchMode.AutoSearch"
SearchFilterCondition="ListSearchFilterCondition.Contains"
SearchTextParseMode="ListSearchTextParseMode.GroupWordsByAnd"
CssClass="sender-create-wizard__receiver-tagbox"
InputId="wizardReceiversTagBox"
Enabled="@(!IsActionLocked)">
<TagDisplayTemplate Context="tagContext">
<div class="sender-create-wizard__receiver-tag">
@if (tagContext.DataItem is CreateEnvelopeWizardReceiver receiver)
{
<span class="sender-create-wizard__receiver-tag-name">@receiver.FullName</span>
<span class="sender-create-wizard__receiver-tag-email">@receiver.EmailAddress</span>
}
else
{
<span class="sender-create-wizard__receiver-tag-email">@tagContext.DisplayText</span>
}
<button type="button"
class="sender-create-wizard__receiver-tag-remove"
@onclick="tagContext.RemoveTagAction"
aria-label="Remove receiver"
disabled="@IsActionLocked">x</button>
</div>
</TagDisplayTemplate>
<EmptyDataAreaTemplate Context="emptyContext">
<div class="sender-create-wizard__tagbox-empty"></div>
</EmptyDataAreaTemplate>
</DxTagBox>
</div>
}
}
else
{
<div class="sender-create-wizard__upload-stage">
<label class="sender-create-wizard__upload-btn @(IsActionLocked ? "sender-create-wizard__upload-btn--disabled" : string.Empty)">
@if (_documentBytes is null)
{
<span>@L("UploadPdf", "PDF hochladen")</span>
}
else
{
<span>@L("SenderWizard_ReplacePdf", "PDF ersetzen")</span>
}
<InputFile OnChange="OnDocumentSelectedAsync"
accept=".pdf"
disabled="@IsActionLocked"
class="sender-create-wizard__hidden-input" />
</label>
@if (_documentBytes is not null)
{
<div class="sender-create-wizard__document-preview-card">
<div class="sender-create-wizard__document-meta">
<div class="sender-create-wizard__document-name">@_documentFileName</div>
<div class="sender-create-wizard__document-size">@_documentSizeText</div>
</div>
<div class="sender-create-wizard__document-preview-wrap">
<SenderPdfImageEditor IsPlacementMode="false"
PageCount="@_documentPreviewPageCount"
Pages="@_documentPreviewPages"
PdfDataUrl="@DocumentPreviewDataUrl"
PdfFileName="@_documentFileName"
Fields="@_documentPreviewFieldOverlays"
EditorClick="OnDocumentPreviewEditorClickAsync"
RemoveField="OnDocumentPreviewRemoveFieldAsync"
MoveField="OnDocumentPreviewMoveFieldAsync" />
</div>
</div>
}
else
{
<PdfFileDropZone Disabled="@IsActionLocked"
HasDocument="false"
UploadText='@L("SenderWizard_DocumentRequiredHint", "Bitte laden Sie ein PDF-Dokument hoch, um fortzufahren.")'
DragDropHintText='@L("SenderWizard_DragDropPdfHint", "Oder ziehen Sie die PDF-Datei hierher.")'
FileSelected="OnDocumentSelectedAsync" />
}
</div>
}
</div>
}
</MudStep>
}
</ChildContent>
</MudStepper>
</MudPaper>
@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)" />
@if (CanSkipCurrentStep)
{
<DxButton Text="@L("Skip", "Überspringen")"
RenderStyle="ButtonRenderStyle.Secondary"
Click="SkipOptionalStep"
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 = 5;
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 readonly List<CreateEnvelopeWizardReceiver> _receivers = [];
private IEnumerable<string> _receiverTags = [];
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 int _documentPreviewPageCount;
private List<PdfPageImageDto> _documentPreviewPages = [];
private bool _isDocumentUploading;
private bool _isStepActionRunning;
private bool _isFinishing;
private readonly IReadOnlyList<SenderPdfFieldOverlay> _documentPreviewFieldOverlays = [];
private string? _validationMessage;
private bool IsActionLocked => _isDocumentUploading || _isStepActionRunning || _isFinishing;
private bool IsFirstStep => _currentStep <= 0;
private bool IsLastStep => _currentStep >= TotalSteps - 1;
private bool CanSkipCurrentStep => _currentStep == 3;
private IReadOnlyList<RecentReceiverSuggestionDto> FilteredRecentSuggestions =>
BuildFilteredRecentSuggestions();
private string? DocumentPreviewDataUrl => _documentBytes is { Length: > 0 }
? $"data:application/pdf;base64,{Convert.ToBase64String(_documentBytes)}"
: null;
private string CurrentStepTitle => GetStepTitle(_currentStep);
private string CurrentStepDescription => GetStepDescription(_currentStep);
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 Optionen"),
4 => L("SenderWizard_Step_Document", "Dokument"),
_ => 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", "Fügen Sie optionale Nachricht und Einstellungen hinzu."),
4 => L("SenderWizard_Step_Document_Desc", "Laden Sie das Dokument hoch und platzieren Sie Signaturfelder."),
_ => 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 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));
_receiverTags = _receivers.Select(x => x.EmailAddress).ToList();
_receiverName = string.Empty;
_receiverEmail = string.Empty;
_validationMessage = null;
return Task.CompletedTask;
}
private Task RemoveReceiver(CreateEnvelopeWizardReceiver receiver)
{
if (IsActionLocked)
return Task.CompletedTask;
_receivers.Remove(receiver);
_receiverTags = _receivers.Select(x => x.EmailAddress).ToList();
_validationMessage = null;
return Task.CompletedTask;
}
private Task OnReceiverTagsChanged()
{
var selectedEmails = _receiverTags.ToHashSet(StringComparer.OrdinalIgnoreCase);
_receivers.RemoveAll(x => !selectedEmails.Contains(x.EmailAddress));
_receiverTags = _receivers.Select(x => x.EmailAddress).ToList();
_validationMessage = null;
return Task.CompletedTask;
}
private IReadOnlyList<RecentReceiverSuggestionDto> BuildFilteredRecentSuggestions()
{
if (_recentReceiverSuggestions.Count == 0)
return [];
var handler = new ReceiverSuggestionFilterHandler(_recentReceiverSuggestions);
var filtered = handler.Filter(_receiverName, _receiverEmail);
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);
_documentPreviewPages = [];
try
{
_documentPreviewPages = (await PdfRenderService.RenderPagesAsync(_documentBytes))
.OrderBy(x => x.PageNumber)
.ToList();
}
catch
{
_documentPreviewPages = [];
}
_documentPreviewPageCount = _documentPreviewPages.Count > 0
? _documentPreviewPages.Count
: EstimatePdfPageCount(_documentBytes) ?? 1;
}
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 SkipOptionalStep()
{
if (IsActionLocked)
return;
if (!CanSkipCurrentStep)
return;
_message = string.Empty;
_validationMessage = null;
await MoveNextAsync();
}
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 void ApplyRecentSuggestion(RecentReceiverSuggestionDto suggestion)
{
if (IsActionLocked)
return;
_receiverName = suggestion.FullName;
_receiverEmail = suggestion.EmailAddress;
_validationMessage = null;
}
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;
}
return true;
}
if (_currentStep == 4)
{
if (_documentBytes is null || _documentBytes.Length == 0)
{
_validationMessage = L("SenderEditor_Validation_UploadPdf", "Bitte laden Sie zuerst ein PDF-Dokument hoch.");
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 static int? EstimatePdfPageCount(byte[]? documentBytes)
{
if (documentBytes is null || documentBytes.Length == 0)
return null;
try
{
var text = System.Text.Encoding.ASCII.GetString(documentBytes);
const string marker = "/Type /Page";
var count = 0;
var index = 0;
while (true)
{
index = text.IndexOf(marker, index, StringComparison.Ordinal);
if (index < 0)
break;
count++;
index += marker.Length;
}
return count > 0 ? count : null;
}
catch
{
return null;
}
}
private Task OnDocumentPreviewEditorClickAsync(MouseEventArgs _)
{
return Task.CompletedTask;
}
private Task OnDocumentPreviewRemoveFieldAsync(int _)
{
return Task.CompletedTask;
}
private Task OnDocumentPreviewMoveFieldAsync(SenderPdfFieldMoveRequest _)
{
return Task.CompletedTask;
}
private void OnTfaChanged(ChangeEventArgs args)
{
_validationMessage = null;
_tfaEnabled = ParseCheckboxValue(args);
if (_tfaEnabled)
_useAccessCode = true;
}
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 (_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;
_receivers.Clear();
_receiverTags = [];
_recentReceiverSuggestions.Clear();
_isLoadingRecentSuggestions = false;
_recentSuggestionsLoaded = false;
_documentBytes = null;
_documentFileName = null;
_documentSizeText = string.Empty;
_documentPreviewPageCount = 0;
_documentPreviewPages = [];
ApplyDefaultEnvelopeType();
}
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;
}
}