Improve document upload UX and refactor wizard layout

Enhanced the `CreateEnvelopeWizardPopup` component:
- Added a new grid layout (`sender-create-wizard__body-layout`) to separate main content and side panel.
- Introduced a collapsible side panel for document upload and preview.
- Replaced old upload logic with `PdfFileDropZone` and added support for displaying document metadata (name, size) and replacement hints.
- Added asynchronous document preview rendering with `EnsureDocumentPreviewPreparedAsync`.

Localization updates:
- Added new localized strings for document upload hints in `de-DE`, `en-US`, and `fr-FR`.

CSS updates:
- Styled the new layout and side panel with transitions.
- Updated `PdfFileDropZone` styles for different states (e.g., document uploaded).

Refactored code for better maintainability and improved user feedback during document upload and preview preparation.
This commit is contained in:
2026-11-10 19:29:33 +01:00
parent c6fe7722b4
commit b65098fafb
6 changed files with 502 additions and 348 deletions

View File

@@ -28,387 +28,404 @@
<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;
<div class="sender-create-wizard__body-layout @(_currentStep == 4 ? "sender-create-wizard__body-layout--document" : string.Empty)">
<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>
<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">
<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("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")' />
<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>
<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>
<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("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")' />
<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 @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 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 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" />
<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>
}
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__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")' />
<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("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")' />
<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>
<InputNumber @bind-Value="_firstReminderDays" class="form-control form-control-sm" disabled="@(!_sendReminderEmails)" />
<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">
<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>
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__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__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__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 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" />
<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>
<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__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">
<DxTextBox Text="@_receiverEmail"
TextChanged="OnReceiverEmailChanged"
BindValueMode="BindValueMode.OnInput"
NullText="name@beispiel.de"
CssClass="w-100"
Enabled="@(!IsActionLocked)" />
<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>
<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>
}
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)
}
else if (_currentStep == 1)
{
<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 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>
}
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 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>
}
</div>
</div>
}
</MudStep>
}
</ChildContent>
</MudStepper>
</MudPaper>
</MudStep>
}
</ChildContent>
</MudStepper>
</MudPaper>
<aside class="sender-create-wizard__wizard-side @(_currentStep == 4 ? "sender-create-wizard__wizard-side--hidden" : string.Empty)" aria-hidden="@(_currentStep == 4)">
<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))
{
@@ -507,6 +524,7 @@
private int _documentPreviewPageCount;
private List<PdfPageImageDto> _documentPreviewPages = [];
private bool _isDocumentUploading;
private bool _isDocumentPreviewPrepared;
private bool _isStepActionRunning;
private bool _isFinishing;
@@ -736,22 +754,12 @@
_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 = 0;
_isDocumentPreviewPrepared = false;
_documentPreviewPageCount = _documentPreviewPages.Count > 0
? _documentPreviewPages.Count
: EstimatePdfPageCount(_documentBytes) ?? 1;
if (_currentStep == 4)
await EnsureDocumentPreviewPreparedAsync();
}
finally
{
@@ -792,6 +800,9 @@
if (_currentStep == 1)
await EnsureRecentSuggestionsLoadedAsync();
if (_currentStep == 4)
await EnsureDocumentPreviewPreparedAsync();
}
}
finally
@@ -948,6 +959,33 @@
await AddReceiver();
}
private async Task EnsureDocumentPreviewPreparedAsync()
{
if (_documentBytes is null || _documentBytes.Length == 0 || _isDocumentPreviewPrepared)
return;
try
{
_isDocumentUploading = true;
_documentPreviewPages = (await PdfRenderService.RenderPagesAsync(_documentBytes))
.OrderBy(x => x.PageNumber)
.ToList();
}
catch
{
_documentPreviewPages = [];
}
finally
{
_documentPreviewPageCount = _documentPreviewPages.Count > 0
? _documentPreviewPages.Count
: EstimatePdfPageCount(_documentBytes) ?? 1;
_isDocumentPreviewPrepared = true;
_isDocumentUploading = false;
}
}
private bool ValidateCurrentStep()
{
if (_currentStep == 0)
@@ -1260,6 +1298,7 @@
_documentSizeText = string.Empty;
_documentPreviewPageCount = 0;
_documentPreviewPages = [];
_isDocumentPreviewPrepared = false;
ApplyDefaultEnvelopeType();
}

View File

@@ -2,10 +2,24 @@
<div class="sender-pdf-dropzone @DropZoneClass">
<div class="sender-pdf-dropzone__content">
<div class="sender-pdf-dropzone__title">@CurrentActionText</div>
@if (!string.IsNullOrWhiteSpace(DragDropHintText))
@if (HasDocument)
{
<div class="sender-pdf-dropzone__hint">@DragDropHintText</div>
@if (!string.IsNullOrWhiteSpace(DocumentName))
{
<div class="sender-pdf-dropzone__doc-name">@DocumentName</div>
}
@if (!string.IsNullOrWhiteSpace(DocumentSizeText))
{
<div class="sender-pdf-dropzone__doc-size">@DocumentSizeText</div>
}
@if (!string.IsNullOrWhiteSpace(UploadedHintText))
{
<div class="sender-pdf-dropzone__hint">@UploadedHintText</div>
}
}
else if (!string.IsNullOrWhiteSpace(CurrentActionText))
{
<div class="sender-pdf-dropzone__hint">@CurrentActionText</div>
}
</div>
@@ -38,6 +52,15 @@
[Parameter]
public string DragDropHintText { get; set; } = "Datei hierhin ziehen oder klicken";
[Parameter]
public string? DocumentName { get; set; }
[Parameter]
public string? DocumentSizeText { get; set; }
[Parameter]
public string UploadedHintText { get; set; } = "Zum Ersetzen klicken oder eine Datei hierher ziehen.";
[Parameter]
public EventCallback<InputFileChangeEventArgs> FileSelected { get; set; }
@@ -53,6 +76,9 @@
if (Disabled)
return "sender-pdf-dropzone--disabled";
if (HasDocument)
return "sender-pdf-dropzone--has-document";
return _isDragActive ? "sender-pdf-dropzone--drag-active" : string.Empty;
}
}