Refactor and enhance envelope wizard with MudBlazor

Replaced custom wizard UI with MudBlazor's MudStepper for a modern and consistent design. Added dynamic step titles, descriptions, and forms for envelope creation, including advanced settings like certification, reminders, and email notifications. Integrated PDF upload with preview functionality.

Refactored `CreateEnvelopeWizardPopup.razor` for better maintainability and modularity. Removed redundant code and centralized step-related logic. Improved localization, accessibility, and user experience.

Updated `EnvelopeGenerator.Server.Client.csproj` and `EnvelopeGenerator.Server.csproj` to include MudBlazor. Registered MudBlazor services in `Program.cs` and added required CSS/JS in `App.razor`. Updated styles in `sender-page.css` to align with the new stepper design.
This commit is contained in:
2026-11-05 15:22:21 +01:00
parent 595bbf3f25
commit e3d2225a44
8 changed files with 390 additions and 358 deletions

View File

@@ -1,9 +1,6 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.MessagingService.Application.Common.Dto;
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Application.EmailSending.Commands;
using DigitalData.MessagingService.Client;
using DigitalData.MessagingService.Domain.Entities;
using EnvelopeGenerator.Application.Common.Interfaces.Services.Notification;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Domain.Entities;

View File

@@ -5,6 +5,7 @@
@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
@@ -27,333 +28,341 @@
<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>
<MudPaper Class="sender-create-wizard__stepper-shell" Elevation="0">
<MudStepper Vertical="true"
NonLinear="false"
ShowResetButton="false"
ActiveIndex="@_currentStep"
Class="sender-create-wizard__stepper">
<ChildContent>
@for (var step = 0; step < TotalSteps; step++)
{
var stepIndex = step;
<div class="sender-create-wizard__meta">
<div class="sender-create-wizard__title">@CurrentStepTitle</div>
<div class="sender-create-wizard__description">@CurrentStepDescription</div>
</div>
<MudStep Title="@GetStepTitle(stepIndex)">
<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">
@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__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">@L("CertificationType", "Zertifizierung")</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">@L("FinalEmailToCreator", "Finale E-Mail an Ersteller")</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">@L("FinalEmailToReceivers", "Finale E-Mail an Empfänger")</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" />
@L("TFAEnabled", "2FA aktiviert")
</label>
<label class="sender-create-wizard__settings-toggle">
<input type="checkbox" checked="@_useAccessCode" @onchange="OnUseAccessCodeChanged" disabled="@_tfaEnabled" />
@L("UseAccessCode", "Zugriffscode verwenden")
</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" />
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
</label>
</div>
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label">@L("FirstReminderDays", "Erste Erinnerung (Tage)")</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">@L("ReminderIntervalDays", "Erinnerungsintervall (Tage)")</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">@L("ExpiresWhenDays", "Läuft ab nach (Tage)")</label>
<InputNumber @bind-Value="_expiresWhenDays" class="form-control form-control-sm" />
</div>
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label">@L("ExpiresWarningWhenDays", "Ablaufwarnung (Tage)")</label>
<InputNumber @bind-Value="_expiresWarningWhenDays" class="form-control form-control-sm" />
</div>
</div>
</div>
</div>
}
else if (_currentStep == 3)
{
<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-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>
}
@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__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">@L("CertificationType", "Zertifizierung")</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">@L("FinalEmailToCreator", "Finale E-Mail an Ersteller")</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">@L("FinalEmailToReceivers", "Finale E-Mail an Empfänger")</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" />
@L("TFAEnabled", "2FA aktiviert")
</label>
<label class="sender-create-wizard__settings-toggle">
<input type="checkbox" checked="@_useAccessCode" @onchange="OnUseAccessCodeChanged" disabled="@_tfaEnabled" />
@L("UseAccessCode", "Zugriffscode verwenden")
</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" />
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
</label>
</div>
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label">@L("FirstReminderDays", "Erste Erinnerung (Tage)")</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">@L("ReminderIntervalDays", "Erinnerungsintervall (Tage)")</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">@L("ExpiresWhenDays", "Läuft ab nach (Tage)")</label>
<InputNumber @bind-Value="_expiresWhenDays" class="form-control form-control-sm" />
</div>
<div class="sender-create-wizard__field">
<label class="sender-create-wizard__label">@L("ExpiresWarningWhenDays", "Ablaufwarnung (Tage)")</label>
<InputNumber @bind-Value="_expiresWarningWhenDays" class="form-control form-control-sm" />
</div>
</div>
</div>
</div>
}
else if (_currentStep == 3)
{
<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-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>
</BodyContentTemplate>
<FooterContentTemplate>
@@ -465,7 +474,11 @@
? $"data:application/pdf;base64,{Convert.ToBase64String(_documentBytes)}"
: null;
private string CurrentStepTitle => _currentStep switch
private string CurrentStepTitle => GetStepTitle(_currentStep);
private string CurrentStepDescription => GetStepDescription(_currentStep);
private string GetStepTitle(int step) => step switch
{
0 => L("SenderWizard_Step_Basics", "Grunddaten"),
1 => L("SenderWizard_Step_Message", "Nachricht und Optionen"),
@@ -475,7 +488,7 @@
_ => L("SenderWizard_Step_Basics", "Grunddaten")
};
private string CurrentStepDescription => _currentStep switch
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_Message_Desc", "Fügen Sie optionale Nachricht und Einstellungen hinzu."),

View File

@@ -12,6 +12,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MudBlazor" Version="8.0.0" />
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using EnvelopeGenerator.Server.Client.Services;
using EnvelopeGenerator.Server.Client.Options;
using MudBlazor.Services;
using DevExpress.Blazor.Reporting;
using DevExpress.XtraReports.Web.Extensions;
using DevExpress.DataAccess.Web;
@@ -16,6 +17,7 @@ CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
builder.Services.AddLocalization();
builder.Services.AddMudServices();
// Named HttpClient for API calls (both for services and DevExpress components)
builder.Services.AddHttpClient("EnvelopeGenerator.Server", client =>

View File

@@ -9,6 +9,7 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css" />
<link rel="stylesheet" href="css/app.css" />
<link rel="stylesheet" href="css/envelope-viewer.css" />
<link rel="stylesheet" href="_content/MudBlazor/MudBlazor.min.css" />
<link rel="stylesheet" href="EnvelopeGenerator.Server.styles.css" />
<HeadOutlet />
<!-- Apply saved theme immediately on page load — before WASM boots.
@@ -37,6 +38,7 @@
<script src="js/receiver-signature.js?v=9"></script>
<script src="js/signflow-theme.js"></script>
<script src="js/envelope-editor.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script src="_framework/blazor.web.js"></script>
</body>

View File

@@ -30,6 +30,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.22" />
<PackageReference Include="MudBlazor" Version="8.0.0" />
<PackageReference Include="DevExpress.Blazor" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />

View File

@@ -32,6 +32,7 @@ using NLog;
using NLog.Web;
using Scalar.AspNetCore;
using System.Globalization;
using MudBlazor.Services;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("EnvelopeGenerator.Server logging initialized!");
@@ -60,6 +61,7 @@ try
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
builder.Services.AddMudServices();
// Add API Controllers
builder.Services.AddControllers()

View File

@@ -1116,57 +1116,71 @@
z-index: 2;
}
.sender-create-wizard__progress {
position: relative;
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
align-items: center;
gap: 0.5rem;
.sender-create-wizard__stepper-shell {
border: 1px solid rgba(126, 34, 206, 0.12);
border-radius: 10px;
background: rgba(126, 34, 206, 0.04);
padding: 0.75rem;
}
.sender-create-wizard__progress-track {
position: absolute;
left: 12px;
right: 12px;
top: 50%;
height: 2px;
transform: translateY(-50%);
background: rgba(126, 34, 206, 0.18);
z-index: 0;
.sender-create-wizard__stepper {
margin: 0;
}
.sender-create-wizard__step {
position: relative;
z-index: 1;
display: flex;
justify-content: center;
.sender-create-wizard__stepper .mud-step {
pointer-events: none;
}
.sender-create-wizard__step-index {
width: 24px;
height: 24px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.78rem;
.sender-create-wizard__stepper .mud-stepper-actions,
.sender-create-wizard__stepper .mud-stepper-action {
display: none !important;
}
.sender-create-wizard__stepper .mud-step-label {
color: #1f2937;
font-size: 0.86rem;
font-weight: 700;
}
.sender-create-wizard__stepper .mud-step-content {
color: #64748b;
}
.sender-create-wizard__stepper .mud-stepper-vertical .mud-stepper-step {
margin-bottom: 0.25rem;
}
.sender-create-wizard__stepper .mud-step-icon {
color: #64748b;
background: #f8fafc;
border: 1px solid rgba(126, 34, 206, 0.25);
}
.sender-create-wizard__step--active .sender-create-wizard__step-index {
.sender-create-wizard__stepper .mud-step-completed .mud-step-icon,
.sender-create-wizard__stepper .mud-step-current .mud-step-icon {
color: #ffffff;
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
border-color: transparent;
}
.sender-create-wizard__stepper .mud-step-current .mud-step-icon {
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
box-shadow: 0 0 0 3px rgba(126, 34, 206, 0.15);
}
.sender-create-wizard__step--done .sender-create-wizard__step-index {
color: #ffffff;
.sender-create-wizard__stepper .mud-step-completed .mud-step-icon {
background: #2a5298;
border-color: transparent;
}
.sender-create-wizard__stepper-description {
font-size: 0.8rem;
line-height: 1.35;
color: #64748b;
margin-top: 0.2rem;
padding-right: 0.35rem;
}
.sender-create-wizard__step-content {
margin-top: 0.7rem;
}
.sender-create-wizard__meta {