Compare commits
25 Commits
refactor/w
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 89a4e789f1 | |||
| 94658b0d9b | |||
| 6ad76fa4b0 | |||
| 31933f5568 | |||
| 47c4f33dfc | |||
| 26d41d9178 | |||
| 238c938ce7 | |||
| 5fb7b5a79e | |||
| 2c8b65d576 | |||
| e1f24249a9 | |||
| f6dc454193 | |||
| b65098fafb | |||
| c6fe7722b4 | |||
| 2065102fb3 | |||
| 858b748e65 | |||
| 25c03b350b | |||
| ed9ea0ac54 | |||
| 18c1adb68a | |||
| ec8b9adb9c | |||
| 4a10db0865 | |||
| ceb2860883 | |||
| 11e72bac53 | |||
| 638b734648 | |||
| eec1b4926b | |||
| d7611a6b59 |
@@ -287,6 +287,7 @@ Manual testing workflow:
|
||||
- Audit `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/` for unnecessary request/payload remapping and remove redundant mappings where direct request DTO usage is safe; review client HTTP methods end-to-end for similar avoidable transformation layers.
|
||||
- Centralize dependency injection registrations into dedicated extension methods/modules (especially server + client service registrations in `Program.cs`) to reduce startup composition sprawl and improve maintainability.
|
||||
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfRenderController.cs` + matching client service contract to remove JSON/base64 `byte[]` transport for PDF rendering; prefer binary streaming (`application/octet-stream` or multipart), avoid `data:` URL bloat where possible, and validate end-to-end memory/latency behavior with large PDFs.
|
||||
- Move sender PDF image editor initial zoom from hardcoded value to Options pattern configuration (bind from `appsettings.json` and inject via `IOptions`/`IOptionsMonitor`) so default zoom (for example `%180`) is environment-configurable.
|
||||
- Establish a DevExpress toast-based notification architecture using `DxToastProvider` + `IToastNotificationService` (tab/page-scoped provider strategy, standardized `ToastOptions` factory, and severity-to-style mapping) so sender/receiver flows show consistent, non-duplicated, actionable feedback.
|
||||
- Eliminate manual property-by-property request→domain mapping drift (example: `CreateEnvelopeCommandHandler`) by introducing a standardized mapping policy (AutoMapper/profile conventions or reflection/source-generator based mapper) and enforcing it across envelope workflows; this needs to be solved in the shared company Core library level so all services get the same guardrails by default.
|
||||
- P1 security workstream: remove dynamic SQL string composition/interpolation from active save/create/update paths (`string.Format`, `$"...{input}..."`, `ToSqlParam`) and migrate to parameterized queries only; treat this as a potential SQL injection vulnerability (stability + security risk), execute repo-wide audit, and add CI guardrails to block reintroduction.
|
||||
|
||||
@@ -622,10 +622,10 @@
|
||||
<value>Zertifizierungstyp</value>
|
||||
</data>
|
||||
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||
<value>Finale E-Mail an Ersteller</value>
|
||||
<value>Abgeschlossenes Dokument an Ersteller</value>
|
||||
</data>
|
||||
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||
<value>Finale E-Mail an Empfänger</value>
|
||||
<value>Abgeschlossenes Dokument an Empfänger</value>
|
||||
</data>
|
||||
<data name="SendReminderEmails" xml:space="preserve">
|
||||
<value>Erinnerungs-E-Mails senden</value>
|
||||
@@ -749,7 +749,14 @@
|
||||
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Seite</value></data>
|
||||
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Signaturfeld platzieren</value></data>
|
||||
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Alle Signaturfelder entfernen</value></data>
|
||||
<data name="SenderEditor_RemoveAllSignatureFieldsConfirm" xml:space="preserve"><value>Diese Aktion entfernt alle platzierten Signaturfelder unwiderruflich. Fortfahren?</value></data>
|
||||
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Bitte wählen Sie eine PDF-Datei aus.</value></data>
|
||||
<data name="SenderEditor_SendConfirm_Message" xml:space="preserve"><value>Wollen Sie den Umschlag jetzt versenden? Hinweis: Nach der Übergabe kann der Vorgang nicht mehr bearbeitet werden.</value></data>
|
||||
<data name="SenderEditor_SendConfirm_Title" xml:space="preserve"><value>Umschlag jetzt versenden?</value></data>
|
||||
<data name="SenderEditor_SendConfirm_Note" xml:space="preserve"><value>Bitte prüfen Sie Empfänger, Signaturfelder und Einstellungen vor dem Versenden.</value></data>
|
||||
<data name="SenderEditor_SendConfirm_Action" xml:space="preserve"><value>Jetzt versenden</value></data>
|
||||
<data name="SenderEditor_SendConfirm_InProgress" xml:space="preserve"><value>Umschlag wird versendet...</value></data>
|
||||
<data name="SenderEditor_SendConfirm_InProgressHint" xml:space="preserve"><value>Bitte warten Sie einen Moment. Der Vorgang wird abgeschlossen.</value></data>
|
||||
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>Der bestehende Entwurf wurde versendet.</value></data>
|
||||
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Umschlag gesendet</value></data>
|
||||
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>Umschlag wurde erfolgreich gesendet.</value></data>
|
||||
@@ -777,6 +784,10 @@
|
||||
<data name="Signed" xml:space="preserve"><value>Unterschrieben</value></data>
|
||||
<data name="SignedDocument" xml:space="preserve"><value>Signiertes Dokument</value></data>
|
||||
<data name="System" xml:space="preserve"><value>System</value></data>
|
||||
<data name="SenderWizard_Step_Start" xml:space="preserve"><value>Start</value></data>
|
||||
<data name="SenderWizard_DocumentUploadSimpleHint" xml:space="preserve"><value>Dokument hinzufuegen: klicken oder per Drag-and-Drop ablegen</value></data>
|
||||
<data name="SenderWizard_DocumentReplaceHint" xml:space="preserve"><value>Zum Ersetzen klicken oder eine Datei auf diese Flaeche ziehen.</value></data>
|
||||
<data name="SenderWizard_FinalUploadHint" xml:space="preserve"><value>Letzte Moeglichkeit im Wizard: Bitte laden Sie jetzt Ihr PDF ueber den rechten Upload-Bereich hoch. Im Editor koennen Sie spaeter ebenfalls ein Dokument hochladen oder ersetzen.</value></data>
|
||||
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</value></data>
|
||||
<data name="UploadPdf" xml:space="preserve"><value>PDF hochladen</value></data>
|
||||
<data name="UserName" xml:space="preserve"><value>Benutzername</value></data>
|
||||
|
||||
@@ -622,10 +622,10 @@
|
||||
<value>Certification Type</value>
|
||||
</data>
|
||||
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||
<value>Final Email to Creator</value>
|
||||
<value>Completed Document to Creator</value>
|
||||
</data>
|
||||
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||
<value>Final Email to Receivers</value>
|
||||
<value>Completed Document to Receivers</value>
|
||||
</data>
|
||||
<data name="SendReminderEmails" xml:space="preserve">
|
||||
<value>Send Reminder Emails</value>
|
||||
@@ -749,7 +749,14 @@
|
||||
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Page</value></data>
|
||||
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Place signature field</value></data>
|
||||
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Remove all signature fields</value></data>
|
||||
<data name="SenderEditor_RemoveAllSignatureFieldsConfirm" xml:space="preserve"><value>This action will permanently remove all placed signature fields. Continue?</value></data>
|
||||
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Please select a PDF file.</value></data>
|
||||
<data name="SenderEditor_SendConfirm_Message" xml:space="preserve"><value>Do you want to send the envelope now? Note: After handover, the process can no longer be edited.</value></data>
|
||||
<data name="SenderEditor_SendConfirm_Title" xml:space="preserve"><value>Send envelope now?</value></data>
|
||||
<data name="SenderEditor_SendConfirm_Note" xml:space="preserve"><value>Please review recipients, signature fields, and settings before sending.</value></data>
|
||||
<data name="SenderEditor_SendConfirm_Action" xml:space="preserve"><value>Send now</value></data>
|
||||
<data name="SenderEditor_SendConfirm_InProgress" xml:space="preserve"><value>Sending envelope...</value></data>
|
||||
<data name="SenderEditor_SendConfirm_InProgressHint" xml:space="preserve"><value>Please wait a moment while we complete the process.</value></data>
|
||||
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>The existing draft has been sent.</value></data>
|
||||
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Envelope sent</value></data>
|
||||
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>Envelope was sent successfully.</value></data>
|
||||
@@ -777,6 +784,10 @@
|
||||
<data name="Signed" xml:space="preserve"><value>Signed</value></data>
|
||||
<data name="SignedDocument" xml:space="preserve"><value>Signed document</value></data>
|
||||
<data name="System" xml:space="preserve"><value>System</value></data>
|
||||
<data name="SenderWizard_Step_Start" xml:space="preserve"><value>Start</value></data>
|
||||
<data name="SenderWizard_DocumentUploadSimpleHint" xml:space="preserve"><value>Add document: click or drag and drop</value></data>
|
||||
<data name="SenderWizard_DocumentReplaceHint" xml:space="preserve"><value>Click to replace or drag a file onto this area.</value></data>
|
||||
<data name="SenderWizard_FinalUploadHint" xml:space="preserve"><value>Last chance in the wizard: please upload your PDF now using the upload area on the right. In the editor, you can still upload or replace a document later.</value></data>
|
||||
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>An unexpected error occurred. Please try again later.</value></data>
|
||||
<data name="UploadPdf" xml:space="preserve"><value>Upload PDF</value></data>
|
||||
<data name="UserName" xml:space="preserve"><value>Username</value></data>
|
||||
|
||||
@@ -622,10 +622,10 @@
|
||||
<value>Type de certification</value>
|
||||
</data>
|
||||
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||
<value>E-mail final au créateur</value>
|
||||
<value>Document terminé au créateur</value>
|
||||
</data>
|
||||
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||
<value>E-mail final aux destinataires</value>
|
||||
<value>Document terminé aux destinataires</value>
|
||||
</data>
|
||||
<data name="SendReminderEmails" xml:space="preserve">
|
||||
<value>Envoyer des e-mails de rappel</value>
|
||||
@@ -749,7 +749,14 @@
|
||||
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Page</value></data>
|
||||
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Placer un champ de signature</value></data>
|
||||
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Supprimer tous les champs de signature</value></data>
|
||||
<data name="SenderEditor_RemoveAllSignatureFieldsConfirm" xml:space="preserve"><value>Cette action supprimera définitivement tous les champs de signature placés. Continuer?</value></data>
|
||||
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Veuillez sélectionner un fichier PDF.</value></data>
|
||||
<data name="SenderEditor_SendConfirm_Message" xml:space="preserve"><value>Voulez-vous envoyer l'enveloppe maintenant ? Remarque : après la transmission, le processus ne peut plus être modifié.</value></data>
|
||||
<data name="SenderEditor_SendConfirm_Title" xml:space="preserve"><value>Envoyer l'enveloppe maintenant ?</value></data>
|
||||
<data name="SenderEditor_SendConfirm_Note" xml:space="preserve"><value>Veuillez vérifier les destinataires, les champs de signature et les paramètres avant l'envoi.</value></data>
|
||||
<data name="SenderEditor_SendConfirm_Action" xml:space="preserve"><value>Envoyer maintenant</value></data>
|
||||
<data name="SenderEditor_SendConfirm_InProgress" xml:space="preserve"><value>Envoi de l'enveloppe...</value></data>
|
||||
<data name="SenderEditor_SendConfirm_InProgressHint" xml:space="preserve"><value>Veuillez patienter un instant pendant la finalisation du traitement.</value></data>
|
||||
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>Le brouillon existant a été envoyé.</value></data>
|
||||
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Enveloppe envoyée</value></data>
|
||||
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>L'enveloppe a été envoyée avec succès.</value></data>
|
||||
@@ -777,6 +784,10 @@
|
||||
<data name="Signed" xml:space="preserve"><value>Signé</value></data>
|
||||
<data name="SignedDocument" xml:space="preserve"><value>Document signé</value></data>
|
||||
<data name="System" xml:space="preserve"><value>Système</value></data>
|
||||
<data name="SenderWizard_Step_Start" xml:space="preserve"><value>Démarrer</value></data>
|
||||
<data name="SenderWizard_DocumentUploadSimpleHint" xml:space="preserve"><value>Ajouter un document : cliquer ou glisser-deposer</value></data>
|
||||
<data name="SenderWizard_DocumentReplaceHint" xml:space="preserve"><value>Cliquez pour remplacer ou glissez un fichier sur cette zone.</value></data>
|
||||
<data name="SenderWizard_FinalUploadHint" xml:space="preserve"><value>Derniere possibilite dans l'assistant : veuillez televerser votre PDF maintenant via la zone d'upload a droite. Dans l'editeur, vous pourrez encore televerser ou remplacer un document plus tard.</value></data>
|
||||
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>Une erreur inattendue s'est produite. Veuillez réessayer plus tard.</value></data>
|
||||
<data name="UploadPdf" xml:space="preserve"><value>Téléverser un PDF</value></data>
|
||||
<data name="UserName" xml:space="preserve"><value>Nom d'utilisateur</value></data>
|
||||
|
||||
@@ -295,10 +295,10 @@
|
||||
<value>Use Access Code</value>
|
||||
</data>
|
||||
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||
<value>Final Email to Creator</value>
|
||||
<value>Completed Document to Creator</value>
|
||||
</data>
|
||||
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||
<value>Final Email to Receivers</value>
|
||||
<value>Completed Document to Receivers</value>
|
||||
</data>
|
||||
<data name="SendReminderEmails" xml:space="preserve">
|
||||
<value>Send Reminder Emails</value>
|
||||
|
||||
@@ -132,10 +132,10 @@
|
||||
<value>Utiliser un code d'acces</value>
|
||||
</data>
|
||||
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||
<value>E-mail final au createur</value>
|
||||
<value>Document termine au createur</value>
|
||||
</data>
|
||||
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||
<value>E-mail final aux destinataires</value>
|
||||
<value>Document termine aux destinataires</value>
|
||||
</data>
|
||||
<data name="SendReminderEmails" xml:space="preserve">
|
||||
<value>Envoyer des e-mails de rappel</value>
|
||||
|
||||
@@ -298,10 +298,10 @@
|
||||
<value>Zugriffscode verwenden</value>
|
||||
</data>
|
||||
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||
<value>Finale E-Mail an Ersteller</value>
|
||||
<value>Abgeschlossenes Dokument an Ersteller</value>
|
||||
</data>
|
||||
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||
<value>Finale E-Mail an Empfänger</value>
|
||||
<value>Abgeschlossenes Dokument an Empfänger</value>
|
||||
</data>
|
||||
<data name="SendReminderEmails" xml:space="preserve">
|
||||
<value>Erinnerungs-E-Mails senden</value>
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
private double _grabOffsetXPt;
|
||||
private double _grabOffsetYPt;
|
||||
private int _currentPage = 1;
|
||||
private int _zoomPercent = 112;
|
||||
private int _zoomPercent = 180;
|
||||
private bool _zoomOverrideInitialized;
|
||||
private bool _isDownloading;
|
||||
private bool _isPrinting;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,10 +2,24 @@
|
||||
|
||||
<div class="sender-pdf-dropzone @DropZoneClass">
|
||||
<div class="sender-pdf-dropzone__content">
|
||||
<div class="sender-pdf-dropzone__title">@CurrentActionText</div>
|
||||
@if (!string.IsNullOrWhiteSpace(DragDropHintText))
|
||||
@if (HasDocument)
|
||||
{
|
||||
<div class="sender-pdf-dropzone__hint">@DragDropHintText</div>
|
||||
@if (!string.IsNullOrWhiteSpace(DocumentName))
|
||||
{
|
||||
<div class="sender-pdf-dropzone__doc-name">@DocumentName</div>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(DocumentSizeText))
|
||||
{
|
||||
<div class="sender-pdf-dropzone__doc-size">@DocumentSizeText</div>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(UploadedHintText))
|
||||
{
|
||||
<div class="sender-pdf-dropzone__hint">@UploadedHintText</div>
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(CurrentActionText))
|
||||
{
|
||||
<div class="sender-pdf-dropzone__hint">@CurrentActionText</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -38,6 +52,15 @@
|
||||
[Parameter]
|
||||
public string DragDropHintText { get; set; } = "Datei hierhin ziehen oder klicken";
|
||||
|
||||
[Parameter]
|
||||
public string? DocumentName { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? DocumentSizeText { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string UploadedHintText { get; set; } = "Zum Ersetzen klicken oder eine Datei hierher ziehen.";
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<InputFileChangeEventArgs> FileSelected { get; set; }
|
||||
|
||||
@@ -53,6 +76,9 @@
|
||||
if (Disabled)
|
||||
return "sender-pdf-dropzone--disabled";
|
||||
|
||||
if (HasDocument)
|
||||
return "sender-pdf-dropzone--has-document";
|
||||
|
||||
return _isDragActive ? "sender-pdf-dropzone--drag-active" : string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,23 +4,19 @@ namespace EnvelopeGenerator.Server.Client.Components.Sender.ReceiverSuggestionFi
|
||||
|
||||
public class ReceiverSuggestionFilterHandler(IEnumerable<RecentReceiverSuggestionDto> suggestions)
|
||||
{
|
||||
private readonly IReadOnlyList<Entry> _entries = suggestions
|
||||
private readonly IReadOnlyList<Entry> _entries = [.. suggestions
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.FullName) && !string.IsNullOrWhiteSpace(x.EmailAddress))
|
||||
.Select(x => new Entry(new ReceiverFilterCriteria(x.FullName, x.EmailAddress), x))
|
||||
.ToList();
|
||||
.Select(x => new Entry(new ReceiverFilterCriteria(x.FullName, x.EmailAddress), x))];
|
||||
|
||||
private readonly record struct Entry(ReceiverFilterCriteria Criteria, RecentReceiverSuggestionDto Suggestion);
|
||||
|
||||
public IEnumerable<RecentReceiverSuggestionDto> Filter(ReceiverFilterCriteria criteria)
|
||||
public IEnumerable<RecentReceiverSuggestionDto> Filter(string query)
|
||||
{
|
||||
var normalizedQuery = new ReceiverFilterCriteria(query, query);
|
||||
return _entries
|
||||
.Where(x => x.Criteria.Matches(criteria))
|
||||
.Where(x =>
|
||||
x.Criteria.FullName.Contains(normalizedQuery.FullName, StringComparison.Ordinal)
|
||||
|| x.Criteria.EmailAddress.Contains(normalizedQuery.EmailAddress, StringComparison.Ordinal))
|
||||
.Select(x => x.Suggestion);
|
||||
}
|
||||
|
||||
public IEnumerable<RecentReceiverSuggestionDto> Filter(string fullName, string emailAddress)
|
||||
{
|
||||
var criteria = new ReceiverFilterCriteria(fullName, emailAddress);
|
||||
return Filter(criteria);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,12 +289,13 @@
|
||||
@if (_pdfLoaded && _signatureFields.Count > 0)
|
||||
{
|
||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
|
||||
@onclick="ClearAllFieldsAsync"
|
||||
@onclick='() => _clearFieldsConfirmVisible = true'
|
||||
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>
|
||||
<span class="pdf-toolbar__btn-text"> @L("SenderEditor_SignatureField", "Signaturfeld")</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -317,7 +318,7 @@
|
||||
</button>
|
||||
|
||||
<button class="pdf-toolbar__btn pdf-toolbar__btn--success"
|
||||
@onclick="SendAsync"
|
||||
@onclick="RequestSendAsync"
|
||||
disabled="@_isSaving"
|
||||
title='@L("SendEnvelope", "Umschlag senden")'>
|
||||
@if (_isSaving)
|
||||
@@ -506,6 +507,69 @@
|
||||
</FooterContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
@* ── Clear all fields confirmation popup ── *@
|
||||
<DxPopup @bind-Visible="_clearFieldsConfirmVisible"
|
||||
Width="400px"
|
||||
MaxWidth="95vw"
|
||||
ShowHeader="false"
|
||||
ShowFooter="false"
|
||||
CloseOnEscape="true"
|
||||
CloseOnOutsideClick="true">
|
||||
<BodyContentTemplate>
|
||||
<div class="sender-confirm-popup">
|
||||
<p class="sender-confirm-popup__message">@L("SenderEditor_RemoveAllSignatureFieldsConfirm", "Diese Aktion entfernt alle platzierten Signaturfelder unwiderruflich. Fortfahren?")</p>
|
||||
<div class="sender-confirm-popup__actions">
|
||||
<DxButton Text='@L("Cancel", "Abbrechen")'
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
Click='() => _clearFieldsConfirmVisible = false' />
|
||||
<DxButton Text='@L("Delete", "Löschen")'
|
||||
RenderStyle="ButtonRenderStyle.Danger"
|
||||
Click="ClearAllFieldsAsync" />
|
||||
</div>
|
||||
</div>
|
||||
</BodyContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
@* ── Send confirmation popup ── *@
|
||||
<DxPopup @bind-Visible="_sendConfirmVisible"
|
||||
HeaderText='@L("SenderEditor_SendConfirm_Title", "Umschlag jetzt versenden?")'
|
||||
SizeMode="SizeMode.Medium"
|
||||
Width="460px"
|
||||
MaxWidth="95vw"
|
||||
ShowFooter="@(!_isSendConfirmSubmitting)"
|
||||
ShowCloseButton="@(!_isSendConfirmSubmitting)"
|
||||
CloseOnEscape="@(!_isSendConfirmSubmitting)"
|
||||
CloseOnOutsideClick="false">
|
||||
<BodyContentTemplate>
|
||||
@if (_isSendConfirmSubmitting)
|
||||
{
|
||||
<div class="sender-send-progress">
|
||||
<DxWaitIndicator Visible="true" />
|
||||
<div>
|
||||
<p class="sender-send-progress__title">@L("SenderEditor_SendConfirm_InProgress", "Umschlag wird versendet...")</p>
|
||||
<p class="sender-send-progress__hint">@L("SenderEditor_SendConfirm_InProgressHint", "Bitte warten Sie einen Moment. Der Vorgang wird abgeschlossen.")</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="sender-confirm-popup sender-confirm-popup--send">
|
||||
<p class="sender-confirm-popup__note">
|
||||
@L("SenderEditor_SendConfirm_Message", "Wollen Sie den Umschlag jetzt versenden? Hinweis: Nach der Übergabe kann der Vorgang nicht mehr bearbeitet werden.")
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</BodyContentTemplate>
|
||||
<FooterContentTemplate>
|
||||
<DxButton Text='@L("No", "Nein")'
|
||||
RenderStyle="ButtonRenderStyle.Secondary"
|
||||
Click='() => _sendConfirmVisible = false' />
|
||||
<DxButton Text='@L("SenderEditor_SendConfirm_Action", "Ja, versenden")'
|
||||
RenderStyle="ButtonRenderStyle.Primary"
|
||||
Click="ConfirmSendAsync" />
|
||||
</FooterContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
@* ── Save result popup (success + error) ── *@
|
||||
<DxPopup @bind-Visible="_savePopupVisible"
|
||||
HeaderText='@(_saveErrorMessage is null ? _saveSuccessHeader : L("Error", "Fehler"))'
|
||||
@@ -620,6 +684,9 @@
|
||||
// ── Save state ──
|
||||
bool _isSaving = false;
|
||||
bool _savePopupVisible = false;
|
||||
bool _clearFieldsConfirmVisible = false;
|
||||
bool _sendConfirmVisible = false;
|
||||
bool _isSendConfirmSubmitting = false;
|
||||
string? _saveErrorMessage = null;
|
||||
string _saveSuccessHeader = "Erfolg";
|
||||
string _saveSuccessTitle = "Vorgang erfolgreich abgeschlossen.";
|
||||
@@ -1060,6 +1127,7 @@
|
||||
// ── Clear all fields ──
|
||||
async Task ClearAllFieldsAsync()
|
||||
{
|
||||
_clearFieldsConfirmVisible = false;
|
||||
_signatureFields.Clear();
|
||||
_pendingReceiverForPlacement = null;
|
||||
await Task.CompletedTask;
|
||||
@@ -1215,13 +1283,7 @@
|
||||
|
||||
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;
|
||||
NavigationManager.NavigateTo("/sender", forceLoad: false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1235,6 +1297,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
Task RequestSendAsync()
|
||||
{
|
||||
if (_isSaving)
|
||||
return Task.CompletedTask;
|
||||
|
||||
_sendConfirmVisible = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
async Task ConfirmSendAsync()
|
||||
{
|
||||
if (_isSendConfirmSubmitting)
|
||||
return;
|
||||
|
||||
_isSendConfirmSubmitting = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
await SendAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSendConfirmSubmitting = false;
|
||||
|
||||
if (_savePopupVisible)
|
||||
_sendConfirmVisible = false;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
CreateEnvelopeCommand BuildCreateEnvelopeCommand(bool send)
|
||||
{
|
||||
NormalizeSettings();
|
||||
|
||||
@@ -32,6 +32,11 @@
|
||||
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
||||
<link href="@AppVersion.GetVersionedUrl("css/sender-page.css")" rel="stylesheet" />
|
||||
|
||||
<MudThemeProvider />
|
||||
<MudPopoverProvider />
|
||||
<MudDialogProvider />
|
||||
<MudSnackbarProvider />
|
||||
|
||||
<div class="sender-dashboard-layout">
|
||||
<div class="sender-action-bar">
|
||||
<div class="sender-action-bar__inner">
|
||||
@@ -108,12 +113,12 @@
|
||||
}
|
||||
</button>
|
||||
|
||||
<button class="sender-btn" @onclick="ShowPreview" disabled="@(_selectedEnvelope == null)" title="@L("Sender_Preview_Title", "Ausgewählten Umschlag als PDF anzeigen")">
|
||||
<button class="sender-btn" @onclick="ShowPreview" disabled="@(_selectedEnvelope == null)" title="@L("Sender_ShowOriginalDocument_Title", "Originaldokument anzeigen")">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/>
|
||||
<path d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/>
|
||||
</svg>
|
||||
@L("Preview", "Vorschau")
|
||||
@L("Sender_ShowOriginalDocument", "Original Dokument anzeigen")
|
||||
</button>
|
||||
|
||||
<div class="sender-export-group" title="@L("Sender_ExportView_Title", "Export der aktuellen Tabellenansicht")">
|
||||
@@ -218,214 +223,223 @@
|
||||
|
||||
<div class="sender-grid-wrapper">
|
||||
<DxGrid @key="_activeTab"
|
||||
Data="@CurrentGridData"
|
||||
@ref="_grid"
|
||||
KeyFieldName="Id"
|
||||
ShowFilterRow="false"
|
||||
FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always"
|
||||
ShowSearchBox="true"
|
||||
AllowColumnReorder="true"
|
||||
AllowSort="true"
|
||||
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
||||
PageSize="20"
|
||||
PagerVisible="true"
|
||||
SelectionMode="GridSelectionMode.Single"
|
||||
AllowSelectRowByClick="true"
|
||||
SelectedDataItem="@_selectedEnvelope"
|
||||
SelectedDataItemChanged="@((object o) => _selectedEnvelope = o as EnvelopeDto)"
|
||||
RowClick="OnRowClick"
|
||||
RowDoubleClick="OnRowDoubleClick"
|
||||
CustomizeElement="OnCustomizeElement"
|
||||
LayoutAutoLoading="OnGridLayoutAutoLoading"
|
||||
LayoutAutoSaving="OnGridLayoutAutoSaving">
|
||||
<Columns>
|
||||
<DxGridDataColumn FieldName="Id" Caption="ID"
|
||||
DisplayFormat="0"
|
||||
SortIndex="0"
|
||||
SortOrder="GridColumnSortOrder.Descending">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@((cellContext.DataItem as EnvelopeDto)?.Id)
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="Title" Caption="@L("Title", "Titel")">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="Status" Caption="@L("Status", "Status")" ExportEnabled="false">
|
||||
<EditSettings>
|
||||
<DxComboBoxSettings Data="@GetStatusFilterItems(CurrentGridData)"
|
||||
ValueFieldName="Value"
|
||||
TextFieldName="Text"
|
||||
SearchMode="ListSearchMode.AutoSearch"
|
||||
SearchFilterCondition="ListSearchFilterCondition.Contains" />
|
||||
</EditSettings>
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
if (envelope != null)
|
||||
{
|
||||
var statusInfo = GetStatusInfo(GetEffectiveEnvelopeStatus(envelope));
|
||||
<div class="status-badge status-badge--@statusInfo.CssClass">
|
||||
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
|
||||
@statusInfo.Label
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="@L("Receiver", "Empfänger")" ExportEnabled="false">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
if (envelope != null)
|
||||
{
|
||||
var receivers = envelope.EnvelopeReceivers?.ToList() ?? [];
|
||||
var signed = GetSignedReceiverCount(envelope);
|
||||
var total = receivers.Count;
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span style="font-size: 0.875rem; color: #6b7280;">
|
||||
@signed / @total @L("Signed", "unterschrieben")
|
||||
</span>
|
||||
@if (total > 0)
|
||||
{
|
||||
<div style="flex: 1; min-width: 60px; max-width: 120px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden;">
|
||||
<div style="height: 100%; background: linear-gradient(90deg, #81c784 0%, #66bb6a 100%); width: @((signed * 100.0 / total).ToString("F0"))%;"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="EnvelopeTypeTitle" Caption="@L("Type", "Typ")"
|
||||
Width="120px" AllowSort="true" />
|
||||
<DxGridDataColumn FieldName="AddedWhen" Caption="@L("Created", "Erstellt")"
|
||||
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
|
||||
<DxGridDataColumn FieldName="ChangedWhen" Caption="@L("ChangedAt", "Geändert am")"
|
||||
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
|
||||
</Columns>
|
||||
<DetailRowTemplate Context="detailContext">
|
||||
<div style="padding: 1rem; background: #f9fafb;">
|
||||
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">@L("Receiver", "Empfänger")</h6>
|
||||
Data="@CurrentGridData"
|
||||
@ref="_grid"
|
||||
KeyFieldName="Id"
|
||||
ShowFilterRow="false"
|
||||
FilterMenuButtonDisplayMode="GridFilterMenuButtonDisplayMode.Always"
|
||||
ShowSearchBox="true"
|
||||
AllowColumnReorder="true"
|
||||
AllowSort="true"
|
||||
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
||||
PageSize="20"
|
||||
PagerVisible="true"
|
||||
SelectionMode="GridSelectionMode.Single"
|
||||
AllowSelectRowByClick="true"
|
||||
SelectedDataItem="@_selectedEnvelope"
|
||||
SelectedDataItemChanged="@((object o) => _selectedEnvelope = o as EnvelopeDto)"
|
||||
RowClick="OnRowClick"
|
||||
RowDoubleClick="OnRowDoubleClick"
|
||||
CustomizeElement="OnCustomizeElement"
|
||||
LayoutAutoLoading="OnGridLayoutAutoLoading"
|
||||
LayoutAutoSaving="OnGridLayoutAutoSaving"
|
||||
HighlightRowOnHover="true"
|
||||
AutoCollapseDetailRow=true
|
||||
DetailExpandButtonDisplayMode="GridDetailExpandButtonDisplayMode.Auto">
|
||||
<Columns>
|
||||
<DxGridDataColumn FieldName="Id" Caption="ID"
|
||||
DisplayFormat="0"
|
||||
SortIndex="0"
|
||||
SortOrder="GridColumnSortOrder.Descending">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@((cellContext.DataItem as EnvelopeDto)?.Id)
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="Title" Caption="@L("Title", "Titel")">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="Status" Caption="@L("Status", "Status")" ExportEnabled="false">
|
||||
<EditSettings>
|
||||
<DxComboBoxSettings Data="@GetStatusFilterItems(CurrentGridData)"
|
||||
ValueFieldName="Value"
|
||||
TextFieldName="Text"
|
||||
SearchMode="ListSearchMode.AutoSearch"
|
||||
SearchFilterCondition="ListSearchFilterCondition.Contains" />
|
||||
</EditSettings>
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var envelope = detailContext.DataItem as EnvelopeDto;
|
||||
if (envelope?.EnvelopeReceivers?.Any() == true)
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
if (envelope != null)
|
||||
{
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
@foreach (var receiver in envelope.EnvelopeReceivers)
|
||||
{
|
||||
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
|
||||
@{
|
||||
var isRejected = IsReceiverRejected(envelope, receiver);
|
||||
var isSigned = IsReceiverSigned(envelope, receiver);
|
||||
var accessCode = receiver.AccessCode;
|
||||
var hasAccessCode = !string.IsNullOrWhiteSpace(accessCode);
|
||||
var hasReceiverEnvelopeUrl = CanOpenReceiverEnvelopeUrl(envelope, receiver);
|
||||
}
|
||||
<span class="receiver-badge receiver-badge--@(isRejected ? "unsigned" : (isSigned ? "signed" : "unsigned"))" style="min-width: 100px;">
|
||||
@if (isRejected)
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
|
||||
</svg>
|
||||
<span>@GetStatusLabel(EnvelopeStatus.DocumentRejected)</span>
|
||||
}
|
||||
else if (isSigned)
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" 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>
|
||||
<span>@L("Signed", "Unterschrieben")</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" 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="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
||||
</svg>
|
||||
<span>@L("Pending", "Ausstehend")</span>
|
||||
}
|
||||
</span>
|
||||
<div style="flex: 1; font-size: 0.875rem;">
|
||||
<strong style="color: #1f2937;">@receiver.Name</strong>
|
||||
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Receiver?.EmailAddress</span>
|
||||
</div>
|
||||
<div class="sender-access-code">
|
||||
<span class="sender-access-code__label">@L("AccessCode", "Access code")</span>
|
||||
<div class="sender-access-code__row">
|
||||
<input class="sender-access-code__value"
|
||||
value="@(hasAccessCode ? accessCode : "-")"
|
||||
readonly
|
||||
title="@L("AccessCode", "Access code")" />
|
||||
<button type="button"
|
||||
class="sender-access-code__copy"
|
||||
disabled="@(!hasAccessCode)"
|
||||
@onclick='() => CopyAccessCodeAsync(accessCode)'
|
||||
title="@L("Copy", "Copy")">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M10 1.5A1.5 1.5 0 0 1 11.5 3v1H13A1.5 1.5 0 0 1 14.5 5.5v8A1.5 1.5 0 0 1 13 15H5A1.5 1.5 0 0 1 3.5 13.5V12H3A1.5 1.5 0 0 1 1.5 10.5v-8A1.5 1.5 0 0 1 3 1h7zm.5 2A.5.5 0 0 0 10 3H3a.5.5 0 0 0-.5.5v7A.5.5 0 0 0 3 11h.5V5.5A1.5 1.5 0 0 1 5 4h5.5v-.5zM5 5a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-8A.5.5 0 0 0 13 5H5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
@if (_canUseReceiverEnvelopeUrl)
|
||||
{
|
||||
<button type="button"
|
||||
class="sender-access-code__copy"
|
||||
disabled="@(!hasReceiverEnvelopeUrl)"
|
||||
@onclick='() => OpenReceiverEnvelopeUrlAsync(envelope, receiver)'
|
||||
title="@L("Open", "Open")">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M6.364 13.5a1.5 1.5 0 0 1-1.5-1.5V4a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 1 0v4a1.5 1.5 0 0 1-1.5 1.5h-8z"/>
|
||||
<path fill-rule="evenodd" d="M8.5 1a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-1 0V2.707L8.854 7.354a.5.5 0 1 1-.708-.708L12.793 2H9a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">@L("Sender_NoReceivers", "Keine Empfänger")</p>
|
||||
var statusInfo = GetStatusInfo(GetEffectiveEnvelopeStatus(envelope));
|
||||
<div class="status-badge status-badge--@statusInfo.CssClass">
|
||||
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
|
||||
@statusInfo.Label
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<h6 style="font-weight: 600; color: #374151; margin: 1rem 0 0.75rem;">@L("History", "Historie")</h6>
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="@L("Receiver", "Empfänger")" ExportEnabled="false">
|
||||
<CellDisplayTemplate Context="cellContext">
|
||||
@{
|
||||
var historyEntries = envelope is null
|
||||
? Enumerable.Empty<HistoryDto>()
|
||||
: GetOrderedHistories(envelope);
|
||||
if (historyEntries.Any())
|
||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
||||
if (envelope != null)
|
||||
{
|
||||
<div class="sender-history-list">
|
||||
@foreach (var history in historyEntries)
|
||||
var receivers = envelope.EnvelopeReceivers?.ToList() ?? [];
|
||||
var signed = GetSignedReceiverCount(envelope);
|
||||
var total = receivers.Count;
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span style="font-size: 0.875rem; color: #6b7280;">
|
||||
@signed / @total @L("Signed", "unterschrieben")
|
||||
</span>
|
||||
@if (total > 0)
|
||||
{
|
||||
var statusInfo = GetStatusInfo(history.Status);
|
||||
<div class="sender-history-item">
|
||||
<div class="sender-history-item__left">
|
||||
<div class="status-badge status-badge--@statusInfo.CssClass">
|
||||
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
|
||||
@statusInfo.Label
|
||||
</div>
|
||||
<span class="sender-history-item__actor">@GetHistoryActor(history)</span>
|
||||
</div>
|
||||
<div class="sender-history-item__right">
|
||||
@GetHistoryDate(history).ToString("dd.MM.yyyy HH:mm")
|
||||
</div>
|
||||
<div style="flex: 1; min-width: 60px; max-width: 120px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden;">
|
||||
<div style="height: 100%; background: linear-gradient(90deg, #81c784 0%, #66bb6a 100%); width: @((signed * 100.0 / total).ToString("F0"))%;"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">@L("Sender_NoHistory", "Keine Historie vorhanden")</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</DetailRowTemplate>
|
||||
</DxGrid>
|
||||
</CellDisplayTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="EnvelopeTypeTitle" Caption="@L("Type", "Typ")"
|
||||
Width="120px" AllowSort="true" />
|
||||
<DxGridDataColumn FieldName="AddedWhen" Caption="@L("Created", "Erstellt")"
|
||||
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
|
||||
<DxGridDataColumn FieldName="ChangedWhen" Caption="@L("ChangedAt", "Geändert am")"
|
||||
DisplayFormat="{0:dd.MM.yyyy}" Width="110px" AllowSort="true" />
|
||||
</Columns>
|
||||
<DetailRowTemplate Context="detailContext">
|
||||
<div style="padding: 1rem; background: #f9fafb;">
|
||||
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">@L("Receiver", "Empfänger")</h6>
|
||||
@{
|
||||
var envelope = detailContext.DataItem as EnvelopeDto;
|
||||
if(envelope is not null && _grid is not null)
|
||||
{
|
||||
_grid.SelectDataItem(envelope);
|
||||
// _grid!.SelectedDataItem = envelope!;
|
||||
}
|
||||
|
||||
if (envelope?.EnvelopeReceivers?.Any() == true)
|
||||
{
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
@foreach (var receiver in envelope.EnvelopeReceivers)
|
||||
{
|
||||
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
|
||||
@{
|
||||
var isRejected = IsReceiverRejected(envelope, receiver);
|
||||
var isSigned = IsReceiverSigned(envelope, receiver);
|
||||
var accessCode = receiver.AccessCode;
|
||||
var hasAccessCode = !string.IsNullOrWhiteSpace(accessCode);
|
||||
var hasReceiverEnvelopeUrl = CanOpenReceiverEnvelopeUrl(envelope, receiver);
|
||||
}
|
||||
<span class="receiver-badge receiver-badge--@(isRejected ? "unsigned" : (isSigned ? "signed" : "unsigned"))" style="min-width: 100px;">
|
||||
@if (isRejected)
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
|
||||
</svg>
|
||||
<span>@GetStatusLabel(EnvelopeStatus.DocumentRejected)</span>
|
||||
}
|
||||
else if (isSigned)
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" 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>
|
||||
<span>@L("Signed", "Unterschrieben")</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" 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="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
|
||||
</svg>
|
||||
<span>@L("Pending", "Ausstehend")</span>
|
||||
}
|
||||
</span>
|
||||
<div style="flex: 1; font-size: 0.875rem;">
|
||||
<strong style="color: #1f2937;">@receiver.Name</strong>
|
||||
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Receiver?.EmailAddress</span>
|
||||
</div>
|
||||
<div class="sender-access-code">
|
||||
<span class="sender-access-code__label">@L("AccessCode", "Access code")</span>
|
||||
<div class="sender-access-code__row">
|
||||
<input class="sender-access-code__value"
|
||||
value="@(hasAccessCode ? accessCode : "-")"
|
||||
readonly
|
||||
title="@L("AccessCode", "Access code")" />
|
||||
<button type="button"
|
||||
class="sender-access-code__copy"
|
||||
disabled="@(!hasAccessCode)"
|
||||
@onclick='() => CopyAccessCodeAsync(accessCode)'
|
||||
title="@L("Copy", "Copy")">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M10 1.5A1.5 1.5 0 0 1 11.5 3v1H13A1.5 1.5 0 0 1 14.5 5.5v8A1.5 1.5 0 0 1 13 15H5A1.5 1.5 0 0 1 3.5 13.5V12H3A1.5 1.5 0 0 1 1.5 10.5v-8A1.5 1.5 0 0 1 3 1h7zm.5 2A.5.5 0 0 0 10 3H3a.5.5 0 0 0-.5.5v7A.5.5 0 0 0 3 11h.5V5.5A1.5 1.5 0 0 1 5 4h5.5v-.5zM5 5a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-8A.5.5 0 0 0 13 5H5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
@if (_canUseReceiverEnvelopeUrl)
|
||||
{
|
||||
<button type="button"
|
||||
class="sender-access-code__copy"
|
||||
disabled="@(!hasReceiverEnvelopeUrl)"
|
||||
@onclick='() => OpenReceiverEnvelopeUrlAsync(envelope, receiver)'
|
||||
title="@L("Open", "Open")">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M6.364 13.5a1.5 1.5 0 0 1-1.5-1.5V4a1.5 1.5 0 0 1 1.5-1.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 1 0v4a1.5 1.5 0 0 1-1.5 1.5h-8z"/>
|
||||
<path fill-rule="evenodd" d="M8.5 1a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-1 0V2.707L8.854 7.354a.5.5 0 1 1-.708-.708L12.793 2H9a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">@L("Sender_NoReceivers", "Keine Empfänger")</p>
|
||||
}
|
||||
}
|
||||
|
||||
<h6 style="font-weight: 600; color: #374151; margin: 1rem 0 0.75rem;">@L("History", "Historie")</h6>
|
||||
@{
|
||||
var historyEntries = envelope is null
|
||||
? Enumerable.Empty<HistoryDto>()
|
||||
: GetOrderedHistories(envelope);
|
||||
if (historyEntries.Any())
|
||||
{
|
||||
<div class="sender-history-list">
|
||||
@foreach (var history in historyEntries)
|
||||
{
|
||||
var statusInfo = GetStatusInfo(history.Status);
|
||||
<div class="sender-history-item">
|
||||
<div class="sender-history-item__left">
|
||||
<div class="status-badge status-badge--@statusInfo.CssClass">
|
||||
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
|
||||
@statusInfo.Label
|
||||
</div>
|
||||
<span class="sender-history-item__actor">@GetHistoryActor(history)</span>
|
||||
</div>
|
||||
<div class="sender-history-item__right">
|
||||
@GetHistoryDate(history).ToString("dd.MM.yyyy HH:mm")
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">@L("Sender_NoHistory", "Keine Historie vorhanden")</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</DetailRowTemplate>
|
||||
</DxGrid>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -434,13 +448,13 @@
|
||||
</div>
|
||||
|
||||
<CreateEnvelopeWizardPopup Visible="@_createWizardVisible"
|
||||
VisibleChanged="OnCreateWizardVisibleChangedAsync"
|
||||
EnvelopeTypes="@_envelopeTypes"
|
||||
StartEditorRequested="StartEditorFromWizardAsync" />
|
||||
VisibleChanged="OnCreateWizardVisibleChangedAsync"
|
||||
EnvelopeTypes="@_envelopeTypes"
|
||||
StartEditorRequested="StartEditorFromWizardAsync" />
|
||||
|
||||
@* ── PDF Vorschau Popup ────────────────────────────────────────────────── *@
|
||||
<DxPopup @bind-Visible="@_previewVisible"
|
||||
HeaderText="@($"{(_activeTab == "completed" && _selectedEnvelope?.Status == EnvelopeStatus.EnvelopeCompletelySigned ? L("SignedPdf", "Signiertes PDF") : L("Preview", "Vorschau"))} — {_selectedEnvelope?.Title} (#{_selectedEnvelope?.Id})")"
|
||||
HeaderText="@($"{(_activeTab == "completed" && _selectedEnvelope?.Status == EnvelopeStatus.EnvelopeCompletelySigned ? L("SignedPdf", "Signiertes PDF") : L("Sender_OriginalDocument", "Originaldokument"))} — {_selectedEnvelope?.Title} (#{_selectedEnvelope?.Id})")"
|
||||
Width="80vw"
|
||||
MaxWidth="80vw"
|
||||
Height="90vh"
|
||||
@@ -489,6 +503,22 @@
|
||||
else if (_previewPdfBytes is not null && _previewViewerMounted)
|
||||
{
|
||||
<div class="sender-preview-viewer-wrap">
|
||||
@if (_showPreviewHint)
|
||||
{
|
||||
<div class="sender-preview-info" style="display:flex;align-items:center;justify-content:space-between;gap:0.5rem;padding:0.625rem 0.75rem;margin:0 0 0.5rem 0;background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;color:#1e3a8a;font-size:0.85rem;">
|
||||
<span>@L("Sender_OriginalDocument_NoSignatureFieldsHint", "Hinweis: In dieser Ansicht werden keine Unterschriftsfelder angezeigt.")</span>
|
||||
<button type="button"
|
||||
class="sender-preview-info__close"
|
||||
@onclick="DismissPreviewHint"
|
||||
title="@L("Close", "Schliessen")"
|
||||
aria-label="@L("Close", "Schliessen")"
|
||||
style="display:inline-flex;align-items:center;justify-content:center;padding:0.125rem;border:none;background:transparent;box-shadow:none;appearance:none;cursor:pointer;color:#1e3a8a;line-height:1;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
|
||||
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 1 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 1 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
<SenderPdfImageEditor IsPlacementMode="false"
|
||||
PageCount="@_previewPageCount"
|
||||
Pages="@_previewPagePreviews"
|
||||
@@ -577,6 +607,9 @@
|
||||
private string _previewFileName = "document.pdf";
|
||||
private string? _previewErrorMessage = null;
|
||||
private readonly IReadOnlyList<SenderPdfFieldOverlay> _previewFieldOverlays = [];
|
||||
private bool _showPreviewHint = false;
|
||||
private int _previewHintVersion = 0;
|
||||
private const int PreviewHintAutoHideMs = 4000;
|
||||
|
||||
private string? PreviewPdfDataUrl => _previewPdfBytes is { Length: > 0 }
|
||||
? $"data:application/pdf;base64,{Convert.ToBase64String(_previewPdfBytes)}"
|
||||
@@ -613,22 +646,29 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Load theme + DisplayName from server after authentication.
|
||||
// Non-fatal: if this fails the page still loads with default theme.
|
||||
// ThemeService.ScheduleSaveAsync() already logs errors internally.
|
||||
try { await ThemeService.LoadFromServerAsync(); }
|
||||
catch { /* swallow — default theme remains active */ }
|
||||
// Fan out all independent post-auth calls in parallel to reduce initial load time.
|
||||
var themeTask = Task.Run(async () =>
|
||||
{
|
||||
try { await ThemeService.LoadFromServerAsync(); }
|
||||
catch { /* swallow — default theme remains active */ }
|
||||
});
|
||||
|
||||
_canUseReceiverEnvelopeUrl = await EnvelopeReceiverService.GetReceiverEnvelopeUrlFeatureEnabledAsync();
|
||||
var featureTask = EnvelopeReceiverService.GetReceiverEnvelopeUrlFeatureEnabledAsync();
|
||||
var logosTask = CustomImagesService.GetAsync();
|
||||
var typesTask = EnvelopeTypeService.GetAsync();
|
||||
var envelopesTask = EnvelopeService.GetAsync(includeDocAndDocRes: false);
|
||||
|
||||
// Load logo config — authenticated, so this should succeed
|
||||
var logos = await CustomImagesService.GetAsync();
|
||||
await Task.WhenAll(themeTask, featureTask, logosTask, typesTask, envelopesTask);
|
||||
|
||||
_canUseReceiverEnvelopeUrl = await featureTask;
|
||||
|
||||
var logos = await logosTask;
|
||||
_appLogo = logos.App;
|
||||
_companyLogo = logos.Company;
|
||||
|
||||
try
|
||||
{
|
||||
_envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList();
|
||||
_envelopeTypes = (await typesTask).ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -636,7 +676,8 @@
|
||||
_envelopeTypes = [];
|
||||
}
|
||||
|
||||
await LoadEnvelopesAsync();
|
||||
// Use already-fetched envelope data instead of a second HTTP call.
|
||||
await ApplyEnvelopeResultAsync(envelopesTask);
|
||||
}
|
||||
|
||||
async Task LoadEnvelopesAsync()
|
||||
@@ -644,10 +685,15 @@
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await ApplyEnvelopeResultAsync(EnvelopeService.GetAsync(includeDocAndDocRes: false));
|
||||
}
|
||||
|
||||
async Task ApplyEnvelopeResultAsync(Task<IEnumerable<EnvelopeDto>?> envelopesTask)
|
||||
{
|
||||
_isLoading = true;
|
||||
try
|
||||
{
|
||||
_allEnvelopes = await EnvelopeService.GetAsync(includeDocAndDocRes: false) ?? [];
|
||||
_allEnvelopes = (await envelopesTask) ?? [];
|
||||
|
||||
// Split into active and completed based on effective status
|
||||
// (history-aware: rejected envelopes should not stay in active tab).
|
||||
@@ -1061,13 +1107,19 @@
|
||||
var clicked = e.Grid.GetDataItem(e.VisibleIndex) as EnvelopeDto;
|
||||
if (clicked == null) return;
|
||||
|
||||
if (_selectedEnvelope?.Id == clicked.Id)
|
||||
if (e.Grid.IsDetailRowExpanded(e.VisibleIndex))
|
||||
{
|
||||
// Same row clicked again → deselect
|
||||
// Already expanded → collapse and deselect
|
||||
e.Grid.CollapseDetailRow(e.VisibleIndex);
|
||||
_selectedEnvelope = null;
|
||||
e.Grid.DeselectDataItem(clicked);
|
||||
}
|
||||
// Different row: @bind-SelectedDataItem already updated _selectedEnvelope
|
||||
else
|
||||
{
|
||||
// Collapsed → expand and select
|
||||
e.Grid.SelectDataItem(clicked);
|
||||
e.Grid.ExpandDetailRow(e.VisibleIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Double-click: select the row and open preview popup
|
||||
@@ -1132,6 +1184,7 @@
|
||||
|
||||
// PDF ready — mount the viewer now
|
||||
_previewViewerMounted = true;
|
||||
ShowPreviewHint();
|
||||
}
|
||||
catch (HttpRequestException ex) when ((int?)ex.StatusCode == 404)
|
||||
{
|
||||
@@ -1156,6 +1209,9 @@
|
||||
|
||||
void OnPreviewClosed()
|
||||
{
|
||||
_previewHintVersion++;
|
||||
_showPreviewHint = false;
|
||||
|
||||
// Unmount the viewer first — prevent DevExpress async render from
|
||||
// accessing the already-disposed byte array
|
||||
_previewViewerMounted = false;
|
||||
@@ -1187,6 +1243,28 @@
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
void DismissPreviewHint()
|
||||
{
|
||||
_previewHintVersion++;
|
||||
_showPreviewHint = false;
|
||||
}
|
||||
|
||||
void ShowPreviewHint()
|
||||
{
|
||||
_showPreviewHint = true;
|
||||
var currentVersion = ++_previewHintVersion;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(PreviewHintAutoHideMs);
|
||||
if (currentVersion != _previewHintVersion)
|
||||
return;
|
||||
|
||||
_showPreviewHint = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
});
|
||||
}
|
||||
|
||||
static int? EstimatePdfPageCount(byte[]? documentBytes)
|
||||
{
|
||||
if (documentBytes is null || documentBytes.Length == 0)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
@using EnvelopeGenerator.Server.Client.Models
|
||||
@using EnvelopeGenerator.Server.Client.Options
|
||||
@using EnvelopeGenerator.Server.Client.Components.PdfEditor
|
||||
@using MudBlazor
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.Blazor.PdfViewer
|
||||
@using DevExpress.Blazor.Reporting
|
||||
|
||||
@@ -2085,3 +2085,79 @@ body.resizing {
|
||||
[data-sf-theme="blazing-dark"] .pdf-text-layer ::-moz-selection {
|
||||
background: rgba(99, 102, 241, 0.35);
|
||||
}
|
||||
|
||||
/* ── Confirm popup (headerless, footerless) ── */
|
||||
.sender-confirm-popup {
|
||||
padding: 0.25rem 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.sender-confirm-popup__message {
|
||||
margin: 0 0 1.25rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
color: var(--bs-body-color, #e2e8f0);
|
||||
}
|
||||
|
||||
.sender-confirm-popup__actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.sender-confirm-popup--send {
|
||||
padding: 0.5rem 0.25rem;
|
||||
}
|
||||
|
||||
.sender-confirm-popup--send .sender-confirm-popup__note {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sender-confirm-popup__title {
|
||||
margin: 0 0 0.4rem;
|
||||
font-size: 1.02rem;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.sender-confirm-popup__note {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 0.84rem;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.sender-send-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0.25rem;
|
||||
}
|
||||
|
||||
.sender-send-progress__spinner {
|
||||
width: 1.15rem;
|
||||
height: 1.15rem;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.sender-send-progress__title {
|
||||
margin: 0 0 0.2rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.sender-send-progress__hint {
|
||||
margin: 0;
|
||||
font-size: 0.83rem;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
[data-sf-theme="blazing-dark"] .sender-confirm-popup__title,
|
||||
[data-sf-theme="blazing-dark"] .sender-send-progress__title {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
[data-sf-theme="blazing-dark"] .sender-confirm-popup__note,
|
||||
[data-sf-theme="blazing-dark"] .sender-send-progress__hint {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
@@ -1103,9 +1103,90 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.sender-create-wizard__body-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(240px, 280px);
|
||||
gap: 0.85rem;
|
||||
align-items: stretch;
|
||||
transition: grid-template-columns 220ms ease 0.2s, gap 220ms ease 0.2s;
|
||||
}
|
||||
|
||||
.sender-create-wizard-popup.dxbl-modal-root,
|
||||
.sender-create-wizard-popup .dxbl-modal-root,
|
||||
.sender-create-wizard-popup .dxbl-modal,
|
||||
.sender-create-wizard-popup .dxbl-modal-dialog,
|
||||
.sender-create-wizard-popup .dxbl-modal-content,
|
||||
.sender-create-wizard-popup .dxbl-popup,
|
||||
.sender-create-wizard-popup.dxbl-popup {
|
||||
height: 85vh !important;
|
||||
max-height: 85vh !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.sender-create-wizard-popup .dxbl-modal-body,
|
||||
.sender-create-wizard-popup .dxbl-popup-content {
|
||||
max-height: min(78vh, 760px);
|
||||
overflow: auto;
|
||||
flex: 1 1 auto !important;
|
||||
height: auto !important;
|
||||
min-height: 0 !important;
|
||||
max-height: none !important;
|
||||
overflow: hidden !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.sender-create-wizard-popup .dxbl-popup-content > .sender-create-wizard,
|
||||
.sender-create-wizard-popup .sender-create-wizard,
|
||||
.sender-create-wizard-popup .sender-create-wizard__body-layout {
|
||||
flex: 1 1 auto !important;
|
||||
height: auto !important;
|
||||
min-height: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* Scroll only inside the stepper content, so the body never resizes. */
|
||||
.sender-create-wizard__stepper-shell {
|
||||
min-height: 0 !important;
|
||||
overflow: hidden !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.sender-create-wizard__stepper {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
min-height: 0 !important;
|
||||
height: 100% !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* MudStepper internal wrappers must pass height down without scrolling. */
|
||||
.sender-create-wizard__stepper .mud-stepper-content,
|
||||
.sender-create-wizard__stepper .mud-stepper-nav-content,
|
||||
.sender-create-wizard__stepper .mud-stepper-content-wrapper {
|
||||
flex: 1 1 auto !important;
|
||||
min-height: 0 !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.sender-create-wizard__step-content,
|
||||
.sender-create-wizard__step-layout {
|
||||
flex: 1 1 auto !important;
|
||||
min-height: 0 !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
/* The actual scroll area for every step, mirroring the receiver list behavior. */
|
||||
.sender-create-wizard__step-main {
|
||||
flex: 1 1 auto !important;
|
||||
min-height: 0 !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
scrollbar-gutter: stable;
|
||||
padding-right: 0.2rem;
|
||||
}
|
||||
|
||||
.sender-create-wizard-popup .dxbl-popup-footer {
|
||||
@@ -1121,6 +1202,7 @@
|
||||
border-radius: 10px;
|
||||
background: rgba(126, 34, 206, 0.04);
|
||||
padding: 0.75rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sender-create-wizard__stepper {
|
||||
@@ -1137,7 +1219,7 @@
|
||||
}
|
||||
|
||||
.sender-create-wizard__stepper .mud-step-label {
|
||||
color: #1f2937;
|
||||
color: #334155;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -1153,22 +1235,53 @@
|
||||
.sender-create-wizard__stepper .mud-step-icon {
|
||||
color: #64748b;
|
||||
background: #f8fafc;
|
||||
border: 1px solid rgba(126, 34, 206, 0.25);
|
||||
border: 1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
.sender-create-wizard__stepper .mud-step-completed .mud-step-icon,
|
||||
.sender-create-wizard__stepper .mud-step-current .mud-step-icon {
|
||||
color: #ffffff;
|
||||
border-color: transparent;
|
||||
.sender-create-wizard__stepper .mud-step:not(.mud-step-completed) .mud-step-icon {
|
||||
position: relative;
|
||||
color: transparent;
|
||||
background: #e2e8f0;
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
|
||||
.sender-create-wizard__stepper .mud-step:not(.mud-step-completed) .mud-step-icon::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0.55rem;
|
||||
height: 0.55rem;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #64748b;
|
||||
background: #64748b;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.sender-create-wizard__stepper .mud-step-current:not(.mud-step-completed) .mud-step-icon::before {
|
||||
border-color: #0e7490;
|
||||
background: #22d3ee;
|
||||
}
|
||||
|
||||
.sender-create-wizard__stepper .mud-step-current .mud-step-label {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.sender-create-wizard__stepper .mud-step-current .mud-step-icon {
|
||||
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
|
||||
box-shadow: 0 0 0 3px rgba(126, 34, 206, 0.15);
|
||||
box-shadow: 0 0 0 3px rgba(14, 116, 144, 0.2);
|
||||
}
|
||||
|
||||
.sender-create-wizard__stepper .mud-step-completed .mud-step-icon {
|
||||
background: #2a5298;
|
||||
box-shadow: 0 0 0 2px rgba(22, 163, 74, 0.2);
|
||||
}
|
||||
|
||||
.sender-create-wizard__stepper .mud-step-completed .mud-step-label {
|
||||
color: #14532d;
|
||||
}
|
||||
|
||||
.sender-create-wizard__stepper .mud-step-completed .mud-step-content,
|
||||
.sender-create-wizard__stepper .mud-step-current .mud-step-content {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.sender-create-wizard__stepper-description {
|
||||
@@ -1183,6 +1296,33 @@
|
||||
margin-top: 0.7rem;
|
||||
}
|
||||
|
||||
.sender-create-wizard__step-layout {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sender-create-wizard__step-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sender-create-wizard__wizard-side {
|
||||
min-width: 0;
|
||||
max-width: 280px;
|
||||
border: 1px solid rgba(126, 34, 206, 0.12);
|
||||
border-radius: 10px;
|
||||
background: rgba(126, 34, 206, 0.04);
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
opacity: 1;
|
||||
transition: opacity 220ms ease 0.2s, transform 220ms ease 0.2s;
|
||||
}
|
||||
|
||||
.sender-create-wizard__wizard-side .sender-pdf-dropzone {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.sender-create-wizard__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1296,6 +1436,25 @@
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.sender-create-wizard__label-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.sender-create-wizard__help-icon {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
padding: 0;
|
||||
min-width: 1.2rem;
|
||||
min-height: 1.2rem;
|
||||
}
|
||||
|
||||
.sender-create-wizard__help-icon .mud-icon-root {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.sender-create-wizard__required {
|
||||
color: #dc2626;
|
||||
}
|
||||
@@ -1452,6 +1611,31 @@
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sender-create-wizard__receiver-existing-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding-right: 0.2rem;
|
||||
}
|
||||
|
||||
.sender-create-wizard__receiver-existing-list > .sender-create-wizard__receiver-row--new {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sender-create-wizard__receiver-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.sender-create-wizard__receiver-fields-stack {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.sender-create-wizard__receiver-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1463,6 +1647,11 @@
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.sender-create-wizard__receiver-item-fields {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sender-create-wizard__receiver-name {
|
||||
font-size: 0.86rem;
|
||||
font-weight: 700;
|
||||
@@ -1484,6 +1673,41 @@
|
||||
padding: 0.25rem 0.55rem;
|
||||
}
|
||||
|
||||
.sender-create-wizard__receiver-remove-btn {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
height: auto;
|
||||
align-self: stretch;
|
||||
aspect-ratio: 1 / 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
color: #b91c1c;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sender-create-wizard__receiver-remove-btn:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.sender-create-wizard__receiver-add-inline-btn {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
height: auto;
|
||||
align-self: stretch;
|
||||
aspect-ratio: 1 / 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
color: #0f766e;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.sender-create-wizard__upload-stage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1539,6 +1763,11 @@
|
||||
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.16);
|
||||
}
|
||||
|
||||
.sender-pdf-dropzone--has-document {
|
||||
border-color: rgba(16, 185, 129, 0.55);
|
||||
background: linear-gradient(180deg, rgba(16, 185, 129, 0.16), rgba(16, 185, 129, 0.07));
|
||||
}
|
||||
|
||||
.sender-pdf-dropzone--disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
@@ -1561,6 +1790,18 @@
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.sender-pdf-dropzone__doc-name {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
color: #065f46;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.sender-pdf-dropzone__doc-size {
|
||||
font-size: 0.76rem;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.sender-pdf-dropzone__input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -1625,8 +1866,8 @@
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.sender-create-wizard-popup .dxbl-popup {
|
||||
width: 96vw !important;
|
||||
max-width: 96vw !important;
|
||||
width: 90vw !important;
|
||||
max-width: 90vw !important;
|
||||
}
|
||||
|
||||
.sender-create-wizard__progress {
|
||||
@@ -1650,10 +1891,41 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.sender-create-wizard__receiver-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.sender-create-wizard__receiver-existing-list {
|
||||
max-height: min(20vh, 180px);
|
||||
}
|
||||
|
||||
.sender-create-wizard__receiver-fields-stack {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sender-create-wizard__receiver-remove {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sender-create-wizard__body-layout {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.7rem;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sender-create-wizard__wizard-side {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.sender-create-wizard__receiver-remove-btn {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.sender-create-wizard__receiver-add-inline-btn {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.sender-pdf-dropzone {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user