@using EnvelopeGenerator.Application.Common.Dto @using EnvelopeGenerator.Application.Common.Dto.Receiver @using EnvelopeGenerator.Application.Resources @using EnvelopeGenerator.Domain.Constants @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 ModelLocalizer @inject IStringLocalizer UiLocalizer @inject ReceiverService ReceiverService @inject PdfRenderService PdfRenderService
@for (var step = 0; step < TotalSteps; step++) { var isActive = step == _currentStep; var isDone = step < _currentStep;
@(step + 1)
}
@CurrentStepTitle
@CurrentStepDescription
@if (!string.IsNullOrWhiteSpace(_validationMessage)) {
@_validationMessage
} @if (_currentStep == 0) {
} else if (_currentStep == 1) {
} else if (_currentStep == 2) {
@L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen")
@L("SendReminderEmails", "Erinnerungs-E-Mails senden")
} else if (_currentStep == 3) {
@L("SenderWizard_RecentReceivers", "Zuletzt verwendete Empfänger")
@if (_isLoadingRecentSuggestions) {
@L("Loading", "Lädt...")
} else if (_recentReceiverSuggestions.Count > 0) {
@foreach (var suggestion in FilteredRecentSuggestions) { }
}
@if (_receivers.Count == 0) {
@L("SenderEditor_NoReceiversAdded", "Noch keine Empfänger hinzugefügt.")
} else {
@if (tagContext.DataItem is CreateEnvelopeWizardReceiver receiver) { @receiver.FullName @receiver.EmailAddress } else { @tagContext.DisplayText }
} } else {
@if (_documentBytes is not null) {
@_documentFileName
@_documentSizeText
} else { }
}
@code { [Parameter] public bool Visible { get; set; } [Parameter] public EventCallback VisibleChanged { get; set; } [Parameter] public EventCallback StartEditorRequested { get; set; } [Parameter] public IReadOnlyList EnvelopeTypes { get; set; } = []; private const int TotalSteps = 5; 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 bool _useAccessCode; private bool _tfaEnabled; private int _certificationType = (int)CertificationType.AdvancedElectronicSignature; private int _finalEmailToCreator = (int)FinalEmailType.No; private int _finalEmailToReceivers = (int)FinalEmailType.No; private bool _sendReminderEmails; private int _firstReminderDays; private int _reminderIntervalDays; private int _expiresWhenDays; private int _expiresWarningWhenDays; private string _receiverName = string.Empty; private string _receiverEmail = string.Empty; private readonly List _receivers = []; private IEnumerable _receiverTags = []; private readonly List _recentReceiverSuggestions = []; private bool _isLoadingRecentSuggestions; private bool _recentSuggestionsLoaded; private const int SuggestionLimit = 20; private byte[]? _documentBytes; private string? _documentFileName; private string _documentSizeText = string.Empty; private int _documentPreviewPageCount; private List _documentPreviewPages = []; private bool _isDocumentUploading; private bool _isStepActionRunning; private bool _isFinishing; private readonly IReadOnlyList _documentPreviewFieldOverlays = []; 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 FilteredRecentSuggestions => BuildFilteredRecentSuggestions(); private string? DocumentPreviewDataUrl => _documentBytes is { Length: > 0 } ? $"data:application/pdf;base64,{Convert.ToBase64String(_documentBytes)}" : null; private string CurrentStepTitle => _currentStep switch { 0 => L("SenderWizard_Step_Basics", "Grunddaten"), 1 => L("SenderWizard_Step_Message", "Nachricht und Optionen"), 2 => L("SenderEditor_EnvelopeSettings", "Envelope-Einstellungen"), 3 => L("SenderWizard_Step_Receivers", "Empfänger"), 4 => 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_Settings_Desc", "Konfigurieren Sie Sicherheits-, Erinnerungs- und Ablaufoptionen."), 3 => L("SenderWizard_Step_Receivers_Desc", "Definieren Sie die Empfänger für den Signaturprozess."), 4 => 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) ApplyDefaultEnvelopeType(); 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; var selectedType = EnvelopeTypes.FirstOrDefault(x => x.Id == id); if (selectedType is not null) ApplyEnvelopeTypeDefaults(selectedType); 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 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); _documentPreviewPages = []; try { _documentPreviewPages = (await PdfRenderService.RenderPagesAsync(_documentBytes)) .OrderBy(x => x.PageNumber) .ToList(); } catch { _documentPreviewPages = []; } _documentPreviewPageCount = _documentPreviewPages.Count > 0 ? _documentPreviewPages.Count : EstimatePdfPageCount(_documentBytes) ?? 1; } 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 == 3) 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, UseAccessCode: _useAccessCode, TFAEnabled: _tfaEnabled, CertificationType: _certificationType, FinalEmailToCreator: _finalEmailToCreator, FinalEmailToReceivers: _finalEmailToReceivers, SendReminderEmails: _sendReminderEmails, FirstReminderDays: _firstReminderDays, ReminderIntervalDays: _reminderIntervalDays, ExpiresWhenDays: _expiresWhenDays, ExpiresWarningWhenDays: _expiresWarningWhenDays, 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 == 3 && (!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 (!ValidateSettings(out var settingsError)) { _validationMessage = settingsError; return false; } return true; } if (_currentStep == 3) { if (_receivers.Count == 0) { _validationMessage = L("SenderEditor_Validation_ReceiverRequired", "Bitte fügen Sie mindestens einen Empfänger hinzu."); return false; } 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; } 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 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; _tfaEnabled = ParseCheckboxValue(args); if (_tfaEnabled) _useAccessCode = true; } private void OnUseAccessCodeChanged(ChangeEventArgs args) { _validationMessage = null; if (_tfaEnabled) { _useAccessCode = true; return; } _useAccessCode = ParseCheckboxValue(args); } private void OnSendReminderEmailsChanged(ChangeEventArgs args) { _validationMessage = null; _sendReminderEmails = ParseCheckboxValue(args); } private static bool ParseCheckboxValue(ChangeEventArgs args) { if (args.Value is bool boolValue) return boolValue; return bool.TryParse(args.Value?.ToString(), out var parsed) && parsed; } private void ApplyEnvelopeTypeDefaults(EnvelopeTypeDto envelopeType) { if (!string.IsNullOrWhiteSpace(envelopeType.Language)) _language = envelopeType.Language; if (envelopeType.UseAccessCode.HasValue) _useAccessCode = envelopeType.UseAccessCode.Value; if (_tfaEnabled) _useAccessCode = true; if (envelopeType.CertificationType.HasValue) _certificationType = envelopeType.CertificationType.Value; if (envelopeType.FinalEmailToCreator.HasValue) _finalEmailToCreator = envelopeType.FinalEmailToCreator.Value; if (envelopeType.FinalEmailToReceivers.HasValue) _finalEmailToReceivers = envelopeType.FinalEmailToReceivers.Value; if (envelopeType.SendReminderEmails.HasValue) _sendReminderEmails = envelopeType.SendReminderEmails.Value; if (envelopeType.FirstReminderDays.HasValue) _firstReminderDays = envelopeType.FirstReminderDays.Value; if (envelopeType.ReminderIntervalDays.HasValue) _reminderIntervalDays = envelopeType.ReminderIntervalDays.Value; if (envelopeType.ExpiresDays.HasValue) _expiresWhenDays = envelopeType.ExpiresDays.Value; if (envelopeType.ExpiresWarningDays.HasValue) _expiresWarningWhenDays = envelopeType.ExpiresWarningDays.Value; NormalizeSettings(); } private void NormalizeSettings() { _language = string.IsNullOrWhiteSpace(_language) ? "de-DE" : _language.Trim(); if (_tfaEnabled) _useAccessCode = true; _firstReminderDays = Math.Max(0, _firstReminderDays); _reminderIntervalDays = Math.Max(0, _reminderIntervalDays); _expiresWhenDays = Math.Max(0, _expiresWhenDays); _expiresWarningWhenDays = Math.Max(0, _expiresWarningWhenDays); if (_expiresWhenDays > 0 && _expiresWarningWhenDays > _expiresWhenDays) _expiresWarningWhenDays = _expiresWhenDays; } private bool ValidateSettings(out string? message) { NormalizeSettings(); if (_sendReminderEmails && _reminderIntervalDays <= 0) { message = L("SenderEditor_Validation_ReminderInterval", "Bitte geben Sie ein Erinnerungsintervall groesser als 0 Tage ein."); return false; } if (_sendReminderEmails && _firstReminderDays < 0) { message = L("SenderEditor_Validation_FirstReminder", "Bitte geben Sie gueltige Tage fuer die erste Erinnerung ein."); return false; } if (_expiresWhenDays > 0 && _expiresWarningWhenDays > _expiresWhenDays) { message = L("SenderEditor_Validation_ExpiryWarning", "Die Ablaufwarnung darf nicht nach dem Ablaufdatum liegen."); return false; } message = null; return true; } private void ApplyDefaultEnvelopeType() { _selectedEnvelopeTypeId = GetDefaultEnvelopeTypeId(); if (!_selectedEnvelopeTypeId.HasValue) return; var selectedType = EnvelopeTypes.FirstOrDefault(x => x.Id == _selectedEnvelopeTypeId.Value); if (selectedType is not null) ApplyEnvelopeTypeDefaults(selectedType); } private void ResetWizardState() { _currentStep = 0; _validationMessage = null; _title = string.Empty; _selectedEnvelopeTypeId = null; _message = string.Empty; _language = "de-DE"; _useAccessCode = false; _tfaEnabled = false; _certificationType = (int)CertificationType.AdvancedElectronicSignature; _finalEmailToCreator = (int)FinalEmailType.No; _finalEmailToReceivers = (int)FinalEmailType.No; _sendReminderEmails = false; _firstReminderDays = 0; _reminderIntervalDays = 0; _expiresWhenDays = 0; _expiresWarningWhenDays = 0; _receiverName = string.Empty; _receiverEmail = string.Empty; _receivers.Clear(); _receiverTags = []; _recentReceiverSuggestions.Clear(); _isLoadingRecentSuggestions = false; _recentSuggestionsLoaded = false; _documentBytes = null; _documentFileName = null; _documentSizeText = string.Empty; _documentPreviewPageCount = 0; _documentPreviewPages = []; ApplyDefaultEnvelopeType(); } 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; } }