diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor index b163b89f..04915d66 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderEditorPage.razor @@ -3,9 +3,12 @@ @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.EnvelopeReceivers.Commands @using EnvelopeGenerator.Server.Client.Services @using EnvelopeGenerator.Server.Services +@using Microsoft.AspNetCore.WebUtilities @using Microsoft.AspNetCore.Components.Forms @using Microsoft.Extensions.Caching.Memory @inject IJSRuntime JSRuntime @@ -14,6 +17,8 @@ @inject ILogger Logger @inject EnvelopeReceiverPageDataService ReceiverPageDataService @inject EnvelopeReceiverService EnvelopeReceiverService +@inject EnvelopeService EnvelopeService +@inject IEnvelopeAuthService EnvelopeAuthService @inject IMemoryCache MemoryCache @@ -169,41 +174,55 @@ style="display: none;" /> - @if (_pdfLoaded) + @if (_pdfLoaded && _signatureFields.Count > 0) { - @* Clear all fields *@ - @if (_signatureFields.Count > 0) - { - - } - - @* Save *@ - } + + + + @@ -358,7 +377,7 @@ @* ── Save result popup (success + error) ── *@

- Umschlag wurde erfolgreich erstellt. + @_saveSuccessTitle

- Der Umschlag wurde gespeichert und die Empfänger wurden benachrichtigt. + @_saveSuccessBody

@@ -410,12 +429,21 @@
@if (_saveErrorMessage is null) { - + + @if (_saveNavigateToDashboard) + { + + } } else { @@ -434,6 +462,9 @@ [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 @@ -452,6 +483,9 @@ 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" @@ -460,6 +494,10 @@ 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; @@ -481,6 +519,7 @@ // ── Cache key helper ── string SessionKey => $"sender-editor:{Esid}"; + bool IsEditMode => EnvelopeId.HasValue && EnvelopeId.Value > 0; // ── Lifecycle ── @@ -500,12 +539,27 @@ if (string.IsNullOrWhiteSpace(Esid)) { var sid = Guid.NewGuid().ToString("N"); - NavigationManager.NavigateTo($"/sender/editor?esid={sid}", forceLoad: false); + var editQuery = EnvelopeId.HasValue && EnvelopeId.Value > 0 + ? $"&envelopeId={EnvelopeId.Value}" + : string.Empty; + NavigationManager.NavigateTo($"/sender/editor?esid={sid}{editQuery}", forceLoad: false); } } - protected override void OnParametersSet() + 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) @@ -525,6 +579,90 @@ } } + 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); + if (document.ByteData is null || document.ByteData.Length == 0) + throw new InvalidOperationException($"Document for envelope {envelopeId} is missing."); + + _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; + _pdfBytes = DrawPlaceholders(_originalPdfBytes, _signatureFields); + _pdfLoaded = true; + _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 = $"Fehler beim Laden des Umschlags #{envelopeId}: {ex.Message}"; + } + finally + { + _isLoadingEnvelopeForEdit = false; + } + } + // ── PDF upload ── async Task OnPdfFileSelectedAsync(InputFileChangeEventArgs e) { @@ -670,8 +808,48 @@ NavigationManager.NavigateTo("/sender"); } - // ── Save — POST /api/EnvelopeReceiver ── - async Task SaveAsync() + async Task SaveDraftAsync() + { + _isSaving = true; + _saveErrorMessage = null; + await InvokeAsync(StateHasChanged); + + try + { + var previousEnvelopeId = EnvelopeId; + var command = BuildSaveDraftCommand(); + Logger.LogInformation("[SenderEditor] Saving draft. EnvelopeId={EnvelopeId} IsEditMode={IsEditMode}", command.EnvelopeId, IsEditMode); + var result = await EnvelopeService.SaveDraftAsync(command); + + if (result is null) + throw new InvalidOperationException("Draft save did not return a result."); + + EnvelopeId = result.EnvelopeId; + _loadedEnvelopeId = result.EnvelopeId; + _editingEnvelopeId = result.EnvelopeId; + + if (previousEnvelopeId != result.EnvelopeId) + { + var sid = string.IsNullOrWhiteSpace(Esid) ? Guid.NewGuid().ToString("N") : Esid; + NavigationManager.NavigateTo($"/sender/editor?esid={sid}&envelopeId={result.EnvelopeId}", 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 — POST /api/EnvelopeReceiver ── + async Task SendAsync() { // ── Validation ── _titleTouched = true; @@ -763,6 +941,10 @@ // Success — show popup; GoToDashboard clears cache and navigates _saveErrorMessage = null; + _saveSuccessHeader = "Umschlag erstellt"; + _saveSuccessTitle = "Umschlag wurde erfolgreich erstellt."; + _saveSuccessBody = "Der Umschlag wurde gespeichert und die Empfänger wurden benachrichtigt."; + _saveNavigateToDashboard = true; _savePopupVisible = true; } catch (Exception ex) @@ -777,6 +959,70 @@ } } + SaveEnvelopeDraftCommand BuildSaveDraftCommand() + { + 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 SaveEnvelopeDraftFieldDto( + X: f.XPt / 72.0, + Y: f.YPt / 72.0, + Page: f.Page)) + .ToList(); + + return new SaveEnvelopeDraftReceiverDto( + EmailAddress: receiver.Email, + Name: receiver.FullName, + PhoneNumber: string.IsNullOrWhiteSpace(receiver.PhoneNumber) ? null : receiver.PhoneNumber, + Fields: fields); + }).ToList(); + + return new SaveEnvelopeDraftCommand + { + EnvelopeId = envelopeId, + Title = _envelopeTitle, + Message = _envelopeMessage, + TFAEnabled = false, + Document = string.IsNullOrWhiteSpace(docBase64) ? null : new SaveEnvelopeDraftDocumentDto(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() { diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor index 99532253..e2f9feb9 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor @@ -73,7 +73,7 @@ Neuer Umschlag -