Compare commits
34 Commits
a49dd0ff81
...
88b196ed6d
| Author | SHA1 | Date | |
|---|---|---|---|
| 88b196ed6d | |||
| c99511de29 | |||
| 7d0c5a0ee5 | |||
| 7001d7351f | |||
| cf16312394 | |||
| ecb7f45f14 | |||
| 0ba5578d94 | |||
| e093471a24 | |||
| b16ae70762 | |||
| c3e8f09291 | |||
| a9fb82bbea | |||
| 895fd5c509 | |||
| 3b4278d7e0 | |||
| 6d1fb05e10 | |||
| 26da78fa22 | |||
| 9eee2b523d | |||
| 9dc2b9adef | |||
| cc3c5ec9f0 | |||
| 2766d963af | |||
| 8fd9928524 | |||
| fb3ee14f8f | |||
| e3929a99e3 | |||
| b6d86aa3eb | |||
| 4171a3138b | |||
| e98e18cfe0 | |||
| 14aff03de4 | |||
| d828a5bfe2 | |||
| a6e174e7c1 | |||
| fc7aa83513 | |||
| 90661cb856 | |||
| 04e30b0d79 | |||
| d0a50f63db | |||
| 9d20ba1987 | |||
| 66f7b6f5e1 |
424
COPILOT_CONTEXT.md
Normal file
424
COPILOT_CONTEXT.md
Normal 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
17
EnvelopeGenerator.API/AuthScheme.cs
Normal file
17
EnvelopeGenerator.API/AuthScheme.cs
Normal 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";
|
||||
}
|
||||
@@ -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)
|
||||
.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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
@page "/sender"
|
||||
|
||||
<h3>EnvelopeSender</h3>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
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();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@page "/login/{EnvelopeKey}"
|
||||
@page "/envelope/login/{EnvelopeKey}"
|
||||
@using EnvelopeGenerator.ReceiverUI.Services
|
||||
@inject AuthService AuthService
|
||||
@inject NavigationManager Navigation
|
||||
172
EnvelopeGenerator.ReceiverUI/Pages/LoginSenderPage.razor
Normal file
172
EnvelopeGenerator.ReceiverUI/Pages/LoginSenderPage.razor
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Api": {
|
||||
"BaseUrl": "",
|
||||
"ForceToUseFakeDocument": false
|
||||
"UsePredefinedReports": false
|
||||
},
|
||||
"PdfViewer": {
|
||||
"ThumbnailBaseScale": 0.75,
|
||||
|
||||
@@ -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
788
FORM_APPLICATION_CONTEXT.md
Normal 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)
|
||||
Reference in New Issue
Block a user