From 3b367eff7face4874cb95f7b8804f26da38f2c29 Mon Sep 17 00:00:00 2001 From: TekH Date: Fri, 2 Oct 2026 13:39:00 +0200 Subject: [PATCH] Remove `EnvelopeSenderEditorPage.razor` entirely The `EnvelopeSenderEditorPage.razor` file was completely removed, including all Razor markup, Blazor components, JavaScript references, CSS styles, and C# code-behind logic. Key changes: - Removed the `/sender/editor_legacy` route and all associated imports (`@using`) and dependency injections (`@inject`). - Deleted the entire HTML structure, including UI elements for PDF editing, signature placement, and receiver management. - Removed all references to external CSS and JavaScript files. - Deleted Blazor components like `DxPdfViewer`, `DxPopup`, and `DxTextBox`. - Removed C# code for state management, lifecycle methods, event handlers, and helper methods. - Eliminated localization, validation logic, session persistence, and cache management. - Removed PDF manipulation logic (e.g., placeholder rendering with `PdfSharp`). - Deleted receiver management logic, including email suggestions and validation. This change removes the entire functionality of the `EnvelopeSenderEditorPage`, including its UI, backend logic, and external integrations. --- .../Pages/EnvelopeSenderEditorPage.razor | 1292 ----------------- 1 file changed, 1292 deletions(-) delete mode 100644 EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor deleted file mode 100644 index e2d0199c..00000000 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor +++ /dev/null @@ -1,1292 +0,0 @@ -@page "/sender/editor_legacy" -@rendermode InteractiveServer -@using DevExpress.Blazor.PdfViewer -@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.Services -@using EnvelopeGenerator.Server.Services -@using Microsoft.AspNetCore.WebUtilities -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.Extensions.Caching.Memory -@using Microsoft.Extensions.Localization -@inject IJSRuntime JSRuntime -@inject NavigationManager NavigationManager -@inject AppVersionService AppVersion -@inject ILogger Logger -@inject EnvelopeReceiverPageDataService ReceiverPageDataService -@inject EnvelopeService EnvelopeService -@inject IEnvelopeAuthService EnvelopeAuthService -@inject IMemoryCache MemoryCache -@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 - { - @* PDF viewer — click capture active only in placement mode *@ -
- -
- } -
-
- - - -
-
-
-
@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 { - // ── Session query param — persists across SignalR reconnects ── - [SupplyParameterFromQuery(Name = "esid")] - public string? Esid { get; set; } - - [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 DxPdfViewer — used by JS to locate page elements - const string ViewerCssClass = "sender-editor-pdf-viewer"; - - // Cache TTL for editor session (30 min of inactivity) - static readonly TimeSpan SessionTtl = TimeSpan.FromMinutes(30); - - // ── State ── - DxPdfViewer? _pdfViewer; - bool _pdfLoaded = false; - string _fileName = string.Empty; - string? _errorMessage; - byte[]? _pdfBytes; // Current rendered PDF (original + placeholders burned in) - byte[]? _originalPdfBytes; // Pristine upload — never modified, used as base for redraw - int? _loadedEnvelopeId; - int? _editingEnvelopeId; - bool _isLoadingEnvelopeForEdit; - - List _signatureFields = []; - ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen" - - // ── 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(); - - // ── Cache key helper ── - string SessionKey => $"sender-editor:{Esid}"; - bool IsEditMode => EnvelopeId.HasValue && EnvelopeId.Value > 0; - - // ── Lifecycle ── - - protected override void OnInitialized() - { - // Intentionally empty. - // esid redirect is done in OnAfterRenderAsync to avoid NavigationException - // during SSR prerendering, where NavigateTo throws by design. - } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (!firstRender) return; - - // Generate a session id on first interactive render and push it into the URL. - // At this point SignalR is connected and NavigateTo is safe. - if (string.IsNullOrWhiteSpace(Esid)) - { - var sid = Guid.NewGuid().ToString("N"); - var editQuery = EnvelopeId.HasValue && EnvelopeId.Value > 0 - ? $"&envelopeId={EnvelopeId.Value}" - : string.Empty; - NavigationManager.NavigateTo($"/sender/editor_legacy?esid={sid}{editQuery}", forceLoad: false); - } - } - - 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; - - // After the esid is set via query param, try to restore from cache. - if (!string.IsNullOrWhiteSpace(Esid) - && MemoryCache.TryGetValue(SessionKey, out EditorSessionData? cached) - && cached is not null) - { - _originalPdfBytes = cached.OriginalPdfBytes; - _signatureFields = cached.Fields; - _fileName = cached.FileName; - _pdfLoaded = _originalPdfBytes is { Length: > 0 }; - _receivers = cached.Receivers; - _envelopeTitle = cached.Title; - _envelopeMessage = cached.Message; - - // Redraw placeholders onto the original PDF - if (_pdfLoaded) - _pdfBytes = DrawPlaceholders(_originalPdfBytes!, _signatureFields); - } - } - - async Task LoadEnvelopeForEditAsync(int envelopeId) - { - if (_isLoadingEnvelopeForEdit) - return; - - _isLoadingEnvelopeForEdit = true; - _errorMessage = null; - - try - { - var user = EnvelopeAuthService.GetCurrentUser(); - if (user?.Identity is null || !user.Identity.IsAuthenticated) - throw new InvalidOperationException("Sender is not authenticated."); - - var envelopes = await EnvelopeService.GetAsync(id: envelopeId); - var envelope = envelopes?.FirstOrDefault() - ?? throw new InvalidOperationException($"Envelope {envelopeId} not found."); - - var document = await ReceiverPageDataService.GetDocumentDetailsOfSenderAsync(user, 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 }; - _pdfBytes = _pdfLoaded - ? DrawPlaceholders(_originalPdfBytes!, _signatureFields) - : null; - _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) - { - _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 - { - 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; - _signatureFields.Clear(); - _pendingReceiverForPlacement = null; - - // Rendered PDF starts as the clean original (no placeholders yet) - _pdfBytes = _originalPdfBytes; - - PersistSession(); - - 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"); - } - } - - // ── 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; - } - - // Read page dimensions from the original PDF via PdfSharp - double pageWidthPt; - double pageHeightPt; - try - { - using var ms = new System.IO.MemoryStream(_originalPdfBytes); - var doc = PdfSharp.Pdf.IO.PdfReader.Open(ms, PdfSharp.Pdf.IO.PdfDocumentOpenMode.Import); - int pageIndex = Math.Max(0, Math.Min(coords.PageIndex, doc.PageCount - 1)); - var page = doc.Pages[pageIndex]; - pageWidthPt = page.Width.Point; - pageHeightPt = page.Height.Point; - } - catch (Exception ex) - { - Logger.LogError(ex, "[SenderEditor] Failed to read page dimensions from PDF"); - return; - } - - // 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)); - - int page1Based = coords.PageIndex + 1; - - var field = new SignatureFieldDraft( - XPt: xPt, - YPt: yPt, - Page: page1Based, - ReceiverName: _pendingReceiverForPlacement.FullName, - Color: _pendingReceiverForPlacement.Color); - - _signatureFields.Add(field); - _pendingReceiverForPlacement = null; - - // Burn all placeholders onto the original PDF and update the viewer - _pdfBytes = DrawPlaceholders(_originalPdfBytes, _signatureFields); - PersistSession(); - - 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 RemoveFieldAsync(SignatureFieldDraft field) - { - _signatureFields.Remove(field); - _pdfBytes = _originalPdfBytes is not null - ? DrawPlaceholders(_originalPdfBytes, _signatureFields) - : _pdfBytes; - PersistSession(); - await Task.CompletedTask; - } - - // ── Clear all fields ── - async Task ClearAllFieldsAsync() - { - _signatureFields.Clear(); - _pendingReceiverForPlacement = null; - _pdfBytes = _originalPdfBytes; - PersistSession(); - await Task.CompletedTask; - } - - void Cancel() => NavigationManager.NavigateTo("/sender"); - - void GoToDashboard() - { - // Clear session from cache on successful save so it is not restored if user returns - if (!string.IsNullOrWhiteSpace(Esid)) - MemoryCache.Remove(SessionKey); - - 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) - { - var sid = string.IsNullOrWhiteSpace(Esid) ? Guid.NewGuid().ToString("N") : Esid; - NavigationManager.NavigateTo($"/sender/editor_legacy?esid={sid}&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; - } - } - - // ── Cache persistence ── - void PersistSession() - { - if (string.IsNullOrWhiteSpace(Esid)) return; - - var data = new EditorSessionData( - OriginalPdfBytes: _originalPdfBytes ?? [], - Fields: [.. _signatureFields], - FileName: _fileName, - Receivers: [.. _receivers], - Title: _envelopeTitle, - Message: _envelopeMessage); - - MemoryCache.Set(SessionKey, data, SessionTtl); - } - - // ── PdfSharp: burn all placeholder boxes onto the original PDF ── - static byte[] DrawPlaceholders(byte[] originalPdf, IReadOnlyList fields) - { - if (fields.Count == 0) return originalPdf; - - using var inputMs = new System.IO.MemoryStream(originalPdf); - using var outputMs = new System.IO.MemoryStream(); - - var document = PdfSharp.Pdf.IO.PdfReader.Open( - inputMs, PdfSharp.Pdf.IO.PdfDocumentOpenMode.Modify); - - foreach (var field in fields) - { - int pageIndex = field.Page - 1; - if (pageIndex < 0 || pageIndex >= document.PageCount) continue; - - var page = document.Pages[pageIndex]; - using var gfx = PdfSharp.Drawing.XGraphics.FromPdfPage(page); - - // Derive colours from the receiver's hex colour - var fillBrush = new PdfSharp.Drawing.XSolidBrush(HexToXColor(field.Color, alpha: 35)); - var borderPen = new PdfSharp.Drawing.XPen(HexToXColor(field.Color, alpha: 210), 1.5); - var textBrush = new PdfSharp.Drawing.XSolidBrush(HexToXColor(field.Color, alpha: 200)); - var nameBrush = new PdfSharp.Drawing.XSolidBrush(HexToXColor(field.Color, alpha: 230)); - - var fontLabel = new PdfSharp.Drawing.XFont("Arial", 9, PdfSharp.Drawing.XFontStyleEx.Bold); - var fontName = new PdfSharp.Drawing.XFont("Arial", 7, PdfSharp.Drawing.XFontStyleEx.Regular); - - var fmtCenter = new PdfSharp.Drawing.XStringFormat - { - Alignment = PdfSharp.Drawing.XStringAlignment.Center, - LineAlignment = PdfSharp.Drawing.XLineAlignment.Center, - }; - - var rect = new PdfSharp.Drawing.XRect(field.XPt, field.YPt, SigWidthPt, SigHeightPt); - - gfx.DrawRectangle(fillBrush, rect); - gfx.DrawRectangle(borderPen, rect); - - // "UNTERSCHRIFT" label centred in upper two-thirds - var labelRect = new PdfSharp.Drawing.XRect( - field.XPt, field.YPt, SigWidthPt, SigHeightPt * 0.65); - gfx.DrawString("UNTERSCHRIFT", fontLabel, textBrush, labelRect, fmtCenter); - - // Receiver name centred in lower third - var nameRect = new PdfSharp.Drawing.XRect( - field.XPt + 4, field.YPt + SigHeightPt * 0.68, - SigWidthPt - 8, SigHeightPt * 0.30); - var displayName = field.ReceiverName.Length > 22 - ? field.ReceiverName[..19] + "..." - : field.ReceiverName; - gfx.DrawString(displayName, fontName, nameBrush, nameRect, fmtCenter); - } - - document.Save(outputMs); - return outputMs.ToArray(); - } - - /// Converts a CSS hex colour string (e.g. "#4F46E5") to a PdfSharp XColor. - static PdfSharp.Drawing.XColor HexToXColor(string hex, int alpha) - { - var c = System.Drawing.ColorTranslator.FromHtml(hex); - return PdfSharp.Drawing.XColor.FromArgb(alpha, c.R, c.G, c.B); - } - - // ── 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 ReceiverPageDataService.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])); - PersistSession(); - 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); - - record EditorSessionData( - byte[] OriginalPdfBytes, - List Fields, - string FileName, - List Receivers, - string Title, - string Message); - - // ── 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; - } -}