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:
@@ -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."),
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user