Files
EnvelopeGenerator/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Components/Sender/CreateEnvelopeWizardPopup.razor
TekH 47f48ba12a Add PdfFileDropZone for interactive PDF uploads
Replaced static upload message in `CreateEnvelopeWizardPopup.razor` with a new `PdfFileDropZone` component to enhance user experience. The component supports drag-and-drop functionality, file selection, and dynamic states (e.g., disabled, drag-active).

Introduced `PdfFileDropZone.razor` with customizable parameters such as `Disabled`, `HasDocument`, `UploadText`, and `DragDropHintText`. Added event handling for drag-and-drop interactions and file selection.

Updated `sender-page.css` to include styles for the new component, ensuring seamless integration with the existing layout. Adjusted styles for hover effects, drag-active state, and disabled state.

These changes improve usability and make PDF uploads more intuitive.
2026-10-06 10:31:43 +02:00

820 lines
30 KiB
Plaintext

@using EnvelopeGenerator.Application.Common.Dto
@using EnvelopeGenerator.Application.Common.Dto.Receiver
@using EnvelopeGenerator.Application.Resources
@using EnvelopeGenerator.Domain.Resources
@using EnvelopeGenerator.Server.Client.Components.Sender.ReceiverSuggestionFilter
@using EnvelopeGenerator.Server.Client.Services
@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="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">
<div class="sender-create-wizard__progress">
<div class="sender-create-wizard__progress-track"></div>
@for (var step = 0; step < TotalSteps; step++)
{
var isActive = step == _currentStep;
var isDone = step < _currentStep;
<div class="sender-create-wizard__step @(isActive ? "sender-create-wizard__step--active" : string.Empty) @(isDone ? "sender-create-wizard__step--done" : string.Empty)">
<span class="sender-create-wizard__step-index">@(step + 1)</span>
</div>
}
</div>
<div class="sender-create-wizard__meta">
<div class="sender-create-wizard__title">@CurrentStepTitle</div>
<div class="sender-create-wizard__description">@CurrentStepDescription</div>
</div>
@if (!string.IsNullOrWhiteSpace(_validationMessage))
{
<div class="sender-create-wizard__validation">@_validationMessage</div>
}
@if (_currentStep == 0)
{
<div class="sender-create-wizard__form-grid">
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label">
@L("Title", "Titel") <span class="sender-create-wizard__required">*</span>
</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">
@L("EnvelopeType", "Umschlagtyp") <span class="sender-create-wizard__required">*</span>
</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 == 1)
{
<div class="sender-create-wizard__form-grid">
<div class="sender-create-wizard__field sender-create-wizard__field--full">
<label class="sender-create-wizard__label">
@L("Message", "Nachricht") <span class="sender-create-wizard__optional">(@L("Optional", "optional"))</span>
</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">
@L("Language", "Sprache")
</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__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">
@L("FullName", "Vor- und Nachname") <span class="sender-create-wizard__required">*</span>
</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">
@L("EmailAddress", "E-Mail-Adresse") <span class="sender-create-wizard__required">*</span>
</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" for="wizardReceiversTagBox">
@L("Receiver", "Empfänger")
</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-meta">
<div class="sender-create-wizard__document-name">@_documentFileName</div>
<div class="sender-create-wizard__document-size">@_documentSizeText</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>
</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 = 4;
private static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute EmailValidator = new();
private bool _isVisible;
private bool _previousVisible;
private int _currentStep;
private string _title = string.Empty;
private int? _selectedEnvelopeTypeId;
private string _message = string.Empty;
private string _language = "de-DE";
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 bool _isDocumentUploading;
private bool _isStepActionRunning;
private bool _isFinishing;
private string? _validationMessage;
private bool IsActionLocked => _isDocumentUploading || _isStepActionRunning || _isFinishing;
private bool IsFirstStep => _currentStep <= 0;
private bool IsLastStep => _currentStep >= TotalSteps - 1;
private bool CanSkipCurrentStep => _currentStep == 1;
private IReadOnlyList<RecentReceiverSuggestionDto> FilteredRecentSuggestions =>
BuildFilteredRecentSuggestions();
private string CurrentStepTitle => _currentStep switch
{
0 => L("SenderWizard_Step_Basics", "Grunddaten"),
1 => L("SenderWizard_Step_Message", "Nachricht und Optionen"),
2 => L("SenderWizard_Step_Receivers", "Empfänger"),
3 => L("SenderWizard_Step_Document", "Dokument"),
_ => L("SenderWizard_Step_Basics", "Grunddaten")
};
private string CurrentStepDescription => _currentStep switch
{
0 => L("SenderWizard_Step_Basics_Desc", "Starten Sie mit den Pflichtangaben für den Umschlag."),
1 => L("SenderWizard_Step_Message_Desc", "Fügen Sie optionale Nachricht und Einstellungen hinzu."),
2 => L("SenderWizard_Step_Receivers_Desc", "Definieren Sie die Empfänger für den Signaturprozess."),
3 => 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)
_selectedEnvelopeTypeId = GetDefaultEnvelopeTypeId();
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;
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);
}
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)
{
_currentStep++;
if (_currentStep == 2)
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,
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 == 1)
return;
if (_currentStep == 2 && (!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 (_receivers.Count == 0)
{
_validationMessage = L("SenderEditor_Validation_ReceiverRequired", "Bitte fügen Sie mindestens einen Empfänger hinzu.");
return false;
}
return true;
}
if (_currentStep == 3)
{
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 void ResetWizardState()
{
_currentStep = 0;
_validationMessage = null;
_title = string.Empty;
_selectedEnvelopeTypeId = GetDefaultEnvelopeTypeId();
_message = string.Empty;
_language = "de-DE";
_receiverName = string.Empty;
_receiverEmail = string.Empty;
_receivers.Clear();
_receiverTags = [];
_recentReceiverSuggestions.Clear();
_isLoadingRecentSuggestions = false;
_recentSuggestionsLoaded = false;
_documentBytes = null;
_documentFileName = null;
_documentSizeText = string.Empty;
}
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;
}
}