Simplify wizard flow and improve document upload UX

Reduced wizard steps from 5 to 4 by merging document upload into step 3. Added a new localized hint (`SenderWizard_FinalUploadHint`) to guide users during the final document upload. Removed redundant document preview functionality and associated UI elements.

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

Updated step titles and descriptions to reflect the new flow. General code cleanup to simplify logic and improve maintainability.
This commit is contained in:
2026-11-11 17:24:39 +01:00
parent 6ad76fa4b0
commit 94658b0d9b
5 changed files with 52 additions and 242 deletions

View File

@@ -11,7 +11,6 @@
@inject IStringLocalizer<Model> ModelLocalizer
@inject IStringLocalizer<Resource> UiLocalizer
@inject ReceiverService ReceiverService
@inject PdfRenderService PdfRenderService
<DxPopup Visible="@_isVisible"
VisibleChanged="OnPopupVisibleChangedAsync"
@@ -28,7 +27,7 @@
<div class="sender-create-wizard"
tabindex="0"
@onkeydown="OnWizardKeyDownAsync">
<div class="sender-create-wizard__body-layout @(_currentStep == 4 ? "sender-create-wizard__body-layout--document" : string.Empty)">
<div class="sender-create-wizard__body-layout">
<MudPaper Class="sender-create-wizard__stepper-shell" Elevation="0">
<MudStepper Vertical="true"
NonLinear="false"
@@ -44,9 +43,9 @@
{
var stepIndex = step;
<MudStep Title="@GetStepTitle(stepIndex)"
<MudStep Title="@GetStepTitle(stepIndex)"
Completed="@_completedSteps.Contains(stepIndex)"
Skippable="@(stepIndex == 3)">
Skippable="false">
<div class="sender-create-wizard__stepper-description">@GetStepDescription(stepIndex)</div>
@if (stepIndex == _currentStep)
@@ -89,40 +88,46 @@
</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>
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>
}
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label sender-create-wizard__label-row">
<span>@L("Language", "Sprache")</span>
<MudTooltip Text='@L("SenderWizard_Help_Language", "Legt die Sprache fuer Hinweise, E-Mails und Oberflaechentexte fest.")'>
<MudIconButton Icon="@Icons.Material.Filled.Info" Color="Color.Tertiary" Size="Size.Small" Class="sender-create-wizard__help-icon" aria-label='@L("SenderWizard_HelpAria_Language", "Hilfe zum Feld Sprache")' />
</MudTooltip>
</label>
<select @bind="_language" class="sender-create-wizard__select">
<option value="de-DE">Deutsch (de-DE)</option>
<option value="en-US">English (en-US)</option>
<option value="de">Deutsch (de)</option>
<option value="en">English (en)</option>
</select>
</div>
<div class="sender-create-wizard__field sender-create-wizard__field--full">
<div class="sender-create-wizard__stage">
<p>@L("SenderWizard_FinalUploadHint", "Letzte Moeglichkeit im Wizard: Bitte laden Sie jetzt Ihr PDF ueber den rechten Upload-Bereich hoch. Im Editor koennen Sie spaeter ebenfalls ein Dokument hochladen oder ersetzen.")</p>
</div>
</div>
</div>
}
else if (_currentStep == 2)
{
<div class="sender-create-wizard__settings-grid">
@@ -357,57 +362,9 @@
</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)
{
<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>
</div>
</div>
</div>
}
</MudStep>
}
@@ -415,7 +372,7 @@
</MudStepper>
</MudPaper>
<aside class="sender-create-wizard__wizard-side @(_currentStep == 4 ? "sender-create-wizard__wizard-side--hidden" : string.Empty)" aria-hidden="@(_currentStep == 4)">
<aside class="sender-create-wizard__wizard-side">
<PdfFileDropZone Disabled="@IsActionLocked"
HasDocument="@(_documentBytes is { Length: > 0 })"
DocumentName="@_documentFileName"
@@ -441,14 +398,6 @@
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"
@@ -485,7 +434,7 @@
[Parameter]
public IReadOnlyList<EnvelopeTypeDto> EnvelopeTypes { get; set; } = [];
private const int TotalSteps = 5;
private const int TotalSteps = 4;
private static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute EmailValidator = new();
private bool _isVisible;
@@ -521,15 +470,10 @@
private byte[]? _documentBytes;
private string? _documentFileName;
private string _documentSizeText = string.Empty;
private int _documentPreviewPageCount;
private List<PdfPageImageDto> _documentPreviewPages = [];
private bool _isDocumentUploading;
private bool _isDocumentPreviewPrepared;
private bool _isStepActionRunning;
private bool _isFinishing;
private readonly IReadOnlyList<SenderPdfFieldOverlay> _documentPreviewFieldOverlays = [];
private string? _validationMessage;
private bool IsActionLocked => _isDocumentUploading || _isStepActionRunning || _isFinishing;
@@ -537,26 +481,15 @@
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"),
3 => L("SenderWizard_Step_Message", "Nachricht und letzter Dokument-Upload"),
_ => L("SenderWizard_Step_Start", "Start")
};
@@ -565,8 +498,7 @@
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."),
3 => L("SenderWizard_Step_Message_Desc", "Fuegen Sie optionale Nachricht hinzu und nutzen Sie rechts die letzte Upload-Moeglichkeit im Wizard."),
_ => L("SenderWizard_Step_Basics_Desc", "Starten Sie mit den Pflichtangaben für den Umschlag.")
};
@@ -755,12 +687,7 @@
_documentBytes = ms.ToArray();
_documentFileName = file.Name;
_documentSizeText = FormatSize(_documentBytes.Length);
_documentPreviewPages = [];
_documentPreviewPageCount = 0;
_isDocumentPreviewPrepared = false;
if (_currentStep == 4)
await EnsureDocumentPreviewPreparedAsync();
}
finally
{
@@ -802,8 +729,6 @@
if (_currentStep == 1)
await EnsureRecentSuggestionsLoadedAsync();
if (_currentStep == 4)
await EnsureDocumentPreviewPreparedAsync();
}
}
finally
@@ -814,19 +739,6 @@
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)
@@ -960,33 +872,6 @@
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)
@@ -1066,17 +951,6 @@
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;
}
@@ -1094,51 +968,6 @@
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;
@@ -1315,9 +1144,6 @@
_documentBytes = null;
_documentFileName = null;
_documentSizeText = string.Empty;
_documentPreviewPageCount = 0;
_documentPreviewPages = [];
_isDocumentPreviewPrepared = false;
ApplyDefaultEnvelopeType();
NormalizeSettings();