Compare commits
189 Commits
71e375d6ea
...
feat/remov
| Author | SHA1 | Date | |
|---|---|---|---|
| b46c3c8ebe | |||
| 9f712a6b8c | |||
| 4449743105 | |||
| 435be882c9 | |||
| 09257d7766 | |||
| 16fa4b460f | |||
| f4aae8c787 | |||
| ed1468194d | |||
| f9f94c900e | |||
| 18df6221c9 | |||
| b15f95755c | |||
| 4be9e38a9e | |||
| 11c8886d6f | |||
| 1ad673904a | |||
| 32a9d008a4 | |||
| 454dd0b6cc | |||
| aa2a31e993 | |||
| ecc509919c | |||
| f3751274b1 | |||
| adc231f10e | |||
| 999fac7dac | |||
| 86758bb127 | |||
| 498d59a4ce | |||
| ce430c1b80 | |||
| eb219291de | |||
| 80733ab1f7 | |||
| 3d9153da23 | |||
| a0bfd405f0 | |||
| ec3a15a467 | |||
| 80f08b393c | |||
| b22d57d7da | |||
| 01cd8b2607 | |||
| 310d2efc6e | |||
| e5402bafc9 | |||
| f517ab4cfb | |||
| 6d63f9db2b | |||
| 46c7c31e03 | |||
| 8f6db435bb | |||
| 9bf7676464 | |||
| d9556c18ca | |||
| 5b7157f422 | |||
| 119ee3ec16 | |||
| b536b31a4b | |||
| a80efee075 | |||
| 86d6daafbd | |||
| aefa1a8883 | |||
| b3e7f77165 | |||
| bac9bfd036 | |||
| 6effb8a0d6 | |||
| a9ff0d88f2 | |||
| 0fdfe991ea | |||
| 7fc2c14f9c | |||
| 5098deedd1 | |||
| 2e634361c5 | |||
| 60ebebc0d8 | |||
| c2d8b6531c | |||
| 665266457f | |||
| 14d581acac | |||
| eb533a1dc2 | |||
| 36025fad7b | |||
| 0606efabf9 | |||
| 2cb1e266f7 | |||
| 35c1374b9c | |||
| feaea1626b | |||
| babd96200a | |||
| f9cfcf1f19 | |||
| ffb9e07466 | |||
| 5384a08905 | |||
| dda5ff28e1 | |||
| 043349a47f | |||
| c36f7ccf98 | |||
| cf3e85bcd7 | |||
| 77379265c2 | |||
| 9b68c09b0b | |||
| cd6bf05352 | |||
| b2c205b160 | |||
| 33439f4a65 | |||
| b5cdf79799 | |||
| 72915ac001 | |||
| e4d7a0d8f3 | |||
| d5f9de06f3 | |||
| bbc129178f | |||
| 40c95100ff | |||
| 1b2731b4b2 | |||
| ae56902758 | |||
| e80059d34e | |||
| a2443032c5 | |||
| 3d2fe532e5 | |||
| 5e83aa26c9 | |||
| 2c789cd4c0 | |||
| 9ecfe08e2e | |||
| 74da6e37b0 | |||
| 10f65e583a | |||
| 0cdc8f1191 | |||
| 56ccab6377 | |||
| 4d069cdaa0 | |||
| bbbfa4de01 | |||
| 3ff3373b27 | |||
| 2af8815cf6 | |||
| ca24a96084 | |||
| db368b889a | |||
| 8f451b9c2c | |||
| 6120e6062e | |||
| cc2aea90ed | |||
| 762a9e8bca | |||
| 6ed4caea4f | |||
| d94821433a | |||
| 278b9964f1 | |||
| e6722803bb | |||
| 47bc7675c9 | |||
| 789e312316 | |||
| 2a9bbb3fe5 | |||
| bc34317720 | |||
| 76ff3e47e1 | |||
| 2d22bfcd06 | |||
| 185c783824 | |||
| b957b4b4bb | |||
| df154d83cc | |||
| 49ec9fbead | |||
| 01fc29f59e | |||
| 733b70cca2 | |||
| 8f4b751303 | |||
| a5e4f97397 | |||
| 6ca03a50eb | |||
| 96a84ba1a5 | |||
| ec0ea72890 | |||
| 7b912387e7 | |||
| 6c142eba08 | |||
| 489d2808a1 | |||
| 7466fd78f6 | |||
| e34b5ddbbe | |||
| b56f906848 | |||
| fe09c5c7ae | |||
| 0763d82f6e | |||
| 5a30bc050b | |||
| a4b218b9f3 | |||
| 67798b35da | |||
| b5bb2bbaae | |||
| 85a0736106 | |||
| de9c9da176 | |||
| f4571320ce | |||
| 2abfffdeba | |||
| bfd1a9d060 | |||
| 78ed49a077 | |||
| 6aa97adf84 | |||
| 7456babe0d | |||
| 6fe99d0cd0 | |||
| 45018d04b1 | |||
| b5af3e61ed | |||
| 314608f27f | |||
| ba9f233993 | |||
| 9d962708c4 | |||
| c93a056ca5 | |||
| a88a26c248 | |||
| 1e963ea215 | |||
| 02b857382c | |||
| ca4ec7cb6f | |||
| f2356b3ce4 | |||
| d61fe79613 | |||
| 714cb9555f | |||
| 315a022cb8 | |||
| 746635979b | |||
| 31548728cd | |||
| 06c8af2ed8 | |||
| 9f57baf2e5 | |||
| 73d793f0a0 | |||
| 65bb68feef | |||
| c5e97ee30b | |||
| 3a4f449b59 | |||
| 6ca7767e4d | |||
| 4237f0a815 | |||
| 3302be9348 | |||
| 4572e20c51 | |||
| b3a70d7259 | |||
| bb81920d44 | |||
| 3b66de0691 | |||
| 9f6004ba8c | |||
| ef246bae32 | |||
| e4ebb29969 | |||
| 83cdb9dfe9 | |||
| c5db676e01 | |||
| 95c8e15887 | |||
| 561b844e59 | |||
| 011960be75 | |||
| 151c785af9 | |||
| fa354a05cc | |||
| 1326407462 | |||
| a3c653ddb3 | |||
| 8d736cdc5e |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -365,3 +365,8 @@ FodyWeavers.xsd
|
|||||||
/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode
|
/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode
|
||||||
/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs
|
/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs
|
||||||
/EnvelopeGenerator.Tests.Application/annotations.json
|
/EnvelopeGenerator.Tests.Application/annotations.json
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/TekH - SoftHSM Test.md
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
||||||
|
/ignore/EnvelopeGenerator.Form
|
||||||
|
|||||||
107
AGENTS.md
107
AGENTS.md
@@ -4,19 +4,78 @@
|
|||||||
- **`COPILOT_CONTEXT.md`** - Architecture, coordinate systems, migration status
|
- **`COPILOT_CONTEXT.md`** - Architecture, coordinate systems, migration status
|
||||||
- **`FORM_APPLICATION_CONTEXT.md`** - Legacy VB.NET features to migrate
|
- **`FORM_APPLICATION_CONTEXT.md`** - Legacy VB.NET features to migrate
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation Catalog
|
||||||
|
|
||||||
|
This section explains every markdown file in the repo: what it contains, why it exists, and when you need it. Read this before deciding which file to open.
|
||||||
|
|
||||||
|
### Core Architecture Docs (Active — Always Relevant)
|
||||||
|
|
||||||
|
#### `AGENTS.md` ← you are here
|
||||||
|
**What:** Authoritative agent/copilot onboarding guide. Covers project structure, route table, render mode rules, coordinate system, API gaps, and common mistakes.
|
||||||
|
**Why it exists:** Single source of truth for agents so they don't re-derive architecture from code on every session.
|
||||||
|
**When you need it:** Always. Read first on every session.
|
||||||
|
**Maintenance:** Update whenever project structure, routes, render modes, or naming conventions change.
|
||||||
|
|
||||||
|
#### `COPILOT_CONTEXT.md`
|
||||||
|
**What:** Deep-dive architecture reference. Covers the full Blazor Auto hybrid setup, authentication flows (JWT + cookies, per-envelope receiver tokens), service registration patterns, coordinate system proof, and an explicit "prefer Server/Server.Client over old WebUI/ReceiverUI" rule.
|
||||||
|
**Why it exists:** Prevents agents from re-deriving or misinterpreting the hybrid SSR+WASM architecture. Also documents coordinate system evidence to prevent regressions (inches vs. points vs. pixels).
|
||||||
|
**When you need it:**
|
||||||
|
- Implementing any new page or service
|
||||||
|
- Debugging auth/cookie issues
|
||||||
|
- Any coordinate conversion question
|
||||||
|
- Unsure which project a new file should go in
|
||||||
|
**Maintenance:** Update when render mode assignments change, new auth schemes are added, or the coordinate system is touched.
|
||||||
|
|
||||||
|
#### `FORM_APPLICATION_CONTEXT.md`
|
||||||
|
**What:** Comprehensive reverse-engineering of the legacy VB.NET WinForms app (`EnvelopeGenerator.Form`). Documents every form (frmMain, frmEnvelopeEditor, frmFieldEditor, etc.), their UI layout, toolbar actions, data models, coordinate system, and a feature-by-feature migration mapping table to `EnvelopeGenerator.Server`.
|
||||||
|
**Why it exists:** The WinForms app is the functional specification for the sender-side web UI. It documents the full intended workflow that is being migrated to Blazor — especially the envelope creation, signature field placement, and dashboard features that are not yet implemented.
|
||||||
|
**When you need it:**
|
||||||
|
- Implementing any sender-side feature (`/sender`, `/sender/editor`, `/sender/envelope/{id}`)
|
||||||
|
- Understanding what a specific toolbar button or form should do
|
||||||
|
- Designing API endpoints for sender workflow
|
||||||
|
- Checking coordinate system behavior (INCHES proof is here)
|
||||||
|
**Maintenance:** Update the mapping table as features are implemented (change `❌ Not implemented` → `✅ Exists`). Do not rename `ReceiverUI` back — it was the old project name; current target is `EnvelopeGenerator.Server`.
|
||||||
|
|
||||||
|
#### `RECEIVER_PDF_VIEWER_CONTEXT.md`
|
||||||
|
**What:** Detailed technical reference for the receiver-side PDF viewer (`EnvelopeReceiverPage.razor`). Documents the PDF.js integration, signature button rendering, JS interop API (`window.pdfViewer`), signature capture flow, HiDPI/zoom/thumbnail config, and the full component lifecycle.
|
||||||
|
**Why it exists:** The receiver PDF viewer is the most complex component in the codebase. This doc prevents agents from misunderstanding the JS↔Blazor boundary, re-implementing already-solved problems (HiDPI, zoom, signature caching), or breaking the signing flow.
|
||||||
|
**When you need it:**
|
||||||
|
- Working on `EnvelopeReceiverPage.razor` or `pdf-viewer.js`
|
||||||
|
- Adding new signature field rendering logic
|
||||||
|
- Debugging signature rendering, zoom, or thumbnail issues
|
||||||
|
- Implementing sender-side PDF.js overlay (signature field placement) — the receiver side is the reference implementation
|
||||||
|
**Maintenance:** Update when `pdf-viewer.js` API changes, new JS interop functions are added, or the signing flow is modified.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Deployment Docs
|
||||||
|
|
||||||
|
#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/README.md`
|
||||||
|
**What:** IIS publish and deployment guide for `EnvelopeGenerator.Server`. Covers why self-contained publish is required (WASM assembly version strictness), the exact `dotnet publish` command, IIS Application Pool settings (`No Managed Code`), directory structure after publish, and common deployment errors.
|
||||||
|
**Why it exists:** Blazor Auto (Server + WASM hybrid) has non-obvious deployment requirements that differ from a standard ASP.NET Core app. Forgetting self-contained publish or using the wrong app pool will break WASM loading in production.
|
||||||
|
**When you need it:**
|
||||||
|
- Deploying to IIS for the first time
|
||||||
|
- Troubleshooting 500 errors on production after deploy
|
||||||
|
- Setting up a new deployment environment
|
||||||
|
**Maintenance:** Update if the publish target, IIS config, or runtime version changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Active Architecture (Post-Migration)
|
## Active Architecture (Post-Migration)
|
||||||
|
|
||||||
**Frontend:** Blazor Auto (Server+WASM hybrid)
|
**Frontend:** Blazor Auto (Server+WASM hybrid)
|
||||||
- **WebUI** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend
|
- **EnvelopeGenerator.Server** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend
|
||||||
- **WebUI.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic
|
- **EnvelopeGenerator.Server.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic
|
||||||
|
|
||||||
**Backend:** EnvelopeGenerator.API (ASP.NET Core 8.0)
|
**Backend:** EnvelopeGenerator.API (ASP.NET Core 8.0)
|
||||||
|
|
||||||
**Proxy:** YARP in WebUI routes `/api/*` → `localhost:8088` (API)
|
**Proxy:** YARP in EnvelopeGenerator.Server routes `/api/*` → `localhost:8088` (API)
|
||||||
|
|
||||||
### Deprecated Projects - DO NOT USE
|
### Deprecated Projects - DO NOT USE
|
||||||
- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to WebUI)
|
- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to Server)
|
||||||
- `EnvelopeGenerator.Web` - Razor Pages (replaced by WebUI)
|
- `EnvelopeGenerator.Web` - Razor Pages (replaced by Server)
|
||||||
- **VB.NET projects** (`Form`, `Service`, `BBTests`) - Legacy, read-only for reference
|
- **VB.NET projects** (`Form`, `Service`, `BBTests`) - Legacy, read-only for reference
|
||||||
|
|
||||||
## Development Commands
|
## Development Commands
|
||||||
@@ -28,11 +87,11 @@ cd EnvelopeGenerator.API
|
|||||||
dotnet run
|
dotnet run
|
||||||
|
|
||||||
# Terminal 2 - Blazor Frontend
|
# Terminal 2 - Blazor Frontend
|
||||||
cd EnvelopeGenerator.WebUI\EnvelopeGenerator.WebUI
|
cd EnvelopeGenerator.Server\EnvelopeGenerator.Server
|
||||||
dotnet run
|
dotnet run
|
||||||
```
|
```
|
||||||
|
|
||||||
**Critical:** Both must run simultaneously. WebUI proxy forwards `/api/*` to API.
|
**Critical:** Both must run simultaneously. EnvelopeGenerator.Server proxy forwards `/api/*` to API.
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
```powershell
|
```powershell
|
||||||
@@ -46,23 +105,25 @@ EnvelopeGenerator.Domain/ # Entities (Envelope, Receiver, Document, etc
|
|||||||
EnvelopeGenerator.Application/ # MediatR CQRS (Commands, Queries, Handlers)
|
EnvelopeGenerator.Application/ # MediatR CQRS (Commands, Queries, Handlers)
|
||||||
EnvelopeGenerator.Infrastructure/ # EF Core, SQL executors, repositories
|
EnvelopeGenerator.Infrastructure/ # EF Core, SQL executors, repositories
|
||||||
EnvelopeGenerator.API/ # Controllers, endpoints
|
EnvelopeGenerator.API/ # Controllers, endpoints
|
||||||
EnvelopeGenerator.WebUI/ # Server-side Blazor components
|
EnvelopeGenerator.Server/
|
||||||
├─ Components/Pages/ # @rendermode InteractiveServer
|
EnvelopeGenerator.Server/ # Server-side Blazor components
|
||||||
EnvelopeGenerator.WebUI.Client/ # Client-side WASM components
|
├─ Components/Pages/ # @rendermode InteractiveServer
|
||||||
├─ Pages/ # @rendermode InteractiveWebAssembly
|
EnvelopeGenerator.Server.Client/ # Client-side WASM components
|
||||||
├─ Services/ # HTTP API clients
|
├─ Pages/ # @rendermode InteractiveWebAssembly
|
||||||
├─ Models/ # DTOs
|
├─ Services/ # HTTP API clients
|
||||||
|
├─ Models/ # DTOs
|
||||||
```
|
```
|
||||||
|
|
||||||
## Route Structure (Critical)
|
## Route Structure (Critical)
|
||||||
|
|
||||||
| Route | File Location | Render Mode | Purpose |
|
| Route | File Location | Render Mode | Purpose |
|
||||||
|-------|--------------|-------------|---------|
|
|-------|--------------|-------------|---------|
|
||||||
| `/` | `WebUI.Client/Pages/Index.razor` | WASM | Landing page |
|
| `/` | `Server.Client/Pages/Index.razor` | WASM | Landing page |
|
||||||
| `/sender/login` | `WebUI.Client/Pages/LoginSenderPage.razor` | WASM | Sender auth |
|
| `/sender/login` | `Server.Client/Pages/LoginSenderPage.razor` | WASM | Sender auth |
|
||||||
| `/sender` | `WebUI.Client/Pages/EnvelopeSenderPage.razor` | WASM | Sender dashboard |
|
| `/sender` | `Server/Components/Pages/EnvelopeSenderPage.razor` | **Server** | Sender dashboard |
|
||||||
| `/envelope/login/{key}` | `WebUI.Client/Pages/LoginReceiverPage.razor` | WASM | Receiver auth |
|
| `/sender/editor` | `Server/Components/Pages/EnvelopeSenderEditorPage.razor` | **Server** | Envelope editor + PDF viewer |
|
||||||
| `/envelope/{key}` | `WebUI/Components/Pages/EnvelopeReceiverPage.razor` | **Server** | PDF viewer + signing |
|
| `/envelope/login/{key}` | `Server.Client/Pages/LoginReceiverPage.razor` | WASM | Receiver auth |
|
||||||
|
| `/envelope/{key}` | `Server/Components/Pages/EnvelopeReceiverPage.razor` | **Server** | PDF viewer + signing |
|
||||||
|
|
||||||
**Rule:** PDF viewers MUST use `@rendermode InteractiveServer` (DevExpress backend requirement). Everything else uses WASM.
|
**Rule:** PDF viewers MUST use `@rendermode InteractiveServer` (DevExpress backend requirement). Everything else uses WASM.
|
||||||
|
|
||||||
@@ -110,7 +171,7 @@ See `FORM_APPLICATION_CONTEXT.md` for detailed workflow requirements.
|
|||||||
|
|
||||||
## Status Color Coding
|
## Status Color Coding
|
||||||
|
|
||||||
Form app uses DevExpress `CustomDrawCell`. WebUI needs CSS:
|
Form app uses DevExpress `CustomDrawCell`. EnvelopeGenerator.Server needs CSS:
|
||||||
|
|
||||||
```css
|
```css
|
||||||
.envelope-row.status-partly-signed { background-color: #81C784; } /* GREEN_300 */
|
.envelope-row.status-partly-signed { background-color: #81C784; } /* GREEN_300 */
|
||||||
@@ -123,10 +184,10 @@ Form app uses DevExpress `CustomDrawCell`. WebUI needs CSS:
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### YARP Proxy (`WebUI/yarp.json`)
|
### YARP Proxy (`EnvelopeGenerator.Server/yarp.json`)
|
||||||
Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:8088`
|
Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:8088`
|
||||||
|
|
||||||
### PDF.js Settings (`WebUI/wwwroot/appsettings.json`)
|
### PDF.js Settings (`EnvelopeGenerator.Server/wwwroot/appsettings.json`)
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"PdfViewerOptions": {
|
"PdfViewerOptions": {
|
||||||
@@ -177,7 +238,7 @@ Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:
|
|||||||
|
|
||||||
Manual testing workflow:
|
Manual testing workflow:
|
||||||
1. Start API (`dotnet run` in `EnvelopeGenerator.API`)
|
1. Start API (`dotnet run` in `EnvelopeGenerator.API`)
|
||||||
2. Start WebUI (`dotnet run` in `EnvelopeGenerator.WebUI\EnvelopeGenerator.WebUI`)
|
2. Start EnvelopeGenerator.Server (`dotnet run` in `EnvelopeGenerator.Server\EnvelopeGenerator.Server`)
|
||||||
3. Navigate to `https://localhost:5131` (or check console output for port)
|
3. Navigate to `https://localhost:5131` (or check console output for port)
|
||||||
4. Test sender login at `/sender/login`
|
4. Test sender login at `/sender/login`
|
||||||
5. Test receiver flow at `/envelope/login/{envelopeKey}`
|
5. Test receiver flow at `/envelope/login/{envelopeKey}`
|
||||||
@@ -245,7 +306,7 @@ Each envelope maintains independent authentication state.
|
|||||||
**CDN:**
|
**CDN:**
|
||||||
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
||||||
|
|
||||||
**NuGet (WebUI.Client):**
|
**NuGet (Server.Client):**
|
||||||
- `DevExpress.Blazor.*` 25.2.3
|
- `DevExpress.Blazor.*` 25.2.3
|
||||||
- `SkiaSharp.*` 3.119.1 (WASM rendering)
|
- `SkiaSharp.*` 3.119.1 (WASM rendering)
|
||||||
|
|
||||||
|
|||||||
@@ -1,477 +1,351 @@
|
|||||||
# EnvelopeGenerator — AI Context Reference
|
# EnvelopeGenerator — Current Workspace Context
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
Digital document signing system with **unified Blazor Auto (Server+WASM hybrid) frontend** for both Senders and Receivers. Senders create envelopes and place signature fields. Receivers view PDFs, sign documents, export stamped PDFs.
|
Digital document signing system for senders and receivers.
|
||||||
|
|
||||||
**Primary Libraries:** DevExpress + PDF.js (PSPDFKit removed)
|
- Senders authenticate, view envelope lists, and manage envelope workflows.
|
||||||
|
- Receivers authenticate per envelope, open PDFs, create signatures, and apply them in the viewer.
|
||||||
|
- The active UI stack is `Blazor Auto` with server-side and WebAssembly render modes.
|
||||||
|
- Primary UI/PDF libraries are `DevExpress` and `PDF.js`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Migration Notice
|
## Active Application Structure
|
||||||
|
|
||||||
**EnvelopeGenerator.ReceiverUI ? EnvelopeGenerator.WebUI Migration**
|
### Main Host
|
||||||
|
**Primary active application:** `EnvelopeGenerator.Server`
|
||||||
|
|
||||||
The project has been migrated from pure Blazor WebAssembly (`ReceiverUI`) to **Blazor Auto (Server+WASM hybrid)** architecture (`WebUI`) to resolve DevExpress `DxPdfViewer` compatibility issues.
|
`EnvelopeGenerator.Server` is the current runtime host and contains:
|
||||||
|
- Blazor server host
|
||||||
|
- WebAssembly host integration
|
||||||
|
- API controllers
|
||||||
|
- authentication/authorization setup
|
||||||
|
- Swagger/Scalar setup
|
||||||
|
- YARP reverse proxy configuration
|
||||||
|
- DevExpress server-side services
|
||||||
|
- SQL Server distributed cache setup
|
||||||
|
|
||||||
**Reason:** DevExpress `DxPdfViewer` requires backend server-side rendering services that are NOT available in pure WebAssembly projects.
|
### Client Project
|
||||||
|
**Client UI project:** `EnvelopeGenerator.Server.Client`
|
||||||
|
|
||||||
**New Structure:**
|
This project contains:
|
||||||
- **WebUI** (Server project): Hosts server-side components, YARP proxy, DevExpress backend services
|
- WebAssembly-rendered pages
|
||||||
- **WebUI.Client** (WASM project): Client-side components, business logic, services
|
- client-side services
|
||||||
|
- client models and options
|
||||||
|
- sender and receiver login flows
|
||||||
|
|
||||||
**Migration Details:** See `MIGRATION_CONTEXT.md`
|
### Other Projects
|
||||||
|
- `EnvelopeGenerator.Application` — MediatR/CQRS handlers and business logic
|
||||||
|
- `EnvelopeGenerator.Domain` — domain models, constants, shared abstractions
|
||||||
|
- `EnvelopeGenerator.Infrastructure` — EF Core and infrastructure services
|
||||||
|
- `EnvelopeGenerator.PdfEditor` — PDF-related backend utilities
|
||||||
|
- `EnvelopeGenerator.API` — still exists in the solution, but the current merged app host is `EnvelopeGenerator.Server`
|
||||||
|
|
||||||
|
### Legacy / Do Not Touch
|
||||||
|
- `EnvelopeGenerator.Service`
|
||||||
|
- `EnvelopeGenerator.Form`
|
||||||
|
- `EnvelopeGenerator.BBTests`
|
||||||
|
- `EnvelopeGenerator.CommonServices`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deployment Architecture
|
## Current Hosting Model
|
||||||
|
|
||||||
**Two Presentation Projects (Both Required):**
|
`EnvelopeGenerator.Server/Program.cs` currently configures:
|
||||||
|
- `AddRazorComponents()` with both interactive server and interactive WebAssembly components
|
||||||
|
- `AddControllers()` and `MapControllers()`
|
||||||
|
- JWT authentication for sender and receiver flows
|
||||||
|
- cookie authentication
|
||||||
|
- authorization policies using `AuthScheme.Sender`, `AuthScheme.Receiver`, `AuthPolicy.Sender`, `AuthPolicy.Receiver`
|
||||||
|
- `AddReverseProxy()` with `yarp.json`
|
||||||
|
- Swagger / OpenAPI / Scalar
|
||||||
|
- distributed SQL Server cache
|
||||||
|
- DevExpress Blazor and DevExpress PDF Viewer server-side services
|
||||||
|
- request localization middleware
|
||||||
|
|
||||||
1. **EnvelopeGenerator.API** (ASP.NET Core Web API)
|
This means the active app is a **merged UI + API host**.
|
||||||
- Runs independently (development & production)
|
|
||||||
- Backend services for document management, authentication, signature endpoints
|
|
||||||
- Serves as API endpoint for WebUI
|
|
||||||
|
|
||||||
2. **EnvelopeGenerator.WebUI** (Blazor Auto - Server+WASM Hybrid)
|
|
||||||
- **Server Project (`EnvelopeGenerator.WebUI`):**
|
|
||||||
- **YARP Reverse Proxy** configured via `yarp.json`
|
|
||||||
- Proxies `/api/*` requests to `API:8088`
|
|
||||||
- Hosts server-side components (`@rendermode InteractiveServer`)
|
|
||||||
- DevExpress server-side services (DxPdfViewer backend)
|
|
||||||
- **Client Project (`EnvelopeGenerator.WebUI.Client`):**
|
|
||||||
- Client-side components (`@rendermode InteractiveWebAssembly`)
|
|
||||||
- Business logic services (AuthService, DocumentService, etc.)
|
|
||||||
- WASM runtime
|
|
||||||
|
|
||||||
**Request Flow:**
|
|
||||||
```
|
|
||||||
Client ? WebUI:XXXX (Blazor Auto)
|
|
||||||
?? Server-side Pages (DxPdfViewer)
|
|
||||||
?? Client-side Pages (WASM)
|
|
||||||
?? YARP Proxy: /api/* ? API:8088
|
|
||||||
```
|
|
||||||
|
|
||||||
**Configuration:** `EnvelopeGenerator.WebUI/yarp.json`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## WebUI Route Structure
|
## Reverse Proxy
|
||||||
|
|
||||||
### Root Route
|
**Config file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
|
||||||
| Route | File | Location | Render Mode |
|
|
||||||
|
Current YARP usage is focused on **AuthHub forwarding**, not a general `/api/* -> EnvelopeGenerator.API` proxy.
|
||||||
|
|
||||||
|
Configured routes forward:
|
||||||
|
- `POST /api/auth` -> AuthHub `/api/auth/sign-flow`
|
||||||
|
- `POST /api/Auth/envelope-receiver/{key}` -> AuthHub `/api/auth/envelope-receiver/{key}?cookie=true`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Active Routes and Files
|
||||||
|
|
||||||
|
### WebAssembly Pages (`EnvelopeGenerator.Server.Client`)
|
||||||
|
| Route | File | Render Mode | Purpose |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `/` | `Index.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
| `/` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/IndexPage.razor` | WebAssembly | Landing page |
|
||||||
|
| `/sender/login` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginSenderPage.razor` | WebAssembly | Sender login |
|
||||||
|
| `/sender` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor` | WebAssembly (`prerender: false`) | Sender dashboard |
|
||||||
|
| `/envelope/login/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor` | WebAssembly | Receiver login |
|
||||||
|
|
||||||
### Sender Routes
|
### Server Pages (`EnvelopeGenerator.Server`)
|
||||||
| Route | File | Location | Render Mode |
|
| Route | File | Render Mode | Purpose |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `/sender/login` | `LoginSenderPage.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
| `/envelope/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` | InteractiveServer | Main receiver PDF viewer and signing page |
|
||||||
| `/sender` | `EnvelopeSenderPage.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
| `/envelope/DxPdfViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor` | InteractiveServer | DevExpress PDF Viewer test page |
|
||||||
|
| `/envelope/{EnvelopeKey}/DxReportViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxReportViewer.razor` | InteractiveServer | DevExpress report-based PDF rendering |
|
||||||
### Receiver Routes (PDF Viewers)
|
| `/envelope/Embed` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_embed.razor` | InteractiveServer | Embedded browser PDF view test page |
|
||||||
| Route | File | Location | Render Mode |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `/envelope/login/{EnvelopeKey}` | `LoginReceiverPage.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
|
||||||
| `/envelope/{EnvelopeKey}` | `EnvelopeReceiverPage.razor` | `WebUI/Components/Pages/` | `@rendermode InteractiveServer` |
|
|
||||||
| `/envelope/DxPdfViewer` | `EnvelopeReceiverPage_DxPdfViewer.razor` | `WebUI/Components/Pages/` | `@rendermode InteractiveServer` |
|
|
||||||
| `/envelope/{EnvelopeKey}/DxReportViewer` | `EnvelopeReceiverPage_DxReportViewer.razor` | `WebUI/Components/Pages/` | `@rendermode InteractiveServer` |
|
|
||||||
| `/envelope/Embed` | `EnvelopeReceiverPage_embed.razor` | `WebUI/Components/Pages/` | `@rendermode InteractiveServer` |
|
|
||||||
|
|
||||||
**Multi-Envelope Support:** Receivers can login to multiple envelopes simultaneously (per-envelope cookie authentication).
|
|
||||||
|
|
||||||
**Render Mode Strategy:**
|
|
||||||
- **Client-side Pages (WASM):** Login, Sender dashboard, Index (no DevExpress backend required)
|
|
||||||
- **Server-side Pages (Server):** PDF viewers (DevExpress DxPdfViewer requires backend)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture Evolution
|
## Current API Location
|
||||||
|
|
||||||
### Old Architecture (Deprecated v1)
|
The active application exposes controllers from:
|
||||||
- **Sender UI:** `EnvelopeGenerator.Web` (Razor Pages + PSPDFKit)
|
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers`
|
||||||
- **Receiver UI:** Separate project
|
|
||||||
- **Backend:** `EnvelopeGenerator.API`
|
|
||||||
|
|
||||||
### Intermediate Architecture (Deprecated v2)
|
Current controller set includes:
|
||||||
- **Unified Frontend:** `EnvelopeGenerator.ReceiverUI` (Pure Blazor WASM)
|
- `AnnotationController`
|
||||||
- **Backend:** `EnvelopeGenerator.API`
|
- `AuthController`
|
||||||
- **Issue:** DevExpress `DxPdfViewer` displayed blank screen (no backend services in WASM)
|
- `CacheController`
|
||||||
|
- `ConfigController`
|
||||||
|
- `DocumentController`
|
||||||
|
- `EmailTemplateController`
|
||||||
|
- `EnvelopeController`
|
||||||
|
- `EnvelopeReceiverController`
|
||||||
|
- `EnvelopeTypeController`
|
||||||
|
- `HistoryController`
|
||||||
|
- `LocalizationController`
|
||||||
|
- `ReadOnlyController`
|
||||||
|
- `ReceiverController`
|
||||||
|
- `SignatureController`
|
||||||
|
- `TfaRegistrationController`
|
||||||
|
|
||||||
### Current Architecture (Active)
|
Do not assume API behavior lives only in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
|
||||||
- **Frontend:** `EnvelopeGenerator.WebUI` (Blazor Auto - Server+WASM Hybrid)
|
|
||||||
- **WebUI** (Server): Server-side components, YARP proxy, DevExpress backend
|
|
||||||
- **WebUI.Client** (WASM): Client-side components, services, business logic
|
|
||||||
- **Backend:** `EnvelopeGenerator.API`
|
|
||||||
- **Libraries:** DevExpress + PDF.js
|
|
||||||
- **PSPDFKit:** **REMOVED**
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Solution Structure
|
## Authentication Model
|
||||||
|
|
||||||
| Project | Target | Purpose |
|
### Sender
|
||||||
|---|---|---|
|
Client login page uses `EnvelopeGenerator.Server.Client/Services/AuthService.cs`.
|
||||||
| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Backend for **both Senders & Receivers**. Auth, PDF serving, signature endpoints. |
|
|
||||||
| `EnvelopeGenerator.WebUI` | net8.0 | **Blazor Auto Server Project**. YARP proxy, server-side components, DevExpress backend services. |
|
Key sender endpoints:
|
||||||
| `EnvelopeGenerator.WebUI.Client` | net8.0 WASM | **Blazor Auto Client Project**. Client-side components, services, business logic. |
|
- `POST /api/auth?cookie=true` — login
|
||||||
| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | **DEPRECATED.** Pure Blazor WASM (migrated to WebUI). |
|
- `GET /api/auth/check` — current sender access check
|
||||||
| `EnvelopeGenerator.Web` | net7/8/9 | **DEPRECATED.** Legacy Razor Pages (Sender UI). No longer used. |
|
- `POST /api/auth/logout` — logout
|
||||||
| `EnvelopeGenerator.Application` | multi | MediatR CQRS handlers. Business logic. |
|
|
||||||
| `EnvelopeGenerator.Domain` | multi | Domain models, constants, interfaces. |
|
### Receiver
|
||||||
| `EnvelopeGenerator.Infrastructure` | multi | EF Core repos, DB context. |
|
Receiver authentication is **per envelope**.
|
||||||
| `EnvelopeGenerator.PdfEditor` | multi | iText7 utilities (NOT used in WebUI). |
|
|
||||||
| `EnvelopeGenerator.DependencyInjection` | multi | DI registration helpers. |
|
Key receiver endpoints used by client services:
|
||||||
| **VB.NET projects** (Service/Form/BBTests) | net462 | **Legacy. Do NOT touch.** |
|
- `POST /api/Auth/envelope-receiver/{envelopeKey}` — submit access code
|
||||||
|
- `GET /api/auth/check/envelope/{envelopeKey}` — check access
|
||||||
|
- `POST /api/auth/logout/envelope/{envelopeKey}` — logout receiver for one envelope
|
||||||
|
|
||||||
|
Receiver cookie resolution in server auth uses an envelope-specific cookie name derived from:
|
||||||
|
- `AuthTokenSignFLOWReceiver.{envelopeKey}` pattern
|
||||||
|
|
||||||
|
### Receiver Server-Side Authorization
|
||||||
|
`EnvelopeReceiverPage.razor` does **not** rely on its own API access-check call for page authorization.
|
||||||
|
|
||||||
|
It uses:
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- tries the current `HttpContext.User`
|
||||||
|
- if needed, reads the per-envelope receiver cookie directly
|
||||||
|
- validates the JWT with the receiver auth scheme
|
||||||
|
- verifies the token subject matches the route envelope key
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Key Files & Routes
|
## Receiver Page Data Loading
|
||||||
|
|
||||||
### Client-Side Pages (WebUI.Client)
|
Main server-side page data service:
|
||||||
| File | Route | Purpose |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
|
||||||
|---|---|---|
|
|
||||||
| `WebUI.Client/Pages/Index.razor` | `/` | Application entry point (landing page). |
|
|
||||||
| `WebUI.Client/Pages/EnvelopeSenderPage.razor` | `/sender` | Sender dashboard (envelope list). |
|
|
||||||
| `WebUI.Client/Pages/LoginSenderPage.razor` | `/sender/login` | Sender username/password auth. |
|
|
||||||
| `WebUI.Client/Pages/LoginReceiverPage.razor` | `/envelope/login/{EnvelopeKey}` | Receiver access code auth. |
|
|
||||||
|
|
||||||
### Server-Side Pages (WebUI)
|
This service loads directly via MediatR and distributed cache:
|
||||||
| File | Route | Purpose |
|
- document bytes
|
||||||
|---|---|---|
|
- receiver envelope data
|
||||||
| `WebUI/Components/Pages/EnvelopeReceiverPage.razor` | `/envelope/{key}` | Receiver PDF viewer & signing (PDF.js). |
|
- signature placeholders
|
||||||
| `WebUI/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor` | `/envelope/DxPdfViewer` | DevExpress PDF Viewer (test page). |
|
- cached signature data
|
||||||
| `WebUI/Components/Pages/EnvelopeReceiverPage_DxReportViewer.razor` | `/envelope/{key}/DxReportViewer` | DevExpress Report Viewer. |
|
|
||||||
| `WebUI/Components/Pages/EnvelopeReceiverPage_embed.razor` | `/envelope/Embed` | Embedded PDF viewer (iframe). |
|
|
||||||
|
|
||||||
### Services & Assets
|
For signature placeholders, the service:
|
||||||
| File | Purpose |
|
- reads document receiver elements
|
||||||
|---|---|
|
- filters them for the authenticated receiver
|
||||||
| `WebUI.Client/Services/AuthService.cs` | Receiver + Sender authentication. |
|
- converts coordinates to `UnitOfLength.Point` before UI use
|
||||||
| `WebUI.Client/Services/SignatureCacheService.cs` | Signature caching (Redis/SQL). |
|
|
||||||
| `WebUI.Client/Services/DocumentService.cs` | PDF document retrieval. |
|
|
||||||
| `WebUI/wwwroot/js/pdf-viewer.js` | PDF.js wrapper (zoom, pagination, thumbnails). |
|
|
||||||
| `WebUI/wwwroot/js/receiver-signature.js` | Signature pad (draw/type/image). |
|
|
||||||
| `WebUI/wwwroot/css/envelope-viewer.css` | EnvelopeViewer styles. |
|
|
||||||
| `API/Controllers/CacheController.cs` | Signature cache endpoints. |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Coordinate System — CRITICAL
|
## Receiver PDF Viewer
|
||||||
|
|
||||||
**Database Format:** INCHES (GdPicture14 native)
|
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
|
||||||
**Origin:** Top-left corner
|
|
||||||
**Axes:** X right, Y down
|
|
||||||
|
|
||||||
### Conversion Formulas
|
Current receiver viewer characteristics:
|
||||||
|
- route: `/envelope/{EnvelopeKey}`
|
||||||
|
- render mode: `InteractiveServer`
|
||||||
|
- PDF rendering: `PDF.js`
|
||||||
|
- toolbar: page navigation, zoom, thumbnail toggle, signature navigation, signature reset
|
||||||
|
- signature popup: `DxPopup`
|
||||||
|
- thumbnail sidebar: resizable and stored in `localStorage`
|
||||||
|
|
||||||
| From INCHES to | Formula | Example |
|
### JS Assets
|
||||||
|---|---|---|
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
|
||||||
| **DevExpress DX** | `x_DX = x_inches * 100` | 1.5" ? 150 DX |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js`
|
||||||
| **PDF Points** | `x_pt = x_inches * 72` | 1.5" ? 108 pt |
|
|
||||||
| **PDF.js Pixels** | Normalize ? scale | `(x_inches / pageWidth) * canvasWidth * scale` |
|
|
||||||
|
|
||||||
**A4 Dimensions:**
|
### CSS
|
||||||
- Width: 8.27" = 595pt = 827 DX
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css`
|
||||||
- Height: 11.69" = 842pt = 1169 DX
|
|
||||||
|
|
||||||
### Unit Systems
|
### PDF.js CDN
|
||||||
|
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
||||||
| System | Unit | Origin | Y-Axis |
|
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css`
|
||||||
|---|---|---|---|
|
|
||||||
| **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
|
## Signature Workflow
|
||||||
|
|
||||||
**Route:** `/envelope/{EnvelopeKey}`
|
Receiver signatures are handled as a **viewer overlay workflow**.
|
||||||
**Tech:** PDF.js 3.11.174 + Blazor Server (`@rendermode InteractiveServer`) + configurable quality
|
|
||||||
**File:** `WebUI/Components/Pages/EnvelopeReceiverPage.razor`
|
|
||||||
|
|
||||||
### Key Features
|
### Current behavior
|
||||||
1. HiDPI/Retina support (4x quality)
|
1. Server-side authorization validates receiver access.
|
||||||
2. Configurable quality (`appsettings.json`)
|
2. The page loads document bytes, receiver data, signature placeholders, and cached signature state.
|
||||||
3. Unlimited zoom (50%-300%)
|
3. If no cached signature exists, the signature popup opens automatically.
|
||||||
4. Ctrl+Wheel global zoom
|
4. Receiver creates signature using one of three tabs:
|
||||||
5. Resizable thumbnail sidebar (150-400px, localStorage)
|
- draw
|
||||||
6. Responsive (desktop/mobile)
|
- text
|
||||||
|
- image
|
||||||
|
5. Required metadata:
|
||||||
|
- full name
|
||||||
|
- place
|
||||||
|
6. Optional metadata:
|
||||||
|
- position
|
||||||
|
7. Clicking a signature placeholder applies the signature as a client-side overlay in the PDF viewer.
|
||||||
|
|
||||||
### Configuration
|
### Important note
|
||||||
**File:** `WebUI/wwwroot/appsettings.json`
|
Although `itext` is referenced by the server project, the current receiver page signing flow is **not PDF stamping-based**. The active receiver UI uses client-side overlay behavior in the viewer.
|
||||||
|
|
||||||
```json
|
### Signature DTO
|
||||||
{
|
`EnvelopeGenerator.Server.Client/Models/SignatureCaptureDto.cs`
|
||||||
"PdfViewer": {
|
|
||||||
"ThumbnailBaseScale": 0.75,
|
|
||||||
"ThumbnailEnableHiDPI": true,
|
|
||||||
"MainCanvasEnableHiDPI": true,
|
|
||||||
"ZoomStepPercentage": 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript API
|
|
||||||
**File:** `WebUI/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:** `WebUI.Client/Models/SignatureCaptureDto.cs`
|
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public sealed record SignatureCaptureDto {
|
public sealed record SignatureCaptureDto {
|
||||||
public required string DataUrl { get; init; } // base64 PNG
|
public required string DataUrl { get; init; }
|
||||||
public required string FullName { get; init; }
|
public required string FullName { get; init; }
|
||||||
public string Position { get; init; } = ""; // Optional
|
public string Position { get; init; } = "";
|
||||||
public required string Place { get; init; }
|
public required string Place { get; init; }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Signature Caching
|
## Signature Cache
|
||||||
|
|
||||||
**Purpose:** Persist signature across page refreshes (distributed cache: Redis/SQL)
|
### Active cache model
|
||||||
|
The current receiver page cache flow is handled directly in the server project through:
|
||||||
|
- `EnvelopeReceiverPageDataService`
|
||||||
|
- `IDistributedCache`
|
||||||
|
- SQL Server distributed cache configuration from `Program.cs`
|
||||||
|
|
||||||
### API Endpoints
|
### Cache key format
|
||||||
**Controller:** `API/Controllers/CacheController.cs`
|
Current server-side key prefix:
|
||||||
|
- `envelope-generator.receiver-ui.signature:{receiverSignature}`
|
||||||
|
|
||||||
- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save
|
This is different from an envelope-key-only cache convention.
|
||||||
- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load
|
|
||||||
- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete
|
|
||||||
|
|
||||||
**Cache Key Format:**
|
### Config
|
||||||
```
|
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/CacheOptions.cs`
|
||||||
signature:91751687-8ae6-4777-bf5f-b8846085e62e:{envelopeKey}
|
- section name: `Cache`
|
||||||
```
|
- option: `SignatureCacheExpiration`
|
||||||
|
|
||||||
**Configuration:** `appsettings.json`
|
### Related controller
|
||||||
```json
|
A cache API controller also exists in:
|
||||||
{
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/CacheController.cs`
|
||||||
"Cache": {
|
|
||||||
"SignatureCacheExpiration": null // or "02:00:00" for 2h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Service
|
|
||||||
**File:** `WebUI.Client/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
|
## Sender Dashboard
|
||||||
|
|
||||||
**Route:** `/sender/login`
|
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor`
|
||||||
**File:** `WebUI.Client/Pages/LoginSenderPage.razor`
|
|
||||||
**Tech:** Bootstrap 5 + DevExpress Blazing Berry theme
|
|
||||||
|
|
||||||
### AuthService Extension
|
Current behavior:
|
||||||
**File:** `WebUI.Client/Services/AuthService.cs`
|
- checks sender access through `AuthService.CheckSenderAccessAsync()`
|
||||||
|
- redirects to `/sender/login` when unauthorized
|
||||||
|
- loads envelope list through client `EnvelopeService`
|
||||||
|
- separates envelopes into active/completed tabs
|
||||||
|
- uses `DevExpress DxGrid`
|
||||||
|
|
||||||
```csharp
|
The sender page is active, but create/edit/delete actions are still marked with TODO behavior in the UI page.
|
||||||
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
|
## Localization
|
||||||
|
|
||||||
**Route:** `/envelope/login/{EnvelopeKey}`
|
Current server host localization setup in `Program.cs`:
|
||||||
**File:** `WebUI.Client/Pages/LoginReceiverPage.razor`
|
- supported cultures: `de-DE`, `en-US`
|
||||||
|
- request localization middleware is enabled
|
||||||
|
- `QueryStringRequestCultureProvider` is added
|
||||||
|
- cookie-based localization services are registered via `AddCookieBasedLocalizer()`
|
||||||
|
|
||||||
**Multi-Envelope Support:** Cookies are stored per-envelope (e.g., `AuthTokenSignFLOWReceiver.{envelopeKey}`), allowing simultaneous authentication for multiple envelopes in the same browser session.
|
Do not assume the old ReceiverUI-only `localStorage` culture approach is the current source of truth for the active host.
|
||||||
|
|
||||||
### 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 (WebUI.Client)
|
## Coordinate System
|
||||||
|
|
||||||
| Package | Version | Purpose |
|
### Source data
|
||||||
|---|---|---|
|
Database signature coordinates are still based on:
|
||||||
| `DevExpress.Blazor.*` | 25.2.3 | UI components (grids, popups, etc.) |
|
- **unit:** inches
|
||||||
| `SkiaSharp.*` | 3.119.1 | WASM rendering |
|
- **origin:** top-left
|
||||||
| ~~`itext`~~ | ~~8.0.5~~ | **NOT USED** (GPL license) |
|
- **axes:** X right, Y down
|
||||||
|
|
||||||
**External CDN:**
|
### Relevant conversions
|
||||||
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
- inches -> PDF points: `x_pt = x_inches * 72`
|
||||||
|
- inches -> DevExpress DX units: `x_dx = x_inches * 100`
|
||||||
|
|
||||||
|
### Current receiver page behavior
|
||||||
|
The server page data service converts signature placeholders to **points** before sending them into the viewer workflow.
|
||||||
|
|
||||||
|
### Unit systems to keep in mind
|
||||||
|
| System | Unit | Origin | Y-axis |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Database | Inches | Top-left | Down |
|
||||||
|
| PDF.js display | Pixels | Top-left | Down |
|
||||||
|
| PDF points | Points | Depends on PDF model | Depends on consumer |
|
||||||
|
| DevExpress DX | 1/100 inch style coordinates | Top-left-oriented usage in this app | Down-oriented usage |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Mistakes History — Do NOT Repeat
|
## Key Services and Files
|
||||||
|
|
||||||
| Mistake | Why Wrong |
|
### Client services
|
||||||
|---|---|
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs`
|
||||||
| Using iText7 in EnvelopeReceiver | GPL license issue. Use overlay system instead. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs`
|
||||||
| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs`
|
||||||
| Hardcoded quality values in PDF.js | Use `appsettings.json` for configurability. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureCacheService.cs`
|
||||||
| Complex toolbar layouts | User wants simplicity. Keep horizontal layout. |
|
|
||||||
| Over-designed UI (gradients/badges) | User prefers simple text labels. |
|
### Server services
|
||||||
| Ignoring "revert" instructions | Revert HTML structure, not just CSS. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs`
|
||||||
| `BottomMarginBand` for signatures | Repeats on every page. Use DetailBand. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs`
|
||||||
| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand. Calculate per-page. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
|
||||||
|
|
||||||
|
### Server config and host files
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Development Notes
|
## Working Rules for This Workspace
|
||||||
|
|
||||||
### Deprecated Projects
|
- Treat `EnvelopeGenerator.Server` as the active main application host.
|
||||||
**DO NOT USE:**
|
- Treat `EnvelopeGenerator.Server.Client` as the active client UI project.
|
||||||
- `EnvelopeGenerator.ReceiverUI` (Pure Blazor WASM) — Migrated to WebUI (DevExpress compatibility issue)
|
- Prefer current `Server` / `Server.Client` paths over old `WebUI` / `ReceiverUI` references.
|
||||||
- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified WebUI
|
- Do not use `EnvelopeGenerator.Web` or `EnvelopeGenerator.ReceiverUI` as the primary implementation target unless explicitly asked.
|
||||||
- PSPDFKit — Removed, use PDF.js + DevExpress instead
|
- Do not modify the legacy VB.NET projects unless explicitly requested.
|
||||||
|
- For receiver PDF/signature work, prefer the current `PDF.js`-based flow in `EnvelopeReceiverPage.razor`.
|
||||||
### Legacy Projects (VB.NET)
|
- For DevExpress PDF viewer issues, remember server-side services are registered in `EnvelopeGenerator.Server`.
|
||||||
**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
|
**Last Updated:** 2026-06-29
|
||||||
|
|
||||||
### 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:** WebUI serves both Senders and Receivers
|
|
||||||
6. **Render mode:** Client-side (WASM) for login/dashboard, Server-side for PDF viewers
|
|
||||||
|
|
||||||
### 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 WebUI (ReceiverUI/Web are deprecated)
|
|
||||||
5. **Blank DxPdfViewer:** Ensure page has `@rendermode InteractiveServer`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated:** 2025-01-27 (ReceiverUI ? WebUI migration complete)
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions,
|
|||||||
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</response>
|
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</response>
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||||
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
[HttpPost("logout")]
|
[HttpPost("logout")]
|
||||||
public async Task<IActionResult> Logout()
|
public async Task<IActionResult> Logout()
|
||||||
{
|
{
|
||||||
@@ -69,7 +69,7 @@ public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions,
|
|||||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||||
[HttpGet("check")]
|
[HttpGet("check")]
|
||||||
[Authorize]
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
public IActionResult Check(string? role = null)
|
public IActionResult Check(string? role = null)
|
||||||
=> role is not null && !User.IsInRole(role)
|
=> role is not null && !User.IsInRole(role)
|
||||||
? Unauthorized()
|
? Unauthorized()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@@ -13,7 +14,7 @@ namespace EnvelopeGenerator.API.Controllers;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize]
|
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||||
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
|
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
|
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ namespace EnvelopeGenerator.API.Controllers;
|
|||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class SignatureController : ControllerBase
|
public class DocReceiverElementController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of <see cref="SignatureController"/>.
|
/// Initializes a new instance of <see cref="DocReceiverElementController"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SignatureController(IMediator mediator)
|
public DocReceiverElementController(IMediator mediator)
|
||||||
{
|
{
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,8 @@ namespace EnvelopeGenerator.API.Controllers;
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public class EnvelopeController : ControllerBase
|
public class EnvelopeController : ControllerBase
|
||||||
{
|
{
|
||||||
|
public record DeleteEnvelopeRequest(string Reason);
|
||||||
|
|
||||||
private readonly ILogger<EnvelopeController> _logger;
|
private readonly ILogger<EnvelopeController> _logger;
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
@@ -108,4 +110,17 @@ public class EnvelopeController : ControllerBase
|
|||||||
else
|
else
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
[HttpDelete("{id:int}")]
|
||||||
|
public async Task<IActionResult> DeleteAsync([FromRoute] int id, [FromBody] DeleteEnvelopeRequest request)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new DeleteEnvelopeCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = id,
|
||||||
|
Reason = request?.Reason ?? string.Empty
|
||||||
|
}.WithAuth(User.GetId()));
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -214,6 +214,10 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
if (reader.Read())
|
if (reader.Read())
|
||||||
{
|
{
|
||||||
bool outSuccess = reader.GetBoolean(0);
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
|
if (!outSuccess)
|
||||||
|
_logger.LogWarning(
|
||||||
|
"PRSIG_API_ADD_DOC_RECEIVER_ELEM returned OUT_SUCCESS=false. DOC_ID={DocId}, RECEIVER_ID={ReceiverId}, Page={Page}",
|
||||||
|
document.Id, rcv.Id, sign.Page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
@@ -221,8 +225,6 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
#region Create history
|
#region Create history
|
||||||
// ENV_UID, STATUS_ID, USER_ID,
|
// ENV_UID, STATUS_ID, USER_ID,
|
||||||
string sql_hist = @"
|
string sql_hist = @"
|
||||||
USE [DD_ECM]
|
|
||||||
|
|
||||||
DECLARE @OUT_SUCCESS bit;
|
DECLARE @OUT_SUCCESS bit;
|
||||||
|
|
||||||
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
||||||
@@ -244,6 +246,10 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
if (reader.Read())
|
if (reader.Read())
|
||||||
{
|
{
|
||||||
bool outSuccess = reader.GetBoolean(0);
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
|
if (!outSuccess)
|
||||||
|
_logger.LogWarning(
|
||||||
|
"PRSIG_API_ADD_HISTORY_STATE returned OUT_SUCCESS=false. EnvelopeUuid={EnvelopeUuid}",
|
||||||
|
envelope.Uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
|
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
|
||||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.28" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
|
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
|
||||||
<PackageReference Include="itext" Version="8.0.5" />
|
<PackageReference Include="itext" Version="8.0.5" />
|
||||||
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
|
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ public class ExceptionHandlingMiddleware
|
|||||||
message = notFoundEx.Message;
|
message = notFoundEx.Message;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ForbiddenException forbiddenEx:
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||||
|
message = forbiddenEx.Message;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.LogError(exception, "Unhandled exception occurred.");
|
logger.LogError(exception, "Unhandled exception occurred.");
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ using EnvelopeGenerator.Application;
|
|||||||
using DigitalData.Auth.Client;
|
using DigitalData.Auth.Client;
|
||||||
using DigitalData.Core.Abstractions;
|
using DigitalData.Core.Abstractions;
|
||||||
using EnvelopeGenerator.API.Models;
|
using EnvelopeGenerator.API.Models;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using DigitalData.Core.Abstractions.Security.Extensions;
|
using DigitalData.Core.Abstractions.Security.Extensions;
|
||||||
using EnvelopeGenerator.API.Middleware;
|
using EnvelopeGenerator.API.Middleware;
|
||||||
@@ -22,6 +21,7 @@ using NLog.Web;
|
|||||||
using NLog;
|
using NLog;
|
||||||
using DigitalData.Auth.Claims;
|
using DigitalData.Auth.Claims;
|
||||||
using EnvelopeGenerator.API;
|
using EnvelopeGenerator.API;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
|
||||||
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||||
logger.Info("Logging initialized!");
|
logger.Info("Logging initialized!");
|
||||||
@@ -44,7 +44,11 @@ try
|
|||||||
|
|
||||||
var deferredProvider = new DeferredServiceProvider();
|
var deferredProvider = new DeferredServiceProvider();
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers()
|
||||||
|
.AddJsonOptions(options =>
|
||||||
|
{
|
||||||
|
options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
|
||||||
|
});
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
||||||
|
|
||||||
@@ -238,8 +242,9 @@ try
|
|||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddAuthorizationBuilder()
|
builder.Services.AddAuthorizationBuilder()
|
||||||
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy.RequireRole(Role.Sender, Role.Receiver.Full))
|
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy
|
||||||
|
.RequireRole(Role.Sender, Role.Receiver.Full)
|
||||||
|
.AddAuthenticationSchemes(AuthScheme.Sender, AuthScheme.Receiver))
|
||||||
.AddPolicy(AuthPolicy.Sender, policy => policy
|
.AddPolicy(AuthPolicy.Sender, policy => policy
|
||||||
.RequireRole(Role.Sender)
|
.RequireRole(Role.Sender)
|
||||||
.AddAuthenticationSchemes(AuthScheme.Sender))
|
.AddAuthenticationSchemes(AuthScheme.Sender))
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||||
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
|
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
|
||||||
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\API\net8\$(Version)\EnvelopeGenerator.API.zip</DesktopBuildPackageLocation>
|
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\PreRelease\API\net8\$(Version)\EnvelopeGenerator.API.zip</DesktopBuildPackageLocation>
|
||||||
<PackageAsSingleFile>true</PackageAsSingleFile>
|
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||||
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
|
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
|
||||||
<_TargetId>IISWebDeployPackage</_TargetId>
|
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
"https": {
|
"https": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": false,
|
"launchBrowser": true,
|
||||||
"launchUrl": "swagger",
|
"launchUrl": "sender",
|
||||||
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing configuration settings.
|
/// Data Transfer Object representing configuration settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class ConfigDto
|
public class ConfigDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class DocReceiverElementDto : IDocReceiverElement
|
public class DocReceiverElementDto : IDocReceiverElement
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class DocumentDto
|
public class DocumentDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing the status of a document for a specific receiver.
|
/// Data Transfer Object representing the status of a document for a specific receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class DocumentStatusDto
|
public class DocumentStatusDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||||
using DigitalData.UserManager.Application.DTOs.User;
|
using DigitalData.UserManager.Application.DTOs.User;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.History;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record EnvelopeDto : IEnvelope
|
public record EnvelopeDto : IEnvelope
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -133,5 +129,10 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<ReceiverDto>? Receivers { get; set; }
|
public IEnumerable<EnvelopeReceiverDto>? EnvelopeReceivers { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<HistoryDto>? Histories { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record EnvelopeReceiverDto
|
public record EnvelopeReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -75,4 +74,13 @@ public record EnvelopeReceiverDto
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasPhoneNumber { get; init; }
|
public bool HasPhoneNumber { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether this receiver has signed the envelope.
|
||||||
|
/// Checks if there is a DocumentSigned history entry for this receiver in the envelope's history.
|
||||||
|
/// </summary>
|
||||||
|
public bool Signed => Envelope?.Histories?.Any(h =>
|
||||||
|
h.Receiver?.Id == ReceiverId &&
|
||||||
|
h.Status == EnvelopeStatus.DocumentSigned
|
||||||
|
) ?? false;
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
|
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
@@ -8,7 +7,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="DateValid"></param>
|
/// <param name="DateValid"></param>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record EnvelopeReceiverReadOnlyCreateDto(
|
public record EnvelopeReceiverReadOnlyCreateDto(
|
||||||
DateTime DateValid)
|
DateTime DateValid)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
|
|
||||||
@@ -8,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
|||||||
/// Represents a read-only Data Transfer Object (DTO) for an envelope receiver.
|
/// Represents a read-only Data Transfer Object (DTO) for an envelope receiver.
|
||||||
/// Contains information about the receiver, associated envelope, and audit details.
|
/// Contains information about the receiver, associated envelope, and audit details.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class EnvelopeReceiverReadOnlyDto
|
public class EnvelopeReceiverReadOnlyDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object for updating a read-only envelope receiver.
|
/// Data Transfer Object for updating a read-only envelope receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class EnvelopeReceiverReadOnlyUpdateDto
|
public class EnvelopeReceiverReadOnlyUpdateDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class EnvelopeTypeDto
|
public class EnvelopeTypeDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class GtxMessagingResponse : Dictionary<string, object?> { }
|
public class GtxMessagingResponse : Dictionary<string, object?> { }
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record SmsResponse
|
public record SmsResponse
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
@@ -7,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class ReceiverDto
|
public class ReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a user's UI preferences stored in the distributed cache (dbo.TBDD_CACHE).
|
||||||
|
/// </summary>
|
||||||
|
public sealed record UserPreferencesDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The active DevExpress theme name.
|
||||||
|
/// Allowed values: "blazing-berry", "fluent", "purple"
|
||||||
|
/// </summary>
|
||||||
|
public string ThemeName { get; set; } = "blazing-berry";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether dark mode is active.
|
||||||
|
/// Default: false (light mode).
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDarkMode { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Persisted DxGrid layout states, keyed by a stable grid identifier.
|
||||||
|
/// Example keys: "sender.active-envelopes", "sender.completed-envelopes"
|
||||||
|
/// Values are opaque JSON strings produced by DxGrid.SaveLayoutAsync().
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, string> GridLayouts { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The authenticated user's display name ("{GivenName} {Surname}").
|
||||||
|
/// Populated by the server on GET responses from JWT claims.
|
||||||
|
/// NOT persisted in cache — injected at read time by UserPreferencesController.
|
||||||
|
/// Ignored on PUT requests.
|
||||||
|
/// </summary>
|
||||||
|
public string? DisplayName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public static UserPreferencesDto Default { get; } = new UserPreferencesDto();
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages per-user UI preferences (theme, dark mode, grid layouts).
|
||||||
|
/// Preferences are stored in the distributed cache (dbo.TBDD_CACHE) as JSON.
|
||||||
|
/// Cache key schema: "signflow.user-preferences:{username}"
|
||||||
|
/// </summary>
|
||||||
|
public interface IUserPreferencesService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves preferences for the given user.
|
||||||
|
/// Always returns a valid object — never throws, never returns null.
|
||||||
|
/// Returns defaults if no entry exists in cache.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id"></param>
|
||||||
|
/// <param name="ct">Cancellation token.</param>
|
||||||
|
Task<UserPreferencesDto> GetOrCreateAsync(string id, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Persists the given preferences for the user in the distributed cache.
|
||||||
|
/// Failures are non-fatal and silently ignored.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id"></param>
|
||||||
|
/// <param name="preferences">The preferences to persist.</param>
|
||||||
|
/// <param name="ct">Cancellation token.</param>
|
||||||
|
Task SaveAsync(string id, UserPreferencesDto preferences, CancellationToken ct = default);
|
||||||
|
}
|
||||||
@@ -7,7 +7,9 @@ using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the AutoMapper profile configuration for mapping between
|
/// Represents the AutoMapper profile configuration for mapping between
|
||||||
@@ -26,15 +28,17 @@ public class MappingProfile : Profile
|
|||||||
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
||||||
CreateMap<DocumentStatus, DocumentStatusDto>();
|
CreateMap<DocumentStatus, DocumentStatusDto>();
|
||||||
CreateMap<EmailTemplate, EmailTemplateDto>();
|
CreateMap<EmailTemplate, EmailTemplateDto>();
|
||||||
CreateMap<Envelope, EnvelopeDto>().ForMember(dest => dest.Receivers, opt => opt.MapFrom(src => src.EnvelopeReceivers.Select(er => er.Receiver)));
|
CreateMap<Envelope, EnvelopeDto>()
|
||||||
|
.ForMember(dest => dest.EnvelopeTypeTitle,
|
||||||
|
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
|
||||||
CreateMap<Document, DocumentDto>();
|
CreateMap<Document, DocumentDto>();
|
||||||
CreateMap<Domain.Entities.History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<Domain.Entities.History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverDto>();
|
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
CreateMap<EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
||||||
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
||||||
CreateMap<Domain.Entities.Receiver, ReceiverDto>();
|
CreateMap<Receiver, ReceiverDto>();
|
||||||
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
CreateMap<EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
||||||
CreateMap<ElementAnnotation, AnnotationDto>();
|
CreateMap<ElementAnnotation, AnnotationDto>();
|
||||||
|
|
||||||
// DTO to Entity mappings
|
// DTO to Entity mappings
|
||||||
@@ -47,13 +51,13 @@ public class MappingProfile : Profile
|
|||||||
CreateMap<EmailTemplateDto, EmailTemplate>();
|
CreateMap<EmailTemplateDto, EmailTemplate>();
|
||||||
CreateMap<EnvelopeDto, Envelope>();
|
CreateMap<EnvelopeDto, Envelope>();
|
||||||
CreateMap<DocumentDto, Document>();
|
CreateMap<DocumentDto, Document>();
|
||||||
CreateMap<HistoryDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
CreateMap<HistoryDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||||
CreateMap<HistoryCreateDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
CreateMap<HistoryCreateDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||||
CreateMap<EnvelopeReceiverDto, Domain.Entities.EnvelopeReceiver>();
|
CreateMap<EnvelopeReceiverDto, EnvelopeReceiver>();
|
||||||
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
||||||
CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
CreateMap<ReceiverDto, Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
||||||
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
CreateMap<EnvelopeReceiverReadOnlyCreateDto, EnvelopeReceiverReadOnly>();
|
||||||
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, EnvelopeReceiverReadOnly>();
|
||||||
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
||||||
.MapAddedWhen();
|
.MapAddedWhen();
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||||
|
|
||||||
|
public record EnvelopeDeletedNotification : INotification, ISendMailNotification
|
||||||
|
{
|
||||||
|
public required Envelope Envelope { get; init; }
|
||||||
|
|
||||||
|
public required EnvelopeReceiver EnvelopeReceiver { get; init; }
|
||||||
|
|
||||||
|
public required string Reason { get; init; }
|
||||||
|
|
||||||
|
public required string SenderReference { get; init; }
|
||||||
|
|
||||||
|
public EmailTemplateType TemplateType => EmailTemplateType.DocumentDeleted;
|
||||||
|
|
||||||
|
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
|
||||||
|
?? throw new InvalidOperationException("Receiver email is missing for EnvelopeDeletedNotification.");
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted.Handlers;
|
||||||
|
|
||||||
|
public class SendDeletedMailHandler : SendMailHandler<EnvelopeDeletedNotification>
|
||||||
|
{
|
||||||
|
public SendDeletedMailHandler(
|
||||||
|
IRepository<EmailTemplate> tempRepo,
|
||||||
|
IRepository<EmailOut> emailOutRepo,
|
||||||
|
IOptions<MailParams> mailParamsOptions,
|
||||||
|
IOptions<DispatcherParams> dispatcherParamsOptions)
|
||||||
|
: base(tempRepo, emailOutRepo, mailParamsOptions, dispatcherParamsOptions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ConfigureEmailOut(EnvelopeDeletedNotification notification, EmailOut emailOut)
|
||||||
|
{
|
||||||
|
emailOut.ReferenceString = notification.EmailAddress;
|
||||||
|
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
|
||||||
|
emailOut.WfId = (int)EnvelopeStatus.MessageDeletionSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Dictionary<string, string> CreatePlaceHolders(EnvelopeDeletedNotification notification)
|
||||||
|
{
|
||||||
|
var senderName = notification.Envelope.User?.Email;
|
||||||
|
if (string.IsNullOrWhiteSpace(senderName))
|
||||||
|
senderName = notification.SenderReference;
|
||||||
|
|
||||||
|
return new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["[NAME_RECEIVER]"] = notification.EnvelopeReceiver.Name ?? string.Empty,
|
||||||
|
["[NAME_SENDER]"] = senderName ?? string.Empty,
|
||||||
|
["[DOCUMENT_TITLE]"] = notification.Envelope.Title ?? string.Empty,
|
||||||
|
["[REASON]"] = notification.Reason
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Query;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Query;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
||||||
@@ -29,5 +31,6 @@ public record ReceiverQueryBase
|
|||||||
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
|
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
|
||||||
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
[NotMapped]
|
||||||
|
public virtual bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ public class EnvelopeReceiverAddReadSQL : ISQL<Envelope>
|
|||||||
/// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE,
|
/// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE,
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Raw => @"
|
public string Raw => @"
|
||||||
DECLARE @OUT_RECEIVER_ID int
|
DECLARE @OUT_RECEIVER_ID bigint
|
||||||
|
|
||||||
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
|
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
|
||||||
{0},
|
{0},
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ namespace EnvelopeGenerator.Application.Documents.Queries;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Id">The unique identifier of the document. Optional.</param>
|
/// <param name="Id">The unique identifier of the document. Optional.</param>
|
||||||
/// <param name="EnvelopeId">The identifier of the envelope associated with the document. Optional.</param>
|
/// <param name="EnvelopeId">The identifier of the envelope associated with the document. Optional.</param>
|
||||||
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null) : IRequest<DocumentDto>
|
/// <param name="UserId">The unique identifier of the sending user. Optional.</param>
|
||||||
|
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null, int? UserId = null) : IRequest<DocumentDto>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +54,10 @@ public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, Docum
|
|||||||
/// </exception>
|
/// </exception>
|
||||||
public async Task<DocumentDto> Handle(ReadDocumentQuery query, CancellationToken cancel)
|
public async Task<DocumentDto> Handle(ReadDocumentQuery query, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
var docQuery = _repo.Query.Include(doc => doc.Elements).ThenInclude(e => e.Annotations);
|
var docQuery = _repo.Query.Include(doc => doc.Elements).ThenInclude(e => e.Annotations).AsNoTracking();
|
||||||
|
|
||||||
|
if (query.UserId is int userId)
|
||||||
|
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
|
||||||
|
|
||||||
if (query.Id is not null)
|
if (query.Id is not null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Resources\Model.Designer.vb" />
|
<None Remove="Resources\Model.Designer.vb" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -21,7 +21,6 @@
|
|||||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.18" />
|
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||||
@@ -79,25 +78,25 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||||
<PackageReference Include="CommandDotNet">
|
<PackageReference Include="CommandDotNet">
|
||||||
<Version>7.0.5</Version>
|
<Version>7.0.5</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||||
<PackageReference Include="CommandDotNet">
|
<PackageReference Include="CommandDotNet">
|
||||||
<Version>8.1.1</Version>
|
<Version>8.1.1</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||||
<PackageReference Include="CommandDotNet">
|
<PackageReference Include="CommandDotNet">
|
||||||
<Version>8.1.1</Version>
|
<Version>8.1.1</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
|
public record DeleteEnvelopeResultDto(int EnvelopeId, EnvelopeStatus AppliedStatus, bool IsNoOp);
|
||||||
|
|
||||||
|
public record DeleteEnvelopeCommand : IRequest<DeleteEnvelopeResultDto>
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public int EnvelopeId { get; init; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MinLength(1)]
|
||||||
|
public string Reason { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
internal int UserId { get; private set; }
|
||||||
|
|
||||||
|
public DeleteEnvelopeCommand WithAuth(int userId)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.Application.Histories.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
|
public class DeleteEnvelopeCommandHandler : IRequestHandler<DeleteEnvelopeCommand, DeleteEnvelopeResultDto>
|
||||||
|
{
|
||||||
|
private readonly IRepository<Envelope> _envelopeRepository;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public DeleteEnvelopeCommandHandler(
|
||||||
|
IRepository<Envelope> envelopeRepository,
|
||||||
|
IMediator mediator)
|
||||||
|
{
|
||||||
|
_envelopeRepository = envelopeRepository;
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DeleteEnvelopeResultDto> Handle(DeleteEnvelopeCommand request, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var reason = request.Reason?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(reason))
|
||||||
|
throw new BadRequestException("Please provide a reason.");
|
||||||
|
|
||||||
|
var envelope = await _envelopeRepository.Query
|
||||||
|
.Include(e => e.User)
|
||||||
|
.Include(e => e.EnvelopeReceivers)
|
||||||
|
.ThenInclude(er => er.Receiver)
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == request.EnvelopeId, cancel)
|
||||||
|
?? throw new NotFoundException($"Envelope {request.EnvelopeId} not found.");
|
||||||
|
|
||||||
|
if (envelope.UserId != request.UserId)
|
||||||
|
throw new ForbiddenException();
|
||||||
|
|
||||||
|
if (envelope.Status is EnvelopeStatus.EnvelopeDeleted or EnvelopeStatus.EnvelopeWithdrawn)
|
||||||
|
return new DeleteEnvelopeResultDto(envelope.Id, envelope.Status, IsNoOp: true);
|
||||||
|
|
||||||
|
var targetStatus = envelope.Status > EnvelopeStatus.EnvelopeSaved
|
||||||
|
? EnvelopeStatus.EnvelopeWithdrawn
|
||||||
|
: EnvelopeStatus.EnvelopeDeleted;
|
||||||
|
|
||||||
|
envelope.Status = targetStatus;
|
||||||
|
envelope.Comment = reason;
|
||||||
|
envelope.ChangedWhen = DateTime.Now;
|
||||||
|
|
||||||
|
await _envelopeRepository.UpdateAsync(envelope, e => e.Id == envelope.Id, cancel);
|
||||||
|
|
||||||
|
var userReference = envelope.User?.Email;
|
||||||
|
if (string.IsNullOrWhiteSpace(userReference))
|
||||||
|
throw new BadRequestException("Envelope sender email is missing.");
|
||||||
|
|
||||||
|
await _mediator.Send(new CreateHistoryCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = envelope.Id,
|
||||||
|
UserReference = userReference,
|
||||||
|
Status = targetStatus,
|
||||||
|
Comment = reason
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
foreach (var envelopeReceiver in envelope.EnvelopeReceivers ?? Enumerable.Empty<EnvelopeReceiver>())
|
||||||
|
{
|
||||||
|
if (envelopeReceiver.Receiver is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
await _mediator.Publish(new EnvelopeDeletedNotification
|
||||||
|
{
|
||||||
|
Envelope = envelope,
|
||||||
|
EnvelopeReceiver = envelopeReceiver,
|
||||||
|
Reason = reason,
|
||||||
|
SenderReference = userReference
|
||||||
|
}, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DeleteEnvelopeResultDto(envelope.Id, targetStatus, IsNoOp: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,16 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public bool OnlyActive { get; init; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public bool OnlyCompleted { get; init; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Abfrage des Include des Umschlags
|
/// Abfrage des Include des Umschlags
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -132,6 +142,12 @@ public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnum
|
|||||||
query = query.Where(e => !status.Ignore.Contains(e.Status));
|
query = query.Where(e => !status.Ignore.Contains(e.Status));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(request is { OnlyActive: true })
|
||||||
|
query = query.Where(e => Status.Active.Contains(e.Status));
|
||||||
|
|
||||||
|
if (request is { OnlyCompleted: true })
|
||||||
|
query = query.Where(e => Status.Completed.Contains(e.Status));
|
||||||
|
|
||||||
var envelopes = await query
|
var envelopes = await query
|
||||||
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using DigitalData.Core.Abstraction.Application.Repository;
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
@@ -14,7 +13,6 @@ namespace EnvelopeGenerator.Application.Receivers.Commands;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record CreateReceiverCommand : IRequest<(ReceiverDto Receiver, bool AlreadyExists)>
|
public record CreateReceiverCommand : IRequest<(ReceiverDto Receiver, bool AlreadyExists)>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Receivers.Commands;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Receivers.Commands;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object for updating a receiver's information.
|
/// Data Transfer Object for updating a receiver's information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class UpdateReceiverCommand
|
public class UpdateReceiverCommand
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using EnvelopeGenerator.Application.Common.Query;
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Receivers.Queries;
|
namespace EnvelopeGenerator.Application.Receivers.Queries;
|
||||||
|
|
||||||
@@ -12,7 +13,24 @@ namespace EnvelopeGenerator.Application.Receivers.Queries;
|
|||||||
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
||||||
/// um spezifische Informationen über einen Empfänger abzurufen.
|
/// um spezifische Informationen über einen Empfänger abzurufen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>;
|
public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Suchbegriff für eine teilweise Übereinstimmung in der E-Mail Adresse des Empfängers
|
||||||
|
/// </summary>
|
||||||
|
public virtual string? EmailAddressSearch { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether any of the specified query criteria have a value.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This property returns <c>true</c> if at least one of the fields
|
||||||
|
/// <see cref="ReceiverQueryBase.Id"/>, <see cref="ReceiverQueryBase.EmailAddress"/>, <see cref="EmailAddressSearch"/>, or <see cref="ReceiverQueryBase.Signature"/> is not null.
|
||||||
|
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
||||||
|
/// </remarks>
|
||||||
|
[NotMapped]
|
||||||
|
public override bool HasAnyCriteria => EmailAddressSearch is not null || base.HasAnyCriteria;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -53,6 +71,11 @@ public class ReadReceiverQueryHandler : IRequestHandler<ReadReceiverQuery, IEnum
|
|||||||
query = query.Where(r => r.EmailAddress == email);
|
query = query.Where(r => r.EmailAddress == email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.EmailAddressSearch))
|
||||||
|
{
|
||||||
|
query = query.Where(r => EF.Functions.Like(r.EmailAddress, $"%{request.EmailAddressSearch}%"));
|
||||||
|
}
|
||||||
|
|
||||||
if (request.Signature is string signature)
|
if (request.Signature is string signature)
|
||||||
{
|
{
|
||||||
query = query.Where(r => r.Signature == signature);
|
query = query.Where(r => r.Signature == signature);
|
||||||
|
|||||||
@@ -397,4 +397,412 @@ public static class Extensions
|
|||||||
/// <param name="suffix"></param>
|
/// <param name="suffix"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string LockedFooterBody(this IStringLocalizer localizer, string suffix) => localizer[nameof(LockedFooterBody) + suffix].Value;
|
public static string LockedFooterBody(this IStringLocalizer localizer, string suffix) => localizer[nameof(LockedFooterBody) + suffix].Value;
|
||||||
|
|
||||||
|
// Sender-side UI resources
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string NewEnvelope(this IStringLocalizer localizer) => localizer[nameof(NewEnvelope)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string LoadEnvelope(this IStringLocalizer localizer) => localizer[nameof(LoadEnvelope)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteEnvelope(this IStringLocalizer localizer) => localizer[nameof(DeleteEnvelope)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string RefreshData(this IStringLocalizer localizer) => localizer[nameof(RefreshData)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string RefreshedAt(this IStringLocalizer localizer) => localizer[nameof(RefreshedAt)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ShowDocument(this IStringLocalizer localizer) => localizer[nameof(ShowDocument)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ContactReceiver(this IStringLocalizer localizer) => localizer[nameof(ContactReceiver)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EnvelopeId(this IStringLocalizer localizer) => localizer[nameof(EnvelopeId)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string OpenLogDirectory(this IStringLocalizer localizer) => localizer[nameof(OpenLogDirectory)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ShowResultsReport(this IStringLocalizer localizer) => localizer[nameof(ShowResultsReport)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SupportMail(this IStringLocalizer localizer) => localizer[nameof(SupportMail)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ResendInvitation(this IStringLocalizer localizer) => localizer[nameof(ResendInvitation)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Export(this IStringLocalizer localizer) => localizer[nameof(Export)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Receivers(this IStringLocalizer localizer) => localizer[nameof(Receivers)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EmailSalutation(this IStringLocalizer localizer) => localizer[nameof(EmailSalutation)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SignedWhen(this IStringLocalizer localizer) => localizer[nameof(SignedWhen)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AccessCode(this IStringLocalizer localizer) => localizer[nameof(AccessCode)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string User(this IStringLocalizer localizer) => localizer[nameof(User)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Type(this IStringLocalizer localizer) => localizer[nameof(Type)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Title(this IStringLocalizer localizer) => localizer[nameof(Title)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string CreatedOn(this IStringLocalizer localizer) => localizer[nameof(CreatedOn)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string LastModified(this IStringLocalizer localizer) => localizer[nameof(LastModified)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string OpenEnvelopes(this IStringLocalizer localizer) => localizer[nameof(OpenEnvelopes)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string CompletedEnvelopes(this IStringLocalizer localizer) => localizer[nameof(CompletedEnvelopes)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SendAccessCode(this IStringLocalizer localizer) => localizer[nameof(SendAccessCode)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string TwoFactorProperties(this IStringLocalizer localizer) => localizer[nameof(TwoFactorProperties)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Name(this IStringLocalizer localizer) => localizer[nameof(Name)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string PhoneNumber(this IStringLocalizer localizer) => localizer[nameof(PhoneNumber)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AddReceiver(this IStringLocalizer localizer) => localizer[nameof(AddReceiver)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteReceiver(this IStringLocalizer localizer) => localizer[nameof(DeleteReceiver)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AddFile(this IStringLocalizer localizer) => localizer[nameof(AddFile)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string MergeFiles(this IStringLocalizer localizer) => localizer[nameof(MergeFiles)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteFile(this IStringLocalizer localizer) => localizer[nameof(DeleteFile)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ShowFile(this IStringLocalizer localizer) => localizer[nameof(ShowFile)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EditFields(this IStringLocalizer localizer) => localizer[nameof(EditFields)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EditData(this IStringLocalizer localizer) => localizer[nameof(EditData)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Save(this IStringLocalizer localizer) => localizer[nameof(Save)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SendEnvelope(this IStringLocalizer localizer) => localizer[nameof(SendEnvelope)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Cancel(this IStringLocalizer localizer) => localizer[nameof(Cancel)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AddSignature(this IStringLocalizer localizer) => localizer[nameof(AddSignature)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteSignature(this IStringLocalizer localizer) => localizer[nameof(DeleteSignature)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Language(this IStringLocalizer localizer) => localizer[nameof(Language)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string UseAccessCode(this IStringLocalizer localizer) => localizer[nameof(UseAccessCode)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string TwoFactorEnabled(this IStringLocalizer localizer) => localizer[nameof(TwoFactorEnabled)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string CertificationType(this IStringLocalizer localizer) => localizer[nameof(CertificationType)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string FinalEmailToCreator(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToCreator)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string FinalEmailToReceivers(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToReceivers)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SendReminderEmails(this IStringLocalizer localizer) => localizer[nameof(SendReminderEmails)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string FirstReminderDays(this IStringLocalizer localizer) => localizer[nameof(FirstReminderDays)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ReminderIntervalDays(this IStringLocalizer localizer) => localizer[nameof(ReminderIntervalDays)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ExpiresWhenDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWhenDays)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ExpiresWarningDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWarningDays)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Message(this IStringLocalizer localizer) => localizer[nameof(Message)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EnvelopeType(this IStringLocalizer localizer) => localizer[nameof(EnvelopeType)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AllOptions(this IStringLocalizer localizer) => localizer[nameof(AllOptions)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteReason(this IStringLocalizer localizer) => localizer[nameof(DeleteReason)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string PleaseProvideReason(this IStringLocalizer localizer) => localizer[nameof(PleaseProvideReason)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Status(this IStringLocalizer localizer) => localizer[nameof(Status)].Value;
|
||||||
}
|
}
|
||||||
@@ -477,4 +477,178 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Bestätigungen</value>
|
<value>Bestätigungen</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="NewEnvelope" xml:space="preserve">
|
||||||
|
<value>Neuer Umschlag</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoadEnvelope" xml:space="preserve">
|
||||||
|
<value>Umschlag laden</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteEnvelope" xml:space="preserve">
|
||||||
|
<value>Umschlag zurückrufen/löschen</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshData" xml:space="preserve">
|
||||||
|
<value>Daten Aktualisieren</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshedAt" xml:space="preserve">
|
||||||
|
<value>Aktualisiert: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowDocument" xml:space="preserve">
|
||||||
|
<value>Dokument anzeigen</value>
|
||||||
|
</data>
|
||||||
|
<data name="ContactReceiver" xml:space="preserve">
|
||||||
|
<value>Empfänger kontaktieren</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeId" xml:space="preserve">
|
||||||
|
<value>Umschlag-ID: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenLogDirectory" xml:space="preserve">
|
||||||
|
<value>Öffne Log Verzeichnis</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowResultsReport" xml:space="preserve">
|
||||||
|
<value>Ergebnisbericht anzeigen</value>
|
||||||
|
</data>
|
||||||
|
<data name="SupportMail" xml:space="preserve">
|
||||||
|
<value>Support Mail</value>
|
||||||
|
</data>
|
||||||
|
<data name="ResendInvitation" xml:space="preserve">
|
||||||
|
<value>Einladung manuell versenden</value>
|
||||||
|
</data>
|
||||||
|
<data name="Export" xml:space="preserve">
|
||||||
|
<value>Export</value>
|
||||||
|
</data>
|
||||||
|
<data name="Receivers" xml:space="preserve">
|
||||||
|
<value>Empfänger</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailSalutation" xml:space="preserve">
|
||||||
|
<value>Email Anrede</value>
|
||||||
|
</data>
|
||||||
|
<data name="SignedWhen" xml:space="preserve">
|
||||||
|
<value>Unterschrieben wann</value>
|
||||||
|
</data>
|
||||||
|
<data name="AccessCode" xml:space="preserve">
|
||||||
|
<value>Zugangscode</value>
|
||||||
|
</data>
|
||||||
|
<data name="User" xml:space="preserve">
|
||||||
|
<value>Benutzer</value>
|
||||||
|
</data>
|
||||||
|
<data name="Type" xml:space="preserve">
|
||||||
|
<value>Typ</value>
|
||||||
|
</data>
|
||||||
|
<data name="Title" xml:space="preserve">
|
||||||
|
<value>Titel</value>
|
||||||
|
</data>
|
||||||
|
<data name="CreatedOn" xml:space="preserve">
|
||||||
|
<value>Erstellt am</value>
|
||||||
|
</data>
|
||||||
|
<data name="LastModified" xml:space="preserve">
|
||||||
|
<value>Zuletzt geändert am</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenEnvelopes" xml:space="preserve">
|
||||||
|
<value>Offene Umschläge</value>
|
||||||
|
</data>
|
||||||
|
<data name="CompletedEnvelopes" xml:space="preserve">
|
||||||
|
<value>Abgeschlossene Umschläge</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendAccessCode" xml:space="preserve">
|
||||||
|
<value>Zugangscode senden</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorProperties" xml:space="preserve">
|
||||||
|
<value>2-Faktor Eigenschaften</value>
|
||||||
|
</data>
|
||||||
|
<data name="Name" xml:space="preserve">
|
||||||
|
<value>Name</value>
|
||||||
|
</data>
|
||||||
|
<data name="PhoneNumber" xml:space="preserve">
|
||||||
|
<value>Telefonnummer</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
|
<value>Empfänger hinzufügen</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
|
<value>Empfänger löschen</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddFile" xml:space="preserve">
|
||||||
|
<value>Datei hinzufügen</value>
|
||||||
|
</data>
|
||||||
|
<data name="MergeFiles" xml:space="preserve">
|
||||||
|
<value>Dateien zusammenführen</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteFile" xml:space="preserve">
|
||||||
|
<value>Datei löschen</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowFile" xml:space="preserve">
|
||||||
|
<value>Datei anzeigen</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditFields" xml:space="preserve">
|
||||||
|
<value>Felder bearbeiten</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditData" xml:space="preserve">
|
||||||
|
<value>Daten bearbeiten</value>
|
||||||
|
</data>
|
||||||
|
<data name="Save" xml:space="preserve">
|
||||||
|
<value>Speichern</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendEnvelope" xml:space="preserve">
|
||||||
|
<value>Umschlag versenden</value>
|
||||||
|
</data>
|
||||||
|
<data name="Cancel" xml:space="preserve">
|
||||||
|
<value>Abbrechen</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddSignature" xml:space="preserve">
|
||||||
|
<value>Signatur hinzufügen</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteSignature" xml:space="preserve">
|
||||||
|
<value>Signatur löschen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language" xml:space="preserve">
|
||||||
|
<value>Sprache</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseAccessCode" xml:space="preserve">
|
||||||
|
<value>Zugangscode verwenden</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorEnabled" xml:space="preserve">
|
||||||
|
<value>2-Faktor-Authentifizierung aktiviert</value>
|
||||||
|
</data>
|
||||||
|
<data name="CertificationType" xml:space="preserve">
|
||||||
|
<value>Zertifizierungstyp</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||||
|
<value>Finale E-Mail an Ersteller</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||||
|
<value>Finale E-Mail an Empfänger</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendReminderEmails" xml:space="preserve">
|
||||||
|
<value>Erinnerungs-E-Mails senden</value>
|
||||||
|
</data>
|
||||||
|
<data name="FirstReminderDays" xml:space="preserve">
|
||||||
|
<value>Erste Erinnerung (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||||
|
<value>Erinnerungsintervall (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||||
|
<value>Läuft ab nach (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWarningDays" xml:space="preserve">
|
||||||
|
<value>Ablaufwarnung (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message" xml:space="preserve">
|
||||||
|
<value>Nachricht</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeType" xml:space="preserve">
|
||||||
|
<value>Umschlagtyp</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllOptions" xml:space="preserve">
|
||||||
|
<value>Alle Optionen</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReason" xml:space="preserve">
|
||||||
|
<value>Grund für Löschung</value>
|
||||||
|
</data>
|
||||||
|
<data name="PleaseProvideReason" xml:space="preserve">
|
||||||
|
<value>Bitte geben Sie einen Grund an</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status" xml:space="preserve">
|
||||||
|
<value>Status</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -477,4 +477,178 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Confirmations</value>
|
<value>Confirmations</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="NewEnvelope" xml:space="preserve">
|
||||||
|
<value>New Envelope</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoadEnvelope" xml:space="preserve">
|
||||||
|
<value>Load Envelope</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteEnvelope" xml:space="preserve">
|
||||||
|
<value>Delete Envelope</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshData" xml:space="preserve">
|
||||||
|
<value>Reload Data</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshedAt" xml:space="preserve">
|
||||||
|
<value>Refreshed: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowDocument" xml:space="preserve">
|
||||||
|
<value>Show Document</value>
|
||||||
|
</data>
|
||||||
|
<data name="ContactReceiver" xml:space="preserve">
|
||||||
|
<value>Contact Receiver</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeId" xml:space="preserve">
|
||||||
|
<value>Envelope-ID: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenLogDirectory" xml:space="preserve">
|
||||||
|
<value>Open Log Directory</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowResultsReport" xml:space="preserve">
|
||||||
|
<value>Show Results Report</value>
|
||||||
|
</data>
|
||||||
|
<data name="SupportMail" xml:space="preserve">
|
||||||
|
<value>Support Mail</value>
|
||||||
|
</data>
|
||||||
|
<data name="ResendInvitation" xml:space="preserve">
|
||||||
|
<value>Send Invitation Again</value>
|
||||||
|
</data>
|
||||||
|
<data name="Export" xml:space="preserve">
|
||||||
|
<value>Export</value>
|
||||||
|
</data>
|
||||||
|
<data name="Receivers" xml:space="preserve">
|
||||||
|
<value>Receivers</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailSalutation" xml:space="preserve">
|
||||||
|
<value>Email Salutation</value>
|
||||||
|
</data>
|
||||||
|
<data name="SignedWhen" xml:space="preserve">
|
||||||
|
<value>Signed When</value>
|
||||||
|
</data>
|
||||||
|
<data name="AccessCode" xml:space="preserve">
|
||||||
|
<value>Access Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="User" xml:space="preserve">
|
||||||
|
<value>User</value>
|
||||||
|
</data>
|
||||||
|
<data name="Type" xml:space="preserve">
|
||||||
|
<value>Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="Title" xml:space="preserve">
|
||||||
|
<value>Title</value>
|
||||||
|
</data>
|
||||||
|
<data name="CreatedOn" xml:space="preserve">
|
||||||
|
<value>Created On</value>
|
||||||
|
</data>
|
||||||
|
<data name="LastModified" xml:space="preserve">
|
||||||
|
<value>Last Modified</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenEnvelopes" xml:space="preserve">
|
||||||
|
<value>Open Envelopes</value>
|
||||||
|
</data>
|
||||||
|
<data name="CompletedEnvelopes" xml:space="preserve">
|
||||||
|
<value>Completed Envelopes</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendAccessCode" xml:space="preserve">
|
||||||
|
<value>Send Access Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorProperties" xml:space="preserve">
|
||||||
|
<value>2-Factor Properties</value>
|
||||||
|
</data>
|
||||||
|
<data name="Name" xml:space="preserve">
|
||||||
|
<value>Name</value>
|
||||||
|
</data>
|
||||||
|
<data name="PhoneNumber" xml:space="preserve">
|
||||||
|
<value>Phone Number</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
|
<value>Add Receiver</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
|
<value>Delete Receiver</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddFile" xml:space="preserve">
|
||||||
|
<value>Add File</value>
|
||||||
|
</data>
|
||||||
|
<data name="MergeFiles" xml:space="preserve">
|
||||||
|
<value>Merge Files</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteFile" xml:space="preserve">
|
||||||
|
<value>Delete File</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowFile" xml:space="preserve">
|
||||||
|
<value>Show File</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditFields" xml:space="preserve">
|
||||||
|
<value>Edit Fields</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditData" xml:space="preserve">
|
||||||
|
<value>Edit Data</value>
|
||||||
|
</data>
|
||||||
|
<data name="Save" xml:space="preserve">
|
||||||
|
<value>Save</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendEnvelope" xml:space="preserve">
|
||||||
|
<value>Send Envelope</value>
|
||||||
|
</data>
|
||||||
|
<data name="Cancel" xml:space="preserve">
|
||||||
|
<value>Cancel</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddSignature" xml:space="preserve">
|
||||||
|
<value>Add Signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteSignature" xml:space="preserve">
|
||||||
|
<value>Delete Signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language" xml:space="preserve">
|
||||||
|
<value>Language</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseAccessCode" xml:space="preserve">
|
||||||
|
<value>Use Access Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorEnabled" xml:space="preserve">
|
||||||
|
<value>2-Factor Authentication Enabled</value>
|
||||||
|
</data>
|
||||||
|
<data name="CertificationType" xml:space="preserve">
|
||||||
|
<value>Certification Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||||
|
<value>Final Email to Creator</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||||
|
<value>Final Email to Receivers</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendReminderEmails" xml:space="preserve">
|
||||||
|
<value>Send Reminder Emails</value>
|
||||||
|
</data>
|
||||||
|
<data name="FirstReminderDays" xml:space="preserve">
|
||||||
|
<value>First Reminder (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||||
|
<value>Reminder Interval (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||||
|
<value>Expires After (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWarningDays" xml:space="preserve">
|
||||||
|
<value>Expiry Warning (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message" xml:space="preserve">
|
||||||
|
<value>Message</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeType" xml:space="preserve">
|
||||||
|
<value>Envelope Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllOptions" xml:space="preserve">
|
||||||
|
<value>All Options</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReason" xml:space="preserve">
|
||||||
|
<value>Deletion Reason</value>
|
||||||
|
</data>
|
||||||
|
<data name="PleaseProvideReason" xml:space="preserve">
|
||||||
|
<value>Please provide a reason</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status" xml:space="preserve">
|
||||||
|
<value>Status</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -477,4 +477,178 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Confirmations</value>
|
<value>Confirmations</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="NewEnvelope" xml:space="preserve">
|
||||||
|
<value>Nouvelle enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoadEnvelope" xml:space="preserve">
|
||||||
|
<value>Charger l'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteEnvelope" xml:space="preserve">
|
||||||
|
<value>Supprimer l'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshData" xml:space="preserve">
|
||||||
|
<value>Actualiser les données</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshedAt" xml:space="preserve">
|
||||||
|
<value>Actualisé : {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowDocument" xml:space="preserve">
|
||||||
|
<value>Afficher le document</value>
|
||||||
|
</data>
|
||||||
|
<data name="ContactReceiver" xml:space="preserve">
|
||||||
|
<value>Contacter le destinataire</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeId" xml:space="preserve">
|
||||||
|
<value>ID d'enveloppe : {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenLogDirectory" xml:space="preserve">
|
||||||
|
<value>Ouvrir le répertoire des logs</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowResultsReport" xml:space="preserve">
|
||||||
|
<value>Afficher le rapport de résultats</value>
|
||||||
|
</data>
|
||||||
|
<data name="SupportMail" xml:space="preserve">
|
||||||
|
<value>E-mail de support</value>
|
||||||
|
</data>
|
||||||
|
<data name="ResendInvitation" xml:space="preserve">
|
||||||
|
<value>Renvoyer l'invitation</value>
|
||||||
|
</data>
|
||||||
|
<data name="Export" xml:space="preserve">
|
||||||
|
<value>Exporter</value>
|
||||||
|
</data>
|
||||||
|
<data name="Receivers" xml:space="preserve">
|
||||||
|
<value>Destinataires</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailSalutation" xml:space="preserve">
|
||||||
|
<value>Formule de politesse</value>
|
||||||
|
</data>
|
||||||
|
<data name="SignedWhen" xml:space="preserve">
|
||||||
|
<value>Signé quand</value>
|
||||||
|
</data>
|
||||||
|
<data name="AccessCode" xml:space="preserve">
|
||||||
|
<value>Code d'accès</value>
|
||||||
|
</data>
|
||||||
|
<data name="User" xml:space="preserve">
|
||||||
|
<value>Utilisateur</value>
|
||||||
|
</data>
|
||||||
|
<data name="Type" xml:space="preserve">
|
||||||
|
<value>Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="Title" xml:space="preserve">
|
||||||
|
<value>Titre</value>
|
||||||
|
</data>
|
||||||
|
<data name="CreatedOn" xml:space="preserve">
|
||||||
|
<value>Créé le</value>
|
||||||
|
</data>
|
||||||
|
<data name="LastModified" xml:space="preserve">
|
||||||
|
<value>Dernière modification</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenEnvelopes" xml:space="preserve">
|
||||||
|
<value>Enveloppes ouvertes</value>
|
||||||
|
</data>
|
||||||
|
<data name="CompletedEnvelopes" xml:space="preserve">
|
||||||
|
<value>Enveloppes terminées</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendAccessCode" xml:space="preserve">
|
||||||
|
<value>Envoyer le code d'accès</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorProperties" xml:space="preserve">
|
||||||
|
<value>Propriétés 2-facteurs</value>
|
||||||
|
</data>
|
||||||
|
<data name="Name" xml:space="preserve">
|
||||||
|
<value>Nom</value>
|
||||||
|
</data>
|
||||||
|
<data name="PhoneNumber" xml:space="preserve">
|
||||||
|
<value>Numéro de téléphone</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
|
<value>Ajouter un destinataire</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
|
<value>Supprimer le destinataire</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddFile" xml:space="preserve">
|
||||||
|
<value>Ajouter un fichier</value>
|
||||||
|
</data>
|
||||||
|
<data name="MergeFiles" xml:space="preserve">
|
||||||
|
<value>Fusionner les fichiers</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteFile" xml:space="preserve">
|
||||||
|
<value>Supprimer le fichier</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowFile" xml:space="preserve">
|
||||||
|
<value>Afficher le fichier</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditFields" xml:space="preserve">
|
||||||
|
<value>Modifier les champs</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditData" xml:space="preserve">
|
||||||
|
<value>Modifier les données</value>
|
||||||
|
</data>
|
||||||
|
<data name="Save" xml:space="preserve">
|
||||||
|
<value>Enregistrer</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendEnvelope" xml:space="preserve">
|
||||||
|
<value>Envoyer l'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="Cancel" xml:space="preserve">
|
||||||
|
<value>Annuler</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddSignature" xml:space="preserve">
|
||||||
|
<value>Ajouter une signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteSignature" xml:space="preserve">
|
||||||
|
<value>Supprimer la signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language" xml:space="preserve">
|
||||||
|
<value>Langue</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseAccessCode" xml:space="preserve">
|
||||||
|
<value>Utiliser un code d'accès</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorEnabled" xml:space="preserve">
|
||||||
|
<value>Authentification à 2 facteurs activée</value>
|
||||||
|
</data>
|
||||||
|
<data name="CertificationType" xml:space="preserve">
|
||||||
|
<value>Type de certification</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||||
|
<value>E-mail final au créateur</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||||
|
<value>E-mail final aux destinataires</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendReminderEmails" xml:space="preserve">
|
||||||
|
<value>Envoyer des e-mails de rappel</value>
|
||||||
|
</data>
|
||||||
|
<data name="FirstReminderDays" xml:space="preserve">
|
||||||
|
<value>Premier rappel (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||||
|
<value>Intervalle de rappel (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||||
|
<value>Expire après (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWarningDays" xml:space="preserve">
|
||||||
|
<value>Avertissement d'expiration (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message" xml:space="preserve">
|
||||||
|
<value>Message</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeType" xml:space="preserve">
|
||||||
|
<value>Type d'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllOptions" xml:space="preserve">
|
||||||
|
<value>Toutes les options</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReason" xml:space="preserve">
|
||||||
|
<value>Motif de suppression</value>
|
||||||
|
</data>
|
||||||
|
<data name="PleaseProvideReason" xml:space="preserve">
|
||||||
|
<value>Veuillez indiquer une raison</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status" xml:space="preserve">
|
||||||
|
<value>Statut</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
Imports System.IO
|
Imports System.IO
|
||||||
Imports DigitalData.Modules.Database
|
Imports DigitalData.Modules.Database
|
||||||
Imports DigitalData.Modules.Logging
|
Imports DigitalData.Modules.Logging
|
||||||
|
Imports EnvelopeGenerator.CommonServices
|
||||||
Imports EnvelopeGenerator.CommonServices.Jobs
|
Imports EnvelopeGenerator.CommonServices.Jobs
|
||||||
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument
|
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument
|
||||||
Imports GdPicture14
|
Imports GdPicture14
|
||||||
@@ -145,64 +146,91 @@ Public Class frmFinalizePDF
|
|||||||
log.AppendLine($"Document bytes: {oBuffer.Length}")
|
log.AppendLine($"Document bytes: {oBuffer.Length}")
|
||||||
|
|
||||||
' 3. Check what BurnAnnotsToPDF will do internally
|
' 3. Check what BurnAnnotsToPDF will do internally
|
||||||
|
Dim oEnvelopeEntity As Envelope = Nothing
|
||||||
Using scope = Factory.Shared.ScopeFactory.CreateScope()
|
Using scope = Factory.Shared.ScopeFactory.CreateScope()
|
||||||
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
|
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
|
||||||
Dim envelope = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
|
oEnvelopeEntity = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
|
||||||
|
|
||||||
If envelope Is Nothing Then
|
If oEnvelopeEntity Is Nothing Then
|
||||||
log.AppendLine("ERROR: Envelope not found in EF Core!")
|
log.AppendLine("ERROR: Envelope not found in EF Core!")
|
||||||
txtResult.Text = log.ToString()
|
txtResult.Text = log.ToString()
|
||||||
Return
|
Return
|
||||||
End If
|
End If
|
||||||
|
|
||||||
log.AppendLine($"Envelope found: Id={envelope.Id}, EnvelopeTypeId={envelope.EnvelopeTypeId}")
|
log.AppendLine($"Envelope found: Id={oEnvelopeEntity.Id}, EnvelopeTypeId={oEnvelopeEntity.EnvelopeTypeId}")
|
||||||
log.AppendLine($"ReadOnly (IsReadAndConfirm): {envelope.ReadOnly}")
|
log.AppendLine($"ReadOnly (IsReadAndConfirm): {oEnvelopeEntity.ReadOnly}")
|
||||||
|
|
||||||
If envelope.ReadOnly Then
|
If oEnvelopeEntity.ReadOnly Then
|
||||||
log.AppendLine(">>> EARLY RETURN: ReadOnly=True, original PDF returned without burning")
|
log.AppendLine(">>> ReadOnly=True: original PDF will be used without burning (annotations skipped)")
|
||||||
txtResult.Text = log.ToString()
|
|
||||||
Return
|
|
||||||
End If
|
|
||||||
|
|
||||||
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
|
|
||||||
Dim elements = sigRepo _
|
|
||||||
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
|
||||||
.Include(Function(sig) sig.Annotations) _
|
|
||||||
.ToList()
|
|
||||||
|
|
||||||
log.AppendLine($"Elements (Signature) count: {elements.Count}")
|
|
||||||
|
|
||||||
If elements.Any() Then
|
|
||||||
log.AppendLine(">>> PATH: BurnElementAnnotsToPDF (new element-based path)")
|
|
||||||
For Each elem In elements
|
|
||||||
Dim annotCount = If(elem.Annotations IsNot Nothing, elem.Annotations.Count(), 0)
|
|
||||||
log.AppendLine($" Element Id={elem.Id}, Page={elem.Page}, X={elem.X}, Y={elem.Y}, W={elem.Width}, H={elem.Height}, Annotations={annotCount}")
|
|
||||||
If elem.Annotations IsNot Nothing Then
|
|
||||||
For Each annot In elem.Annotations
|
|
||||||
log.AppendLine($" Annot: Name={annot.Name}, Type={annot.Type}, X={annot.X}, Y={annot.Y}, W={annot.Width}, H={annot.Height}")
|
|
||||||
Next
|
|
||||||
End If
|
|
||||||
Next
|
|
||||||
Else
|
Else
|
||||||
log.AppendLine(">>> PATH: BurnInstantJSONAnnotsToPDF (old JSON-based path)")
|
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
|
||||||
|
Dim elements = sigRepo _
|
||||||
|
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
||||||
|
.Include(Function(sig) sig.Annotations) _
|
||||||
|
.ToList()
|
||||||
|
|
||||||
|
log.AppendLine($"Elements (Signature) count: {elements.Count}")
|
||||||
|
|
||||||
|
If elements.Any() Then
|
||||||
|
log.AppendLine(">>> PATH: BurnElementAnnotsToPDF (new element-based path)")
|
||||||
|
For Each elem In elements
|
||||||
|
Dim annotCount = If(elem.Annotations IsNot Nothing, elem.Annotations.Count(), 0)
|
||||||
|
log.AppendLine($" Element Id={elem.Id}, Page={elem.Page}, X={elem.X}, Y={elem.Y}, W={elem.Width}, H={elem.Height}, Annotations={annotCount}")
|
||||||
|
If elem.Annotations IsNot Nothing Then
|
||||||
|
For Each annot In elem.Annotations
|
||||||
|
log.AppendLine($" Annot: Name={annot.Name}, Type={annot.Type}, X={annot.X}, Y={annot.Y}, W={annot.Width}, H={annot.Height}")
|
||||||
|
Next
|
||||||
|
End If
|
||||||
|
Next
|
||||||
|
Else
|
||||||
|
log.AppendLine(">>> PATH: BurnInstantJSONAnnotsToPDF (old JSON-based path)")
|
||||||
|
End If
|
||||||
End If
|
End If
|
||||||
End Using
|
End Using
|
||||||
|
|
||||||
' 4. Actually call BurnAnnotsToPDF (same as Service)
|
' 4. Burn annotations (ReadOnly envelopes return original PDF unchanged)
|
||||||
log.AppendLine("")
|
log.AppendLine("")
|
||||||
log.AppendLine("Calling BurnAnnotsToPDF...")
|
log.AppendLine("Calling BurnAnnotsToPDF...")
|
||||||
Dim oNewBuffer = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
|
Dim oBurnedBuffer As Byte() = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
|
||||||
log.AppendLine($"Result bytes: {oNewBuffer.Length}")
|
log.AppendLine($"Burned bytes: {oBurnedBuffer.Length}")
|
||||||
log.AppendLine($"Same as input: {oBuffer.Length = oNewBuffer.Length AndAlso oBuffer.SequenceEqual(oNewBuffer)}")
|
log.AppendLine($"Unchanged (ReadOnly): {oBuffer.Length = oBurnedBuffer.Length AndAlso oBuffer.SequenceEqual(oBurnedBuffer)}")
|
||||||
|
|
||||||
' 5. Write output
|
' 5. Build State and load Envelope domain model for ReportCreator
|
||||||
|
log.AppendLine("")
|
||||||
|
log.AppendLine("Loading envelope domain model for report...")
|
||||||
|
Dim oState As New State() With {
|
||||||
|
.Database = Database,
|
||||||
|
.LogConfig = LogConfig
|
||||||
|
}
|
||||||
|
Dim oEnvelopeModel As New EnvelopeModel(oState)
|
||||||
|
Dim oEnvelope = oEnvelopeModel.GetById(envelopeId)
|
||||||
|
If oEnvelope Is Nothing Then
|
||||||
|
log.AppendLine("ERROR: Envelope not found via EnvelopeModel!")
|
||||||
|
txtResult.Text = log.ToString()
|
||||||
|
Return
|
||||||
|
End If
|
||||||
|
log.AppendLine($"Envelope loaded: {oEnvelope.Id}")
|
||||||
|
|
||||||
|
' 6. Create signing certificate report
|
||||||
|
log.AppendLine("Creating signing certificate report...")
|
||||||
|
Dim oReportCreator As New ReportCreator(LogConfig, oState)
|
||||||
|
Dim oReportBuffer As Byte() = oReportCreator.CreateReport(oEnvelope)
|
||||||
|
log.AppendLine($"Report bytes: {oReportBuffer.Length}")
|
||||||
|
|
||||||
|
' 7. Merge burned document + report (last page)
|
||||||
|
log.AppendLine("Merging documents...")
|
||||||
|
Dim oMerger As New PDFMerger(LogConfig, pGDPictureLicenseKey)
|
||||||
|
Dim oMergedBuffer As Byte() = oMerger.MergeDocuments(oBurnedBuffer, oReportBuffer)
|
||||||
|
log.AppendLine($"Merged bytes: {oMergedBuffer.Length}")
|
||||||
|
|
||||||
|
' 8. Save and open
|
||||||
Dim desktopPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
|
Dim desktopPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
|
||||||
Dim oNewPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.burned.pdf")
|
Dim oMergedPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.merged.pdf")
|
||||||
File.WriteAllBytes(oNewPath, oNewBuffer)
|
File.WriteAllBytes(oMergedPath, oMergedBuffer)
|
||||||
log.AppendLine($"Output: {oNewPath}")
|
log.AppendLine($"Output: {oMergedPath}")
|
||||||
|
|
||||||
txtResult.Text = log.ToString()
|
txtResult.Text = log.ToString()
|
||||||
Process.Start(oNewPath)
|
Process.Start(oMergedPath)
|
||||||
|
|
||||||
Catch ex As Exception
|
Catch ex As Exception
|
||||||
txtResult.Text = $"ERROR: {ex.Message}{vbCrLf}{vbCrLf}{ex.ToString()}"
|
txtResult.Text = $"ERROR: {ex.Message}{vbCrLf}{vbCrLf}{ex.ToString()}"
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Dim y = frameY + yOffsetofFF
|
Dim y = frameY + yOffsetofFF
|
||||||
|
|
||||||
If annot.Type = AnnotationType.FormField Then
|
If annot.Type = AnnotationType.FormField Then
|
||||||
AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value)
|
AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value, annot.Name)
|
||||||
ElseIf annot.Type = AnnotationType.Image Then
|
ElseIf annot.Type = AnnotationType.Image Then
|
||||||
AddImageAnnotation(
|
AddImageAnnotation(
|
||||||
annot.X / inchFactor,
|
annot.X / inchFactor,
|
||||||
@@ -269,11 +269,14 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Next
|
Next
|
||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Private Sub AddFormFieldValue(x As Double, y As Double, width As Double, height As Double, page As Integer, value As String)
|
Private Sub AddFormFieldValue(x As Double, y As Double, width As Double, height As Double, page As Integer, value As String, Optional egName As String = "")
|
||||||
Manager.SelectPage(page)
|
Manager.SelectPage(page)
|
||||||
|
|
||||||
|
' Truncate if needed
|
||||||
|
Dim displayValue As String = TruncateText(value, egName)
|
||||||
|
|
||||||
' Add the text annotation
|
' Add the text annotation
|
||||||
Dim ant = Manager.AddTextAnnot(x, y, width, height, value)
|
Dim ant = Manager.AddTextAnnot(x, y, width, height, displayValue)
|
||||||
|
|
||||||
' Set the font properties
|
' Set the font properties
|
||||||
ant.FontName = _pdfBurnerParams.FontName
|
ant.FontName = _pdfBurnerParams.FontName
|
||||||
@@ -294,8 +297,9 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Dim oHeight = oBounds.Item(3)
|
Dim oHeight = oBounds.Item(3)
|
||||||
|
|
||||||
Manager.SelectPage(pAnnotation.pageIndex + 1)
|
Manager.SelectPage(pAnnotation.pageIndex + 1)
|
||||||
' Add the text annotation
|
' Add the text annotation (truncate if needed)
|
||||||
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, formFieldValue.value)
|
Dim displayValue As String = TruncateText(formFieldValue.value, pAnnotation.egName)
|
||||||
|
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, displayValue)
|
||||||
|
|
||||||
' Set the font properties
|
' Set the font properties
|
||||||
ant.FontName = _pdfBurnerParams.FontName
|
ant.FontName = _pdfBurnerParams.FontName
|
||||||
@@ -306,6 +310,48 @@ Namespace Jobs.FinalizeDocument
|
|||||||
#End Region
|
#End Region
|
||||||
|
|
||||||
#Region "Helpers"
|
#Region "Helpers"
|
||||||
|
''' <summary>
|
||||||
|
''' Truncates a field value so its rendered width fits within the configured max width (inches).
|
||||||
|
''' Uses Graphics.MeasureString with the configured font for pixel-accurate measurement.
|
||||||
|
''' Appends TruncationSuffix when the value is cut.
|
||||||
|
''' </summary>
|
||||||
|
Private Function TruncateText(value As String, fieldEgName As String) As String
|
||||||
|
If String.IsNullOrEmpty(value) Then Return value
|
||||||
|
If _pdfBurnerParams.TextMaxWidths Is Nothing Then Return value
|
||||||
|
|
||||||
|
Dim maxWidthInches As Single = 0
|
||||||
|
If Not _pdfBurnerParams.TextMaxWidths.TryGetValue(fieldEgName, maxWidthInches) Then Return value
|
||||||
|
If maxWidthInches <= 0 Then Return value
|
||||||
|
|
||||||
|
Dim suffix As String = If(_pdfBurnerParams.TruncationSuffix, "")
|
||||||
|
|
||||||
|
Using bmp As New Bitmap(1, 1)
|
||||||
|
Using g As Graphics = Graphics.FromImage(bmp)
|
||||||
|
Dim font As New Font(_pdfBurnerParams.FontName, _pdfBurnerParams.FontSize, _pdfBurnerParams.FontStyle, GraphicsUnit.Point)
|
||||||
|
Dim maxWidthPx As Single = maxWidthInches * g.DpiX
|
||||||
|
|
||||||
|
' Already fits — no truncation needed
|
||||||
|
If g.MeasureString(value, font).Width <= maxWidthPx Then Return value
|
||||||
|
|
||||||
|
' Binary search for the longest prefix that fits (with suffix)
|
||||||
|
Dim lo As Integer = 0
|
||||||
|
Dim hi As Integer = value.Length
|
||||||
|
While lo < hi
|
||||||
|
Dim mid As Integer = (lo + hi + 1) \ 2
|
||||||
|
Dim candidate As String = value.Substring(0, mid) & suffix
|
||||||
|
If g.MeasureString(candidate, font).Width <= maxWidthPx Then
|
||||||
|
lo = mid
|
||||||
|
Else
|
||||||
|
hi = mid - 1
|
||||||
|
End If
|
||||||
|
End While
|
||||||
|
|
||||||
|
Return value.Substring(0, lo) & suffix
|
||||||
|
End Using
|
||||||
|
End Using
|
||||||
|
End Function
|
||||||
|
|
||||||
|
|
||||||
Private Function ToPointF(pPoints As List(Of Single)) As PointF
|
Private Function ToPointF(pPoints As List(Of Single)) As PointF
|
||||||
Dim oPoints = pPoints.Select(AddressOf ToInches).ToList()
|
Dim oPoints = pPoints.Select(AddressOf ToInches).ToList()
|
||||||
Return New PointF(oPoints.Item(0), oPoints.Item(1))
|
Return New PointF(oPoints.Item(0), oPoints.Item(1))
|
||||||
|
|||||||
@@ -12,5 +12,24 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Public Property FontSize As Integer = 8
|
Public Property FontSize As Integer = 8
|
||||||
|
|
||||||
Public Property FontStyle As FontStyle = FontStyle.Italic
|
Public Property FontStyle As FontStyle = FontStyle.Italic
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' The suffix appended to a truncated field value. Defaults to "..".
|
||||||
|
''' Can be changed to any string (e.g. "…", "...", "").
|
||||||
|
''' </summary>
|
||||||
|
Public Property TruncationSuffix As String = ".."
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Per-field maximum render width in inches.
|
||||||
|
''' Text is measured using the configured Font/FontSize via Graphics.MeasureString (pixel-accurate).
|
||||||
|
''' If the rendered width exceeds the limit, the text is cut and TruncationSuffix is appended.
|
||||||
|
''' Keys must match egName values used in PDFBurner (e.g. "name", "position", "city", "date").
|
||||||
|
''' Set a value to 0 to disable truncation for that field.
|
||||||
|
''' </summary>
|
||||||
|
Public Property TextMaxWidths As New Dictionary(Of String, Single) From {
|
||||||
|
{"position", 1.41F},
|
||||||
|
{"city", 1.41F},
|
||||||
|
{"date", 2.0F}
|
||||||
|
}
|
||||||
End Class
|
End Class
|
||||||
End Namespace
|
End Namespace
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
Imports EnvelopeGenerator.Domain.Constants
|
Imports EnvelopeGenerator.Domain.Constants
|
||||||
Imports EnvelopeGenerator.Domain.Entities
|
Imports EnvelopeGenerator.Domain.Entities
|
||||||
|
Imports EnvelopeGenerator.Domain.Interfaces
|
||||||
|
|
||||||
Public Class ReportItem
|
Public Class ReportItem
|
||||||
|
|
||||||
@@ -11,8 +12,15 @@ Public Class ReportItem
|
|||||||
Public Property ItemStatus As EnvelopeStatus
|
Public Property ItemStatus As EnvelopeStatus
|
||||||
Public ReadOnly Property ItemStatusTranslated As String
|
Public ReadOnly Property ItemStatusTranslated As String
|
||||||
Get
|
Get
|
||||||
Dim oStatus = ItemStatus.ToString()
|
Dim oStatusKey = ItemStatus.ToString()
|
||||||
Return My.Resources.Model.ResourceManager.GetString(oStatus)
|
|
||||||
|
If ItemStatus = EnvelopeStatus.DocumentSigned AndAlso
|
||||||
|
Envelope IsNot Nothing AndAlso
|
||||||
|
Envelope.IsReadAndConfirm() Then
|
||||||
|
oStatusKey = "DocumentSignedRaC"
|
||||||
|
End If
|
||||||
|
|
||||||
|
Return My.Resources.Model.ResourceManager.GetString(oStatusKey)
|
||||||
End Get
|
End Get
|
||||||
End Property
|
End Property
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,9 @@
|
|||||||
<data name="DocumentSigned" xml:space="preserve">
|
<data name="DocumentSigned" xml:space="preserve">
|
||||||
<value>Document signed</value>
|
<value>Document signed</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||||
|
<value>Read and confirmed</value>
|
||||||
|
</data>
|
||||||
<data name="Draft" xml:space="preserve">
|
<data name="Draft" xml:space="preserve">
|
||||||
<value>Draft</value>
|
<value>Draft</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -168,6 +168,9 @@
|
|||||||
<data name="DocumentSigned" xml:space="preserve">
|
<data name="DocumentSigned" xml:space="preserve">
|
||||||
<value>Dokument unterzeichnet</value>
|
<value>Dokument unterzeichnet</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||||
|
<value>Gelesen und bestätigt</value>
|
||||||
|
</data>
|
||||||
<data name="Draft" xml:space="preserve">
|
<data name="Draft" xml:space="preserve">
|
||||||
<value>Entwurf</value>
|
<value>Entwurf</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Domain.Constants
|
namespace EnvelopeGenerator.Domain.Constants
|
||||||
{
|
{
|
||||||
// http://wiki.dd/xwiki13/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/SignFlow/Envelope%20Status/
|
// http://wiki.dd/xwiki_prod/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/signFLOW/signFLOW%20-%20Enwickler-Handbuch/4.%20Anhang/4.3%20Historie%20und%20Status%20der%20Umschl%C3%A4ge/
|
||||||
public enum EnvelopeStatus
|
public enum EnvelopeStatus
|
||||||
{
|
{
|
||||||
Invalid = 0,
|
Invalid = 0,
|
||||||
@@ -49,5 +51,28 @@ namespace EnvelopeGenerator.Domain.Constants
|
|||||||
EnvelopeStatus.EnvelopeCreated,
|
EnvelopeStatus.EnvelopeCreated,
|
||||||
EnvelopeStatus.DocumentMod_Rotation
|
EnvelopeStatus.DocumentMod_Rotation
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static readonly List<EnvelopeStatus> Active = Enum.GetValues(typeof(EnvelopeStatus))
|
||||||
|
.Cast<EnvelopeStatus>()
|
||||||
|
.Where(status => status.IsActive())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
public static readonly List<EnvelopeStatus> Completed = Enum.GetValues(typeof(EnvelopeStatus))
|
||||||
|
.Cast<EnvelopeStatus>()
|
||||||
|
.Where(status => status.IsCompleted())
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EnvelopeStatusExtensions
|
||||||
|
{
|
||||||
|
public static bool IsActive(this EnvelopeStatus status)
|
||||||
|
{
|
||||||
|
return status >= EnvelopeStatus.EnvelopeCreated && status < EnvelopeStatus.EnvelopePartlySigned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsCompleted(this EnvelopeStatus status)
|
||||||
|
{
|
||||||
|
return status >= EnvelopeStatus.EnvelopeCompletelySigned && status <= EnvelopeStatus.EnvelopeWithdrawn;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Domain.Constants
|
namespace EnvelopeGenerator.Domain.Constants
|
||||||
@@ -15,7 +16,11 @@ namespace EnvelopeGenerator.Domain.Constants
|
|||||||
public static class Receiver
|
public static class Receiver
|
||||||
{
|
{
|
||||||
public const string TFA = "EGReceiverTFA";
|
public const string TFA = "EGReceiverTFA";
|
||||||
|
|
||||||
|
[Obsolete("Use Receiver.Authorized")]
|
||||||
public const string Full = "EGReceiver";
|
public const string Full = "EGReceiver";
|
||||||
|
|
||||||
|
public static readonly IReadOnlyList<string> Authorized = new[] { "receiver", "EGReceiver" };
|
||||||
}
|
}
|
||||||
|
|
||||||
public const string Sender = "EGSender";
|
public const string Sender = "EGSender";
|
||||||
|
|||||||
@@ -213,6 +213,15 @@ namespace My.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Gelesen und bestätigt.
|
||||||
|
/// </summary>
|
||||||
|
public static string DocumentSignedRaC {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DocumentSignedRaC", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Entwurf.
|
/// Looks up a localized string similar to Entwurf.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -168,6 +168,9 @@
|
|||||||
<data name="DocumentSigned" xml:space="preserve">
|
<data name="DocumentSigned" xml:space="preserve">
|
||||||
<value>Dokument unterzeichnet</value>
|
<value>Dokument unterzeichnet</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||||
|
<value>Gelesen und bestätigt</value>
|
||||||
|
</data>
|
||||||
<data name="Draft" xml:space="preserve">
|
<data name="Draft" xml:space="preserve">
|
||||||
<value>Entwurf</value>
|
<value>Entwurf</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using EnvelopeGenerator.Infrastructure.Repositories;
|
|||||||
using EnvelopeGenerator.Infrastructure.Executor;
|
using EnvelopeGenerator.Infrastructure.Executor;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
#elif NETFRAMEWORK
|
#elif NETFRAMEWORK
|
||||||
using System;
|
using System;
|
||||||
@@ -55,6 +56,9 @@ namespace EnvelopeGenerator.Infrastructure
|
|||||||
services.TryAddScoped<IEnvelopeTypeRepository, EnvelopeTypeRepository>();
|
services.TryAddScoped<IEnvelopeTypeRepository, EnvelopeTypeRepository>();
|
||||||
services.TryAddScoped<IReceiverRepository, ReceiverRepository>();
|
services.TryAddScoped<IReceiverRepository, ReceiverRepository>();
|
||||||
services.TryAddScoped<IEnvelopeReceiverReadOnlyRepository, EnvelopeReceiverReadOnlyRepository>();
|
services.TryAddScoped<IEnvelopeReceiverReadOnlyRepository, EnvelopeReceiverReadOnlyRepository>();
|
||||||
|
|
||||||
|
// User Preferences — distributed cache (dbo.TBDD_CACHE)
|
||||||
|
services.TryAddScoped<IUserPreferencesService, UserPreferencesService>();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
services.AddDbRepository(opt =>
|
services.AddDbRepository(opt =>
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
#if NET
|
|
||||||
using EnvelopeGenerator.Application.Common.Configurations;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Design;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Infrastructure
|
|
||||||
{
|
|
||||||
public class EGDbContextFactory : IDesignTimeDbContextFactory<EGDbContext>
|
|
||||||
{
|
|
||||||
public EGDbContext CreateDbContext(string[] args)
|
|
||||||
{
|
|
||||||
var config = new ConfigurationBuilder()
|
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
|
||||||
.AddJsonFile("appsettings.migration.json")
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
// create DbContextOptions
|
|
||||||
var optionsBuilder = new DbContextOptionsBuilder<EGDbContext>();
|
|
||||||
optionsBuilder.UseSqlServer(config.GetConnectionString("Default"));
|
|
||||||
|
|
||||||
// create DbTriggerParams
|
|
||||||
var triggerLists = config.GetSection("DbTriggerParams").Get<Dictionary<string, List<string>>>();
|
|
||||||
var dbTriggerParams = new DbTriggerParams();
|
|
||||||
if (triggerLists is not null)
|
|
||||||
foreach (var triggerList in triggerLists)
|
|
||||||
{
|
|
||||||
if (triggerList.Value.Count == 0)
|
|
||||||
continue; // Skip empty trigger lists
|
|
||||||
|
|
||||||
var tableName = triggerList.Key;
|
|
||||||
|
|
||||||
dbTriggerParams[tableName] = new List<string>();
|
|
||||||
|
|
||||||
foreach (var trigger in triggerList.Value)
|
|
||||||
{
|
|
||||||
dbTriggerParams[tableName].Add(trigger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var dbContext = new EGDbContext(optionsBuilder.Options, Options.Create(dbTriggerParams));
|
|
||||||
dbContext.IsMigration = true;
|
|
||||||
return dbContext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" />
|
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||||
<PackageReference Include="QuestPDF" Version="2025.7.1" />
|
<PackageReference Include="QuestPDF" Version="2025.7.1" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
|
|||||||
46
EnvelopeGenerator.Infrastructure/UserPreferencesService.cs
Normal file
46
EnvelopeGenerator.Infrastructure/UserPreferencesService.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#if NET
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Infrastructure;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implements user preferences storage using the distributed cache (dbo.TBDD_CACHE).
|
||||||
|
/// No DB schema changes required — uses the existing SQL Server distributed cache table.
|
||||||
|
/// Cache key schema: "signflow.user-preferences:{username}"
|
||||||
|
/// Expiration: indefinite (no TTL) — preferences persist until explicitly overwritten.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UserPreferencesService(IDistributedCache cache) : IUserPreferencesService
|
||||||
|
{
|
||||||
|
private const string KeyPrefix = "signflow.user-preferences:";
|
||||||
|
|
||||||
|
private static string BuildKey(string id) => $"{KeyPrefix}{id}";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<UserPreferencesDto> GetOrCreateAsync(string id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var json = await cache.GetStringAsync(BuildKey(id), ct);
|
||||||
|
if(json is not null && JsonSerializer.Deserialize<UserPreferencesDto>(json) is UserPreferencesDto pref)
|
||||||
|
{
|
||||||
|
return pref;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// init new
|
||||||
|
await SaveAsync(id, UserPreferencesDto.Default, ct);
|
||||||
|
return UserPreferencesDto.Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task SaveAsync(string id, UserPreferencesDto preferences, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(preferences);
|
||||||
|
|
||||||
|
// No expiration — preferences are stored indefinitely.
|
||||||
|
await cache.SetStringAsync(BuildKey(id), json, new DistributedCacheEntryOptions(), ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<Router AppAssembly="@typeof(Program).Assembly">
|
|
||||||
<Found Context="routeData">
|
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
|
||||||
</Found>
|
|
||||||
<NotFound>
|
|
||||||
<LayoutView Layout="@typeof(MainLayout)">
|
|
||||||
<p>Sorry, there's nothing at this address.</p>
|
|
||||||
</LayoutView>
|
|
||||||
</NotFound>
|
|
||||||
</Router>
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
|
||||||
public class Adjustment
|
|
||||||
{
|
|
||||||
public static Adjustment CreateBalanceForward(DateTime dt, int random)
|
|
||||||
{
|
|
||||||
var rnd = new DeterministicRandom(random);
|
|
||||||
Adjustment res = new Adjustment();
|
|
||||||
res.currentDateTime = dt;
|
|
||||||
res.currentDescription = "Balance Forward";
|
|
||||||
res.currentAmount = rnd.Random(10, 300) * 10;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
public static Adjustment CreatePayment(DateTime dt, int random)
|
|
||||||
{
|
|
||||||
var rnd = new DeterministicRandom(random);
|
|
||||||
Adjustment res = new Adjustment();
|
|
||||||
res.currentDateTime = dt;
|
|
||||||
res.currentDescription = "Payment";
|
|
||||||
res.currentAmount = -rnd.Random(1, 40) * 10;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
public static Adjustment CreateCharge(DateTime dt, int random)
|
|
||||||
{
|
|
||||||
var rnd = new DeterministicRandom(random);
|
|
||||||
Adjustment res = new Adjustment();
|
|
||||||
res.currentDateTime = dt;
|
|
||||||
res.currentDescription = rnd.GetRandomItem(bills);
|
|
||||||
res.currentAmount = rnd.Random(10, 50) * 10;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime currentDateTime;
|
|
||||||
string currentDescription = "";
|
|
||||||
double currentAmount = 0;
|
|
||||||
static readonly string[] bills = new string[] { "Bill - Insurance", "Bill - Electricity", "Bill - Rent", "Bill - Phone", "Bill - Office Supplies" };
|
|
||||||
public DateTime Date { get { return currentDateTime; } }
|
|
||||||
public string Description { get { return currentDescription; } }
|
|
||||||
public double Amount { get { return currentAmount; } }
|
|
||||||
|
|
||||||
public Adjustment()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
using DevExpress.DataAccess.Sql;
|
|
||||||
using DevExpress.DataAccess.Sql.DataApi;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
|
||||||
public class Customer {
|
|
||||||
static List<Customer> currentCustomers = new List<Customer>();
|
|
||||||
|
|
||||||
public static List<Customer> Customers { get { return currentCustomers; } }
|
|
||||||
static Customer() {
|
|
||||||
try {
|
|
||||||
SqlDataSource ds = new SqlDataSource("NWindConnectionString");
|
|
||||||
SelectQuery query = SelectQueryFluentBuilder
|
|
||||||
.AddTable("Customers")
|
|
||||||
.SelectAllColumns()
|
|
||||||
.Build("Customers");
|
|
||||||
ds.Queries.Add(query);
|
|
||||||
ds.RebuildResultSchema();
|
|
||||||
ds.Fill();
|
|
||||||
ITable src = ds.Result["Customers"];
|
|
||||||
foreach(var row in src) {
|
|
||||||
currentCustomers.Add(new Customer() {
|
|
||||||
CustomerID = row.GetValue<string>("CustomerID"),
|
|
||||||
Address = row.GetValue<string>("Address"),
|
|
||||||
CompanyName = row.GetValue<string>("CompanyName"),
|
|
||||||
ContactName = row.GetValue<string>("ContactName"),
|
|
||||||
ContactTitle = row.GetValue<string>("ContactTitle"),
|
|
||||||
Country = row.GetValue<string>("Country"),
|
|
||||||
City = row.GetValue<string>("City"),
|
|
||||||
Fax = row.GetValue<string>("Fax"),
|
|
||||||
Phone = row.GetValue<string>("Phone"),
|
|
||||||
PostalCode = row.GetValue<string>("PostalCode"),
|
|
||||||
Region = row.GetValue<string>("Region")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
currentCustomers.Add(new Customer() {
|
|
||||||
Address = "Obere Str. 57",
|
|
||||||
City = "Berlin",
|
|
||||||
CompanyName = "Alfreds Futterkiste",
|
|
||||||
ContactName = "Maria Anders",
|
|
||||||
ContactTitle = "Sales Representative",
|
|
||||||
Country = "Germany",
|
|
||||||
CustomerID = "ALFKI",
|
|
||||||
Fax = "030-0076545",
|
|
||||||
Phone = "030-0074321",
|
|
||||||
PostalCode = "12209"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CustomerID { get; set; }
|
|
||||||
public string CompanyName { get; set; }
|
|
||||||
public string ContactName { get; set; }
|
|
||||||
public string ContactTitle { get; set; }
|
|
||||||
public string Address { get; set; }
|
|
||||||
public string City { get; set; }
|
|
||||||
public string PostalCode { get; set; }
|
|
||||||
public string Region { get; set; }
|
|
||||||
public string Country { get; set; }
|
|
||||||
public string Phone { get; set; }
|
|
||||||
public string Fax { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
|
||||||
public class DataItem {
|
|
||||||
static readonly string[] accountType = new string[] { "Energy", "Manufacturing", "Estate", "Food", "Services" };
|
|
||||||
public string CustomerID { get; set; }
|
|
||||||
public string CompanyName { get; set; }
|
|
||||||
public string ContactName { get; set; }
|
|
||||||
public string ContactTitle { get; set; }
|
|
||||||
public string Address { get; set; }
|
|
||||||
public string City { get; set; }
|
|
||||||
public string PostalCode { get; set; }
|
|
||||||
public string Region { get; set; }
|
|
||||||
public string Country { get; set; }
|
|
||||||
public string Phone { get; set; }
|
|
||||||
public string Fax { get; set; }
|
|
||||||
public string Email { get; set; }
|
|
||||||
public string Invoice { get; set; }
|
|
||||||
public string CustomerAccount { get; set; }
|
|
||||||
public string CustomerIdentifiers { get; set; }
|
|
||||||
public DateTime BillingDate { get; set; }
|
|
||||||
public DateTime BillingPeriodStart { get; set; }
|
|
||||||
public DateTime BillingPeriodEnd { get; set; }
|
|
||||||
public string Terms { get; set; }
|
|
||||||
public string TermsID { get; set; }
|
|
||||||
public Adjustment[] Adjustments { get; set; }
|
|
||||||
|
|
||||||
public DataItem(int i) {
|
|
||||||
var rnd = new DeterministicRandom(i);
|
|
||||||
Customer c = rnd.GetRandomItem(Customer.Customers);
|
|
||||||
CustomerID = c.CustomerID;
|
|
||||||
CompanyName = c.CompanyName;
|
|
||||||
ContactName = c.ContactName;
|
|
||||||
ContactTitle = c.ContactTitle;
|
|
||||||
Address = c.Address;
|
|
||||||
City = c.City;
|
|
||||||
PostalCode = c.PostalCode;
|
|
||||||
Region = c.Region;
|
|
||||||
Country = c.Country;
|
|
||||||
Phone = c.Phone;
|
|
||||||
Fax = c.Fax;
|
|
||||||
Email = ContactName.Split(' ')[0].Replace(' ', '.').ToLower() + "@" + CompanyName.Split(' ')[0].ToLower() + ".com";
|
|
||||||
Invoice = string.Format("{0}{1}-{2}", rnd.RandomChar, rnd.Random(100, 1000), rnd.Random(100, 1000));
|
|
||||||
CustomerAccount = rnd.GetRandomItem(accountType);
|
|
||||||
CustomerIdentifiers = string.Format("{0}-{1}", rnd.Random(1000, 10000), rnd.Random(10, 100));
|
|
||||||
BillingPeriodStart = rnd.RandomTime();
|
|
||||||
BillingPeriodEnd = rnd.RandomTime(BillingPeriodStart, 7 * 24, 30 * 24);
|
|
||||||
BillingDate = rnd.RandomTime(BillingPeriodEnd, 7 * 24, 30 * 24);
|
|
||||||
Term currentTerm = rnd.GetRandomItem(Term.Terms);
|
|
||||||
Terms = currentTerm.Name;
|
|
||||||
|
|
||||||
int adjustmentsCount = rnd.Random(6) + 4;
|
|
||||||
Adjustments = new Adjustment[adjustmentsCount];
|
|
||||||
int h = (int)((BillingPeriodEnd - BillingPeriodStart).TotalHours / adjustmentsCount);
|
|
||||||
|
|
||||||
Adjustments[0] = Adjustment.CreateBalanceForward(rnd.RandomTime(BillingPeriodStart, 0, h), rnd.Random(10000));
|
|
||||||
|
|
||||||
int[] items = rnd.RandomList(adjustmentsCount - 1, 2);
|
|
||||||
|
|
||||||
for(int j = 1; j < Adjustments.Length; j++) {
|
|
||||||
DateTime nextDate = rnd.RandomTime(BillingPeriodStart.AddHours(h * j), 0, h);
|
|
||||||
switch(items[j - 1]) {
|
|
||||||
case 0:
|
|
||||||
Adjustments[j] = Adjustment.CreateCharge(nextDate, rnd.Random(10000));
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
Adjustments[j] = Adjustment.CreatePayment(nextDate, rnd.Random(10000));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
using System.Collections;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
|
||||||
public class DataItemList : IList<DataItem>, IList {
|
|
||||||
readonly int rowCount;
|
|
||||||
|
|
||||||
public DataItem this[int index] { get { return new DataItem(index); } set { } }
|
|
||||||
public int Count { get { return rowCount; } }
|
|
||||||
public bool IsReadOnly { get { return false; } }
|
|
||||||
public bool IsFixedSize { get { return false; } }
|
|
||||||
public object SyncRoot { get { return true; } }
|
|
||||||
public bool IsSynchronized { get { return true; } }
|
|
||||||
object IList.this[int index] { get { return new DataItem(index); } set { } }
|
|
||||||
|
|
||||||
public DataItemList(int rowCount) {
|
|
||||||
this.rowCount = rowCount;
|
|
||||||
}
|
|
||||||
public IEnumerator<DataItem> GetEnumerator() {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public int Add(object value) {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public bool Contains(object value) {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public void Clear() {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public int IndexOf(object value) {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public void Insert(int index, object value) {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public void Remove(object value) {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public void RemoveAt(int index) {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public void CopyTo(Array array, int index) {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
IEnumerator IEnumerable.GetEnumerator() {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public int IndexOf(DataItem item) {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public void Insert(int index, DataItem item) {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public void Add(DataItem item) {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public bool Contains(DataItem item) {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public void CopyTo(DataItem[] array, int arrayIndex) {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
public bool Remove(DataItem item) {
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
void ICollection<DataItem>.CopyTo(DataItem[] array, int arrayIndex) {
|
|
||||||
CopyTo(array, arrayIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
|
||||||
class DeterministicRandom {
|
|
||||||
const int randomCount = 10000;
|
|
||||||
static readonly int[] deterministicRandomNumbers;
|
|
||||||
static readonly DateTime time;
|
|
||||||
int rnd;
|
|
||||||
int Next {
|
|
||||||
get {
|
|
||||||
rnd = deterministicRandomNumbers[rnd % randomCount];
|
|
||||||
return rnd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public char RandomChar {
|
|
||||||
get {
|
|
||||||
return (char)((int)'A' + Random(0, 26));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public int[] RandomList(int count, int to) {
|
|
||||||
int[] res = new int[count];
|
|
||||||
for(int i = 0; i < Math.Min(count, to); i++)
|
|
||||||
res[i] = i;
|
|
||||||
for(int i = to; i < count; i++)
|
|
||||||
res[i] = Random(to);
|
|
||||||
|
|
||||||
for(int i = 0; i < count; i++) {
|
|
||||||
int ind = Random(count);
|
|
||||||
int temp = res[ind];
|
|
||||||
res[ind] = res[i];
|
|
||||||
res[i] = temp;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
public int Random(int to) {
|
|
||||||
return Random(0, to);
|
|
||||||
}
|
|
||||||
public int Random(int from, int to) {
|
|
||||||
return Next % Math.Max(1, to - from) + from;
|
|
||||||
}
|
|
||||||
public T GetRandomItem<T>(IList<T> list) {
|
|
||||||
return list[Next % list.Count];
|
|
||||||
}
|
|
||||||
public DateTime RandomTime() {
|
|
||||||
return RandomTime(time, 0, 30 * 24);
|
|
||||||
}
|
|
||||||
public DateTime RandomTime(DateTime from, int fromHours, int toHours) {
|
|
||||||
return from.AddHours(Next % (toHours - fromHours) + fromHours);
|
|
||||||
}
|
|
||||||
|
|
||||||
static DeterministicRandom() {
|
|
||||||
time = DateTime.Now.AddDays(-62);
|
|
||||||
Random currentRandom = new Random(randomCount);
|
|
||||||
deterministicRandomNumbers = new int[randomCount];
|
|
||||||
for(int i = 0; i < randomCount; i++)
|
|
||||||
deterministicRandomNumbers[i] = currentRandom.Next(randomCount);
|
|
||||||
}
|
|
||||||
public DeterministicRandom(int i) {
|
|
||||||
this.rnd = i + (i >> 10) + (i >> 20);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
|
||||||
public struct Term {
|
|
||||||
public static readonly Term[] Terms = new Term[] {
|
|
||||||
new Term("Payment seven days after invoice date" ),
|
|
||||||
new Term("Payment ten days after invoice date" ),
|
|
||||||
new Term("End of month" ),
|
|
||||||
new Term("21st of the month following invoice date" ),
|
|
||||||
};
|
|
||||||
readonly string currentName;
|
|
||||||
public string Name { get { return currentName; } }
|
|
||||||
public Term(string currentName) {
|
|
||||||
this.currentName = currentName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<WasmBuildNative>true</WasmBuildNative>
|
|
||||||
<InvariantGlobalization>false</InvariantGlobalization>
|
|
||||||
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
|
|
||||||
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<PackageId>EnvelopeGenerator.ReceiverUI</PackageId>
|
|
||||||
<Authors>Digital Data GmbH</Authors>
|
|
||||||
<Company>Digital Data GmbH</Company>
|
|
||||||
<Product>EnvelopeGenerator.ReceiverUI</Product>
|
|
||||||
<PackageIcon>Assets\icon.ico</PackageIcon>
|
|
||||||
<PackageTags>digital data envelope generator web</PackageTags>
|
|
||||||
<Description>EnvelopeGenerator.ReceiverUI is a Blazor WebAssembly application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description>
|
|
||||||
<Version>1.4.2</Version>
|
|
||||||
<!-- NuGet package version -->
|
|
||||||
<AssemblyVersion>1.4.2.0</AssemblyVersion>
|
|
||||||
<!-- Assembly version for API compatibility -->
|
|
||||||
<FileVersion>1.4.2.0</FileVersion>
|
|
||||||
<!-- Windows file version -->
|
|
||||||
<Copyright>Copyright © 2026 Digital Data GmbH. All rights reserved.</Copyright>
|
|
||||||
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
|
|
||||||
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.3" />
|
|
||||||
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
|
||||||
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.3" />
|
|
||||||
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
|
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.1" />
|
|
||||||
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
|
|
||||||
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.11" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.11" PrivateAssets="all" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Properties\PublishProfiles\" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Update="wwwroot\docs\privacy-policy.en-US.html">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Update="wwwroot\docs\privacy-policy.fr-FR.html">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a pre-assigned signature annotation position on a specific page.
|
|
||||||
/// <br/><br/>
|
|
||||||
/// <b>Coordinate unit (X, Y):</b> Inches (GdPicture14 native unit),
|
|
||||||
/// origin at the <b>top-left</b> corner of the page, both axes increase downward/rightward.
|
|
||||||
/// <br/><br/>
|
|
||||||
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
|
|
||||||
/// Convert: <c>xDX = xInches * 100.0</c>
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Conversion to PDF Points:</b> Multiply by 72 (1 inch = 72 points).
|
|
||||||
/// Convert: <c>xPt = xInches * 72.0</c>
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Y-axis for PDF (bottom-left origin):</b> Flip required for iText7.
|
|
||||||
/// Convert: <c>yPt = (pageHeightInches - yInches - elemHeightInches) * 72.0</c>
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Use SignatureDto with SignatureService.")]
|
|
||||||
public record AnnotationDto
|
|
||||||
{
|
|
||||||
/// <summary>Unique identifier of the annotation.</summary>
|
|
||||||
public long Id { get; init; }
|
|
||||||
|
|
||||||
/// <summary>1-based page number within the document.</summary>
|
|
||||||
public int Page { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Horizontal position in INCHES from the left edge of the page.</summary>
|
|
||||||
public double X { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Vertical position in INCHES from the top edge of the page.</summary>
|
|
||||||
public double Y { get; init; }
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Models.Constants
|
|
||||||
{
|
|
||||||
public enum SenderAppType
|
|
||||||
{
|
|
||||||
LegacyFormApp = 0,
|
|
||||||
ReceiverUIBlazorApp = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Models.Constants;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents the unit of measurement for coordinate values in signature positioning.
|
|
||||||
/// Used for converting coordinates between different systems (GdPicture14, PDF.js, iText7).
|
|
||||||
/// </summary>
|
|
||||||
public enum UnitOfLength
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Inch unit (1 inch = 25.4 mm).
|
|
||||||
/// This is the native unit used by GdPicture14 (EnvelopeGenerator.Form - Legacy VB.NET app).
|
|
||||||
/// Database stores all coordinates (X, Y, Width, Height) in INCHES.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <b>Source:</b> GdPicture14.Annotations.AnnotationStickyNote uses INCHES natively.
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Evidence:</b> VB.NET code directly assigns database values to annotation properties without conversion:
|
|
||||||
/// <code>
|
|
||||||
/// oAnnotation.Left = CSng(pElement.X) ' Direct assignment → INCHES
|
|
||||||
/// oAnnotation.Top = CSng(pElement.Y)
|
|
||||||
/// </code>
|
|
||||||
/// <b>Standard Page Dimensions:</b>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>A4: 8.27" × 11.69" (210mm × 297mm)</item>
|
|
||||||
/// <item>Letter: 8.5" × 11"</item>
|
|
||||||
/// </list>
|
|
||||||
/// </remarks>
|
|
||||||
Inch = 0,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// PDF Point unit (1 point = 1/72 inch).
|
|
||||||
/// This is the standard unit used by PDF specification and PDF.js viewer.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <b>Definition:</b> According to PDF specification and Microsoft documentation:
|
|
||||||
/// <br/>
|
|
||||||
/// <i>"PDF pages are sized in point units. 1 pt == 1/72 inch"</i>
|
|
||||||
/// <br/><br/>
|
|
||||||
/// <b>Conversion Formula:</b>
|
|
||||||
/// <code>
|
|
||||||
/// points = inches * 72.0
|
|
||||||
/// inches = points / 72.0
|
|
||||||
/// </code>
|
|
||||||
/// <b>Important:</b> Point ≠ Pixel!
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item><b>Point (pt):</b> Device-independent unit (always 1/72 inch)</item>
|
|
||||||
/// <item><b>Pixel (px):</b> Device-dependent unit (varies with screen DPI)</item>
|
|
||||||
/// <item>At 72 DPI: 1 point = 1 pixel (coincidence)</item>
|
|
||||||
/// <item>At 96 DPI: 1 point ≈ 1.33 pixels</item>
|
|
||||||
/// <item>At 300 DPI: 1 point ≈ 4.17 pixels</item>
|
|
||||||
/// </list>
|
|
||||||
/// <b>Standard Page Dimensions (in points):</b>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>A4: 595 × 842 points (8.27" × 11.69" × 72)</item>
|
|
||||||
/// <item>Letter: 612 × 792 points (8.5" × 11" × 72)</item>
|
|
||||||
/// </list>
|
|
||||||
/// <b>Usage in EnvelopeGenerator:</b>
|
|
||||||
/// <list type="bullet">
|
|
||||||
/// <item>PDF.js viewer expects coordinates in points</item>
|
|
||||||
/// <item>iText7 library uses points for PDF manipulation</item>
|
|
||||||
/// <item>PSPDFKit (Web) uses points for annotation placement</item>
|
|
||||||
/// </list>
|
|
||||||
/// </remarks>
|
|
||||||
Point
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Client-side model for the envelope receiver returned by
|
|
||||||
/// <c>GET api/EnvelopeReceiver/{envelopeKey}</c>.
|
|
||||||
/// </summary>
|
|
||||||
public record EnvelopeReceiverDto
|
|
||||||
{
|
|
||||||
public int EnvelopeId { get; init; }
|
|
||||||
public int ReceiverId { get; init; }
|
|
||||||
public int Sequence { get; init; }
|
|
||||||
|
|
||||||
public string? Name { get; init; }
|
|
||||||
public string? JobTitle { get; init; }
|
|
||||||
public string? CompanyName { get; init; }
|
|
||||||
public string? PrivateMessage { get; init; }
|
|
||||||
|
|
||||||
public DateTime AddedWhen { get; init; }
|
|
||||||
public DateTime? ChangedWhen { get; init; }
|
|
||||||
public bool HasPhoneNumber { get; init; }
|
|
||||||
|
|
||||||
public EnvelopeClientDto? Envelope { get; init; }
|
|
||||||
public ReceiverClientDto? Receiver { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Client-side model for the envelope data embedded in <see cref="EnvelopeReceiverDto"/>.
|
|
||||||
/// </summary>
|
|
||||||
public record EnvelopeClientDto
|
|
||||||
{
|
|
||||||
public int Id { get; init; }
|
|
||||||
public int UserId { get; init; }
|
|
||||||
public int Status { get; init; }
|
|
||||||
public string StatusName { get; init; } = string.Empty;
|
|
||||||
public string Uuid { get; init; } = string.Empty;
|
|
||||||
public string Title { get; init; } = string.Empty;
|
|
||||||
public string Message { get; init; } = string.Empty;
|
|
||||||
public DateTime AddedWhen { get; init; }
|
|
||||||
public DateTime? ChangedWhen { get; init; }
|
|
||||||
public string Language { get; init; } = "de-DE";
|
|
||||||
public int? EnvelopeTypeId { get; init; }
|
|
||||||
public string? EnvelopeTypeTitle { get; init; }
|
|
||||||
public int? ContractType { get; init; }
|
|
||||||
public int? CertificationType { get; init; }
|
|
||||||
public bool UseAccessCode { get; init; }
|
|
||||||
public bool TFAEnabled { get; init; }
|
|
||||||
public IEnumerable<DocumentClientDto>? Documents { get; init; }
|
|
||||||
public EnvelopeSenderDto? User { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sender (user) information embedded in <see cref="EnvelopeClientDto"/>.
|
|
||||||
/// </summary>
|
|
||||||
public record EnvelopeSenderDto
|
|
||||||
{
|
|
||||||
public int Id { get; init; }
|
|
||||||
public string? Username { get; init; }
|
|
||||||
public string? FullName { get; init; }
|
|
||||||
public string? Email { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Client-side model for a document embedded in <see cref="EnvelopeClientDto"/>.
|
|
||||||
/// </summary>
|
|
||||||
public record DocumentClientDto
|
|
||||||
{
|
|
||||||
public int Id { get; init; }
|
|
||||||
public int EnvelopeId { get; init; }
|
|
||||||
public DateTime AddedWhen { get; init; }
|
|
||||||
public IEnumerable<SignatureClientDto>? Elements { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Client-side model for a signature/annotation element embedded in <see cref="DocumentClientDto"/>.
|
|
||||||
/// </summary>
|
|
||||||
public record SignatureClientDto
|
|
||||||
{
|
|
||||||
public int Id { get; init; }
|
|
||||||
public int DocumentId { get; init; }
|
|
||||||
public int ReceiverId { get; init; }
|
|
||||||
public int ElementType { get; init; }
|
|
||||||
public double X { get; init; }
|
|
||||||
public double Y { get; init; }
|
|
||||||
public double Width { get; init; }
|
|
||||||
public double Height { get; init; }
|
|
||||||
public int Page { get; init; }
|
|
||||||
public bool Required { get; init; }
|
|
||||||
public string? Tooltip { get; init; }
|
|
||||||
public bool ReadOnly { get; init; }
|
|
||||||
public int AnnotationIndex { get; init; }
|
|
||||||
public DateTime AddedWhen { get; init; }
|
|
||||||
public DateTime? ChangedWhen { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Client-side model for the receiver data embedded in <see cref="EnvelopeReceiverDto"/>.
|
|
||||||
/// </summary>
|
|
||||||
public record ReceiverClientDto
|
|
||||||
{
|
|
||||||
public int Id { get; init; }
|
|
||||||
public string? EmailAddress { get; init; }
|
|
||||||
public string? Signature { get; init; }
|
|
||||||
public DateTime AddedWhen { get; init; }
|
|
||||||
public DateTime? TfaRegDeadline { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a captured signature with metadata created by the receiver in the signature popup.
|
|
||||||
/// This model holds the signature image (as base64 data URL) along with signer information
|
|
||||||
/// used for rendering applied signatures on the PDF canvas.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// <b>Used in:</b> EnvelopeViewer.razor signature popup workflow
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Creation:</b> User draws/types/uploads signature and fills required fields
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Storage:</b> Session-only (Blazor component state, lost on page refresh)
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Rendering:</b> Applied signatures display: Image + Separator + Name/Position/Place/Date
|
|
||||||
/// </remarks>
|
|
||||||
public sealed record SignatureCaptureDto
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Base64-encoded data URL of the signature image.
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Format:</b> <c>data:image/png;base64,iVBORw0KG...</c>
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Source:</b> Canvas.toDataURL() from signature pad (draw/text/image tabs)
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Usage:</b> Set as <c>img.src</c> in applied signature overlay
|
|
||||||
/// </summary>
|
|
||||||
public required string DataUrl { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Full name of the signer (first and last name).
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Required:</b> Yes (validated in popup)
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Display:</b> Bold text in applied signature block
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Example:</b> "Max Mustermann"
|
|
||||||
/// </summary>
|
|
||||||
public required string FullName { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Job title or position of the signer.
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Required:</b> No (optional field)
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Display:</b> Normal weight text between name and place/date
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Example:</b> "Geschäftsführer" or empty string
|
|
||||||
/// </summary>
|
|
||||||
public string Position { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Location/place where the signature was created.
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Required:</b> Yes (validated in popup)
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Display:</b> Shown with current date in German format (dd.MM.yyyy)
|
|
||||||
/// <br/>
|
|
||||||
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
|
||||||
/// </summary>
|
|
||||||
public required string Place { get; init; }
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
using EnvelopeGenerator.ReceiverUI.Models.Constants;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a signature position on a PDF page.
|
|
||||||
/// Coordinates stored in INCHES (GdPicture14 native unit).
|
|
||||||
/// Origin: Top-left corner, X increases right, Y increases down.
|
|
||||||
/// </summary>
|
|
||||||
public class SignatureDto
|
|
||||||
{
|
|
||||||
/// <summary>Unique identifier.</summary>
|
|
||||||
public int Id { get; init; }
|
|
||||||
|
|
||||||
private double _x;
|
|
||||||
private double _y;
|
|
||||||
|
|
||||||
/// <summary>Horizontal position in INCHES from left edge.</summary>
|
|
||||||
public double X
|
|
||||||
{
|
|
||||||
get => _x * Factor;
|
|
||||||
init => _x = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Vertical position in INCHES from top edge.</summary>
|
|
||||||
public double Y
|
|
||||||
{
|
|
||||||
get => _y * Factor;
|
|
||||||
init => _y = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>1-based page number.</summary>
|
|
||||||
public int Page { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Sender application type that created this signature.</summary>
|
|
||||||
public SenderAppType SenderAppType { get; init; }
|
|
||||||
|
|
||||||
private UnitOfLength _unitOfLength;
|
|
||||||
|
|
||||||
public SignatureDto Convert(UnitOfLength unitOfLength)
|
|
||||||
{
|
|
||||||
_unitOfLength = unitOfLength;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double Factor
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (SenderAppType != SenderAppType.LegacyFormApp)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException(
|
|
||||||
$"SenderAppType '{SenderAppType}' is not yet implemented. " +
|
|
||||||
$"Currently, only '{nameof(SenderAppType.LegacyFormApp)}' is supported. " +
|
|
||||||
$"Future implementations will handle '{nameof(SenderAppType.ReceiverUIBlazorApp)}' and other types.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// LegacyFormApp uses GdPicture14 with INCHES
|
|
||||||
return _unitOfLength switch
|
|
||||||
{
|
|
||||||
UnitOfLength.Inch => 1.0, // No conversion needed: INCHES → INCHES
|
|
||||||
UnitOfLength.Point => 72.0, // INCHES → PDF Points: 1 inch = 72 points (PDF standard, NOT pixels!)
|
|
||||||
_ => throw new InvalidOperationException(
|
|
||||||
$"Unknown UnitOfLength: {_unitOfLength}. Expected '{nameof(UnitOfLength.Inch)}' or '{nameof(UnitOfLength.Point)}'.")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class SignatureDtoExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converts all signatures in the collection to the specified unit of length.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Type of the collection (IEnumerable, List, etc.)</typeparam>
|
|
||||||
/// <param name="signatures">Collection of SignatureDto objects to convert.</param>
|
|
||||||
/// <param name="unitOfLength">Target unit of measurement (Inch or Point).</param>
|
|
||||||
/// <returns>The same collection with all signatures converted to the specified unit.</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">Thrown when signatures collection is null.</exception>
|
|
||||||
/// <remarks>
|
|
||||||
/// <b>Usage:</b>
|
|
||||||
/// <code>
|
|
||||||
/// var signatures = await SignatureService.GetAsync(envelopeKey);
|
|
||||||
/// var convertedSignatures = signatures.ConvertAll(UnitOfLength.Point);
|
|
||||||
/// </code>
|
|
||||||
/// <b>Note:</b> This method modifies each SignatureDto object in place and returns the same collection.
|
|
||||||
/// </remarks>
|
|
||||||
public static T Convert<T>(this T signatures, UnitOfLength unitOfLength)
|
|
||||||
where T : IEnumerable<SignatureDto>
|
|
||||||
{
|
|
||||||
if (signatures == null)
|
|
||||||
throw new ArgumentNullException(nameof(signatures));
|
|
||||||
|
|
||||||
foreach (var signature in signatures)
|
|
||||||
{
|
|
||||||
signature.Convert(unitOfLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
return signatures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
The following open source libraries are used and included within thеse sample/demonstration projects:
|
|
||||||
|
|
||||||
Bootstrap (Open Source – MIT License)
|
|
||||||
Copyright (c) 2011-2021 Twitter, Inc. / Copyright (c) 2011-2021 The Bootstrap Authors
|
|
||||||
https://github.com/twbs/bootstrap/blob/main/LICENSE
|
|
||||||
|
|
||||||
open-iconic (Open Source – MIT License and SIL Open Font License)
|
|
||||||
Copyright (c) 2014 Waybury
|
|
||||||
https://github.com/iconic/open-iconic/blob/master/ICON-LICENSE
|
|
||||||
https://github.com/iconic/open-iconic/blob/master/FONT-LICENSE
|
|
||||||
|
|
||||||
The open source libraries included in these sample/demonstration projects are done so pursuant to each individual open source library license and subject to the disclaimers and limitations on liability set forth in each open source library license.
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Options;
|
|
||||||
|
|
||||||
public class ApiOptions
|
|
||||||
{
|
|
||||||
public const string SectionName = "Api";
|
|
||||||
|
|
||||||
public string BaseUrl { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public bool UsePredefinedReports { get; set; } = false;
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Options;
|
|
||||||
|
|
||||||
public class PdfViewerOptions
|
|
||||||
{
|
|
||||||
public const string SectionName = "PdfViewer";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Base scale for thumbnail rendering (0.2 - 1.5 recommended)
|
|
||||||
/// Higher values = better quality but slower rendering
|
|
||||||
/// Default: 0.75
|
|
||||||
/// </summary>
|
|
||||||
public double ThumbnailBaseScale { get; set; } = 0.75;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enable HiDPI/Retina support for thumbnails
|
|
||||||
/// Default: true
|
|
||||||
/// </summary>
|
|
||||||
public bool ThumbnailEnableHiDPI { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum device pixel ratio multiplier for thumbnails (1.0 - 3.0)
|
|
||||||
/// Caps DPR to avoid excessive memory usage on 4K+ displays
|
|
||||||
/// Default: 2.0
|
|
||||||
/// </summary>
|
|
||||||
public double ThumbnailMaxDPR { get; set; } = 2.0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enable HiDPI/Retina support for main PDF canvas
|
|
||||||
/// Default: true
|
|
||||||
/// </summary>
|
|
||||||
public bool MainCanvasEnableHiDPI { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum device pixel ratio multiplier for main canvas (1.0 - 3.0)
|
|
||||||
/// Default: 2.0
|
|
||||||
/// </summary>
|
|
||||||
public double MainCanvasMaxDPR { get; set; } = 2.0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enable smooth zoom transition (fade effect)
|
|
||||||
/// Default: true
|
|
||||||
/// </summary>
|
|
||||||
public bool EnableSmoothZoom { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Zoom transition duration in milliseconds (50 - 500)
|
|
||||||
/// Default: 150
|
|
||||||
/// </summary>
|
|
||||||
public int ZoomTransitionDuration { get; set; } = 150;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Opacity during rendering (0.0 - 1.0)
|
|
||||||
/// Lower values = more visible fade effect
|
|
||||||
/// Default: 0.85
|
|
||||||
/// </summary>
|
|
||||||
public double RenderingOpacity { get; set; } = 0.85;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Delay between thumbnail renders in milliseconds (10 - 200)
|
|
||||||
/// Higher values = less browser stress, slower initial load
|
|
||||||
/// Default: 50
|
|
||||||
/// </summary>
|
|
||||||
public int ThumbnailRenderDelay { get; set; } = 50;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Zoom step percentage (1 - 50)
|
|
||||||
/// Controls how much zoom changes per click or scroll
|
|
||||||
/// Default: 5 (5% per step)
|
|
||||||
/// </summary>
|
|
||||||
public int ZoomStepPercentage { get; set; } = 5;
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,112 +0,0 @@
|
|||||||
@page "/envelope/DxPdfViewer"
|
|
||||||
@using System.IO
|
|
||||||
@using DevExpress.Blazor
|
|
||||||
@using System.Reflection
|
|
||||||
|
|
||||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.custom-drop-zone {
|
|
||||||
padding: 0 !important;
|
|
||||||
border-style: dashed;
|
|
||||||
border-width: 2px !important;
|
|
||||||
height: 230px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: rgba(183, 183, 183, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-drop-zone.custom-drop-zone-hover {
|
|
||||||
border-style: solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-drop-zone svg {
|
|
||||||
width: 42px;
|
|
||||||
height: 42px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-drop-zone > *:not(#overviewDemoSelectButton) {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-viewer {
|
|
||||||
height: 800px !important;
|
|
||||||
min-height: 800px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-viewer .dxbrv-surface-wrapper,
|
|
||||||
.pdf-viewer .dxbrv-document-surface {
|
|
||||||
height: 100% !important;
|
|
||||||
min-height: 750px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-viewer .dxbrv-report-preview-content {
|
|
||||||
width: auto !important;
|
|
||||||
height: auto !important;
|
|
||||||
min-width: 200px !important;
|
|
||||||
min-height: 200px !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div id="overviewDemoDropZone" class="card custom-drop-zone rounded-3 w-100 m-0">
|
|
||||||
<span class="drop-file-icon mb-3"></span>
|
|
||||||
<span class="drop-file-label">Drag and Drop File Here</span><span class="m-1">or</span>
|
|
||||||
<DxButton Id="overviewDemoSelectButton"
|
|
||||||
CssClass="m-1"
|
|
||||||
RenderStyle="ButtonRenderStyle.Primary"
|
|
||||||
Text="Select File" />
|
|
||||||
</div>
|
|
||||||
<DxFileInput @ref="fileInput"
|
|
||||||
AcceptedFileTypes="@ALLOWED_FILE_TYPES"
|
|
||||||
AllowedFileExtensions="@ALLOWED_FILE_TYPES"
|
|
||||||
CssClass="w-100"
|
|
||||||
ExternalDropZoneCssSelector="#overviewDemoDropZone"
|
|
||||||
ExternalDropZoneDragOverCssClass="custom-drop-zone-hover"
|
|
||||||
ExternalSelectButtonCssSelector="#overviewDemoSelectButton"
|
|
||||||
FilesUploading="OnFilesUploading"
|
|
||||||
MaxFileSize="2000000">
|
|
||||||
</DxFileInput>
|
|
||||||
|
|
||||||
@if (DocumentContent != null && DocumentContent.Length > 0)
|
|
||||||
{
|
|
||||||
<div class="alert alert-success mt-3">
|
|
||||||
PDF loaded: @DocumentContent.Length bytes
|
|
||||||
</div>
|
|
||||||
<DxPdfViewer CssClass="w-100 pdf-viewer" DocumentContent="@DocumentContent" />
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="alert alert-info mt-3">
|
|
||||||
Please upload a PDF file to view it.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
|
||||||
readonly List<string> ALLOWED_FILE_TYPES = new List<string> { ".pdf" };
|
|
||||||
DxFileInput fileInput;
|
|
||||||
byte[] DocumentContent { get; set; }
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
|
||||||
Stream stream = assembly.GetManifestResourceStream("EnvelopeGenerator.ReceiverUI.Resources.Invoice.pdf");
|
|
||||||
if (stream != null)
|
|
||||||
{
|
|
||||||
using (stream)
|
|
||||||
using (var binaryReader = new BinaryReader(stream))
|
|
||||||
DocumentContent = binaryReader.ReadBytes((int)stream.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
protected async Task OnFilesUploading(FilesUploadingEventArgs args)
|
|
||||||
{
|
|
||||||
using (MemoryStream stream = new MemoryStream())
|
|
||||||
{
|
|
||||||
IFileInputSelectedFile file = args.Files[0];
|
|
||||||
await file.OpenReadStream(file.Size).CopyToAsync(stream);
|
|
||||||
DocumentContent = stream.ToArray();
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
@page "/envelope/Embed"
|
|
||||||
@using System.IO
|
|
||||||
@using DevExpress.Blazor
|
|
||||||
@using System.Reflection
|
|
||||||
|
|
||||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.custom-drop-zone {
|
|
||||||
padding: 0 !important;
|
|
||||||
border-style: dashed;
|
|
||||||
border-width: 2px !important;
|
|
||||||
height: 230px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: rgba(183, 183, 183, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-drop-zone.custom-drop-zone-hover {
|
|
||||||
border-style: solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-drop-zone svg {
|
|
||||||
width: 42px;
|
|
||||||
height: 42px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-drop-zone > *:not(#overviewDemoSelectButton) {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-viewer {
|
|
||||||
height: 800px !important;
|
|
||||||
min-height: 800px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-viewer .dxbrv-surface-wrapper,
|
|
||||||
.pdf-viewer .dxbrv-document-surface {
|
|
||||||
height: 100% !important;
|
|
||||||
min-height: 750px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-viewer .dxbrv-report-preview-content {
|
|
||||||
width: auto !important;
|
|
||||||
height: auto !important;
|
|
||||||
min-width: 200px !important;
|
|
||||||
min-height: 200px !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div id="overviewDemoDropZone" class="card custom-drop-zone rounded-3 w-100 m-0">
|
|
||||||
<span class="drop-file-icon mb-3"></span>
|
|
||||||
<span class="drop-file-label">Drag and Drop File Here</span><span class="m-1">or</span>
|
|
||||||
<DxButton Id="overviewDemoSelectButton"
|
|
||||||
CssClass="m-1"
|
|
||||||
RenderStyle="ButtonRenderStyle.Primary"
|
|
||||||
Text="Select File" />
|
|
||||||
</div>
|
|
||||||
<DxFileInput @ref="fileInput"
|
|
||||||
AcceptedFileTypes="@ALLOWED_FILE_TYPES"
|
|
||||||
AllowedFileExtensions="@ALLOWED_FILE_TYPES"
|
|
||||||
CssClass="w-100"
|
|
||||||
ExternalDropZoneCssSelector="#overviewDemoDropZone"
|
|
||||||
ExternalDropZoneDragOverCssClass="custom-drop-zone-hover"
|
|
||||||
ExternalSelectButtonCssSelector="#overviewDemoSelectButton"
|
|
||||||
FilesUploading="OnFilesUploading"
|
|
||||||
MaxFileSize="2000000">
|
|
||||||
</DxFileInput>
|
|
||||||
|
|
||||||
@if (DocumentContent != null && DocumentContent.Length > 0)
|
|
||||||
{
|
|
||||||
<div class="alert alert-success mt-3">
|
|
||||||
PDF loaded: @DocumentContent.Length bytes
|
|
||||||
</div>
|
|
||||||
<embed src="@GetPdfDataUrl()" type="application/pdf" class="w-100 pdf-viewer" />
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="alert alert-info mt-3">
|
|
||||||
Please upload a PDF file to view it.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
|
||||||
readonly List<string> ALLOWED_FILE_TYPES = new List<string> { ".pdf" };
|
|
||||||
DxFileInput fileInput;
|
|
||||||
byte[] DocumentContent { get; set; }
|
|
||||||
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
|
||||||
Stream stream = assembly.GetManifestResourceStream("EnvelopeGenerator.ReceiverUI.Resources.Invoice.pdf");
|
|
||||||
if (stream != null)
|
|
||||||
{
|
|
||||||
using (stream)
|
|
||||||
using (var binaryReader = new BinaryReader(stream))
|
|
||||||
DocumentContent = binaryReader.ReadBytes((int)stream.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task OnFilesUploading(FilesUploadingEventArgs args)
|
|
||||||
{
|
|
||||||
using (MemoryStream stream = new MemoryStream())
|
|
||||||
{
|
|
||||||
IFileInputSelectedFile file = args.Files[0];
|
|
||||||
await file.OpenReadStream(file.Size).CopyToAsync(stream);
|
|
||||||
DocumentContent = stream.ToArray();
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetPdfDataUrl()
|
|
||||||
{
|
|
||||||
if (DocumentContent == null || DocumentContent.Length == 0)
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
string base64 = Convert.ToBase64String(DocumentContent);
|
|
||||||
return $"data:application/pdf;base64,{base64}#toolbar=0&navpanes=0&scrollbar=1";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
@page "/sender"
|
|
||||||
|
|
||||||
<h3>EnvelopeSender</h3>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
@page "/example/documentviewer"
|
|
||||||
|
|
||||||
<DxDocumentViewer ReportName="LargeDatasetReport" CssClass="dx-blazor-reporting-container" Height="@null" Width="@null">
|
|
||||||
<DxDocumentViewerTabPanelSettings Width="340" />
|
|
||||||
</DxDocumentViewer>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
@page "/example/sender"
|
|
||||||
@using DevExpress.DataAccess.Json;
|
|
||||||
@using EnvelopeGenerator.ReceiverUI.Services;
|
|
||||||
|
|
||||||
<DxReportDesigner ReportName="LargeDatasetReport" CssClass="dx-blazor-reporting-container" Height="@null" Width="@null" AllowMDI="true" DataSources="DataSources">
|
|
||||||
<DxReportDesignerWizardSettings UseFullscreenWizard="true" />
|
|
||||||
</DxReportDesigner>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
Dictionary<string, object> DataSources = new Dictionary<string, object>();
|
|
||||||
protected override async Task OnInitializedAsync() {
|
|
||||||
await base.OnInitializedAsync();
|
|
||||||
var connection = CustomDataSourceWizardJsonDataConnectionStorage.GetDefaultConnection();
|
|
||||||
JsonDataSource jsonDataSource = new JsonDataSource();
|
|
||||||
jsonDataSource.JsonSource = connection.GetJsonSource();
|
|
||||||
await jsonDataSource.FillAsync();
|
|
||||||
DataSources.Add("Northwind", jsonDataSource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,746 +0,0 @@
|
|||||||
@page "/example/receiver/{EnvelopeKey}"
|
|
||||||
@page "/example/report-viewer/{EnvelopeKey}"
|
|
||||||
@using System.Drawing
|
|
||||||
@using DevExpress.Blazor
|
|
||||||
@using DevExpress.Drawing
|
|
||||||
@using DevExpress.Utils
|
|
||||||
@using DevExpress.XtraPrinting
|
|
||||||
@using DevExpress.XtraPrinting.Drawing
|
|
||||||
@using Microsoft.JSInterop
|
|
||||||
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
|
|
||||||
@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand
|
|
||||||
@using XRLabel = DevExpress.XtraReports.UI.XRLabel
|
|
||||||
@using XRPictureBox = DevExpress.XtraReports.UI.XRPictureBox
|
|
||||||
@using XRControl = DevExpress.XtraReports.UI.XRControl
|
|
||||||
@using ImageSizeMode = DevExpress.XtraPrinting.ImageSizeMode
|
|
||||||
@using EnvelopeGenerator.ReceiverUI.Services
|
|
||||||
@using DevExpress.Blazor.Reporting
|
|
||||||
@using Microsoft.Extensions.Options
|
|
||||||
@using EnvelopeGenerator.ReceiverUI.Options
|
|
||||||
@using EnvelopeGenerator.ReceiverUI.Models
|
|
||||||
@implements IDisposable
|
|
||||||
@inject IJSRuntime JSRuntime
|
|
||||||
@inject AnnotationService AnnotationService
|
|
||||||
@inject IOptions<ApiOptions> AppOptions
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
@inject InMemoryReportStorageWebExtension ReportStorage
|
|
||||||
@inject EnvelopeGenerator.ReceiverUI.Services.DocumentService DocumentService
|
|
||||||
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
|
|
||||||
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService
|
|
||||||
|
|
||||||
<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" />
|
|
||||||
|
|
||||||
<div class="receiver-page-layout">
|
|
||||||
|
|
||||||
<div class="receiver-signature-panel">
|
|
||||||
|
|
||||||
@* ?? Envelope info header ???????????????????????????????????????????????? *@
|
|
||||||
@if (_envelopeReceiver is not null) {
|
|
||||||
<div class="receiver-info-header">
|
|
||||||
<div class="receiver-info-header__gradient">
|
|
||||||
<div class="receiver-info-header__left">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" class="receiver-info-header__icon" viewBox="0 0 16 16">
|
|
||||||
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<div class="receiver-info-header__title">@(_envelopeReceiver.Envelope?.Title ?? "Dokument")</div>
|
|
||||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName) || !string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) {
|
|
||||||
<div class="receiver-info-header__sender">
|
|
||||||
Von
|
|
||||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName)) {
|
|
||||||
<strong>@_envelopeReceiver.Envelope!.User!.FullName</strong>
|
|
||||||
}
|
|
||||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) {
|
|
||||||
<span class="opacity-75"><@_envelopeReceiver.Envelope!.User!.Email></span>
|
|
||||||
}
|
|
||||||
· @_envelopeReceiver.Envelope?.AddedWhen.ToString("dd.MM.yyyy")
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="receiver-info-header__badges">
|
|
||||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Name)) {
|
|
||||||
<span class="receiver-info-badge">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
|
||||||
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Z"/>
|
|
||||||
</svg>
|
|
||||||
@_envelopeReceiver.Name
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.CompanyName)) {
|
|
||||||
<span class="receiver-info-badge">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
|
||||||
<path d="M14.763.075A.5.5 0 0 1 15 .5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5V14h-1v1.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V10a.5.5 0 0 1 .342-.474L6 7.64V4.5a.5.5 0 0 1 .276-.447l8-4a.5.5 0 0 1 .487.022Z"/>
|
|
||||||
</svg>
|
|
||||||
@_envelopeReceiver.CompanyName
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.JobTitle)) {
|
|
||||||
<span class="receiver-info-badge receiver-info-badge--muted">@_envelopeReceiver.JobTitle</span>
|
|
||||||
}
|
|
||||||
@{
|
|
||||||
var docElements = _envelopeReceiver.Envelope?.Documents?.FirstOrDefault()?.Elements;
|
|
||||||
int sigCount = docElements?.Count() ?? _annotations.Count;
|
|
||||||
}
|
|
||||||
@if (sigCount > 0) {
|
|
||||||
<span class="receiver-info-badge receiver-info-badge--accent">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
|
||||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
|
||||||
</svg>
|
|
||||||
@sigCount @(sigCount == 1 ? "Unterschriftsfeld" : "Unterschriftsfelder")
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message)) {
|
|
||||||
<div class="receiver-info-message">@_envelopeReceiver.Envelope!.Message</div>
|
|
||||||
}
|
|
||||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage)) {
|
|
||||||
<div class="receiver-info-private-message">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1 flex-shrink-0" 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>
|
|
||||||
@_envelopeReceiver.PrivateMessage
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@* ?? Signature action bar ???????????????????????????????????????????????? *@
|
|
||||||
<div class="receiver-action-bar">
|
|
||||||
<div class="receiver-action-bar__inner">
|
|
||||||
<button class="btn btn-sm @(_capturedSignature is not null ? "btn-outline-success" : "btn-primary")" @onclick="OpenSignaturePopupAsync">
|
|
||||||
@if (_capturedSignature is not null) {
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
|
||||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
|
||||||
</svg>
|
|
||||||
<span>Unterschrift gespeichert</span>
|
|
||||||
} else {
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
|
||||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
|
||||||
</svg>
|
|
||||||
<span>Unterschrift erstellen</span>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@if (_annotations.Count > 0 && !SignatureApplied) {
|
|
||||||
<div class="receiver-action-bar__progress">
|
|
||||||
<div class="progress" style="height:5px; min-width:70px; width:90px;">
|
|
||||||
<div class="progress-bar @(_checkedAnnotations.Count == _annotations.Count ? "bg-success" : "bg-primary")"
|
|
||||||
style="width:@(_annotations.Count > 0 ? (_checkedAnnotations.Count * 100 / _annotations.Count) : 0)%"
|
|
||||||
role="progressbar"></div>
|
|
||||||
</div>
|
|
||||||
<span class="text-muted" style="font-size:0.75rem;">
|
|
||||||
@_checkedAnnotations.Count / @_annotations.Count
|
|
||||||
Seite@(AnnotationPages.Count() == 1 ? "" : "n") @string.Join(",", AnnotationPages)
|
|
||||||
</span>
|
|
||||||
@if (_capturedSignature is null) {
|
|
||||||
<span class="text-muted fst-italic" style="font-size:0.72rem;">Zuerst Unterschrift erstellen</span>
|
|
||||||
} else if (_checkedAnnotations.Count == _annotations.Count) {
|
|
||||||
<span class="text-success fw-semibold" style="font-size:0.72rem;">✓ Wird angewendet…</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(SignatureValidationMessage)) {
|
|
||||||
<span class="text-danger" style="font-size:0.78rem;">@SignatureValidationMessage</span>
|
|
||||||
}
|
|
||||||
@if (SignatureApplied) {
|
|
||||||
<span class="text-success" style="font-size:0.78rem;">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
|
||||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
|
|
||||||
</svg>
|
|
||||||
Unterschrift angewendet
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="ms-auto d-flex align-items-center gap-2">
|
|
||||||
<button class="btn btn-sm btn-success" disabled="@(!SignatureApplied)" @onclick="ExportSignedPdfAsync">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
|
||||||
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
|
||||||
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
|
||||||
</svg>
|
|
||||||
PDF exportieren
|
|
||||||
</button>
|
|
||||||
@if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
|
||||||
<button class="btn btn-sm btn-outline-danger" @onclick="LogoutAsync" disabled="@IsLoggingOut" title="Abmelden">
|
|
||||||
@if (IsLoggingOut) {
|
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
||||||
} else {
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
||||||
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
|
|
||||||
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DxPopup @bind-Visible="SignaturePopupVisible"
|
|
||||||
HeaderText="Unterschrift erfassen"
|
|
||||||
Width="620px"
|
|
||||||
ShowFooter="true"
|
|
||||||
CloseOnEscape="false"
|
|
||||||
ShowCloseButton="false"
|
|
||||||
CloseOnOutsideClick="false"
|
|
||||||
Shown="OnPopupShownAsync">
|
|
||||||
<BodyContentTemplate>
|
|
||||||
<ul class="nav nav-tabs mb-3">
|
|
||||||
<li class="nav-item">
|
|
||||||
<button type="button" class="nav-link @(ActiveSignatureTab == SignatureTabDraw ? "active" : null)" @onclick="() => SetSignatureTabAsync(SignatureTabDraw)">Zeichnen</button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<button type="button" class="nav-link @(ActiveSignatureTab == SignatureTabText ? "active" : null)" @onclick="() => SetSignatureTabAsync(SignatureTabText)">Text</button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<button type="button" class="nav-link @(ActiveSignatureTab == SignatureTabImage ? "active" : null)" @onclick="() => SetSignatureTabAsync(SignatureTabImage)">Bild</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
@if(ActiveSignatureTab == SignatureTabDraw) {
|
|
||||||
<p class="text-muted mb-2">Bitte unterschreiben Sie im folgenden Feld.</p>
|
|
||||||
<canvas id="receiver-signature-pad" width="520" height="160" class="border rounded bg-white w-100" style="max-width: 520px; touch-action: none;"></canvas>
|
|
||||||
} else if(ActiveSignatureTab == SignatureTabText) {
|
|
||||||
<p class="text-muted mb-2">Geben Sie Ihre Unterschrift als Text ein und waehlen Sie eine Schriftart.</p>
|
|
||||||
<div class="row g-2 mb-2">
|
|
||||||
<div class="col-12 col-md-7">
|
|
||||||
<input class="form-control" placeholder="Ihre Unterschrift" value="@TypedSignatureText" @oninput="OnTypedSignatureChanged" />
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-5">
|
|
||||||
<select class="form-select" value="@TypedSignatureFont" @onchange="OnTypedSignatureFontChanged">
|
|
||||||
@foreach(var font in TypedSignatureFonts) {
|
|
||||||
<option value="@font.Value" style="font-family: @font.Value">@font.Text</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<canvas id="receiver-typed-signature-pad" width="520" height="160" class="border rounded bg-white w-100" style="max-width: 520px;"></canvas>
|
|
||||||
} else {
|
|
||||||
<p class="text-muted mb-2">Laden Sie ein Bild Ihrer Unterschrift hoch.</p>
|
|
||||||
<input id="receiver-signature-image-input" class="form-control mb-2" type="file" accept="image/png,image/jpeg,image/webp" />
|
|
||||||
<canvas id="receiver-image-signature-pad" width="520" height="160" class="border rounded bg-white w-100" style="max-width: 520px;"></canvas>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
<div class="border-top mt-3 pt-3">
|
|
||||||
<p class="text-muted mb-2">Bitte geben Sie die folgenden Angaben ein. Das Datum wird automatisch hinzugefuegt.</p>
|
|
||||||
<div class="row g-2">
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<label class="form-label" for="receiver-signer-name">Vor- und Nachname *</label>
|
|
||||||
<input id="receiver-signer-name" class="form-control" value="@SignerFullName" @oninput="args => SignerFullName = args.Value?.ToString() ?? string.Empty" />
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<label class="form-label" for="receiver-signer-position">Position</label>
|
|
||||||
<input id="receiver-signer-position" class="form-control" value="@SignerPosition" @oninput="args => SignerPosition = args.Value?.ToString() ?? string.Empty" />
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-6">
|
|
||||||
<label class="form-label" for="receiver-signature-place">Ort *</label>
|
|
||||||
<input id="receiver-signature-place" class="form-control" value="@SignaturePlace" @oninput="args => SignaturePlace = args.Value?.ToString() ?? string.Empty" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if(!string.IsNullOrWhiteSpace(PopupValidationMessage)) {
|
|
||||||
<div class="text-danger mt-2">@PopupValidationMessage</div>
|
|
||||||
}
|
|
||||||
</BodyContentTemplate>
|
|
||||||
<FooterContentTemplate>
|
|
||||||
<div class="d-flex gap-2 flex-wrap justify-content-end w-100">
|
|
||||||
<button class="btn btn-outline-secondary" @onclick="RenewSignatureAsync">Unterschrift erneuern</button>
|
|
||||||
<button class="btn btn-primary" @onclick="SaveSignatureAsync">Speichern</button>
|
|
||||||
</div>
|
|
||||||
</FooterContentTemplate>
|
|
||||||
</DxPopup>
|
|
||||||
|
|
||||||
<div class="receiver-viewer-wrapper">
|
|
||||||
@if(Report is not null) {
|
|
||||||
<DxReportViewer @key="ViewerKey" @ref="reportViewer" Report="Report" RootCssClasses="w-100 h-100" Zoom="1.3" />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
|
|
||||||
const string SignatureTabDraw = "draw";
|
|
||||||
const string SignatureTabText = "text";
|
|
||||||
const string SignatureTabImage = "image";
|
|
||||||
const string DrawCanvasId = "receiver-signature-pad";
|
|
||||||
const string TypedCanvasId = "receiver-typed-signature-pad";
|
|
||||||
const string ImageInputId = "receiver-signature-image-input";
|
|
||||||
const string ImageCanvasId = "receiver-image-signature-pad";
|
|
||||||
|
|
||||||
readonly (string Text, string Value)[] TypedSignatureFonts = {
|
|
||||||
("Brush Script", "'Brush Script MT', cursive"),
|
|
||||||
("Segoe Script", "'Segoe Script', cursive"),
|
|
||||||
("Lucida Handwriting", "'Lucida Handwriting', cursive"),
|
|
||||||
("Comic Sans", "'Comic Sans MS', cursive"),
|
|
||||||
("Cursive", "cursive")
|
|
||||||
};
|
|
||||||
|
|
||||||
[Parameter] public string EnvelopeKey { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
DxReportViewer? reportViewer;
|
|
||||||
XtraReport? Report;
|
|
||||||
bool SignatureApplied;
|
|
||||||
bool SignaturePopupVisible;
|
|
||||||
string? SignatureValidationMessage;
|
|
||||||
string? PopupValidationMessage;
|
|
||||||
string ActiveSignatureTab = SignatureTabDraw;
|
|
||||||
string TypedSignatureText = string.Empty;
|
|
||||||
string TypedSignatureFont = "'Brush Script MT', cursive";
|
|
||||||
string SignerFullName = string.Empty;
|
|
||||||
string SignerPosition = string.Empty;
|
|
||||||
string SignaturePlace = string.Empty;
|
|
||||||
int ViewerKey;
|
|
||||||
bool IsLoggingOut;
|
|
||||||
|
|
||||||
IReadOnlyList<AnnotationDto> _annotations = [];
|
|
||||||
IEnumerable<int> AnnotationPages => _annotations.Select(a => a.Page).Distinct().OrderBy(p => p);
|
|
||||||
EnvelopeReceiverDto? _envelopeReceiver;
|
|
||||||
record SignatureCapture(string DataUrl, string FullName, string Position, string Place);
|
|
||||||
SignatureCapture? _capturedSignature;
|
|
||||||
byte[]? _basePdfBytes;
|
|
||||||
// annotation IDs the user has checked via overlay checkboxes
|
|
||||||
readonly HashSet<long> _checkedAnnotations = [];
|
|
||||||
DotNetObjectReference<ReportViewer>? _dotNetRef;
|
|
||||||
int _lastOverlayViewerKey = -1;
|
|
||||||
|
|
||||||
async Task LogoutAsync() {
|
|
||||||
if (string.IsNullOrWhiteSpace(EnvelopeKey) || IsLoggingOut) return;
|
|
||||||
IsLoggingOut = true;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey);
|
|
||||||
Navigation.NavigateTo($"/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
|
||||||
|
|
||||||
// ? REDIRECT: /receiver/{key} -> /envelope/{key} (NEW PDF.js viewer)
|
|
||||||
if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
|
||||||
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
|
||||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
|
||||||
if (!hasAccess) {
|
|
||||||
Navigation.NavigateTo($"/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ActiveSignatureTab = SignatureTabDraw;
|
|
||||||
SignaturePopupVisible = true;
|
|
||||||
SignatureValidationMessage = null;
|
|
||||||
PopupValidationMessage = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_annotations = await AnnotationService.GetAnnotationsAsync(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();
|
|
||||||
Report = initialReport;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
|
||||||
if (firstRender)
|
|
||||||
_dotNetRef = DotNetObjectReference.Create(this);
|
|
||||||
|
|
||||||
|
|
||||||
if (Report is not null && _annotations.Count > 0
|
|
||||||
&& _capturedSignature is not null && !SignatureApplied
|
|
||||||
&& _lastOverlayViewerKey != ViewerKey) {
|
|
||||||
_lastOverlayViewerKey = ViewerKey;
|
|
||||||
await JSRuntime.InvokeVoidAsync(
|
|
||||||
"receiverSignature.installAnnotationCheckboxes",
|
|
||||||
_annotations, _checkedAnnotations.ToArray(), _dotNetRef);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[JSInvokable]
|
|
||||||
public async Task OnAnnotationToggled(long annotationId, bool isChecked) {
|
|
||||||
if (isChecked)
|
|
||||||
_checkedAnnotations.Add(annotationId);
|
|
||||||
else
|
|
||||||
_checkedAnnotations.Remove(annotationId);
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
|
|
||||||
if (_capturedSignature is not null
|
|
||||||
&& !SignatureApplied
|
|
||||||
&& _annotations.Count > 0
|
|
||||||
&& _checkedAnnotations.Count == _annotations.Count) {
|
|
||||||
// K?sa bekleme: kullan?c? son tick'in görsel feedback'ini görsün
|
|
||||||
await Task.Delay(400);
|
|
||||||
await SubmitSignaturesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenSignaturePopupAsync() {
|
|
||||||
ActiveSignatureTab = SignatureTabDraw;
|
|
||||||
SignaturePopupVisible = true;
|
|
||||||
SignatureValidationMessage = null;
|
|
||||||
PopupValidationMessage = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task OnPopupShownAsync() {
|
|
||||||
await InitializeActiveSignatureTabAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task SetSignatureTabAsync(string tab) {
|
|
||||||
ActiveSignatureTab = tab;
|
|
||||||
PopupValidationMessage = null;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
await Task.Delay(50);
|
|
||||||
await InitializeActiveSignatureTabAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task InitializeActiveSignatureTabAsync() {
|
|
||||||
if(ActiveSignatureTab == SignatureTabDraw) {
|
|
||||||
await JSRuntime.InvokeVoidAsync("receiverSignature.initialize", DrawCanvasId);
|
|
||||||
} else if(ActiveSignatureTab == SignatureTabText) {
|
|
||||||
await JSRuntime.InvokeVoidAsync("receiverSignature.initializeTyped", TypedCanvasId);
|
|
||||||
await RenderTypedSignatureAsync();
|
|
||||||
} else {
|
|
||||||
await JSRuntime.InvokeVoidAsync("receiverSignature.initializeImage", ImageInputId, ImageCanvasId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task RenewSignatureAsync() {
|
|
||||||
PopupValidationMessage = null;
|
|
||||||
|
|
||||||
if(ActiveSignatureTab == SignatureTabDraw) {
|
|
||||||
await JSRuntime.InvokeVoidAsync("receiverSignature.clear", DrawCanvasId);
|
|
||||||
} else if(ActiveSignatureTab == SignatureTabText) {
|
|
||||||
TypedSignatureText = string.Empty;
|
|
||||||
await JSRuntime.InvokeVoidAsync("receiverSignature.clearTyped", TypedCanvasId);
|
|
||||||
} else {
|
|
||||||
await JSRuntime.InvokeVoidAsync("receiverSignature.clearImage", ImageInputId, ImageCanvasId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CloseSignaturePopup() {
|
|
||||||
PopupValidationMessage = null;
|
|
||||||
SignaturePopupVisible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task OnTypedSignatureChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) {
|
|
||||||
TypedSignatureText = args.Value?.ToString() ?? string.Empty;
|
|
||||||
await RenderTypedSignatureAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task OnTypedSignatureFontChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) {
|
|
||||||
TypedSignatureFont = args.Value?.ToString() ?? TypedSignatureFont;
|
|
||||||
await RenderTypedSignatureAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task RenderTypedSignatureAsync() {
|
|
||||||
await JSRuntime.InvokeVoidAsync("receiverSignature.renderTypedSignature", TypedCanvasId, TypedSignatureText, TypedSignatureFont);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task SaveSignatureAsync() {
|
|
||||||
if (string.IsNullOrWhiteSpace(SignerFullName)) {
|
|
||||||
PopupValidationMessage = "Bitte geben Sie Vor- und Nachname ein.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (string.IsNullOrWhiteSpace(SignaturePlace)) {
|
|
||||||
PopupValidationMessage = "Bitte geben Sie den Ort ein.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var signatureDataUrl = await GetActiveSignatureDataUrlAsync();
|
|
||||||
if (string.IsNullOrWhiteSpace(signatureDataUrl)) {
|
|
||||||
PopupValidationMessage = "Die Unterschrift ist fuer den PDF-Export erforderlich.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PopupValidationMessage = null;
|
|
||||||
SignatureValidationMessage = null;
|
|
||||||
_capturedSignature = new(signatureDataUrl, SignerFullName.Trim(), SignerPosition.Trim(), SignaturePlace.Trim());
|
|
||||||
|
|
||||||
// If no annotations, apply immediately (no checkbox step needed)
|
|
||||||
if (_annotations.Count == 0) {
|
|
||||||
var freshReport = BuildFreshBaseReport();
|
|
||||||
AddSignature(freshReport, _capturedSignature.DataUrl, _capturedSignature.FullName, _capturedSignature.Position, _capturedSignature.Place);
|
|
||||||
Report = freshReport;
|
|
||||||
SignatureApplied = true;
|
|
||||||
SignaturePopupVisible = false;
|
|
||||||
ViewerKey++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close popup; checkboxes will appear on the PDF via OnAfterRenderAsync
|
|
||||||
SignaturePopupVisible = false;
|
|
||||||
_lastOverlayViewerKey = -1; // force overlay reinstall
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task SubmitSignaturesAsync() {
|
|
||||||
if (_checkedAnnotations.Count == 0) {
|
|
||||||
SignatureValidationMessage = "Bitte markieren Sie mindestens ein Unterschriftsfeld im Dokument.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_checkedAnnotations.Count < _annotations.Count) {
|
|
||||||
SignatureValidationMessage = $"Bitte markieren Sie alle {_annotations.Count} Unterschriftsfelder. Noch {_annotations.Count - _checkedAnnotations.Count} offen.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SignatureValidationMessage = null;
|
|
||||||
var freshReport = BuildFreshBaseReport();
|
|
||||||
foreach (var ann in _annotations)
|
|
||||||
AddSignatureAtAnnotation(freshReport, ann, _capturedSignature!.DataUrl, _capturedSignature.FullName, _capturedSignature.Position, _capturedSignature.Place);
|
|
||||||
|
|
||||||
Report = freshReport;
|
|
||||||
SignatureApplied = true;
|
|
||||||
ViewerKey++;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task<string?> GetActiveSignatureDataUrlAsync() {
|
|
||||||
if(ActiveSignatureTab == SignatureTabDraw)
|
|
||||||
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getDataUrl", DrawCanvasId);
|
|
||||||
|
|
||||||
if(ActiveSignatureTab == SignatureTabText) {
|
|
||||||
await RenderTypedSignatureAsync();
|
|
||||||
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getTypedDataUrl", TypedCanvasId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getImageDataUrl", ImageCanvasId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task ExportSignedPdfAsync() {
|
|
||||||
if(!SignatureApplied || Report is null) {
|
|
||||||
SignatureValidationMessage = "Bitte fuegen Sie die Unterschrift zuerst zum Bericht hinzu.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
SignatureValidationMessage = null;
|
|
||||||
await reportViewer!.ExportToAsync(ExportFormat.Pdf);
|
|
||||||
} catch(Exception) {
|
|
||||||
SignatureValidationMessage = "Das signierte PDF konnte nicht exportiert werden. Bitte laden Sie die Seite neu und versuchen Sie es erneut.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
XtraReport BuildFreshBaseReport() {
|
|
||||||
if (_basePdfBytes is { Length: > 0 }) {
|
|
||||||
var report = new XtraReport();
|
|
||||||
var detail = new DevExpress.XtraReports.UI.DetailBand();
|
|
||||||
report.Bands.Add(detail);
|
|
||||||
detail.Controls.Add(new DevExpress.XtraReports.UI.XRPdfContent { Source = _basePdfBytes, GenerateOwnPages = true });
|
|
||||||
return report;
|
|
||||||
}
|
|
||||||
return CreateReportInstance();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void AddAnnotationPlaceholders(XtraReport report, IReadOnlyList<AnnotationDto> annotations) {
|
|
||||||
var bottomMargin = report.Bands.OfType<BottomMarginBand>().FirstOrDefault();
|
|
||||||
if (bottomMargin is null) {
|
|
||||||
bottomMargin = new BottomMarginBand();
|
|
||||||
report.Bands.Add(bottomMargin);
|
|
||||||
}
|
|
||||||
|
|
||||||
const float sigWidth = 230F;
|
|
||||||
const float sigHeight = 154F;
|
|
||||||
const float bottomPad = 6F;
|
|
||||||
const float defaultTopPad = 8F;
|
|
||||||
const float maxBandHeight = 210F;
|
|
||||||
|
|
||||||
float requiredHeight = defaultTopPad + sigHeight + bottomPad;
|
|
||||||
bottomMargin.HeightF = Math.Min(maxBandHeight, Math.Max(bottomMargin.HeightF, requiredHeight));
|
|
||||||
float topPad = Math.Max(0F, bottomMargin.HeightF - bottomPad - sigHeight);
|
|
||||||
|
|
||||||
foreach (var ann in annotations) {
|
|
||||||
float sigX = (float)(ann.X);
|
|
||||||
var annotId = ann.Id.ToString();
|
|
||||||
|
|
||||||
var placeholder = new XRLabel {
|
|
||||||
Name = $"receiverSignaturePlaceholder_{annotId}",
|
|
||||||
Text = "\u270e Bitte unterschreiben",
|
|
||||||
BoundsF = new RectangleF(sigX, topPad, sigWidth, sigHeight),
|
|
||||||
Borders = BorderSide.All,
|
|
||||||
BorderColor = System.Drawing.Color.FromArgb(230, 81, 0),
|
|
||||||
BackColor = System.Drawing.Color.FromArgb(30, 255, 236, 153),
|
|
||||||
Font = new DXFont("Open Sans", 10F, DXFontStyle.Regular),
|
|
||||||
ForeColor = System.Drawing.Color.FromArgb(94, 38, 0),
|
|
||||||
TextAlignment = TextAlignment.MiddleCenter
|
|
||||||
};
|
|
||||||
|
|
||||||
bottomMargin.Controls.Add(placeholder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
XtraReport CreateReportInstance() {
|
|
||||||
return ReportStorage.TryGetReport("LargeDatasetReport", out var savedReport)
|
|
||||||
? savedReport
|
|
||||||
: PredefinedReports.ReportsFactory.GetReport("LargeDatasetReport");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void AddSignature(XtraReport report, string signatureDataUrl, string signerFullName, string signerPosition, string signaturePlace) {
|
|
||||||
var imageBytes = Convert.FromBase64String(signatureDataUrl[(signatureDataUrl.IndexOf(',') + 1)..]);
|
|
||||||
using var imageStream = new MemoryStream(imageBytes);
|
|
||||||
var imageSource = new ImageSource(DXImage.FromStream(imageStream));
|
|
||||||
var bottomMargin = report.Bands.OfType<BottomMarginBand>().FirstOrDefault();
|
|
||||||
|
|
||||||
if(bottomMargin is null) {
|
|
||||||
bottomMargin = new BottomMarginBand();
|
|
||||||
report.Bands.Add(bottomMargin);
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveExistingSignature(bottomMargin);
|
|
||||||
|
|
||||||
// Layout constants
|
|
||||||
const float sigX = 390F;
|
|
||||||
const float sigWidth = 230F;
|
|
||||||
const float sigImgHeight = 70F;
|
|
||||||
const float infoHeight = 65F; // up to 4 lines at 8pt
|
|
||||||
const float innerGap = 5F;
|
|
||||||
const float bottomPad = 6F;
|
|
||||||
const float defaultTopPad = 8F;
|
|
||||||
const float maxBandHeight = 210F;
|
|
||||||
|
|
||||||
float requiredHeight = defaultTopPad + sigImgHeight + innerGap + infoHeight + bottomPad;
|
|
||||||
|
|
||||||
// Grow band if needed, but cap at maxBandHeight to avoid overlapping page content
|
|
||||||
bottomMargin.HeightF = Math.Min(maxBandHeight, Math.Max(bottomMargin.HeightF, requiredHeight));
|
|
||||||
|
|
||||||
// If band is tighter than required, compress top padding so content still fits
|
|
||||||
float topPad = Math.Max(0F, bottomMargin.HeightF - bottomPad - infoHeight - innerGap - sigImgHeight);
|
|
||||||
|
|
||||||
float imageY = topPad;
|
|
||||||
float labelY = imageY + sigImgHeight + innerGap;
|
|
||||||
|
|
||||||
var signatureInformation = string.IsNullOrWhiteSpace(signerPosition)
|
|
||||||
? $"{signerFullName}\n{signaturePlace}, {DateTime.Now:d}"
|
|
||||||
: $"{signerFullName}\n{signerPosition}\n{signaturePlace}, {DateTime.Now:d}";
|
|
||||||
|
|
||||||
var signature = new XRPictureBox {
|
|
||||||
Name = "receiverSignatureImage",
|
|
||||||
ImageSource = imageSource,
|
|
||||||
BoundsF = new RectangleF(sigX, imageY, sigWidth, sigImgHeight),
|
|
||||||
Sizing = ImageSizeMode.ZoomImage,
|
|
||||||
Borders = BorderSide.Bottom,
|
|
||||||
BorderColor = System.Drawing.Color.FromArgb(73, 80, 87)
|
|
||||||
};
|
|
||||||
|
|
||||||
var signatureLabel = new XRLabel {
|
|
||||||
Name = "receiverSignatureLabel",
|
|
||||||
Text = signatureInformation,
|
|
||||||
Multiline = true,
|
|
||||||
BoundsF = new RectangleF(sigX, labelY, sigWidth, infoHeight),
|
|
||||||
Font = new DXFont("Open Sans", 8F, DXFontStyle.Regular),
|
|
||||||
ForeColor = System.Drawing.Color.FromArgb(73, 80, 87),
|
|
||||||
TextAlignment = TextAlignment.TopLeft
|
|
||||||
};
|
|
||||||
|
|
||||||
bottomMargin.Controls.AddRange(new XRControl[] { signature, signatureLabel });
|
|
||||||
}
|
|
||||||
|
|
||||||
static void RemoveExistingSignature(BottomMarginBand bottomMargin) {
|
|
||||||
var controls = bottomMargin.Controls
|
|
||||||
.Cast<XRControl>()
|
|
||||||
.Where(control => control.Name is "receiverSignatureLabel" or "receiverSignatureImage")
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
foreach(var control in controls)
|
|
||||||
bottomMargin.Controls.Remove(control);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void AddSignatureAtAnnotation(XtraReport report, AnnotationDto? annotation, string signatureDataUrl, string signerFullName, string signerPosition, string signaturePlace) {
|
|
||||||
var imageBytes = Convert.FromBase64String(signatureDataUrl[(signatureDataUrl.IndexOf(',') + 1)..]);
|
|
||||||
using var imageStream = new MemoryStream(imageBytes);
|
|
||||||
var imageSource = new ImageSource(DXImage.FromStream(imageStream));
|
|
||||||
|
|
||||||
var detail = report.Bands.OfType<DevExpress.XtraReports.UI.DetailBand>().FirstOrDefault();
|
|
||||||
if (detail is null) return;
|
|
||||||
|
|
||||||
var annotId = annotation?.Id.ToString() ?? "0";
|
|
||||||
RemoveExistingSignatureById(detail, annotId);
|
|
||||||
|
|
||||||
const float sigWidth = 230F;
|
|
||||||
const float sigImgHeight = 70F;
|
|
||||||
const float infoHeight = 48.75F;
|
|
||||||
const float innerGap = 5F;
|
|
||||||
|
|
||||||
float sigX = (float)(annotation?.X ?? 390.0);
|
|
||||||
float imageY = (float)(annotation?.Y ?? 900.0);
|
|
||||||
float labelY = imageY + sigImgHeight + innerGap;
|
|
||||||
int targetPage = annotation?.Page ?? 1;
|
|
||||||
|
|
||||||
var signatureInformation = string.IsNullOrWhiteSpace(signerPosition)
|
|
||||||
? $"{signerFullName}\n{signaturePlace}, {DateTime.Now:d}"
|
|
||||||
: $"{signerFullName}\n{signerPosition}\n{signaturePlace}, {DateTime.Now:d}";
|
|
||||||
|
|
||||||
var signature = new XRPictureBox {
|
|
||||||
Name = $"receiverSignatureImage_{annotId}",
|
|
||||||
ImageSource = imageSource,
|
|
||||||
BoundsF = new RectangleF(sigX, imageY, sigWidth, sigImgHeight),
|
|
||||||
Sizing = ImageSizeMode.ZoomImage,
|
|
||||||
Borders = BorderSide.Bottom,
|
|
||||||
BorderColor = System.Drawing.Color.FromArgb(73, 80, 87),
|
|
||||||
BackColor = Color.FromArgb(219, 219, 219)
|
|
||||||
};
|
|
||||||
|
|
||||||
var signatureLabel = new XRLabel {
|
|
||||||
Name = $"receiverSignatureLabel_{annotId}",
|
|
||||||
Text = signatureInformation,
|
|
||||||
Multiline = true,
|
|
||||||
BoundsF = new RectangleF(sigX, labelY - 5, sigWidth, infoHeight),
|
|
||||||
Font = new DXFont("Open Sans", 8F, DXFontStyle.Regular),
|
|
||||||
ForeColor = System.Drawing.Color.FromArgb(73, 80, 87),
|
|
||||||
TextAlignment = TextAlignment.TopCenter,
|
|
||||||
BackColor = Color.FromArgb(219, 219, 219)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show each control only on the target page using an independent print counter
|
|
||||||
int sigPrintCount = 0;
|
|
||||||
signature.BeforePrint += (_, e) => {
|
|
||||||
sigPrintCount++;
|
|
||||||
e.Cancel = sigPrintCount != targetPage;
|
|
||||||
};
|
|
||||||
|
|
||||||
int lblPrintCount = 0;
|
|
||||||
signatureLabel.BeforePrint += (_, e) => {
|
|
||||||
lblPrintCount++;
|
|
||||||
e.Cancel = lblPrintCount != targetPage;
|
|
||||||
};
|
|
||||||
|
|
||||||
detail.Controls.AddRange(new XRControl[] { signature, signatureLabel });
|
|
||||||
}
|
|
||||||
|
|
||||||
static void RemoveExistingSignatureById(DevExpress.XtraReports.UI.DetailBand detail, string annotId) {
|
|
||||||
var controls = detail.Controls
|
|
||||||
.Cast<XRControl>()
|
|
||||||
.Where(c => c.Name == $"receiverSignatureImage_{annotId}" || c.Name == $"receiverSignatureLabel_{annotId}")
|
|
||||||
.ToArray();
|
|
||||||
foreach (var c in controls)
|
|
||||||
detail.Controls.Remove(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
_dotNetRef?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
@page "/example/test"
|
|
||||||
@using System.Drawing
|
|
||||||
@using DevExpress.Drawing
|
|
||||||
@using DevExpress.Utils
|
|
||||||
@using DevExpress.XtraPrinting
|
|
||||||
@using DevExpress.XtraPrinting.Drawing
|
|
||||||
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
|
|
||||||
@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand
|
|
||||||
@using XRLabel = DevExpress.XtraReports.UI.XRLabel
|
|
||||||
@using XRPictureBox = DevExpress.XtraReports.UI.XRPictureBox
|
|
||||||
@using XRControl = DevExpress.XtraReports.UI.XRControl
|
|
||||||
@using ImageSizeMode = DevExpress.XtraPrinting.ImageSizeMode
|
|
||||||
@using EnvelopeGenerator.ReceiverUI.Services
|
|
||||||
@using DevExpress.Blazor.Reporting
|
|
||||||
@inject IJSRuntime JSRuntime
|
|
||||||
@inject InMemoryReportStorageWebExtension ReportStorage
|
|
||||||
@inject EnvelopeGenerator.ReceiverUI.Services.DocumentService DocumentService
|
|
||||||
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
|
|
||||||
@inject HttpClient Http
|
|
||||||
@using System;
|
|
||||||
|
|
||||||
<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" />
|
|
||||||
|
|
||||||
<div class="receiver-page-layout">
|
|
||||||
|
|
||||||
<div class="receiver-viewer-wrapper">
|
|
||||||
<embed class="w-100 h-50" src="/docs/Document.pdf" type="application/pdf" />
|
|
||||||
@if (PdfBytes is { Length: > 0 }) {
|
|
||||||
<DxPdfViewer DocumentContent="PdfBytes" CssClass="w-100 h-50" />
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
<div>Not found</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
|
|
||||||
const string SignatureTabDraw = "draw";
|
|
||||||
const string SignatureTabText = "text";
|
|
||||||
const string SignatureTabImage = "image";
|
|
||||||
const string DrawCanvasId = "receiver-signature-pad";
|
|
||||||
const string TypedCanvasId = "receiver-typed-signature-pad";
|
|
||||||
const string ImageInputId = "receiver-signature-image-input";
|
|
||||||
const string ImageCanvasId = "receiver-image-signature-pad";
|
|
||||||
|
|
||||||
readonly (string Text, string Value)[] TypedSignatureFonts = {
|
|
||||||
("Brush Script", "'Brush Script MT', cursive"),
|
|
||||||
("Segoe Script", "'Segoe Script', cursive"),
|
|
||||||
("Lucida Handwriting", "'Lucida Handwriting', cursive"),
|
|
||||||
("Comic Sans", "'Comic Sans MS', cursive"),
|
|
||||||
("Cursive", "cursive")
|
|
||||||
};
|
|
||||||
|
|
||||||
[Parameter] public string? EnvelopeKey { get; set; }
|
|
||||||
|
|
||||||
DxReportViewer? reportViewer;
|
|
||||||
XtraReport? Report;
|
|
||||||
string PdfViewerUrl = string.Empty;
|
|
||||||
byte[]? PdfBytes;
|
|
||||||
byte[]? SignedPdfBytes;
|
|
||||||
bool SignatureApplied;
|
|
||||||
bool SignaturePopupVisible;
|
|
||||||
string? PopupValidationMessage;
|
|
||||||
string ActiveSignatureTab = SignatureTabDraw;
|
|
||||||
string TypedSignatureText = string.Empty;
|
|
||||||
string TypedSignatureFont = "'Brush Script MT', cursive";
|
|
||||||
string SignerFullName = string.Empty;
|
|
||||||
string SignerPosition = string.Empty;
|
|
||||||
string SignaturePlace = string.Empty;
|
|
||||||
int ViewerKey;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
|
||||||
PdfBytes = await Http.GetByteArrayAsync("/docs/Document.pdf");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
@page "/envelope/login/{EnvelopeKey}"
|
|
||||||
@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="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>
|
|
||||||
</div>
|
|
||||||
<h5 class="mb-0 fw-semibold">Dokument öffnen</h5>
|
|
||||||
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">Sicherer Zugang mit Zugangscode</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-body p-4">
|
|
||||||
|
|
||||||
<p class="text-muted mb-4" style="font-size: 0.875rem; line-height: 1.5;">
|
|
||||||
Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
@if (LoginResult == EnvelopeLoginResult.NotFound) {
|
|
||||||
<div class="alert alert-warning 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.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<strong>Dokument nicht gefunden.</strong><br />
|
|
||||||
<span style="font-size:0.85rem;">Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
} else if (LoginResult == EnvelopeLoginResult.InvalidCode) {
|
|
||||||
<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ültiger Zugangscode.</strong><br />
|
|
||||||
<span style="font-size:0.85rem;">Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
} else if (LoginResult == EnvelopeLoginResult.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-4">
|
|
||||||
<label class="form-label fw-medium" for="login-access-code">
|
|
||||||
Zugangscode
|
|
||||||
<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="M3.5 11.5a3.5 3.5 0 1 1 3.163-5H14L15.5 8 14 9.5l-1-1-1 1-1-1-1 1-1-1-1.837 1.337A3.5 3.5 0 0 1 3.5 11.5zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<input id="login-access-code"
|
|
||||||
type="@(ShowCode ? "text" : "password")"
|
|
||||||
class="form-control border-start-0 border-end-0 @(LoginResult == EnvelopeLoginResult.InvalidCode ? "is-invalid" : null)"
|
|
||||||
placeholder="Zugangscode eingeben"
|
|
||||||
@bind="AccessCode"
|
|
||||||
@bind:event="oninput"
|
|
||||||
@onkeydown="OnKeyDownAsync"
|
|
||||||
disabled="@IsLoading"
|
|
||||||
autocomplete="one-time-code" />
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-outline-secondary border-start-0"
|
|
||||||
style="border-left: none;"
|
|
||||||
tabindex="-1"
|
|
||||||
@onclick="() => ShowCode = !ShowCode">
|
|
||||||
@if (ShowCode) {
|
|
||||||
<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(AccessCode))">
|
|
||||||
@if (IsLoading) {
|
|
||||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
|
||||||
<span>Überprüfen …</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 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>Dokument öffnen</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 Absender des Dokuments.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
[Parameter] public string EnvelopeKey { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
string AccessCode = string.Empty;
|
|
||||||
bool ShowCode;
|
|
||||||
bool IsLoading;
|
|
||||||
EnvelopeLoginResult? LoginResult;
|
|
||||||
|
|
||||||
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) {
|
|
||||||
if (e.Key == "Enter")
|
|
||||||
await SubmitAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task SubmitAsync() {
|
|
||||||
if (string.IsNullOrWhiteSpace(AccessCode) || IsLoading) return;
|
|
||||||
|
|
||||||
IsLoading = true;
|
|
||||||
LoginResult = null;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
|
|
||||||
var result = await AuthService.LoginEnvelopeReceiverAsync(EnvelopeKey, AccessCode.Trim());
|
|
||||||
|
|
||||||
if (result == EnvelopeLoginResult.Success) {
|
|
||||||
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LoginResult = result;
|
|
||||||
IsLoading = false;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,123 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 2.0
|
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
|
||||||
that is mostly human readable. The generation and parsing of the
|
|
||||||
various data types are done through the TypeConverter classes
|
|
||||||
associated with the data types.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
|
||||||
<resheader name="version">2.0</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
|
||||||
<comment>This is a comment</comment>
|
|
||||||
</data>
|
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
|
||||||
name/value pairs.
|
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
|
||||||
text/value conversion through the TypeConverter architecture.
|
|
||||||
Classes that don't support this are serialized and stored with the
|
|
||||||
mimetype set.
|
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
|
||||||
read any of the formats listed below.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
|
||||||
value : The object must be serialized into a byte array
|
|
||||||
: using a System.ComponentModel.TypeConverter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
-->
|
|
||||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
|
||||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<xsd:element name="metadata">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="assembly">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:attribute name="alias" type="xsd:string" />
|
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="data">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="resheader">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:choice>
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:schema>
|
|
||||||
<resheader name="resmimetype">
|
|
||||||
<value>text/microsoft-resx</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="version">
|
|
||||||
<value>2.0</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<metadata name="objectDataSource1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
|
||||||
<value>17, 17</value>
|
|
||||||
</metadata>
|
|
||||||
</root>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using DevExpress.XtraReports.UI;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.PredefinedReports {
|
|
||||||
public static class ReportsFactory
|
|
||||||
{
|
|
||||||
public static readonly Dictionary<string, Func<XtraReport>> Reports = new() {
|
|
||||||
["LargeDatasetReport"] = () => new PredefinedReports.Report()
|
|
||||||
};
|
|
||||||
|
|
||||||
public static XtraReport GetReport(string reportName) {
|
|
||||||
return Reports[reportName]();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
|
||||||
using EnvelopeGenerator.ReceiverUI;
|
|
||||||
using DevExpress.DataAccess.Web;
|
|
||||||
using EnvelopeGenerator.ReceiverUI.Services;
|
|
||||||
using EnvelopeGenerator.ReceiverUI.Options;
|
|
||||||
using DevExpress.XtraReports.Services;
|
|
||||||
using DevExpress.Blazor.Reporting;
|
|
||||||
using DevExpress.XtraReports.Web.Extensions;
|
|
||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
|
||||||
builder.RootComponents.Add<App>("#app");
|
|
||||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
|
||||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
|
||||||
builder.Services.Configure<ApiOptions>(opts =>
|
|
||||||
builder.Configuration.GetSection(ApiOptions.SectionName).Bind(opts));
|
|
||||||
builder.Services.Configure<PdfViewerOptions>(opts =>
|
|
||||||
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
|
|
||||||
builder.Services.AddScoped<DocumentService>();
|
|
||||||
builder.Services.AddScoped<AuthService>();
|
|
||||||
builder.Services.AddScoped<AnnotationService>();
|
|
||||||
builder.Services.AddScoped<EnvelopeReceiverService>();
|
|
||||||
builder.Services.AddScoped<SignatureService>();
|
|
||||||
builder.Services.AddScoped<SignatureCacheService>();
|
|
||||||
builder.Services.AddSingleton<AppVersionService>();
|
|
||||||
|
|
||||||
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
|
|
||||||
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
|
|
||||||
|
|
||||||
builder.Services.AddDevExpressBlazorReportingWebAssembly(configure => {
|
|
||||||
configure.UseDevelopmentMode();
|
|
||||||
});
|
|
||||||
builder.Services.AddScoped<IDataSourceWizardJsonConnectionStorage, CustomDataSourceWizardJsonDataConnectionStorage>();
|
|
||||||
builder.Services.AddScoped<IJsonDataConnectionProviderFactory, CustomJsonDataConnectionProviderFactory>();
|
|
||||||
builder.Services.AddScoped<IObjectDataSourceWizardTypeProvider, ObjectDataSourceWizardCustomTypeProvider>();
|
|
||||||
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.ReceiverUI.Data.DataItemList));
|
|
||||||
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.ReceiverUI.PredefinedReports.Report));
|
|
||||||
builder.Services.AddSingleton<InMemoryReportStorageWebExtension>();
|
|
||||||
builder.Services.AddSingleton<ReportStorageWebExtension>(sp => sp.GetRequiredService<InMemoryReportStorageWebExtension>());
|
|
||||||
builder.Services.AddScoped<IReportProviderAsync, CustomReportProvider>();
|
|
||||||
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
|
|
||||||
|
|
||||||
var host = builder.Build();
|
|
||||||
await FontLoader.LoadFonts(host.Services.GetRequiredService<HttpClient>(), new List<string> { "opensans.ttf" });
|
|
||||||
await host.RunAsync();
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"profiles": {
|
|
||||||
"EnvelopeGenerator.ReceiverUI": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"launchBrowser": true,
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
},
|
|
||||||
"applicationUrl": "https://localhost:52936;http://localhost:52937"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
using System.Net.Http.Json;
|
|
||||||
using System.Text.Json;
|
|
||||||
using EnvelopeGenerator.ReceiverUI.Models;
|
|
||||||
using EnvelopeGenerator.ReceiverUI.Options;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves annotation positions from the API.
|
|
||||||
/// The URL is composed as <c>{BaseUrl}/api/Annotation/{envelopeKey}</c>.
|
|
||||||
/// During development, <c>BaseUrl</c> is empty so the request resolves to the
|
|
||||||
/// YARP-proxied route on the same origin, which currently serves
|
|
||||||
/// <c>fake-data/annotations.json</c>. To switch to real data, update the
|
|
||||||
/// YARP route in <c>yarp.json</c> — no code change required.
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Use SignatureService.")]
|
|
||||||
public class AnnotationService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
|
||||||
{
|
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
|
||||||
|
|
||||||
public async Task<IReadOnlyList<AnnotationDto>> GetAnnotationsAsync(string envelopeKey, CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
var url = $"{apiOptions.Value.BaseUrl}/api/Annotation/{Uri.EscapeDataString(envelopeKey)}";
|
|
||||||
var response = await http.GetAsync(url, cancel);
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
var result = await response.Content.ReadFromJsonAsync<List<AnnotationDto>>(_jsonOptions, cancel);
|
|
||||||
return result ?? [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides application version for cache busting static assets.
|
|
||||||
/// Version is automatically incremented on each build via AssemblyVersion.
|
|
||||||
/// </summary>
|
|
||||||
public class AppVersionService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Current application version (e.g., "1.0.0.0")
|
|
||||||
/// </summary>
|
|
||||||
public string Version { get; }
|
|
||||||
|
|
||||||
public AppVersionService()
|
|
||||||
{
|
|
||||||
// Get version from assembly metadata
|
|
||||||
Version = typeof(AppVersionService).Assembly.GetName().Version?.ToString() ?? "1.0.0.0";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generates versioned URL for static assets (cache busting)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Asset path (e.g., "css/envelope-viewer.css")</param>
|
|
||||||
/// <returns>Versioned URL (e.g., "css/envelope-viewer.css?v=1.0.0.0")</returns>
|
|
||||||
public string GetVersionedUrl(string path) => $"{path}?v={Version}";
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Net.Http.Json;
|
|
||||||
using EnvelopeGenerator.ReceiverUI.Options;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks whether the current user holds a valid receiver token for the given envelope key.
|
|
||||||
/// Calls GET /api/auth/check/envelope/{envelopeKey}.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<bool> CheckEnvelopeAccessAsync(string envelopeKey, CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
var response = await http.GetAsync($"{_api.BaseUrl}/api/auth/check/envelope/{Uri.EscapeDataString(envelopeKey)}", cancel);
|
|
||||||
return response.StatusCode == HttpStatusCode.OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Submits the access code for the given envelope key.
|
|
||||||
/// Calls POST /api/Auth/envelope-receiver/{key} with multipart/form-data.
|
|
||||||
/// On success the API sets an authentication cookie automatically.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string envelopeKey, string accessCode, CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
var form = new MultipartFormDataContent();
|
|
||||||
form.Add(new StringContent(accessCode), "AccessCode");
|
|
||||||
|
|
||||||
var response = await http.PostAsync(
|
|
||||||
$"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(envelopeKey)}",
|
|
||||||
form, cancel);
|
|
||||||
|
|
||||||
return response.StatusCode switch
|
|
||||||
{
|
|
||||||
HttpStatusCode.OK => EnvelopeLoginResult.Success,
|
|
||||||
HttpStatusCode.Unauthorized => EnvelopeLoginResult.InvalidCode,
|
|
||||||
HttpStatusCode.NotFound => EnvelopeLoginResult.NotFound,
|
|
||||||
_ => EnvelopeLoginResult.Error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes the per-envelope receiver cookie for the given envelope key.
|
|
||||||
/// Calls POST /api/auth/logout/envelope/{envelopeKey}.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<bool> LogoutEnvelopeReceiverAsync(string envelopeKey, CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
var response = await http.PostAsync(
|
|
||||||
$"{_api.BaseUrl}/api/auth/logout/envelope/{Uri.EscapeDataString(envelopeKey)}",
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using DevExpress.DataAccess.Json;
|
|
||||||
using DevExpress.DataAccess.Web;
|
|
||||||
using DevExpress.DataAccess.Wizard.Services;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.Services
|
|
||||||
{
|
|
||||||
public class CustomDataSourceWizardJsonDataConnectionStorage : IDataSourceWizardJsonConnectionStorage
|
|
||||||
{
|
|
||||||
public static JsonDataConnection GetDefaultConnection() {
|
|
||||||
var uriJsonSource = new UriJsonSource() {
|
|
||||||
Uri = new Uri(@"https://raw.githubusercontent.com/DevExpress-Examples/DataSources/master/JSON/customers.json"),
|
|
||||||
};
|
|
||||||
return new JsonDataConnection(uriJsonSource) { StoreConnectionNameOnly = true, Name = "NWindProductsJson" };
|
|
||||||
}
|
|
||||||
public static List<JsonDataConnection> GetConnections() {
|
|
||||||
var connections = new List<JsonDataConnection> {
|
|
||||||
GetDefaultConnection()
|
|
||||||
};
|
|
||||||
return connections;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IJsonConnectionStorageService.CanSaveConnection => false;
|
|
||||||
bool IJsonConnectionStorageService.ContainsConnection(string connectionName) {
|
|
||||||
return GetConnections().Any(x => x.Name == connectionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerable<JsonDataConnection> IJsonConnectionStorageService.GetConnections() {
|
|
||||||
return GetConnections();
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonDataConnection IJsonDataConnectionProviderService.GetJsonDataConnection(string name) {
|
|
||||||
var connection = GetConnections().FirstOrDefault(x => x.Name == name);
|
|
||||||
if(connection == null)
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
return connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
void IJsonConnectionStorageService.SaveConnection(string connectionName, JsonDataConnection connection, bool saveCredentials) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
using DevExpress.DataAccess.Json;
|
|
||||||
using DevExpress.DataAccess.Web;
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.Services
|
|
||||||
{
|
|
||||||
public class CustomJsonDataConnectionProviderFactory : IJsonDataConnectionProviderFactory {
|
|
||||||
public IJsonDataConnectionProviderService Create() {
|
|
||||||
return new WebDocumentViewerJsonDataConnectionProvider(CustomDataSourceWizardJsonDataConnectionStorage.GetConnections());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class WebDocumentViewerJsonDataConnectionProvider : IJsonDataConnectionProviderService
|
|
||||||
{
|
|
||||||
readonly List<JsonDataConnection> jsonDataConnections;
|
|
||||||
public WebDocumentViewerJsonDataConnectionProvider(List<JsonDataConnection> jsonDataConnections) {
|
|
||||||
this.jsonDataConnections = jsonDataConnections;
|
|
||||||
}
|
|
||||||
public JsonDataConnection GetJsonDataConnection(string name) {
|
|
||||||
var connection = jsonDataConnections.FirstOrDefault(x => x.Name == name);
|
|
||||||
if(connection == null)
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
return connection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
using DevExpress.XtraReports.UI;
|
|
||||||
using DevExpress.XtraReports.Services;
|
|
||||||
using EnvelopeGenerator.ReceiverUI.PredefinedReports;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.Services
|
|
||||||
{
|
|
||||||
public class CustomReportProvider : IReportProviderAsync {
|
|
||||||
private readonly InMemoryReportStorageWebExtension reportStorage;
|
|
||||||
|
|
||||||
public CustomReportProvider(InMemoryReportStorageWebExtension reportStorage) {
|
|
||||||
this.reportStorage = reportStorage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<XtraReport> GetReportAsync(string id, ReportProviderContext context) {
|
|
||||||
if(reportStorage.TryGetReport(id, out var savedReport))
|
|
||||||
return Task.FromResult(savedReport);
|
|
||||||
|
|
||||||
return Task.FromResult(ReportsFactory.GetReport(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Net.Http;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using EnvelopeGenerator.ReceiverUI.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
|
||||||
|
|
||||||
public class DocumentService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
|
||||||
{
|
|
||||||
private readonly ApiOptions _api = apiOptions.Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fetches the PDF bytes 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<byte[]?> GetDocumentAsync(string envelopeKey, CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
var response = await http.GetAsync($"{_api.BaseUrl}/api/Document/{Uri.EscapeDataString(envelopeKey)}", cancel);
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user