Compare commits

..

50 Commits

Author SHA1 Message Date
77379265c2 Add dotnet-tools.json and publish script for .NET 8
Added a `dotnet-tools.json` file to define a .NET tool manifest, specifying the `dotnet-ef` tool with version `10.0.9`.

Introduced a `publish.bat` script for publishing the `EnvelopeGenerator.Server` project as a self-contained application targeting `win-x64` and .NET 8. The script handles cleaning, publishing, output verification, and provides deployment instructions for IIS.

Updated the `EnvelopeGenerator.sln` file to include a reference to the `publish.bat` script under the `EnvelopeGenerator.Server` project using a `SolutionItems` section.
2026-07-02 15:53:49 +02:00
9b68c09b0b Update project version to 1.0.1-beta
Updated `<Version>`, `<FileVersion>`, and `<AssemblyVersion>` in `EnvelopeGenerator.Server.csproj` to reflect a minor version update (1.0.0-beta -> 1.0.1-beta). This likely includes bug fixes, small improvements, or other non-breaking changes.
2026-07-02 15:51:36 +02:00
cd6bf05352 Add self-contained deployment and production config
Updated the IISProfileNet8.pubxml file to enable self-contained deployment by setting `<SelfContained>` to `true`. Target runtime was specified as `win-x64` using the `<RuntimeIdentifier>` property. Configured the environment for production by adding the `<EnvironmentName>` property set to `Production`.
2026-07-02 15:50:39 +02:00
b2c205b160 Add Publish & Deployment Guide to README.md
Added a detailed "Publish & Deployment Guide" to the README for the `EnvelopeGenerator.Server` project. The guide includes:

- Table of contents for easy navigation.
- Explanation of differences between `EnvelopeGenerator.Server` and a standard ASP.NET Core API.
- Justification for self-contained publishing and its benefits.
- Recommended `dotnet publish` command with parameter explanations.
- IIS configuration steps, including Application Pool settings, `web.config` adjustments, and module requirements.
- Verification checklist for published output files.
- Instructions for setting up logging and recycling the IIS Application Pool.
- Directory structure overview post-publish.
- Troubleshooting common deployment errors with solutions.

This guide ensures developers and administrators can successfully publish and deploy the application.
2026-07-02 15:44:36 +02:00
33439f4a65 Update .gitignore and modify server files
The `.gitignore` file was updated to ignore `FodyWeavers.xsd`.
Added a new directory `/publish-output` to the server folder.
The file `tekh_softHSM_test.md` was removed and re-added,
indicating potential modifications or reorganization.
2026-07-02 15:44:05 +02:00
b5cdf79799 Update .gitignore, add files, and modify configurations
Updated `.gitignore` to include `MigrationBackup/`. Added new files: `FodyWeavers.xsd`, `annotations.json`, and two markdown files for SoftHSM testing. Modified `/EnvelopeGenerator.Web/.config/dotnet-tools.json`, `/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode`, and `/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs` to reflect configuration and testing updates.
2026-07-02 12:44:12 +02:00
72915ac001 Add default descending sort to ID column in grid
Updated the `DxGridDataColumn` for the `Id` field in
`EnvelopeSenderPage.razor` to include default sorting.
The column now has `SortIndex=0` and `SortOrder=GridColumnSortOrder.Descending`,
ensuring the grid sorts by ID in descending order by default.
2026-07-02 02:13:56 +02:00
e4d7a0d8f3 Update project metadata and versioning scheme
Updated `<Version>` to `1.0.0-beta` to reflect a pre-release version. Aligned `<FileVersion>` and `<AssemblyVersion>` with the new versioning scheme (`1.0.0.0`). Replaced `<PackageOutputPath>` with `<Copyright>` for better metadata alignment. No changes to `<DocumentationFile>`.
2026-07-02 02:13:15 +02:00
d5f9de06f3 Update project metadata and enable XML documentation
Replaced `<TargetFramework>` with `<TargetFrameworks>` to allow for future multi-targeting. Enabled XML documentation file generation with `<GenerateDocumentationFile>`. Added project metadata including `<PackageId>`, `<Authors>`, `<Company>`, `<Product>`, and versioning properties (`<Version>`, `<FileVersion>`, `<AssemblyVersion>`). Included copyright notice and documentation file path configuration.
2026-07-02 02:05:47 +02:00
bbc129178f Add IIS publish profile for .NET 8.0 deployment
A new `IISProfileNet8.pubxml` file was added to configure publishing settings for a .NET 8.0 application. Key configurations include:

- Publish method set to `Package`.
- Build configuration set to `Release` and platform to `Any CPU`.
- Automatic site launch after publishing enabled.
- `App_Data` folder included in the package.
- Unique project identifier defined.
- Build package location specified with a version placeholder.
- Package created as a single file.
- IIS application path set to `EnvelopeGenerator`.
- Deployment target set to `IISWebDeployPackage`.
- Target framework set to .NET 8.0.
2026-07-02 02:03:37 +02:00
40c95100ff Add Title and Message fields to Envelope editor
Introduced input fields for "Title" and "Message" in the
EnvelopeSenderEditorPage, allowing users to specify metadata
for envelopes. The "Title" field is required and validated,
while the "Message" field is optional with a default value.

Updated the save logic to validate the title and display
appropriate error messages. Persisted the "Title" and
"Message" fields in the session cache via the
`EditorSessionData` record.

Modified the `CreateEnvelopeReceiverCommand` to use the
user-provided "Title" and "Message" values. Adjusted the UI
layout and styling to accommodate the new fields, ensuring
a seamless user experience.
2026-07-02 02:00:14 +02:00
1b2731b4b2 Simplify URL structure and update logging dependencies
Updated navigation logic and routes to remove the `/report` suffix, simplifying the URL structure for envelope-related pages.

- Updated `LoginReceiverPage.razor` and `ReceiverSignedPage.razor` to redirect to `/envelope/{EnvelopeKey}`.
- Changed `ReceiverPage.razor` route to `/envelope/{EnvelopeKey}`.
- Updated `ILogger` dependencies in `ReceiverPage.razor` and `ReceiverSignedPage.razor` to reflect new class names.
- Modified `EnvelopeReceiverPage.razor` route to `/envelope/{EnvelopeKey}/deprc`, indicating deprecation or restructuring.
2026-07-02 01:59:48 +02:00
ae56902758 Add SenderAuthCookieHandler for cookie-based JWT auth
Added a custom DelegatingHandler, SenderAuthCookieHandler, to forward the browser's Cookie header to outgoing HttpClient requests in Blazor Server. Registered the handler as a transient service and integrated it into the named HttpClient pipeline for internal API calls. This enables Blazor Server components to make authenticated API calls using cookie-based JWT authentication (AuthScheme.Sender).
2026-07-02 01:48:25 +02:00
e80059d34e Add envelope creation functionality
Introduced the ability to create envelopes with documents and receivers via a new `CreateAsync` method in `EnvelopeReceiverService`. Integrated this functionality into `EnvelopeSenderEditorPage.razor` with UI updates, including a save button spinner, validation checks, and a result popup for success or error feedback.

- Added `CreateAsync` method to handle `POST /api/EnvelopeReceiver` API calls.
- Injected `EnvelopeReceiverService` into `EnvelopeSenderEditorPage.razor`.
- Implemented save logic with validation for PDF upload, receivers, and signature fields.
- Added success and error popups for user feedback.
- Improved logging for envelope creation and validation warnings.
- Refactored save logic for better readability and maintainability.
2026-07-02 01:44:53 +02:00
a2443032c5 Add logging for stored procedure failure cases
Added warning logs in `EnvelopeReceiverController` to handle
cases where stored procedures return `OUT_SUCCESS=false`.
For `PRSIG_API_ADD_DOC_RECEIVER_ELEM`, log `DOC_ID`,
`RECEIVER_ID`, and `Page`. For `PRSIG_API_ADD_HISTORY_STATE`,
log `EnvelopeUuid`. These changes enhance error visibility
and debugging.
2026-07-02 01:12:44 +02:00
3d2fe532e5 Change @OUT_RECEIVER_ID type from int to bigint
Updated the SQL query in `EnvelopeReceiverAddReadSQL.cs` to change the data type of the `@OUT_RECEIVER_ID` variable from `int` to `bigint`. This modification ensures support for larger identifier values, addressing potential limitations of the previous `int` type.
2026-07-02 01:12:28 +02:00
5e83aa26c9 Refactor authentication and enhance logging
Updated `Authorize` attributes in multiple controllers to use
`AuthenticationSchemes = AuthScheme.Sender` instead of
`Policy = AuthPolicy.Sender`, reflecting a shift in the
authentication mechanism.

Added detailed logging in `EnvelopeReceiverController` to handle
cases where stored procedures return `OUT_SUCCESS=false`,
providing contextual information for debugging.

Removed unused SQL code and declarations in
`EnvelopeReceiverController` to improve code readability and
maintainability.
2026-07-02 01:11:09 +02:00
2c789cd4c0 Add dynamic color support for receivers
Introduced a `Color` property to `ReceiverDraft` and `SignatureFieldDraft` models, enabling dynamic color assignment from a predefined palette (`ReceiverPalette`). Updated the UI to reflect receiver-specific colors in the sender-receiver chips, placement mode hint bar, and signature placement button.

Refactored PDF rendering logic to dynamically derive visual styles (fill, border, and text colors) from receiver colors. Added a `HexToXColor` utility for converting hex color strings to `PdfSharp.Drawing.XColor`.

Removed hardcoded visual styles and replaced them with dynamic, receiver-specific styling. Simplified receiver addition logic to automatically assign colors from the palette. These changes improve clarity and maintainability while enhancing the user experience.
2026-07-01 23:27:38 +02:00
9ecfe08e2e Move session ID logic to OnAfterRenderAsync
The session ID generation and redirection logic was moved from
OnInitialized to OnAfterRenderAsync to address issues with
NavigationException during SSR prerendering. OnInitialized is
now intentionally left empty, and the new OnAfterRenderAsync
method ensures the session ID is appended to the URL only after
the first interactive render, when SignalR is connected and
NavigateTo is safe.
2026-07-01 23:10:39 +02:00
74da6e37b0 Refactor and enhance PDF signature editor
- Added `IMemoryCache` for session persistence and caching.
- Introduced session persistence via query parameter (`esid`).
- Replaced overlay-based click handling with normalized PDF coordinates.
- Added `PdfSharp` integration to render signature placeholders.
- Updated button behavior for receiver-specific signature placement.
- Improved receiver popup validation and email suggestion handling.
- Removed unused overlay synchronization logic.
- Refactored CSS for better PDF viewer layout and toolbar alignment.
- Enhanced logging with additional context for signature actions.
- General code cleanup for readability and maintainability.
2026-07-01 22:50:47 +02:00
10f65e583a Enhance IndexPage UI and navigation functionality
- Injected `NavigationManager` into `IndexPage.razor` to enable navigation.
- Improved layout alignment with additional classes in `<div>`.
- Added a new button for navigation to `/sender` with icons and text.
- Introduced a feature badge for "PDF-Export" with an SVG icon.
- Updated `.home-btn-primary` styles in `app.css`:
  - Ensured text color is always white with `!important`.
  - Made text bolder by increasing `font-weight` to `700`.
2026-07-01 20:09:44 +02:00
0cdc8f1191 Add "Cancel" button and navigation to sender page
Added the `NavigationManager` service injection to the
`EnvelopeSenderEditorPage.razor` file to enable navigation.
Introduced a "Cancel" button in the toolbar, styled with
`pdf-toolbar__btn pdf-toolbar__btn--reset`, which triggers
the `Cancel` method on click. The `Cancel` method navigates
the user to the `/sender` route, improving the user experience
by providing a clear way to cancel the current operation.
2026-07-01 19:28:30 +02:00
56ccab6377 Refactor email suggestion handling logic
Refactored the handling of email suggestions to separate selection
and commitment logic. Updated `<DxListBox>`'s `ValueChanged`
handler to use `OnReceiverEmailSuggestionCommittedAsync` for
finalizing selections. Introduced `SelectReceiverEmailSuggestion`
as a synchronous helper method for managing selection updates.
Centralized clearing of suggestions in the new
`OnReceiverEmailSuggestionCommittedAsync` method. Simplified
keyboard navigation logic by replacing asynchronous calls with
synchronous selection handling. These changes improve code
clarity and reduce unnecessary asynchronous operations.
2026-07-01 19:15:40 +02:00
4d069cdaa0 Merge branch 'feat/migr-DxReportViewer' of http://git.dd:3000/AppStd/EnvelopeGenerator into feat/migr-DxReportViewer 2026-07-01 17:16:19 +02:00
bbbfa4de01 Add phone number support for receivers in UI
Added functionality to display a receiver's phone number in the
sender-receiver chip if provided. Introduced a new input field
in the receiver popup for entering an optional phone number.

Updated the `ReceiverDraft` model and `_receivers` list to
include and store phone numbers. Modified methods to handle
phone number input and saving. Added CSS styles for displaying
the phone number in the sender-receiver chip.
2026-07-01 17:16:15 +02:00
3ff3373b27 Improve email suggestion handling and keyboard navigation
Enhanced the email suggestion feature by adding keyboard navigation support (`ArrowUp`, `ArrowDown`, `Enter`, `Escape`) and introducing `_selectedReceiverEmailSuggestion` to track the current selection. Updated methods to synchronize input and suggestions, pre-select matching suggestions, and reset the state when the popup is opened or closed. Improved error handling and ensured clean state management for better user experience.
2026-07-01 17:10:54 +02:00
2af8815cf6 Replace DxButton with HTML buttons and update styles
Replaced DevExpress DxButton components with standard HTML
<button> elements in EnvelopeSenderEditorPage.razor for
"Add Receiver" and "Add Signature" actions. The new buttons
include SVG icons and text for improved customization and
styling.

Updated envelope-viewer.css to add new styles for the
buttons, including `.sender-toolbar-action-btn` and
`.sender-toolbar-action-btn--compact` classes. Removed
unused styles related to DxButton components and adjusted
`.sender-receiver-chip__action` for proper width handling.

These changes improve design flexibility and maintainability.
2026-07-01 17:02:19 +02:00
ca24a96084 Add receiver management to EnvelopeSenderEditorPage
Introduced a new "Receivers" panel in `EnvelopeSenderEditorPage` to manage receivers. Added a popup for adding receivers with validation, email suggestions, and caching for performance. Updated the layout to display receiver details and signature fields.

Injected `EnvelopeReceiverPageDataService` for receiver-related operations. Added a `ReceiverDraft` model and implemented methods for managing receivers. Enhanced CSS for the new UI elements and ensured responsiveness. Minor refactoring and cleanup included.
2026-07-01 16:32:28 +02:00
db368b889a Merge branch 'feat/migr-DxReportViewer' of http://git.dd:3000/AppStd/EnvelopeGenerator into feat/migr-DxReportViewer 2026-07-01 15:43:33 +02:00
8f451b9c2c Implement navigation for CreateEnvelope method
Replaced the placeholder console log in the `CreateEnvelope` method with actual navigation functionality. The method now uses `Navigation.NavigateTo` to redirect users to the `/sender/editor` page, enabling the intended behavior for creating envelopes.
2026-07-01 15:43:26 +02:00
6120e6062e Update ReceiverController namespace and enhance Get method
The namespace of the `ReceiverController` class was updated from `EnvelopeGenerator.GeneratorAPI.Controllers` to `EnvelopeGenerator.Server.Controllers`.

The `Get` method was enhanced with the following changes:
- Added `[Authorize]` attribute with `AuthScheme.Sender`.
- Updated the method signature to include an optional `ReadReceiverQuery? receiver` parameter (defaulting to `null`) and a new `bool onlyEmailAddress` parameter (defaulting to `false`).
- Modified logic to handle `receiver` being `null` by creating a new `ReadReceiverQuery`.
- Added handling for `onlyEmailAddress` to return a list of email addresses if true.
- Simplified the result handling and removed the previous `receiver.HasAnyCriteria` logic.
2026-07-01 15:16:17 +02:00
cc2aea90ed Enhance query criteria and add partial email matching
- Added `[NotMapped]` attribute to `HasAnyCriteria` in `ReceiverQueryBase` to exclude it from database mapping.
- Made `HasAnyCriteria` in `ReceiverQueryBase` virtual for overriding.
- Introduced `EmailAddressSearch` in `ReadReceiverQuery` for partial email matching.
- Overrode `HasAnyCriteria` in `ReadReceiverQuery` to include `EmailAddressSearch`.
- Updated `ReadReceiverQueryHandler` to support partial email matching using `EF.Functions.Like`.
2026-07-01 15:15:50 +02:00
762a9e8bca Improve PDF viewer overlay synchronization
Refactor `EnvelopeSenderEditorPage.razor` to enhance the structure and behavior of the PDF editor wrapper:
- Add `class="pdf-editor-wrapper"` and update `overflow` to `auto`.
- Update `DxPdfViewer`'s `CssClass` to `sender-editor-pdf-viewer`.
- Introduce `OnAfterRenderAsync` to synchronize the overlay with the viewer.

Add new styles in `envelope-viewer.css` for better layout:
- Ensure `.pdf-editor-wrapper` and `.sender-editor-pdf-viewer` occupy full dimensions.
- Center and align content within the PDF viewer.

Enhance `envelope-editor.js` with `syncOverlayToPage`:
- Dynamically adjust overlay position and size relative to the viewer.
- Use `MutationObserver` and event listeners for real-time synchronization.
- Handle delayed rendering with scheduled sync attempts.

These changes improve overlay alignment, user experience, and code maintainability.
2026-07-01 14:26:11 +02:00
6ed4caea4f Merge branch 'feat/migr-DxReportViewer' of http://git.dd:3000/AppStd/EnvelopeGenerator into feat/migr-DxReportViewer 2026-07-01 13:59:29 +02:00
d94821433a Enable WebAssembly mode and add blazing-berry theme
Added the `@rendermode InteractiveWebAssembly` directive to
`IndexPage.razor` to enable interactive WebAssembly rendering.
Included a `<link>` element to reference the `blazing-berry.bs5.min.css`
stylesheet from the `DevExpress.Blazor.Themes` content folder to
apply the "blazing-berry" theme for enhanced styling.
2026-07-01 12:58:25 +02:00
278b9964f1 Update signing confirmation text and logout navigation
The user-facing text in `EnvelopeReceiverReportSignedPage.razor` has been updated to provide a more detailed and formal confirmation message for signing a document.

- Replaced "Dokument erfolgreich unterschrieben" with "Möchten Sie das Dokument verbindlich unterschreiben?".
- Updated the follow-up message to clarify the irreversibility of the action and the electronic signing process.

Additionally, the logout navigation behavior has been modified:
- Changed the post-logout redirect from `/envelope/login/{EnvelopeKey}` to the root page (`/`), while retaining the `forceLoad` parameter.
2026-07-01 12:56:51 +02:00
e6722803bb Add submit confirmation popup and logout functionality
Added dependency injection for `AuthService`, `ReceiverAuthorizationService`, `PageDataService`, and `Logger` to enable their usage in the component. Introduced a "Submit" button in the UI to confirm the signing process and complete the workflow.

Implemented a `DxPopup` component to display a confirmation dialog when the "Submit" button is clicked. The popup includes a message about the successful signing of the document and asks the user to confirm whether to complete the process and log out.

Added state variables `_isLoggingOut` and `_submitConfirmVisible` to manage the popup visibility and logout process. Created `OpenSubmitConfirmPopup` to toggle the popup and `SubmitAndLogoutAsync` to handle the submission process, including logging out via `AuthService` and navigating to the login page.

Updated the `@code` block with the new state variables and methods for managing the submit and logout functionality.
2026-07-01 12:36:50 +02:00
47bc7675c9 Handle cache miss and redirect in SignedPage
Added a check for `_sig` being `null` to handle cache misses or missing `sid`. Logged a warning with `Sid` and `EnvelopeKey` details when this occurs. Implemented a redirection to the report page (`/envelope/{EnvelopeKey}/report`) using `Navigation.NavigateTo` with `forceLoad: true`. Added an early return to prevent further execution after redirection.
2026-07-01 11:47:09 +02:00
789e312316 Refactor signature box layout and improve readability
Refactored the signature box layout to dynamically calculate
positions based on content, improving flexibility and precision.
Introduced new constants (`lineH`, `bgPad`) to standardize
spacing and replaced hardcoded values for better maintainability.
Adjusted background rectangle sizing to fit content dynamically
and improved text layout logic to handle optional fields more
gracefully. Simplified image area logic and reduced redundant
calculations. Overall, improved code readability and alignment
for a cleaner, more compact layout.
2026-07-01 11:38:07 +02:00
2a9bbb3fe5 Add envelope editor with PDF upload and signature tools
Introduced a new Blazor page `EnvelopeSenderEditorPage.razor` for editing envelopes with an interactive interface. Integrated `DxPdfViewer` for rendering PDFs and added functionality for uploading, viewing, and interacting with PDF files.

Key features:
- Action bar with buttons for uploading PDFs, toggling signature placement mode, clearing fields, and saving.
- Placement mode for adding signature fields via an overlay, with visual placeholders.
- JavaScript interop (`envelope-editor.js`) for precise click coordinate mapping.
- Error handling for unsupported file types and size limits (50 MB).
- Logging for debugging key actions like PDF uploads and field placements.

Defined constants for accurate signature field dimensions and scaling. Added models (`SignatureFieldDraft`, `OverlayCoords`) to manage state and interactions.
2026-07-01 11:25:04 +02:00
bc34317720 Adjust signature box layout and add background color
Updated the signature box proportions by adjusting `imgRatio` to 52% and `textRatio` to 43%. Added a cream-tone background with extra padding (`bgPad`) and rendered it behind the signature content. Tightened gaps between the image, separator line, and text area. Increased text row height slightly for better spacing.
2026-07-01 11:12:16 +02:00
76ff3e47e1 Render signatures on PDF using PdfSharp
Added functionality to render captured signatures onto a PDF document using the PdfSharp library. Introduced a new `_signatures` field to store signature data and updated `OnInitializedAsync` to fetch and process signatures. Implemented the `DrawSignaturesOnPdf` method to overlay signature images, separator lines, and signer details (name, position, place, date) onto the PDF. Added a helper method `DataUrlToBytes` for decoding Base64-encoded signature images. Defined constants for layout and styling to ensure consistent rendering. Updated `Dispose` to clean up resources.
2026-07-01 10:30:35 +02:00
2d22bfcd06 Simplify signed document viewer logic
Removed all signature capture and validation functionality, including the signature popup, JavaScript interop, and related backend logic. Simplified the `DxReportViewer` initialization to directly display the signed document. Added support for retrieving cached signatures via the `sid` query parameter. Streamlined error handling, logging, and page metadata. Cleaned up unused imports, constants, and methods to reduce complexity.
2026-07-01 09:57:19 +02:00
185c783824 Improve signature handling and navigation stability
Updated button logic to display "Unterschreiben" only when signature fields exist. Changed `_signaturePopupVisible` to always be `false` for consistent popup behavior. Improved navigation after caching a signature by nullifying `_report`, adding a delay for UI updates, and using `forceLoad: true` for clean circuit teardown. These changes enhance user experience and prevent potential crashes.
2026-07-01 09:55:55 +02:00
b957b4b4bb Update navigation path for successful login
Changed the navigation path in `LoginReceiverPage.razor` to redirect users to `/envelope/{EnvelopeKey}/report` instead of `/envelope/{EnvelopeKey}` upon a successful login. The `forceLoad: true` parameter remains unchanged to ensure a full page reload.
2026-07-01 09:54:59 +02:00
df154d83cc Refactor signature caching and navigation logic
Replaced the previous signature persistence mechanism with
`IMemoryCache` for temporary storage of captured signatures
using a unique `Guid` key and a 1-minute TTL. Added logging
to track cached signatures and their associated envelope keys.

Removed the logic for rebuilding and displaying reports with
overlaid signatures. Instead, implemented navigation to a
new signed page (`/envelope/{EnvelopeKey}/signed`) with the
signature ID passed as a query parameter.
2026-07-01 01:41:19 +02:00
49ec9fbead Add PdfSharp font resolver for .NET 8 compatibility
Added a `PdfSharpFontResolver` class to enable font resolution
for PdfSharp in .NET 8, addressing the lack of system font
access. The resolver reads fonts from the Windows Fonts folder
and supports the Arial font family. Registered the resolver
globally in `Program.cs` using `GlobalFontSettings.FontResolver`.

Updated `Program.cs` with comments explaining the necessity of
the resolver for .NET 8. The resolver includes methods to map
font family names to specific font files and load font data.
Throws a `FileNotFoundException` if required fonts are missing.

Made minor formatting changes in `Program.cs` without altering
the `SwaggerDoc` description functionality.
2026-07-01 01:25:24 +02:00
01fc29f59e Refactor signature handling and add signed page UI
Refactored `EnvelopeReceiverReportPage.razor` to replace the logic for burning captured signatures with a new approach that draws placeholder boxes for signature fields using the `DrawSignaturePlaceholders` method. Removed the `BurnSignaturesIntoPdf` method and introduced `PDFsharp` for rendering placeholders.

Added `EnvelopeReceiverReportSignedPage.razor` to handle signed envelopes, including a detailed UI for document display, metadata, and a signature popup with "Draw," "Text," and "Image" modes. Integrated JavaScript interop for signature creation and validation.

Updated `EnvelopeGenerator.Server.csproj` to include the `PDFsharp` library. Enhanced error handling, logging, and UI feedback. Improved code readability and maintainability through cleanup and refactoring.
2026-07-01 01:25:09 +02:00
733b70cca2 Refactor PDF handling; remove iText dependency
Replaced iText-based PDF processing with DevExpress PdfGraphics API.
Removed `itext` and `itext.bouncy-castle-adapter` dependencies.
Simplified `BuildReport` to burn signatures directly into PDFs
and render all pages using `XRPdfContent` with `GenerateOwnPages = true`.
Consolidated subreport logic into `BuildReport` and removed
`BuildPageSubreport`. Eliminated unused constants and methods,
including `GetPdfPageCount`. Updated XML documentation to reflect
the new implementation. Placeholder implementation for
`BurnSignaturesIntoPdf` added, pending further development.
2026-06-30 23:55:39 +02:00
8f4b751303 Add envelope report page with signature capture
Added a new Razor page `EnvelopeReceiverReportPage.razor` to display and manage envelope reports at the route `/envelope/{EnvelopeKey}/report`. Integrated DevExpress Blazor Reporting components (`DxReportViewer`, `DxPopup`) for rendering PDF documents and capturing user signatures.

Implemented a multi-tab signature capture interface supporting drawing, text input with font selection, and image uploads. Added support for dynamically overlaying captured signatures on PDF documents using `XRPictureBox`.

Introduced dependency injection for services like `AuthService`, `ReceiverAuthorizationService`, and `PageDataService` to handle authentication, data retrieval, and logging. Included lifecycle methods for user authorization, PDF loading, and restoring cached signatures.

Added validation for signature input, error handling for missing data, and utility methods for building reports, extracting PDF page counts, and converting base64 data URLs. Integrated JavaScript interop for canvas-based signature handling.

Included custom styles and assets, and implemented disposal logic for cleaning up resources.
2026-06-30 23:27:20 +02:00
42 changed files with 4706 additions and 2948 deletions

4
.gitignore vendored
View File

@@ -365,3 +365,7 @@ FodyWeavers.xsd
/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode /EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode
/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs /EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs
/EnvelopeGenerator.Tests.Application/annotations.json /EnvelopeGenerator.Tests.Application/annotations.json
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/TekH - SoftHSM Test.md
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md

View File

@@ -185,79 +185,11 @@ For signature placeholders, the service:
Current receiver viewer characteristics: Current receiver viewer characteristics:
- route: `/envelope/{EnvelopeKey}` - route: `/envelope/{EnvelopeKey}`
- render mode: `InteractiveServer` - render mode: `InteractiveServer`
- PDF rendering: **Migration in progress from `PDF.js` to `DxPdfViewer`** - PDF rendering: `PDF.js`
- toolbar: page navigation, zoom, thumbnail toggle, signature navigation, signature reset - toolbar: page navigation, zoom, thumbnail toggle, signature navigation, signature reset
- signature popup: `DxPopup` - signature popup: `DxPopup`
- thumbnail sidebar: resizable and stored in `localStorage` - thumbnail sidebar: resizable and stored in `localStorage`
### ⚠️ CRITICAL: DevExpress DxPdfViewer Control Requirements
**Verified API for installed `DevExpress.Blazor.PdfViewer` v25.2.3:**
| Property | Access | Notes |
|----------|--------|-------|
| `DocumentContent` | `[Parameter]` GET/SET | Feed PDF as `byte[]` |
| `ZoomLevel` | `[Parameter]` GET/SET | **Factor** (not percentage): `1.5` = 150% |
| `IsSinglePagePreview` | `[Parameter]` GET/SET | Single page mode |
| `CssClass` | `[Parameter]` GET/SET | CSS class |
| `DocumentName` | `[Parameter]` GET/SET | Download filename |
| `SizeMode` | `[Parameter]` GET/SET | `Small` / `Medium` / `Large` |
| `PageCount` | Read-only GET | Total pages — **no JS call needed** |
| `ActivePageIndex` | Read-only GET | Current page (0-based) — **cannot SET** |
| `CustomizeToolbar` | Event | Only available toolbar event |
**Does NOT exist in v25.2.3 — do NOT use:**
- `GoToPageAsync()`
- `GoToNextPageAsync()`
- `ZoomAsync()`
- `PageNumberChanged` event ❌
- `ZoomLevelChanged` event ❌
- `ToolbarVisible` property ❌
**Correct approach:**
```razor
<DxPdfViewer @ref="_pdfViewer"
DocumentContent="@_pdfDocumentContent"
ZoomLevel="@_viewerZoomLevel"
IsSinglePagePreview="true"
CustomizeToolbar="OnCustomizeToolbar" />
```
```csharp
// ZoomLevel: always divide by 100 (factor, not percentage)
_viewerZoomLevel = _currentZoom / 100d; // 150 -> 1.5
// PageCount: read directly, no JS needed
_totalPages = _pdfViewer.PageCount;
// Page navigation: only via CustomizeToolbar buttons
protected void OnCustomizeToolbar(ToolbarModel toolbarModel)
{
toolbarModel.AllItems.Clear();
var nextButton = new ToolbarItem
{
IconCssClass = "dx-icon-chevronnext",
Enabled = _currentPage < _totalPages,
Click = async (args) =>
{
_currentPage++;
_viewerZoomLevel = _currentZoom / 100d;
await InvokeAsync(StateHasChanged);
await RenderSignatureButtonsAsync();
}
};
toolbarModel.AllItems.Add(nextButton);
}
```
**JavaScript role after migration:**
- Overlay geometry calculations only
- Thumbnail rendering via PDF.js helper
- Custom UI interactions (sidebar resize, signature canvas)
- **NOT** for controlling DxPdfViewer page or zoom
See `DEVEXPRESS_V25_LIMITATIONS.md` for complete verified API reference.
### JS Assets ### JS Assets
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js` - `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js` - `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js`

View File

@@ -1,300 +0,0 @@
# Debug Tools for DevExpress DxPdfViewer Integration
## Purpose
This document describes temporary debug tools added to diagnose DevExpress DOM structure and page navigation issues.
## IMPORTANT: TEMPORARY DEBUG CODE
**These debug tools are TEMPORARY and should be REMOVED after resolving the page navigation issue.**
---
## Debug Tools Added
### 1. Debug UI Button (Toolbar)
**Location:** `EnvelopeReceiverPage.razor` - Toolbar Section
**Visual:** Orange button with "?" icon in the PDF viewer toolbar
**What it does:**
- Opens a floating overlay panel showing DevExpress DOM analysis
- Displays all input elements found in DxPdfViewer
- Shows which CSS selectors successfully find the page input
- Provides a "Test: Go to Page 2" button for live testing
**Code Location:**
```razor
@* DEBUG: DevExpress DOM Inspector *@
<div class="pdf-toolbar__section">
<button class="pdf-toolbar__btn" @onclick="ShowDebugUI" ...>
```
**C# Method:**
```csharp
async Task ShowDebugUI()
{
await JSRuntime.InvokeVoidAsync("dxPdfViewerShowDebugUI");
}
```
---
### 2. JavaScript Debug Functions
**Location:** `pdf-viewer.js`
**Functions Added:**
#### `window.dxPdfViewerDebugDOM()`
- Console-based debug function
- Logs detailed DOM analysis to browser console
- Returns analysis object for programmatic inspection
#### `window.dxPdfViewerShowDebugUI()`
- HTML overlay-based debug function
- Creates visual debug panel without console interaction
- No security warnings (no need to paste code)
---
## How to Use
### Step 1: Run Application
```powershell
dotnet run --project EnvelopeGenerator.Server/EnvelopeGenerator.Server
```
### Step 2: Open Receiver Page
Navigate to: `https://localhost:8088/envelope/{EnvelopeKey}`
### Step 3: Click Debug Button
- Look for the **orange "?" button** in the PDF toolbar (left side, after thumbnails toggle)
- Click it to open the debug overlay
### Step 4: Review Debug Information
The overlay shows:
- **Total Inputs**: Number of input elements found
- **Input Elements**: Details of each input (type, class, ID, value)
- **Selector Tests**: Which CSS selectors work (✓) and which don't (✗)
- **Toolbar**: Whether toolbar element was found
- **DxWidget**: Whether DevExpress widget element was found
### Step 5: Test Page Navigation
Click the **"Test: Go to Page 2"** button in the overlay
### Step 6: Report Results
**Copy the following information:**
1. **Total Inputs**: X
2. **Input Details**: (type, className, id for each input)
3. **Selector Test Results**: (which selectors show ✓ FOUND)
4. **Test Result**: Did PDF actually navigate to page 2? (Yes/No)
5. **Console Messages**: Any errors or warnings in F12 console
---
## What to Look For
### ✓ Success Indicators
- At least one selector shows **✓ FOUND**
- "Test: Go to Page 2" button actually changes PDF page
- Console shows: `✓ Found page input with selector: "..."`
### ✗ Problem Indicators
- All selectors show **✗ NOT FOUND**
- "Test: Go to Page 2" does nothing
- Console shows: `✗ Page input not found`
---
## After Diagnosis
Once the correct selector is identified:
### 1. Update `window.dxPdfViewerGoToPage()`
Update the `selectors` array in `pdf-viewer.js` to prioritize the working selector:
```javascript
const selectors = [
'WORKING_SELECTOR_HERE', // ✓ Move this to top
'input[type="number"]',
// ... rest
];
```
### 2. Remove Debug Code
**Files to clean up:**
#### `EnvelopeReceiverPage.razor`
Remove:
```razor
@* DEBUG: DevExpress DOM Inspector *@
<div class="pdf-toolbar__section">
<button class="pdf-toolbar__btn" @onclick="ShowDebugUI" ...>
</button>
</div>
```
Remove C# method:
```csharp
async Task ShowDebugUI() { ... }
```
#### `pdf-viewer.js`
Remove:
```javascript
// ⚠ AUTO-DEBUG: Display results in HTML overlay
window.dxPdfViewerShowDebugUI = function() { ... }
```
Keep:
- `window.dxPdfViewerDebugDOM()` - can be useful for future debugging (optional)
- `window.dxPdfViewerGoToPage()` - this is permanent (after fixing selector)
---
## Troubleshooting
### Debug UI doesn't open
- Check browser console (F12) for JavaScript errors
- Ensure `pdf-viewer.js` is loaded
- Verify DxPdfViewer has finished rendering
### "Page input not found" error
- DevExpress may not have rendered toolbar yet
- Try waiting 2-3 seconds after page load
- Check if DxPdfViewer is visible on screen
### Selector works but page doesn't change
- DevExpress may require different event sequence
- Try adding more events (focus, click, etc.)
- May need to find DevExpress client API instead
---
## SOLUTION: CustomizeToolbar + Manual State Tracking
**Identified root cause:**
- DevExpress v25.2.3 has no event support
- `PageNumberChanged` event does not exist
- `ZoomLevelChanged` event does not exist
- `ToolbarVisible` property does not exist
- `GoToPageAsync()` method does not exist
- Only `CustomizeToolbar` event is available
**Verified working API (v25.2.3):**
- `DocumentContent` byte[] for feeding PDF ✓
- `ZoomLevel` double zoom factor (1.5 = 150%) ✓
- `IsSinglePagePreview` bool single page mode ✓
- `PageCount` int (GET only) **replaces JS call**
- `ActivePageIndex` int (GET only) current page index ✓
- `CssClass`, `DocumentName`, `SizeMode`
**Implemented strategy:**
- Create custom navigation/zoom buttons via `CustomizeToolbar`
- Manual state tracking with `_currentPage`, `_currentZoom`, `_viewerZoomLevel`
- Manually trigger overlay refresh after button clicks
- Replace JS getTotalPages() call with `_totalPages = _pdfViewer.PageCount`
**Correct code example:**
```csharp
protected void OnCustomizeToolbar(ToolbarModel toolbarModel)
{
toolbarModel.AllItems.Clear();
var prevButton = new ToolbarItem
{
Text = "Previous",
IconCssClass = "dx-icon-chevronprev",
Enabled = _currentPage > 1,
Click = async (args) =>
{
if (_currentPage > 1)
{
_currentPage--;
_viewerZoomLevel = _currentZoom / 100d; // 150 -> 1.5
await InvokeAsync(StateHasChanged);
await RenderSignatureButtonsAsync();
}
}
};
var nextButton = new ToolbarItem
{
Text = "Next",
IconCssClass = "dx-icon-chevronnext",
Enabled = _currentPage < _totalPages,
Click = async (args) =>
{
if (_currentPage < _totalPages)
{
_currentPage++;
_viewerZoomLevel = _currentZoom / 100d; // 150 -> 1.5
await InvokeAsync(StateHasChanged);
await RenderSignatureButtonsAsync();
}
}
};
toolbarModel.AllItems.Add(prevButton);
toolbarModel.AllItems.Add(nextButton);
}
```
**PageCount usage (instead of JS):**
```csharp
// In OnAfterRenderAsync
if (_pdfViewer is not null && _pdfViewer.PageCount > 0)
{
_totalPages = _pdfViewer.PageCount; // JS getTotalPages() no longer needed
_pdfLoaded = true;
await InvokeAsync(StateHasChanged);
}
```
**Known limitations:**
1. If user scrolls PDF, C# receives no notification, overlays may desync
2. Thumbnail navigation only updates state, cannot move viewer
3. Cross-page signature navigation limited without programmatic page switching
**See:** `DEVEXPRESS_V25_LIMITATIONS.md` complete verified API reference
---
## Expected Timeline
1.**Day 1**: Add debug tools (DONE)
2.**Day 1**: Collect DOM analysis data (DONE)
3.**Day 1**: Identify root cause (DONE - v25.2.3 has no events)
4.**Day 1**: Define workaround strategy (DONE - Custom toolbar with manual tracking)
5.**Day 1**: Implement workaround (DONE)
6.**Day 2**: Test and document limitations
7.**Day 2**: Consider DevExpress upgrade or accept limitations
---
## Related Files
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
- `RECEIVER_PDF_VIEWER_CONTEXT.md` (main context document - **UPDATED with new strategy**)
---
## Notes
- Debug UI uses inline styles to avoid CSS conflicts
- Overlay is positioned at `z-index: 99999` to appear above everything
- Close button removes overlay from DOM completely
- All debug output also goes to browser console for advanced inspection
- **Debug findings led to complete strategy change - see RECEIVER_PDF_VIEWER_CONTEXT.md section 12-14**
---
**Remember: This is TEMPORARY debugging code. Delete after completing the new implementation strategy!**

View File

@@ -1,231 +0,0 @@
# DevExpress Blazor PdfViewer v25.2.3 - Verified API Reference
> **Source:** All information in this document has been verified from the actual source code of `DevExpress.Blazor.PdfViewer` v25.2.3 package.
> AI-generated API suggestions (GoToPageAsync, PageNumberChanged, etc.) are NOT real do not use them.
---
## Verified Available Parameters
| Property | Type | Access | Default | Description |
|----------|------|--------|---------|-------------|
| `DocumentContent` | `byte[]` | `[Parameter]` GET/SET | | Feeds PDF content as byte array |
| `CssClass` | `string` | `[Parameter]` GET/SET | | Assigns CSS class to component |
| `DocumentName` | `string` | `[Parameter]` GET/SET | `"Document"` | Download filename |
| `IsSinglePagePreview` | `bool` | `[Parameter]` GET/SET | `false` | Single page mode |
| `SizeMode` | `SizeMode?` | `[Parameter]` GET/SET | `null` | `Small`, `Medium`, `Large` |
| `ZoomLevel` | `double` | `[Parameter]` GET/SET | `-1` | **Factor** (not percentage). `1.5` = 150% |
| `ActivePageIndex` | `int` | GET only | | Active page index (0-based). No SET. |
| `PageCount` | `int` | GET only | | Total page count in document |
---
## Available Events
- **`CustomizeToolbar`** Allows toolbar customization
- **`ZoomLevelChanged`** Fires when ZoomLevel property changes (EventCallback<double>)
## Missing Events (NOT AVAILABLE in v25.2.3)
- **`PageNumberChanged`** / **`ActivePageIndexChanged`** Not available
- User scrolling or native toolbar page changes do not trigger C# code
---
## Missing Properties (NOT AVAILABLE in v25.2.3)
- **`ToolbarVisible`** Not available (toolbar cannot be completely hidden)
- **`ActivePageIndex` (settable)** Read-only; no programmatic page navigation
---
## Missing Methods (NOT AVAILABLE in v25.2.3)
- **`GoToPageAsync()`** Not available
- **`GoToNextPageAsync()`** Not available
- **`ZoomAsync()`** Not available
---
## Critical Integration Notes
### ZoomLevel takes factor, not percentage
```csharp
// CORRECT
_viewerZoomLevel = 1.5; // viewer displays "150%"
_viewerZoomLevel = _currentZoom / 100d; // _currentZoom=150 -> 1.5
// WRONG
_viewerZoomLevel = 150; // viewer displays "15000%"
```
### PageCount replaces JS call
```csharp
// CORRECT - read directly from component (no JS needed)
_totalPages = _pdfViewer.PageCount;
// OLD method (no longer needed for this purpose)
// _totalPages = await JSRuntime.InvokeAsync<int>("pdfViewer.getTotalPages");
```
### ActivePageIndex is read-only
```csharp
// CORRECT - read for state synchronization
var currentPage = _pdfViewer.ActivePageIndex + 1; // convert to 1-based
// COMPILE ERROR - no setter
// _pdfViewer.ActivePageIndex = 3; // COMPILE ERROR
```
### DocumentContent byte[] feeding
```razor
<DxPdfViewer @ref="_pdfViewer"
DocumentContent="@_pdfDocumentContent"
ZoomLevel="@_viewerZoomLevel"
IsSinglePagePreview="true" />
@code {
DxPdfViewer? _pdfViewer;
byte[]? _pdfDocumentContent; // populate in OnInitializedAsync
double _viewerZoomLevel = 1.5; // 150%
}
```
---
## Impact on EnvelopeReceiverPage
### Features That Don't Work
1. **Event-driven overlay updates** No page/zoom change events
2. **Thumbnail click navigation** Cannot navigate viewer to specific page via C# API
3. **Cross-page signature navigation** No programmatic page change API
4. **Automatic overlay synchronization** User scroll/native toolbar doesn't trigger C#
### Features That Work
1. **ZoomLevel binding** Custom zoom buttons can update viewer zoom
2. **PageCount** Total pages can be read directly from component
3. **IsSinglePagePreview** Single page mode works
4. **DocumentContent** byte[] feeding works perfectly
5. **CustomizeToolbar** Only way to add custom buttons to toolbar
---
## Workaround Strategy
CustomizeToolbar event is used to add custom navigation/zoom buttons.
Manual state tracking (`_currentPage`, `_currentZoom`, `_viewerZoomLevel`) is kept in C#.
Overlay refresh is manually triggered only after button clicks.
```csharp
protected void OnCustomizeToolbar(ToolbarModel toolbarModel)
{
toolbarModel.AllItems.Clear();
var prevButton = new ToolbarItem
{
Text = "Previous",
IconCssClass = "dx-icon-chevronprev",
Enabled = _currentPage > 1,
Click = async (args) =>
{
if (_currentPage > 1)
{
_currentPage--;
_viewerZoomLevel = _currentZoom / 100d;
await InvokeAsync(StateHasChanged);
await RenderSignatureButtonsAsync();
}
}
};
var nextButton = new ToolbarItem
{
Text = "Next",
IconCssClass = "dx-icon-chevronnext",
Enabled = _currentPage < _totalPages,
Click = async (args) =>
{
if (_currentPage < _totalPages)
{
_currentPage++;
_viewerZoomLevel = _currentZoom / 100d;
await InvokeAsync(StateHasChanged);
await RenderSignatureButtonsAsync();
}
}
};
var zoomInButton = new ToolbarItem
{
IconCssClass = "dx-icon-plus",
Enabled = _currentZoom < 300,
Click = async (args) =>
{
_currentZoom = Math.Min(_currentZoom + 10, 300);
_viewerZoomLevel = _currentZoom / 100d; // 150 -> 1.5
await InvokeAsync(StateHasChanged);
await RenderSignatureButtonsAsync();
}
};
var zoomOutButton = new ToolbarItem
{
IconCssClass = "dx-icon-minus",
Enabled = _currentZoom > 50,
Click = async (args) =>
{
_currentZoom = Math.Max(_currentZoom - 10, 50);
_viewerZoomLevel = _currentZoom / 100d; // 150 -> 1.5
await InvokeAsync(StateHasChanged);
await RenderSignatureButtonsAsync();
}
};
toolbarModel.AllItems.Add(prevButton);
toolbarModel.AllItems.Add(nextButton);
toolbarModel.AllItems.Add(zoomInButton);
toolbarModel.AllItems.Add(zoomOutButton);
}
```
### PageCount reading example (in OnAfterRenderAsync)
```csharp
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!_pdfLoaded && _pdfDocumentContent is { Length: > 0 })
{
await Task.Delay(300); // wait for DxPdfViewer to load
if (_pdfViewer is not null && _pdfViewer.PageCount > 0)
{
_totalPages = _pdfViewer.PageCount; // read directly instead of JS
_pdfLoaded = true;
await InvokeAsync(StateHasChanged);
await RenderThumbnailsAsync();
await RenderSignatureButtonsAsync();
}
}
}
```
---
## Known Acceptable Limitations
1. If user scrolls PDF, C# `_currentPage` does not synchronize
2. Thumbnail clicks update state but cannot move DevExpress viewer to target page
3. Browser zoom gestures do not trigger overlay updates
4. Custom toolbar buttons correctly trigger overlay updates
---
## References
- DevExpress official documentation: https://docs.devexpress.com/Blazor/DevExpress.Blazor.PdfViewer.DxPdfViewer
- Verified package: `DevExpress.Blazor.PdfViewer` v25.2.3
- **Note:** AI-suggested APIs (GoToPageAsync, PageNumberChanged, ActivePageIndexChanged, ZoomAsync, ToolbarVisible) are NOT real. Do not use.

View File

@@ -214,6 +214,10 @@ public class EnvelopeReceiverController : ControllerBase
if (reader.Read()) if (reader.Read())
{ {
bool outSuccess = reader.GetBoolean(0); bool outSuccess = reader.GetBoolean(0);
if (!outSuccess)
_logger.LogWarning(
"PRSIG_API_ADD_DOC_RECEIVER_ELEM returned OUT_SUCCESS=false. DOC_ID={DocId}, RECEIVER_ID={ReceiverId}, Page={Page}",
document.Id, rcv.Id, sign.Page);
} }
} }
#endregion #endregion
@@ -221,8 +225,6 @@ public class EnvelopeReceiverController : ControllerBase
#region Create history #region Create history
// ENV_UID, STATUS_ID, USER_ID, // ENV_UID, STATUS_ID, USER_ID,
string sql_hist = @" string sql_hist = @"
USE [DD_ECM]
DECLARE @OUT_SUCCESS bit; DECLARE @OUT_SUCCESS bit;
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE] EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
@@ -244,6 +246,10 @@ public class EnvelopeReceiverController : ControllerBase
if (reader.Read()) if (reader.Read())
{ {
bool outSuccess = reader.GetBoolean(0); bool outSuccess = reader.GetBoolean(0);
if (!outSuccess)
_logger.LogWarning(
"PRSIG_API_ADD_HISTORY_STATE returned OUT_SUCCESS=false. EnvelopeUuid={EnvelopeUuid}",
envelope.Uuid);
} }
} }
#endregion #endregion

View File

@@ -1,4 +1,6 @@
namespace EnvelopeGenerator.Application.Common.Query; using System.ComponentModel.DataAnnotations.Schema;
namespace EnvelopeGenerator.Application.Common.Query;
/// <summary> /// <summary>
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen. /// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
@@ -29,5 +31,6 @@ public record ReceiverQueryBase
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null. /// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para> /// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
/// </remarks> /// </remarks>
public bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null; [NotMapped]
public virtual bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
} }

View File

@@ -13,7 +13,7 @@ public class EnvelopeReceiverAddReadSQL : ISQL<Envelope>
/// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE, /// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE,
/// </summary> /// </summary>
public string Raw => @" public string Raw => @"
DECLARE @OUT_RECEIVER_ID int DECLARE @OUT_RECEIVER_ID bigint
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER] EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
{0}, {0},

View File

@@ -5,6 +5,7 @@ using EnvelopeGenerator.Application.Common.Query;
using MediatR; using MediatR;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;
namespace EnvelopeGenerator.Application.Receivers.Queries; namespace EnvelopeGenerator.Application.Receivers.Queries;
@@ -12,7 +13,24 @@ namespace EnvelopeGenerator.Application.Receivers.Queries;
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen. /// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
/// um spezifische Informationen über einen Empfänger abzurufen. /// um spezifische Informationen über einen Empfänger abzurufen.
/// </summary> /// </summary>
public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>; public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>
{
/// <summary>
/// Suchbegriff für eine teilweise Übereinstimmung in der E-Mail Adresse des Empfängers
/// </summary>
public virtual string? EmailAddressSearch { get; set; }
/// <summary>
/// Checks whether any of the specified query criteria have a value.
/// </summary>
/// <remarks>
/// This property returns <c>true</c> if at least one of the fields
/// <see cref="ReceiverQueryBase.Id"/>, <see cref="ReceiverQueryBase.EmailAddress"/>, <see cref="EmailAddressSearch"/>, or <see cref="ReceiverQueryBase.Signature"/> is not null.
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
/// </remarks>
[NotMapped]
public override bool HasAnyCriteria => EmailAddressSearch is not null || base.HasAnyCriteria;
}
/// <summary> /// <summary>
/// ///
@@ -53,6 +71,11 @@ public class ReadReceiverQueryHandler : IRequestHandler<ReadReceiverQuery, IEnum
query = query.Where(r => r.EmailAddress == email); query = query.Where(r => r.EmailAddress == email);
} }
if (!string.IsNullOrWhiteSpace(request.EmailAddressSearch))
{
query = query.Where(r => EF.Functions.Like(r.EmailAddress, $"%{request.EmailAddressSearch}%"));
}
if (request.Signature is string signature) if (request.Signature is string signature)
{ {
query = query.Where(r => r.Signature == signature); query = query.Where(r => r.Signature == signature);

View File

@@ -138,7 +138,9 @@
SelectedDataItemChanged="@OnSelectedEnvelopeChanged" SelectedDataItemChanged="@OnSelectedEnvelopeChanged"
CustomizeElement="OnCustomizeElement"> CustomizeElement="OnCustomizeElement">
<Columns> <Columns>
<DxGridDataColumn FieldName="Id" Caption="ID"> <DxGridDataColumn FieldName="Id" Caption="ID"
SortIndex="0"
SortOrder="GridColumnSortOrder.Descending">
<CellDisplayTemplate Context="cellContext"> <CellDisplayTemplate Context="cellContext">
@((cellContext.DataItem as EnvelopeDto)?.Id) @((cellContext.DataItem as EnvelopeDto)?.Id)
</CellDisplayTemplate> </CellDisplayTemplate>
@@ -234,7 +236,9 @@
SelectedDataItemChanged="@OnSelectedEnvelopeChanged" SelectedDataItemChanged="@OnSelectedEnvelopeChanged"
CustomizeElement="OnCustomizeElement"> CustomizeElement="OnCustomizeElement">
<Columns> <Columns>
<DxGridDataColumn FieldName="Id" Caption="ID"> <DxGridDataColumn FieldName="Id" Caption="ID"
SortIndex="0"
SortOrder="GridColumnSortOrder.Descending">
<CellDisplayTemplate Context="cellContext"> <CellDisplayTemplate Context="cellContext">
@((cellContext.DataItem as EnvelopeDto)?.Id) @((cellContext.DataItem as EnvelopeDto)?.Id)
</CellDisplayTemplate> </CellDisplayTemplate>
@@ -385,8 +389,7 @@
void CreateEnvelope() void CreateEnvelope()
{ {
// TODO: Navigate to envelope creation page Navigation.NavigateTo("/sender/editor");
JSRuntime.InvokeVoidAsync("console.log", "Create envelope clicked - not yet implemented");
} }
void EditEnvelope() void EditEnvelope()

View File

@@ -1,5 +1,7 @@
@page "/" @page "/"
@inject IJSRuntime JS @inject IJSRuntime JS
@inject NavigationManager Navigation
@rendermode InteractiveWebAssembly
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" /> <link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
@@ -25,7 +27,18 @@
<span id="home-description"></span> <span id="home-description"></span>
</p> </p>
<div class="mt-4 pt-3 border-top"> <div class="mt-4 pt-3 border-top d-flex flex-column align-items-center gap-4">
<button class="home-btn-primary btn px-4 py-2 d-flex align-items-center gap-2" @onclick='() => Navigation.NavigateTo("/sender")'>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
</svg>
Zur Umschlag-Übersicht
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>
</button>
<div class="d-flex flex-wrap justify-content-center gap-3"> <div class="d-flex flex-wrap justify-content-center gap-3">
<div class="home-feature-badge"> <div class="home-feature-badge">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16"> <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
@@ -47,6 +60,7 @@
PDF-Export PDF-Export
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,6 +2,7 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
using EnvelopeGenerator.Server.Client.Models; using EnvelopeGenerator.Server.Client.Models;
namespace EnvelopeGenerator.Server.Client.Services; namespace EnvelopeGenerator.Server.Client.Services;
@@ -9,6 +10,7 @@ namespace EnvelopeGenerator.Server.Client.Services;
/// <summary> /// <summary>
/// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver /// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver
/// from <c>GET /api/EnvelopeReceiver/{envelopeKey}</c>. /// from <c>GET /api/EnvelopeReceiver/{envelopeKey}</c>.
/// Also creates new envelopes via <c>POST /api/EnvelopeReceiver</c>.
/// </summary> /// </summary>
public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory) public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
{ {
@@ -37,4 +39,28 @@ public class EnvelopeReceiverService(IHttpClientFactory httpClientFactory)
return await response.Content.ReadFromJsonAsync<EnvelopeReceiverDto>(_jsonOptions, cancel); return await response.Content.ReadFromJsonAsync<EnvelopeReceiverDto>(_jsonOptions, cancel);
} }
/// <summary>
/// Creates a new envelope with document and receivers via <c>POST /api/EnvelopeReceiver</c>.
/// Requires sender authentication cookie to be present in the request.
/// </summary>
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
public async Task<CreateEnvelopeReceiverResponse?> CreateAsync(
CreateEnvelopeReceiverCommand request,
CancellationToken cancel = default)
{
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
var response = await http.PostAsJsonAsync("/api/EnvelopeReceiver", request, _jsonOptions, cancel);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(cancel);
throw new HttpRequestException(
$"Fehler beim Erstellen des Umschlags. Status: {(int)response.StatusCode} {body}",
null,
response.StatusCode);
}
return await response.Content.ReadFromJsonAsync<CreateEnvelopeReceiverResponse>(_jsonOptions, cancel);
}
} }

View File

@@ -0,0 +1,723 @@
@page "/envelope/{EnvelopeKey}"
@rendermode InteractiveServer
@using DevExpress.Blazor.Reporting
@using DevExpress.XtraReports.UI
@using EnvelopeGenerator.Server.Client.Models
@using EnvelopeGenerator.Server.Client.Models.Constants
@using EnvelopeGenerator.Server.Client.Services
@using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver
@using Microsoft.JSInterop
@using DevExpress.Blazor
@using System.Drawing
@using System.Security.Claims
@using Microsoft.Extensions.Caching.Memory
@inject NavigationManager Navigation
@inject IJSRuntime JSRuntime
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
@inject AppVersionService AppVersion
@inject IMemoryCache MemoryCache
@inject ILogger<ReceiverPage> Logger
@implements IDisposable
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" />
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
<script src="@AppVersion.GetVersionedUrl("js/receiver-signature.js")"></script>
<div class="envelope-viewer-layout">
<div class="envelope-action-bar">
<div class="envelope-action-bar__inner" style="flex-direction: column; align-items: stretch; padding: 0.35rem 1.5rem; gap: 0.35rem;">
@* Row 1: Title + Sender + Badges *@
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem;">
@* Left: Title + Sender *@
<div style="flex: 0 1 auto; min-width: 0; display: flex; align-items: center; gap: 0.75rem;">
@if (_envelopeReceiver is not null)
{
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
@(_envelopeReceiver.Envelope?.Title ?? "Dokument")
</div>
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName) || !string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email))
{
<span style="font-size: 0.7rem; color: #6b7280; white-space: nowrap;">
Von
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName))
{
<span style="font-weight: 500; color: #374151;">@_envelopeReceiver.Envelope.User.FullName</span>
}
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email))
{
<span>&lt;@_envelopeReceiver.Envelope.User.Email&gt;</span>
}
@if (_envelopeReceiver.Envelope?.AddedWhen != null)
{
<span>&nbsp;·&nbsp;@_envelopeReceiver.Envelope.AddedWhen.ToString("dd.MM.yyyy")</span>
}
</span>
}
}
else
{
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937;">Dokumentenansicht</div>
}
</div>
@* Right: Badges + Signature status *@
<div class="d-flex align-items-center" style="gap: 0.75rem; flex: 0 0 auto;">
@if (_envelopeReceiver is not null)
{
<div class="d-flex flex-wrap align-items-center" style="gap: 0.3rem; font-size: 0.7rem;">
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Name))
{
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem; background: #f3f4f6; border-radius: 0.25rem; color: #374151; white-space: nowrap;">
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Z" />
</svg>
@_envelopeReceiver.Name
</span>
}
@if (_signatures.Count > 0)
{
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem; background: @(_capturedSignature is not null ? "#d1fae5" : "#ede9fe"); border-radius: 0.25rem; color: @(_capturedSignature is not null ? "#065f46" : "#6d28d9"); font-weight: 500; white-space: nowrap;">
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
</svg>
@_signatures.Count Unterschrift@(_signatures.Count != 1 ? "en" : "")
@if (_capturedSignature is not null)
{
<span class="ms-1">✓</span>
}
</span>
}
@if (_envelopeReceiver.Envelope?.UseAccessCode ?? false)
{
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem; background: #fef3c7; border-radius: 0.25rem; color: #92400e; font-weight: 500; white-space: nowrap;">
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z" />
</svg>
Code
</span>
}
@if (_envelopeReceiver.Envelope?.TFAEnabled ?? false)
{
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem; background: #dbeafe; border-radius: 0.25rem; color: #1e40af; font-weight: 500; white-space: nowrap;">
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M5.338 1.59a61.44 61.44 0 0 0-2.837.856.481.481 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.725 10.725 0 0 0 2.287 2.233c.346.244.652.42.893.533.12.057.218.095.293.118a.55.55 0 0 0 .101.025.615.615 0 0 0 .1-.025c.076-.023.174-.061.294-.118.24-.113.547-.29.893-.533a10.726 10.726 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.775 11.775 0 0 1-2.517 2.453 7.159 7.159 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7.158 7.158 0 0 1-1.048-.625 11.777 11.777 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 62.456 62.456 0 0 1 5.072.56z" />
<path d="M10.854 5.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 7.793l2.646-2.647a.5.5 0 0 1 .708 0z" />
</svg>
2FA
</span>
}
</div>
}
@* Unterschreiben button — visible only when signature fields exist *@
@if (_signatures.Count > 0)
{
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change pdf-toolbar__btn--signature-change-active"
@onclick="OpenSignaturePopup"
title="Unterschreiben"
style="flex-shrink: 0;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
</svg>
<span class="pdf-toolbar__btn-text">Unterschreiben</span>
</button>
}
</div>
</div>
@* Row 2: Messages *@
@if (_envelopeReceiver is not null && (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message) || !string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage)))
{
<div style="display: flex; align-items: flex-start; gap: 0.5rem; font-size: 0.7rem; padding-top: 0.15rem; border-top: 1px solid #e5e7eb;">
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message))
{
<div style="flex: 1; min-width: 0; padding: 0.2rem 0.4rem; background: #f9fafb; border-radius: 0.25rem; border-left: 2px solid #9ca3af; display: flex; align-items: flex-start; gap: 0.25rem;">
<span style="font-weight: 500; color: #374151; flex-shrink: 0;">📧</span>
<span style="color: #6b7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">@_envelopeReceiver.Envelope.Message</span>
</div>
}
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage))
{
<div style="flex: 1; min-width: 0; padding: 0.2rem 0.4rem; background: #fef3c7; border-radius: 0.25rem; border-left: 2px solid #f59e0b; display: flex; align-items: flex-start; gap: 0.25rem;">
<span style="font-weight: 500; color: #92400e; flex-shrink: 0;">🔒</span>
<span style="color: #92400e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">@_envelopeReceiver.PrivateMessage</span>
</div>
}
</div>
}
</div>
</div>
<div class="envelope-content" style="padding: 0; overflow: hidden;">
@if (_isLoading)
{
<div class="d-flex justify-content-center align-items-center h-100">
<div class="text-center">
<div class="spinner-border text-white mb-3" style="width: 3.5rem; height: 3.5rem;" role="status">
<span class="visually-hidden">Lädt...</span>
</div>
<p class="text-white fw-semibold">Dokument wird geladen...</p>
</div>
</div>
}
else if (_errorMessage is not null)
{
<div class="error-container">
<div class="alert alert-danger shadow-lg">
<div class="d-flex align-items-start">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="me-3 flex-shrink-0" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
</svg>
<div>
<h5 class="mb-2">Fehler beim Laden des Dokuments</h5>
<p class="mb-0">@_errorMessage</p>
</div>
</div>
</div>
</div>
}
else if (_report is not null)
{
<DxReportViewer @ref="_reportViewer"
Report="_report"
RootCssClasses="w-100 h-100" />
}
</div>
</div>
@* Signature Popup *@
<DxPopup @bind-Visible="_signaturePopupVisible"
HeaderText="Unterschrift erstellen"
Width="620px"
MaxWidth="95vw"
ShowFooter="true"
CloseOnOutsideClick="false"
ShowCloseButton="false"
CloseOnEscape="false"
Shown="OnPopupShownAsync">
<BodyContentTemplate>
<ul class="nav nav-tabs mb-3" style="border-bottom: 2px solid #e9ecef;">
<li class="nav-item">
<button type="button"
class="nav-link @(_activeSignatureTab == SignatureTabDraw ? "active" : "")"
style="@(_activeSignatureTab == SignatureTabDraw ? "border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;" : "color: #6c757d;")"
@onclick="() => SetSignatureTabAsync(SignatureTabDraw)">
Zeichnen
</button>
</li>
<li class="nav-item">
<button type="button"
class="nav-link @(_activeSignatureTab == SignatureTabText ? "active" : "")"
style="@(_activeSignatureTab == SignatureTabText ? "border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;" : "color: #6c757d;")"
@onclick="() => SetSignatureTabAsync(SignatureTabText)">
Text
</button>
</li>
<li class="nav-item">
<button type="button"
class="nav-link @(_activeSignatureTab == SignatureTabImage ? "active" : "")"
style="@(_activeSignatureTab == SignatureTabImage ? "border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;" : "color: #6c757d;")"
@onclick="() => SetSignatureTabAsync(SignatureTabImage)">
Bild
</button>
</li>
</ul>
@if (_activeSignatureTab == SignatureTabDraw)
{
<p style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.75rem;">Bitte unterschreiben Sie im folgenden Feld.</p>
<canvas id="rp-signature-pad"
width="560"
height="180"
style="border: 2px solid #e9ecef; border-radius: 8px; background: white; width: 100%; max-width: 560px; touch-action: none; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"></canvas>
}
else if (_activeSignatureTab == SignatureTabText)
{
<p style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.75rem;">Geben Sie Ihre Unterschrift als Text ein und wählen Sie eine Schriftart.</p>
<div class="row g-3 mb-3">
<div class="col-12 col-md-7">
<input class="form-control"
placeholder="Ihre Unterschrift"
value="@_typedSignatureText"
@oninput="OnTypedSignatureChanged"
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
</div>
<div class="col-12 col-md-5">
<select class="form-select"
value="@_typedSignatureFont"
@onchange="OnTypedSignatureFontChanged"
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;">
@foreach (var font in TypedSignatureFonts)
{
<option value="@font.Value" style="font-family: @font.Value">@font.Text</option>
}
</select>
</div>
</div>
<canvas id="rp-typed-signature-pad"
width="560"
height="180"
style="border: 2px solid #e9ecef; border-radius: 8px; background: white; width: 100%; max-width: 560px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"></canvas>
}
else
{
<p style="color: #6c757d; font-size: 0.875rem; margin-bottom: 0.75rem;">Laden Sie ein Bild Ihrer Unterschrift hoch.</p>
<input id="rp-signature-image-input"
class="form-control mb-3"
type="file"
accept="image/png,image/jpeg,image/webp"
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
<canvas id="rp-image-signature-pad"
width="560"
height="180"
style="border: 2px solid #e9ecef; border-radius: 8px; background: white; width: 100%; max-width: 560px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);"></canvas>
}
<div style="border-top: 2px solid #e9ecef; margin-top: 1.5rem; padding-top: 1.5rem;">
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label" for="rp-signer-name" style="font-size: 0.875rem; font-weight: 500; color: #495057;">
Vor- und Nachname <span style="color: #dc3545;">*</span>
</label>
<input id="rp-signer-name"
class="form-control"
value="@_signerFullName"
@oninput="args => _signerFullName = args.Value?.ToString() ?? string.Empty"
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
</div>
<div class="col-12 col-md-6">
<label class="form-label" for="rp-signer-position" style="font-size: 0.875rem; font-weight: 500; color: #495057;">
Position <span style="color: #6c757d; font-weight: 400;">(optional)</span>
</label>
<input id="rp-signer-position"
class="form-control"
value="@_signerPosition"
@oninput="args => _signerPosition = args.Value?.ToString() ?? string.Empty"
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
</div>
<div class="col-12 col-md-6">
<label class="form-label" for="rp-signature-place" style="font-size: 0.875rem; font-weight: 500; color: #495057;">
Ort <span style="color: #dc3545;">*</span>
</label>
<input id="rp-signature-place"
class="form-control"
value="@_signaturePlace"
@oninput="args => _signaturePlace = args.Value?.ToString() ?? string.Empty"
style="border: 2px solid #e9ecef; border-radius: 6px; padding: 0.625rem;" />
</div>
</div>
</div>
@if (!string.IsNullOrWhiteSpace(_popupValidationMessage))
{
<div style="background: #fee; border-left: 4px solid #dc3545; padding: 0.75rem 1rem; margin-top: 1rem; border-radius: 4px;">
<span style="color: #dc3545; font-size: 0.875rem; font-weight: 500;">@_popupValidationMessage</span>
</div>
}
</BodyContentTemplate>
<FooterContentTemplate>
<div class="d-flex gap-2 justify-content-between w-100" style="padding: 0.5rem 0;">
<button class="btn btn-outline-secondary"
@onclick="RenewSignatureAsync"
style="border-radius: 6px; padding: 0.625rem 1.25rem; font-weight: 500;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z" />
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z" />
</svg>
Erneuern
</button>
<button class="btn btn-primary"
@onclick="SaveSignatureAsync"
style="background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%); border: none; border-radius: 6px; padding: 0.625rem 2rem; font-weight: 600; box-shadow: 0 2px 4px rgba(79, 70, 229, 0.3);">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
</svg>
Speichern
</button>
</div>
</FooterContentTemplate>
</DxPopup>
@code {
// ----- Constants -----
const string SignatureTabDraw = "draw";
const string SignatureTabText = "text";
const string SignatureTabImage = "image";
const string DrawCanvasId = "rp-signature-pad";
const string TypedCanvasId = "rp-typed-signature-pad";
const string ImageInputId = "rp-signature-image-input";
const string ImageCanvasId = "rp-image-signature-pad";
readonly (string Text, string Value)[] TypedSignatureFonts =
[
("Brush Script", "'Brush Script MT', cursive"),
("Segoe Script", "'Segoe Script', cursive"),
("Lucida Handwriting", "'Lucida Handwriting', cursive"),
("Comic Sans", "'Comic Sans MS', cursive"),
("Cursive", "cursive"),
];
// ----- Parameters -----
[Parameter] public string? EnvelopeKey { get; set; }
// ----- Page state -----
bool _isLoading = true;
string? _errorMessage;
byte[]? _pdfBytes;
IReadOnlyList<SignatureDto> _signatures = [];
EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver;
ClaimsPrincipal? _receiverUser;
// ----- Report viewer -----
DxReportViewer? _reportViewer;
XtraReport? _report;
// ----- Signature popup state -----
SignatureCaptureDto? _capturedSignature;
bool _signaturePopupVisible = false;
string? _popupValidationMessage;
string _activeSignatureTab = SignatureTabDraw;
string _typedSignatureText = string.Empty;
string _typedSignatureFont = "'Brush Script MT', cursive";
string _signerFullName = string.Empty;
string _signerPosition = string.Empty;
string _signaturePlace = string.Empty;
// ----- Lifecycle -----
protected override async Task OnInitializedAsync()
{
if (string.IsNullOrWhiteSpace(EnvelopeKey))
{
_errorMessage = "Envelope-Schlüssel fehlt.";
_isLoading = false;
return;
}
// Authorization — same pattern as EnvelopeReceiverPage
_receiverUser = await ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey);
if (_receiverUser is null)
{
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
return;
}
try
{
// Load PDF bytes via MediatR (uses authenticated user's claims)
_pdfBytes = await PageDataService.GetDocumentAsync(_receiverUser);
if (_pdfBytes is not { Length: > 0 })
{
_errorMessage = "Dokument konnte nicht geladen werden: Keine Daten empfangen.";
_isLoading = false;
return;
}
// Load signature fields for this receiver
_signatures = await PageDataService.GetSignaturesAsync(_receiverUser);
// Load envelope receiver metadata
_envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey);
if (_envelopeReceiver is null)
Logger.LogWarning("Envelope receiver data is null for {EnvelopeKey}", EnvelopeKey);
// Build initial report (no signature image yet)
_report = BuildReport(_pdfBytes, _signatures, capturedSignature: null);
// Try to restore cached signature
try
{
var cachedSignature = await PageDataService.GetCachedSignatureAsync(_receiverUser);
if (cachedSignature is not null)
{
_capturedSignature = cachedSignature;
_signerFullName = cachedSignature.FullName;
_signerPosition = cachedSignature.Position;
_signaturePlace = cachedSignature.Place;
_signaturePopupVisible = false;
// Rebuild with cached signature overlaid
_report = BuildReport(_pdfBytes, _signatures, _capturedSignature);
}
else
{
_activeSignatureTab = SignatureTabDraw;
_signaturePopupVisible = false;
_popupValidationMessage = null;
}
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to load cached signature for {EnvelopeKey}", EnvelopeKey);
_activeSignatureTab = SignatureTabDraw;
_signaturePopupVisible = false;
_popupValidationMessage = null;
}
}
catch (Exception ex)
{
_errorMessage = $"Fehler beim Laden des Dokuments: {ex.Message}";
Logger.LogError(ex, "Unexpected error for {EnvelopeKey}", EnvelopeKey);
}
_isLoading = false;
await InvokeAsync(StateHasChanged);
}
// ----- Report builder -----
/// <summary>
/// Builds an XtraReport wrapping the PDF bytes.
/// If a signature is captured and there are signature fields, the signature image is
/// first burned into the PDF via DevExpress PdfDocumentProcessor, then the modified
/// PDF is handed to XRPdfContent with GenerateOwnPages = true so that all pages appear.
/// </summary>
static XtraReport BuildReport(
byte[] pdfBytes,
IReadOnlyList<SignatureDto> signatures,
SignatureCaptureDto? capturedSignature)
{
// Always draw placeholder boxes on signature fields so the user knows where to sign.
// When a captured signature exists, it will be applied in the Signed page instead.
byte[] sourcePdf = pdfBytes;
if (signatures.Count > 0)
{
sourcePdf = DrawSignaturePlaceholders(pdfBytes, signatures);
}
var report = new XtraReport
{
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4,
Landscape = false,
Margins = new System.Drawing.Printing.Margins(0, 0, 0, 0),
};
var detail = new DetailBand { HeightF = 0f };
report.Bands.Add(detail);
detail.Controls.Add(new XRPdfContent
{
Source = sourcePdf,
GenerateOwnPages = true,
});
return report;
}
/// <summary>
/// Uses PdfSharp to draw a visible signature placeholder box on every signature field.
/// sig.X / sig.Y come from GetSignaturesAsync(UnitOfLength.Point) → already in PDF points.
/// PdfSharp coordinate origin: bottom-left, Y up. Conversion: pdfY = pageH - sigY - sigH
/// Signature field size (fixed): 1.77" × 1.96" = 127.44pt × 141.12pt
/// </summary>
static byte[] DrawSignaturePlaceholders(
byte[] pdfBytes,
IReadOnlyList<SignatureDto> signatures)
{
if (signatures.Count == 0) return pdfBytes;
using var inputMs = new System.IO.MemoryStream(pdfBytes);
using var outputMs = new System.IO.MemoryStream();
var document = PdfSharp.Pdf.IO.PdfReader.Open(
inputMs,
PdfSharp.Pdf.IO.PdfDocumentOpenMode.Modify);
const double sigW = 1.77 * 72; // 127.44 pt
const double sigH = 1.96 * 72; // 141.12 pt
foreach (var sig in signatures)
{
int pageIndex = sig.Page - 1;
if (pageIndex < 0 || pageIndex >= document.PageCount) continue;
var page = document.Pages[pageIndex];
// PdfSharp XGraphics uses top-left origin, Y down — same as sig.X/sig.Y
// No coordinate conversion needed.
using var gfx = PdfSharp.Drawing.XGraphics.FromPdfPage(page);
var rect = new PdfSharp.Drawing.XRect(sig.X, sig.Y, sigW, sigH);
// Filled semi-transparent rectangle
var fillBrush = new PdfSharp.Drawing.XSolidBrush(
PdfSharp.Drawing.XColor.FromArgb(40, 60, 80, 160));
var borderPen = new PdfSharp.Drawing.XPen(
PdfSharp.Drawing.XColor.FromArgb(200, 60, 80, 200), 1.5);
gfx.DrawRectangle(fillBrush, rect);
gfx.DrawRectangle(borderPen, rect);
// "UNTERSCHRIFT" label centred in the box
var font = new PdfSharp.Drawing.XFont("Arial", 9,
PdfSharp.Drawing.XFontStyleEx.Bold);
var textBrush = new PdfSharp.Drawing.XSolidBrush(
PdfSharp.Drawing.XColor.FromArgb(200, 40, 60, 140));
var textFmt = new PdfSharp.Drawing.XStringFormat
{
Alignment = PdfSharp.Drawing.XStringAlignment.Center,
LineAlignment = PdfSharp.Drawing.XLineAlignment.Center,
};
gfx.DrawString("UNTERSCHRIFT", font, textBrush, rect, textFmt);
}
document.Save(outputMs);
return outputMs.ToArray();
}
/// <summary>Converts a base64 data URL (data:image/...;base64,...) to raw bytes.</summary>
static byte[]? DataUrlToBytes(string dataUrl)
{
try
{
var commaIndex = dataUrl.IndexOf(',');
if (commaIndex < 0) return null;
return Convert.FromBase64String(dataUrl[(commaIndex + 1)..]);
}
catch
{
return null;
}
}
// ----- Signature popup handlers -----
void OpenSignaturePopup()
{
_activeSignatureTab = SignatureTabDraw;
_signaturePopupVisible = true;
_popupValidationMessage = null;
}
async Task OnPopupShownAsync()
{
await InitializeActiveSignatureTabAsync();
}
async Task SetSignatureTabAsync(string tab)
{
_activeSignatureTab = tab;
_popupValidationMessage = null;
await InvokeAsync(StateHasChanged);
await Task.Delay(50);
await InitializeActiveSignatureTabAsync();
}
async Task InitializeActiveSignatureTabAsync()
{
if (_activeSignatureTab == SignatureTabDraw)
await JSRuntime.InvokeVoidAsync("receiverSignature.initialize", DrawCanvasId);
else if (_activeSignatureTab == SignatureTabText)
{
await JSRuntime.InvokeVoidAsync("receiverSignature.initializeTyped", TypedCanvasId);
await RenderTypedSignatureAsync();
}
else
await JSRuntime.InvokeVoidAsync("receiverSignature.initializeImage", ImageInputId, ImageCanvasId);
}
async Task RenewSignatureAsync()
{
_popupValidationMessage = null;
if (_activeSignatureTab == SignatureTabDraw)
await JSRuntime.InvokeVoidAsync("receiverSignature.clear", DrawCanvasId);
else if (_activeSignatureTab == SignatureTabText)
{
_typedSignatureText = string.Empty;
await JSRuntime.InvokeVoidAsync("receiverSignature.clearTyped", TypedCanvasId);
}
else
await JSRuntime.InvokeVoidAsync("receiverSignature.clearImage", ImageInputId, ImageCanvasId);
}
async Task OnTypedSignatureChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args)
{
_typedSignatureText = args.Value?.ToString() ?? string.Empty;
await RenderTypedSignatureAsync();
}
async Task OnTypedSignatureFontChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args)
{
_typedSignatureFont = args.Value?.ToString() ?? _typedSignatureFont;
await RenderTypedSignatureAsync();
}
async Task RenderTypedSignatureAsync()
{
await JSRuntime.InvokeVoidAsync("receiverSignature.renderTypedSignature",
TypedCanvasId, _typedSignatureText, _typedSignatureFont);
}
async Task SaveSignatureAsync()
{
if (string.IsNullOrWhiteSpace(_signerFullName))
{
_popupValidationMessage = "Bitte geben Sie Vor- und Nachname ein.";
return;
}
if (string.IsNullOrWhiteSpace(_signaturePlace))
{
_popupValidationMessage = "Bitte geben Sie den Ort ein.";
return;
}
var signatureDataUrl = await GetActiveSignatureDataUrlAsync();
if (string.IsNullOrWhiteSpace(signatureDataUrl))
{
_popupValidationMessage = "Die Unterschrift ist erforderlich.";
return;
}
_popupValidationMessage = null;
_capturedSignature = new SignatureCaptureDto
{
DataUrl = signatureDataUrl,
FullName = _signerFullName.Trim(),
Position = _signerPosition.Trim(),
Place = _signaturePlace.Trim(),
};
_signaturePopupVisible = false;
// Store signature in IMemoryCache with a Guid key (1 minute TTL)
var sid = Guid.NewGuid().ToString("N");
MemoryCache.Set(
sid,
_capturedSignature,
TimeSpan.FromMinutes(1));
Logger.LogInformation(
"Signature cached with sid={Sid} for envelope {EnvelopeKey}", sid, EnvelopeKey);
// Null the report → DxReportViewer removed from DOM → no crash on dispose
_report = null;
await InvokeAsync(StateHasChanged);
await Task.Delay(50);
// Navigate — forceLoad:true for clean circuit teardown
Navigation.NavigateTo(
$"/envelope/{Uri.EscapeDataString(EnvelopeKey!)}/signed?sid={sid}",
forceLoad: true);
}
async Task<string?> GetActiveSignatureDataUrlAsync()
{
if (_activeSignatureTab == SignatureTabDraw)
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getDataUrl", DrawCanvasId);
if (_activeSignatureTab == SignatureTabText)
{
await RenderTypedSignatureAsync();
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getTypedDataUrl", TypedCanvasId);
}
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getImageDataUrl", ImageCanvasId);
}
// ----- Disposal -----
public void Dispose()
{
_report?.Dispose();
}
}

View File

@@ -0,0 +1,396 @@
@page "/envelope/{EnvelopeKey}/signed"
@rendermode InteractiveServer
@using DevExpress.Blazor.Reporting
@using DevExpress.XtraReports.UI
@using EnvelopeGenerator.Server.Client.Models
@using EnvelopeGenerator.Server.Client.Services
@using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver
@using Microsoft.Extensions.Caching.Memory
@using System.Security.Claims
@inject NavigationManager Navigation
@inject IJSRuntime JSRuntime
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService ReceiverAuthorizationService
@inject EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService PageDataService
@inject AppVersionService AppVersion
@inject IMemoryCache MemoryCache
@inject ILogger<ReceiverSignedPage> Logger
@implements IDisposable
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
<link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" />
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
<div class="envelope-viewer-layout">
<div class="envelope-action-bar">
<div class="envelope-action-bar__inner" style="flex-direction: column; align-items: stretch; padding: 0.35rem 1.5rem; gap: 0.35rem;">
<div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem;">
<div style="flex: 0 1 auto; min-width: 0; display: flex; align-items: center; gap: 0.75rem;">
@if (_envelopeReceiver is not null)
{
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
@(_envelopeReceiver.Envelope?.Title ?? "Dokument")
</div>
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName))
{
<span style="font-size: 0.7rem; color: #6b7280; white-space: nowrap;">
Von <span style="font-weight: 500; color: #374151;">@_envelopeReceiver.Envelope.User.FullName</span>
</span>
}
}
else
{
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937;">Signiertes Dokument</div>
}
</div>
@* Right: Submit button *@
<div style="flex: 0 0 auto;">
<button class="pdf-toolbar__btn pdf-toolbar__btn--signature-change pdf-toolbar__btn--signature-change-active"
@onclick="OpenSubmitConfirmPopup"
disabled="@_isLoggingOut"
title="Abschließen"
style="flex-shrink: 0;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" 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 class="pdf-toolbar__btn-text">Abschließen</span>
</button>
</div>
</div>
</div>
</div>
<div class="envelope-content" style="padding: 0; overflow: hidden;">
@if (_isLoading)
{
<div class="d-flex justify-content-center align-items-center h-100">
<div class="text-center">
<div class="spinner-border text-white mb-3" style="width: 3.5rem; height: 3.5rem;" role="status">
<span class="visually-hidden">Lädt...</span>
</div>
<p class="text-white fw-semibold">Dokument wird geladen...</p>
</div>
</div>
}
else if (_errorMessage is not null)
{
<div class="error-container">
<div class="alert alert-danger shadow-lg">
<div class="d-flex align-items-start">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="me-3 flex-shrink-0" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
</svg>
<div>
<h5 class="mb-2">Fehler</h5>
<p class="mb-0">@_errorMessage</p>
</div>
</div>
</div>
</div>
}
else if (_report is not null)
{
<DxReportViewer Report="_report" RootCssClasses="w-100 h-100" />
}
</div>
</div>
@* Submit confirmation popup *@
<DxPopup @bind-Visible="_submitConfirmVisible"
HeaderText="Unterschrift bestätigen"
Width="440px"
MaxWidth="95vw"
ShowFooter="true"
CloseOnOutsideClick="false"
ShowCloseButton="false"
CloseOnEscape="false">
<BodyContentTemplate>
<div style="display: flex; align-items: flex-start; gap: 1rem; padding: 0.5rem 0;">
<div style="flex-shrink: 0; width: 40px; height: 40px; background: #d1fae5; border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#065f46" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
</svg>
</div>
<div>
<p style="margin: 0 0 0.4rem; font-weight: 600; color: #1f2937; font-size: 0.95rem;">
Möchten Sie das Dokument verbindlich unterschreiben?
</p>
<p style="margin: 0; color: #6b7280; font-size: 0.85rem; line-height: 1.5;">
Diese Aktion kann nicht rückgängig gemacht werden. Mit der Bestätigung erklären Sie, das oben angezeigte Dokument elektronisch unterzeichnet zu haben. Das unterzeichnete Dokument wird anschließend an alle beteiligten Parteien übermittelt.
</p>
</div>
</div>
</BodyContentTemplate>
<FooterContentTemplate>
<div class="d-flex gap-2 justify-content-end w-100" style="padding: 0.5rem 0;">
<button class="btn btn-outline-secondary"
@onclick="() => _submitConfirmVisible = false"
style="border-radius: 6px; padding: 0.5rem 1.25rem; font-weight: 500;">
Abbrechen
</button>
<button class="btn btn-primary"
@onclick="SubmitAndLogoutAsync"
disabled="@_isLoggingOut"
style="background: linear-gradient(135deg, #059669 0%, #047857 100%); border: none; border-radius: 6px; padding: 0.5rem 1.5rem; font-weight: 600; box-shadow: 0 2px 4px rgba(5, 150, 105, 0.3);">
@if (_isLoggingOut)
{
<span class="spinner-border spinner-border-sm me-1" role="status"></span>
}
Abschließen
</button>
</div>
</FooterContentTemplate>
</DxPopup>
@code {
[Parameter] public string? EnvelopeKey { get; set; }
[SupplyParameterFromQuery(Name = "sid")]
public string? Sid { get; set; }
bool _isLoading = true;
string? _errorMessage;
ClaimsPrincipal? _receiverUser;
EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver;
IReadOnlyList<SignatureDto> _signatures = [];
XtraReport? _report;
SignatureCaptureDto? _sig;
// ----- Submit / logout state -----
bool _isLoggingOut = false;
bool _submitConfirmVisible = false;
void OpenSubmitConfirmPopup() => _submitConfirmVisible = true;
async Task SubmitAndLogoutAsync()
{
if (_isLoggingOut) return;
_isLoggingOut = true;
_submitConfirmVisible = false;
await InvokeAsync(StateHasChanged);
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey!);
Navigation.NavigateTo("/", forceLoad: true);
}
protected override async Task OnInitializedAsync()
{
if (string.IsNullOrWhiteSpace(EnvelopeKey))
{
_errorMessage = "Envelope-Schlüssel fehlt.";
_isLoading = false;
return;
}
_receiverUser = await ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey);
if (_receiverUser is null)
{
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
return;
}
// Read signature from IMemoryCache
if (!string.IsNullOrWhiteSpace(Sid)
&& MemoryCache.TryGetValue(Sid, out SignatureCaptureDto? cached)
&& cached is not null)
{
_sig = cached;
}
// Cache miss or missing sid — redirect back to report page
if (_sig is null)
{
Logger.LogWarning(
"[SignedPage] Cache miss or no sid={Sid} for {EnvelopeKey}, redirecting to report page.",
Sid, EnvelopeKey);
Navigation.NavigateTo(
$"/envelope/{Uri.EscapeDataString(EnvelopeKey)}",
forceLoad: true);
return;
}
try
{
var pdfBytes = await PageDataService.GetDocumentAsync(_receiverUser);
if (pdfBytes is not { Length: > 0 })
{
_errorMessage = "Dokument konnte nicht geladen werden.";
_isLoading = false;
return;
}
_envelopeReceiver = await PageDataService.GetEnvelopeReceiverAsync(EnvelopeKey);
_signatures = await PageDataService.GetSignaturesAsync(_receiverUser);
// Burn signature image + info onto PDF via PdfSharp
if (_sig is not null && _signatures.Count > 0)
pdfBytes = DrawSignaturesOnPdf(pdfBytes, _signatures, _sig);
var report = new XtraReport
{
PaperKind = DevExpress.Drawing.Printing.DXPaperKind.A4,
Landscape = false,
Margins = new System.Drawing.Printing.Margins(0, 0, 0, 0),
};
var detail = new DetailBand();
report.Bands.Add(detail);
detail.Controls.Add(new XRPdfContent
{
Source = pdfBytes,
GenerateOwnPages = true,
});
_report = report;
}
catch (Exception ex)
{
_errorMessage = $"Fehler: {ex.Message}";
Logger.LogError(ex, "Error loading signed page for {EnvelopeKey}", EnvelopeKey);
}
_isLoading = false;
await InvokeAsync(StateHasChanged);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
if (_sig is not null)
{
await JSRuntime.InvokeVoidAsync("console.log",
$"[SignedPage] sid={Sid} | FullName={_sig.FullName} | Place={_sig.Place} | Position={_sig.Position} | DataUrl.Length={_sig.DataUrl?.Length ?? 0}");
}
else
{
await JSRuntime.InvokeVoidAsync("console.log",
$"[SignedPage] Cache miss or no sid. sid={Sid}");
}
}
public void Dispose()
{
_report?.Dispose();
}
// ----- PDF signature rendering -----
/// <summary>
/// Uses PdfSharp to burn the captured signature onto the PDF at each signature field.
/// Layout per field (top-left origin, Y down, units = PDF points):
/// [top 65%] signature image
/// [separator line]
/// [bottom 35%] FullName (bold) / Position (optional) / Place, Date
/// </summary>
static byte[] DrawSignaturesOnPdf(
byte[] pdfBytes,
IReadOnlyList<SignatureDto> signatures,
SignatureCaptureDto sig)
{
var imgBytes = DataUrlToBytes(sig.DataUrl);
if (imgBytes is not { Length: > 0 }) return pdfBytes;
using var inputMs = new System.IO.MemoryStream(pdfBytes);
using var outputMs = new System.IO.MemoryStream();
var document = PdfSharp.Pdf.IO.PdfReader.Open(
inputMs, PdfSharp.Pdf.IO.PdfDocumentOpenMode.Modify);
const double sigW = 1.77 * 72; // 127.44 pt
const double sigH = 1.96 * 72; // 141.12 pt
const double imgRatio = 0.52; // top 52% = image
const double lineH = 11.5; // fixed row height matching font size (bold 7.5pt + normal 6.5pt)
const double bgPad = 3.0; // background box padding around content (pt)
var black = PdfSharp.Drawing.XColor.FromArgb(255, 20, 20, 20);
var darkGray = PdfSharp.Drawing.XColor.FromArgb(255, 80, 80, 80);
var lineColor = PdfSharp.Drawing.XColor.FromArgb(180, 100, 100, 120);
var bgColor = PdfSharp.Drawing.XColor.FromArgb(255, 255, 253, 240);
var bgBrush = new PdfSharp.Drawing.XSolidBrush(bgColor);
var fontBold = new PdfSharp.Drawing.XFont("Arial", 7.5, PdfSharp.Drawing.XFontStyleEx.Bold);
var fontNormal = new PdfSharp.Drawing.XFont("Arial", 6.5, PdfSharp.Drawing.XFontStyleEx.Regular);
var linePen = new PdfSharp.Drawing.XPen(lineColor, 0.5);
var fmtLeft = new PdfSharp.Drawing.XStringFormat
{
Alignment = PdfSharp.Drawing.XStringAlignment.Near,
LineAlignment = PdfSharp.Drawing.XLineAlignment.Near,
};
var date = DateTime.Now.ToString("dd.MM.yyyy");
foreach (var field in signatures)
{
int pageIndex = field.Page - 1;
if (pageIndex < 0 || pageIndex >= document.PageCount) continue;
var page = document.Pages[pageIndex];
using var gfx = PdfSharp.Drawing.XGraphics.FromPdfPage(page);
double x = field.X;
double y = field.Y;
// --- Calculate layout positions first (needed for bg rect) ---
double imgH = sigH * imgRatio;
double lineY = y + imgH + 1.0; // 1pt gap between image and separator
double textY = lineY + 1.5; // 1.5pt gap below separator line
double padding = 3;
// Row 1: FullName
double row1Y = textY;
// Row 2: Position (optional)
double row2Y = row1Y + lineH;
// Row 3: Place, Date — immediately after row2 regardless of position
double row3Y = !string.IsNullOrWhiteSpace(sig.Position) ? row2Y + lineH : row2Y;
double contentBottom = row3Y + lineH;
// --- Background rectangle sized to actual content (not full sigH) ---
var bgRect = new PdfSharp.Drawing.XRect(
x - bgPad,
y - bgPad,
sigW + bgPad * 2,
(contentBottom - y) + bgPad * 2);
gfx.DrawRectangle(bgBrush, bgRect);
// --- Image area ---
var imgRect = new PdfSharp.Drawing.XRect(x, y, sigW, imgH);
using var imgStream = new System.IO.MemoryStream(imgBytes);
var xImg = PdfSharp.Drawing.XImage.FromStream(imgStream);
gfx.DrawImage(xImg, imgRect);
// --- Separator line ---
gfx.DrawLine(linePen, x + 2, lineY, x + sigW - 2, lineY);
// --- Text rows ---
// Row 1: FullName (bold)
var nameRect = new PdfSharp.Drawing.XRect(x + padding, row1Y, sigW - padding * 2, lineH);
gfx.DrawString(sig.FullName, fontBold, new PdfSharp.Drawing.XSolidBrush(black), nameRect, fmtLeft);
// Row 2: Position (optional)
if (!string.IsNullOrWhiteSpace(sig.Position))
{
var posRect = new PdfSharp.Drawing.XRect(x + padding, row2Y, sigW - padding * 2, lineH);
gfx.DrawString(sig.Position, fontNormal, new PdfSharp.Drawing.XSolidBrush(darkGray), posRect, fmtLeft);
}
// Row 3: Place, Date
var placeDate = $"{sig.Place}, {date}";
var dateRect = new PdfSharp.Drawing.XRect(x + padding, row3Y, sigW - padding * 2, lineH);
gfx.DrawString(placeDate, fontNormal, new PdfSharp.Drawing.XSolidBrush(darkGray), dateRect, fmtLeft);
}
document.Save(outputMs);
return outputMs.ToArray();
}
static byte[]? DataUrlToBytes(string? dataUrl)
{
if (string.IsNullOrWhiteSpace(dataUrl)) return null;
var comma = dataUrl.IndexOf(',');
if (comma < 0) return null;
return Convert.FromBase64String(dataUrl[(comma + 1)..]);
}
}

View File

@@ -1,4 +1,4 @@
@page "/envelope/{EnvelopeKey}" @page "/envelope/{EnvelopeKey}/deprc"
@rendermode InteractiveServer @rendermode InteractiveServer
@using EnvelopeGenerator.Server.Client.Models @using EnvelopeGenerator.Server.Client.Models
@using EnvelopeGenerator.Server.Client.Models.Constants @using EnvelopeGenerator.Server.Client.Models.Constants
@@ -9,7 +9,6 @@
@using EnvelopeGenerator.Server.Client.Options @using EnvelopeGenerator.Server.Client.Options
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using DevExpress.Blazor @using DevExpress.Blazor
@using DevExpress.Blazor.PdfViewer
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject IOptions<PdfViewerOptions> PdfViewerOptions @inject IOptions<PdfViewerOptions> PdfViewerOptions
@inject IJSRuntime JSRuntime @inject IJSRuntime JSRuntime
@@ -22,6 +21,7 @@
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" /> <link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" /> <link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script src="@AppVersion.GetVersionedUrl("js/pdf-viewer.js")"></script> <script src="@AppVersion.GetVersionedUrl("js/pdf-viewer.js")"></script>
<script src="@AppVersion.GetVersionedUrl("js/receiver-signature.js")"></script> <script src="@AppVersion.GetVersionedUrl("js/receiver-signature.js")"></script>
@@ -223,6 +223,27 @@
</button> </button>
</div> </div>
<div class="pdf-toolbar__divider"></div>
<div class="pdf-toolbar__section pdf-toolbar__zoom-section">
<button class="pdf-toolbar__btn" @onclick="ZoomOut" disabled="@(_currentZoom <= 50)" title="Verkleinern">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0zM4 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1H4z" />
</svg>
</button>
<div class="pdf-toolbar__zoom-slider-container">
<input type="range" class="pdf-toolbar__zoom-slider" min="50" max="300" step="@(PdfViewerOptions.Value.ZoomStepPercentage)" value="@_currentZoom" @oninput="OnZoomSliderChanged" title="@(_currentZoom)%" />
<div class="pdf-toolbar__zoom-label">@(_currentZoom)%</div>
</div>
<button class="pdf-toolbar__btn" @onclick="ZoomIn" disabled="@(_currentZoom >= 300)" title="Vergrößern">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0zM6.5 3a.5.5 0 0 0-1 0v2.5H3a.5.5 0 0 0 0 1h2.5V9a.5.5 0 0 0 1 0V6.5H9a.5.5 0 0 0 0-1H6.5V3z" />
</svg>
</button>
</div>
<div class="pdf-toolbar__divider"></div>
@if (_totalSignatures > 0) @if (_totalSignatures > 0)
{ {
<div class="pdf-toolbar__section"> <div class="pdf-toolbar__section">
@@ -327,16 +348,10 @@
</div> </div>
} }
<div class="pdf-canvas-wrapper"> <div class="pdf-canvas-wrapper">
<div id="pdf-dx-viewer-host" class="envelope-dx-viewer-host"> <div class="pdf-page-container">
@if (_pdfDocumentContent is not null && _pdfDocumentContent.Length > 0) <canvas id="pdf-canvas" class="pdf-canvas"></canvas>
{ <div id="pdf-text-layer" class="pdf-text-layer"></div>
<DxPdfViewer @ref="_pdfViewer" <div id="pdf-signature-layer" class="pdf-signature-layer"></div>
CssClass="envelope-dx-pdf-viewer"
DocumentContent="@_pdfDocumentContent"
ZoomLevel="@_viewerZoomLevel"
IsSinglePagePreview="true" />
}
<div id="pdf-signature-layer" class="pdf-signature-layer pdf-signature-layer--dx"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -533,16 +548,13 @@
bool _isLoading = true; bool _isLoading = true;
string? _errorMessage; string? _errorMessage;
string? _pdfDataUrl; string? _pdfDataUrl;
byte[]? _pdfDocumentContent;
bool _pdfLoaded = false; bool _pdfLoaded = false;
int _currentPage = 1; int _currentPage = 1;
int _totalPages = 0; int _totalPages = 0;
int _currentZoom = 150; int _currentZoom = 150;
double _viewerZoomLevel = 1.5;
bool _showThumbnails = true; bool _showThumbnails = true;
bool _isLoggingOut = false; bool _isLoggingOut = false;
DotNetObjectReference<EnvelopeReceiverPage>? _dotNetRef; DotNetObjectReference<EnvelopeReceiverPage>? _dotNetRef;
DxPdfViewer? _pdfViewer;
IReadOnlyList<SignatureDto> _signatures = []; IReadOnlyList<SignatureDto> _signatures = [];
EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver; EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver;
ClaimsPrincipal? _receiverUser; ClaimsPrincipal? _receiverUser;
@@ -603,7 +615,6 @@
if (pdfBytes is { Length: > 0 }) if (pdfBytes is { Length: > 0 })
{ {
_pdfDocumentContent = pdfBytes;
var base64 = Convert.ToBase64String(pdfBytes); var base64 = Convert.ToBase64String(pdfBytes);
_pdfDataUrl = $"data:application/pdf;base64,{base64}"; _pdfDataUrl = $"data:application/pdf;base64,{base64}";
} }
@@ -710,18 +721,17 @@
options.ZoomStepPercentage options.ZoomStepPercentage
}); });
var success = await JSRuntime.InvokeAsync<bool>("pdfViewer.initialize", _pdfDataUrl, "pdf-dx-viewer-host", "pdf-signature-layer", _dotNetRef); var success = await JSRuntime.InvokeAsync<bool>("pdfViewer.initialize", "pdf-canvas", _pdfDataUrl, _dotNetRef);
if (success) if (success)
{ {
_pdfLoaded = true; _pdfLoaded = true;
_totalPages = await JSRuntime.InvokeAsync<int>("pdfViewer.getTotalPages"); _totalPages = await JSRuntime.InvokeAsync<int>("pdfViewer.getTotalPages");
_currentPage = 1; _currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
// Attach resize listeners // Attach resize listeners
await JSRuntime.InvokeVoidAsync("pdfViewer.attachResizeListeners", _dotNetRef); await JSRuntime.InvokeVoidAsync("pdfViewer.attachResizeListeners", _dotNetRef);
await JSRuntime.InvokeVoidAsync("pdfViewer.attachViewerInteractionListeners", "pdf-dx-viewer-host", _dotNetRef);
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
@@ -744,49 +754,59 @@
[JSInvokable] [JSInvokable]
public async Task OnZoomChanged(double scale) public async Task OnZoomChanged(double scale)
{ {
var requestedZoom = (int)Math.Round(scale * 100, MidpointRounding.AwayFromZero); _currentZoom = (int)(scale * 100);
await SetZoom(requestedZoom); await InvokeAsync(StateHasChanged);
// Small delay for canvas render to complete (reduced from 100ms to 10ms)
await Task.Delay(10);
await RenderSignatureButtonsAsync();
} }
async Task NextPage() async Task NextPage()
{ {
if (_currentPage >= _totalPages) if (await JSRuntime.InvokeAsync<bool>("pdfViewer.nextPage"))
return; {
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
_currentPage++; await RenderSignatureButtonsAsync();
await ApplyViewerStateAsync(); }
} }
async Task PreviousPage() async Task PreviousPage()
{ {
if (_currentPage <= 1) if (await JSRuntime.InvokeAsync<bool>("pdfViewer.previousPage"))
return; {
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
_currentPage--; await RenderSignatureButtonsAsync();
await ApplyViewerStateAsync(); }
} }
async Task ZoomIn() async Task ZoomIn()
{ {
if (_currentZoom >= 300) return; if (_currentZoom >= 300) return;
await SetZoom(_currentZoom + PdfViewerOptions.Value.ZoomStepPercentage); await JSRuntime.InvokeVoidAsync("pdfViewer.zoomIn");
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
_currentZoom = (int)(scale * 100);
// Update signature overlay positions after zoom
await RenderSignatureButtonsAsync();
} }
async Task ZoomOut() async Task ZoomOut()
{ {
if (_currentZoom <= 50) return; if (_currentZoom <= 50) return;
await SetZoom(_currentZoom - PdfViewerOptions.Value.ZoomStepPercentage); await JSRuntime.InvokeVoidAsync("pdfViewer.zoomOut");
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
_currentZoom = (int)(scale * 100);
// Update signature overlay positions after zoom
await RenderSignatureButtonsAsync();
} }
async Task SetZoom(int percentage) async Task SetZoom(int percentage)
{ {
_currentZoom = Math.Clamp(percentage, 50, 300); var scale = percentage / 100.0;
_viewerZoomLevel = _currentZoom / 100d; await JSRuntime.InvokeVoidAsync("pdfViewer.setScale", scale);
_currentZoom = percentage;
await InvokeAsync(StateHasChanged);
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
await Task.Delay(150);
await RenderSignatureButtonsAsync();
} }
async Task OnZoomSliderChanged(ChangeEventArgs e) async Task OnZoomSliderChanged(ChangeEventArgs e)
@@ -804,20 +824,18 @@
{ {
if (int.TryParse(e.Value?.ToString(), out var pageNum) && pageNum >= 1 && pageNum <= _totalPages) if (int.TryParse(e.Value?.ToString(), out var pageNum) && pageNum >= 1 && pageNum <= _totalPages)
{ {
_currentPage = pageNum; if (await JSRuntime.InvokeAsync<bool>("pdfViewer.goToPage", pageNum))
await ApplyViewerStateAsync(); {
_currentPage = pageNum;
}
} }
} }
async Task FitToWidth() async Task FitToWidth()
{ {
_viewerZoomLevel = -2; await JSRuntime.InvokeVoidAsync("pdfViewer.fitToWidth");
_currentZoom = 150; var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
_currentZoom = (int)(scale * 100);
await InvokeAsync(StateHasChanged);
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
await Task.Delay(150);
await RenderSignatureButtonsAsync();
} }
async Task ToggleThumbnails() async Task ToggleThumbnails()
@@ -835,11 +853,11 @@
async Task GoToPageFromThumbnail(int pageNum) async Task GoToPageFromThumbnail(int pageNum)
{ {
if (pageNum < 1 || pageNum > _totalPages) if (await JSRuntime.InvokeAsync<bool>("pdfViewer.goToPage", pageNum))
return; {
_currentPage = pageNum;
_currentPage = pageNum; await RenderSignatureButtonsAsync();
await ApplyViewerStateAsync(); }
} }
async Task RenderSignatureButtonsAsync() async Task RenderSignatureButtonsAsync()
@@ -848,7 +866,6 @@
try try
{ {
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons", _signatures, _currentPage, _dotNetRef); await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons", _signatures, _currentPage, _dotNetRef);
await UpdateSignatureCounterAsync(); await UpdateSignatureCounterAsync();
} }
@@ -889,13 +906,7 @@
public async Task OnPageChangedBySignatureNav(int newPage) public async Task OnPageChangedBySignatureNav(int newPage)
{ {
_currentPage = newPage; _currentPage = newPage;
await ApplyViewerStateAsync(); await RenderSignatureButtonsAsync();
}
[JSInvokable]
public async Task OnZoomGestureRequested(int zoomPercentage)
{
await SetZoom(zoomPercentage);
} }
async Task UpdateSignatureCounterAsync() async Task UpdateSignatureCounterAsync()
@@ -1178,22 +1189,6 @@
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
async Task ApplyViewerStateAsync()
{
if (!_pdfLoaded)
return;
if (_viewerZoomLevel > 0)
{
_viewerZoomLevel = _currentZoom / 100d;
}
await InvokeAsync(StateHasChanged);
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
await Task.Delay(150);
await RenderSignatureButtonsAsync();
}
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
if (_pdfLoaded) if (_pdfLoaded)

View File

@@ -10,4 +10,3 @@
@using EnvelopeGenerator.Server.Client @using EnvelopeGenerator.Server.Client
@using EnvelopeGenerator.Server.Components @using EnvelopeGenerator.Server.Components
@using DevExpress.Blazor @using DevExpress.Blazor
@using DevExpress.Blazor.PdfViewer

View File

@@ -40,7 +40,7 @@ public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions,
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</response> /// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</response>
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)] [ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)] [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[Authorize(Policy = AuthPolicy.Sender)] [Authorize(AuthenticationSchemes = AuthScheme.Sender)]
[HttpPost("logout")] [HttpPost("logout")]
public IActionResult Logout() public IActionResult Logout()
{ {

View File

@@ -30,7 +30,7 @@ public class DocumentController(IMediator mediator, IAuthorizationService authSe
/// <param name="query">Encoded envelope key.</param> /// <param name="query">Encoded envelope key.</param>
/// <param name="cancel">Cancellation token.</param> /// <param name="cancel">Cancellation token.</param>
[HttpGet] [HttpGet]
[Authorize(Policy = AuthPolicy.Sender)] [Authorize(AuthenticationSchemes = AuthScheme.Sender)]
public async Task<IActionResult> GetDocument(CancellationToken cancel, [FromQuery] ReadDocumentQuery? query = null) public async Task<IActionResult> GetDocument(CancellationToken cancel, [FromQuery] ReadDocumentQuery? query = null)
{ {
if (query is null) if (query is null)

View File

@@ -24,7 +24,7 @@ namespace EnvelopeGenerator.Server.Controllers;
/// </param> /// </param>
[Route("api/[controller]")] [Route("api/[controller]")]
[ApiController] [ApiController]
[Authorize(Policy = AuthPolicy.Sender)] [Authorize(AuthenticationSchemes = AuthScheme.Sender)]
public class EmailTemplateController(IMediator mediator) : ControllerBase public class EmailTemplateController(IMediator mediator) : ControllerBase
{ {
/// <summary> /// <summary>

View File

@@ -150,7 +150,7 @@ public class EnvelopeReceiverController : ControllerBase
/// <response code="400">Wenn ein Fehler im HTTP-Body auftritt</response> /// <response code="400">Wenn ein Fehler im HTTP-Body auftritt</response>
/// <response code="401">Wenn kein autorisierter Token vorhanden ist</response> /// <response code="401">Wenn kein autorisierter Token vorhanden ist</response>
/// <response code="500">Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden.</response> /// <response code="500">Es handelt sich um einen unerwarteten Fehler. Die Protokolle sollten überprüft werden.</response>
[Authorize] [Authorize(AuthenticationSchemes = AuthScheme.Sender)]
[HttpPost] [HttpPost]
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel) public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeReceiverCommand request, CancellationToken cancel)
{ {
@@ -214,6 +214,10 @@ public class EnvelopeReceiverController : ControllerBase
if (reader.Read()) if (reader.Read())
{ {
bool outSuccess = reader.GetBoolean(0); bool outSuccess = reader.GetBoolean(0);
if (!outSuccess)
_logger.LogWarning(
"PRSIG_API_ADD_DOC_RECEIVER_ELEM returned OUT_SUCCESS=false. DOC_ID={DocId}, RECEIVER_ID={ReceiverId}, Page={Page}",
document.Id, rcv.Id, sign.Page);
} }
} }
#endregion #endregion
@@ -221,8 +225,6 @@ public class EnvelopeReceiverController : ControllerBase
#region Create history #region Create history
// ENV_UID, STATUS_ID, USER_ID, // ENV_UID, STATUS_ID, USER_ID,
string sql_hist = @" string sql_hist = @"
USE [DD_ECM]
DECLARE @OUT_SUCCESS bit; DECLARE @OUT_SUCCESS bit;
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE] EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
@@ -244,6 +246,10 @@ public class EnvelopeReceiverController : ControllerBase
if (reader.Read()) if (reader.Read())
{ {
bool outSuccess = reader.GetBoolean(0); bool outSuccess = reader.GetBoolean(0);
if (!outSuccess)
_logger.LogWarning(
"PRSIG_API_ADD_HISTORY_STATE returned OUT_SUCCESS=false. EnvelopeUuid={EnvelopeUuid}",
envelope.Uuid);
} }
} }
#endregion #endregion

View File

@@ -3,7 +3,7 @@ using EnvelopeGenerator.Application.Receivers.Queries;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.GeneratorAPI.Controllers; namespace EnvelopeGenerator.Server.Controllers;
/// <summary> /// <summary>
/// Controller für die Verwaltung von Empfängern. /// Controller für die Verwaltung von Empfängern.
@@ -33,15 +33,16 @@ public class ReceiverController : ControllerBase
/// <param name="receiver">Die Abfrageparameter, einschließlich E-Mail-Adresse und Signatur.</param> /// <param name="receiver">Die Abfrageparameter, einschließlich E-Mail-Adresse und Signatur.</param>
/// <returns>Eine Liste von Empfängern oder ein Fehlerstatus.</returns> /// <returns>Eine Liste von Empfängern oder ein Fehlerstatus.</returns>
[HttpGet] [HttpGet]
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery receiver) [Authorize(AuthenticationSchemes = AuthScheme.Sender)]
public async Task<IActionResult> Get([FromQuery] ReadReceiverQuery? receiver = null, [FromQuery] bool onlyEmailAddress = false)
{ {
if (!receiver.HasAnyCriteria) var result = await _mediator.Send(receiver ?? new ReadReceiverQuery());
{
var all = await _mediator.Send(new ReadReceiverQuery());
return Ok(all);
}
var result = await _mediator.Send(receiver); if (result is null)
return result is null ? NotFound() : Ok(result); return NotFound();
else if (onlyEmailAddress)
return Ok(result.Select(r => r.EmailAddress).ToList());
else
return Ok(result);
} }
} }

View File

@@ -1,9 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFrameworks>net8.0</TargetFrameworks>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageId>EnvelopeGenerator.Server</PackageId>
<Title></Title>
<Authors>Digital Data GmbH</Authors>
<Company>Digital Data GmbH</Company>
<Product>EnvelopeGenerator.Server</Product>
<Version>1.0.1-beta</Version>
<FileVersion>1.0.1.0</FileVersion>
<AssemblyVersion>1.0.1.0</AssemblyVersion>
<Copyright>Copyright © 2026 Digital Data GmbH. All rights reserved.</Copyright>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -28,13 +39,12 @@
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" /> <PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
<PackageReference Include="HtmlSanitizer" Version="9.0.892" /> <PackageReference Include="HtmlSanitizer" Version="9.0.892" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" /> <PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" />
<PackageReference Include="itext" Version="8.0.5" />
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" /> <PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
<PackageReference Include="NLog" Version="5.2.5" /> <PackageReference Include="NLog" Version="5.2.5" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" /> <PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
<PackageReference Include="PDFsharp" Version="6.2.4" />
<PackageReference Include="Scalar.AspNetCore" Version="2.2.1" /> <PackageReference Include="Scalar.AspNetCore" Version="2.2.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />

View File

@@ -0,0 +1,33 @@
namespace EnvelopeGenerator.Server.Handlers;
/// <summary>
/// A <see cref="DelegatingHandler"/> that forwards the incoming HTTP request's
/// <c>Cookie</c> header to all outgoing <see cref="System.Net.Http.HttpClient"/> calls
/// made by Blazor Server components.
///
/// Problem it solves:
/// Blazor Server runs on the server process. When a component calls an API endpoint
/// that requires cookie-based JWT authentication (AuthScheme.Sender), the HttpClient
/// does not automatically include the browser's cookies — those only travel with
/// browser-initiated requests. This handler copies the <c>Cookie</c> header from the
/// current <see cref="IHttpContextAccessor.HttpContext"/> into every outgoing request
/// so that the API's JwtBearer <c>OnMessageReceived</c> callback can extract the token.
///
/// Thread safety:
/// The handler is registered as Transient and is resolved per-request by the
/// IHttpClientFactory pipeline, so there is no shared state between requests.
/// </summary>
public class SenderAuthCookieHandler(IHttpContextAccessor httpContextAccessor) : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var cookieHeader = httpContextAccessor.HttpContext?.Request.Headers["Cookie"].ToString();
if (!string.IsNullOrWhiteSpace(cookieHeader))
request.Headers.TryAddWithoutValidation("Cookie", cookieHeader);
return base.SendAsync(request, cancellationToken);
}
}

View File

@@ -69,6 +69,9 @@ try
builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpContextAccessor();
// Named HttpClient for internal API calls // Named HttpClient for internal API calls
// SenderAuthCookieHandler forwards the browser's Cookie header so that
// Blazor Server components can call cookie-authenticated endpoints (AuthScheme.Sender).
builder.Services.AddTransient<EnvelopeGenerator.Server.Handlers.SenderAuthCookieHandler>();
builder.Services.AddHttpClient("EnvelopeGenerator.Server", (sp, client) => builder.Services.AddHttpClient("EnvelopeGenerator.Server", (sp, client) =>
{ {
var httpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>(); var httpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>();
@@ -79,7 +82,8 @@ try
// Set base address to current host for SSR scenarios // Set base address to current host for SSR scenarios
client.BaseAddress = new Uri($"{request.Scheme}://{request.Host}"); client.BaseAddress = new Uri($"{request.Scheme}://{request.Host}");
} }
}); })
.AddHttpMessageHandler<EnvelopeGenerator.Server.Handlers.SenderAuthCookieHandler>();
// CORS Policy // CORS Policy
var allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ?? var allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ??
@@ -104,7 +108,7 @@ try
{ {
Version = "v1", Version = "v1",
Title = "signFLOW Absender-API", Title = "signFLOW Absender-API",
Description = "Eine API zur Verwaltung der Erstellung, des Versands und der Nachverfolgung von Umschlägen in der signFLOW-Anwendung.", Description = "Eine API zur Verwaltung der Erstellung, des Versands und der Nachverfolgung von Umschl<EFBFBD>gen in der signFLOW-Anwendung.",
Contact = new OpenApiContact Contact = new OpenApiContact
{ {
Name = "Digital Data GmbH", Name = "Digital Data GmbH",
@@ -338,6 +342,10 @@ try
builder.Services.AddDevExpressBlazor(); builder.Services.AddDevExpressBlazor();
builder.Services.AddDevExpressServerSideBlazorPdfViewer(); builder.Services.AddDevExpressServerSideBlazorPdfViewer();
// PdfSharp font resolver — required for .NET 8 (no system font access without it)
PdfSharp.Fonts.GlobalFontSettings.FontResolver =
EnvelopeGenerator.Server.Services.PdfSharpFontResolver.Instance;
// Configuration Options // Configuration Options
builder.Services.Configure<EnvelopeGenerator.Server.Client.Options.ApiOptions>( builder.Services.Configure<EnvelopeGenerator.Server.Client.Options.ApiOptions>(
builder.Configuration.GetSection("ApiOptions")); builder.Configuration.GetSection("ApiOptions"));

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<WebPublishMethod>Package</WebPublishMethod>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<SiteUrlToLaunchAfterPublish />
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<ExcludeApp_Data>false</ExcludeApp_Data>
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
<DesktopBuildPackageLocation>M:\App&amp;Service\0 DD - Smart UP\signFLOW\API\net8\$(Version)\EnvelopeGenerator.Server.zip</DesktopBuildPackageLocation>
<PackageAsSingleFile>true</PackageAsSingleFile>
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
<_TargetId>IISWebDeployPackage</_TargetId>
<TargetFramework>net8.0</TargetFramework>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<EnvironmentName>Production</EnvironmentName>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,225 @@
# EnvelopeGenerator.Server — Publish & Deployment Guide
## Inhaltsverzeichnis
1. [Unterschied zu einer normalen ASP.NET Core API](#unterschied-zu-einer-normalen-aspnet-core-api)
2. [Warum Self-Contained Publish?](#warum-self-contained-publish)
3. [Publish-Befehl (Terminal)](#publish-befehl-terminal)
4. [IIS-Konfiguration](#iis-konfiguration)
5. [Verzeichnisstruktur nach dem Publish](#verzeichnisstruktur-nach-dem-publish)
6. [Häufige Fehler](#häufige-fehler)
---
## Unterschied zu einer normalen ASP.NET Core API
`EnvelopeGenerator.Server` ist **keine** gewöhnliche ASP.NET Core Web API. Es handelt sich um eine **Blazor Auto (Server + WebAssembly Hybrid)**-Anwendung.
| Merkmal | Normale ASP.NET Core API | EnvelopeGenerator.Server (Blazor Auto) |
|---|---|---|
| Projekttyp | `Microsoft.NET.Sdk.Web` | `Microsoft.NET.Sdk.Web` + WASM Client |
| Frontend | Keins / Razor Pages | Blazor Server + Blazor WASM |
| WASM-Komponente | Nein | Ja (`EnvelopeGenerator.Server.Client`) |
| Framework-DLL-Bindung | Tolerant gegenüber Runtime-Versionen | **Strikt**: WASM erwartet exakte Assembly-Versionen |
| Publish ohne .NET auf Zielserver | Nicht nötig (FDD reicht meist) | **Self-Contained Pflicht** empfohlen |
| IIS Application Pool | `.NET CLR v4.0` oder `No Managed Code` | **Zwingend: `No Managed Code`** |
| Publish-Paketgröße | ~520 MB | **~500 MB** (enthält .NET Runtime) |
| `web.config processPath` | `dotnet` + `.dll` | **`.\EnvelopeGenerator.Server.exe`** |
### Warum die WASM-Komponente den Unterschied macht
Die WASM-Seite der Anwendung (`EnvelopeGenerator.Server.Client`) bindet Assemblies wie
`Microsoft.Extensions.DependencyInjection.Abstractions` in einer **fest definierten Version**.
Bei einem **Framework-Dependent Deployment** werden diese Assemblies nicht mitgeliefert und
müssen auf dem Zielserver vorhanden sein — in der exakt passenden Version.
Fehlt die passende .NET-Runtime auf dem Zielserver, erscheint folgender Fehler beim Start:
```
Unhandled exception. System.IO.FileNotFoundException:
Could not load file or assembly
'Microsoft.Extensions.DependencyInjection.Abstractions, Version=8.0.0.0'
```
---
## Warum Self-Contained Publish?
Beim **Self-Contained Deployment** werden **alle benötigten .NET Runtime-DLLs** in das
Ausgabeverzeichnis kopiert. Die Anwendung ist damit vollständig unabhängig von der auf dem
Zielserver installierten .NET-Version.
| | Framework-Dependent | Self-Contained |
|---|---|---|
| .NET auf Zielserver nötig | Ja | **Nein** |
| Paketgröße | ~20 MB | ~500 MB |
| `runtimeconfig.json` | `frameworkVersion` vorhanden | `includedFrameworks` (Runtime eingebettet) |
| Fehleranfälligkeit auf Fremd-PC | Hoch | Minimal |
---
## Publish-Befehl (Terminal)
### Empfohlener Befehl (Self-Contained, win-x64)
```bat
dotnet publish EnvelopeGenerator.Server\EnvelopeGenerator.Server\EnvelopeGenerator.Server.csproj ^
-c Release ^
-f net8.0 ^
--self-contained true ^
--runtime win-x64 ^
-o .\publish-output
```
> Dieser Befehl muss vom **Solution-Root-Verzeichnis** aus ausgefuehrt werden.
> Alternativ: `publish.bat` im selben Verzeichnis wie diese README ausfuehren.
### Parameter-Erklaerung
| Parameter | Bedeutung |
|---|---|
| `-c Release` | Release-Konfiguration (optimiert, kein Debug-Code) |
| `-f net8.0` | Ziel-Framework explizit angeben (Pflicht, da `<TargetFrameworks>` mehrere Werte haben kann) |
| `--self-contained true` | Alle .NET Runtime-DLLs ins Ausgabeverzeichnis kopieren |
| `--runtime win-x64` | Zielplattform: Windows 64-Bit |
| `-o .\publish-output` | Ausgabeverzeichnis |
### Doğrulama nach dem Publish
Nach erfolgreichem Publish folgende Dateien im Ausgabeverzeichnis prüfen:
```powershell
# Diese Dateien MÜSSEN vorhanden sein (Self-Contained-Nachweis):
Test-Path ".\publish-output\hostfxr.dll" # .NET Host
Test-Path ".\publish-output\coreclr.dll" # .NET Core Runtime
Test-Path ".\publish-output\Microsoft.Extensions.DependencyInjection.Abstractions.dll"
Test-Path ".\publish-output\EnvelopeGenerator.Server.exe"
Test-Path ".\publish-output\web.config"
```
Alle Ergebnisse müssen `True` sein.
---
## IIS-Konfiguration
> **WICHTIG:** Diese Einstellungen unterscheiden sich von einer normalen ASP.NET Core API
> und sind zwingend erforderlich.
### 1. Application Pool — `No Managed Code`
ASP.NET Core (und damit auch Blazor) verwaltet seinen eigenen Runtime-Lifecycle.
IIS darf **keinen** .NET CLR-Managed-Code-Kontext aktivieren.
**Einstellung:**
```
IIS Manager
→ Application Pools
→ [Pool-Name der Anwendung] → Basic Settings
→ .NET CLR Version: "No Managed Code" ← ZWINGEND
```
> **Fehler bei falscher Einstellung:** HTTP 500.30 — ASP.NET Core app failed to start
> (sc-win32-status: 574 in IIS-Logs)
### 2. ASP.NET Core Module V2
Das IIS-Modul `AspNetCoreModuleV2` muss installiert sein.
Es wird über das **.NET Hosting Bundle** mitgeliefert.
Prüfen:
```
IIS Manager → Modules → "AspNetCoreModuleV2" vorhanden?
```
Falls nicht installiert: [.NET 8 Hosting Bundle herunterladen](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
und nach der Installation IIS neu starten:
```cmd
net stop was /y && net start w3svc
```
### 3. web.config — Korrekte Konfiguration für Self-Contained
Nach dem Publish wird `web.config` automatisch generiert. Für Self-Contained muss sie so aussehen:
```xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*"
modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath=".\EnvelopeGenerator.Server.exe"
stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout"
hostingModel="inprocess" />
</system.webServer>
</location>
</configuration>
```
**Kritische Unterschiede zu Framework-Dependent:**
| Eigenschaft | Framework-Dependent (FALSCH) | Self-Contained (RICHTIG) |
|---|---|---|
| `processPath` | `dotnet` | `.\EnvelopeGenerator.Server.exe` |
| `arguments` | `.\EnvelopeGenerator.Server.dll` | *(leer oder weggelassen)* |
### 4. Berechtigungen für das `logs`-Verzeichnis
Wenn `stdoutLogEnabled="true"` gesetzt wird (zur Fehlerdiagnose), muss das `logs`-Verzeichnis
existieren und der IIS-Prozess muss Schreibrechte haben:
```powershell
New-Item -ItemType Directory -Path "C:\inetpub\wwwroot\<App-Pfad>\logs" -Force
icacls "C:\inetpub\wwwroot\<App-Pfad>\logs" /grant "IIS_IUSRS:(OI)(CI)F"
```
> Ohne dieses Verzeichnis kann die Anwendung bei aktiviertem Logging **nicht starten**.
### 5. Application Pool Recycle nach Deployment
Nach jedem Deployment den Application Pool neu starten:
```cmd
# IIS Manager → Application Pools → [Pool] → Recycle
# oder per Kommandozeile (als Administrator):
%windir%\system32\inetsrv\appcmd recycle apppool /apppool.name:"<Pool-Name>"
```
---
## Verzeichnisstruktur nach dem Publish
```
publish-output\
├── EnvelopeGenerator.Server.exe ← Startpunkt (Self-Contained)
├── EnvelopeGenerator.Server.dll ← Managed Assembly
├── EnvelopeGenerator.Server.runtimeconfig.json
├── EnvelopeGenerator.Server.deps.json
├── web.config ← IIS-Konfiguration (auto-generiert)
├── hostfxr.dll ← .NET Host (Self-Contained-Nachweis)
├── coreclr.dll ← .NET Core Runtime
├── Microsoft.Extensions.*.dll ← Framework-DLLs (jetzt enthalten!)
├── DevExpress.*.dll ← UI-Komponenten
├── wwwroot\ ← Statische Web-Assets
│ ├── _framework\ ← WASM-Binaries
│ └── ...
└── logs\ ← Stdout-Logs (manuell anlegen!)
```
---
## Häufige Fehler
| Fehler | Ursache | Lösung |
|---|---|---|
| `FileNotFoundException: Microsoft.Extensions.DependencyInjection.Abstractions` | Framework-Dependent Publish auf Server ohne .NET 8 | Self-Contained Publish verwenden |
| `HTTP 500.30` in IIS | App startet nicht | Application Pool auf `No Managed Code` setzen |
| `HTTP 500.30` + `sc-win32-status: 574` | App Pool falsch oder `AspNetCoreModuleV2` fehlt | Pool prüfen + Hosting Bundle installieren |
| App startet per `.exe`, aber nicht in IIS | `web.config` hat noch `processPath="dotnet"` | `web.config` auf `.\EnvelopeGenerator.Server.exe` korrigieren |
| Logs-Verzeichnis fehlt → App startet nicht | `stdoutLogEnabled="true"` aber `logs\` existiert nicht | `logs\`-Ordner anlegen + IIS_IUSRS Schreibrecht geben |

View File

@@ -4,12 +4,14 @@ using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver; using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
using EnvelopeGenerator.Application.Documents.Queries; using EnvelopeGenerator.Application.Documents.Queries;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries; using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Application.Receivers.Queries;
using EnvelopeGenerator.Server.Client.Models; using EnvelopeGenerator.Server.Client.Models;
using EnvelopeGenerator.Server.Client.Models.Constants; using EnvelopeGenerator.Server.Client.Models.Constants;
using EnvelopeGenerator.Server.Extensions; using EnvelopeGenerator.Server.Extensions;
using EnvelopeGenerator.Server.Options; using EnvelopeGenerator.Server.Options;
using MediatR; using MediatR;
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using ApplicationEnvelopeReceiverDto = EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto; using ApplicationEnvelopeReceiverDto = EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto;
@@ -21,7 +23,8 @@ namespace EnvelopeGenerator.Server.Services;
public class EnvelopeReceiverPageDataService( public class EnvelopeReceiverPageDataService(
IMediator mediator, IMediator mediator,
IDistributedCache cache, IDistributedCache cache,
IOptions<CacheOptions> cacheOptions) IOptions<CacheOptions> cacheOptions,
IMemoryCache memoryCache)
{ {
private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:"; private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:";
@@ -101,4 +104,23 @@ public class EnvelopeReceiverPageDataService(
Page = element.Page, Page = element.Page,
SenderAppType = (EnvelopeGenerator.Server.Client.Models.Constants.SenderAppType)element.SenderAppType SenderAppType = (EnvelopeGenerator.Server.Client.Models.Constants.SenderAppType)element.SenderAppType
}; };
private static readonly string ReceiverEmailSearchCacheKey = Guid.NewGuid().ToString();
public async Task<IEnumerable<string>> SearchReceiverEMailsAsync(string emailSearchTerm, CancellationToken cancellationToken = default)
{
return await memoryCache.GetOrCreateAsync(ReceiverEmailSearchCacheKey + emailSearchTerm, async entry =>
{
var query = new ReadReceiverQuery { EmailAddressSearch = emailSearchTerm };
var receivers = await mediator.Send(query, cancellationToken);
if(receivers.Any())
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(30);
else
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);
return receivers.Select(r => r.EmailAddress);
}) ?? [];
}
} }

View File

@@ -0,0 +1,46 @@
using PdfSharp.Fonts;
namespace EnvelopeGenerator.Server.Services;
/// <summary>
/// PdfSharp 6.x IFontResolver for .NET 8.
/// PdfSharp cannot access system fonts on .NET Core/8 without an explicit resolver.
/// This implementation reads fonts directly from the Windows Fonts folder.
/// Register once at startup: GlobalFontSettings.FontResolver = PdfSharpFontResolver.Instance;
/// </summary>
public class PdfSharpFontResolver : IFontResolver
{
public static readonly PdfSharpFontResolver Instance = new();
private static readonly string FontsFolder =
Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
public FontResolverInfo? ResolveTypeface(string familyName, bool isBold, bool isItalic)
{
var key = familyName.ToLowerInvariant() switch
{
"arial" => isBold ? "arialbd" : "arial",
_ => null
};
return key is null ? null : new FontResolverInfo(key);
}
public byte[] GetFont(string faceName)
{
var fileName = faceName switch
{
"arialbd" => "arialbd.ttf",
_ => "arial.ttf",
};
var path = Path.Combine(FontsFolder, fileName);
if (!File.Exists(path))
throw new FileNotFoundException(
$"Font file not found: {path}. " +
"Ensure Arial is installed on the server.");
return File.ReadAllBytes(path);
}
}

View File

@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.9",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}

View File

@@ -308,8 +308,8 @@ article {
.home-btn-primary { .home-btn-primary {
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
border: none; border: none;
color: #fff; color: #fff !important;
font-weight: 500; font-weight: 700;
border-radius: 8px; border-radius: 8px;
transition: filter 0.15s, box-shadow 0.15s; transition: filter 0.15s, box-shadow 0.15s;
box-shadow: 0 2px 10px rgba(44,62,80,0.30); box-shadow: 0 2px 10px rgba(44,62,80,0.30);

View File

@@ -51,6 +51,41 @@
overflow: auto; overflow: auto;
} }
.pdf-editor-wrapper {
width: 100%;
height: 100%;
}
.sender-editor-pdf-viewer {
width: 100%;
height: 100%;
}
.sender-editor-pdf-viewer .dxbl-toolbar {
justify-content: center;
}
.sender-editor-pdf-viewer .dxbl-toolbar-left {
margin-inline: auto;
}
.sender-editor-pdf-viewer .dxbrv-document-surface {
display: flex;
flex-direction: column;
align-items: center;
}
.sender-editor-pdf-viewer .dxbrv-report-preview-content-flex-item {
width: 100%;
display: flex;
justify-content: center;
}
.sender-editor-pdf-viewer .dxbrv-report-preview-content {
margin-left: auto;
margin-right: auto;
}
.pdf-viewer-container { .pdf-viewer-container {
height: 100%; height: 100%;
display: flex; display: flex;
@@ -546,6 +581,285 @@ body.resizing {
white-space: nowrap; white-space: nowrap;
} }
.sender-toolbar-action-btn {
min-width: auto;
padding: 0.5rem 0.75rem;
}
.sender-toolbar-action-btn--compact {
padding: 0.45rem 0.7rem;
}
.sender-receivers-panel {
display: flex;
flex-direction: column;
gap: 0.625rem;
padding: 0.75rem 0.9rem;
border-radius: 12px;
background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%);
border: 1px solid rgba(126, 34, 206, 0.12);
}
.sender-receivers-panel__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
flex-wrap: wrap;
}
.sender-receivers-panel__title-wrap {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.sender-receivers-panel__title {
font-size: 0.8rem;
font-weight: 700;
color: #4c1d95;
letter-spacing: 0.02em;
}
.sender-receivers-panel__count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.5rem;
min-height: 1.5rem;
padding: 0 0.45rem;
border-radius: 999px;
background: rgba(79, 70, 229, 0.12);
color: #5b21b6;
font-size: 0.72rem;
font-weight: 700;
}
.sender-receivers-panel__add-btn .dxbl-btn {
border-radius: 8px;
font-weight: 600;
}
.pdf-toolbar-like-btn .dxbl-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-height: 34px;
padding: 0.45rem 0.85rem;
border-radius: 8px;
border: 1px solid rgba(126, 34, 206, 0.2);
background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%);
color: #1e293b;
font-size: 0.75rem;
font-weight: 600;
box-shadow: none;
transition: all 0.2s ease;
}
.pdf-toolbar-like-btn .dxbl-btn:hover:not(:disabled) {
background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%);
border-color: rgba(126, 34, 206, 0.4);
color: #1e293b;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(126, 34, 206, 0.2);
}
.pdf-toolbar-like-btn .dxbl-btn:active:not(:disabled) {
transform: translateY(0);
box-shadow: 0 2px 6px rgba(126, 34, 206, 0.15);
}
.pdf-toolbar-like-btn--add .dxbl-btn::before,
.pdf-toolbar-like-btn--signature .dxbl-btn::before {
display: inline-block;
font-size: 0.9rem;
line-height: 1;
font-weight: 700;
}
.pdf-toolbar-like-btn--add .dxbl-btn::before {
content: '+';
color: #7e22ce;
}
.pdf-toolbar-like-btn--signature .dxbl-btn {
color: #7e22ce;
}
.pdf-toolbar-like-btn--signature .dxbl-btn::before {
content: '?';
color: #7e22ce;
}
.pdf-toolbar-like-btn--signature .dxbl-btn:hover:not(:disabled) {
color: #7e22ce;
}
.sender-receivers-panel__empty {
font-size: 0.78rem;
color: #64748b;
}
.sender-receivers-list {
display: flex;
flex-wrap: wrap;
gap: 0.625rem;
}
.sender-receiver-chip {
display: inline-flex;
align-items: center;
gap: 0.75rem;
min-width: 220px;
max-width: 100%;
padding: 0.625rem 0.75rem;
border-radius: 12px;
background: rgba(255, 255, 255, 0.88);
border: 1px solid rgba(126, 34, 206, 0.12);
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.06);
}
.sender-receiver-chip__body {
min-width: 0;
flex: 1;
}
.sender-receiver-chip__name {
font-size: 0.78rem;
font-weight: 700;
color: #1f2937;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sender-receiver-chip__email {
margin-top: 0.15rem;
font-size: 0.72rem;
color: #64748b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sender-receiver-chip__phone {
margin-top: 0.15rem;
font-size: 0.72rem;
color: #64748b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sender-receiver-popup .dxbl-modal {
border-radius: 18px;
}
.sender-receiver-popup .dxbl-popup {
max-width: min(720px, calc(100vw - 2rem));
}
.sender-receiver-popup .dxbl-popup-content {
padding: 1rem 1.25rem 1.1rem;
}
.sender-receiver-popup .dxbl-popup-header {
padding: 0.95rem 1.25rem;
}
.sender-receiver-popup .dxbl-popup-footer {
padding: 0.75rem 1.25rem 1rem;
}
.sender-receiver-popup__body {
display: flex;
flex-direction: column;
gap: 0.9rem;
}
.sender-receiver-popup__form-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 1rem 1.25rem;
align-items: start;
min-height: 250px;
}
.sender-receiver-popup__field {
min-width: 0;
}
.sender-receiver-popup__label {
margin-bottom: 0.45rem;
font-size: 0.82rem;
font-weight: 600;
color: #475569;
}
.sender-receiver-popup__field .dxbl-text-edit,
.sender-receiver-popup__field .dxbl-dropdown-edit {
width: 100%;
}
.sender-receiver-popup__field .dxbl-input-editor,
.sender-receiver-popup__field .dxbl-text-edit-input {
min-height: 38px;
}
.sender-receiver-popup__suggestions-shell {
min-height: 188px;
margin-top: 0.5rem;
}
.sender-receiver-popup__suggestions {
border: 1px solid rgba(126, 34, 206, 0.12);
border-radius: 10px;
background: rgba(255, 255, 255, 0.98);
overflow: hidden;
}
.sender-receiver-popup__suggestions .dxbl-listbox {
border: none;
}
.sender-receiver-popup__suggestions .dxbl-listbox-scroll-viewer {
max-height: 180px;
}
.sender-receiver-popup__hint {
font-size: 0.78rem;
color: #64748b;
}
.sender-receiver-popup__loading {
font-size: 0.78rem;
color: #4f46e5;
font-weight: 600;
}
.sender-receiver-popup__validation {
padding: 0.625rem 0.75rem;
border-radius: 10px;
background: rgba(239, 68, 68, 0.08);
color: #b91c1c;
font-size: 0.8rem;
font-weight: 600;
}
.sender-receiver-popup__footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
width: 100%;
}
.sender-receiver-popup__footer .dxbl-btn {
min-width: 148px;
border-radius: 8px;
font-weight: 600;
}
.pdf-frame { .pdf-frame {
background: white; background: white;
border-radius: 16px; border-radius: 16px;
@@ -584,32 +898,6 @@ body.resizing {
justify-content: flex-start; justify-content: flex-start;
} }
.envelope-dx-viewer-host {
position: relative;
width: 100%;
height: 100%;
min-height: 720px;
background: #fff;
border-radius: 12px;
overflow: hidden;
}
.envelope-dx-pdf-viewer {
width: 100%;
height: 100%;
min-height: 720px;
}
.envelope-dx-pdf-viewer .dxbl-pdf-viewer,
.envelope-dx-pdf-viewer .dxbl-pdfviewer,
.envelope-dx-pdf-viewer .dxbl-pdf-viewer-container,
.envelope-dx-pdf-viewer .dxbl-scroll-viewer,
.envelope-dx-pdf-viewer .dxbl-scroll-viewer-content,
.envelope-dx-pdf-viewer .dxbl-pdf-viewer-content,
.envelope-dx-pdf-viewer .dxbl-pdfviewer-content {
height: 100%;
}
.pdf-page-container { .pdf-page-container {
position: relative; position: relative;
display: inline-block; display: inline-block;
@@ -667,11 +955,6 @@ body.resizing {
z-index: 20; z-index: 20;
} }
.pdf-signature-layer--dx {
width: 100%;
height: 100%;
}
.pdf-signature-layer .signature-button { .pdf-signature-layer .signature-button {
pointer-events: auto; pointer-events: auto;
} }
@@ -779,6 +1062,30 @@ body.resizing {
flex-wrap: wrap; flex-wrap: wrap;
} }
.sender-receivers-panel {
padding: 0.625rem 0.75rem;
}
.sender-receiver-chip {
width: 100%;
min-width: 0;
flex-wrap: wrap;
}
.sender-receiver-chip__action {
width: 100%;
}
.sender-receiver-chip__action {
width: 100%;
}
.sender-receiver-popup__form-grid {
grid-template-columns: 1fr;
gap: 0.85rem;
min-height: 0;
}
.envelope-title { .envelope-title {
font-size: 1rem; font-size: 1rem;
} }

View File

@@ -0,0 +1,95 @@
window.envelopeEditor = {
/**
* Returns the click position normalised to [0,1] relative to the rendered PDF page
* element inside DxPdfViewer (or DxReportViewer as fallback).
*
* Normalising means the result is independent of zoom level: no matter how much the
* user has zoomed in/out, the same physical spot on the PDF will always yield the same
* normalised value. C# multiplies by the page's point dimensions to get PDF points.
*
* @param {string} viewerCssClass - CssClass set on DxPdfViewer (e.g. "sender-editor-pdf-viewer")
* @param {number} clientX - MouseEvent.clientX from Blazor
* @param {number} clientY - MouseEvent.clientY from Blazor
* @returns {{ normX, normY, pageIndex } | null}
* normX / normY : 0..1 fraction within the page element
* pageIndex : 0-based index of the page the click landed on (-1 if not found)
*/
getClickCoordsOnPdfPage: function (viewerCssClass, clientX, clientY) {
// Find the viewer root element
const viewer = document.querySelector('.' + viewerCssClass);
if (!viewer) {
console.warn('[envelopeEditor] viewer not found for class:', viewerCssClass);
return null;
}
// --- Candidate page elements (ordered by preference) ---
// DxPdfViewer renders individual pages as .dxbl-pdfv-page elements.
// DxReportViewer uses .dxbrv-report-preview-content-img as fallback.
const pageSelectors = [
'.dxbl-pdfv-page',
'.dxbrv-report-preview-page',
'.dxbrv-report-preview-content-img',
];
let allPages = [];
for (const sel of pageSelectors) {
const found = Array.from(viewer.querySelectorAll(sel));
if (found.length > 0) {
allPages = found;
break;
}
}
if (allPages.length === 0) {
console.warn('[envelopeEditor] no page elements found inside viewer');
return null;
}
// --- Find which page the click landed on ---
// Walk through all pages; pick the one whose bounding rect contains the click.
// If none contains it exactly, fall back to the page closest vertically.
let targetPage = null;
let targetIndex = -1;
let minDist = Infinity;
for (let i = 0; i < allPages.length; i++) {
const rect = allPages[i].getBoundingClientRect();
// Exact hit
if (clientX >= rect.left && clientX <= rect.right &&
clientY >= rect.top && clientY <= rect.bottom) {
targetPage = allPages[i];
targetIndex = i;
break;
}
// Track closest page (vertical centre distance) as fallback
const cy = rect.top + rect.height / 2;
const dist = Math.abs(clientY - cy);
if (dist < minDist) {
minDist = dist;
targetPage = allPages[i];
targetIndex = i;
}
}
if (!targetPage) return null;
const pageRect = targetPage.getBoundingClientRect();
// Clamp click inside page boundaries before normalising
const clampedX = Math.max(pageRect.left, Math.min(clientX, pageRect.right));
const clampedY = Math.max(pageRect.top, Math.min(clientY, pageRect.bottom));
const normX = (clampedX - pageRect.left) / pageRect.width;
const normY = (clampedY - pageRect.top) / pageRect.height;
return {
normX: normX,
normY: normY,
pageIndex: targetIndex
};
}
};

View File

@@ -0,0 +1,96 @@
@echo off
setlocal
echo ============================================================
echo EnvelopeGenerator.Server - Self-Contained Publish
echo Target: win-x64 / .NET 8 / Release
echo ============================================================
echo.
REM Must be run from the solution root directory.
REM This file is located under: EnvelopeGenerator.Server\
set PROJECT=EnvelopeGenerator.Server\EnvelopeGenerator.Server.csproj
set OUTPUT=publish-output
set RID=win-x64
set FRAMEWORK=net8.0
echo [1/3] Cleaning previous publish output...
if exist "%OUTPUT%" (
rmdir /s /q "%OUTPUT%"
echo Removed: %OUTPUT%
) else (
echo Nothing to clean.
)
echo.
echo [2/3] Publishing...
echo Project : %PROJECT%
echo Output : %OUTPUT%
echo RID : %RID%
echo Framework: %FRAMEWORK%
echo.
dotnet publish "%PROJECT%" ^
-c Release ^
-f %FRAMEWORK% ^
--self-contained true ^
--runtime %RID% ^
-o "%OUTPUT%"
if %ERRORLEVEL% neq 0 (
echo.
echo [ERROR] Publish failed! ERRORLEVEL=%ERRORLEVEL%
pause
exit /b %ERRORLEVEL%
)
echo.
echo [3/3] Verifying output...
set PASS=1
if not exist "%OUTPUT%\EnvelopeGenerator.Server.exe" (
echo [FAIL] EnvelopeGenerator.Server.exe not found!
set PASS=0
)
if not exist "%OUTPUT%\hostfxr.dll" (
echo [FAIL] hostfxr.dll not found! (Not a self-contained publish?)
set PASS=0
)
if not exist "%OUTPUT%\coreclr.dll" (
echo [FAIL] coreclr.dll not found! (Not a self-contained publish?)
set PASS=0
)
if not exist "%OUTPUT%\Microsoft.Extensions.DependencyInjection.Abstractions.dll" (
echo [FAIL] Microsoft.Extensions.DependencyInjection.Abstractions.dll not found!
set PASS=0
)
if not exist "%OUTPUT%\web.config" (
echo [FAIL] web.config not found!
set PASS=0
)
if "%PASS%"=="1" (
echo.
echo ============================================================
echo PUBLISH SUCCEEDED
echo Output folder: %~dp0%OUTPUT%
echo ============================================================
echo.
echo Next steps:
echo 1. Copy the contents of '%OUTPUT%' to the IIS application directory
echo 2. Set the IIS Application Pool to 'No Managed Code'
echo 3. Recycle the Application Pool
echo.
) else (
echo.
echo ============================================================
echo PUBLISH COMPLETED BUT VERIFICATION FAILED
echo Review the FAIL messages above.
echo ============================================================
echo.
)
pause
endlocal

View File

@@ -45,6 +45,9 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUI", "EnvelopeGenerator.ReceiverUI\EnvelopeGenerator.ReceiverUI.csproj", "{FB2D306B-1042-4A70-31ED-F991A1599371}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.ReceiverUI", "EnvelopeGenerator.ReceiverUI\EnvelopeGenerator.ReceiverUI.csproj", "{FB2D306B-1042-4A70-31ED-F991A1599371}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server", "{BF1700D5-592E-4FFA-84E8-5480E289A1F0}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server", "{BF1700D5-592E-4FFA-84E8-5480E289A1F0}"
ProjectSection(SolutionItems) = preProject
EnvelopeGenerator.Server\publish.bat = EnvelopeGenerator.Server\publish.bat
EndProjectSection
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server\EnvelopeGenerator.Server\EnvelopeGenerator.Server.csproj", "{4E6C54DA-576D-0955-2564-9EC890BB8279}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnvelopeGenerator.Server", "EnvelopeGenerator.Server\EnvelopeGenerator.Server\EnvelopeGenerator.Server.csproj", "{4E6C54DA-576D-0955-2564-9EC890BB8279}"
EndProject EndProject

View File

@@ -1,454 +0,0 @@
# DevExpress DxPdfViewer Migration Plan
## EnvelopeReceiverPage.razor - PDF.js to DevExpress v25.2.3
**Created:** June 30, 2026
**Target File:** `EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
**Current Implementation:** PDF.js 3.11.174 with custom JavaScript overlays
**Target Implementation:** DevExpress DxPdfViewer v25.2.3 with hybrid approach
---
## Confirmed DevExpress v25.2.3 API
### Available Properties
- `DocumentContent` (byte[]) - Two-way bindable
- `ZoomLevel` (double) - Two-way bindable, **factor not percentage** (1.5 = 150%)
- `ActivePageIndex` (int) - **Read-only**, 0-based
- `PageCount` (int) - **Read-only**
- `IsSinglePagePreview` (bool)
- `CssClass` (string)
- `DocumentName` (string)
- `SizeMode` (SizeMode?)
### Available Events
- `CustomizeToolbar` - Toolbar customization
- `ZoomLevelChanged` - EventCallback<double> when zoom changes
### Available Methods
- `PrintAsync()` - Browser print dialog
- `DownloadAsync()` - Download PDF
### NOT Available (Critical Gaps)
- ❌ No `GoToPageAsync()` or any programmatic page navigation
- ❌ No `ActivePageIndexChanged` or `PageNumberChanged` event
-`ActivePageIndex` is read-only (cannot set programmatically)
- ❌ No way to detect user scrolling between pages
---
## Migration Strategy: Hybrid Approach
Given the API limitations, we'll use a **hybrid approach**:
1. **DevExpress DxPdfViewer** for PDF rendering
2. **Custom toolbar** via `CustomizeToolbar` event
3. **Manual state tracking** for current page/zoom
4. **ZoomLevelChanged event** for zoom synchronization
5. **JavaScript overlays** for signature buttons (same as PDF.js implementation)
6. **Graceful degradation** for features that can't be implemented
### What Works
✅ PDF rendering with DevExpress
✅ Custom zoom controls via toolbar
✅ Zoom level synchronization via `ZoomLevelChanged` event
✅ Signature button overlays (JavaScript, same as current)
✅ Signature capture workflow (unchanged)
✅ Page count display
### What Has Limitations
⚠️ **Page navigation** - Custom toolbar buttons only (no thumbnail click navigation to viewer)
⚠️ **Page tracking** - Manual state only (no event when user scrolls in native viewer)
⚠️ **Thumbnail navigation** - Updates state but cannot move viewer to that page
⚠️ **Signature button clicks** - Cannot navigate viewer to signature page programmatically
---
## Implementation Steps
### Step 1: Update Component Structure
**Current:**
```razor
<canvas id="pdfCanvas" style="@CanvasStyle"></canvas>
```
**New:**
```razor
<DxPdfViewer @ref="_pdfViewer"
DocumentContent="@_pdfDocumentContent"
@bind-ZoomLevel="_viewerZoomLevel"
ZoomLevelChanged="OnZoomLevelChanged"
CustomizeToolbar="OnCustomizeToolbar"
IsSinglePagePreview="true"
CssClass="receiver-pdf-viewer" />
```
### Step 2: Add Component Fields
```csharp
private DxPdfViewer? _pdfViewer;
private byte[]? _pdfDocumentContent;
private double _viewerZoomLevel = 1.0; // DevExpress expects factor (1.0 = 100%)
private int _currentPage = 1; // Manual tracking (1-based)
private int _currentZoom = 100; // Manual tracking (percentage)
private int _totalPages = 0;
```
### Step 3: Implement CustomizeToolbar
```csharp
protected void OnCustomizeToolbar(ToolbarModel toolbarModel)
{
toolbarModel.AllItems.Clear();
// Previous Page Button
toolbarModel.AllItems.Add(new ToolbarItem
{
Text = "Previous",
IconCssClass = "dx-icon-chevronprev",
Enabled = _currentPage > 1,
Click = async (args) =>
{
if (_currentPage > 1)
{
_currentPage--;
await RefreshOverlaysAsync();
}
}
});
// Page Info Display
toolbarModel.AllItems.Add(new ToolbarItem
{
Text = $"Page {_currentPage} of {_totalPages}",
BeginGroup = true
});
// Next Page Button
toolbarModel.AllItems.Add(new ToolbarItem
{
Text = "Next",
IconCssClass = "dx-icon-chevronnext",
Enabled = _currentPage < _totalPages,
Click = async (args) =>
{
if (_currentPage < _totalPages)
{
_currentPage++;
await RefreshOverlaysAsync();
}
}
});
// Zoom Out Button
toolbarModel.AllItems.Add(new ToolbarItem
{
IconCssClass = "dx-icon-minus",
BeginGroup = true,
Enabled = _currentZoom > 50,
Click = async (args) =>
{
_currentZoom = Math.Max(_currentZoom - 10, 50);
_viewerZoomLevel = _currentZoom / 100.0;
await InvokeAsync(StateHasChanged);
}
});
// Zoom Display
toolbarModel.AllItems.Add(new ToolbarItem
{
Text = $"{_currentZoom}%"
});
// Zoom In Button
toolbarModel.AllItems.Add(new ToolbarItem
{
IconCssClass = "dx-icon-plus",
Enabled = _currentZoom < 300,
Click = async (args) =>
{
_currentZoom = Math.Min(_currentZoom + 10, 300);
_viewerZoomLevel = _currentZoom / 100.0;
await InvokeAsync(StateHasChanged);
}
});
// Download Button
toolbarModel.AllItems.Add(new ToolbarItem
{
IconCssClass = "dx-icon-download",
BeginGroup = true,
Click = async (args) =>
{
if (_pdfViewer != null)
{
await _pdfViewer.DownloadAsync();
}
}
});
}
```
### Step 4: Implement ZoomLevelChanged Event
```csharp
private async Task OnZoomLevelChanged(double newZoomLevel)
{
// Synchronize manual tracking with DevExpress viewer
_currentZoom = (int)Math.Round(newZoomLevel * 100);
// Refresh signature overlays with new zoom
await RefreshOverlaysAsync();
// Force toolbar update to show new zoom percentage
await InvokeAsync(StateHasChanged);
}
```
### Step 5: Load PDF Document
```csharp
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
if (!string.IsNullOrEmpty(_envelopeKey))
{
// Existing envelope loading logic...
var envelope = await EnvelopeService.GetEnvelopeByKeyAsync(_envelopeKey);
if (envelope?.Document?.BinaryContent != null)
{
_pdfDocumentContent = envelope.Document.BinaryContent;
}
}
}
```
### Step 6: Read PageCount After Render
```csharp
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (!_pdfLoaded && _pdfDocumentContent is { Length: > 0 })
{
// Wait for DevExpress to load PDF
await Task.Delay(300);
if (_pdfViewer is not null && _pdfViewer.PageCount > 0)
{
_totalPages = _pdfViewer.PageCount;
_pdfLoaded = true;
// Initial overlay render
await RefreshOverlaysAsync();
await InvokeAsync(StateHasChanged);
}
}
}
```
### Step 7: Signature Overlay Rendering (Keep JavaScript)
```csharp
private async Task RefreshOverlaysAsync()
{
if (!_pdfLoaded || _signatures == null) return;
// Filter signatures for current page
var currentPageSignatures = _signatures
.Where(s => s.PageNumber == _currentPage)
.ToList();
// Call JavaScript to render overlays (same as PDF.js implementation)
await JSRuntime.InvokeVoidAsync(
"pdfViewer.renderSignatureButtons",
currentPageSignatures,
_currentPage,
DotNetObjectReference.Create(this)
);
}
```
### Step 8: Handle Thumbnail Clicks (Best Effort)
```csharp
[JSInvokable]
public async Task OnThumbnailClick(int pageNumber)
{
if (pageNumber < 1 || pageNumber > _totalPages) return;
// Update manual state
_currentPage = pageNumber;
// Refresh overlays
await RefreshOverlaysAsync();
await InvokeAsync(StateHasChanged);
// NOTE: DevExpress viewer will NOT navigate to this page
// User must use custom toolbar buttons to navigate
// This is a known limitation of v25.2.3
}
```
### Step 9: Handle Signature Button Clicks (Best Effort)
```csharp
[JSInvokable]
public async Task OnSignatureButtonClick(string signatureId)
{
var signature = _signatures?.FirstOrDefault(s => s.Id == signatureId);
if (signature == null) return;
// Update to signature's page
_currentPage = signature.PageNumber;
// Open signature modal
_showSignatureModal = true;
_selectedSignatureId = signatureId;
await RefreshOverlaysAsync();
await InvokeAsync(StateHasChanged);
// NOTE: DevExpress viewer will NOT navigate to signature page
// User sees modal but viewer stays on current page
// This is a known limitation of v25.2.3
}
```
### Step 10: Update CSS for DevExpress
```css
.receiver-pdf-viewer {
width: 100%;
height: 600px;
border: 1px solid #dee2e6;
border-radius: 4px;
}
/* Signature overlay buttons (same as PDF.js) */
.signature-button {
position: absolute;
border: 2px solid #0d6efd;
background-color: rgba(13, 110, 253, 0.1);
cursor: pointer;
transition: all 0.2s;
}
.signature-button:hover {
background-color: rgba(13, 110, 253, 0.3);
border-color: #0a58ca;
}
.signature-button.signed {
border-color: #198754;
background-color: rgba(25, 135, 84, 0.1);
}
```
---
## Testing Checklist
### Phase 1: Basic PDF Rendering
- [ ] PDF loads in DevExpress viewer
- [ ] PageCount is correctly read
- [ ] Initial zoom is 100%
- [ ] Custom toolbar appears with all buttons
### Phase 2: Navigation
- [ ] Previous button navigates (updates state)
- [ ] Next button navigates (updates state)
- [ ] Page info displays correctly
- [ ] Buttons disable at first/last page
### Phase 3: Zoom
- [ ] Zoom in button increases zoom
- [ ] Zoom out button decreases zoom
- [ ] Zoom display shows correct percentage
- [ ] ZoomLevelChanged event fires
- [ ] Buttons disable at 50%/300%
### Phase 4: Signature Overlays
- [ ] Signature buttons render on correct positions
- [ ] Overlays update when page changes (custom toolbar)
- [ ] Overlays update when zoom changes
- [ ] Click opens signature modal
- [ ] Signed signatures show green border
### Phase 5: Signature Workflow
- [ ] Draw signature works
- [ ] Type signature works
- [ ] Upload image works
- [ ] Signature applies to PDF
- [ ] Overlay updates to "signed" state
### Phase 6: Edge Cases
- [ ] Multi-page PDF (10+ pages)
- [ ] PDF with no signatures
- [ ] PDF with multiple signatures on same page
- [ ] Browser refresh preserves state
- [ ] Mobile responsive layout
### Known Limitations to Document
- [ ] User scrolling in viewer doesn't update custom toolbar page number
- [ ] Thumbnail clicks don't navigate viewer (state updates only)
- [ ] Signature button clicks don't navigate viewer to that page
- [ ] Native DevExpress toolbar is hidden (custom toolbar only)
---
## Rollback Plan
If migration fails or critical issues are discovered:
1. Keep PDF.js files in `wwwroot/js/`
2. Create branch `feature/devexpress-migration` before starting
3. Master branch keeps PDF.js implementation
4. Can revert by checking out master
---
## Success Criteria
Migration is successful if:
1. ✅ PDF renders correctly in DevExpress viewer
2. ✅ Custom toolbar navigation works
3. ✅ Zoom controls work and synchronize
4. ✅ Signature overlays render correctly
5. ✅ Signature capture and application works
6. ✅ Performance is acceptable (no lag on 20+ page PDFs)
7. ✅ Mobile/tablet layout works
8. ⚠️ User is informed about navigation limitations (documentation/tooltips)
---
## Timeline Estimate
- **Step 1-2:** Component structure update - 30 minutes
- **Step 3:** CustomizeToolbar implementation - 1 hour
- **Step 4:** ZoomLevelChanged event - 30 minutes
- **Step 5-6:** PDF loading and PageCount - 30 minutes
- **Step 7:** Signature overlays - 1 hour (testing positioning)
- **Step 8-9:** Thumbnail/signature navigation - 1 hour
- **Step 10:** CSS updates - 30 minutes
- **Testing:** Full checklist - 2 hours
- **Documentation:** User-facing limitations - 30 minutes
**Total Estimated Time:** 7-8 hours
---
## Next Steps
1. ✅ Verify DevExpress API capabilities (DONE - this document)
2. ⬜ Create feature branch `feature/devexpress-migration`
3. ⬜ Backup current EnvelopeReceiverPage.razor
4. ⬜ Implement Steps 1-10
5. ⬜ Complete testing checklist
6. ⬜ Manual testing with real envelopes
7. ⬜ Document known limitations for users
8. ⬜ Merge to master or rollback based on results

File diff suppressed because it is too large Load Diff

View File

@@ -1,290 +0,0 @@
# Session Summary - DevExpress Migration Research
**Date:** June 30, 2026
**Task:** Research and plan PDF.js to DevExpress DxPdfViewer migration
---
## What We Accomplished
### 1. Documentation Review & Translation ✅
- Translated all Turkish sections in `DEBUG_NOTES.md` to English
- Translated all Turkish sections in `DEVEXPRESS_V25_LIMITATIONS.md` to English
- Translated all Turkish sections in `TESTING_CHECKLIST.md` to English
- Fixed character encoding issues (? symbols → proper characters)
### 2. API Verification ✅
- Verified DevExpress DxPdfViewer v25.2.3 API from official documentation
- **Discovered:** `ZoomLevelChanged` event IS available (contrary to initial documentation)
- **Confirmed:** No `ActivePageIndexChanged` or `PageNumberChanged` events
- **Confirmed:** `ActivePageIndex` is read-only (no programmatic page navigation)
- **Confirmed:** No `GoToPageAsync()` or similar methods
### 3. Complete API Documentation ✅
**Available in v25.2.3:**
- `DocumentContent` (byte[]) - Two-way bindable
- `ZoomLevel` (double) - Two-way bindable, factor not percentage
- `ActivePageIndex` (int) - Read-only, 0-based
- `PageCount` (int) - Read-only
- `CustomizeToolbar` event - Toolbar customization
- `ZoomLevelChanged` event - EventCallback<double>
- `PrintAsync()` method
- `DownloadAsync()` method
**NOT Available:**
- ❌ No page navigation API (GoToPageAsync, etc.)
- ❌ No ActivePageIndexChanged event
- ❌ ActivePageIndex is read-only (cannot set)
### 4. Migration Strategy Defined ✅
**Chosen Approach:** Hybrid Implementation
- DevExpress DxPdfViewer for PDF rendering
- Custom toolbar via `CustomizeToolbar` event
- Manual state tracking for current page/zoom
- `ZoomLevelChanged` event for zoom synchronization
- JavaScript overlays for signature buttons (keep existing)
- Graceful degradation for unavailable features
**What Works:**
- ✅ PDF rendering with DevExpress
- ✅ Custom zoom controls via toolbar
- ✅ Zoom level synchronization via event
- ✅ Signature overlays (JavaScript, unchanged)
- ✅ Signature capture workflow (unchanged)
**Known Limitations:**
- ⚠️ User scrolling in viewer doesn't trigger C# page tracking
- ⚠️ Thumbnail clicks can't navigate viewer (only state updates)
- ⚠️ Signature button clicks can't navigate viewer to that page
- ⚠️ Must use custom toolbar for navigation
### 5. Complete Implementation Plan Created ✅
**Created:** `MIGRATION_PLAN.md` with:
- Step-by-step implementation guide (10 steps)
- Complete code examples for each step
- Testing checklist (30+ items)
- Success criteria
- Rollback plan
- Timeline estimate: 7-8 hours
### 6. Updated Documentation ✅
**Updated:** `DEVEXPRESS_V25_LIMITATIONS.md`
- Removed `ZoomLevelChanged` from "Missing Events" section
- Added to "Available Events" section
- Corrected notes about AI-suggested APIs
---
## Key Decisions Made
### Decision 1: Hybrid Approach vs Multi-Instance
**Chosen:** Hybrid (single DxPdfViewer + custom toolbar)
**Rejected:** Multi-instance (separate viewer per page)
**Reason:** Memory/performance concerns for 30+ page PDFs
### Decision 2: CustomizeToolbar for Navigation
**Chosen:** Custom toolbar buttons for page navigation
**Alternative:** Extract single page to byte[] and swap DocumentContent
**Reason:** More maintainable, better UX, proven pattern
### Decision 3: Keep JavaScript Overlays
**Chosen:** Continue using PDF.js overlay JavaScript
**Alternative:** Pure Blazor overlays with absolute positioning
**Reason:** Already working, coordinate system already solved, no rework needed
### Decision 4: ZoomLevelChanged Event Utilization
**Chosen:** Use `ZoomLevelChanged` to synchronize overlays
**Alternative:** Manual zoom tracking only
**Reason:** Native zoom controls in DevExpress toolbar will trigger event, keeping overlays in sync
---
## Files Created/Modified
### Created:
1. `MIGRATION_PLAN.md` - Complete step-by-step migration guide
2. `SESSION_SUMMARY.md` - This file
### Modified:
1. `DEBUG_NOTES.md` - Translated Turkish → English
2. `DEVEXPRESS_V25_LIMITATIONS.md` - Translated + corrected ZoomLevelChanged availability
3. `TESTING_CHECKLIST.md` - Translated Turkish → English
---
## Critical Findings
### Finding 1: ZoomLevelChanged Event Available
**Impact:** Medium-High
**Details:** Initial documentation incorrectly stated this event was missing. It IS available in v25.2.3, which improves zoom synchronization capabilities.
**Benefit:** Can detect when user uses native DevExpress zoom controls and update signature overlays accordingly.
### Finding 2: No Page Navigation API
**Impact:** High
**Details:** Absolutely no way to programmatically navigate viewer to specific page. `ActivePageIndex` is read-only, no methods available.
**Workaround:** Custom toolbar buttons that update manual state tracking. User must use these buttons instead of scrolling or native viewer navigation.
### Finding 3: No Page Change Event
**Impact:** Medium
**Details:** When user scrolls in native viewer, no C# event fires. Cannot detect page changes.
**Workaround:** Disable native scrolling via `IsSinglePagePreview="true"`, force use of custom toolbar buttons only.
---
## Remaining Work
### Immediate Next Steps:
1. Create feature branch `feature/devexpress-migration`
2. Backup current `EnvelopeReceiverPage.razor`
3. Begin implementation following `MIGRATION_PLAN.md` steps 1-10
### Testing Required:
1. Basic PDF rendering (Phase 1)
2. Navigation with custom toolbar (Phase 2)
3. Zoom synchronization (Phase 3)
4. Signature overlays (Phase 4)
5. Full signature workflow (Phase 5)
6. Edge cases and limitations (Phase 6)
### Documentation Required:
1. User-facing documentation about navigation limitations
2. Update `RECEIVER_PDF_VIEWER_CONTEXT.md` with DevExpress details
3. Add tooltips/help text in UI explaining custom toolbar usage
---
## Risk Assessment
### Low Risk ✅
- PDF rendering - DevExpress is proven
- Signature capture - No changes to existing modal
- Zoom controls - ZoomLevelChanged event available
- Signature overlays - Keep existing JavaScript
### Medium Risk ⚠️
- Custom toolbar UX - Users must learn new navigation pattern
- Page tracking - Manual state only, can desync if user finds way to scroll
- Performance - DevExpress rendering speed vs PDF.js unknown
### High Risk ❌
- None identified - Rollback plan exists if critical issues found
---
## Success Metrics
**Migration is successful if:**
1. All signature workflows complete successfully
2. PDF rendering performance is acceptable (<2s for 20-page PDF)
3. No critical bugs in signature placement
4. Custom toolbar navigation works reliably
5. Mobile/tablet layout works
6. Users can complete signing workflow without confusion
**Migration is failed if:**
1. Signature positioning is broken
2. Performance is significantly worse than PDF.js
3. Critical workflow steps are blocked
4. Mobile layout is unusable
→ If failed, rollback to PDF.js implementation on master branch
---
## Questions Answered
### Q1: Can we use DevExpress DxPdfViewer in v25.2.3?
**A:** Yes, but with significant API limitations requiring workarounds.
### Q2: Can we programmatically navigate to specific pages?
**A:** No. Must use custom toolbar buttons with manual state tracking.
### Q3: Can we detect when user changes pages or zoom?
**A:** Zoom yes (`ZoomLevelChanged` event), page changes no.
### Q4: Do we need to rewrite signature overlay logic?
**A:** No. Keep existing PDF.js JavaScript for overlays.
### Q5: How long will migration take?
**A:** Estimated 7-8 hours implementation + testing.
---
## Recommendations
### For Immediate Action:
1. ✅ Proceed with migration using hybrid approach
2. ✅ Use `MIGRATION_PLAN.md` as implementation guide
3. ✅ Create feature branch before starting
4. ✅ Test thoroughly with multi-page PDFs and multiple signatures
### For Future Consideration:
1. Monitor DevExpress v26.x for API improvements (page navigation, events)
2. Consider user feedback on custom toolbar navigation
3. Evaluate if `IsSinglePagePreview="true"` provides best UX
4. Consider adding help tooltips for navigation buttons
### Not Recommended:
1. ❌ Multi-instance approach (memory concerns)
2. ❌ Full rewrite of signature overlay logic (unnecessary)
3. ❌ Trying to hack `ActivePageIndex` with reflection (breaks on updates)
4. ❌ JavaScript interop to control DevExpress viewer (unsupported)
---
## Technical Debt Notes
### Introduced by Migration:
1. Manual page state tracking (desync possible if native scrolling enabled)
2. Custom toolbar implementation (must maintain alongside DevExpress updates)
3. Mixed Blazor/JavaScript architecture (overlays in JS, viewer in Blazor)
### Mitigated by Migration:
1. PDF.js version management (DevExpress handles PDF rendering)
2. PDF.js security updates (DevExpress responsibility)
3. Cross-browser PDF rendering quirks (DevExpress tested)
### Neutral:
1. Signature capture logic (unchanged)
2. Coordinate system complexity (unchanged, still INCHES in DB)
3. Redis/SQL signature caching (unchanged)
---
## Conclusion
**Research Phase: COMPLETE ✅**
We have thoroughly researched the DevExpress DxPdfViewer v25.2.3 API, identified all limitations, designed a viable hybrid migration strategy, and created a complete implementation plan.
**Key Insight:** The migration is feasible with acceptable tradeoffs. The main limitation (no programmatic page navigation) can be worked around with custom toolbar buttons. The availability of `ZoomLevelChanged` event is a positive discovery that improves the solution.
**Recommendation:** Proceed with migration following `MIGRATION_PLAN.md`.
**Next Session:** Begin implementation starting with Steps 1-2 (component structure).
---
## Appendix: Session Statistics
- Documentation pages reviewed: 5+ DevExpress documentation pages
- Files translated: 3 (DEBUG_NOTES.md, DEVEXPRESS_V25_LIMITATIONS.md, TESTING_CHECKLIST.md)
- Files created: 2 (MIGRATION_PLAN.md, SESSION_SUMMARY.md)
- Files updated: 3
- API endpoints verified: 15+
- Code examples written: 10+
- Test cases defined: 30+
- Estimated implementation time: 7-8 hours
- Estimated testing time: 2 hours
**Total Documentation:** ~500 lines of comprehensive migration guidance
---
**End of Session Summary**

View File

@@ -1,161 +0,0 @@
# DevExpress v25.2.3 - Testing Checklist
> **Important:** This checklist has been updated according to the verified real API for v25.2.3.
> `GoToPageAsync()`, `PageNumberChanged`, `ZoomLevelChanged`, `ToolbarVisible` do NOT exist.
## Build Status
- **Build:** Successful
- **DevExpress Version:** 25.2.3
- **Strategy:** `CustomizeToolbar` + manual state tracking
---
## Test Scenarios
### 1. PDF Loading
- [ ] PDF document loads successfully
- [ ] DevExpress PdfViewer displays the document
- [ ] `_pdfViewer.PageCount > 0` check passes
- [ ] `_totalPages = _pdfViewer.PageCount` gets correct value (no JS call needed)
- [ ] `_pdfLoaded = true` is set
- [ ] Toolbar is visible and shows correct page count (e.g., "Page 1 / 5")
- [ ] Zoom level displays correctly (e.g., "150%") if `_viewerZoomLevel = 1.5`
### 2. CustomizeToolbar Navigation
- [ ] **Previous Page button** (◀) works and updates page counter
- [ ] **Next Page button** (▶) works and updates page counter
- [ ] Previous button is **disabled on page 1**
- [ ] Next button is **disabled on last page**
- [ ] Page counter updates correctly (e.g., "Page 2 / 5")
- [ ] Each navigation button's Click handler calls `RenderSignatureButtonsAsync()`
### 3. CustomizeToolbar Zoom
- [ ] **Zoom In button** (+) increases zoom
- [ ] **Zoom Out button** () decreases zoom
- [ ] `_viewerZoomLevel = _currentZoom / 100d` is calculated (150 → 1.5)
- [ ] Viewer does NOT display **"15000%"** (incorrect value detection)
- [ ] Zoom is constrained to 50% - 300% range
- [ ] PDF viewer zoom level changes visually
### 4. Signature Overlay Rendering
- [ ] **Signature placeholders** appear on correct pages
- [ ] Overlays are positioned correctly over the PDF
- [ ] Overlays **re-render after page changes**
- [ ] Overlays **re-render after zoom changes**
- [ ] Overlay sizes scale with zoom level
### 5. Signature Navigation
- [ ] Custom signature toolbar is visible (if signatures exist)
- [ ] Previous/Next signature buttons work
- [ ] Signature counter shows correct values (e.g., "0 / 3")
- [ ] "X open" badge shows unsigned count
- [ ] **Cross-page navigation:** `_currentPage` updates and overlays refresh
- [ ]**Known limitation:** DxPdfViewer visible page cannot be changed programmatically
### 6. Thumbnail Sidebar
- [ ] Thumbnails render (may take a few seconds)
- [ ] Thumbnail click **updates `_currentPage` state**
- [ ] Thumbnail click **refreshes overlays**
- [ ]**Known limitation:** Thumbnail click does not navigate DevExpress viewer
- [ ] Active thumbnail is highlighted correctly
### 7. Signature Capture & Application
- [ ] Signature popup opens on first load (if no cache)
- [ ] Draw signature works
- [ ] Text signature works
- [ ] Image upload signature works
- [ ] Clicking signature placeholder applies signature
- [ ] Applied signature overlays are positioned correctly
- [ ] Counter updates after signature applied (e.g., "1 / 3")
### 8. Known Limitations (v25.2.3 API Limit)
- [ ]**User cannot scroll PDF to change pages** (only CustomizeToolbar buttons)
- [ ] ⚠ Thumbnail clicks do not navigate viewer (only update state)
- [ ] ⚠ Browser zoom gestures do not trigger overlay updates
- [ ] ✓ Custom toolbar buttons correctly trigger overlay updates
- [ ]`_pdfViewer.PageCount` eliminates need for JS `getTotalPages()` call
- [ ]`ZoomLevel = _currentZoom / 100d` calculates correct zoom factor
---
## Common Issues
### Issue: Toolbar not visible
**Reason:** `_pdfLoaded = false` or `_totalPages = 0`
**Solution:** In `OnAfterRenderAsync`, check `_pdfViewer.PageCount > 0`, set `_pdfLoaded = true`
```csharp
if (_pdfViewer is not null && _pdfViewer.PageCount > 0)
{
_totalPages = _pdfViewer.PageCount; // no JS call needed
_pdfLoaded = true;
await InvokeAsync(StateHasChanged);
}
```
### Issue: Signature overlays not visible
**Reason:** `RenderSignatureButtonsAsync()` not called after page/zoom change
**Solution:** Verify each button Click handler in `OnCustomizeToolbar` calls `RenderSignatureButtonsAsync()`
### Issue: Page navigation not working
**Reason:** `OnCustomizeToolbar` button Click lambdas not updating `_currentPage`
**Solution:** Check that each button updates `_currentPage` and calls `StateHasChanged()` and `RenderSignatureButtonsAsync()`
### Issue: Zoom not working or showing "15000%"
**Reason:** Incorrect value assigned to `_viewerZoomLevel`
**Solution:** `_viewerZoomLevel = _currentZoom / 100d` ZoomLevel takes **factor**, not percentage
```csharp
// CORRECT
_currentZoom = 150; // UI display: "150%"
_viewerZoomLevel = 150 / 100d; // Pass to DxPdfViewer: 1.5
// WRONG
_viewerZoomLevel = 150; // DxPdfViewer displays "15000%"
```
### Issue: Total page count is 0
**Reason:** JS call made before timing or fails
**Solution:** Use `_pdfViewer.PageCount` directly, no need for `pdfViewer.getTotalPages()` JS call
---
## Success Criteria
**Minimum working functionality:**
- PDF loads and displays
- `_totalPages = _pdfViewer.PageCount` gets correct page count
- Custom toolbar navigation works (previous/next/zoom)
- Signature overlays render on current page
- Signature capture and application works
- Overlays update after navigation/zoom
**Known acceptable limitations (v25.2.3 API limit):**
- Thumbnail clicks do not navigate viewer (only update state)
- User scroll/native toolbar navigation does not update C# state
- Cross-page signature navigation is limited
---
## Next Steps If All Tests Pass
1. Remove debug toolbar button (if present)
2. Clean up unused code comments
3. Update user documentation about navigation limitations
---
## Architecture Reference: Verified v25.2.3 API
| Property | Access | Usage |
|----------|--------|-------|
| `DocumentContent` | `[Parameter]` GET/SET | Feed PDF with `byte[]` |
| `ZoomLevel` | `[Parameter]` GET/SET | **Factor**: `_currentZoom / 100d` |
| `IsSinglePagePreview` | `[Parameter]` GET/SET | `true` = single page mode |
| `PageCount` | GET only | Total pages no JS needed |
| `ActivePageIndex` | GET only | Active page index (0-based) no SET |
| `CssClass` | `[Parameter]` GET/SET | Assign CSS class |
| `SizeMode` | `[Parameter]` GET/SET | `Small` / `Medium` / `Large` |
| `DocumentName` | `[Parameter]` GET/SET | Download filename |
**Not available:** `GoToPageAsync()`, `PageNumberChanged`, `ZoomLevelChanged`, `ToolbarVisible`