Refactor the PDF upload button to dynamically adjust its appearance and behavior based on `_isPageReady` and `_isPdfUploadInProgress` states. Add a spinner to indicate upload progress and disable the `<InputFile>` component during uploads or when the page is not ready. Introduce `_isPdfUploadInProgress` state to prevent multiple uploads and ensure consistent UI updates. Update the `OnPdfFileSelectedAsync` method to handle this state, including a `finally` block to reset it and trigger UI updates. Consolidate redundant code for disabled states and improve conditional rendering for better maintainability. Enhance visual feedback for loading states and make minor style adjustments for consistency.
1239 lines
54 KiB
Plaintext
1239 lines
54 KiB
Plaintext
@page "/sender/editor2"
|
||
@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.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<SenderEditorPage> Logger
|
||
@inject DocumentService DocumentService
|
||
@inject PdfRenderService PdfRenderService
|
||
@inject ReceiverService ReceiverService
|
||
@inject EnvelopeService EnvelopeService
|
||
@inject IStringLocalizer<Resource> UiLocalizer
|
||
|
||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||
<link href="_content/DevExpress.Blazor.Viewer/css/dx-blazor-viewer-components.bs5.css" rel="stylesheet" />
|
||
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
||
<script src="@AppVersion.GetVersionedUrl("js/envelope-editor.js")"></script>
|
||
|
||
<div class="envelope-viewer-layout">
|
||
|
||
@* ── Action Bar ── *@
|
||
<div class="envelope-action-bar">
|
||
<div class="envelope-action-bar__inner"
|
||
style="flex-direction: row; align-items: center; padding: 0.35rem 1.5rem; gap: 0.75rem;">
|
||
|
||
@* Left: Title + meta inputs + receivers *@
|
||
<div style="flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: stretch; gap: 0.5rem;">
|
||
|
||
@* ── Title & Message inputs ── *@
|
||
<div style="display: flex; align-items: flex-start; gap: 0.5rem; flex-wrap: wrap;">
|
||
|
||
@* Titel *@
|
||
<div style="display: flex; flex-direction: column; gap: 0.2rem; flex: 1 1 180px; min-width: 140px;">
|
||
<label style="font-size: 0.65rem; font-weight: 600; color: #6b7280;
|
||
text-transform: uppercase; letter-spacing: 0.05em;">
|
||
@L("Title", "Titel") <span style="color:#dc2626;">*</span>
|
||
</label>
|
||
<input type="text"
|
||
value="@_envelopeTitle"
|
||
@oninput="e => _envelopeTitle = e.Value?.ToString() ?? string.Empty"
|
||
placeholder='@L("SenderEditor_Title_Placeholder", "z.B. Arbeitsvertrag 2025")'
|
||
maxlength="120"
|
||
style="font-size: 0.78rem; font-weight: 500; color: #1f2937;
|
||
border: 1px solid #d1d5db; border-radius: 5px;
|
||
padding: 0.3rem 0.55rem; outline: none; width: 100%;
|
||
background: #fff;
|
||
@(_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle) ? "border-color:#dc2626;" : "")" />
|
||
@if (_titleTouched && string.IsNullOrWhiteSpace(_envelopeTitle))
|
||
{
|
||
<span style="font-size:0.6rem;color:#dc2626;">@L("RequiredField", "Pflichtfeld")</span>
|
||
}
|
||
</div>
|
||
|
||
@* Nachricht *@
|
||
<div style="display: flex; flex-direction: column; gap: 0.2rem; flex: 2 1 260px; min-width: 180px;">
|
||
<label style="font-size: 0.65rem; font-weight: 600; color: #6b7280;
|
||
text-transform: uppercase; letter-spacing: 0.05em;">
|
||
@L("Message", "Nachricht") <span style="color:#6b7280;font-weight:400;">(@L("Optional", "optional"))</span>
|
||
</label>
|
||
<input type="text"
|
||
value="@_envelopeMessage"
|
||
@oninput="e => _envelopeMessage = e.Value?.ToString() ?? string.Empty"
|
||
placeholder='@L("SenderEditor_Message_Placeholder", "Bitte unterzeichnen Sie das beigefügte Dokument.")'
|
||
maxlength="500"
|
||
style="font-size: 0.78rem; color: #1f2937;
|
||
border: 1px solid #d1d5db; border-radius: 5px;
|
||
padding: 0.3rem 0.55rem; outline: none; width: 100%;
|
||
background: #fff;" />
|
||
</div>
|
||
|
||
@* PDF filename badge *@
|
||
@if (_pdfLoaded)
|
||
{
|
||
<div style="display:flex;align-items:flex-end;gap:0.4rem;padding-bottom:0.05rem;flex-shrink:0;">
|
||
<span style="font-size: 0.7rem; color: #6b7280;">@_fileName</span>
|
||
@if (_signatureFields.Count > 0)
|
||
{
|
||
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem;
|
||
background: #ede9fe; border-radius: 0.25rem; color: #6d28d9;
|
||
font-weight: 500; font-size: 0.7rem; white-space: nowrap;">
|
||
@_signatureFields.Count @L("SenderEditor_SignatureField", "Signaturfeld")@(_signatureFields.Count != 1 ? "er" : "")
|
||
</span>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<div class="sender-receivers-panel">
|
||
<div class="sender-receivers-panel__header">
|
||
<div class="sender-receivers-panel__title-wrap">
|
||
<span class="sender-receivers-panel__title">@L("Receiver", "Empfänger")</span>
|
||
<span class="sender-receivers-panel__count">@_receivers.Count</span>
|
||
</div>
|
||
|
||
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn"
|
||
@onclick="OpenAddReceiverPopup"
|
||
title='@L("AddReceiver", "Empfänger hinzufügen")'>
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M8 1a.5.5 0 0 1 .5.5v6h6a.5.5 0 0 1 0 1h-6v6a.5.5 0 0 1-1 0v-6h-6a.5.5 0 0 1 0-1h6v-6A.5.5 0 0 1 8 1z" />
|
||
</svg>
|
||
<span class="pdf-toolbar__btn-text">@L("AddReceiver", "Empfänger hinzufügen")</span>
|
||
</button>
|
||
</div>
|
||
|
||
@if (_receivers.Count == 0)
|
||
{
|
||
<div class="sender-receivers-panel__empty">
|
||
@L("SenderEditor_NoReceiversAdded", "Noch keine Empfänger hinzugefügt.")
|
||
</div>
|
||
}
|
||
else
|
||
{
|
||
<div class="sender-receivers-list">
|
||
@foreach (var receiver in _receivers)
|
||
{
|
||
<div class="sender-receiver-chip"
|
||
style="border-left: 3px solid @receiver.Color;">
|
||
<div class="sender-receiver-chip__body">
|
||
<div class="sender-receiver-chip__name">@receiver.FullName</div>
|
||
<div class="sender-receiver-chip__email">@receiver.Email</div>
|
||
@if (!string.IsNullOrWhiteSpace(receiver.PhoneNumber))
|
||
{
|
||
<div class="sender-receiver-chip__phone">@receiver.PhoneNumber</div>
|
||
}
|
||
</div>
|
||
|
||
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change sender-toolbar-action-btn sender-toolbar-action-btn--compact
|
||
@(_pendingReceiverForPlacement?.Id == receiver.Id ? "pdf-toolbar__btn--signature-change-active" : "")"
|
||
@onclick="() => ActivatePlacementForReceiver(receiver)"
|
||
style="@(_pendingReceiverForPlacement?.Id == receiver.Id ? $"background:{receiver.Color};color:#fff;border-color:{receiver.Color};" : $"color:{receiver.Color};border-color:{receiver.Color};")"
|
||
title='@L("SenderEditor_PlaceSignatureField", "Signaturfeld platzieren")'>
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10z" />
|
||
</svg>
|
||
<span class="pdf-toolbar__btn-text">@L("SenderEditor_AddSignature", "Signatur hinzufügen")</span>
|
||
</button>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
@* Right: Buttons *@
|
||
<div style="display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0;">
|
||
|
||
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
|
||
@onclick="Cancel"
|
||
title='@L("Cancel", "Abbrechen")'>
|
||
@L("Cancel", "Abbrechen")
|
||
</button>
|
||
|
||
@* PDF Upload *@
|
||
<label class="pdf-toolbar__btn"
|
||
title='@L("UploadPdf", "PDF hochladen")'
|
||
style="cursor: @((_isPageReady && !_isPdfUploadInProgress) ? "pointer" : "not-allowed");
|
||
opacity:@((_isPageReady && !_isPdfUploadInProgress) ? "1" : "0.6");
|
||
display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.3rem 0.75rem; font-size: 0.75rem; font-weight: 500;">
|
||
@if (_isPdfUploadInProgress)
|
||
{
|
||
<span class="spinner-border spinner-border-sm" role="status" style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
|
||
}
|
||
else
|
||
{
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z" />
|
||
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z" />
|
||
</svg>
|
||
}
|
||
@if (_isPdfUploadInProgress)
|
||
{
|
||
@L("UploadPdf", "PDF hochladen")
|
||
@:
|
||
@:...
|
||
}
|
||
else if (_isPageReady)
|
||
{
|
||
@L("UploadPdf", "PDF hochladen")
|
||
}
|
||
else
|
||
{
|
||
@L("Loading", "Wird geladen")
|
||
@:
|
||
@:...
|
||
}
|
||
<InputFile OnChange="OnPdfFileSelectedAsync"
|
||
accept=".pdf"
|
||
disabled="@(!_isPageReady || _isPdfUploadInProgress)"
|
||
style="display: none;" />
|
||
</label>
|
||
|
||
@if (_pdfLoaded && _signatureFields.Count > 0)
|
||
{
|
||
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
|
||
@onclick="ClearAllFieldsAsync"
|
||
title='@L("SenderEditor_RemoveAllSignatureFields", "Alle Signaturfelder entfernen")'>
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
|
||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1 0-2h3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1h3a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3h11V2h-11v1z" />
|
||
</svg>
|
||
</button>
|
||
}
|
||
|
||
<button class="pdf-toolbar__btn pdf-toolbar__btn--draft"
|
||
@onclick="SaveDraftAsync"
|
||
disabled="@_isSaving"
|
||
title='@L("Save", "Speichern")'>
|
||
@if (_isSaving)
|
||
{
|
||
<span class="spinner-border spinner-border-sm me-1" role="status"
|
||
style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
|
||
}
|
||
else
|
||
{
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||
<path d="M2 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V5.414A2 2 0 0 0 15.414 4L12 0.586A2 2 0 0 0 10.586 0H2zm9 1.5v2A1.5 1.5 0 0 1 9.5 7h-3A1.5 1.5 0 0 1 5 5.5v-4h5.586a1 1 0 0 1 .707.293L14.207 4.707A1 1 0 0 1 14.5 5.414V12a.5.5 0 0 1-.5.5H2a.5.5 0 0 1-.5-.5V4A.5.5 0 0 1 2 3.5h9z"/>
|
||
<path d="M5.5 1.5v4a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-4h-4zM4 9a1 1 0 0 0-1 1v3h10v-3a1 1 0 0 0-1-1H4z"/>
|
||
</svg>
|
||
}
|
||
<span class="pdf-toolbar__btn-text">@L("Save", "Speichern")</span>
|
||
</button>
|
||
|
||
<button class="pdf-toolbar__btn pdf-toolbar__btn--success"
|
||
@onclick="SendAsync"
|
||
disabled="@_isSaving"
|
||
title='@L("SendEnvelope", "Umschlag senden")'
|
||
style="background: linear-gradient(135deg,#059669,#047857); color:#fff; border-radius:6px; padding:0.3rem 0.75rem; font-size:0.75rem; font-weight:600; border:none;">
|
||
@if (_isSaving)
|
||
{
|
||
<span class="spinner-border spinner-border-sm me-1" role="status"
|
||
style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
|
||
}
|
||
else
|
||
{
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
||
</svg>
|
||
}
|
||
@L("Send", "Senden")
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
@* Placement mode hint bar *@
|
||
@if (_pendingReceiverForPlacement is not null)
|
||
{
|
||
<div style="background: @_pendingReceiverForPlacement.Color; color: white; font-size: 0.75rem; font-weight: 500;
|
||
padding: 0.3rem 1.5rem; text-align: center; letter-spacing: 0.01em;">
|
||
📌 @L("SenderEditor_ClickDocumentFor", "Klicken Sie auf die Stelle im Dokument für") <strong>@_pendingReceiverForPlacement.FullName</strong>.
|
||
<button @onclick="CancelPlacement"
|
||
style="background:none;border:none;color:white;cursor:pointer;font-size:0.75rem;text-decoration:underline;padding:0;">
|
||
@L("Cancel", "Abbrechen")
|
||
</button>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
@* ── Content ── *@
|
||
<div class="envelope-content" style="padding: 0; overflow: hidden;">
|
||
@if (!_pdfLoaded)
|
||
{
|
||
@* Empty state *@
|
||
<div class="d-flex justify-content-center align-items-center h-100">
|
||
<div class="text-center" style="max-width: 420px;">
|
||
<div style="width: 72px; height: 72px; background: rgba(255,255,255,0.15);
|
||
border-radius: 50%; display: flex; align-items: center;
|
||
justify-content: center; margin: 0 auto 1.25rem;">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="white" viewBox="0 0 16 16">
|
||
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z" />
|
||
</svg>
|
||
</div>
|
||
<h5 class="text-white mb-2">@L("SenderEditor_NoDocumentLoaded", "Kein Dokument geladen")</h5>
|
||
<p class="text-white mb-4" style="opacity: 0.75; font-size: 0.85rem;">
|
||
@L("SenderEditor_UploadPdfToPlaceFields", "Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren.")
|
||
</p>
|
||
<label class="btn btn-light btn-sm"
|
||
style="cursor: @((_isPageReady && !_isPdfUploadInProgress) ? "pointer" : "not-allowed");
|
||
opacity:@((_isPageReady && !_isPdfUploadInProgress) ? "1" : "0.8");
|
||
font-weight: 500;">
|
||
@if (_isPdfUploadInProgress)
|
||
{
|
||
<span class="spinner-border spinner-border-sm me-1" role="status" style="width:0.8rem;height:0.8rem;border-width:2px;"></span>
|
||
}
|
||
else
|
||
{
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z" />
|
||
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z" />
|
||
</svg>
|
||
}
|
||
@if (_isPdfUploadInProgress)
|
||
{
|
||
@L("UploadPdf", "PDF hochladen")
|
||
@:
|
||
@:...
|
||
}
|
||
else if (_isPageReady)
|
||
{
|
||
@L("UploadPdf", "PDF hochladen")
|
||
}
|
||
else
|
||
{
|
||
@L("Loading", "Wird geladen")
|
||
@:
|
||
@:...
|
||
}
|
||
<InputFile OnChange="OnPdfFileSelectedAsync"
|
||
accept=".pdf"
|
||
disabled="@(!_isPageReady || _isPdfUploadInProgress)"
|
||
style="display: none;" />
|
||
</label>
|
||
</div>
|
||
</div>
|
||
}
|
||
else if (_errorMessage is not null)
|
||
{
|
||
<div class="error-container">
|
||
<div class="alert alert-danger shadow-lg m-4">
|
||
<strong>@L("Error", "Fehler"):</strong> @_errorMessage
|
||
</div>
|
||
</div>
|
||
}
|
||
else
|
||
{
|
||
<SenderPdfImageEditor IsPlacementMode="@(_pendingReceiverForPlacement is not null)"
|
||
PageCount="@_pageCount"
|
||
Pages="@_pagePreviews"
|
||
PdfDataUrl="@PdfDataUrl"
|
||
Fields="@FieldOverlays"
|
||
EditorClick="OnPdfAreaClickAsync"
|
||
RemoveField="OnRemoveFieldByIndexAsync" />
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<DxPopup @bind-Visible="_receiverPopupVisible"
|
||
HeaderText='@L("AddReceiver", "Empfänger hinzufügen")'
|
||
ShowFooter="true"
|
||
CloseOnEscape="true"
|
||
Width="720px"
|
||
CssClass="sender-receiver-popup">
|
||
<BodyContentTemplate Context="popupContext">
|
||
<div class="sender-receiver-popup__body">
|
||
<div class="sender-receiver-popup__form-grid">
|
||
<div class="sender-receiver-popup__field">
|
||
<div class="sender-receiver-popup__label">@L("FullName", "Vor- und Nachname")</div>
|
||
<DxTextBox Text="@_receiverDraftName"
|
||
TextChanged="OnReceiverNameChanged"
|
||
NullText="Max Mustermann"
|
||
ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto"
|
||
BindValueMode="BindValueMode.OnInput"
|
||
CssClass="w-100" />
|
||
</div>
|
||
|
||
<div class="sender-receiver-popup__field">
|
||
<div class="sender-receiver-popup__label">@L("PhoneNumber", "Telefonnummer") (@L("Optional", "optional"))</div>
|
||
<DxTextBox Text="@_receiverDraftPhoneNumber"
|
||
TextChanged="OnReceiverPhoneNumberChanged"
|
||
NullText="+49 170 1234567"
|
||
ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto"
|
||
BindValueMode="BindValueMode.OnInput"
|
||
CssClass="w-100" />
|
||
</div>
|
||
|
||
<div class="sender-receiver-popup__field">
|
||
<div class="sender-receiver-popup__label">@L("EmailAddress", "E-Mail-Adresse")</div>
|
||
<div @onkeydown="OnReceiverEmailKeyDownAsync">
|
||
<DxTextBox Text="@_receiverDraftEmail"
|
||
TextChanged="OnReceiverEmailTextChangedAsync"
|
||
NullText="name@beispiel.de"
|
||
ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto"
|
||
BindValueMode="BindValueMode.OnInput"
|
||
CssClass="w-100" />
|
||
</div>
|
||
|
||
<div class="sender-receiver-popup__suggestions-shell">
|
||
@if (_receiverEmailSuggestions.Count > 0)
|
||
{
|
||
<div class="sender-receiver-popup__suggestions">
|
||
<DxListBox TData="string"
|
||
TValue="string"
|
||
Data="@_receiverEmailSuggestions"
|
||
Value="@_selectedReceiverEmailSuggestion"
|
||
ValueChanged="OnReceiverEmailSuggestionCommittedAsync"
|
||
SelectionMode="ListBoxSelectionMode.Single"
|
||
CssClass="w-100" />
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sender-receiver-popup__hint">
|
||
@L("SenderEditor_EmailSuggestionsHint", "Bereits verwendete E-Mail-Adressen werden bei der Eingabe vorgeschlagen.")
|
||
</div>
|
||
|
||
@if (_isReceiverEmailSearchRunning)
|
||
{
|
||
<div class="sender-receiver-popup__loading">@L("SenderEditor_EmailSuggestionsLoading", "E-Mail-Vorschläge werden geladen…")</div>
|
||
}
|
||
|
||
@if (!string.IsNullOrWhiteSpace(_receiverPopupValidationMessage))
|
||
{
|
||
<div class="sender-receiver-popup__validation">@_receiverPopupValidationMessage</div>
|
||
}
|
||
</div>
|
||
</BodyContentTemplate>
|
||
<FooterContentTemplate>
|
||
<div class="sender-receiver-popup__footer">
|
||
<DxButton Text='@L("Cancel", "Abbrechen")'
|
||
RenderStyle="ButtonRenderStyle.Secondary"
|
||
Click="CloseAddReceiverPopup" />
|
||
<DxButton Text='@L("AddReceiver", "Empfänger hinzufügen")'
|
||
RenderStyle="ButtonRenderStyle.Primary"
|
||
Click="SaveReceiverAsync" />
|
||
</div>
|
||
</FooterContentTemplate>
|
||
</DxPopup>
|
||
|
||
@* ── Save result popup (success + error) ── *@
|
||
<DxPopup @bind-Visible="_savePopupVisible"
|
||
HeaderText='@(_saveErrorMessage is null ? _saveSuccessHeader : L("Error", "Fehler"))'
|
||
Width="460px"
|
||
MaxWidth="95vw"
|
||
ShowFooter="true"
|
||
CloseOnOutsideClick="false"
|
||
ShowCloseButton="false"
|
||
CloseOnEscape="false">
|
||
<BodyContentTemplate>
|
||
@if (_saveErrorMessage is null)
|
||
{
|
||
<div style="display:flex;align-items:flex-start;gap:1rem;padding:0.5rem 0;">
|
||
<div style="flex-shrink:0;width:40px;height:40px;background:#d1fae5;border-radius:50%;
|
||
display:flex;align-items:center;justify-content:center;">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#065f46" viewBox="0 0 16 16">
|
||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<p style="margin:0 0 0.4rem;font-weight:600;color:#1f2937;font-size:0.95rem;">
|
||
@_saveSuccessTitle
|
||
</p>
|
||
<p style="margin:0;color:#6b7280;font-size:0.85rem;line-height:1.5;">
|
||
@_saveSuccessBody
|
||
</p>
|
||
</div>
|
||
</div>
|
||
}
|
||
else
|
||
{
|
||
<div style="display:flex;align-items:flex-start;gap:1rem;padding:0.5rem 0;">
|
||
<div style="flex-shrink:0;width:40px;height:40px;background:#fee2e2;border-radius:50%;
|
||
display:flex;align-items:center;justify-content:center;">
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#991b1b" viewBox="0 0 16 16">
|
||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<p style="margin:0 0 0.4rem;font-weight:600;color:#1f2937;font-size:0.95rem;">
|
||
@L("SenderEditor_ErrorCreatingEnvelope", "Fehler beim Erstellen des Umschlags")
|
||
</p>
|
||
<p style="margin:0;color:#6b7280;font-size:0.85rem;line-height:1.5;word-break:break-word;">
|
||
@_saveErrorMessage
|
||
</p>
|
||
</div>
|
||
</div>
|
||
}
|
||
</BodyContentTemplate>
|
||
<FooterContentTemplate>
|
||
<div style="display:flex;gap:0.5rem;justify-content:flex-end;width:100%;padding:0.5rem 0;">
|
||
@if (_saveErrorMessage is null)
|
||
{
|
||
<button class="btn btn-outline-secondary"
|
||
@onclick="() => _savePopupVisible = false"
|
||
style="border-radius:6px;padding:0.5rem 1.25rem;font-weight:500;">
|
||
@L("Close", "Schließen")
|
||
</button>
|
||
|
||
@if (_saveNavigateToDashboard)
|
||
{
|
||
<button class="btn btn-primary"
|
||
@onclick="GoToDashboard"
|
||
style="background:linear-gradient(135deg,#059669,#047857);border:none;border-radius:6px;
|
||
padding:0.5rem 1.5rem;font-weight:600;color:#fff;">
|
||
@L("SenderEditor_ToOverview", "Zur Übersicht")
|
||
</button>
|
||
}
|
||
}
|
||
else
|
||
{
|
||
<button class="btn btn-outline-secondary"
|
||
@onclick="() => _savePopupVisible = false"
|
||
style="border-radius:6px;padding:0.5rem 1.25rem;font-weight:500;">
|
||
@L("Close", "Schließen")
|
||
</button>
|
||
}
|
||
</div>
|
||
</FooterContentTemplate>
|
||
</DxPopup>
|
||
|
||
@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<PdfPageImageDto> _pagePreviews = [];
|
||
int? _loadedEnvelopeId;
|
||
int? _editingEnvelopeId;
|
||
bool _isLoadingEnvelopeForEdit;
|
||
|
||
List<SignatureFieldDraft> _signatureFields = [];
|
||
ReceiverDraft? _pendingReceiverForPlacement; // Set when user clicks "Signatur hinzufügen"
|
||
|
||
IReadOnlyList<SenderPdfFieldOverlay> 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<ReceiverDraft> _receivers = [];
|
||
bool _receiverPopupVisible;
|
||
string _receiverDraftName = string.Empty;
|
||
string _receiverDraftEmail = string.Empty;
|
||
string _receiverDraftPhoneNumber = string.Empty;
|
||
string? _selectedReceiverEmailSuggestion;
|
||
string? _receiverPopupValidationMessage;
|
||
bool _isReceiverEmailSearchRunning;
|
||
List<string> _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<DocReceiverElementDto>())
|
||
.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<NormalisedCoords?>(
|
||
"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;
|
||
}
|
||
|
||
// ── 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;
|
||
}
|
||
}
|