Compare commits
18 Commits
feat/migr-
...
feat/migr-
| Author | SHA1 | Date | |
|---|---|---|---|
| 762a9e8bca | |||
| 6ed4caea4f | |||
| d94821433a | |||
| 278b9964f1 | |||
| e6722803bb | |||
| 47bc7675c9 | |||
| 789e312316 | |||
| 2a9bbb3fe5 | |||
| bc34317720 | |||
| 76ff3e47e1 | |||
| 2d22bfcd06 | |||
| 185c783824 | |||
| b957b4b4bb | |||
| df154d83cc | |||
| 49ec9fbead | |||
| 01fc29f59e | |||
| 733b70cca2 | |||
| 8f4b751303 |
@@ -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`
|
||||||
|
|||||||
300
DEBUG_NOTES.md
300
DEBUG_NOTES.md
@@ -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!**
|
|
||||||
@@ -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.
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
|
@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" />
|
||||||
|
|
||||||
|
|||||||
@@ -161,7 +161,7 @@
|
|||||||
|
|
||||||
if (result == EnvelopeLoginResult.Success)
|
if (result == EnvelopeLoginResult.Success)
|
||||||
{
|
{
|
||||||
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}/report", forceLoad: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -803,21 +823,19 @@
|
|||||||
async Task OnPageInputChanged(ChangeEventArgs e)
|
async Task OnPageInputChanged(ChangeEventArgs e)
|
||||||
{
|
{
|
||||||
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)
|
||||||
|
{
|
||||||
|
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.goToPage", pageNum))
|
||||||
{
|
{
|
||||||
_currentPage = pageNum;
|
_currentPage = pageNum;
|
||||||
await ApplyViewerStateAsync();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 ApplyViewerStateAsync();
|
await RenderSignatureButtonsAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -0,0 +1,723 @@
|
|||||||
|
@page "/envelope/{EnvelopeKey}/report"
|
||||||
|
@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<EnvelopeReceiverReportPage> 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><@_envelopeReceiver.Envelope.User.Email></span>
|
||||||
|
}
|
||||||
|
@if (_envelopeReceiver.Envelope?.AddedWhen != null)
|
||||||
|
{
|
||||||
|
<span> · @_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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<EnvelopeReceiverReportSignedPage> 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)}/report",
|
||||||
|
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)..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
@page "/envelope/editor"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
@using DevExpress.Blazor.PdfViewer
|
||||||
|
@using DevExpress.Blazor.Reporting.Models
|
||||||
|
@using DevExpress.Blazor
|
||||||
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
@inject AppVersionService AppVersion
|
||||||
|
@inject ILogger<EnvelopeSenderEditorPage> Logger
|
||||||
|
|
||||||
|
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||||
|
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
||||||
|
<script src="@AppVersion.GetVersionedUrl("js/envelope-editor.js")"></script>
|
||||||
|
|
||||||
|
<div class="envelope-viewer-layout">
|
||||||
|
|
||||||
|
@* ── Action Bar ── *@
|
||||||
|
<div class="envelope-action-bar">
|
||||||
|
<div class="envelope-action-bar__inner"
|
||||||
|
style="flex-direction: row; align-items: center; padding: 0.35rem 1.5rem; gap: 0.75rem;">
|
||||||
|
|
||||||
|
@* Left: Title *@
|
||||||
|
<div style="flex: 1; min-width: 0; display: flex; align-items: center; gap: 0.75rem;">
|
||||||
|
<div style="font-size: 0.9rem; font-weight: 600; color: #1f2937; white-space: nowrap;">
|
||||||
|
Neues Dokument
|
||||||
|
</div>
|
||||||
|
@if (_pdfLoaded)
|
||||||
|
{
|
||||||
|
<span style="font-size: 0.7rem; color: #6b7280;">@_fileName</span>
|
||||||
|
@if (_signatureFields.Count > 0)
|
||||||
|
{
|
||||||
|
<span style="display: inline-flex; align-items: center; padding: 0.125rem 0.4rem;
|
||||||
|
background: #ede9fe; border-radius: 0.25rem; color: #6d28d9;
|
||||||
|
font-weight: 500; font-size: 0.7rem; white-space: nowrap;">
|
||||||
|
@_signatureFields.Count Signaturfeld@(_signatureFields.Count != 1 ? "er" : "")
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* Right: Buttons *@
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0;">
|
||||||
|
|
||||||
|
@* PDF Upload *@
|
||||||
|
<label class="pdf-toolbar__btn"
|
||||||
|
title="PDF hochladen"
|
||||||
|
style="cursor: pointer; display: inline-flex; align-items: center; gap: 0.3rem; padding: 0.3rem 0.75rem; font-size: 0.75rem; font-weight: 500;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z" />
|
||||||
|
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z" />
|
||||||
|
</svg>
|
||||||
|
PDF hochladen
|
||||||
|
<InputFile OnChange="OnPdfFileSelectedAsync"
|
||||||
|
accept=".pdf"
|
||||||
|
style="display: none;" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
@if (_pdfLoaded)
|
||||||
|
{
|
||||||
|
@* Toggle placement mode *@
|
||||||
|
<button class="pdf-toolbar__btn @(_placementMode ? "pdf-toolbar__btn--signature-change-active" : "pdf-toolbar__btn--signature-change")"
|
||||||
|
@onclick="TogglePlacementMode"
|
||||||
|
title="@(_placementMode ? "Platzierungsmodus beenden" : "Signaturfeld hinzufügen")">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10z" />
|
||||||
|
</svg>
|
||||||
|
<span class="pdf-toolbar__btn-text">
|
||||||
|
@(_placementMode ? "Abbrechen" : "+ Signaturfeld")
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@* Clear all fields *@
|
||||||
|
@if (_signatureFields.Count > 0)
|
||||||
|
{
|
||||||
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--reset"
|
||||||
|
@onclick="ClearAllFields"
|
||||||
|
title="Alle Signaturfelder entfernen">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
|
||||||
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1 0-2h3a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1h3a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3h11V2h-11v1z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
@* Save *@
|
||||||
|
<button class="pdf-toolbar__btn pdf-toolbar__btn--success"
|
||||||
|
@onclick="SaveAsync"
|
||||||
|
title="Speichern"
|
||||||
|
style="background: linear-gradient(135deg,#059669,#047857); color:#fff; border-radius:6px; padding:0.3rem 0.75rem; font-size:0.75rem; font-weight:600; border:none;">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* Placement mode hint bar *@
|
||||||
|
@if (_placementMode)
|
||||||
|
{
|
||||||
|
<div style="background: #4F46E5; color: white; font-size: 0.75rem; font-weight: 500;
|
||||||
|
padding: 0.3rem 1.5rem; text-align: center; letter-spacing: 0.01em;">
|
||||||
|
📌 Klicken Sie auf die gewünschte Stelle im Dokument, um ein Signaturfeld zu platzieren.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* ── Content ── *@
|
||||||
|
<div class="envelope-content" style="padding: 0; overflow: hidden;">
|
||||||
|
@if (!_pdfLoaded)
|
||||||
|
{
|
||||||
|
@* Empty state *@
|
||||||
|
<div class="d-flex justify-content-center align-items-center h-100">
|
||||||
|
<div class="text-center" style="max-width: 420px;">
|
||||||
|
<div style="width: 72px; height: 72px; background: rgba(255,255,255,0.15);
|
||||||
|
border-radius: 50%; display: flex; align-items: center;
|
||||||
|
justify-content: center; margin: 0 auto 1.25rem;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="white" viewBox="0 0 16 16">
|
||||||
|
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h5 class="text-white mb-2">Kein Dokument geladen</h5>
|
||||||
|
<p class="text-white mb-4" style="opacity: 0.75; font-size: 0.85rem;">
|
||||||
|
Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren.
|
||||||
|
</p>
|
||||||
|
<label class="btn btn-light btn-sm" style="cursor: pointer; 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 d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z" />
|
||||||
|
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z" />
|
||||||
|
</svg>
|
||||||
|
PDF hochladen
|
||||||
|
<InputFile OnChange="OnPdfFileSelectedAsync" accept=".pdf" style="display: none;" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (_errorMessage is not null)
|
||||||
|
{
|
||||||
|
<div class="error-container">
|
||||||
|
<div class="alert alert-danger shadow-lg m-4">
|
||||||
|
<strong>Fehler:</strong> @_errorMessage
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@* PDF viewer + overlay wrapper *@
|
||||||
|
<div id="pdf-editor-wrapper" class="pdf-editor-wrapper"
|
||||||
|
style="position: relative; width: 100%; height: 100%; overflow: auto;">
|
||||||
|
|
||||||
|
@* DxPdfViewer — zoom fixed to 1.0 for reliable coordinate mapping *@
|
||||||
|
<DxPdfViewer @ref="_pdfViewer"
|
||||||
|
DocumentContent="@_pdfBytes"
|
||||||
|
ZoomLevel="1.0"
|
||||||
|
IsSinglePagePreview="false"
|
||||||
|
CssClass="sender-editor-pdf-viewer" />
|
||||||
|
|
||||||
|
@* Transparent overlay for click capture (active only in placement mode) *@
|
||||||
|
<div id="pdf-editor-overlay"
|
||||||
|
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||||
|
z-index: 20;
|
||||||
|
pointer-events: @(_placementMode ? "auto" : "none");
|
||||||
|
cursor: @(_placementMode ? "crosshair" : "default");"
|
||||||
|
@onclick="OnOverlayClickAsync">
|
||||||
|
|
||||||
|
@* Render placed signature field placeholders *@
|
||||||
|
@foreach (var field in _signatureFields)
|
||||||
|
{
|
||||||
|
var f = field; // capture for lambda
|
||||||
|
<div style="position: absolute;
|
||||||
|
left: @(f.DisplayX.ToString("F1", System.Globalization.CultureInfo.InvariantCulture))px;
|
||||||
|
top: @(f.DisplayY.ToString("F1", System.Globalization.CultureInfo.InvariantCulture))px;
|
||||||
|
width: @(SigDisplayW)px;
|
||||||
|
height: @(SigDisplayH)px;
|
||||||
|
border: 2px dashed #4F46E5;
|
||||||
|
background: rgba(79,70,229,0.10);
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2px;
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: default;"
|
||||||
|
@onclick:stopPropagation="true">
|
||||||
|
<span style="font-size: 0.6rem; font-weight: 700; color: #4F46E5;
|
||||||
|
letter-spacing: 0.05em; text-transform: uppercase;">
|
||||||
|
Unterschrift
|
||||||
|
</span>
|
||||||
|
<span style="font-size: 0.55rem; color: #6d28d9; opacity: 0.8;">
|
||||||
|
S.@f.Page
|
||||||
|
</span>
|
||||||
|
<button @onclick="() => RemoveField(f)"
|
||||||
|
style="position: absolute; top: 2px; right: 4px;
|
||||||
|
background: none; border: none; cursor: pointer;
|
||||||
|
color: #6d28d9; font-size: 0.75rem; line-height: 1;
|
||||||
|
padding: 0; opacity: 0.7;"
|
||||||
|
title="Entfernen">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
// ── Constants ──
|
||||||
|
// Signature field size in PDF points (fixed): 1.77" × 1.96" × 72 pt/inch
|
||||||
|
const double SigWidthPt = 1.77 * 72; // 127.44 pt
|
||||||
|
const double SigHeightPt = 1.96 * 72; // 141.12 pt
|
||||||
|
|
||||||
|
// Display size of the overlay placeholder (pixels at zoom=1.0).
|
||||||
|
// At zoom=1.0, 1 CSS px ≈ 1 pt in the DxPdfViewer render.
|
||||||
|
const double SigDisplayW = SigWidthPt;
|
||||||
|
const double SigDisplayH = SigHeightPt;
|
||||||
|
|
||||||
|
// ── State ──
|
||||||
|
DxPdfViewer? _pdfViewer;
|
||||||
|
byte[]? _pdfBytes;
|
||||||
|
bool _pdfLoaded = false;
|
||||||
|
string _fileName = string.Empty;
|
||||||
|
string? _errorMessage;
|
||||||
|
bool _placementMode = false;
|
||||||
|
List<SignatureFieldDraft> _signatureFields = [];
|
||||||
|
|
||||||
|
// ── PDF upload ──
|
||||||
|
async Task OnPdfFileSelectedAsync(InputFileChangeEventArgs e)
|
||||||
|
{
|
||||||
|
_errorMessage = null;
|
||||||
|
var file = e.File;
|
||||||
|
if (file is null) return;
|
||||||
|
|
||||||
|
if (!file.Name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_errorMessage = "Bitte wählen Sie eine PDF-Datei aus.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Max 50 MB
|
||||||
|
const long maxBytes = 50 * 1024 * 1024;
|
||||||
|
using var ms = new System.IO.MemoryStream();
|
||||||
|
await file.OpenReadStream(maxBytes).CopyToAsync(ms);
|
||||||
|
_pdfBytes = ms.ToArray();
|
||||||
|
_fileName = file.Name;
|
||||||
|
_pdfLoaded = true;
|
||||||
|
_signatureFields.Clear();
|
||||||
|
_placementMode = false;
|
||||||
|
|
||||||
|
Logger.LogInformation("PDF loaded: {Name} ({Size} bytes)", _fileName, _pdfBytes.Length);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_errorMessage = $"Fehler beim Laden der Datei: {ex.Message}";
|
||||||
|
Logger.LogError(ex, "Failed to load PDF file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Placement mode ──
|
||||||
|
void TogglePlacementMode() => _placementMode = !_placementMode;
|
||||||
|
|
||||||
|
void ClearAllFields()
|
||||||
|
{
|
||||||
|
_signatureFields.Clear();
|
||||||
|
_placementMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoveField(SignatureFieldDraft field) => _signatureFields.Remove(field);
|
||||||
|
|
||||||
|
// ── Overlay click → add signature field ──
|
||||||
|
async Task OnOverlayClickAsync(MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_placementMode) return;
|
||||||
|
|
||||||
|
// Get overlay container bounds via JS
|
||||||
|
var coords = await JSRuntime.InvokeAsync<OverlayCoords>(
|
||||||
|
"envelopeEditor.getClickCoords", "pdf-editor-overlay",
|
||||||
|
e.ClientX, e.ClientY);
|
||||||
|
|
||||||
|
if (coords is null) return;
|
||||||
|
|
||||||
|
// At zoom=1.0: container pixels ≈ PDF display pixels.
|
||||||
|
// DxPdfViewer renders at 96 dpi by default; PDF points = 72 dpi.
|
||||||
|
// Scale factor: 96/72 = 1.333 → px / 1.333 = pt
|
||||||
|
const double pxToPt = 72.0 / 96.0;
|
||||||
|
|
||||||
|
double xPt = coords.RelX * pxToPt;
|
||||||
|
double yPt = coords.RelY * pxToPt;
|
||||||
|
|
||||||
|
// Active page: DxPdfViewer.ActivePageIndex is 0-based
|
||||||
|
int page = (_pdfViewer?.ActivePageIndex ?? 0) + 1;
|
||||||
|
|
||||||
|
// Display position (px on overlay) — keep in px for CSS
|
||||||
|
double displayX = coords.RelX;
|
||||||
|
double displayY = coords.RelY;
|
||||||
|
|
||||||
|
// Prevent placing outside bounds
|
||||||
|
if (displayX < 0 || displayY < 0) return;
|
||||||
|
if (displayX + SigDisplayW > coords.ContainerW) displayX = coords.ContainerW - SigDisplayW;
|
||||||
|
if (displayY + SigDisplayH > coords.ContainerH) displayY = coords.ContainerH - SigDisplayH;
|
||||||
|
|
||||||
|
var field = new SignatureFieldDraft(xPt, yPt, page, displayX, displayY);
|
||||||
|
_signatureFields.Add(field);
|
||||||
|
|
||||||
|
Logger.LogInformation(
|
||||||
|
"Signature field added: Page={Page} X={X:F1}pt Y={Y:F1}pt",
|
||||||
|
page, xPt, yPt);
|
||||||
|
|
||||||
|
// Exit placement mode after one click (user can re-click button for next)
|
||||||
|
_placementMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Save ──
|
||||||
|
async Task SaveAsync()
|
||||||
|
{
|
||||||
|
if (_signatureFields.Count == 0)
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync("console.log",
|
||||||
|
"[SenderEditor] No signature fields to save.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var f in _signatureFields)
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync("console.log",
|
||||||
|
$"[SenderEditor] Field: Page={f.Page} X={f.XPt:F2}pt ({f.XPt/72:F3}in) Y={f.YPt:F2}pt ({f.YPt/72:F3}in)");
|
||||||
|
}
|
||||||
|
|
||||||
|
await JSRuntime.InvokeVoidAsync("console.log",
|
||||||
|
$"[SenderEditor] Total fields: {_signatureFields.Count}");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!_pdfLoaded || _errorMessage is not null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await JSRuntime.InvokeVoidAsync(
|
||||||
|
"envelopeEditor.syncOverlayToPage",
|
||||||
|
"pdf-editor-wrapper",
|
||||||
|
"pdf-editor-overlay");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Models ──
|
||||||
|
record SignatureFieldDraft(double XPt, double YPt, int Page, double DisplayX, double DisplayY);
|
||||||
|
|
||||||
|
record OverlayCoords(double RelX, double RelY, double ContainerW, double ContainerH);
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
|
|||||||
@@ -28,13 +28,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" />
|
||||||
|
|||||||
@@ -104,7 +104,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 +338,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"));
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,33 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pdf-editor-wrapper {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-editor-pdf-viewer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
@@ -584,32 +611,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 +668,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
window.envelopeEditor = {
|
||||||
|
_overlaySyncState: {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns click coordinates relative to the overlay element.
|
||||||
|
* @param {string} overlayId - The id of the overlay div
|
||||||
|
* @param {number} clientX - MouseEventArgs.ClientX from Blazor
|
||||||
|
* @param {number} clientY - MouseEventArgs.ClientY from Blazor
|
||||||
|
* @returns {{ relX, relY, containerW, containerH }}
|
||||||
|
*/
|
||||||
|
getClickCoords: function (overlayId, clientX, clientY) {
|
||||||
|
const el = document.getElementById(overlayId);
|
||||||
|
if (!el) return null;
|
||||||
|
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
relX: clientX - rect.left,
|
||||||
|
relY: clientY - rect.top,
|
||||||
|
containerW: rect.width,
|
||||||
|
containerH: rect.height
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
syncOverlayToPage: function (wrapperId, overlayId) {
|
||||||
|
const wrapper = document.getElementById(wrapperId);
|
||||||
|
const overlay = document.getElementById(overlayId);
|
||||||
|
|
||||||
|
if (!wrapper || !overlay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = window.envelopeEditor._overlaySyncState[overlayId];
|
||||||
|
if (existing) {
|
||||||
|
return existing.sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
const findTarget = (currentWrapper) => {
|
||||||
|
const page = currentWrapper.querySelector(".dxbrv-report-preview-content");
|
||||||
|
if (page) {
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentWrapper.querySelector(".dxbrv-report-preview-content-img") ||
|
||||||
|
currentWrapper.querySelector("img.dxbrv-report-preview-content-img") ||
|
||||||
|
currentWrapper.querySelector(".dxbrv-document-surface img");
|
||||||
|
};
|
||||||
|
|
||||||
|
const sync = () => {
|
||||||
|
const currentWrapper = document.getElementById(wrapperId);
|
||||||
|
const currentOverlay = document.getElementById(overlayId);
|
||||||
|
|
||||||
|
if (!currentWrapper || !currentOverlay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = findTarget(currentWrapper);
|
||||||
|
if (!target) {
|
||||||
|
currentOverlay.style.left = "0px";
|
||||||
|
currentOverlay.style.top = "0px";
|
||||||
|
currentOverlay.style.width = "0px";
|
||||||
|
currentOverlay.style.height = "0px";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapperRect = currentWrapper.getBoundingClientRect();
|
||||||
|
const targetRect = target.getBoundingClientRect();
|
||||||
|
|
||||||
|
currentOverlay.style.left = `${targetRect.left - wrapperRect.left + currentWrapper.scrollLeft}px`;
|
||||||
|
currentOverlay.style.top = `${targetRect.top - wrapperRect.top + currentWrapper.scrollTop}px`;
|
||||||
|
currentOverlay.style.width = `${targetRect.width}px`;
|
||||||
|
currentOverlay.style.height = `${targetRect.height}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleSync = () => requestAnimationFrame(sync);
|
||||||
|
|
||||||
|
const observer = new MutationObserver(scheduleSync);
|
||||||
|
observer.observe(wrapper, { childList: true, subtree: true, attributes: true });
|
||||||
|
|
||||||
|
wrapper.addEventListener("scroll", scheduleSync, { passive: true });
|
||||||
|
window.addEventListener("resize", scheduleSync);
|
||||||
|
|
||||||
|
window.envelopeEditor._overlaySyncState[overlayId] = {
|
||||||
|
sync,
|
||||||
|
observer
|
||||||
|
};
|
||||||
|
|
||||||
|
sync();
|
||||||
|
setTimeout(sync, 50);
|
||||||
|
setTimeout(sync, 150);
|
||||||
|
setTimeout(sync, 400);
|
||||||
|
}
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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**
|
|
||||||
@@ -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`
|
|
||||||
Reference in New Issue
Block a user