@page "/sender/editor" @rendermode InteractiveWebAssembly @using DevExpress.Blazor.Reporting.Models @using DevExpress.Blazor @using EnvelopeGenerator.Application.Common.Dto @using EnvelopeGenerator.Application.Envelopes.Commands @using EnvelopeGenerator.Application.Resources @using EnvelopeGenerator.Server.Client.Components.PdfEditor @using EnvelopeGenerator.Server.Client.Services @using Microsoft.AspNetCore.WebUtilities @using Microsoft.AspNetCore.Components.Forms @using Microsoft.Extensions.Localization @inject IJSRuntime JSRuntime @inject NavigationManager NavigationManager @inject AppVersionService AppVersion @inject ILogger Logger @inject DocumentService DocumentService @inject PdfRenderService PdfRenderService @inject ReceiverService ReceiverService @inject EnvelopeService EnvelopeService @inject IStringLocalizer UiLocalizer
@* ── Action Bar ── *@
@* Left: Title + meta inputs + receivers *@
@* ── Title & Message inputs ── *@
@* Titel *@
@if (_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle)) { @L("RequiredField", "Pflichtfeld") }
@* Nachricht *@
@* PDF filename badge *@ @if (_pdfLoaded) {
@_fileName @if (_signatureFields.Count > 0) { @_signatureFields.Count @L("SenderEditor_SignatureField", "Signaturfeld")@(_signatureFields.Count != 1 ? "er" : "") }
}
@L("Receiver", "Empfänger") @_receivers.Count
@if (_receivers.Count == 0) {
@L("SenderEditor_NoReceiversAdded", "Noch keine Empfänger hinzugefügt.")
} else {
@foreach (var receiver in _receivers) {
@receiver.FullName
@if (!string.IsNullOrWhiteSpace(receiver.PhoneNumber)) {
@receiver.PhoneNumber
}
}
}
@* Right: Buttons *@
@* PDF Upload *@ @if (_pdfLoaded && _signatureFields.Count > 0) { }
@* Placement mode hint bar *@ @if (_pendingReceiverForPlacement is not null) {
📌 @L("SenderEditor_ClickDocumentFor", "Klicken Sie auf die Stelle im Dokument für") @_pendingReceiverForPlacement.FullName.  
}
@* ── Content ── *@
@if (!_pdfLoaded) { @* Empty state *@
@L("SenderEditor_NoDocumentLoaded", "Kein Dokument geladen")

@L("SenderEditor_UploadPdfToPlaceFields", "Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren.")

} else if (_errorMessage is not null) {
@L("Error", "Fehler"): @_errorMessage
} else { }
@L("FullName", "Vor- und Nachname")
@L("PhoneNumber", "Telefonnummer") (@L("Optional", "optional"))
@L("EmailAddress", "E-Mail-Adresse")
@if (_receiverEmailSuggestions.Count > 0) {
}
@L("SenderEditor_EmailSuggestionsHint", "Bereits verwendete E-Mail-Adressen werden bei der Eingabe vorgeschlagen.")
@if (_isReceiverEmailSearchRunning) {
@L("SenderEditor_EmailSuggestionsLoading", "E-Mail-Vorschläge werden geladen…")
} @if (!string.IsNullOrWhiteSpace(_receiverPopupValidationMessage)) {
@_receiverPopupValidationMessage
}
@* ── Save result popup (success + error) ── *@ @if (_saveErrorMessage is null) {

@_saveSuccessTitle

@_saveSuccessBody

} else {

@L("SenderEditor_ErrorCreatingEnvelope", "Fehler beim Erstellen des Umschlags")

@_saveErrorMessage

}
@if (_saveErrorMessage is null) { @if (_saveNavigateToDashboard) { } } else { }
@code { [SupplyParameterFromQuery(Name = "envelopeId")] public int? EnvelopeId { get; set; } // ── Constants ── // Signature field size in PDF points (fixed): 1.77" × 1.96" const double SigWidthPt = 1.77 * 72; // 127.44 pt const double SigHeightPt = 1.96 * 72; // 141.12 pt // CssClass for JS click coordinate helper const string ViewerCssClass = "sender-editor-pdf-viewer"; // ── State ── bool _pdfLoaded = false; bool _isPageReady; bool _isPdfUploadInProgress; string _fileName = string.Empty; string? _errorMessage; byte[]? _originalPdfBytes; // Pristine upload — never modified, used as base for redraw int _pageCount; List _pagePreviews = []; int? _loadedEnvelopeId; int? _editingEnvelopeId; bool _isLoadingEnvelopeForEdit; List _signatureFields = []; ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen" IReadOnlyList FieldOverlays => _signatureFields .Select((f, i) => new SenderPdfFieldOverlay(i, f.Page, f.XPt, f.YPt, f.ReceiverName, f.Color)) .ToList(); string? PdfDataUrl => _originalPdfBytes is { Length: > 0 } ? $"data:application/pdf;base64,{Convert.ToBase64String(_originalPdfBytes)}" : null; // ── Save state ── bool _isSaving = false; bool _savePopupVisible = false; string? _saveErrorMessage = null; string _saveSuccessHeader = "Erfolg"; string _saveSuccessTitle = "Vorgang erfolgreich abgeschlossen."; string _saveSuccessBody = string.Empty; bool _saveNavigateToDashboard = false; // ── Envelope metadata ── string _envelopeTitle = string.Empty; string _envelopeMessage = string.Empty; bool _titleTouched = false; List _receivers = []; bool _receiverPopupVisible; string _receiverDraftName = string.Empty; string _receiverDraftEmail = string.Empty; string _receiverDraftPhoneNumber = string.Empty; string? _selectedReceiverEmailSuggestion; string? _receiverPopupValidationMessage; bool _isReceiverEmailSearchRunning; List _receiverEmailSuggestions = []; int _receiverEmailSearchVersion; static readonly System.ComponentModel.DataAnnotations.EmailAddressAttribute ReceiverEmailValidator = new(); bool IsEditMode => EnvelopeId.HasValue && EnvelopeId.Value > 0; // ── Lifecycle ── protected override async Task OnParametersSetAsync() { if (EnvelopeId.HasValue && EnvelopeId.Value > 0) _editingEnvelopeId = EnvelopeId.Value; if (IsEditMode && _loadedEnvelopeId != EnvelopeId) { await LoadEnvelopeForEditAsync(EnvelopeId!.Value); return; } if (IsEditMode) return; } protected override Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { _isPageReady = true; StateHasChanged(); } return Task.CompletedTask; } async Task LoadEnvelopeForEditAsync(int envelopeId) { if (_isLoadingEnvelopeForEdit) return; _isLoadingEnvelopeForEdit = true; _errorMessage = null; try { var envelopes = await EnvelopeService.GetAsync(id: envelopeId); var envelope = envelopes?.FirstOrDefault() ?? throw new InvalidOperationException($"Envelope {envelopeId} not found."); var document = await DocumentService.GetSenderDocumentDetailsByEnvelopeIdAsync(envelopeId); _envelopeTitle = envelope.Title ?? string.Empty; _envelopeMessage = envelope.Message ?? string.Empty; var envelopeReceivers = envelope.EnvelopeReceivers?.ToList() ?? []; _receivers = envelopeReceivers .Select((er, index) => new ReceiverDraft( Guid.NewGuid(), er.Name ?? er.Receiver?.EmailAddress ?? $"Receiver {er.ReceiverId}", er.Receiver?.EmailAddress ?? string.Empty, string.Empty, ReceiverPalette[index % ReceiverPalette.Length])) .ToList(); var receiverById = envelopeReceivers.ToDictionary(er => er.ReceiverId, er => er); var colorByEmail = _receivers .Where(r => !string.IsNullOrWhiteSpace(r.Email)) .GroupBy(r => r.Email, StringComparer.OrdinalIgnoreCase) .ToDictionary(g => g.Key, g => g.First().Color, StringComparer.OrdinalIgnoreCase); _signatureFields = (document?.Elements ?? Enumerable.Empty()) .Select(element => { receiverById.TryGetValue(element.ReceiverId, out var envelopeReceiver); var receiverName = envelopeReceiver?.Name ?? envelopeReceiver?.Receiver?.EmailAddress ?? element.FullName ?? $"Receiver {element.ReceiverId}"; var receiverEmail = envelopeReceiver?.Receiver?.EmailAddress; var color = (!string.IsNullOrWhiteSpace(receiverEmail) && colorByEmail.TryGetValue(receiverEmail, out var receiverColor)) ? receiverColor : ReceiverPalette[Math.Abs(element.ReceiverId) % ReceiverPalette.Length]; return new SignatureFieldDraft( XPt: element.X * 72.0, YPt: element.Y * 72.0, Page: element.Page, ReceiverName: receiverName, Color: color); }) .ToList(); _originalPdfBytes = document?.ByteData; _pdfLoaded = _originalPdfBytes is { Length: > 0 }; _pagePreviews = []; _pageCount = _pagePreviews.Count > 0 ? _pagePreviews.Count : EstimatePdfPageCount(_originalPdfBytes) ?? Math.Max(1, _signatureFields.Select(f => f.Page).DefaultIfEmpty(1).Max()); _fileName = $"Envelope-{envelopeId}.pdf"; _pendingReceiverForPlacement = null; _loadedEnvelopeId = envelopeId; _editingEnvelopeId = envelopeId; } catch (Exception ex) { Logger.LogError(ex, "Failed to load envelope {EnvelopeId} for edit mode", envelopeId); _errorMessage = string.Format( L("SenderEditor_LoadEnvelopeError", "Fehler beim Laden des Umschlags #{0}: {1}"), envelopeId, ex.Message); } finally { _isLoadingEnvelopeForEdit = false; } } // ── PDF upload ── async Task OnPdfFileSelectedAsync(InputFileChangeEventArgs e) { if (!_isPageReady) return; if (_isPdfUploadInProgress) return; _errorMessage = null; var file = e.File; if (file is null) return; if (!file.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) { _errorMessage = L("SenderEditor_SelectPdf", "Bitte wählen Sie eine PDF-Datei aus."); return; } try { _isPdfUploadInProgress = true; await InvokeAsync(StateHasChanged); const long maxBytes = 50 * 1024 * 1024; using var ms = new System.IO.MemoryStream(); await file.OpenReadStream(maxBytes).CopyToAsync(ms); _originalPdfBytes = ms.ToArray(); _fileName = file.Name; _pdfLoaded = true; _pagePreviews = (await PdfRenderService.RenderPagesAsync(_originalPdfBytes)) .OrderBy(p => p.PageNumber) .ToList(); _pageCount = _pagePreviews.Count > 0 ? _pagePreviews.Count : EstimatePdfPageCount(_originalPdfBytes) ?? 1; _signatureFields.Clear(); _pendingReceiverForPlacement = null; Logger.LogInformation("PDF loaded: {Name} ({Size} bytes)", _fileName, _originalPdfBytes.Length); } catch (Exception ex) { _errorMessage = string.Format( L("SenderEditor_LoadFileError", "Fehler beim Laden der Datei: {0}"), ex.Message); Logger.LogError(ex, "Failed to load PDF file"); } finally { _isPdfUploadInProgress = false; await InvokeAsync(StateHasChanged); } } // ── Placement mode ── void ActivatePlacementForReceiver(ReceiverDraft receiver) { // Toggle: clicking the same receiver again cancels placement _pendingReceiverForPlacement = _pendingReceiverForPlacement?.Id == receiver.Id ? null : receiver; } void CancelPlacement() => _pendingReceiverForPlacement = null; // ── PDF area click → place field ── async Task OnPdfAreaClickAsync(MouseEventArgs e) { if (_pendingReceiverForPlacement is null) return; if (!_pdfLoaded || _originalPdfBytes is null) return; // Ask JS for the normalised click position within the rendered PDF page var coords = await JSRuntime.InvokeAsync( "envelopeEditor.getClickCoordsOnPdfPage", ViewerCssClass, e.ClientX, e.ClientY); if (coords is null) { Logger.LogWarning("[SenderEditor] getClickCoordsOnPdfPage returned null"); return; } int page1Based = coords.PageIndex + 1; var pagePreview = _pagePreviews.FirstOrDefault(p => p.PageNumber == page1Based); var pageWidthPt = pagePreview?.WidthPt ?? 595.0; var pageHeightPt = pagePreview?.HeightPt ?? 842.0; // Convert normalised [0,1] → PDF points; clamp so box stays inside page double xPt = coords.NormX * pageWidthPt; double yPt = coords.NormY * pageHeightPt; xPt = Math.Max(0, Math.Min(xPt, pageWidthPt - SigWidthPt)); yPt = Math.Max(0, Math.Min(yPt, pageHeightPt - SigHeightPt)); var field = new SignatureFieldDraft( XPt: xPt, YPt: yPt, Page: page1Based, ReceiverName: _pendingReceiverForPlacement.FullName, Color: _pendingReceiverForPlacement.Color); _signatureFields.Add(field); _pendingReceiverForPlacement = null; Logger.LogInformation( "[SenderEditor] Field added: Page={Page} X={X:F1}pt Y={Y:F1}pt Receiver={Receiver}", page1Based, xPt, yPt, field.ReceiverName); } // ── Remove a single field ── async Task OnRemoveFieldByIndexAsync(int index) { if (index < 0 || index >= _signatureFields.Count) return; _signatureFields.RemoveAt(index); await Task.CompletedTask; } async Task OnMoveFieldAsync(SenderPdfFieldMoveRequest request) { if (request.Index < 0 || request.Index >= _signatureFields.Count) return; var field = _signatureFields[request.Index]; _signatureFields[request.Index] = field with { XPt = request.XPt, YPt = request.YPt, Page = request.Page }; await Task.CompletedTask; } // ── Clear all fields ── async Task ClearAllFieldsAsync() { _signatureFields.Clear(); _pendingReceiverForPlacement = null; await Task.CompletedTask; } 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; } } void Cancel() => NavigationManager.NavigateTo("/sender"); void GoToDashboard() { NavigationManager.NavigateTo("/sender"); } async Task SaveDraftAsync() { _isSaving = true; _saveErrorMessage = null; await InvokeAsync(StateHasChanged); try { var previousEnvelopeId = EnvelopeId; var command = BuildCreateEnvelopeCommand(send: false); Logger.LogInformation("[SenderEditor] Saving draft. EnvelopeId={EnvelopeId} IsEditMode={IsEditMode}", command.EnvelopeId, IsEditMode); var result = await EnvelopeService.UpsertAsync(command); if (result is null) throw new InvalidOperationException("Draft save did not return a result."); EnvelopeId = result.Id; _loadedEnvelopeId = result.Id; _editingEnvelopeId = result.Id; if (previousEnvelopeId != result.Id) { NavigationManager.NavigateTo($"/sender/editor2?envelopeId={result.Id}", forceLoad: false); } NavigationManager.NavigateTo("/sender", forceLoad: false); } catch (Exception ex) { Logger.LogError(ex, "[SenderEditor] Failed to save draft"); _saveErrorMessage = ex.Message; _savePopupVisible = true; } finally { _isSaving = false; } } // ── Send envelope ── async Task SendAsync() { // ── Validation ── _titleTouched = true; if (!_pdfLoaded || _originalPdfBytes is null) { _saveErrorMessage = L("SenderEditor_Validation_UploadPdf", "Bitte laden Sie zuerst ein PDF-Dokument hoch."); _savePopupVisible = true; return; } if (string.IsNullOrWhiteSpace(_envelopeTitle)) { _saveErrorMessage = L("SenderEditor_Validation_TitleRequired", "Bitte geben Sie einen Titel ein."); _savePopupVisible = true; return; } if (_receivers.Count == 0) { _saveErrorMessage = L("SenderEditor_Validation_ReceiverRequired", "Bitte fügen Sie mindestens einen Empfänger hinzu."); _savePopupVisible = true; return; } if (_signatureFields.Count == 0) { _saveErrorMessage = L("SenderEditor_Validation_SignatureFieldRequired", "Bitte platzieren Sie mindestens ein Signaturfeld."); _savePopupVisible = true; return; } // Warn if any receiver has no signature field, but don't block var receiversWithoutField = _receivers .Where(r => !_signatureFields.Any(f => f.ReceiverName == r.FullName)) .ToList(); if (receiversWithoutField.Count > 0) { Logger.LogWarning( "[SenderEditor] Receivers without signature field: {Names}", string.Join(", ", receiversWithoutField.Select(r => r.FullName))); } _isSaving = true; _saveErrorMessage = null; await InvokeAsync(StateHasChanged); try { var envelopeId = _editingEnvelopeId ?? EnvelopeId ?? _loadedEnvelopeId ?? GetEnvelopeIdFromCurrentUri(); if (!envelopeId.HasValue || envelopeId.Value <= 0) { var saveResult = await EnvelopeService.UpsertAsync(BuildCreateEnvelopeCommand(send: false)) ?? throw new InvalidOperationException("Draft save did not return a result."); envelopeId = saveResult.Id; EnvelopeId = saveResult.Id; _loadedEnvelopeId = saveResult.Id; _editingEnvelopeId = saveResult.Id; } var sendResult = await EnvelopeService.UpsertAsync(BuildCreateEnvelopeCommand(send: true) with { EnvelopeId = envelopeId }) ?? throw new InvalidOperationException("Send did not return a result."); Logger.LogInformation("[SenderEditor] Envelope sent from draft. Id={Id} Status={Status}", sendResult.Id, sendResult.Status); // Success — show popup; GoToDashboard clears cache and navigates _saveErrorMessage = null; _saveSuccessHeader = L("SenderEditor_Sent_Header", "Umschlag gesendet"); _saveSuccessTitle = L("SenderEditor_Sent_Title", "Umschlag wurde erfolgreich gesendet."); _saveSuccessBody = L("SenderEditor_Sent_Body", "Der bestehende Entwurf wurde versendet."); _saveNavigateToDashboard = true; _savePopupVisible = true; } catch (Exception ex) { Logger.LogError(ex, "[SenderEditor] Failed to send envelope draft"); _saveErrorMessage = ex.Message; _savePopupVisible = true; } finally { _isSaving = false; } } CreateEnvelopeCommand BuildCreateEnvelopeCommand(bool send) { var envelopeId = _editingEnvelopeId ?? EnvelopeId ?? _loadedEnvelopeId ?? GetEnvelopeIdFromCurrentUri(); if (IsEditMode && envelopeId is null) throw new InvalidOperationException("Edit mode envelope id is missing."); var docBase64 = _originalPdfBytes is { Length: > 0 } ? Convert.ToBase64String(_originalPdfBytes) : null; var receiverCommands = _receivers.Select(receiver => { var fields = _signatureFields .Where(f => f.ReceiverName == receiver.FullName) .Select(f => new CreateEnvelopeFieldDto( X: f.XPt / 72.0, Y: f.YPt / 72.0, Page: f.Page)) .ToList(); return new CreateEnvelopeReceiverDto( EmailAddress: receiver.Email, Name: receiver.FullName, PhoneNumber: string.IsNullOrWhiteSpace(receiver.PhoneNumber) ? null : receiver.PhoneNumber, Fields: fields); }).ToList(); return new CreateEnvelopeCommand { EnvelopeId = envelopeId, Title = _envelopeTitle, Message = _envelopeMessage, TFAEnabled = false, Send = send, Document = string.IsNullOrWhiteSpace(docBase64) ? null : new CreateEnvelopeDocumentDto(docBase64), Receivers = receiverCommands }; } int? GetEnvelopeIdFromCurrentUri() { try { var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri); if (string.IsNullOrWhiteSpace(uri.Query)) return null; var query = QueryHelpers.ParseQuery(uri.Query); if (!query.TryGetValue("envelopeId", out var raw)) return null; return int.TryParse(raw.ToString(), out var id) && id > 0 ? id : null; } catch { return null; } } // ── Receiver popup ── void OpenAddReceiverPopup() { _receiverDraftName = string.Empty; _receiverDraftEmail = string.Empty; _receiverDraftPhoneNumber = string.Empty; _selectedReceiverEmailSuggestion = null; _receiverPopupValidationMessage = null; _receiverEmailSuggestions.Clear(); _receiverPopupVisible = true; } void CloseAddReceiverPopup() { _receiverPopupVisible = false; _receiverPopupValidationMessage = null; _selectedReceiverEmailSuggestion = null; _isReceiverEmailSearchRunning = false; } void OnReceiverNameChanged(string? value) { _receiverDraftName = value ?? string.Empty; _receiverPopupValidationMessage = null; } void OnReceiverPhoneNumberChanged(string? value) { _receiverDraftPhoneNumber = value ?? string.Empty; _receiverPopupValidationMessage = null; } async Task OnReceiverEmailKeyDownAsync(KeyboardEventArgs e) { if (_receiverEmailSuggestions.Count == 0) { if (e.Key == "Escape") _selectedReceiverEmailSuggestion = null; return; } var currentIndex = _selectedReceiverEmailSuggestion is null ? -1 : _receiverEmailSuggestions.FindIndex(em => string.Equals(em, _selectedReceiverEmailSuggestion, StringComparison.OrdinalIgnoreCase)); if (e.Key == "ArrowDown") { var next = currentIndex < _receiverEmailSuggestions.Count - 1 ? currentIndex + 1 : 0; SelectReceiverEmailSuggestion(_receiverEmailSuggestions[next]); } else if (e.Key == "ArrowUp") { var next = currentIndex > 0 ? currentIndex - 1 : _receiverEmailSuggestions.Count - 1; SelectReceiverEmailSuggestion(_receiverEmailSuggestions[next]); } else if (e.Key == "Enter") { var sel = currentIndex >= 0 && currentIndex < _receiverEmailSuggestions.Count ? _receiverEmailSuggestions[currentIndex] : _receiverEmailSuggestions.FirstOrDefault(); if (!string.IsNullOrWhiteSpace(sel)) await OnReceiverEmailSuggestionCommittedAsync(sel); } else if (e.Key == "Escape") { _receiverEmailSuggestions.Clear(); _selectedReceiverEmailSuggestion = null; } } void SelectReceiverEmailSuggestion(string? value) { if (string.IsNullOrWhiteSpace(value)) return; _selectedReceiverEmailSuggestion = value.Trim(); _receiverDraftEmail = _selectedReceiverEmailSuggestion; _receiverPopupValidationMessage = null; } Task OnReceiverEmailSuggestionCommittedAsync(string? value) { SelectReceiverEmailSuggestion(value); _receiverEmailSuggestions.Clear(); return Task.CompletedTask; } async Task OnReceiverEmailTextChangedAsync(string? value) { _receiverDraftEmail = value?.Trim() ?? string.Empty; _selectedReceiverEmailSuggestion = _receiverDraftEmail; _receiverPopupValidationMessage = null; var searchVersion = ++_receiverEmailSearchVersion; if (string.IsNullOrWhiteSpace(_receiverDraftEmail) || _receiverDraftEmail.Length < 2) { _receiverEmailSuggestions.Clear(); _selectedReceiverEmailSuggestion = null; _isReceiverEmailSearchRunning = false; return; } _isReceiverEmailSearchRunning = true; try { var results = await ReceiverService.SearchReceiverEmailsAsync(_receiverDraftEmail); if (searchVersion != _receiverEmailSearchVersion) return; _receiverEmailSuggestions = results .Where(em => !string.IsNullOrWhiteSpace(em)) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(em => em) .Take(12) .ToList(); _selectedReceiverEmailSuggestion = _receiverEmailSuggestions.FirstOrDefault(em => string.Equals(em, _receiverDraftEmail, StringComparison.OrdinalIgnoreCase)); } catch (Exception ex) { Logger.LogWarning(ex, "Failed to load receiver email suggestions for {SearchTerm}", _receiverDraftEmail); if (searchVersion == _receiverEmailSearchVersion) { _receiverEmailSuggestions.Clear(); _selectedReceiverEmailSuggestion = null; } } finally { if (searchVersion == _receiverEmailSearchVersion) _isReceiverEmailSearchRunning = false; } } Task SaveReceiverAsync() { var fullName = _receiverDraftName.Trim(); var email = _receiverDraftEmail.Trim(); var phoneNumber = _receiverDraftPhoneNumber.Trim(); if (string.IsNullOrWhiteSpace(fullName)) { _receiverPopupValidationMessage = L("SenderEditor_Validation_FullNameRequired", "Bitte geben Sie einen Vor- und Nachnamen ein."); return Task.CompletedTask; } if (string.IsNullOrWhiteSpace(email)) { _receiverPopupValidationMessage = L("SenderEditor_Validation_EmailRequired", "Bitte geben Sie eine E-Mail-Adresse ein."); return Task.CompletedTask; } if (!ReceiverEmailValidator.IsValid(email)) { _receiverPopupValidationMessage = L("SenderEditor_Validation_EmailInvalid", "Bitte geben Sie eine gültige E-Mail-Adresse ein."); return Task.CompletedTask; } if (_receivers.Any(r => string.Equals(r.Email, email, StringComparison.OrdinalIgnoreCase))) { _receiverPopupValidationMessage = L("SenderEditor_Validation_EmailDuplicate", "Diese E-Mail-Adresse wurde bereits hinzugefügt."); return Task.CompletedTask; } _receivers.Add(new ReceiverDraft(Guid.NewGuid(), fullName, email, phoneNumber, ReceiverPalette[_receivers.Count % ReceiverPalette.Length])); CloseAddReceiverPopup(); return Task.CompletedTask; } // ── Models ── record SignatureFieldDraft(double XPt, double YPt, int Page, string ReceiverName, string Color); record NormalisedCoords(double NormX, double NormY, int PageIndex); record ReceiverDraft(Guid Id, string FullName, string Email, string PhoneNumber, string Color); // ── Receiver colour palette (cycles when > 8 receivers) ── static readonly string[] ReceiverPalette = [ "#4F46E5", // indigo "#059669", // emerald "#DC2626", // red "#D97706", // amber "#7C3AED", // violet "#0891B2", // cyan "#BE185D", // pink "#65A30D", // lime ]; string L(string key, string fallback) { var value = UiLocalizer[key]; return value.ResourceNotFound ? fallback : value.Value; } }