Compare commits

...

34 Commits

Author SHA1 Message Date
88b196ed6d Refactor report handling and improve async operations
Refactored the `Report` property to `_report` with nullable support
and updated `EnvelopeKey` to use `init` for immutability.
Made `CreateReport` asynchronous, returning `Task<XtraReport>`,
and removed redundant `BasePdfBytes` property. Simplified predefined
report fetching by removing `ReportStorage.TryGetReport`. Improved
error handling for missing or invalid `pdfBytes` in `CreateReport`.
Made minor formatting and structural improvements for clarity.
2026-06-11 14:11:56 +02:00
c99511de29 Add FORM_APPLICATION_CONTEXT.md for migration documentation
Added a new file, `FORM_APPLICATION_CONTEXT.md`, to the solution file `EnvelopeGenerator.sln`. This file provides detailed documentation of the legacy VB.NET Windows Forms application, including its architecture, workflows, and migration plan to the ReceiverUI + API architecture.

The documentation outlines the application's purpose, primary libraries, project structure, and key forms with their respective features and behaviors. It also describes data models, controllers, and their methods, along with technical details such as coordinate systems, status colors, and master-detail grid patterns.

Additionally, the file includes implementation notes for key features like drag-and-drop file uploads, auto-complete for receiver emails, and PDF merging. Workflow summaries and a data flow diagram are provided to clarify the application's processes. The document concludes with key takeaways to ensure consistency during the migration.
2026-06-11 13:52:55 +02:00
7d0c5a0ee5 Improve error handling and logging for envelope receiver
Added a null check in `EnvelopeReceiverPage.razor` to log a warning when `_envelopeReceiver` is null. Updated `ReportViewer.razor` to wrap `EnvelopeReceiverService.GetAsync` in a `try-catch` block, logging `HttpRequestException` errors and allowing the UI to handle null values gracefully.

Enhanced `EnvelopeReceiverService.GetAsync` to throw detailed `HttpRequestException` on API failures, including status code and reason phrase. Added `using System.Net;` to support HTTP-related classes. Updated method documentation to reflect the new behavior.

These changes improve error diagnostics, logging, and maintainability across the codebase.
2026-06-11 13:40:30 +02:00
7001d7351f Remove default "fake" fallback for EnvelopeKey usage
The default value "fake" for the `EnvelopeKey` parameter has been
removed when calling `AnnotationService.GetAnnotationsAsync` and
`EnvelopeReceiverService.GetAsync`. The code now directly uses
the `EnvelopeKey` variable, assuming it will always have a valid
value or that null/empty handling is managed elsewhere.
2026-06-11 13:37:55 +02:00
cf16312394 Refactor error handling in DocumentService and consumers
Refactored `DocumentService.GetDocumentAsync` to throw
`HttpRequestException` on failure instead of returning a tuple.
Updated all consumers to handle exceptions, improving error
handling consistency across the application.

Enhanced error messages for better user feedback and added
logging for improved traceability. Updated `EnvelopeReceiverPage`,
`EnvelopeReceiverPage_DxReportViewer`, and `ReportViewer` to
use the new exception-based API and handle errors gracefully.

This change simplifies the API, improves maintainability, and
makes the application more robust and user-friendly.
2026-06-11 13:33:15 +02:00
ecb7f45f14 Refactor report creation and property initialization
Replaced nullable fields with non-nullable properties for `Report`
and `BasePdfBytes` to improve initialization and encapsulation.
Updated `OnInitializedAsync` to use `BasePdfBytes` and added
error handling for missing PDF bytes.

Refactored report creation logic by removing `BuildFreshBaseReport`
and `CreateReportInstance`, consolidating functionality into a
new `CreateReport` method. Enhanced `CreateReport` to support
both predefined and dynamically generated reports, simplifying
the code structure and improving maintainability.
2026-06-11 13:12:40 +02:00
0ba5578d94 Update appsettings.json configuration options
Removed the `ForceToUseFakeDocument` property from the `Api` section, as it is no longer needed. Added a new property, `UsePredefinedReports`, to the `Api` section with a default value of `false` to enable or disable the use of predefined reports.
2026-06-11 13:11:51 +02:00
e093471a24 Refactor: Replace ForceToUseFakeDocument property
Replaced the `ForceToUseFakeDocument` property in `ApiOptions`
with `UsePredefinedReports` to improve clarity and better align
with business requirements. Updated all references in
`EnvelopeReceiverPage_DxReportViewer.razor` and
`ReportViewer.razor` to use the new property, ensuring
consistency and maintaining functionality.
2026-06-11 13:07:17 +02:00
b16ae70762 Remove signature-related functionality
This commit removes all features and UI elements related to signature handling in the `EnvelopeReceiverPage_DxReportViewer.razor` file.

Key changes include:
- Removed signature creation (drawing, typing, uploading) and associated UI components like the signature popup and action bar.
- Removed annotation handling logic, including toggling checkboxes and applying signatures to annotations.
- Removed PDF export functionality for signed documents.
- Deleted methods and properties related to signature handling, such as `AddSignature`, `OnInitializedAsync`, and `SignaturePopupVisible`.
- Removed `@inject` services and JavaScript interop calls related to signature handling.
- Simplified the `DxReportViewer` usage to only display the report without overlays or interactions.
- Removed envelope metadata display and logout functionality.

The page now focuses solely on displaying reports without any signature-related features.
2026-06-11 13:02:53 +02:00
c3e8f09291 Simplify EnvelopeReceiverPage logic and update settings
Removed redundant `EnvelopeKey` checks in `LogoutAsync` and
`OnInitializedAsync` methods, simplifying logout and initialization
logic. Updated navigation URL to `/envelope/login/`. Streamlined
document fetching logic by removing unnecessary conditions.

Added logging in `CreateReportInstance` to track report creation.
Enabled `ForceToUseFakeDocument` in `appsettings.json` to default
to using fake documents. These changes improve code clarity and
align behavior with updated requirements.
2026-06-11 12:51:04 +02:00
a9fb82bbea Refactor _dotNetRef type and remove unused redirection
Updated the `_dotNetRef` field to use a more specific type, `DotNetObjectReference<EnvelopeReceiverPage_DxReportViewer>`, for better alignment with the component. Removed obsolete redirection logic from `/receiver/{key}` to `/envelope/{key}` in `OnInitializedAsync`, as it is no longer needed or handled elsewhere. Retained envelope access validation logic.
2026-06-11 11:59:21 +02:00
895fd5c509 Add route for DxReportViewer in yarp.json
Introduce a new route configuration for `receiver-ui-envelope-dxreportviewer` in `yarp.json`.
The route matches requests to `/envelope/{EnvelopeKey}/DxReportViewer` for `GET` and `HEAD` methods.
It is part of the `receiver-ui` cluster, with an order of `90`, and includes a transformation to redirect the path to `/index.html`.
This change supports a new endpoint in the application.
2026-06-11 11:58:58 +02:00
3b4278d7e0 Add envelope viewer with signature functionality
Introduced a new Razor page `EnvelopeReceiverPage_DxReportViewer.razor` to manage and sign envelope-related documents. Integrated DevExpress Blazor components for PDF rendering and signature handling.

Key features:
- Added sections for envelope info, signature actions, and a PDF viewer.
- Enabled signature creation via drawing, text input, or image upload.
- Validated and applied signatures to annotated fields in the document.
- Integrated JavaScript interop for signature capture and rendering.
- Supported exporting signed PDFs and dynamic signature overlays.
- Ensured proper resource cleanup with `IDisposable` implementation.
2026-06-11 11:52:11 +02:00
6d1fb05e10 Update solution items in EnvelopeGenerator.sln
Replaced `COPILOT_CONTEXT_EN.md` with `COPILOT_CONTEXT.md`
in the `ProjectSection(SolutionItems)` of the solution file
`EnvelopeGenerator.sln` to reflect updated documentation.
2026-06-11 11:38:59 +02:00
26da78fa22 Refactor ReceiverUI: Rename files, update routes
Renamed Razor component files in ReceiverUI to follow a consistent "Page" naming convention. Updated routes to reference the renamed files. Introduced the root route (`/`) with `Index.razor` as the application entry point.

Updated documentation to reflect the file renames, route changes, and multi-envelope support. Clarified Redis/SQL caching details and deprecated the "Web" frontend in favor of `ReceiverUI`.

These changes improve maintainability, consistency, and developer experience.
2026-06-11 11:38:32 +02:00
9eee2b523d Refactor YARP routes and update configurations
Renamed the `receiver-ui-receiver` route to `receiver-ui-root` and updated its `Order` value from `100` to `300`. Changed the `Match` path for `receiver-ui-root` from `/receiver/{**catch-all}` to `/`. Removed the `receiver-ui-login` route entirely.

Added a `Transforms` section to the `receiver-ui-root` and `receiver-ui-sender` routes, setting the path to `/index.html`.
2026-06-11 11:38:19 +02:00
9dc2b9adef rename Pages\*.razor to Pages\*Page.razor to resolve conflicts 2026-06-11 11:07:32 +02:00
cc3c5ec9f0 Unify architecture with Blazor WASM and YARP proxy
Refactored the deployment architecture to include two distinct
presentation projects: `EnvelopeGenerator.API` (ASP.NET Core Web
API with YARP Reverse Proxy) and `EnvelopeGenerator.ReceiverUI`
(Blazor WebAssembly). The API now acts as the single entry point,
proxying requests to the ReceiverUI and external authentication
services.

Redefined the route structure for clarity, introducing sender
routes (`/sender/login`, `/sender`) and receiver routes
(`/envelope/login/{EnvelopeKey}`, `/envelope/{EnvelopeKey}`).
Added multi-envelope support with per-envelope cookies for
simultaneous authentication.

Renamed `EnvelopeViewer` to `EnvelopeReceiver` to reflect its
expanded functionality for viewing and signing envelopes.
Replaced iText7 and PSPDFKit with PDF.js 3.11.174 for document
viewing and signing, with configurable quality settings.

Updated `AuthService` and `SignatureCacheService` to support the
new route structure and multi-envelope authentication. Adjusted
sender login flow to redirect to `/sender` upon success.

Updated documentation to reflect the new architecture, route
structure, and file renames. Deprecated libraries and past
mistakes were documented to avoid repetition.
2026-06-11 11:02:01 +02:00
2766d963af Update login route and refactor _dotNetRef type
Updated the login route from `/login/{EnvelopeKey}` to
`/envelope/login/{EnvelopeKey}` across the application, including
navigation paths in `EnvelopeReceiver.razor` and the `@page` directive
in `LoginReceiver.razor`.

Refactored `_dotNetRef` in `EnvelopeReceiver.razor` to use
`DotNetObjectReference<enveloperece>?` instead of
`DotNetObjectReference<EnvelopeReceiver>?` to reflect a change in the
referenced type.
2026-06-11 10:59:50 +02:00
8fd9928524 Update routes and add EnvelopeSender component
Added a new `EnvelopeSender.razor` component with the route `/sender`.
Updated the route for `LoginSender.razor` from `/login` to `/sender/login`.
Modified navigation logic in `LoginSender.razor` to redirect to
`/sender` after a successful login instead of the root route.
2026-06-11 10:50:28 +02:00
fb3ee14f8f Initialize _pdfLoaded and update _dotNetRef type
The `_pdfLoaded` variable was initialized to `false` in the `EnvelopeReceiver.razor` file. Additionally, the type of `_dotNetRef` was changed from `DotNetObjectReference<EnvelopeViewer>?` to `DotNetObjectReference<EnvelopeReceiver>?` to reflect the correct object reference.
2026-06-11 10:49:31 +02:00
e3929a99e3 rename EnvelopeViewer to EnvelopeReceiver 2026-06-11 10:48:03 +02:00
b6d86aa3eb Refactor documentation for unified architecture
Updated documentation to reflect the transition to a unified Blazor WASM frontend for both Senders and Receivers. Removed references to PSPDFKit and legacy components, emphasizing the use of PDF.js and DevExpress.

Key changes include:
- Revised purpose and architecture sections.
- Updated solution structure to mark `EnvelopeGenerator.Web` as deprecated.
- Enhanced coordinate system explanation with conversion formulas.
- Documented new `EnvelopeViewer` features and signature workflows.
- Added details on signature caching and login flows for Senders and Receivers.
- Expanded "Mistakes History" to highlight lessons learned.
- Added quick reference for debugging and development consistency.

These changes improve clarity, maintainability, and alignment with the current system architecture.
2026-06-11 10:25:44 +02:00
4171a3138b Add LoginSender.razor for Sender user authentication
Implemented a new `LoginSender.razor` component to provide a login page for "Sender" users. The page includes:
- A responsive card layout with a header, input fields for "Username" and "Password," and a submit button with a loading spinner.
- Error handling for invalid credentials and server errors, with appropriate alerts.
- A password visibility toggle for better user experience.
- Integration with `AuthService` for authentication and `NavigationManager` for redirection.

Added Blazor code-behind logic to manage state, handle login submission, and trigger login on "Enter" key press. The page is styled with a custom theme and includes a footer for support information.
2026-06-11 10:09:59 +02:00
e98e18cfe0 Add sender login functionality to AuthService
Added a `SenderLoginResult` enum to represent outcomes of sender login attempts.
Implemented the `LoginSenderAsync` method in `AuthService` to authenticate sender users via the `/api/auth?cookie=true` endpoint.
The method handles HTTP response codes and returns appropriate `SenderLoginResult` values.
Included the `System.Net.Http.Json` namespace to support JSON-based HTTP requests.
2026-06-11 10:01:33 +02:00
14aff03de4 rename Login.razor as LoginReceiver.razor 2026-06-11 09:59:58 +02:00
d828a5bfe2 Refactor authentication schemes for sender and receiver
Updated `AuthScheme` to introduce a distinct `Sender` scheme
and renamed the `Receiver` scheme for clarity. Updated
`Program.cs` to use the new `Sender` scheme in JWT
authentication and explicitly associate authentication
schemes with `Sender` and `Receiver` policies. Removed the
deprecated `AuthPolicy.ReceiverTFA` policy. These changes
improve the separation and maintainability of authentication
and authorization logic.
2026-06-10 22:25:02 +02:00
a6e174e7c1 Refactor JWT auth scheme configuration
Replaced hardcoded per-envelope receiver JWT auth scheme string with a new `AuthScheme` static class containing a `Receiver` constant. Updated `Program.cs` to use `AuthScheme.Receiver` for authentication and policy configuration. Removed redundant comments and unused constants. Added necessary `using` directive for `AuthScheme`.
2026-06-10 17:14:46 +02:00
fc7aa83513 Refactor authorization policies in Program.cs
Reformatted the `AddAuthorizationBuilder()` method for improved readability and consistency. Updated `AuthPolicy.Receiver` to include an additional role, `"receiver"`, in the `RequireRole` method. No functional changes were made to other policies. These changes enhance code maintainability and introduce a minor adjustment to the `AuthPolicy.Receiver` policy.
2026-06-10 15:39:57 +02:00
90661cb856 Enhance SaveSignatureBehavior with validation and repo logic
Updated SaveSignatureBehavior.cs to improve functionality:
- Added new dependencies: AutoMapper, IRepository, and EF Core.
- Enhanced constructor to initialize new dependencies.
- Updated Handle method to validate signatures and handle errors.
- Introduced repository operations for querying and updating elements.
- Improved error handling with BadRequestException.
- Cleaned up redundant code and resolved namespace conflicts.
2026-06-10 15:18:53 +02:00
04e30b0d79 Add ReceiverAppType property and enum to SigningCommand
Added a new `ReceiverAppType` property to the `SigningCommand`
record, initialized to `ReceiverAppType.ReceiverUI`. Introduced
a `ReceiverAppType` enum with values `ReceiverUI` and `LegacyWeb`.
Updated `SignCommandHandler` to reflect these changes without
modifying its functionality.
2026-06-10 15:18:22 +02:00
d0a50f63db Improve validation and error handling in AnnotationBehavior
Updated `using` directives to include `DigitalData.Core.Exceptions` for enhanced exception handling. Updated the `[Obsolete]` attribute message to reflect PSPDFKit library deprecation. Renamed `cancellationToken` to `cancel` for consistency.

Added validation to ensure `PsPdfKitAnnotation` is only supported for the `LegacyWeb` receiver type. Introduced stricter checks for missing or invalid annotation data, throwing `BadRequestException` when necessary. Updated `await next` calls to use the renamed parameter.

These changes improve code clarity, enforce stricter validation, and enhance error handling.
2026-06-10 15:17:25 +02:00
9d20ba1987 Rename ElementId to Id in Signature record
The `ElementId` property in the `Signature` record was renamed to `Id`. This change simplifies the property name, making it more concise and aligning with standard naming conventions or domain terminology.
2026-06-10 12:38:38 +02:00
66f7b6f5e1 Enhance AutoMapper mappings and add base64 decoding
Updated `MappingProfile` to map `Signature.DataUrl` to
`DocReceiverElement.Ink` using the new `MapDataUrlToRequiredBytes`
extension method. Added `MapDataUrlToRequiredBytes` to handle
base64-encoded data URLs, converting them to byte arrays.

Introduced a `using System;` directive in `AutoMapperAuditingExtensions.cs`
to support `DateTime`. Retained `MapChangedWhen` functionality while
extending mapping capabilities for handling base64 data URLs.
2026-06-10 12:36:35 +02:00
25 changed files with 1676 additions and 1250 deletions

424
COPILOT_CONTEXT.md Normal file
View File

@@ -0,0 +1,424 @@
# EnvelopeGenerator — AI Context Reference
## Purpose
Digital document signing system with **unified Blazor WASM frontend** for both Senders and Receivers. Senders create envelopes and place signature fields. Receivers view PDFs, sign documents, export stamped PDFs.
**Primary Libraries:** DevExpress + PDF.js (PSPDFKit removed)
---
## Deployment Architecture
**Two Presentation Projects (Both Required):**
1. **EnvelopeGenerator.API** (ASP.NET Core Web API)
- Runs independently (development & production)
- **YARP Reverse Proxy** configured via `yarp.json`
- Proxies requests to:
- `EnvelopeGenerator.ReceiverUI` (Blazor WASM)
- External Auth.API service
- Serves as single entry point for all requests
2. **EnvelopeGenerator.ReceiverUI** (Blazor WebAssembly)
- Runs on separate host/port
- Accessed **only through API proxy** (not directly)
- Serves static files (HTML, JS, CSS, WASM)
**Request Flow:**
```
Client ? API:8088 (YARP Proxy) ? ReceiverUI:52936 (Blazor WASM)
? Auth.API:9090 (External Auth Service)
```
**Configuration:** `EnvelopeGenerator.API/yarp.json`
---
## ReceiverUI Route Structure
### Root Route
| Route | File | Purpose |
|---|---|---|
| `/` | `Index.razor` | Application entry point (landing page). |
### Sender Routes
| Route | File | Purpose |
|---|---|---|
| `/sender/login` | `LoginSenderPage.razor` | Username/password authentication |
| `/sender` | `EnvelopeSenderPage.razor` | Sender dashboard (envelope list) |
### Receiver Routes
| Route | File | Purpose |
|---|---|---|
| `/envelope/login/{EnvelopeKey}` | `LoginReceiverPage.razor` | Access code authentication for specific envelope |
| `/envelope/{EnvelopeKey}` | `EnvelopeReceiverPage.razor` | View & sign envelope (PDF.js viewer) |
**Multi-Envelope Support:** Receivers can login to multiple envelopes simultaneously (per-envelope cookie authentication).
---
## Architecture Evolution
### Old Architecture (Deprecated)
- **Sender UI:** `EnvelopeGenerator.Web` (Razor Pages + PSPDFKit)
- **Receiver UI:** `EnvelopeGenerator.ReceiverUI` (Blazor WASM + PDF.js)
- **Backend:** `EnvelopeGenerator.API`
### Current Architecture
- **Unified Frontend:** `EnvelopeGenerator.ReceiverUI` (Blazor WASM) — **Both Senders & Receivers**
- **Backend:** `EnvelopeGenerator.API`**Both Senders & Receivers**
- **Libraries:** DevExpress + PDF.js
- **PSPDFKit:** **REMOVED**
---
## Solution Structure
| Project | Target | Purpose |
|---|---|---|
| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Backend for **both Senders & Receivers**. Auth, PDF serving, signature endpoints. |
| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | **Unified Blazor WebAssembly Frontend**. UI for **both Senders & Receivers**. YARP proxy to API. |
| `EnvelopeGenerator.Web` | net7/8/9 | **DEPRECATED.** Legacy Razor Pages (Sender UI). No longer used. |
| `EnvelopeGenerator.Application` | multi | MediatR CQRS handlers. Business logic. |
| `EnvelopeGenerator.Domain` | multi | Domain models, constants, interfaces. |
| `EnvelopeGenerator.Infrastructure` | multi | EF Core repos, DB context. |
| `EnvelopeGenerator.PdfEditor` | multi | iText7 utilities (NOT used in ReceiverUI). |
| `EnvelopeGenerator.DependencyInjection` | multi | DI registration helpers. |
| **VB.NET projects** (Service/Form/BBTests) | net462 | **Legacy. Do NOT touch.** |
---
## Key Files & Routes
| File | Route/Purpose |
|---|---|
| `ReceiverUI/Pages/Index.razor` | `/` — Application entry point (landing page). |
| `ReceiverUI/Pages/EnvelopeSenderPage.razor` | `/sender` — Sender dashboard (envelope list). |
| `ReceiverUI/Pages/EnvelopeReceiverPage.razor` | `/envelope/{key}` — Receiver PDF viewer & signing. |
| `ReceiverUI/Pages/LoginSenderPage.razor` | `/sender/login` — Sender username/password auth. |
| `ReceiverUI/Pages/LoginReceiverPage.razor` | `/envelope/login/{EnvelopeKey}` — Receiver access code auth. |
| `ReceiverUI/wwwroot/js/pdf-viewer.js` | PDF.js wrapper (zoom, pagination, thumbnails). |
| `ReceiverUI/wwwroot/js/receiver-signature.js` | Signature pad (draw/type/image). |
| `ReceiverUI/wwwroot/css/envelope-viewer.css` | EnvelopeViewer styles. |
| `ReceiverUI/Services/AuthService.cs` | Receiver + Sender authentication. |
| `ReceiverUI/Services/SignatureCacheService.cs` | Signature caching (Redis/SQL). |
| `API/Controllers/CacheController.cs` | Signature cache endpoints. |
---
## Coordinate System — CRITICAL
**Database Format:** INCHES (GdPicture14 native)
**Origin:** Top-left corner
**Axes:** X right, Y down
### Conversion Formulas
| From INCHES to | Formula | Example |
|---|---|---|
| **DevExpress DX** | `x_DX = x_inches * 100` | 1.5" ? 150 DX |
| **PDF Points** | `x_pt = x_inches * 72` | 1.5" ? 108 pt |
| **PDF.js Pixels** | Normalize ? scale | `(x_inches / pageWidth) * canvasWidth * scale` |
**A4 Dimensions:**
- Width: 8.27" = 595pt = 827 DX
- Height: 11.69" = 842pt = 1169 DX
### Unit Systems
| System | Unit | Origin | Y-Axis |
|---|---|---|---|
| **Database (GdPicture14)** | Inches | Top-left | Down |
| PDF.js | Pixels | Top-left | Down |
| iText7 PDF | Points (1/72") | **Bottom-left** | **Up** (flip required) |
| ~~PSPDFKit~~ | ~~Points~~ | ~~Top-left~~ | **REMOVED** |
---
## EnvelopeReceiver — PDF.js Viewer & Signing
**Route:** `/envelope/{EnvelopeKey}`
**Tech:** PDF.js 3.11.174 + Blazor WASM + configurable quality
**File:** `ReceiverUI/Pages/EnvelopeReceiverPage.razor`
### Key Features
1. HiDPI/Retina support (4x quality)
2. Configurable quality (`appsettings.json`)
3. Unlimited zoom (50%-300%)
4. Ctrl+Wheel global zoom
5. Resizable thumbnail sidebar (150-400px, localStorage)
6. Responsive (desktop/mobile)
### Configuration
**File:** `ReceiverUI/wwwroot/appsettings.json`
```json
{
"PdfViewer": {
"ThumbnailBaseScale": 0.75,
"ThumbnailEnableHiDPI": true,
"MainCanvasEnableHiDPI": true,
"ZoomStepPercentage": 5
}
}
```
### JavaScript API
**File:** `ReceiverUI/wwwroot/js/pdf-viewer.js`
```javascript
window.pdfViewer = {
initialize(canvasId, pdfDataUrl, dotNetRef),
renderPage(num),
renderSignatureButtons(signatures, pageNum, dotNetRef),
applySignature(signatureId, dataUrl, fullName, position, place),
zoomIn(), zoomOut(), dispose()
}
```
---
## Signature Workflow — EnvelopeReceiver
**IMPORTANT:** iText7 NOT used (GPL license issue). Client-side overlay system only.
### Workflow Steps
1. **Page Load:**
- Check `SignatureCacheService` for cached signature
- If cached ? skip popup, load signature
- If not ? show automatic popup (mandatory)
2. **Signature Popup (DxPopup):**
- **Cannot close** (no X, no ESC, no outside-click)
- **3 Tabs:** Draw (canvas) / Text (font select) / Image (upload)
- **Required:** Full name, Place
- **Optional:** Position
- **Save ?** Store in `_capturedSignature`, cache via API
3. **Signature Buttons:**
- Render purple "Unterschreiben" buttons at signature field positions
- Coordinates: INCHES ? POINTS ? Pixels (scaled)
- File: `pdf-viewer.js` ? `renderSignatureButtons()`
4. **Apply Signature (Click "Unterschreiben"):**
- JS: Remove button, create HTML overlay
- Format: Image + separator + text (Name, Position, Place, Date)
- **NOT stamped on PDF bytes** (visual overlay only)
5. **Re-rendering:**
- Zoom/Page change ? recalculate button positions
- Session state: `_capturedSignature` (lost on refresh)
### Data Model
**File:** `ReceiverUI/Models/SignatureCaptureDto.cs`
```csharp
public sealed record SignatureCaptureDto {
public required string DataUrl { get; init; } // base64 PNG
public required string FullName { get; init; }
public string Position { get; init; } = ""; // Optional
public required string Place { get; init; }
}
```
---
## Signature Caching
**Purpose:** Persist signature across page refreshes (distributed cache: Redis/SQL)
### API Endpoints
**Controller:** `API/Controllers/CacheController.cs`
- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save
- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load
- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete
**Cache Key Format:**
```
signature:91751687-8ae6-4777-bf5f-b8846085e62e:{envelopeKey}
```
**Configuration:** `appsettings.json`
```json
{
"Cache": {
"SignatureCacheExpiration": null // or "02:00:00" for 2h
}
}
```
### Service
**File:** `ReceiverUI/Services/SignatureCacheService.cs`
```csharp
public class SignatureCacheService {
Task SaveSignatureAsync(string envelopeKey, SignatureCaptureDto signature);
Task<SignatureCaptureDto?> GetSignatureAsync(string envelopeKey);
Task DeleteSignatureAsync(string envelopeKey);
}
```
**Error Handling:** Fire-and-forget saves, graceful degradation on load failure.
---
## Sender Login
**Route:** `/sender/login`
**File:** `ReceiverUI/Pages/LoginSenderPage.razor`
**Tech:** Bootstrap 5 + DevExpress Blazing Berry theme
### AuthService Extension
**File:** `ReceiverUI/Services/AuthService.cs`
```csharp
public enum SenderLoginResult { Success, InvalidCredentials, Error }
public async Task<SenderLoginResult> LoginSenderAsync(string username, string password) {
var response = await http.PostAsJsonAsync(
$"{_api.BaseUrl}/api/auth?cookie=true",
new { username, password });
return response.StatusCode switch {
HttpStatusCode.OK => SenderLoginResult.Success,
HttpStatusCode.Unauthorized => SenderLoginResult.InvalidCredentials,
_ => SenderLoginResult.Error
};
}
```
### API Integration
**Endpoint:** `POST /api/auth?cookie=true`
**Request:**
```json
{ "username": "TekH", "password": "***" }
```
**Response:**
- `200 OK` ? Cookie set, redirect to `/sender`
- `401 Unauthorized` ? Show error: "Ungültige Anmeldedaten"
- Other ? Show error: "Serverfehler"
**Cookie:** HTTP-only, Secure (HTTPS), SameSite=Strict
### UI Flow
1. User enters username + password
2. Click "Anmelden" or press Enter
3. Call `AuthService.LoginSenderAsync()`
4. Success ? `Navigation.NavigateTo("/sender", forceLoad: true)`
5. Error ? Display alert
---
## Receiver Login
**Route:** `/envelope/login/{EnvelopeKey}`
**File:** `ReceiverUI/Pages/LoginReceiverPage.razor`
**Multi-Envelope Support:** Cookies are stored per-envelope (e.g., `AuthTokenSignFLOWReceiver.{envelopeKey}`), allowing simultaneous authentication for multiple envelopes in the same browser session.
### AuthService Method
```csharp
public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string key, string accessCode) {
var form = new MultipartFormDataContent();
form.Add(new StringContent(accessCode), "AccessCode");
var response = await http.PostAsync(
$"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(key)}", form);
return response.StatusCode switch {
HttpStatusCode.OK => EnvelopeLoginResult.Success,
HttpStatusCode.Unauthorized => EnvelopeLoginResult.InvalidCode,
HttpStatusCode.NotFound => EnvelopeLoginResult.NotFound,
_ => EnvelopeLoginResult.Error
};
}
```
**Success:** Redirect to `/envelope/{key}`
---
## NuGet Packages (ReceiverUI)
| Package | Version | Purpose |
|---|---|---|
| `DevExpress.Blazor.*` | 25.2.3 | UI components (grids, popups, etc.) |
| `SkiaSharp.*` | 3.119.1 | WASM rendering |
| ~~`itext`~~ | ~~8.0.5~~ | **NOT USED** (GPL license) |
**External CDN:**
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
---
## Mistakes History — Do NOT Repeat
| Mistake | Why Wrong |
|---|---|
| Using iText7 in EnvelopeReceiver | GPL license issue. Use overlay system instead. |
| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. |
| Hardcoded quality values in PDF.js | Use `appsettings.json` for configurability. |
| Complex toolbar layouts | User wants simplicity. Keep horizontal layout. |
| Over-designed UI (gradients/badges) | User prefers simple text labels. |
| Ignoring "revert" instructions | Revert HTML structure, not just CSS. |
| `BottomMarginBand` for signatures | Repeats on every page. Use DetailBand. |
| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand. Calculate per-page. |
---
## Development Notes
### Deprecated Projects
**DO NOT USE:**
- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified ReceiverUI
- PSPDFKit — Removed, use PDF.js + DevExpress instead
### Legacy Projects (VB.NET)
**DO NOT TOUCH:** `EnvelopeGenerator.Service`, `EnvelopeGenerator.Form`, `EnvelopeGenerator.BBTests`
### Signature Coordinate Evidence
**File:** `EnvelopeGenerator.Form/frmFieldEditor.vb` (VB.NET)
```vb
Private Const SIGNATURE_WIDTH As Single = 1.77 ' inches
Private Const SIGNATURE_HEIGHT As Single = 1.96 ' inches
Sub LoadAnnotation(pElement As Signature, ...)
oAnnotation.Left = CSng(pElement.X) ' Direct INCHES assignment
oAnnotation.Top = CSng(pElement.Y)
End Sub
```
Proves database uses INCHES natively.
---
## Quick Reference
### When working with coordinates:
1. **Database ? UI:** INCHES × 72 = PDF Points
2. **UI ? Display:** Points × scale = Pixels
3. **iText7 stamping:** Flip Y-axis (top-down ? bottom-up)
### When adding features:
1. Check `Mistakes History` first
2. Prefer simplicity over complexity
3. Use `appsettings.json` for configuration
4. Keep consistent with existing design (Bootstrap 5 + Blazing Berry)
5. **Unified frontend:** ReceiverUI serves both Senders and Receivers
### When debugging:
1. **Coordinates:** Always check unit system (inches/points/pixels)
2. **Authentication:** Check cookie name/domain/SameSite
3. **Cache:** Check Redis/SQL connection + key format
4. **Frontend confusion:** Only use ReceiverUI (Web is deprecated)
---
**Last Updated:** Session 19 (Razor file naming convention + Index route proxy)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
namespace EnvelopeGenerator.API;
/// <summary>
///
/// </summary>
public static class AuthScheme
{
/// <summary>
/// Scheme name used for per-envelope receiver JWT authentication.
/// </summary>
public const string Receiver = "EnvelopeGenerator.API.ReceiverJWT";
/// <summary>
/// Scheme name used for per-envelope sender JWT authentication.
/// </summary>
public const string Sender = "EnvelopeGenerator.API.SenderJWT";
}

View File

@@ -21,6 +21,7 @@ using EnvelopeGenerator.API.Options;
using NLog.Web;
using NLog;
using DigitalData.Auth.Claims;
using EnvelopeGenerator.API;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Info("Logging initialized!");
@@ -130,15 +131,12 @@ try
var authTokenKeys = config.GetOrDefault<AuthTokenKeys>();
// Scheme name used for per-envelope receiver JWT authentication.
const string EnvelopeReceiverScheme = "EnvelopeReceiverJwt";
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(opt =>
.AddJwtBearer(AuthScheme.Sender, opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
@@ -176,7 +174,7 @@ try
// last path segment of the request URL.
// This enables simultaneous authentication for multiple envelopes
// within the same browser session.
.AddJwtBearer(EnvelopeReceiverScheme, opt =>
.AddJwtBearer(AuthScheme.Receiver, opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
@@ -240,19 +238,16 @@ try
});
builder.Services.AddAuthorizationBuilder()
.AddPolicy(AuthPolicy.SenderOrReceiver, policy =>
policy.RequireRole(Role.Sender, Role.Receiver.Full))
.AddPolicy(AuthPolicy.Sender, policy =>
policy.RequireRole(Role.Sender))
// Per-envelope policy: uses the dedicated EnvelopeReceiverJwt scheme so it
// never conflicts with the default JwtBearer scheme.
.AddPolicy(AuthPolicy.Receiver, policy =>
policy
.AddAuthenticationSchemes(EnvelopeReceiverScheme)
.RequireAuthenticatedUser()
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy.RequireRole(Role.Sender, Role.Receiver.Full))
.AddPolicy(AuthPolicy.Sender, policy => policy
.RequireRole(Role.Sender)
.AddAuthenticationSchemes(AuthScheme.Sender))
.AddPolicy(AuthPolicy.Receiver, policy => policy
.AddAuthenticationSchemes(AuthScheme.Receiver)
.RequireAuthenticatedUser()
.RequireRole(Role.Receiver.Full, "receiver"))
.AddPolicy(AuthPolicy.ReceiverTFA, policy =>
policy.RequireRole(Role.Receiver.TFA));
.AddPolicy(AuthPolicy.ReceiverTFA, policy => policy.RequireRole(Role.Receiver.TFA));
// User manager
#pragma warning disable CS0618 // Type or member is obsolete

View File

@@ -1,21 +1,16 @@
{
"ReverseProxy": {
"Routes": {
"receiver-ui-receiver": {
"receiver-ui-root": {
"ClusterId": "receiver-ui",
"Order": 100,
"Order": 300,
"Match": {
"Path": "/receiver/{**catch-all}",
"Path": "/",
"Methods": [ "GET", "HEAD" ]
}
},
"receiver-ui-login": {
"ClusterId": "receiver-ui",
"Order": 100,
"Match": {
"Path": "/login/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"Transforms": [
{ "PathSet": "/index.html" }
]
},
"receiver-ui-sender": {
"ClusterId": "receiver-ui",
@@ -23,7 +18,10 @@
"Match": {
"Path": "/sender/{**catch-all}",
"Methods": [ "GET", "HEAD" ]
}
},
"Transforms": [
{ "PathSet": "/index.html" }
]
},
"receiver-ui-envelope": {
"ClusterId": "receiver-ui",
@@ -36,6 +34,17 @@
{ "PathSet": "/index.html" }
]
},
"receiver-ui-envelope-dxreportviewer": {
"ClusterId": "receiver-ui",
"Order": 90,
"Match": {
"Path": "/envelope/{EnvelopeKey}/DxReportViewer",
"Methods": [ "GET", "HEAD" ]
},
"Transforms": [
{ "PathSet": "/index.html" }
]
},
"receiver-ui-blazor-framework": {
"ClusterId": "receiver-ui",
"Order": 50,

View File

@@ -40,7 +40,9 @@ public class MappingProfile : Profile
// DTO to Entity mappings
CreateMap<ConfigDto, Config>();
CreateMap<DocReceiverElementDto, DocReceiverElement>();
CreateMap<Signature, DocReceiverElement>().MapChangedWhen();
CreateMap<Signature, DocReceiverElement>()
.ForMember(dest => dest.Ink, opt => opt.MapFrom(src => src.DataUrl.MapDataUrlToRequiredBytes()))
.MapChangedWhen();
CreateMap<DocumentStatusDto, DocumentStatus>();
CreateMap<EmailTemplateDto, EmailTemplate>();
CreateMap<EnvelopeDto, Envelope>();

View File

@@ -15,7 +15,7 @@ public sealed record Signature
/// <summary>
/// TBDD_DOCUMENT_RECEIVER_ELEMENT.ID - identifies the specific signature field on the PDF page.
/// </summary>
public required int ElementId { get; init; }
public required int Id { get; init; }
/// <summary>
/// Base64-encoded data URL of the signature image.

View File

@@ -1,5 +1,6 @@
using AutoMapper;
using EnvelopeGenerator.Domain.Interfaces.Auditing;
using System;
namespace EnvelopeGenerator.Application.Common.Extensions;
@@ -21,4 +22,21 @@ public static class AutoMapperAuditingExtensions
public static IMappingExpression<TSource, TDestination> MapChangedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
where TDestination : IHasChangedWhen
=> expression.ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(_ => DateTime.Now));
/// <summary>
/// Converts a base64 data URL string to a byte array.
/// Handles data URLs in the format: "data:image/png;base64,iVBORw0KG..."
/// </summary>
/// <param name="dataUrl">The base64 data URL string from Canvas.toDataURL()</param>
/// <returns>The decoded byte array, or null if the input is null or empty</returns>
public static byte[]? MapDataUrlToRequiredBytes(this string dataUrl)
{
// Remove data URL prefix (e.g., "data:image/png;base64,")
var base64Index = dataUrl.IndexOf(',', StringComparison.Ordinal);
if (base64Index == -1)
throw new ArgumentException("Invalid data URL format. Unable to extract base64 data.", nameof(dataUrl));
var base64Data = dataUrl[(base64Index + 1)..];
return Convert.FromBase64String(base64Data);
}
}

View File

@@ -1,4 +1,5 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Entities;
@@ -10,13 +11,13 @@ namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
/// Pipeline behavior that saves annotations.
/// Executes first in the signing process.
/// </summary>
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
[Obsolete("The PSPDFKit library is deprecated.")]
public class AnnotationBehavior : IPipelineBehavior<SigningCommand, Unit>
{
private readonly IRepository<ElementAnnotation> _repo;
/// <summary>
///
/// Initializes a new instance of the <see cref="AnnotationBehavior"/> class.
/// </summary>
/// <param name="repository"></param>
public AnnotationBehavior(IRepository<ElementAnnotation> repository)
@@ -29,13 +30,21 @@ public class AnnotationBehavior : IPipelineBehavior<SigningCommand, Unit>
/// </summary>
/// <param name="request"></param>
/// <param name="next"></param>
/// <param name="cancellationToken"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
{
if (request.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
await _repo.CreateAsync(annot.Structured, cancellationToken);
if(request.ReceiverAppType != ReceiverAppType.LegacyWeb)
if(request.PsPdfKitAnnotation is null)
return await next(cancel);
else
throw new BadRequestException("PsPdfKit Annotation are only supported for the legacy web receiver type.");
return await next(cancellationToken);
if (request.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
await _repo.CreateAsync(annot.Structured, cancel);
else
throw new BadRequestException("Annotation data is missing or invalid.");
return await next(cancel);
}
}

View File

@@ -1,22 +1,11 @@
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.Core.Exceptions;
using EnvelopeGenerator.Application.Common.Dto;
using EnvelopeGenerator.Application.DocStatus.Commands;
using EnvelopeGenerator.Domain.Constants;
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using System.Text.Json;
<<<<<<< TODO: Unmerged change from project 'EnvelopeGenerator.Application (net8.0)', Before:
=======
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand.SigningCommand;
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand;
>>>>>>> After
<<<<<<< TODO: Unmerged change from project 'EnvelopeGenerator.Application (net9.0)', Before:
=======
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand;
>>>>>>> After
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand.SigningCommand.SigningCommand;
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand.SigningCommand;
using EnvelopeGenerator.Application.DocReceiverElements.Commands.SigningCommand;
using Microsoft.EntityFrameworkCore;
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
@@ -28,13 +17,22 @@ public class SaveSignatureBehavior : IPipelineBehavior<SigningCommand, Unit>
{
private readonly ISender _sender;
private readonly IRepository<DocReceiverElement> _elementRepo;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
public SaveSignatureBehavior(ISender sender)
/// <param name="elementRepo"></param>
/// <param name="mapper"></param>
public SaveSignatureBehavior(ISender sender, IRepository<DocReceiverElement> elementRepo, IMapper mapper)
{
_sender = sender;
_elementRepo = elementRepo;
_elementRepo = elementRepo;
_mapper = mapper;
}
/// <summary>
@@ -42,10 +40,31 @@ public class SaveSignatureBehavior : IPipelineBehavior<SigningCommand, Unit>
/// </summary>
/// <param name="request"></param>
/// <param name="next"></param>
/// <param name="cancellationToken"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
{
return await next(cancellationToken);
if (request.ReceiverAppType == ReceiverAppType.LegacyWeb)
return await next(cancel);
else if(request.Signatures is not IEnumerable<Signature> signatures)
throw new BadRequestException($"Signatures are required for saving signature behavior.");
var elements = await _elementRepo
.Where(e => e.Document.EnvelopeId == request.Envelope.Id)
.Where(e => e.ReceiverId == request.Receiver.Id)
.ToListAsync(cancel);
foreach (var element in elements)
{
var signatures = request.Signatures.Where(s => s.Id == element.Id).ToList();
if(signatures.Count == 0)
throw new BadRequestException("No signature found for element with id {element.Id}.");
else if(signatures.Count > 1)
throw new BadRequestException("Multiple signatures found for element with id {element.Id}.");
await _elementRepo.UpdateAsync(signatures.First(), e => e.Id == element.Id, cancel);
}
return await next(cancel);
}
}

View File

@@ -36,6 +36,11 @@ public record SigningCommand : EnvelopeReceiverQueryBase, IRequest
///
/// </summary>
public IEnumerable<Signature>? Signatures { get; init; }
/// <summary>
///
/// </summary>
public ReceiverAppType ReceiverAppType { get; init; } = ReceiverAppType.ReceiverUI;
}
/// <summary>
@@ -55,3 +60,19 @@ public class SignCommandHandler : IRequestHandler<SigningCommand>
return Task.CompletedTask;
}
}
/// <summary>
///
/// </summary>
public enum ReceiverAppType
{
/// <summary>
///
/// </summary>
ReceiverUI = 0,
/// <summary>
///
/// </summary>
LegacyWeb = 1,
}

View File

@@ -6,5 +6,5 @@ public class ApiOptions
public string BaseUrl { get; set; } = string.Empty;
public bool ForceToUseFakeDocument { get; set; } = false;
public bool UsePredefinedReports { get; set; } = false;
}

View File

@@ -16,7 +16,7 @@
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService
@inject AppVersionService AppVersion
@inject ILogger<EnvelopeViewer> logger
@inject ILogger<EnvelopeReceiverPage> logger
@implements IAsyncDisposable
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
@@ -511,7 +511,7 @@ int _totalPages = 0;
int _currentZoom = 150;
bool _showThumbnails = true;
bool _isLoggingOut = false;
DotNetObjectReference<EnvelopeViewer>? _dotNetRef;
DotNetObjectReference<EnvelopeReceiverPage>? _dotNetRef;
IReadOnlyList<SignatureDto> _signatures = [];
EnvelopeReceiverDto? _envelopeReceiver;
@@ -545,7 +545,7 @@ const int MaxThumbnailWidth = 400;
_isLoggingOut = true;
await InvokeAsync(StateHasChanged);
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey);
Navigation.NavigateTo($"/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
}
protected override async Task OnInitializedAsync() {
@@ -558,24 +558,28 @@ const int MaxThumbnailWidth = 400;
// Check authentication
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
if (!hasAccess) {
Navigation.NavigateTo($"/login/{Uri.EscapeDataString(EnvelopeKey)}");
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
return;
}
try {
var (pdfBytes, statusCode) = await DocumentService.GetDocumentAsync(EnvelopeKey);
var pdfBytes = await DocumentService.GetDocumentAsync(EnvelopeKey);
if (pdfBytes is { Length: > 0 }) {
var base64 = Convert.ToBase64String(pdfBytes);
_pdfDataUrl = $"data:application/pdf;base64,{base64}";
} else {
_errorMessage = $"Dokument konnte nicht geladen werden. HTTP Status: {statusCode}";
_errorMessage = "Dokument konnte nicht geladen werden: Keine Daten empfangen.";
}
var signatures = await SignatureService.GetAsync(EnvelopeKey);
_signatures = signatures.Convert(UnitOfLength.Point);
_envelopeReceiver = await EnvelopeReceiverService.GetAsync(EnvelopeKey);
if (_envelopeReceiver is null)
{
logger.LogWarning("Envelope receiver data is null for envelope {EnvelopeKey}", EnvelopeKey);
}
await JSRuntime.InvokeVoidAsync("console.log", "Loaded signatures:", _signatures);
@@ -608,8 +612,12 @@ const int MaxThumbnailWidth = 400;
_popupValidationMessage = null;
}
} catch (HttpRequestException ex) {
_errorMessage = $"Dokument konnte nicht geladen werden: {ex.Message}";
logger.LogError(ex, "Failed to load document for envelope {EnvelopeKey}", EnvelopeKey);
} catch (Exception ex) {
_errorMessage = $"Fehler: {ex.Message}";
logger.LogError(ex, "Unexpected error during initialization for envelope {EnvelopeKey}", EnvelopeKey);
}
_isLoading = false;

View File

@@ -0,0 +1,49 @@
@page "/envelope/{EnvelopeKey}/DxReportViewer"
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
@using DevExpress.Blazor.Reporting
@using Microsoft.Extensions.Options
@using EnvelopeGenerator.ReceiverUI.Options
@using EnvelopeGenerator.ReceiverUI.Services
@inject InMemoryReportStorageWebExtension ReportStorage
@inject DocumentService DocumentService
@inject IOptions<ApiOptions> AppOptions
<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" />
@if (_report is not null) {
<DxReportViewer Report="_report" RootCssClasses="w-100 h-100" Zoom="1.3" />
}
@code {
[Parameter] public string EnvelopeKey { get; init; } = null!;
XtraReport? _report = null;
protected override async Task OnInitializedAsync()
{
_report = await CreateReport();
}
async Task<XtraReport> CreateReport()
{
if (AppOptions.Value.UsePredefinedReports)
{
return PredefinedReports.ReportsFactory.GetReport("LargeDatasetReport");
}
else
{
var pdfBytes = await DocumentService.GetDocumentAsync(EnvelopeKey);
if (pdfBytes is null || pdfBytes.Length == 0)
throw new InvalidOperationException($"No PDF bytes found for EnvelopeKey: {EnvelopeKey}");
var report = new XtraReport();
var detail = new DevExpress.XtraReports.UI.DetailBand();
report.Bands.Add(detail);
detail.Controls.Add(new DevExpress.XtraReports.UI.XRPdfContent { Source = pdfBytes, GenerateOwnPages = true });
return report;
}
}
}

View File

@@ -0,0 +1,7 @@
@page "/sender"
<h3>EnvelopeSender</h3>
@code {
}

View File

@@ -342,13 +342,24 @@ Shown="OnPopupShownAsync">
}
}
_annotations = await AnnotationService.GetAnnotationsAsync(EnvelopeKey ?? "fake");
_envelopeReceiver = await EnvelopeReceiverService.GetAsync(EnvelopeKey ?? "fake");
_annotations = await AnnotationService.GetAnnotationsAsync(EnvelopeKey);
if (!AppOptions.Value.ForceToUseFakeDocument && !string.IsNullOrWhiteSpace(EnvelopeKey)) {
var (pdfBytes, _) = await DocumentService.GetDocumentAsync(EnvelopeKey);
if (pdfBytes is { Length: > 0 })
_basePdfBytes = pdfBytes;
try {
_envelopeReceiver = await EnvelopeReceiverService.GetAsync(EnvelopeKey);
} catch (HttpRequestException ex) {
// Log error but continue - UI will handle null envelope receiver gracefully
Console.WriteLine($"Failed to load envelope receiver: {ex.Message}");
}
if (!AppOptions.Value.UsePredefinedReports && !string.IsNullOrWhiteSpace(EnvelopeKey)) {
try {
var pdfBytes = await DocumentService.GetDocumentAsync(EnvelopeKey);
if (pdfBytes is { Length: > 0 })
_basePdfBytes = pdfBytes;
} catch (HttpRequestException ex) {
// Log error but continue - will use predefined report instead
Console.WriteLine($"Failed to load document: {ex.Message}");
}
}
var initialReport = BuildFreshBaseReport();

View File

@@ -1,4 +1,4 @@
@page "/login/{EnvelopeKey}"
@page "/envelope/login/{EnvelopeKey}"
@using EnvelopeGenerator.ReceiverUI.Services
@inject AuthService AuthService
@inject NavigationManager Navigation

View File

@@ -0,0 +1,172 @@
@page "/sender/login"
@using EnvelopeGenerator.ReceiverUI.Services
@inject AuthService AuthService
@inject NavigationManager Navigation
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
<div class="login-page-wrapper d-flex align-items-center justify-content-center min-vh-100">
<div class="login-card card shadow border-0" style="max-width: 440px; width: 100%;">
<div class="card-header text-white text-center py-4 border-0" style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border-radius: calc(0.375rem - 1px) calc(0.375rem - 1px) 0 0;">
<div class="mb-2">
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" viewBox="0 0 16 16">
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
</svg>
</div>
<h5 class="mb-0 fw-semibold">Sender Anmeldung</h5>
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">Sicherer Zugang zum Sender-Dashboard</p>
</div>
<div class="card-body p-4">
<p class="text-muted mb-4" style="font-size: 0.875rem; line-height: 1.5;">
Bitte melden Sie sich mit Ihren Zugangsdaten an, um auf das Sender-Dashboard zuzugreifen.
</p>
@if (LoginResult == SenderLoginResult.InvalidCredentials) {
<div class="alert alert-danger d-flex align-items-start gap-2 py-2" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
</svg>
<div>
<strong>Ungültige Anmeldedaten.</strong><br />
<span style="font-size:0.85rem;">Benutzername oder Passwort ist falsch. Bitte versuchen Sie es erneut.</span>
</div>
</div>
} else if (LoginResult == SenderLoginResult.Error) {
<div class="alert alert-secondary d-flex align-items-start gap-2 py-2" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" 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>
<strong>Serverfehler.</strong><br />
<span style="font-size:0.85rem;">Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</span>
</div>
</div>
}
<div class="mb-3">
<label class="form-label fw-medium" for="login-username">
Benutzername
<span class="text-danger ms-1">*</span>
</label>
<div class="input-group">
<span class="input-group-text bg-light">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#6c757d" viewBox="0 0 16 16">
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/>
</svg>
</span>
<input id="login-username"
type="text"
class="form-control @(LoginResult == SenderLoginResult.InvalidCredentials ? "is-invalid" : null)"
placeholder="Benutzername eingeben"
@bind="Username"
@bind:event="oninput"
@onkeydown="OnKeyDownAsync"
disabled="@IsLoading"
autocomplete="username" />
</div>
</div>
<div class="mb-4">
<label class="form-label fw-medium" for="login-password">
Passwort
<span class="text-danger ms-1">*</span>
</label>
<div class="input-group">
<span class="input-group-text bg-light border-end-0">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#6c757d" 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>
</span>
<input id="login-password"
type="@(ShowPassword ? "text" : "password")"
class="form-control border-start-0 border-end-0 @(LoginResult == SenderLoginResult.InvalidCredentials ? "is-invalid" : null)"
placeholder="Passwort eingeben"
@bind="Password"
@bind:event="oninput"
@onkeydown="OnKeyDownAsync"
disabled="@IsLoading"
autocomplete="current-password" />
<button type="button"
class="btn btn-outline-secondary border-start-0"
style="border-left: none;"
tabindex="-1"
@onclick="() => ShowPassword = !ShowPassword">
@if (ShowPassword) {
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/>
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z"/>
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709z"/>
<path fill-rule="evenodd" d="M13.646 14.354l-12-12 .708-.708 12 12-.708.708z"/>
</svg>
} else {
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
</svg>
}
</button>
</div>
</div>
<button class="btn btn-primary w-100 py-2 fw-medium"
style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border: none;"
@onclick="SubmitAsync"
disabled="@(IsLoading || string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password))">
@if (IsLoading) {
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
<span>Anmelden …</span>
} else {
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M10 3.5a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-2a.5.5 0 0 1 1 0v2A1.5 1.5 0 0 1 9.5 14h-8A1.5 1.5 0 0 1 0 12.5v-9A1.5 1.5 0 0 1 1.5 2h8A1.5 1.5 0 0 1 11 3.5v2a.5.5 0 0 1-1 0v-2z"/>
<path fill-rule="evenodd" d="M4.146 8.354a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5H14.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3z"/>
</svg>
<span>Anmelden</span>
}
</button>
</div>
<div class="card-footer text-center text-muted py-3 border-0 bg-transparent" style="font-size: 0.78rem;">
Bei Problemen wenden Sie sich bitte an den Administrator.
</div>
</div>
</div>
@code {
string Username = string.Empty;
string Password = string.Empty;
bool ShowPassword;
bool IsLoading;
SenderLoginResult? LoginResult;
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) {
if (e.Key == "Enter")
await SubmitAsync();
}
async Task SubmitAsync() {
if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password) || IsLoading) return;
IsLoading = true;
LoginResult = null;
await InvokeAsync(StateHasChanged);
var result = await AuthService.LoginSenderAsync(Username.Trim(), Password.Trim());
if (result == SenderLoginResult.Success) {
Navigation.NavigateTo("/sender", forceLoad: true);
return;
}
LoginResult = result;
IsLoading = false;
await InvokeAsync(StateHasChanged);
}
}

View File

@@ -1,4 +1,5 @@
using System.Net;
using System.Net.Http.Json;
using EnvelopeGenerator.ReceiverUI.Options;
using Microsoft.Extensions.Options;
@@ -6,6 +7,8 @@ namespace EnvelopeGenerator.ReceiverUI.Services;
public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
public enum SenderLoginResult { Success, InvalidCredentials, Error }
public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
{
private readonly ApiOptions _api = apiOptions.Value;
@@ -54,4 +57,25 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
null, cancel);
return response.IsSuccessStatusCode;
}
/// <summary>
/// Authenticates a sender user with username and password.
/// Calls POST /api/auth?cookie=true with JSON body.
/// On success the API sets an authentication cookie automatically.
/// </summary>
public async Task<SenderLoginResult> LoginSenderAsync(string username, string password, CancellationToken cancel = default)
{
var requestBody = new { username, password };
var response = await http.PostAsJsonAsync(
$"{_api.BaseUrl}/api/auth?cookie=true",
requestBody, cancel);
return response.StatusCode switch
{
HttpStatusCode.OK => SenderLoginResult.Success,
HttpStatusCode.Unauthorized => SenderLoginResult.InvalidCredentials,
_ => SenderLoginResult.Error
};
}
}

View File

@@ -11,17 +11,25 @@ public class DocumentService(HttpClient http, IOptions<ApiOptions> apiOptions)
/// <summary>
/// Fetches the PDF bytes for the given envelope key from the API.
/// Returns null bytes with the HTTP status code on failure.
/// Throws HttpRequestException on failure with appropriate status code.
/// </summary>
public async Task<(byte[]? Bytes, HttpStatusCode StatusCode)> GetDocumentAsync(string envelopeKey, CancellationToken cancel = default)
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
public async Task<byte[]?> GetDocumentAsync(string envelopeKey, CancellationToken cancel = default)
{
var response = await http.GetAsync($"{_api.BaseUrl}/api/Document/{Uri.EscapeDataString(envelopeKey)}", cancel);
if (!response.IsSuccessStatusCode)
return (null, response.StatusCode);
{
var statusCode = (int)response.StatusCode;
var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
throw new HttpRequestException(
$"Failed to load document. Status: {statusCode} ({reasonPhrase})",
null,
response.StatusCode);
}
var bytes = await response.Content.ReadAsByteArrayAsync(cancel);
return (bytes, response.StatusCode);
return bytes;
}
}

View File

@@ -1,3 +1,4 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using EnvelopeGenerator.ReceiverUI.Models;
@@ -14,13 +15,25 @@ public class EnvelopeReceiverService(HttpClient http, IOptions<ApiOptions> apiOp
{
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
/// <summary>
/// Fetches the envelope receiver data for the given envelope key from the API.
/// Throws HttpRequestException on failure with appropriate status code.
/// </summary>
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
public async Task<EnvelopeReceiverDto?> GetAsync(string envelopeKey, CancellationToken cancel = default)
{
var url = $"{apiOptions.Value.BaseUrl}/api/EnvelopeReceiver/{Uri.EscapeDataString(envelopeKey)}";
var response = await http.GetAsync(url, cancel);
if (!response.IsSuccessStatusCode)
return null;
{
var statusCode = (int)response.StatusCode;
var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
throw new HttpRequestException(
$"Failed to load envelope receiver data. Status: {statusCode} ({reasonPhrase})",
null,
response.StatusCode);
}
return await response.Content.ReadFromJsonAsync<EnvelopeReceiverDto>(_jsonOptions, cancel);
}

View File

@@ -1,7 +1,7 @@
{
"Api": {
"BaseUrl": "",
"ForceToUseFakeDocument": false
"UsePredefinedReports": false
},
"PdfViewer": {
"ThumbnailBaseScale": 0.75,

View File

@@ -21,7 +21,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "core", "core", "{9943209E-1
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B291-4E19-99B9-E4FA3AFAB62C}"
ProjectSection(SolutionItems) = preProject
COPILOT_CONTEXT_EN.md = COPILOT_CONTEXT_EN.md
COPILOT_CONTEXT.md = COPILOT_CONTEXT.md
FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CBC2432-A561-4440-89BC-671B66A24146}"

788
FORM_APPLICATION_CONTEXT.md Normal file
View File

@@ -0,0 +1,788 @@
# EnvelopeGenerator.Form VB.NET Desktop Application Context
## Purpose
**Legacy Windows Forms application** for envelope creation, management, and signature field placement. Built with **DevExpress components** and **GdPicture14** for PDF manipulation. This application is being **migrated to ReceiverUI + API** architecture.
**Primary Libraries:** DevExpress XtraGrid/XtraEditors, GdPicture14, VB.NET (.NET Framework 4.6.2)
---
## Application Architecture
### Projects Structure
| Project | Purpose |
|---|---|
| **EnvelopeGenerator.Form** | Main WinForms UI (VB.NET) |
| **EnvelopeGenerator.CommonServices** | Shared services/helpers |
| **EnvelopeGenerator.Service** | Windows Service (legacy) |
| **EnvelopeGenerator.BBTests** | Tests |
### Key Forms (Pages)
#### 1. **frmMain.vb** - Envelope Overview (Dashboard)
**Route Equivalent:** `/sender` (EnvelopeSenderPage.razor)
**Purpose:** Main dashboard showing all envelopes with status filtering.
**Key Features:**
- **Tab-based layout** with 2+2 tabs:
- **Tab 0:** Active Envelopes (not sent/partially signed)
- **Tab 1:** Completed Envelopes (signed/rejected/deleted)
- **Tab 2:** Reports (Admin only)
- **Tab 3:** Additional Reports (Admin only)
**Envelope Status Colors:**
| Status | Color | Hex Code | Description |
|---|---|---|---|
| `EnvelopePartlySigned` | Green | `#81C784` (GREEN_300) | At least one receiver signed |
| `EnvelopeQueued` / `EnvelopeSent` | Orange | `#FFB74D` (ORANGE_300) | Sent to receivers, awaiting signatures |
| `EnvelopeCompletelySigned` | Green | `#81C784` (GREEN_300) | All receivers signed |
| `EnvelopeDeleted` / `EnvelopeWithdrawn` / `EnvelopeRejected` | Red | `#E57373` (RED_300) | Envelope cancelled/rejected |
**Receiver Status Colors (in detail grids):**
| Status | Color | Hex Code |
|---|---|---|
| `Signed` | Green | `#81C784` (GREEN_300) |
| `Not Signed` | Red | `#E57373` (RED_300) |
**Grid Layouts:**
1. **GridEnvelopes** (Active):
- Columns: ID, Title, Status, Created Date, Creator
- **Master-Detail:** Expands to show:
- **ViewReceivers:** Receiver list with status colors
- **ViewHistory:** Envelope history (status changes, emails sent)
2. **GridCompleted** (Completed):
- Same structure as GridEnvelopes
- **ViewReceiversCompleted** and **ViewHistoryCompleted** detail grids
**Toolbar Actions:**
| Button | Action | Enabled When |
|---|---|---|
| **Create Envelope** | Opens `frmEnvelopeEditor` | Always |
| **Edit Envelope** | Opens `frmEnvelopeEditor` with selected envelope | Envelope selected & not sent |
| **Delete Envelope** | Shows `frmRueckruf` (reason dialog) ? deletes | Envelope selected |
| **Show Document** | Opens PDF in temp folder | Envelope & document selected |
| **Contact Receiver** | Opens mailto link | Receiver selected |
| **Resend Invitation** | Resends email to receiver | Receiver selected |
| **Send Access Code** | Manually sends access code email | Receiver selected & UseAccessCode=true |
| **Info Mail** | Opens mailto to support team | Receiver selected (for issues) |
| **2FA Properties** | Shows `frm2Factor_Properties` dialog | Receiver selected & TFA enabled |
| **Export Report (EB)** | Exports completed envelope result PDF | Completed envelope selected |
| **Refresh** | Reloads data | Always |
| **Export to Excel** | Exports current grid to XLSX | Always |
**Auto-Refresh:** Timer refreshes every N seconds (configurable), but **only when `frmEnvelopeEditor` is not open**.
**Persistence:** Grid layouts saved to `{GridViewName}_UserLayout.xml` in user AppData folder.
---
#### 2. **frmEnvelopeEditor.vb** - Envelope Creation/Edit
**Route Equivalent:** `/sender/envelope/{id}` (future)
**Purpose:** Create or edit envelope details, add documents, manage receivers.
**Workflow:**
1. **On New Envelope:** Opens `frmEnvelopeMainData` popup **first** (title, type, settings)
2. **After popup OK:** Main editor loads
3. **Add Document:** Single PDF file (via Open Dialog or Drag & Drop)
4. **Add Receivers:** Grid with auto-complete from previous emails
5. **Edit Fields:** Opens `frmFieldEditor` for signature field placement
6. **Send Envelope:** Validates and sends invitations
**UI Layout:**
- **Top Ribbon:** DevExpress ribbon with actions
- **Left Panel:** Document list (thumbnail + details)
- **Right Panel:** Receiver list (name, email, access code, phone)
- **Bottom Bar:** Envelope ID, Creator Email, Info messages
**Ribbon Actions:**
| Button | Action | Enabled When |
|---|---|---|
| **Add File** | Opens file dialog | No document added |
| **Merge Files** | Opens `frmOrderFiles` to concatenate PDFs | No document added |
| **Delete File** | Removes document | Document selected |
| **Show File** | Opens PDF in default viewer | Document selected |
| **Edit Fields** | Opens `frmFieldEditor` | Document + receivers exist |
| **Edit Data** | Opens `frmEnvelopeMainData` | Always |
| **Save** | Saves envelope without validation | Always |
| **Send Envelope** | Validates & sends invitations | Document + receivers exist |
| **Cancel** | Closes with save prompt | Always |
| **Delete Receiver** | Removes receiver from list | Receiver selected |
**Receiver Grid:**
- **Columns:** Name, Email, Access Code, Phone (if TFA enabled)
- **Auto-complete:** Email field suggests previous receivers
- **Validation:** Email format, Phone format (+49... for TFA)
- **Access Code:** Auto-generated on email entry (if UseAccessCode=true)
- **Color Assignment:** Each receiver gets a unique color (for signature fields)
**Document Grid:**
- **Single file only** (currently limited to 1 PDF)
- **Thumbnail:** Generated via GdPicture14
- **Page Count:** Displayed
**Drag & Drop:**
- Drop PDF files directly onto form
- Only 1 file allowed
- Visual feedback (red bar if >1 file)
**Validation:**
- Title required
- Message required
- At least 1 document
- At least 1 receiver
- Valid email addresses
- Valid phone numbers (if TFA enabled)
- Signature fields exist for each receiver (before sending)
---
#### 3. **frmEnvelopeMainData.vb** - Envelope Settings Popup
**Route Equivalent:** Part of `/sender/envelope/{id}` (inline in ReceiverUI)
**Purpose:** Configure envelope metadata and behavior. **Shown as modal popup** before main editor.
**Fields:**
| Field | Type | Description |
|---|---|---|
| **Title** | Text | Envelope title (required) |
| **Envelope Type** | Dropdown | Template (ContractType, Language, defaults) |
| **Language** | Dropdown | de/en |
| **Use Access Code** | Checkbox | Require 6-digit code for signing |
| **2FA Enabled** | Checkbox | Require SMS verification (forces Access Code ON) |
| **Certification Type** | Dropdown | Basic / Advanced / Qualified |
| **Final Email to Creator** | Dropdown | No / OnComplete / OnCompleteOrReject |
| **Final Email to Receivers** | Dropdown | No / OnComplete / OnCompleteOrReject |
| **Send Reminder Emails** | Checkbox | Auto-send reminders |
| **First Reminder Days** | Number | Days before first reminder |
| **Reminder Interval Days** | Number | Days between reminders |
| **Expires When Days** | Number | Days until envelope expires |
| **Expires Warning Days** | Number | Days before expiry to warn |
**Layout:**
- **Compact mode:** Only Title, Type, Language visible
- **Expanded mode:** Click "All Options" to show reminder/expiry settings
**Behavior:**
- **On Type Selection:** Auto-fills all settings from template
- **On 2FA Enable:** Forces "Use Access Code" ON and disables checkbox
- **On New Envelope:** Shows before main editor
- **On Edit Envelope:** Opens as separate dialog, Type field read-only
**Validation:** Title is required (red border via Adorner).
---
#### 4. **frmFieldEditor.vb** - Signature Field Placement
**Route Equivalent:** `/sender/envelope/{id}/fields` (future, or inline in editor)
**Purpose:** Place signature fields on PDF pages using **GdPicture14** viewer with annotations.
**UI Layout:**
- **Left:** PDF Viewer (GdViewer) with annotation tools
- **Right:** Thumbnail navigator (ThumbnailEx2)
- **Top:** Toolbar with receiver selector and actions
**Toolbar:**
| Button | Action |
|---|---|
| **Receiver Selector** | Popup menu with receiver names (colored circles) |
| **Add Signature** | Draws new signature annotation |
| **Delete** | Removes selected annotation |
| **Save** | Saves signature fields to database |
**Signature Field Details:**
- **Size:** 1.77" × 1.96" (4.5cm × 5cm) - **FIXED SIZE**
- **Color:** Matches receiver color (from grid assignment)
- **Label:** "SIGNATUR" (or localized "Signature")
- **Position:** Draggable on PDF canvas
- **Database Format:** Coordinates stored in **INCHES** (GdPicture native)
**Coordinate System:**
- **Origin:** Top-left corner
- **Units:** Inches (not points or pixels)
- **Axes:** X right, Y down
- **Storage:** Direct INCHES values (no conversion)
**Evidence from Code:**
```vb
Private Const SIGNATURE_WIDTH As Single = 1.77 ' inches
Private Const SIGNATURE_HEIGHT As Single = 1.96 ' inches
Sub LoadAnnotation(pElement As DocReceiverElement, ...)
oAnnotation.Left = CSng(pElement.X) ' Direct INCHES assignment
oAnnotation.Top = CSng(pElement.Y)
End Sub
```
**Multi-Receiver Support:**
- **Receiver switcher:** Click receiver name in popup menu
- **Current receiver:** Highlighted in toolbar (name + colored circle)
- **Other receivers' fields:** Shown semi-transparent (30% opacity), not selectable
- **Save:** Saves current receiver's fields, switches receiver, reloads all annotations
**Annotation Behavior:**
- **New annotation:** User clicks "Add Signature" ? draws interactive annotation ? auto-sized to 1.77×1.96
- **Existing annotation:** Loaded from database, locked size (can move but not resize)
- **Styling:** Filled rectangle with centered text "SIGNATUR"
- **Validation:** No resize, no text edit, no rotation
**Unsaved Changes Prompt:**
- On form close: "There are unsaved changes. Save? Yes/No/Cancel"
- Yes ? Saves ? Closes
- No ? Discards ? Closes
- Cancel ? Stays open
---
#### 5. **frmRueckruf.vb** - Delete Reason Dialog
**Route Equivalent:** Inline confirmation in ReceiverUI
**Purpose:** Capture reason for envelope deletion/withdrawal.
**Fields:**
- **Envelope ID** (display only)
- **Envelope Title** (display only)
- **Reason** (required text box)
**Buttons:**
- **OK:** Confirms deletion with reason
- **Cancel:** Cancels operation
**Global Variables:**
```vb
Public CurrentEnvelopID As Long
Public CurrentEnvelopetitle As String
Public Shared Continue_Reject As Boolean = False
Public Shared Reject_reason As String = ""
```
---
#### 6. **frm2Factor_Properties.vb** - 2FA Management
**Route Equivalent:** `/admin/2fa/{email}` (future admin panel)
**Purpose:** View/manage 2FA settings for a receiver.
**Fields:**
- **Email Address** (display only)
- **TOTP Secret Key** (display only)
- **Registration Deadline** (display only)
**Usage:** Admin tool to debug 2FA issues or reset TOTP secrets.
---
#### 7. **frmOrderFiles.vb** - PDF Merge Tool
**Route Equivalent:** Inline in ReceiverUI (future)
**Purpose:** Select multiple PDFs and merge them into a single document.
**UI:**
- **File list:** Selected PDFs
- **Up/Down buttons:** Reorder files
- **Add/Remove buttons:** Manage list
**Merge Logic:**
- Uses GdPicture14 `MergeDocuments()`
- Output: Single PDF in temp folder
- Auto-loaded as envelope document
---
## Data Models (Controllers)
### EnvelopeListController
**Purpose:** Load envelope lists for dashboard grids.
**Methods:**
- `ListEnvelopes()` ? Active envelopes (not completed)
- `ListCompleted()` ? Completed envelopes (signed/rejected/deleted)
- `DeleteEnvelope(envelope, reason)` ? Soft delete with reason
- `GetPieChart()` ? DevExpress ChartControl (not used)
- `GetEnvelopeReceiverAddresses(userId)` ? List of previous receiver emails (for auto-complete)
---
### EnvelopeEditorController
**Purpose:** Manage envelope CRUD and document/receiver operations.
**Key Methods:**
| Method | Purpose |
|---|---|
| `CreateDocument(filePath)` | Imports PDF, extracts pages/thumbnail, saves to DB |
| `DeleteDocument(document)` | Removes document from envelope |
| `SaveReceivers(envelope, receivers)` | Saves/updates receivers with access codes |
| `DeleteReceiver(receiver)` | Removes receiver (checks if fields exist) |
| `ElementsExist(receiverId)` | Checks if signature fields exist for receiver |
| `SaveEnvelope()` | Persists envelope to database |
| `SendEnvelope()` | Validates, queues emails, sends invitations |
| `ValidateEnvelopeForSending(errors)` | Checks all receivers have signature fields |
| `GetLastNameByEmailAdress(email)` | Returns previous name for email (auto-fill) |
| `GetLastPhoneByEmailAdress(email)` | Returns previous phone for email (auto-fill) |
| `DeleteEnvelopeFromDisk(envelope)` | Cleans up temp files |
**ActionService Methods:**
- `ResendReceiver(envelope, receiver)` ? Resends invitation email
- `ManuallySendAccessCode(envelope, receiver)` ? Sends access code email
---
### FieldEditorController
**Purpose:** Manage signature field annotations (DocReceiverElement).
**Key Methods:**
| Method | Purpose |
|---|---|
| `LoadElements()` | Loads all signature fields from database |
| `AddOrUpdateElement(annotation, orientation)` | Converts GdPicture annotation ? DocReceiverElement |
| `SaveElements(receiverId)` | Persists signature fields for receiver |
| `DeleteElement(element)` | Removes signature field |
| `GetElement(annotation)` | Finds element by annotation tag |
| `GetElementInfo(tag)` | Parses annotation tag (receiverId\|page\|guid) |
**Annotation Tag Format:**
```
"{receiverId}|{page}|{elementId}"
Example: "42|1|-1" (receiver 42, page 1, unsaved)
Example: "42|1|137" (receiver 42, page 1, element ID 137)
```
**Element Coordinate Conversion:**
```vb
' Database stores INCHES directly (GdPicture native)
Sub AddOrUpdateElement(annotation, orientation)
element.X = annotation.Left ' INCHES
element.Y = annotation.Top ' INCHES
element.Width = annotation.Width ' INCHES
element.Height = annotation.Height ' INCHES
element.Page = currentPage
End Sub
```
---
## Migration to ReceiverUI + API
### Mapping Table
| Form Feature | ReceiverUI Equivalent | Status |
|---|---|---|
| **frmMain** (Envelope list) | `/sender` (EnvelopeSenderPage.razor) | ? Exists |
| Tab 0: Active Envelopes | `/sender` default view | ? Grid with filters |
| Tab 1: Completed Envelopes | `/sender` with status filter | ? Same grid |
| Status colors (Green/Orange/Red) | CSS classes `.status-signed`, `.status-pending`, etc. | ?? **Needs implementation** |
| Master-detail grids (Receivers/History) | Expandable rows or modal dialogs | ?? **Needs implementation** |
| Toolbar: Create Envelope | Button ? `/sender/envelope/new` | ?? **Needs implementation** |
| Toolbar: Edit Envelope | Button ? `/sender/envelope/{id}` | ?? **Needs implementation** |
| Toolbar: Delete Envelope | Button ? Shows delete reason modal | ?? **Needs implementation** |
| Toolbar: Show Document | Downloads PDF | ?? **Needs implementation** |
| Toolbar: Contact Receiver | `mailto:` link | ?? **Needs implementation** |
| Toolbar: Resend Invitation | API call ? refresh grid | ?? **Needs implementation** |
| Toolbar: Export Excel | Download XLSX | ?? **Needs implementation** |
| Auto-refresh timer | SignalR or polling | ?? **Needs implementation** |
| Grid layout persistence | LocalStorage | ?? **Needs implementation** |
| **frmEnvelopeEditor** | `/sender/envelope/{id}` | ? **Not implemented** |
| ? **frmEnvelopeMainData** popup | Inline form section or stepper | ? **Not implemented** |
| Document upload (Drag & Drop) | Blazor InputFile with drag zone | ?? **Needs implementation** |
| Receiver grid (auto-complete) | DevExpress Blazor Grid with lookup | ?? **Needs implementation** |
| Merge PDFs (frmOrderFiles) | Client-side PDF.js or server-side | ?? **Needs implementation** |
| **frmFieldEditor** | `/sender/envelope/{id}/fields` or inline | ? **Not implemented** |
| GdPicture PDF viewer | PDF.js + canvas overlay (like EnvelopeReceiverPage) | ?? **Partial (receiver side only)** |
| Signature field placement | Drag & drop signature boxes on canvas | ? **Not implemented** |
| Receiver color coding | CSS variables + signature field borders | ? **Not implemented** |
| Multi-receiver switcher | Dropdown or tabs | ? **Not implemented** |
| Thumbnail navigator | PDF.js thumbnail sidebar (like receiver) | ?? **Exists for receiver** |
| Save signature fields | API POST `/api/Envelope/{id}/Elements` | ? **Not implemented** |
| **frm2Factor_Properties** | Admin panel `/admin/2fa/{email}` | ? **Not implemented** |
| **frmRueckruf** (Delete reason) | Modal dialog in EnvelopeSenderPage | ?? **Needs implementation** |
---
## Critical Implementation Notes
### 1. **Coordinate System Consistency**
**Database (Form App):** INCHES (GdPicture native)
**ReceiverUI (PDF.js):** Pixels on canvas
**Conversion Required:**
```csharp
// Sender side (placing fields):
float xInches = canvasPixelX / (canvasWidth / pageWidthInches);
float yInches = canvasPixelY / (canvasHeight / pageHeightInches);
// Receiver side (displaying fields):
float canvasX = (xInches / pageWidthInches) * canvasWidth;
float canvasY = (yInches / pageHeightInches) * canvasHeight;
```
**A4 Page Dimensions:**
- Width: 8.27" = 595pt
- Height: 11.69" = 842pt
---
### 2. **Status Color System**
Form app uses **DevExpress CustomDrawCell** event for row coloring. ReceiverUI should use:
```css
/* Envelope status colors */
.envelope-row.status-partly-signed { background-color: #81C784; } /* GREEN_300 */
.envelope-row.status-queued,
.envelope-row.status-sent { background-color: #FFB74D; } /* ORANGE_300 */
.envelope-row.status-completed { background-color: #81C784; } /* GREEN_300 */
.envelope-row.status-deleted,
.envelope-row.status-rejected { background-color: #E57373; } /* RED_300 */
/* Receiver status colors */
.receiver-row.signed { background-color: #81C784; }
.receiver-row.unsigned { background-color: #E57373; }
```
---
### 3. **Master-Detail Grid Pattern**
Form app uses **nested GridViews** (ViewReceivers, ViewHistory). ReceiverUI options:
**Option A:** DevExpress Blazor Grid with master-detail template
```razor
<DxGrid Data="@Envelopes">
<DxGridDataColumn FieldName="Title" />
<DxGridDetailRowTemplate>
<DxGrid Data="@context.EnvelopeReceivers">
<DxGridDataColumn FieldName="Name" />
<DxGridDataColumn FieldName="Status" />
</DxGrid>
</DxGridDetailRowTemplate>
</DxGrid>
```
**Option B:** Click row ? show modal with receivers/history
```razor
<DxGrid @ref="grid" Data="@Envelopes" RowClick="OnRowClick">
...
</DxGrid>
<DxPopup @bind-Visible="showDetailPopup">
<DxGrid Data="@selectedEnvelope.EnvelopeReceivers">
...
</DxGrid>
</DxPopup>
```
---
### 4. **Signature Field Editor Architecture**
**Form App:**
- GdPicture14 native annotations
- Fixed size (1.77×1.96 inches)
- Color-coded per receiver
- Draggable, non-resizable
**ReceiverUI Equivalent:**
- PDF.js canvas + HTML overlay (like receiver signature buttons)
- `<div class="signature-field-placeholder">` positioned absolutely
- Drag & drop with JS (`onmousedown`, `onmousemove`, `onmouseup`)
- Snap to grid (optional)
- Store coordinates in INCHES (convert from pixels)
**Example HTML Overlay:**
```html
<div class="pdf-signature-layer">
<div class="signature-field"
data-receiver-id="42"
data-page="1"
style="position: absolute; left: 200px; top: 300px; width: 177px; height: 196px; background: rgba(255,0,0,0.3); border: 2px dashed red;">
Receiver: John Doe
</div>
</div>
```
**JS Dragging Logic:**
```javascript
function makeSignatureFieldDraggable(element) {
let offsetX, offsetY;
element.onmousedown = (e) => {
offsetX = e.clientX - element.offsetLeft;
offsetY = e.clientY - element.offsetTop;
document.onmousemove = (e) => {
element.style.left = (e.clientX - offsetX) + 'px';
element.style.top = (e.clientY - offsetY) + 'px';
};
document.onmouseup = () => {
document.onmousemove = null;
saveFieldPosition(element); // Convert px ? inches ? API
};
};
}
```
---
### 5. **Auto-Complete Receiver Email**
Form app uses **DevExpress ComboBox** with `AllReceiverEmails` list. ReceiverUI options:
**Option A:** DevExpress Blazor TagBox with remote data
```razor
<DxTagBox Data="@previousEmails"
@bind-Values="@selectedEmails"
AllowCustomTags="true"
ClearButtonDisplayMode="Auto">
</DxTagBox>
```
**Option B:** HTML5 datalist
```razor
<input list="receiver-emails" @bind="newReceiverEmail" />
<datalist id="receiver-emails">
@foreach (var email in previousEmails) {
<option value="@email" />
}
</datalist>
```
---
### 6. **Drag & Drop File Upload**
Form app uses **WinForms DragDrop** events. ReceiverUI equivalent:
```razor
<div class="file-drop-zone"
@ondragover="HandleDragOver"
@ondragover:preventDefault
@ondrop="HandleDrop"
@ondrop:preventDefault>
<p>Drop PDF here or click to browse</p>
<InputFile OnChange="HandleFileSelected" accept=".pdf" />
</div>
@code {
async Task HandleDrop(DragEventArgs e) {
var files = e.DataTransfer.Files;
if (files.Length > 1) {
errorMessage = "Only one file allowed";
return;
}
await UploadDocument(files[0]);
}
}
```
---
### 7. **PDF Merge (frmOrderFiles)**
Form app uses **GdPicture14 MergeDocuments**. ReceiverUI options:
**Option A:** Server-side merge (iText7 or PDFSharp)
```csharp
[HttpPost("api/Document/Merge")]
public async Task<IActionResult> MergePDFs([FromForm] List<IFormFile> files) {
using var outputStream = new MemoryStream();
using var pdfDocument = new PdfDocument(new PdfWriter(outputStream));
foreach (var file in files) {
using var inputStream = file.OpenReadStream();
using var sourcePdf = new PdfDocument(new PdfReader(inputStream));
sourcePdf.CopyPagesTo(1, sourcePdf.GetNumberOfPages(), pdfDocument);
}
pdfDocument.Close();
return File(outputStream.ToArray(), "application/pdf", "merged.pdf");
}
```
**Option B:** Client-side merge (PDF-lib.js in Blazor JS interop)
```javascript
async function mergePDFs(fileDataUrls) {
const pdfDoc = await PDFDocument.create();
for (const dataUrl of fileDataUrls) {
const existingPdfBytes = await fetch(dataUrl).then(res => res.arrayBuffer());
const pdf = await PDFDocument.load(existingPdfBytes);
const copiedPages = await pdfDoc.copyPages(pdf, pdf.getPageIndices());
copiedPages.forEach(page => pdfDoc.addPage(page));
}
const pdfBytes = await pdfDoc.save();
return pdfBytes;
}
```
---
## Form App Workflow Summary
### New Envelope Workflow (Sender)
```
1. Click "Create Envelope" in frmMain
?
2. frmEnvelopeMainData popup opens
- Enter Title (required)
- Select Envelope Type (loads defaults)
- Configure settings (Access Code, 2FA, Language, etc.)
- Click OK
?
3. frmEnvelopeEditor opens
- Add PDF document (drag & drop or browse)
- Add receivers (email auto-complete, access code auto-gen)
- Click "Edit Fields"
?
4. frmFieldEditor opens
- Select receiver from dropdown (colored circles)
- Click "Add Signature" ? draw box on PDF
- Drag to position (1.77×1.96 inches, fixed size)
- Repeat for each receiver
- Click "Save"
?
5. Back to frmEnvelopeEditor
- Click "Send Envelope"
- Validation:
* Title exists
* Message exists
* Document exists
* Receivers exist
* All receivers have signature fields
- Confirmation: "Do you want to start the signature process now?"
- Click Yes ? Envelope status = EnvelopeSent
?
6. Emails sent to receivers with invitation links
?
7. frmMain refreshes, envelope moves to "Sent" (orange color)
```
### Receiver Signing Workflow (External)
```
1. Receiver clicks link in email
?
2. Web browser opens (EnvelopeGenerator.Web or ReceiverUI)
- If UseAccessCode=true: Enter 6-digit code
- If TFA=true: Enter SMS code
?
3. PDF viewer loads with signature fields highlighted
- Click "Sign" button on each field
- Draw/type/upload signature
- Enter name, position, place
?
4. Submit ? Backend stamps signature on PDF
?
5. Envelope status updates:
- First signature ? EnvelopePartlySigned (green)
- Last signature ? EnvelopeCompletelySigned (green)
?
6. Final emails sent (if configured):
- To creator: "All receivers signed"
- To receivers: "Envelope completed, download PDF"
```
---
## Missing Features in ReceiverUI (To Implement)
### High Priority
1. ? **Envelope list grid** (`/sender`) with status colors
2. ? **Master-detail grids** (receivers/history)
3. ? **Create/Edit envelope form** (`/sender/envelope/{id}`)
4. ? **Envelope settings popup/stepper** (title, type, options)
5. ? **Document upload** (drag & drop, single PDF)
6. ? **Receiver management** (add/edit/delete with auto-complete)
7. ? **Signature field editor** (PDF.js + draggable overlays)
8. ? **Send envelope** (validation + API call)
### Medium Priority
9. ? **Delete envelope** (with reason modal)
10. ? **Resend invitation** (per receiver)
11. ? **Show document** (PDF preview)
12. ? **Contact receiver** (mailto link)
13. ? **Export to Excel** (grid data)
14. ? **PDF merge tool** (multi-file select + merge)
15. ? **Grid layout persistence** (LocalStorage)
### Low Priority
16. ? **2FA management** (admin panel)
17. ? **Reports tab** (statistics, admin only)
18. ? **Auto-refresh** (SignalR or polling)
19. ? **Ghost mode** (impersonate user, admin only)
20. ? **Log file export** (debug tool)
---
## Data Flow Diagram
```
????????????????????????
? frmMain ? ? Envelope list (Active/Completed tabs)
? (Dashboard) ? ? Status colors (Green/Orange/Red)
???????????????????????? ? Master-detail grids (Receivers/History)
? ? Toolbar actions (Create/Edit/Delete/Send)
? Click "Create"
?
????????????????????????
? frmEnvelopeMainData ? ? Popup: Title, Type, Settings
? (Settings Popup) ? ? Dropdowns: Language, Certification, Final Emails
???????????????????????? ? Checkboxes: Access Code, 2FA, Reminders
? Click OK
?
????????????????????????
? frmEnvelopeEditor ? ? Document upload (drag & drop)
? (Main Editor) ? ? Receiver grid (email auto-complete)
???????????????????????? ? Merge PDFs button
? Click "Edit Fields"
?
????????????????????????
? frmFieldEditor ? ? GdPicture PDF viewer
? (Signature Placer) ? ? Receiver selector (colored circles)
???????????????????????? ? Draggable signature boxes (1.77×1.96")
? Click "Save"
?
????????????????????????
? Database ? ? DocReceiverElement (X, Y, Width, Height in INCHES)
? (TBSIG_ELEMENT) ? ? EnvelopeReceiver (Name, Email, Access Code, Phone)
???????????????????????? ? Envelope (Title, Status, Settings)
? Click "Send Envelope"
?
????????????????????????
? Email Service ? ? Send invitation emails to receivers
? ? ? Template: Link + Access Code (if enabled)
????????????????????????
?
?
????????????????????????
? Receiver Web Page ? ? PDF.js viewer with signature buttons
? (EnvelopeReceiverPage)? ? Click button ? Signature popup (draw/type/image)
???????????????????????? ? Submit ? API stamps signature on PDF
?
?
????????????????????????
? Database Update ? ? EnvelopeHistory (Signed, Timestamp)
? ? ? Envelope Status ? PartlySigned / CompletelySigned
????????????????????????
? All signed?
?
????????????????????????
? Final Email Service ? ? Send "Completed" email to creator/receivers
? ? ? Attach signed PDF
????????????????????????
```
---
## Key Takeaways for Migration
1. **Tab-based layout** ? Single grid with status filter dropdown
2. **Status colors** ? CSS classes or inline styles
3. **Master-detail grids** ? Expandable rows or modal dialogs
4. **Popup settings form** ? Inline form or stepper UI
5. **GdPicture PDF viewer** ? PDF.js with canvas overlay
6. **Signature field dragging** ? HTML5 drag & drop + absolute positioning
7. **Coordinate system** ? **ALWAYS INCHES** in database, convert to/from pixels for UI
8. **Receiver colors** ? CSS variables or inline styles
9. **Auto-complete emails** ? DevExpress TagBox or HTML5 datalist
10. **PDF merge** ? Server-side (iText7) or client-side (PDF-lib.js)
---
**Last Updated:** Session 20 (Form Application Analysis)