Compare commits

...

26 Commits

Author SHA1 Message Date
89a4e789f1 Improve popup layout and scrolling behavior
Updated `CreateEnvelopeWizardPopup.razor` to enforce consistent popup dimensions by setting height properties to `85vh` and disabling scrolling.

Enhanced `sender-page.css` with new classes to manage flexible layouts and scrolling behavior for the wizard's stepper and step content. Removed fixed height and scrolling constraints from `.sender-create-wizard__receiver-existing-list` for better layout flexibility.

These changes improve the user experience by ensuring consistent dimensions, reducing unnecessary scrolling, and maintaining a structured layout.
2026-11-11 23:50:45 +01:00
94658b0d9b Simplify wizard flow and improve document upload UX
Reduced wizard steps from 5 to 4 by merging document upload into step 3. Added a new localized hint (`SenderWizard_FinalUploadHint`) to guide users during the final document upload. Removed redundant document preview functionality and associated UI elements.

Refactored the document upload area to always remain visible on the right side of the wizard, using the `PdfFileDropZone` component. Removed the "Skip" button for optional steps and cleaned up unused methods, properties, and CSS classes.

Updated step titles and descriptions to reflect the new flow. General code cleanup to simplify logic and improve maintainability.
2026-11-11 17:24:39 +01:00
6ad76fa4b0 Improve PDF preview UX in SenderPage.razor
Updated button labels and popup headers to reflect a change in functionality from "Preview" to "Show Original Document." Added an informational hint in the PDF preview viewer to notify users about the absence of signature fields, with auto-hide and dismiss functionality. Introduced state management for the hint display, including methods to show and dismiss it, and ensured proper reset on preview close. Enhanced localization and styling for better user clarity.
2026-11-11 16:58:47 +01:00
31933f5568 Add "Send Confirmation" feature for envelope sending
Introduced a "Send Confirmation" popup to improve user experience
when sending envelopes. The popup allows users to review recipients,
signature fields, and settings before confirming the send action.

- Added localized strings in `Resource.de-DE.resx`, `Resource.en-US.resx`,
  and `Resource.fr-FR.resx` for confirmation messages and actions.
- Updated `SenderEditorPage.razor` to include the confirmation popup
  (`DxPopup`) with body and footer templates.
- Added `RequestSendAsync` and `ConfirmSendAsync` methods to manage
  the confirmation process and sending logic.
- Replaced the old success popup logic with direct navigation to the
  sender dashboard after a successful send.
- Updated the toolbar button to trigger the confirmation popup.
- Styled the confirmation popup in `envelope-viewer.css`, including
  support for dark themes.
- Ensured proper state management for the popup and sending process.

These changes enhance usability by providing a clear confirmation
step and progress feedback during envelope sending.
2026-11-11 16:17:34 +01:00
47c4f33dfc Refactor and enhance SenderPage grid and UI behavior
Refactored the `DxGrid` component in `SenderPage.razor` to improve readability, maintainability, and user experience. Added new grid properties like `HighlightRowOnHover` and `DetailExpandButtonDisplayMode`. Simplified and reorganized column and template structures.

Enhanced detail row behavior to handle row expansion and collapse more intuitively. Updated receiver and history displays with progress bars, badges, and improved styling.

Performed code cleanup by removing redundant blocks, consolidating logic, and eliminating unused variables. Refactored `ThemeService.LoadFromServerAsync` for better readability.

Made minor adjustments to the `CreateEnvelopeWizardPopup` configuration and improved general UI elements for better alignment, spacing, and styling.
2026-11-11 14:07:19 +01:00
26d41d9178 Add AutoCollapseDetailRow and improve attribute layout
The `AutoCollapseDetailRow=true` attribute was added to enable automatic collapsing of detail rows in the grid. Additionally, the `LayoutAutoSaving` attribute was moved to its own line for improved readability.
2026-11-11 09:24:31 +01:00
238c938ce7 Optimize post-auth calls with parallel execution
Refactored the code to improve performance by parallelizing independent post-authentication service calls using `Task.WhenAll`. This reduces the initial page load time by avoiding redundant HTTP calls and reusing pre-fetched data.

- Moved `ThemeService.LoadFromServerAsync` to a separate task.
- Parallelized service calls for theme, logos, envelope types, and envelopes.
- Introduced `ApplyEnvelopeResultAsync` to process envelope data.
- Updated `LoadEnvelopesAsync` to reuse pre-fetched envelope data.
- Removed redundant HTTP calls to streamline initialization logic.
2026-11-11 09:15:46 +01:00
5fb7b5a79e Enforce behavior for AdvancedElectronicSignature
Added logic to enforce automatic enabling of Two-Factor Authentication (2FA) and access code usage when the `AdvancedElectronicSignature` certification type is selected.

- Added `@bind:after` event handler to `<select>` for `_certificationType` to handle changes.
- Introduced `IsFesSelected` property to check if `AdvancedElectronicSignature` is selected.
- Updated `OnTfaChanged` to enforce `_tfaEnabled` and `_useAccessCode` when `IsFesSelected`.
- Added `OnCertificationTypeChanged` method to reset validation and normalize settings.
- Updated `NormalizeSettings` to ensure `_tfaEnabled` and `_useAccessCode` are set appropriately.
- Called `NormalizeSettings` during component initialization to ensure consistent state.

These changes ensure the UI and logic reflect the enforced behavior for the `AdvancedElectronicSignature` certification type.
2026-11-10 21:17:30 +01:00
2c8b65d576 Add localized label to signature field button
A `<span>` element with the class `pdf-toolbar__btn-text` was added to the signature field button. This element displays localized text for "Signaturfeld" (Signature Field) using the `@L` localization function. The change improves clarity and accessibility by adding a visible label next to the button's icon.
2026-11-10 21:06:00 +01:00
e1f24249a9 Add confirmation popup for clearing signature fields
Added a confirmation popup to `SenderEditorPage.razor` for clearing all signature fields, requiring user confirmation before proceeding. Updated resource files (`Resource.de-DE.resx`, `Resource.en-US.resx`, `Resource.fr-FR.resx`) with localized confirmation messages. Modified the "Remove all signature fields" button to trigger the popup instead of directly invoking the clear action.

Introduced a new `_clearFieldsConfirmVisible` variable to manage popup visibility. Updated the `ClearAllFieldsAsync` method to close the popup before clearing fields. Added new CSS styles in `envelope-viewer.css` to style the confirmation popup and its elements.
2026-11-10 21:00:36 +01:00
f6dc454193 Adjust popup width for better responsiveness
Updated the `DxPopup` component in `CreateEnvelopeWizardPopup.razor` to use `90vw` for both width and max-width, replacing the previous values of `860px` and `96vw`. Corresponding changes were made in `sender-page.css` for `.sender-create-wizard-popup .dxbl-popup` under the media query for screens with a max width of `760px`. These changes improve layout consistency and responsiveness.
2026-11-10 19:37:46 +01:00
b65098fafb Improve document upload UX and refactor wizard layout
Enhanced the `CreateEnvelopeWizardPopup` component:
- Added a new grid layout (`sender-create-wizard__body-layout`) to separate main content and side panel.
- Introduced a collapsible side panel for document upload and preview.
- Replaced old upload logic with `PdfFileDropZone` and added support for displaying document metadata (name, size) and replacement hints.
- Added asynchronous document preview rendering with `EnsureDocumentPreviewPreparedAsync`.

Localization updates:
- Added new localized strings for document upload hints in `de-DE`, `en-US`, and `fr-FR`.

CSS updates:
- Styled the new layout and side panel with transitions.
- Updated `PdfFileDropZone` styles for different states (e.g., document uploaded).

Refactored code for better maintainability and improved user feedback during document upload and preview preparation.
2026-11-10 19:29:33 +01:00
c6fe7722b4 refactor(AGENTS.md): add todo 2026-11-10 17:58:58 +01:00
2065102fb3 Update default zoom level in PDF editor to 180%
The `_zoomPercent` variable was updated from 112 to 180, increasing the default zoom level for the PDF image editor. This change ensures that users start with a higher magnification when interacting with the editor.
2026-11-10 17:19:51 +01:00
858b748e65 Refactor receiver list UI and improve accessibility
Reorganized the receiver list in `CreateEnvelopeWizardPopup.razor`:
- Separated "existing receivers" and "new receiver" input rows.
- Added editable fields for `FullName` and `EmailAddress` with real-time updates.
- Introduced "Remove" and "Add Receiver" buttons with updated icons and accessibility attributes.

Updated `sender-page.css`:
- Styled `.sender-create-wizard__receiver-existing-list` for better usability and responsiveness.
- Improved button designs with hover effects and consistent sizing.
- Simplified layout and removed redundant styles.

Enhanced responsiveness and accessibility:
- Made receiver list scrollable and visually distinct.
- Improved localization support with updated titles and `aria-labels`.

Removed redundant code to streamline implementation.
2026-11-10 09:41:38 +01:00
25c03b350b Refactor: Remove unused receiver filtering and list UI
Removed the title and input field for filtering recent receivers
from the suggestions section. Also removed the label, tooltip,
and help icon for the receiver list, along with the container
used to display added receivers. These elements were no longer
needed in the updated UI flow.
2026-11-09 16:34:43 +01:00
ed9ea0ac54 Refactor receiver row layout in CreateEnvelopeWizard
Refactor the structure and class names in `CreateEnvelopeWizardPopup.razor` to improve the layout and alignment of receiver rows:
- Replace `sender-create-wizard__receiver-item` with `sender-create-wizard__receiver-row`.
- Replace `sender-create-wizard__form-grid` with `sender-create-wizard__receiver-fields-stack`.

Update `sender-page.css` to introduce new classes:
- Add `.sender-create-wizard__receiver-row` for flex layout with proper alignment and spacing.
- Add `.sender-create-wizard__receiver-fields-stack` for grid-based field organization.

Adjust styles for responsiveness, including column layouts for smaller contexts. These changes enhance the visual structure and responsiveness of the receiver rows.
2026-11-09 16:16:08 +01:00
18c1adb68a Refactor receiver management UI in wizard
Simplify and restructure the UI for managing receivers in the sender creation wizard. Replace separate fields for adding new receivers with an inline approach, allowing users to add receivers directly within the receiver list.

Introduce a new inline receiver item with fields for name and email, along with an "Add" button. Update the receiver list layout to accommodate the new design.

Add styles for the inline receiver item and "Add Receiver" button in `sender-page.css`, including hover effects, disabled states, and alignment adjustments. Ensure the new design integrates seamlessly with the existing layout.
2026-11-09 16:08:00 +01:00
ec8b9adb9c Refactor receiver management in wizard popup
Replaced `DxTagBox` with a table-like structure for managing receivers, enabling direct editing of `FullName` and `EmailAddress` fields. Added validation for required fields and duplicate email addresses. Updated `RemoveReceiver` functionality with a new button and improved accessibility.

Removed `_receiverTags` and related logic, simplifying state management. Introduced new methods for handling receiver updates. Updated CSS to style the new layout and components. Improved code clarity and user experience.
2026-11-09 15:51:07 +01:00
4a10db0865 Refactor recent suggestion handling to use async method
Replaced the synchronous `ApplyRecentSuggestion` method with the new asynchronous `ApplyRecentSuggestionAndAddAsync` method. Updated the `@onclick` event handler in the suggestion list to use the async method. Removed `_validationMessage = null;` from the logic, and added an `await AddReceiver()` call to handle receiver addition asynchronously. These changes improve responsiveness and support additional async operations.
2026-11-09 13:34:17 +01:00
ceb2860883 Add filter for recent receiver suggestions
Introduced a filter input box in `CreateEnvelopeWizardPopup.razor` to allow users to search recent receiver suggestions dynamically. Added `_receiverSuggestionFilterText` property and `OnReceiverSuggestionFilterChanged` method to handle filtering logic.

Refactored `ReceiverSuggestionFilterHandler` to simplify its API. Updated the `Filter` method to accept a single query string and removed redundant overloads. Improved filtering logic to match `FullName` or `EmailAddress` using case-sensitive comparison.

These changes enhance usability by enabling quick filtering of receiver suggestions.
2026-11-09 13:14:31 +01:00
11e72bac53 Update resource keys and labels for final emails
Updated "FinalEmailToCreator" and "FinalEmailToReceivers" text in `.resx` files to use "Completed Document" terminology in English, German, and French. Adjusted corresponding labels, tooltips, and aria-labels in `CreateEnvelopeWizardPopup.razor` to reflect these changes in German.
2026-11-09 11:33:44 +01:00
638b734648 Enhance UI with tooltips and MudBlazor integration
Added contextual tooltips (`MudTooltip` and `MudIconButton`) to form fields in `CreateEnvelopeWizardPopup.razor` for improved user guidance. Updated label layouts using the new `sender-create-wizard__label-row` class for better alignment with tooltips. Integrated MudBlazor components (`MudThemeProvider`, `MudPopoverProvider`, `MudDialogProvider`, `MudSnackbarProvider`) in `SenderPage.razor` to enable enhanced UI features.

Updated `_Imports.razor` to include `MudBlazor` for global availability. Enhanced accessibility by adding `aria-label` attributes to help icons. Improved user experience with detailed explanations for fields like "Title," "Envelope Type," and others. Updated checkbox labels and numeric input fields with tooltips for additional context.

Added new CSS styles in `sender-page.css` to ensure proper alignment and sizing of tooltips and icons. These changes improve the overall usability, accessibility, and design consistency of the application.
2026-11-09 11:22:53 +01:00
eec1b4926b Update localization for wizard step titles
Added a new localization key `SenderWizard_Step_Start` to resource files for German, English, and French locales. Updated `CreateEnvelopeWizardPopup.razor` to replace `SenderWizard_Step_Basics` with `SenderWizard_Step_Start` for step 0, ensuring consistent naming of the first wizard step across the application.
2026-11-09 09:35:10 +01:00
d7611a6b59 Enhance stepper functionality and visual design
Added new properties to `<MudStepper>` for customization and
visual feedback. Introduced `_completedSteps` to track step
completion dynamically. Updated logic to manage completed
steps and reset state. Improved CSS for stepper appearance,
including step labels, icons, and content differentiation.
These changes improve usability and flexibility of the
stepper component.
2026-11-09 09:13:58 +01:00
0cf603fc79 Swap steps 1 and 3 in CreateEnvelopeWizardPopup
Reorganize the wizard steps to prioritize "Receivers" (step 1)
before "Message and Options" (step 3). Update `_currentStep`
logic, conditional checks, and validation to reflect the new
step order. Adjust `GetStepTitle` and `GetStepDescription`
methods to align with the updated step titles and descriptions.
Ensure user input handling and step-skipping behavior are
consistent with the new flow.
2026-11-05 16:18:17 +01:00
16 changed files with 1278 additions and 702 deletions

View File

@@ -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.

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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)

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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%;
}