Compare commits
413 Commits
120485ee8d
...
bugfix/sav
| Author | SHA1 | Date | |
|---|---|---|---|
| d0c3d79fbc | |||
| e81c7896a1 | |||
| 127e2b253e | |||
| 6d0b78072e | |||
| 3721ea9a1f | |||
| f86588551c | |||
| 0eaa9ff08f | |||
| ec79d7622e | |||
| f558d03bdf | |||
| db79053c62 | |||
| 85dd85aa92 | |||
| c7bc9fbcca | |||
| 1e8e5c96c4 | |||
| 61d04ca51a | |||
| af674a811d | |||
| 123889451f | |||
| 3b5075fe44 | |||
| 48302a374f | |||
| b2c245d6bd | |||
| e9c0ec139a | |||
| 819bdcfcfb | |||
| ad7686b7c2 | |||
| ade542ded0 | |||
| 01c1283622 | |||
| d969dbe843 | |||
| 41779a4f58 | |||
| bf739c1360 | |||
| 0f167d731e | |||
| 021d7cfe29 | |||
| e2322b41cd | |||
| 618451f0c1 | |||
| 96c7cd6e8a | |||
| 4047ba7c6b | |||
| 8386b85592 | |||
| 640f0b95fe | |||
| 181f7f4dec | |||
| 95961a296f | |||
| db613358d5 | |||
| 7a126050cf | |||
| 7081c5f64e | |||
| 12ec5b28fa | |||
| 925e60a45c | |||
| 897860b2bf | |||
| 8933cff558 | |||
| b0b9ba7212 | |||
| 33c75ea7e1 | |||
| 513efa0833 | |||
| 0c81d79a9b | |||
| 696cd1b623 | |||
| 2434f67627 | |||
| 776700b247 | |||
| 7a114f47a6 | |||
| ed44d841e7 | |||
| 4bd0cc2e94 | |||
| 8a4963472c | |||
| 4d3f72a3ed | |||
| 1ebd43e3b0 | |||
| 19aeda5aef | |||
| 940d129c11 | |||
| 44bbdbb835 | |||
| 223017006b | |||
| ad9250c7b1 | |||
| 5cc47ca9c7 | |||
| 156988403a | |||
| e960c02cd4 | |||
| a0e2492757 | |||
| 380a5f4c0c | |||
| 28f8b829e3 | |||
| d00c459124 | |||
| 287c14e114 | |||
| 212ba9d569 | |||
| 735f072dd1 | |||
| 0f53d0509e | |||
| 48cfd3f416 | |||
| 0d2e43b2d6 | |||
| 5aaa37b8ec | |||
| 051a8ce211 | |||
| 519a8bf3f9 | |||
| a7d0b40e39 | |||
| 3dc9a180d3 | |||
| 4dba965c75 | |||
| 4d2538cc2c | |||
| d201eacc97 | |||
| 9daa671655 | |||
| 3fbf011fe3 | |||
| a815705fa4 | |||
| 6e44d0d4b5 | |||
| 42a853cb0d | |||
| 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 | |||
| 71e375d6ea | |||
| 05f64e2b61 | |||
| ed17852542 | |||
| 9947774ba8 | |||
| c6c1decd2a | |||
| 0fdaa1a38d | |||
| 5d66de9f32 | |||
| b6ec5307b6 | |||
| 106e62a912 | |||
| 27940f5d34 | |||
| e776c2edb4 | |||
| 3f0f5d7fb9 | |||
| e11bc9df8e | |||
| 4dca17d39c | |||
| 8baf6b5553 | |||
| 3ca99fdd83 | |||
| 9e37bf1fe2 | |||
| 9a0837caa9 | |||
| 030646f33d | |||
| 88317e40f5 | |||
| 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 | |||
| 3a2fa77862 | |||
| cfa6dbd2de | |||
| eb2603f389 | |||
| 456178bee1 | |||
| 2c41c74510 | |||
| bb73795d68 | |||
| 207992d95a | |||
| d6bafc64a6 | |||
| 3090711892 | |||
| 9dbd8f7952 | |||
| 48a41f2987 | |||
| 96688a951c | |||
| 6f07de3ec4 | |||
| 4611266224 | |||
| c529d03129 | |||
| 829fab9647 | |||
| b2e3605b54 | |||
| 8cbdee2491 | |||
| 151c785af9 | |||
| fa354a05cc | |||
| 1326407462 | |||
| a3c653ddb3 | |||
| 8d736cdc5e | |||
| 4281eaeb22 | |||
| 150fca5f47 | |||
| 1f889d8b58 | |||
| d599fe3156 | |||
| 6c40c48ac8 | |||
| 536b8ef5da | |||
| d35a35c75e | |||
| 7fb1a87cf2 | |||
| a3b33637fd | |||
| bc3134a033 | |||
| f106255c6b | |||
| cb103dcb69 | |||
| 8c1dd9c40d | |||
| ee358ffaab | |||
| 0780dbdd94 | |||
| d722742fe8 | |||
| 8c42105f58 | |||
| 88b196ed6d | |||
| c99511de29 | |||
| 7d0c5a0ee5 | |||
| 7001d7351f | |||
| cf16312394 | |||
| ecb7f45f14 | |||
| 0ba5578d94 | |||
| e093471a24 | |||
| b16ae70762 | |||
| c3e8f09291 | |||
| a9fb82bbea | |||
| 895fd5c509 | |||
| 3b4278d7e0 | |||
| 6d1fb05e10 | |||
| 26da78fa22 | |||
| 9eee2b523d | |||
| 9dc2b9adef | |||
| cc3c5ec9f0 | |||
| 2766d963af | |||
| 8fd9928524 | |||
| fb3ee14f8f | |||
| e3929a99e3 | |||
| b6d86aa3eb | |||
| 4171a3138b | |||
| e98e18cfe0 | |||
| 14aff03de4 | |||
| d828a5bfe2 | |||
| a6e174e7c1 | |||
| fc7aa83513 | |||
| 90661cb856 | |||
| 04e30b0d79 | |||
| d0a50f63db | |||
| 9d20ba1987 | |||
| 66f7b6f5e1 | |||
| a49dd0ff81 | |||
| e420e8d47a | |||
| d7f86adffe | |||
| cfb5c15fda | |||
| a0ed3e2fe4 | |||
| f5505190e9 | |||
| 1bfdbac8ff | |||
| 4a29511491 | |||
| 1089304bf1 | |||
| 9b606a0d3b | |||
| cb6dea319b | |||
| d59aa6157d | |||
| 1569647b60 | |||
| 3bb2a013ab | |||
| 215b755f92 | |||
| 3a94733047 | |||
| 7793d3cbb9 | |||
| 9174065365 | |||
| 19824afc1c | |||
| c7fe3f0b9c | |||
| a98024063a | |||
| e0cab3f965 | |||
| e5347b063d | |||
| ecd695ad37 | |||
| 5a8809ffc1 | |||
| b832637a6a | |||
| fceb266630 | |||
| 87bdef9d5e | |||
| 2fb32fb982 | |||
| 63d050244c | |||
| 126a4acb12 | |||
| 082cb322ef | |||
| cd077aa1bd | |||
| 49ac35153e | |||
| 91a563d995 | |||
| 308cdd03f2 | |||
| f35068e368 | |||
| e490805025 | |||
| 08550f87e6 | |||
| a22ec7a7d3 | |||
| f4681f85e7 | |||
| 0ed4a44df0 | |||
| b8926f25c4 | |||
| 5b220932d3 | |||
| 50c02314ef | |||
| 223bb88f54 | |||
| 38f4da00da |
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
|
||||||
|
|||||||
331
AGENTS.md
Normal file
331
AGENTS.md
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
# EnvelopeGenerator - Agent Guide
|
||||||
|
|
||||||
|
## Must Read First
|
||||||
|
- **`COPILOT_CONTEXT.md`** - Architecture, coordinate systems, migration status
|
||||||
|
- **`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)
|
||||||
|
|
||||||
|
**Frontend:** Blazor Auto (Server+WASM hybrid)
|
||||||
|
- **EnvelopeGenerator.Server** (Server): `@rendermode InteractiveServer` - PDF viewers requiring DevExpress backend
|
||||||
|
- **EnvelopeGenerator.Server.Client** (WASM): `@rendermode InteractiveWebAssembly` - Login, dashboards, business logic
|
||||||
|
|
||||||
|
**Backend + API Host:** `EnvelopeGenerator.Server` (ASP.NET Core 8.0, merged Blazor + Web API host)
|
||||||
|
|
||||||
|
**Proxy:** YARP in `EnvelopeGenerator.Server` is used for external integrations (for example AuthHub forwarding), not for routing app APIs to `EnvelopeGenerator.API`.
|
||||||
|
|
||||||
|
### Deprecated Projects - DO NOT USE
|
||||||
|
- `EnvelopeGenerator.ReceiverUI` - Pure WASM (migrated to Server)
|
||||||
|
- `EnvelopeGenerator.Web` - Razor Pages (replaced by Server)
|
||||||
|
- **VB.NET projects** (`Form`, `Service`, `BBTests`) - Legacy, read-only for reference
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Run Active Host
|
||||||
|
```powershell
|
||||||
|
# Terminal - Blazor + Web API host
|
||||||
|
cd EnvelopeGenerator.Server\EnvelopeGenerator.Server
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical:** `EnvelopeGenerator.Server` is the active runtime host for both UI and API controllers.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
```powershell
|
||||||
|
dotnet build EnvelopeGenerator.sln
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Boundaries
|
||||||
|
|
||||||
|
```
|
||||||
|
EnvelopeGenerator.Domain/ # Entities (Envelope, Receiver, Document, etc.)
|
||||||
|
EnvelopeGenerator.Application/ # MediatR CQRS (Commands, Queries, Handlers)
|
||||||
|
EnvelopeGenerator.Infrastructure/ # EF Core, SQL executors, repositories
|
||||||
|
EnvelopeGenerator.Server/
|
||||||
|
EnvelopeGenerator.Server/ # Server-side Blazor + API controllers
|
||||||
|
├─ Components/Pages/ # @rendermode InteractiveServer
|
||||||
|
├─ Controllers/ # Active Web API endpoints
|
||||||
|
EnvelopeGenerator.Server.Client/ # Client-side WASM components
|
||||||
|
├─ Pages/ # @rendermode InteractiveWebAssembly
|
||||||
|
├─ Services/ # HTTP API clients
|
||||||
|
├─ Models/ # DTOs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Route Structure (Critical)
|
||||||
|
|
||||||
|
| Route | File Location | Render Mode | Purpose |
|
||||||
|
|-------|--------------|-------------|---------|
|
||||||
|
| `/` | `Server.Client/Pages/Index.razor` | WASM | Landing page |
|
||||||
|
| `/sender/login` | `Server.Client/Pages/LoginSenderPage.razor` | WASM | Sender auth |
|
||||||
|
| `/sender` | `Server/Components/Pages/EnvelopeSenderPage.razor` | **Server** | Sender dashboard |
|
||||||
|
| `/sender/editor` | `Server/Components/Pages/EnvelopeSenderEditorPage.razor` | **Server** | Envelope editor + PDF viewer |
|
||||||
|
| `/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.
|
||||||
|
|
||||||
|
## Coordinate System (CRITICAL)
|
||||||
|
|
||||||
|
**Database stores INCHES** (GdPicture14 native). Origin: top-left, Y-axis down.
|
||||||
|
|
||||||
|
### Conversions
|
||||||
|
```csharp
|
||||||
|
// Database (INCHES) → PDF Points
|
||||||
|
float points = inches * 72;
|
||||||
|
|
||||||
|
// Database (INCHES) → DevExpress DX
|
||||||
|
float dx = inches * 100;
|
||||||
|
|
||||||
|
// PDF.js Pixels → Database (INCHES)
|
||||||
|
float inches = (pixelX / canvasWidth) * pageWidthInches;
|
||||||
|
```
|
||||||
|
|
||||||
|
**A4 Page:** 8.27" wide × 11.69" tall = 595pt × 842pt
|
||||||
|
|
||||||
|
**Signature Field Size:** 1.77" × 1.96" (FIXED, do not change)
|
||||||
|
|
||||||
|
**Evidence:** See `COPILOT_CONTEXT.md` lines 158-185, `EnvelopeGenerator.Form/frmFieldEditor.vb`
|
||||||
|
|
||||||
|
## API Architecture Quirks
|
||||||
|
|
||||||
|
### Monolithic Endpoint (Avoid for UI)
|
||||||
|
`POST /api/EnvelopeReceiver` - Creates envelope+document+receivers+fields atomically.
|
||||||
|
- **Use case:** External API consumers
|
||||||
|
- **Not suitable for:** Step-by-step UI workflow (no draft support, no partial updates)
|
||||||
|
|
||||||
|
### Sender Workflow Endpoint
|
||||||
|
```
|
||||||
|
POST /api/Envelope # Create/update/save/send envelope workflow payload
|
||||||
|
DELETE /api/Envelope/{id} # Delete with reason
|
||||||
|
POST /api/Envelope/{id}/document # Upload PDF
|
||||||
|
POST /api/Envelope/{id}/receivers # Add receiver
|
||||||
|
POST /api/Envelope/{id}/signature-fields # Place signature field
|
||||||
|
```
|
||||||
|
|
||||||
|
See `FORM_APPLICATION_CONTEXT.md` for detailed workflow requirements.
|
||||||
|
|
||||||
|
## Status Color Coding
|
||||||
|
|
||||||
|
Form app uses DevExpress `CustomDrawCell`. EnvelopeGenerator.Server needs CSS:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.envelope-row.status-partly-signed { background-color: #81C784; } /* GREEN_300 */
|
||||||
|
.envelope-row.status-queued,
|
||||||
|
.envelope-row.status-sent { background-color: #FFB74D; } /* ORANGE_300 */
|
||||||
|
.envelope-row.status-completed { background-color: #81C784; }
|
||||||
|
.envelope-row.status-deleted,
|
||||||
|
.envelope-row.status-rejected { background-color: #E57373; } /* RED_300 */
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### YARP Proxy (`EnvelopeGenerator.Server/yarp.json`)
|
||||||
|
Routes `/api/*`, `/swagger/*`, `/openapi/*`, `/scalar/*` → `https://localhost:8088`
|
||||||
|
|
||||||
|
### PDF.js Settings (`EnvelopeGenerator.Server/wwwroot/appsettings.json`)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"PdfViewerOptions": {
|
||||||
|
"ThumbnailBaseScale": 0.75,
|
||||||
|
"ThumbnailEnableHiDPI": true,
|
||||||
|
"MainCanvasEnableHiDPI": true,
|
||||||
|
"ZoomStepPercentage": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Host Config (`EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`)
|
||||||
|
- `ConnectionStrings:Default` - SQL Server DB
|
||||||
|
- `AllowedOrigins` - CORS (includes `http://localhost:5131`, `http://localhost:7192`)
|
||||||
|
- `Cache:SignatureCacheExpiration` - Signature persistence timeout
|
||||||
|
- `PSPDFKitLicenseKey` - **DEPRECATED** (use PDF.js instead)
|
||||||
|
|
||||||
|
## Migration Status
|
||||||
|
|
||||||
|
### Complete ✅
|
||||||
|
- Receiver login/authentication
|
||||||
|
- PDF viewing with PDF.js (HiDPI, zoom, thumbnails)
|
||||||
|
- Signature capture (draw/type/image)
|
||||||
|
- Signature caching (Redis/SQL)
|
||||||
|
- Sender login
|
||||||
|
|
||||||
|
### Missing (High Priority) ❌
|
||||||
|
- Sender dashboard (`/sender`) - Empty stub
|
||||||
|
- Envelope editor (`/sender/envelope/{id}`)
|
||||||
|
- Signature field placement tool (PDF.js + draggable overlays)
|
||||||
|
- Granular API endpoints (draft, receivers, fields)
|
||||||
|
- Master-detail grids for receivers/history
|
||||||
|
|
||||||
|
## Common Mistakes (DO NOT REPEAT)
|
||||||
|
|
||||||
|
| Mistake | Why Wrong |
|
||||||
|
|---------|-----------|
|
||||||
|
| Using iText7 in receiver pages | GPL license issue. Use PDF.js overlays. |
|
||||||
|
| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. |
|
||||||
|
| `@rendermode InteractiveWebAssembly` on PDF viewers | DevExpress DxPdfViewer requires server-side rendering. |
|
||||||
|
| Hardcoded quality in PDF.js | Use `appsettings.json` `PdfViewerOptions`. |
|
||||||
|
| Coordinates in points/pixels for DB | Database uses INCHES. Convert before save. |
|
||||||
|
| `BottomMarginBand` for signatures | Repeats on every page. Use `DetailBand`. |
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
**No automated tests exist yet.**
|
||||||
|
|
||||||
|
Manual testing workflow:
|
||||||
|
1. Start EnvelopeGenerator.Server (`dotnet run` in `EnvelopeGenerator.Server\EnvelopeGenerator.Server`)
|
||||||
|
2. Navigate to `https://localhost:5131` (or check console output for port)
|
||||||
|
3. Test sender login at `/sender/login`
|
||||||
|
4. Test receiver flow at `/envelope/login/{envelopeKey}`
|
||||||
|
|
||||||
|
## Technical Debt TODOs
|
||||||
|
|
||||||
|
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor` to reduce complexity.
|
||||||
|
- Move sender dashboard business/status logic into dedicated services and extension methods (for example effective status resolution, tab classification, receiver signed/rejected detection, and grid-layout filter sanitation).
|
||||||
|
- Keep `EnvelopeSenderPage.razor` focused on UI composition/state orchestration; avoid embedding heavy domain logic directly in the Razor component.
|
||||||
|
- Apply a modular page architecture across Server pages (starting with `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`): split large pages into smaller subcomponents/partial modules, eliminate duplicated code paths, and centralize shared UI/domain logic for maintainability.
|
||||||
|
- Perform a senior-level page performance review across sender/receiver flows before further UX changes: verify route transitions, component mount/unmount behavior, loading-state correctness, duplicate API calls, and perceived latency (first meaningful paint and return-navigation responsiveness). Treat loading logic defects as architecture/performance issues first, not styling problems.
|
||||||
|
- Audit `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/` for unnecessary request/payload remapping and remove redundant mappings where direct request DTO usage is safe; review client HTTP methods end-to-end for similar avoidable transformation layers.
|
||||||
|
- Centralize dependency injection registrations into dedicated extension methods/modules (especially server + client service registrations in `Program.cs`) to reduce startup composition sprawl and improve maintainability.
|
||||||
|
- Refactor `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/PdfRenderController.cs` + matching client service contract to remove JSON/base64 `byte[]` transport for PDF rendering; prefer binary streaming (`application/octet-stream` or multipart), avoid `data:` URL bloat where possible, and validate end-to-end memory/latency behavior with large PDFs.
|
||||||
|
- Establish a DevExpress toast-based notification architecture using `DxToastProvider` + `IToastNotificationService` (tab/page-scoped provider strategy, standardized `ToastOptions` factory, and severity-to-style mapping) so sender/receiver flows show consistent, non-duplicated, actionable feedback.
|
||||||
|
- Eliminate manual property-by-property request→domain mapping drift (example: `CreateEnvelopeCommandHandler`) by introducing a standardized mapping policy (AutoMapper/profile conventions or reflection/source-generator based mapper) and enforcing it across envelope workflows; this needs to be solved in the shared company Core library level so all services get the same guardrails by default.
|
||||||
|
- P1 security workstream: remove dynamic SQL string composition/interpolation from active save/create/update paths (`string.Format`, `$"...{input}..."`, `ToSqlParam`) and migrate to parameterized queries only; treat this as a potential SQL injection vulnerability (stability + security risk), execute repo-wide audit, and add CI guardrails to block reintroduction.
|
||||||
|
- Alternative UX design (Server component): in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeSenderPage.razor`, enforce deterministic row double-click behavior as exactly one action (preview OR edit), and keep row action buttons/tooltips aligned with the same rule to eliminate ambiguous navigation.
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
**SQL Server** (DD_ECM)
|
||||||
|
- Connection string in `EnvelopeGenerator.Server/EnvelopeGenerator.Server/appsettings.json`
|
||||||
|
- EF Core migrations NOT used (manual SQL scripts)
|
||||||
|
- Stored procedures: `PRSIG_*` prefix
|
||||||
|
|
||||||
|
**Key Tables:**
|
||||||
|
- `TBSIG_ENVELOPE` - Envelope metadata
|
||||||
|
- `TBSIG_ENVELOPE_RECEIVER` - Receiver assignments
|
||||||
|
- `TBSIG_DOC_RECEIVER_ELEMENT` - Signature fields (X, Y in INCHES)
|
||||||
|
- `TBSIG_RECEIVER` - Receiver registry
|
||||||
|
- `TBSIG_DOCUMENT` - PDF binary data
|
||||||
|
- `TBSIG_ENVELOPE_HISTORY` - Audit trail
|
||||||
|
|
||||||
|
## DevExpress
|
||||||
|
|
||||||
|
**License:** Commercial (v25.2.3)
|
||||||
|
**Components Used:**
|
||||||
|
- `DxGrid` - Master-detail grids
|
||||||
|
- `DxPdfViewer` - Server-side PDF rendering
|
||||||
|
- `DxPopup` - Modal dialogs
|
||||||
|
- `DxToolbar` - Action bars
|
||||||
|
- `DxFormLayout` - Forms
|
||||||
|
|
||||||
|
**Theme:** Blazing Berry (default)
|
||||||
|
|
||||||
|
## JavaScript Interop
|
||||||
|
|
||||||
|
**PDF Viewer:** `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 Pad:** `wwwroot/js/receiver-signature.js`
|
||||||
|
```javascript
|
||||||
|
window.receiverSignature = {
|
||||||
|
initializeDrawPad(canvasId, dotNetRef),
|
||||||
|
getSignatureDataUrl(canvasId),
|
||||||
|
clearPad(canvasId)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Envelope Support
|
||||||
|
|
||||||
|
Receivers can login to **multiple envelopes simultaneously** via per-envelope cookies:
|
||||||
|
```
|
||||||
|
AuthTokenSignFLOWReceiver.{envelopeKey}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each envelope maintains independent authentication state.
|
||||||
|
|
||||||
|
## External Dependencies
|
||||||
|
|
||||||
|
**CDN:**
|
||||||
|
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
||||||
|
|
||||||
|
**NuGet (Server.Client):**
|
||||||
|
- `DevExpress.Blazor.*` 25.2.3
|
||||||
|
- `SkiaSharp.*` 3.119.1 (WASM rendering)
|
||||||
|
|
||||||
|
**External Services:**
|
||||||
|
- LDAP/AD authentication (optional)
|
||||||
|
- GTX Messaging (SMS 2FA)
|
||||||
|
- Email dispatcher (signFlow)
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
None required. All config in `appsettings.json`.
|
||||||
|
|
||||||
|
**Local dev ports:**
|
||||||
|
- App host (UI + API): `https://localhost:5131` or `http://localhost:7192`
|
||||||
351
COPILOT_CONTEXT.md
Normal file
351
COPILOT_CONTEXT.md
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
# EnvelopeGenerator — Current Workspace Context
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Digital document signing system for senders and receivers.
|
||||||
|
|
||||||
|
- 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`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Active Application Structure
|
||||||
|
|
||||||
|
### Main Host
|
||||||
|
**Primary active application:** `EnvelopeGenerator.Server`
|
||||||
|
|
||||||
|
`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
|
||||||
|
|
||||||
|
### Client Project
|
||||||
|
**Client UI project:** `EnvelopeGenerator.Server.Client`
|
||||||
|
|
||||||
|
This project contains:
|
||||||
|
- WebAssembly-rendered pages
|
||||||
|
- client-side services
|
||||||
|
- client models and options
|
||||||
|
- sender and receiver login flows
|
||||||
|
|
||||||
|
### 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` — deprecated project (no longer part of the active solution/runtime path)
|
||||||
|
|
||||||
|
### Legacy / Do Not Touch
|
||||||
|
- `EnvelopeGenerator.Service`
|
||||||
|
- `EnvelopeGenerator.Form`
|
||||||
|
- `EnvelopeGenerator.BBTests`
|
||||||
|
- `EnvelopeGenerator.CommonServices`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Hosting Model
|
||||||
|
|
||||||
|
`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
|
||||||
|
|
||||||
|
This means the active app is a **merged UI + API host**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reverse Proxy
|
||||||
|
|
||||||
|
**Config file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
|
||||||
|
|
||||||
|
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 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/` | `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 |
|
||||||
|
|
||||||
|
### Server Pages (`EnvelopeGenerator.Server`)
|
||||||
|
| Route | File | Render Mode | Purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/envelope/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` | InteractiveServer | Main receiver PDF viewer and signing page |
|
||||||
|
| `/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 |
|
||||||
|
| `/envelope/Embed` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_embed.razor` | InteractiveServer | Embedded browser PDF view test page |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current API Location
|
||||||
|
|
||||||
|
The active application exposes controllers from:
|
||||||
|
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers`
|
||||||
|
|
||||||
|
Current controller set includes:
|
||||||
|
- `AnnotationController`
|
||||||
|
- `AuthController`
|
||||||
|
- `CacheController`
|
||||||
|
- `ConfigController`
|
||||||
|
- `DocumentController`
|
||||||
|
- `EmailTemplateController`
|
||||||
|
- `EnvelopeController`
|
||||||
|
- `EnvelopeReceiverController`
|
||||||
|
- `EnvelopeTypeController`
|
||||||
|
- `HistoryController`
|
||||||
|
- `LocalizationController`
|
||||||
|
- `ReadOnlyController`
|
||||||
|
- `ReceiverController`
|
||||||
|
- `SignatureController`
|
||||||
|
- `TfaRegistrationController`
|
||||||
|
|
||||||
|
Do not assume API behavior lives in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication Model
|
||||||
|
|
||||||
|
### Sender
|
||||||
|
Client login page uses `EnvelopeGenerator.Server.Client/Services/AuthService.cs`.
|
||||||
|
|
||||||
|
Key sender endpoints:
|
||||||
|
- `POST /api/auth?cookie=true` — login
|
||||||
|
- `GET /api/auth/check` — current sender access check
|
||||||
|
- `POST /api/auth/logout` — logout
|
||||||
|
|
||||||
|
### Receiver
|
||||||
|
Receiver authentication is **per envelope**.
|
||||||
|
|
||||||
|
Key receiver endpoints used by client services:
|
||||||
|
- `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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Receiver Page Data Loading
|
||||||
|
|
||||||
|
Main server-side page data service:
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
|
||||||
|
|
||||||
|
This service loads directly via MediatR and distributed cache:
|
||||||
|
- document bytes
|
||||||
|
- receiver envelope data
|
||||||
|
- signature placeholders
|
||||||
|
- cached signature data
|
||||||
|
|
||||||
|
For signature placeholders, the service:
|
||||||
|
- reads document receiver elements
|
||||||
|
- filters them for the authenticated receiver
|
||||||
|
- converts coordinates to `UnitOfLength.Point` before UI use
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Receiver PDF Viewer
|
||||||
|
|
||||||
|
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
### JS Assets
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js`
|
||||||
|
|
||||||
|
### CSS
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css`
|
||||||
|
|
||||||
|
### PDF.js CDN
|
||||||
|
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
||||||
|
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Signature Workflow
|
||||||
|
|
||||||
|
Receiver signatures are handled as a **viewer overlay workflow**.
|
||||||
|
|
||||||
|
### Current behavior
|
||||||
|
1. Server-side authorization validates receiver access.
|
||||||
|
2. The page loads document bytes, receiver data, signature placeholders, and cached signature state.
|
||||||
|
3. If no cached signature exists, the signature popup opens automatically.
|
||||||
|
4. Receiver creates signature using one of three tabs:
|
||||||
|
- draw
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
### Important note
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Signature DTO
|
||||||
|
`EnvelopeGenerator.Server.Client/Models/SignatureCaptureDto.cs`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed record SignatureCaptureDto {
|
||||||
|
public required string DataUrl { get; init; }
|
||||||
|
public required string FullName { get; init; }
|
||||||
|
public string Position { get; init; } = "";
|
||||||
|
public required string Place { get; init; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Signature Cache
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
### Cache key format
|
||||||
|
Current server-side key prefix:
|
||||||
|
- `envelope-generator.receiver-ui.signature:{receiverSignature}`
|
||||||
|
|
||||||
|
This is different from an envelope-key-only cache convention.
|
||||||
|
|
||||||
|
### Config
|
||||||
|
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/CacheOptions.cs`
|
||||||
|
- section name: `Cache`
|
||||||
|
- option: `SignatureCacheExpiration`
|
||||||
|
|
||||||
|
### Related controller
|
||||||
|
A cache API controller also exists in:
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/CacheController.cs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sender Dashboard
|
||||||
|
|
||||||
|
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor`
|
||||||
|
|
||||||
|
Current behavior:
|
||||||
|
- 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`
|
||||||
|
|
||||||
|
The sender page is active, but create/edit/delete actions are still marked with TODO behavior in the UI page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Localization
|
||||||
|
|
||||||
|
Current server host localization setup in `Program.cs`:
|
||||||
|
- supported cultures: `de-DE`, `en-US`
|
||||||
|
- request localization middleware is enabled
|
||||||
|
- `QueryStringRequestCultureProvider` is added
|
||||||
|
- cookie-based localization services are registered via `AddCookieBasedLocalizer()`
|
||||||
|
|
||||||
|
Do not assume the old ReceiverUI-only `localStorage` culture approach is the current source of truth for the active host.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coordinate System
|
||||||
|
|
||||||
|
### Source data
|
||||||
|
Database signature coordinates are still based on:
|
||||||
|
- **unit:** inches
|
||||||
|
- **origin:** top-left
|
||||||
|
- **axes:** X right, Y down
|
||||||
|
|
||||||
|
### Relevant conversions
|
||||||
|
- 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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Services and Files
|
||||||
|
|
||||||
|
### Client services
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureCacheService.cs`
|
||||||
|
|
||||||
|
### Server services
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs`
|
||||||
|
- `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`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Working Rules for This Workspace
|
||||||
|
|
||||||
|
- Treat `EnvelopeGenerator.Server` as the active main application host.
|
||||||
|
- Treat `EnvelopeGenerator.Server.Client` as the active client UI project.
|
||||||
|
- Prefer current `Server` / `Server.Client` paths over old `WebUI` / `ReceiverUI` references.
|
||||||
|
- Do not use `EnvelopeGenerator.Web` or `EnvelopeGenerator.ReceiverUI` as the primary implementation target unless explicitly asked.
|
||||||
|
- 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`.
|
||||||
|
- For DevExpress PDF viewer issues, remember server-side services are registered in `EnvelopeGenerator.Server`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** 2026-06-29
|
||||||
@@ -1,946 +0,0 @@
|
|||||||
# EnvelopeGenerator — Copilot Context Notes (English)
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
A digital document signing system. Senders upload PDFs and place signature annotation fields via PSPDFKit (EnvelopeGenerator.Web). Receivers open the document in a Blazor WASM viewer, confirm each signature field via a checkbox overlay, draw/type/upload their signature, and export the stamped PDF.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Solution Structure
|
|
||||||
|
|
||||||
| Project | Target | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Receiver auth (cookie), annotation reading, PDF serving. |
|
|
||||||
| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | Blazor WebAssembly. Receiver UI. YARP proxies API calls. |
|
|
||||||
| `EnvelopeGenerator.Web` | net7/8/9 | Razor Pages. Sender UI + PSPDFKit annotation placement. |
|
|
||||||
| `EnvelopeGenerator.Application` | multi | MediatR CQRS handlers. |
|
|
||||||
| `EnvelopeGenerator.Domain` | multi | Domain models, constants, interfaces. |
|
|
||||||
| `EnvelopeGenerator.Infrastructure` | multi | EF Core repos, DB context. |
|
|
||||||
| `EnvelopeGenerator.PdfEditor` | multi | iText7 utilities. NOT used in ReceiverUI flow. |
|
|
||||||
| `EnvelopeGenerator.DependencyInjection` | multi | DI registration helpers. |
|
|
||||||
| VB.NET projects (Service/Form/BBTests) | net462 | Legacy. Do NOT touch. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Files
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|---|---|
|
|
||||||
| `ReceiverUI/Pages/EnvelopeViewer.razor` | **NEW** PDF.js-based viewer (`/envelope/{key}`). Replaces ReportViewer.razor. Simple read-only PDF viewing with zoom/navigation. |
|
|
||||||
| `ReceiverUI/Pages/ReportViewer.razor` | **LEGACY** DevExpress-based signing page (`/receiver/{key}`). Still used for signature workflow. Will be deprecated. |
|
|
||||||
| `ReceiverUI/wwwroot/js/pdf-viewer.js` | **NEW** PDF.js wrapper: rendering, zoom, pagination, mouse wheel control. |
|
|
||||||
| `ReceiverUI/wwwroot/js/receiver-signature.js` | JS: checkbox overlay, signature pad (draw/type/image). |
|
|
||||||
| `ReceiverUI/wwwroot/css/envelope-viewer.css` | **NEW** Styles for EnvelopeViewer.razor (external CSS, not inline). |
|
|
||||||
| `ReceiverUI/wwwroot/fake-data/annotations.json` | Dev-mode fake annotations (YARP proxy target). |
|
|
||||||
| `ReceiverUI/Models/AnnotationDto.cs` | Annotation position model. All properties non-nullable. |
|
|
||||||
| `ReceiverUI/Services/AnnotationService.cs` | Fetches `List<AnnotationDto>` from API or fake-data. |
|
|
||||||
| `ReceiverUI/Services/DocumentService.cs` | Fetches PDF bytes from API. |
|
|
||||||
| `ReceiverUI/Services/AuthService.cs` | Manages receiver session cookie. |
|
|
||||||
| `API/Controllers/AnnotationController.cs` | GET `api/Annotation/{key}` ? annotation list. |
|
|
||||||
| `API/Controllers/DocumentController.cs` | GET `api/Document/{key}` ? PDF bytes. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SignatureDto / AnnotationDto — Coordinate System
|
|
||||||
|
|
||||||
**Database Storage Format:** INCHES (GdPicture14 native unit)
|
|
||||||
**Origin:** Top-left corner of page
|
|
||||||
**Axes:** X increases rightward, Y increases downward
|
|
||||||
|
|
||||||
### Source Evidence (VB.NET Legacy Code)
|
|
||||||
|
|
||||||
```vb
|
|
||||||
' From: EnvelopeGenerator.Form/frmFieldEditor.vb
|
|
||||||
' GdPicture14.Annotations.AnnotationStickyNote
|
|
||||||
|
|
||||||
'Breite und Höhe in Inches (4,5*5cm)
|
|
||||||
Private Const SIGNATURE_WIDTH As Single = 1.77 ' 1.77 inches = 4.5cm
|
|
||||||
Private Const SIGNATURE_HEIGHT As Single = 1.96 ' 1.96 inches = 5cm
|
|
||||||
|
|
||||||
Sub LoadAnnotation(pElement As Signature, ...)
|
|
||||||
oAnnotation.Left = CSng(pElement.X) ' Direct assignment ? INCHES
|
|
||||||
oAnnotation.Top = CSng(pElement.Y)
|
|
||||||
oAnnotation.Width = CSng(pElement.Width)
|
|
||||||
oAnnotation.Height = CSng(pElement.Height)
|
|
||||||
End Sub
|
|
||||||
```
|
|
||||||
|
|
||||||
### Conversion Formulas
|
|
||||||
|
|
||||||
```
|
|
||||||
Inches ? DevExpress (DX): x_DX = x_inches * 100.0
|
|
||||||
y_DX = y_inches * 100.0
|
|
||||||
|
|
||||||
Inches ? PDF Points: x_pt = x_inches * 72.0
|
|
||||||
y_pt = x_inches * 72.0
|
|
||||||
|
|
||||||
Inches ? PDF.js Canvas: normalize to [0,1], then scale to pixels
|
|
||||||
x_norm = x_inches / pageWidth_inches
|
|
||||||
y_norm = y_inches / pageHeight_inches
|
|
||||||
x_px = x_norm * canvasWidth * scale * dpr
|
|
||||||
y_px = y_norm * canvasHeight * scale * dpr
|
|
||||||
```
|
|
||||||
|
|
||||||
### Unit Comparison Table
|
|
||||||
|
|
||||||
| System | Unit | Origin | Conversion from INCHES |
|
|
||||||
|--------|------|--------|------------------------|
|
|
||||||
| **GdPicture14** (Source) | **Inches** | Top-left | Database format (no conversion) |
|
|
||||||
| DevExpress (LEGACY) | 1/100 inch (DX) | Top-left | `x_DX = x_inches * 100` |
|
|
||||||
| PDF.js (NEW) | Pixels | Top-left | `normalize ? scale` |
|
|
||||||
| PDF Points (iText7) | Points (1/72") | **Bottom-left** | `x_pt = x_inches * 72` + Y-flip |
|
|
||||||
| PSPDFKit (Web) | Points (1/72") | Top-left | `x_pt = x_inches * 72` |
|
|
||||||
|
|
||||||
**A4 Page Dimensions:**
|
|
||||||
- Width: 8.27 inches = 595 points = 827 DX units
|
|
||||||
- Height: 11.69 inches = 842 points = 1169 DX units
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## EnvelopeViewer (NEW) — PDF.js Read-Only Viewer
|
|
||||||
|
|
||||||
**Route:** `/envelope/{EnvelopeKey}`
|
|
||||||
**Purpose:** Modern, high-performance PDF viewing without signing functionality.
|
|
||||||
**Technology:** PDF.js 3.11.174 + custom JavaScript + configurable quality settings
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
|
|
||||||
**Blazor Component (`EnvelopeViewer.razor`):**
|
|
||||||
- Fetches PDF via `DocumentService.GetDocumentAsync(EnvelopeKey)`
|
|
||||||
- Converts to base64 data URL: `data:application/pdf;base64,{base64}`
|
|
||||||
- Initializes PDF.js viewer via JSInterop with `DotNetObjectReference` for callbacks
|
|
||||||
- Quality settings loaded from `appsettings.json` via `IOptions<PdfViewerOptions>`
|
|
||||||
- Thumbnail sidebar with resizable splitter (150px-400px, localStorage persistence)
|
|
||||||
- CSS externalized to `envelope-viewer.css`
|
|
||||||
|
|
||||||
**JavaScript (`pdf-viewer.js`):**
|
|
||||||
```javascript
|
|
||||||
window.pdfViewer = {
|
|
||||||
qualityOptions, // Configurable from appsettings.json
|
|
||||||
setQualityOptions(options), // Dynamic quality update
|
|
||||||
initialize(canvasId, pdfDataUrl, dotNetRef),
|
|
||||||
renderPage(num),
|
|
||||||
renderThumbnail(pageNum, canvasId),
|
|
||||||
attachWheelEvent(), // Ctrl+Wheel zoom (configurable step)
|
|
||||||
zoomIn(), zoomOut(), // Configurable step percentage
|
|
||||||
dispose()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Options (`PdfViewerOptions.cs`):**
|
|
||||||
```csharp
|
|
||||||
public class PdfViewerOptions {
|
|
||||||
public double ThumbnailBaseScale { get; set; } = 0.75; // 0.2-1.5
|
|
||||||
public bool ThumbnailEnableHiDPI { get; set; } = true;
|
|
||||||
public double ThumbnailMaxDPR { get; set; } = 2.0; // 1.0-3.0
|
|
||||||
public bool MainCanvasEnableHiDPI { get; set; } = true;
|
|
||||||
public double MainCanvasMaxDPR { get; set; } = 2.0;
|
|
||||||
public bool EnableSmoothZoom { get; set; } = true;
|
|
||||||
public int ZoomTransitionDuration { get; set; } = 150; // ms
|
|
||||||
public double RenderingOpacity { get; set; } = 0.85; // 0.0-1.0
|
|
||||||
public int ThumbnailRenderDelay { get; set; } = 50; // ms
|
|
||||||
public int ZoomStepPercentage { get; set; } = 5; // 1-50%
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration (appsettings.json)
|
|
||||||
|
|
||||||
**Location:** `EnvelopeGenerator.ReceiverUI/wwwroot/appsettings.json`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"PdfViewer": {
|
|
||||||
"ThumbnailBaseScale": 0.75,
|
|
||||||
"ThumbnailEnableHiDPI": true,
|
|
||||||
"ThumbnailMaxDPR": 2.0,
|
|
||||||
"MainCanvasEnableHiDPI": true,
|
|
||||||
"MainCanvasMaxDPR": 2.0,
|
|
||||||
"EnableSmoothZoom": true,
|
|
||||||
"ZoomTransitionDuration": 150,
|
|
||||||
"RenderingOpacity": 0.85,
|
|
||||||
"ThumbnailRenderDelay": 50,
|
|
||||||
"ZoomStepPercentage": 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Usage:** Edit file ? F5 (browser refresh) ? new settings applied
|
|
||||||
|
|
||||||
**Presets:**
|
|
||||||
- **High Quality**: `ThumbnailBaseScale: 1.0, MaxDPR: 3.0` (powerful devices)
|
|
||||||
- **Balanced**: Default values (recommended)
|
|
||||||
- **Performance**: `ThumbnailBaseScale: 0.5, EnableHiDPI: false` (mobile/low-end)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
1. **HiDPI/Retina Support** ? 4x quality on Retina displays
|
|
||||||
2. **Configurable Quality** ? All parameters in appsettings.json
|
|
||||||
3. **Unlimited Zoom** ? 50%-300%, configurable step (default 5%)
|
|
||||||
4. **Global Ctrl+Wheel Zoom** ? Works anywhere on page
|
|
||||||
5. **Thumbnail Sidebar** ? Resizable (150-400px), high-quality rendering
|
|
||||||
6. **Smooth Transitions** ? Configurable fade effect
|
|
||||||
7. **Responsive Design** ? Desktop/mobile adaptive layout
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
1. **Unlimited Zoom:**
|
|
||||||
- Canvas size not restricted by `max-width`
|
|
||||||
- Frame stays fixed, scroll bars appear automatically
|
|
||||||
- `text-align: center` for small sizes, full scroll for zoomed views
|
|
||||||
|
|
||||||
2. **Global Mouse Wheel Zoom:**
|
|
||||||
- Event listener on `document.body` (works anywhere on page)
|
|
||||||
- `Ctrl + Mouse Wheel` triggers `zoomIn()`/`zoomOut()`
|
|
||||||
- Calls `dotNetReference.invokeMethodAsync('OnZoomChanged', scale)` to update Blazor UI
|
|
||||||
- `{ passive: false }` to enable `preventDefault()`
|
|
||||||
|
|
||||||
3. **Render Task Cancellation:**
|
|
||||||
- Stores `currentRenderTask` to cancel previous render if new one starts
|
|
||||||
- Catches `RenderingCancelledException` to avoid console errors
|
|
||||||
- Queue system (`pageNumPending`) for rapid page changes
|
|
||||||
|
|
||||||
4. **Thumbnail Sidebar:**
|
|
||||||
- Left panel with page previews (sequential rendering, 50ms delay)
|
|
||||||
- Click to navigate to specific page
|
|
||||||
- Active page highlighted with gradient border
|
|
||||||
- No header/title (maximizes thumbnail space)
|
|
||||||
- Toggle button in toolbar to show/hide
|
|
||||||
|
|
||||||
5. **Resizable Splitter:**
|
|
||||||
- 4px draggable divider between thumbnails and canvas
|
|
||||||
- Min width: 150px, Max width: 400px
|
|
||||||
- Visual feedback: gradient on hover/active
|
|
||||||
- User preference saved to localStorage (`envelopeViewer_thumbnailWidth`)
|
|
||||||
- Global mouse events (works anywhere during drag)
|
|
||||||
- `col-resize` cursor (?) for intuitive UX
|
|
||||||
|
|
||||||
6. **Flex Layout:**
|
|
||||||
- Thumbnails and canvas in same container (`.pdf-frame`)
|
|
||||||
- `display: flex, flex-direction: row, align-items: stretch`
|
|
||||||
- Perfect vertical alignment (same top/bottom position)
|
|
||||||
- Responsive: column layout on mobile (<768px)
|
|
||||||
|
|
||||||
7. **Responsive Design:**
|
|
||||||
- Desktop: 90% width, 1200px max
|
|
||||||
- Mobile: 95% width, adjusted heights, thumbnails collapse to top
|
|
||||||
- Adaptive padding and font sizes
|
|
||||||
|
|
||||||
### Initialization Flow
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
OnInitializedAsync():
|
|
||||||
1. Fetch PDF bytes from DocumentService
|
|
||||||
2. Convert to base64 data URL
|
|
||||||
3. Set _isLoading = false
|
|
||||||
|
|
||||||
OnAfterRenderAsync(firstRender):
|
|
||||||
1. Load saved thumbnail width from localStorage
|
|
||||||
2. Create DotNetObjectReference
|
|
||||||
3. Send PdfViewerOptions to JavaScript
|
|
||||||
4. Initialize PDF.js viewer
|
|
||||||
5. Attach splitter resize listeners
|
|
||||||
6. Render thumbnails sequentially (configurable delay)
|
|
||||||
```
|
|
||||||
|
|
||||||
**User Interactions:**
|
|
||||||
- Zoom: Buttons/Ctrl+Wheel/Slider ? configurable step percentage
|
|
||||||
- Pages: Buttons/Input/Thumbnails ? navigate
|
|
||||||
- Sidebar: Toggle button ? show/hide thumbnails
|
|
||||||
- Splitter: Drag ? resize sidebar (150-400px)
|
|
||||||
|
|
||||||
**Cleanup:**
|
|
||||||
```csharp
|
|
||||||
DisposeAsync():
|
|
||||||
- Dispose PDF.js viewer
|
|
||||||
- Detach event listeners
|
|
||||||
- Dispose DotNetObjectReference
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Key Differences from ReportViewer
|
|
||||||
|
|
||||||
| Feature | EnvelopeViewer (NEW) | ReportViewer (LEGACY) |
|
|
||||||
|---------|----------------------|------------------------|
|
|
||||||
| Technology | PDF.js + Canvas | DevExpress XtraReports |
|
|
||||||
| Route | `/envelope/{key}` | `/receiver/{key}` |
|
|
||||||
| Purpose | Read-only viewing | Signature workflow |
|
|
||||||
| Dependencies | PDF.js CDN | DevExpress NuGet packages |
|
|
||||||
| Zoom | Unlimited, smooth | Report viewer default |
|
|
||||||
| Mouse Wheel | Custom Ctrl+Wheel | Browser default |
|
|
||||||
| File Size | Minimal (JS + CSS) | Heavy (DX libs) |
|
|
||||||
| Maintenance | Simple, standard web | Complex, vendor-specific |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ReceiverUI Signing Flow (ReportViewer.razor — LEGACY)
|
|
||||||
|
|
||||||
### On Load (`OnInitializedAsync`)
|
|
||||||
1. `AuthService.CheckEnvelopeAccessAsync` ? redirect to login if unauthorized
|
|
||||||
2. `AnnotationService.GetAnnotationsAsync` ? fills `_annotations`
|
|
||||||
3. `DocumentService.GetDocumentAsync` ? fills `_basePdfBytes` (real mode)
|
|
||||||
4. `BuildFreshBaseReport()` ? `XtraReport` for `DxReportViewer`
|
|
||||||
|
|
||||||
### Signature Popup
|
|
||||||
- Tabs: Draw / Text / Image
|
|
||||||
- Fields: full name (required), position (optional), place (required)
|
|
||||||
- Saved to `_capturedSignature` record
|
|
||||||
- If annotations exist ? popup closes ? JS checkbox overlays installed
|
|
||||||
|
|
||||||
### JS Checkbox Overlay (`receiver-signature.js`)
|
|
||||||
- `receiverSignature.installAnnotationCheckboxes(annotations, checkedIds, dotNetRef)`
|
|
||||||
- One `.annot-sig-cb-wrapper` div per annotation, absolutely positioned over viewer scroll container
|
|
||||||
- Position: `left = pageRect.left + ann.x * scaleX`, `top = pageRect.top + ann.y * scaleY`
|
|
||||||
- `scaleX = pagePixelWidth / 827`, `scaleY = pagePixelHeight / 1169`
|
|
||||||
- Click ? `dotNetRef.invokeMethodAsync('OnAnnotationToggled', id, checked)`
|
|
||||||
|
|
||||||
### Apply Signatures ("Unterschriften anwenden" — `SubmitSignaturesAsync`)
|
|
||||||
|
|
||||||
**Real PDF mode (`_basePdfBytes` is set):**
|
|
||||||
- Calls `StampSignaturesOnPdf` using **iText7** directly on PDF bytes
|
|
||||||
- Coordinate conversion: `xPt = ann.X * (72/100)`, `yPt = pageHeight - ann.Y * (72/100) - sigHeight` (Y flip: DX top-down ? PDF bottom-up)
|
|
||||||
- Returns stamped bytes ? loaded into new `XtraReport` with `XRPdfContent`
|
|
||||||
- Viewer refreshed with `ViewerKey++`
|
|
||||||
|
|
||||||
**Dev/fake mode (`_basePdfBytes` is null):**
|
|
||||||
- Falls back to `AddSignatureAtAnnotation` (XtraReports DetailBand + BeforePrint counter)
|
|
||||||
- This is intentionally left as a dev fallback only
|
|
||||||
|
|
||||||
### Export
|
|
||||||
- `reportViewer.ExportToAsync(ExportFormat.Pdf)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## StampSignaturesOnPdf — iText7 Implementation
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Located in ReportViewer.razor @code section
|
|
||||||
// Called by SubmitSignaturesAsync when _basePdfBytes is available
|
|
||||||
|
|
||||||
static byte[] StampSignaturesOnPdf(
|
|
||||||
byte[] sourcePdfBytes, byte[] signatureImageBytes,
|
|
||||||
string signerFullName, string signerPosition, string signaturePlace,
|
|
||||||
IReadOnlyList<AnnotationDto> annotations)
|
|
||||||
{
|
|
||||||
// Opens PDF with PdfReader/PdfWriter
|
|
||||||
// For each annotation:
|
|
||||||
// pageNum = ann.Page (clamped to totalPages)
|
|
||||||
// xPt = ann.X * (72f/100f)
|
|
||||||
// imgBottomY = pageHeight - ann.Y * (72f/100f) - sigHeightPt ? Y-axis flip
|
|
||||||
// PdfCanvas.AddImageFittedIntoRectangle(imageData, rect, false)
|
|
||||||
// Separator line at bottom of image
|
|
||||||
// Canvas text block below separator (font: Helvetica 7pt, color: RGB(73,80,87))
|
|
||||||
// Returns stamped byte[]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## BuildFreshBaseReport()
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// Real PDF mode:
|
|
||||||
var report = new XtraReport();
|
|
||||||
var detail = new DetailBand();
|
|
||||||
report.Bands.Add(detail);
|
|
||||||
detail.Controls.Add(new XRPdfContent { Source = _basePdfBytes, GenerateOwnPages = true });
|
|
||||||
return report;
|
|
||||||
|
|
||||||
// Dev/fake mode: returns pre-built report from ReportStorage
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## NuGet Packages in ReceiverUI
|
|
||||||
|
|
||||||
| Package | Version | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `DevExpress.Blazor.Reporting.Viewer` | 25.2.3 | DxReportViewer (LEGACY, used in ReportViewer.razor) |
|
|
||||||
| `DevExpress.Blazor.PdfViewer` | 25.2.3 | PDF viewer (not used in EnvelopeViewer) |
|
|
||||||
| `DevExpress.Drawing.Skia` | 25.2.3 | Drawing backend |
|
|
||||||
| `itext` | 8.0.5 | PDF stamping (iText7) |
|
|
||||||
| `SkiaSharp.*` | 3.119.1 | WASM native rendering |
|
|
||||||
|
|
||||||
**External CDN (EnvelopeViewer):**
|
|
||||||
- PDF.js 3.11.174 (via `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`)
|
|
||||||
- PDF.js Worker (`pdf.worker.min.js`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Mistakes History — Do NOT Repeat
|
|
||||||
|
|
||||||
| Mistake | Why Wrong | Session |
|
|
||||||
|---|---|---|
|
|
||||||
| `BottomMarginBand` for per-page signatures | Repeats on every page; Y offset wrong | 4 |
|
|
||||||
| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand; 35 pages ? 140 pages | 8 |
|
|
||||||
| `e.Graph?.PrintingSystem` in BeforePrint | `Graph` not on `CancelEventArgs` | 5 |
|
|
||||||
| `ctrl.Report?.PrintingSystem` | `PrintingSystem` not on `XtraReportBase` in WASM | — |
|
|
||||||
| Adding stamp endpoint to `DocumentController` | Not needed; stamping is done client-side in ReceiverUI | — |
|
|
||||||
| iText7 via API (server-side) | Unnecessary; iText7 runs fine in WASM directly | 10 |
|
|
||||||
| **PDF.js: Hardcoded quality values** | **Use appsettings.json for configurability** | **11** |
|
|
||||||
| **PDF.js: Hardcoded zoom step (1%)** | **Too granular; use configurable percentage** | **11** |
|
|
||||||
| **Toolbar: Complex left/center/right layout** | **User wants simple horizontal layout; failed multiple times to implement** | **11** |
|
|
||||||
| **Zoom label: Badge style (gradient/border/padding)** | **Over-designed; user prefers simple text label** | **11** |
|
|
||||||
| **Attempting to "improve" simple designs** | **User requests simplicity; AI keeps over-engineering** | **11** |
|
|
||||||
| **Ignoring explicit "revert" instructions** | **User said revert toolbar, AI tried to fix CSS instead of reverting HTML structure** | **11** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DevExpress Article (2023-08-28) — Why It Does NOT Apply
|
|
||||||
|
|
||||||
The article describes **X.509 cryptographic digital signatures** via `PdfDocumentSigner` + `Pkcs7Signer`.
|
|
||||||
Our use case is **visual/image stamping** at specific page coordinates — different problem, different API.
|
|
||||||
`XRPdfSignature` in the article requires pre-placed fields in the report designer, not runtime coordinates.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Layout Architecture (EnvelopeViewer)
|
|
||||||
|
|
||||||
### HTML Structure
|
|
||||||
```html
|
|
||||||
<div class="pdf-viewer-container">
|
|
||||||
<div class="pdf-toolbar">
|
|
||||||
<!-- Zoom, page navigation, thumbnail toggle -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pdf-frame">
|
|
||||||
@if (_showThumbnails) {
|
|
||||||
<div class="pdf-thumbnails" style="width: @(_thumbnailWidth)px">
|
|
||||||
<!-- Page previews -->
|
|
||||||
</div>
|
|
||||||
<div class="pdf-splitter" @onmousedown="OnSplitterMouseDown">
|
|
||||||
<!-- Resizable divider -->
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="pdf-canvas-wrapper">
|
|
||||||
<canvas id="pdf-canvas" class="pdf-canvas"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### CSS Flexbox Layout
|
|
||||||
```css
|
|
||||||
.pdf-frame {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row; /* Side-by-side */
|
|
||||||
align-items: stretch; /* Same height */
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-thumbnails {
|
|
||||||
flex-shrink: 0; /* Fixed width */
|
|
||||||
width: 260px; /* Dynamic via inline style */
|
|
||||||
border-right: none; /* Seamless join with splitter */
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-splitter {
|
|
||||||
width: 4px;
|
|
||||||
cursor: col-resize;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-canvas-wrapper {
|
|
||||||
flex: 1; /* Fill remaining space */
|
|
||||||
overflow: auto; /* Scrollable for zoom */
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resizable Splitter Workflow
|
|
||||||
```
|
|
||||||
1. User hovers splitter ? cursor: col-resize (?)
|
|
||||||
2. Mouse down ? OnSplitterMouseDown(e)
|
|
||||||
- _isResizing = true
|
|
||||||
- Store start position (clientX) and width
|
|
||||||
- Add 'resizing' class to body (prevent text selection)
|
|
||||||
- Call pdfViewer.startResize()
|
|
||||||
|
|
||||||
3. Mouse move (global) ? OnSplitterMouseMove(clientX)
|
|
||||||
- Calculate delta = clientX - startX
|
|
||||||
- newWidth = startWidth + delta
|
|
||||||
- Clamp to 150-400px range
|
|
||||||
- Update _thumbnailWidth
|
|
||||||
- StateHasChanged() for reactive UI
|
|
||||||
|
|
||||||
4. Mouse up (global) ? OnSplitterMouseUp()
|
|
||||||
- _isResizing = false
|
|
||||||
- Remove 'resizing' class
|
|
||||||
- Save to localStorage("envelopeViewer_thumbnailWidth")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Responsive Behavior
|
|
||||||
- **Desktop (>768px)**: Flex row, side-by-side
|
|
||||||
- **Mobile (?768px)**: Flex column, thumbnails on top
|
|
||||||
- **Thumbnail toggle**: Controlled by `@if (_showThumbnails)` in Razor markup
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Signature Buttons in EnvelopeViewer — Interactive Overlay System
|
|
||||||
|
|
||||||
**Purpose:** Render clickable "Unterschreiben" (Sign) buttons on PDF canvas at signature field positions fetched from database.
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
|
|
||||||
**Blazor Component (`EnvelopeViewer.razor`):**
|
|
||||||
```csharp
|
|
||||||
IReadOnlyList<SignatureDto> _signatures = [];
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
|
||||||
var signatures = await SignatureService.GetAsync(EnvelopeKey);
|
|
||||||
_signatures = signatures.Convert(UnitOfLength.Point); // INCHES ? POINTS
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task RenderSignatureButtonsAsync() {
|
|
||||||
await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons",
|
|
||||||
_signatures, _currentPage, _dotNetRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
[JSInvokable]
|
|
||||||
public void OnSignatureButtonClick(int signatureId) {
|
|
||||||
Console.WriteLine($"Signature #{signatureId} signed");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**JavaScript (`pdf-viewer.js`):**
|
|
||||||
```javascript
|
|
||||||
renderSignatureButtons(signatures, currentPageNum, dotNetRef) {
|
|
||||||
this.clearSignatureButtons(); // Remove old buttons
|
|
||||||
|
|
||||||
const pageSignatures = signatures.filter(sig => sig.page === currentPageNum);
|
|
||||||
const signatureLayer = document.getElementById('pdf-signature-layer');
|
|
||||||
|
|
||||||
pageSignatures.forEach(sig => {
|
|
||||||
// Convert POINTS to display pixels
|
|
||||||
const xPx = sig.x * this.scale; // sig.x already in PDF POINTS
|
|
||||||
const yPx = sig.y * this.scale;
|
|
||||||
|
|
||||||
const button = document.createElement('button');
|
|
||||||
button.className = 'signature-button';
|
|
||||||
button.style.left = `${xPx}px`;
|
|
||||||
button.style.top = `${yPx}px`;
|
|
||||||
button.style.width = '150px';
|
|
||||||
button.style.height = '60px';
|
|
||||||
|
|
||||||
// German text + pen icon
|
|
||||||
button.innerHTML = `
|
|
||||||
<div>Unterschreiben</div>
|
|
||||||
<svg>...</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
this.dotNetReference.invokeMethodAsync('OnSignatureButtonClick', sig.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
signatureLayer.appendChild(button);
|
|
||||||
this.signatureButtons.push(button);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
clearSignatureButtons() {
|
|
||||||
this.signatureButtons.forEach(btn => btn.parentNode?.removeChild(btn));
|
|
||||||
this.signatureButtons = [];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**HTML Structure:**
|
|
||||||
```html
|
|
||||||
<div class="pdf-page-container">
|
|
||||||
<canvas id="pdf-canvas"></canvas>
|
|
||||||
<div id="pdf-text-layer"></div>
|
|
||||||
<div id="pdf-signature-layer"></div> <!-- NEW -->
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
**CSS (`envelope-viewer.css`):**
|
|
||||||
```css
|
|
||||||
.pdf-signature-layer {
|
|
||||||
position: absolute;
|
|
||||||
left: 0; top: 0; right: 0; bottom: 0;
|
|
||||||
overflow: visible;
|
|
||||||
pointer-events: none; /* Pass clicks through to canvas */
|
|
||||||
z-index: 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-button {
|
|
||||||
pointer-events: auto; /* Re-enable for buttons */
|
|
||||||
position: absolute;
|
|
||||||
width: 150px; height: 60px;
|
|
||||||
background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
/* Hover: scale(1.05), shadow, darker gradient */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rendering Triggers
|
|
||||||
|
|
||||||
1. **Initial Load:** After PDF renders (`OnAfterRenderAsync`)
|
|
||||||
2. **Page Change:** `NextPage()`, `PreviousPage()`, `GoToPageFromThumbnail()`
|
|
||||||
3. **Zoom Change:** `OnZoomChanged()` (re-calculates pixel positions)
|
|
||||||
|
|
||||||
### Coordinate Conversion Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
Database (INCHES)
|
|
||||||
? SignatureService.GetAsync()
|
|
||||||
SignatureDto.X/Y (INCHES)
|
|
||||||
? .Convert(UnitOfLength.Point)
|
|
||||||
SignatureDto.X/Y (PDF POINTS, × 72)
|
|
||||||
? JavaScript
|
|
||||||
Display Pixels (× this.scale)
|
|
||||||
? CSS
|
|
||||||
button.style.left/top
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example Calculation:**
|
|
||||||
- Database: `X = 1.5 inches, Y = 2.0 inches`
|
|
||||||
- After conversion: `X = 108 points, Y = 144 points` (× 72)
|
|
||||||
- At scale 1.5: `X = 162px, Y = 216px`
|
|
||||||
- Button positioned at `left: 162px, top: 216px`
|
|
||||||
|
|
||||||
### Button Design
|
|
||||||
|
|
||||||
**Visual Spec:**
|
|
||||||
- **Size:** 150px × 60px
|
|
||||||
- **Background:** Purple gradient `#4F46E5` ? `#4338CA` (hover: darker)
|
|
||||||
- **Text:** "Unterschreiben" (18px, bold, white)
|
|
||||||
- **Icon:** Pen SVG (24px white)
|
|
||||||
- **Effects:**
|
|
||||||
- Hover: `scale(1.05)` + shadow `0 4px 12px rgba(79, 70, 229, 0.4)`
|
|
||||||
- Active: `scale(0.98)`
|
|
||||||
- Focus: `2px solid #7e22ce` outline
|
|
||||||
|
|
||||||
**Accessibility:**
|
|
||||||
- `tabindex="0"` for keyboard navigation
|
|
||||||
- Focus outline for keyboard users
|
|
||||||
- Click handler with semantic button element
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Signature Workflow in EnvelopeViewer — NEW Implementation (Session 13-14)
|
|
||||||
|
|
||||||
**IMPORTANT: iText7 NOT USED in EnvelopeViewer**
|
|
||||||
- **Reason:** GPL license incompatibility (requires source code sharing)
|
|
||||||
- **Alternative:** Client-side signature overlay system (HTML + Canvas API)
|
|
||||||
- **Export:** Signatures are visual overlays only, NOT stamped on PDF bytes
|
|
||||||
- **Future:** Consider PSPDFKit or commercial PDF library for actual PDF stamping
|
|
||||||
|
|
||||||
### Signature Data Structure
|
|
||||||
|
|
||||||
**Captured Signature (`SignatureCaptureDto`):**
|
|
||||||
```csharp
|
|
||||||
// Model: EnvelopeGenerator.ReceiverUI/Models/SignatureCaptureDto.cs
|
|
||||||
public sealed record SignatureCaptureDto
|
|
||||||
{
|
|
||||||
public required string DataUrl { get; init; } // base64 PNG: "data:image/png;base64,iVBORw0KG..."
|
|
||||||
public required string FullName { get; init; } // Required: "Max Mustermann"
|
|
||||||
public string Position { get; init; } = string.Empty; // Optional: "Geschäftsführer"
|
|
||||||
public required string Place { get; init; } // Required: "Berlin"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage in components:
|
|
||||||
SignatureCaptureDto? _capturedSignature;
|
|
||||||
|
|
||||||
// Initialization with object initializer (required properties):
|
|
||||||
_capturedSignature = new SignatureCaptureDto
|
|
||||||
{
|
|
||||||
DataUrl = signatureDataUrl,
|
|
||||||
FullName = _signerFullName.Trim(),
|
|
||||||
Position = _signerPosition.Trim(),
|
|
||||||
Place = _signaturePlace.Trim()
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Applied Signature (HTML Overlay):**
|
|
||||||
```html
|
|
||||||
<div class="applied-signature" data-signature-id="42" style="left: 162px; top: 216px;">
|
|
||||||
<img src="data:image/png;base64,..." /> <!-- Signature image (max 70px height) -->
|
|
||||||
<div style="border-top: 1px solid #495057;"></div> <!-- Separator line -->
|
|
||||||
<div style="font-size: 9px; color: #495057;">
|
|
||||||
<strong>Max Mustermann</strong> <!-- Name (bold, #212529) -->
|
|
||||||
<br>Geschäftsführer <!-- Position (optional) -->
|
|
||||||
<br>Berlin, 26.01.2025 <!-- Place, Date (dd.MM.yyyy) -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Complete Workflow (Session 14 Update)
|
|
||||||
|
|
||||||
**Step 1: Page Load & Automatic Popup**
|
|
||||||
```csharp
|
|
||||||
protected override async Task OnInitializedAsync() {
|
|
||||||
// ... load PDF and signatures ...
|
|
||||||
|
|
||||||
// Open signature popup automatically
|
|
||||||
_activeSignatureTab = SignatureTabDraw;
|
|
||||||
_signaturePopupVisible = true;
|
|
||||||
_popupValidationMessage = null;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Opens automatically on page load (no manual trigger needed)
|
|
||||||
- Cannot be closed manually (no X button, ESC disabled, no outside-click)
|
|
||||||
- User MUST create signature before viewing PDF
|
|
||||||
|
|
||||||
**Step 2: Signature Creation Popup (DxPopup)**
|
|
||||||
|
|
||||||
**Tabs:**
|
|
||||||
1. **Zeichnen (Draw):** Canvas-based signature pad (`receiver-signature.js`)
|
|
||||||
- Touch-friendly: `touch-action: none`
|
|
||||||
- Line width: 2.5px, black (`#111`)
|
|
||||||
- Canvas: 560×180px, rounded corners, shadow
|
|
||||||
|
|
||||||
2. **Text:** Type signature with font selection
|
|
||||||
- Fonts: Brush Script, Segoe Script, Lucida Handwriting, Comic Sans, Cursive
|
|
||||||
- Real-time preview on canvas
|
|
||||||
|
|
||||||
3. **Bild (Image):** Upload PNG/JPG/WebP
|
|
||||||
- File input with preview
|
|
||||||
- Auto-resize to fit canvas
|
|
||||||
|
|
||||||
**Required Fields:**
|
|
||||||
- ? **Vor- und Nachname** (Full Name) — Red asterisk `*`
|
|
||||||
- ? **Ort** (Place) — Red asterisk `*`
|
|
||||||
- ? **Position** — Optional, gray "(optional)" label
|
|
||||||
|
|
||||||
**Validation:**
|
|
||||||
```csharp
|
|
||||||
async Task SaveSignatureAsync() {
|
|
||||||
if (string.IsNullOrWhiteSpace(_signerFullName)) {
|
|
||||||
_popupValidationMessage = "Bitte geben Sie Vor- und Nachname ein.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (string.IsNullOrWhiteSpace(_signaturePlace)) {
|
|
||||||
_popupValidationMessage = "Bitte geben Sie den Ort ein.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var signatureDataUrl = await GetActiveSignatureDataUrlAsync();
|
|
||||||
if (string.IsNullOrWhiteSpace(signatureDataUrl)) {
|
|
||||||
_popupValidationMessage = "Die Unterschrift ist erforderlich.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to session state
|
|
||||||
_capturedSignature = new(signatureDataUrl, _signerFullName.Trim(),
|
|
||||||
_signerPosition.Trim(), _signaturePlace.Trim());
|
|
||||||
_signaturePopupVisible = false;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Design (Modern & Clean):**
|
|
||||||
- **Tabs:** Purple active state (`#4F46E5`), 3px bottom border
|
|
||||||
- **Inputs:** 2px solid border (`#e9ecef`), 6px border-radius, consistent padding
|
|
||||||
- **Canvas:** Light shadow (`0 1px 3px rgba(0,0,0,0.1)`), rounded corners
|
|
||||||
- **Buttons:**
|
|
||||||
- **Erneuern:** Outline secondary with refresh icon
|
|
||||||
- **Speichern:** Purple gradient + checkmark icon + shadow
|
|
||||||
- **Error:** Red left border (`4px solid #dc3545`), light red background (`#fee`)
|
|
||||||
|
|
||||||
**Step 3: PDF Viewing with Signature Buttons**
|
|
||||||
|
|
||||||
After popup closes:
|
|
||||||
```csharp
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
|
||||||
// ... PDF initialization ...
|
|
||||||
|
|
||||||
await RenderSignatureButtonsAsync(); // Render "Unterschreiben" buttons
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Buttons appear at signature field positions:**
|
|
||||||
- Purple gradient background
|
|
||||||
- "Unterschreiben" text + pen icon
|
|
||||||
- Hover: scale(1.05) + darker color
|
|
||||||
- Positioned using PDF POINTS ? display pixels conversion
|
|
||||||
|
|
||||||
**Step 4: Apply Signature (Click "Unterschreiben")**
|
|
||||||
|
|
||||||
**C# Handler:**
|
|
||||||
```csharp
|
|
||||||
[JSInvokable]
|
|
||||||
public async Task OnSignatureButtonClick(int signatureId) {
|
|
||||||
if (_capturedSignature == null) return;
|
|
||||||
|
|
||||||
await JSRuntime.InvokeVoidAsync("pdfViewer.applySignature",
|
|
||||||
signatureId,
|
|
||||||
_capturedSignature.DataUrl,
|
|
||||||
_capturedSignature.FullName,
|
|
||||||
_capturedSignature.Position,
|
|
||||||
_capturedSignature.Place);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**JavaScript Implementation:**
|
|
||||||
```javascript
|
|
||||||
async applySignature(signatureId, signatureDataUrl, fullName, position, place) {
|
|
||||||
// 1. Find and remove button
|
|
||||||
const button = this.signatureButtons.find(btn =>
|
|
||||||
btn.getAttribute('data-signature-id') == signatureId);
|
|
||||||
button.parentNode.removeChild(button);
|
|
||||||
|
|
||||||
// 2. Create signature container (German standard format)
|
|
||||||
const signatureContainer = document.createElement('div');
|
|
||||||
signatureContainer.className = 'applied-signature';
|
|
||||||
signatureContainer.style.position = 'absolute';
|
|
||||||
signatureContainer.style.left = button.style.left; // Same position as button
|
|
||||||
signatureContainer.style.top = button.style.top;
|
|
||||||
signatureContainer.style.width = '230px';
|
|
||||||
signatureContainer.style.backgroundColor = '#f8f9fa';
|
|
||||||
signatureContainer.style.border = '1px solid #dee2e6';
|
|
||||||
signatureContainer.style.borderRadius = '6px';
|
|
||||||
signatureContainer.style.padding = '12px';
|
|
||||||
|
|
||||||
// 3. Add signature image
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = signatureDataUrl;
|
|
||||||
img.style.width = '100%';
|
|
||||||
img.style.maxHeight = '70px';
|
|
||||||
img.style.objectFit = 'contain';
|
|
||||||
|
|
||||||
// 4. Add separator line (German standard)
|
|
||||||
const separator = document.createElement('div');
|
|
||||||
separator.style.borderTop = '1px solid #495057';
|
|
||||||
separator.style.marginTop = '6px';
|
|
||||||
separator.style.marginBottom = '8px';
|
|
||||||
|
|
||||||
// 5. Add text information
|
|
||||||
const today = new Date();
|
|
||||||
const dateStr = today.toLocaleDateString('de-DE', {
|
|
||||||
day: '2-digit', month: '2-digit', year: 'numeric'
|
|
||||||
}); // "26.01.2025"
|
|
||||||
|
|
||||||
const infoHtml = [
|
|
||||||
`<strong>${this.escapeHtml(fullName)}</strong>`,
|
|
||||||
position ? this.escapeHtml(position) : null,
|
|
||||||
`${this.escapeHtml(place)}, ${dateStr}`
|
|
||||||
].filter(x => x).join('<br>');
|
|
||||||
|
|
||||||
const info = document.createElement('div');
|
|
||||||
info.style.fontSize = '9px';
|
|
||||||
info.style.color = '#495057';
|
|
||||||
info.innerHTML = infoHtml;
|
|
||||||
|
|
||||||
// 6. Assemble and add to layer
|
|
||||||
signatureContainer.appendChild(img);
|
|
||||||
signatureContainer.appendChild(separator);
|
|
||||||
signatureContainer.appendChild(info);
|
|
||||||
|
|
||||||
document.getElementById('pdf-signature-layer').appendChild(signatureContainer);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**German Standard Layout:**
|
|
||||||
```
|
|
||||||
???????????????????????????????
|
|
||||||
? [Signature Image] ? ? Base64 PNG, max 70px height
|
|
||||||
? ?
|
|
||||||
??????????????????????????????? ? 1px separator (#495057)
|
|
||||||
? ?
|
|
||||||
? Max Mustermann (Bold) ? ? Name (font-weight: 600, #212529)
|
|
||||||
? Geschäftsführer ? ? Position (optional, normal weight)
|
|
||||||
? Berlin, 26.01.2025 ? ? Place, Date (dd.MM.yyyy)
|
|
||||||
? ?
|
|
||||||
???????????????????????????????
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 5: Persistence & Re-rendering**
|
|
||||||
|
|
||||||
- **Zoom/Page Change:** Applied signatures re-render automatically
|
|
||||||
- **Session State:** `_capturedSignature` stored in Blazor component
|
|
||||||
- **Limitation:** Lost on page refresh (no server-side storage)
|
|
||||||
- **Future:** Export to PDF with actual byte stamping (requires non-GPL library)
|
|
||||||
|
|
||||||
**Security:**
|
|
||||||
```javascript
|
|
||||||
escapeHtml(text) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text; // Browser auto-escapes
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Protects against XSS attacks from malicious input in Name/Position/Place fields.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Popup Design Specification
|
|
||||||
|
|
||||||
**DxPopup Properties:**
|
|
||||||
```razor
|
|
||||||
<DxPopup @bind-Visible="_signaturePopupVisible"
|
|
||||||
HeaderText="Unterschrift erstellen"
|
|
||||||
Width="620px"
|
|
||||||
MaxWidth="95vw"
|
|
||||||
ShowFooter="true" <!-- REQUIRED for buttons to appear -->
|
|
||||||
CloseOnOutsideClick="false"
|
|
||||||
ShowCloseButton="false" <!-- No X button -->
|
|
||||||
CloseOnEscape="false"> <!-- ESC disabled -->
|
|
||||||
```
|
|
||||||
|
|
||||||
**Tab Design:**
|
|
||||||
- **Active Tab:** `border-bottom: 3px solid #4F46E5; color: #4F46E5; font-weight: 600;`
|
|
||||||
- **Inactive Tab:** `color: #6c757d;`
|
|
||||||
- **Tab Bar:** `border-bottom: 2px solid #e9ecef;`
|
|
||||||
|
|
||||||
**Input Styling:**
|
|
||||||
```css
|
|
||||||
input, select {
|
|
||||||
border: 2px solid #e9ecef;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.625rem;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Canvas Styling:**
|
|
||||||
```css
|
|
||||||
canvas {
|
|
||||||
border: 2px solid #e9ecef;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: white;
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Button Styling:**
|
|
||||||
```css
|
|
||||||
/* Erneuern (Renew) */
|
|
||||||
.btn-outline-secondary {
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.625rem 1.25rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Speichern (Save) */
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, #4F46E5 0%, #4338CA 100%);
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.625rem 2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
box-shadow: 0 2px 4px rgba(79, 70, 229, 0.3);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Future Enhancement Required:**
|
|
||||||
- Replace iText7 with commercial PDF library (e.g., PSPDFKit, Syncfusion)
|
|
||||||
- Or use server-side stamping with Azure PDF Services
|
|
||||||
- Or accept GPL license and open-source the stamping code
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
using EnvelopeGenerator.API.Extensions;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
|
||||||
using EnvelopeGenerator.Application.Documents.Queries;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using MediatR;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.API.Controllers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
public class SignatureController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly IMediator _mediator;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of <see cref="SignatureController"/>.
|
|
||||||
/// </summary>
|
|
||||||
public SignatureController(IMediator mediator)
|
|
||||||
{
|
|
||||||
_mediator = mediator;
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: update to use signature query
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="envelopeKey"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
|
||||||
[HttpGet("{envelopeKey}")]
|
|
||||||
public async Task<IActionResult> GetAnnotsOfReceiver(string envelopeKey, CancellationToken cancel)
|
|
||||||
{
|
|
||||||
int envelopeId = User.GetEnvelopeIdOfReceiver();
|
|
||||||
|
|
||||||
int receiverId = User.GetReceiverIdOfReceiver();
|
|
||||||
|
|
||||||
var doc = await _mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
|
|
||||||
|
|
||||||
if (doc.Elements is not IEnumerable<SignatureDto> docSignatures)
|
|
||||||
return NotFound("Document is empty.");
|
|
||||||
|
|
||||||
var rcvSignatures = docSignatures.Where(s => s.ReceiverId == receiverId).ToList();
|
|
||||||
|
|
||||||
if (rcvSignatures is null)
|
|
||||||
return NotFound("No signatures found for the current receiver.");
|
|
||||||
else
|
|
||||||
return Ok(rcvSignatures);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.API;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides custom claim types for envelope-related information.
|
|
||||||
/// </summary>
|
|
||||||
public static class EnvelopeClaimTypes
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Claim type for the title of an envelope.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly string Title = $"Envelope{nameof(Title)}";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Claim type for the ID of an envelope.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly string Id = $"Envelope{nameof(Id)}";
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
||||||
<PackageId>EnvelopeGenerator.GeneratorAPI</PackageId>
|
|
||||||
<Title></Title>
|
|
||||||
<Authors>Digital Data GmbH</Authors>
|
|
||||||
<Company>Digital Data GmbH</Company>
|
|
||||||
<Product>EnvelopeGenerator.GeneratorAPI</Product>
|
|
||||||
<Version>1.4.0</Version>
|
|
||||||
<FileVersion>1.4.0</FileVersion>
|
|
||||||
<AssemblyVersion>1.4.0</AssemblyVersion>
|
|
||||||
<PackageOutputPath>Copyright © 2025 Digital Data GmbH. All rights reserved.</PackageOutputPath>
|
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Remove="ClientApp\**" />
|
|
||||||
<Content Remove="ClientApp\**" />
|
|
||||||
<EmbeddedResource Remove="ClientApp\**" />
|
|
||||||
<None Remove="ClientApp\**" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="yarp.json" CopyToOutputDirectory="PreserveNewest" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="AspNetCore.Scalar" Version="1.1.8" />
|
|
||||||
<PackageReference Include="DigitalData.Auth.Claims" Version="1.0.3" />
|
|
||||||
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
|
|
||||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
|
||||||
<PackageReference Include="itext" Version="8.0.5" />
|
|
||||||
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" Condition="'$(TargetFramework)' == 'net9.0'" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.6" Condition="'$(TargetFramework)' == 'net9.0'" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.17" Condition="'$(TargetFramework)' == 'net8.0'" />
|
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
|
||||||
<PackageReference Include="NLog" Version="5.2.5" />
|
|
||||||
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.0" />
|
|
||||||
<PackageReference Include="Scalar.AspNetCore" Version="2.2.1" />
|
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
|
|
||||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher.Abstraction" Version="3.2.0" />
|
|
||||||
<PackageReference Include="Yarp.ReverseProxy" Version="2.1.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
|
||||||
<PackageReference Include="System.DirectoryServices" Version="7.0.1" />
|
|
||||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="7.0.1" />
|
|
||||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="7.0.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
|
||||||
<PackageReference Include="System.DirectoryServices" Version="8.0.0" />
|
|
||||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="8.0.1" />
|
|
||||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="8.0.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
|
||||||
<PackageReference Include="System.DirectoryServices" Version="9.0.4" />
|
|
||||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="9.0.4" />
|
|
||||||
<PackageReference Include="System.DirectoryServices.Protocols" Version="9.0.4" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="wwwroot\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.Domain\EnvelopeGenerator.Domain.csproj" />
|
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.Infrastructure\EnvelopeGenerator.Infrastructure.csproj" />
|
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.PdfEditor\EnvelopeGenerator.PdfEditor.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.API.Extensions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides helper methods for working with envelope-specific authentication claims.
|
|
||||||
/// </summary>
|
|
||||||
public static class ReceiverClaimExtensions
|
|
||||||
{
|
|
||||||
private static readonly string[] EnvelopeIdClaimTypes = [EnvelopeClaimTypes.Id, "envelope_id", "EnvelopeId"];
|
|
||||||
private static readonly string[] ReceiverIdClaimTypes = ["receiver_id", "ReceiverId"];
|
|
||||||
private static readonly string[] EnvelopeUuidClaimTypes = [ClaimTypes.NameIdentifier, "envelope_uuid", "EnvelopeUuid"];
|
|
||||||
private static readonly string[] ReceiverSignatureClaimTypes = [ClaimTypes.Hash, "receiver_sig", "ReceiverSignature"];
|
|
||||||
|
|
||||||
private static string GetRequiredClaimOfReceiver(this ClaimsPrincipal user, string claimType)
|
|
||||||
{
|
|
||||||
var value = user.FindFirstValue(claimType);
|
|
||||||
if (value is not null)
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
var identity = user.Identity;
|
|
||||||
var principalName = identity?.Name ?? "(anonymous)";
|
|
||||||
var authType = identity?.AuthenticationType ?? "(none)";
|
|
||||||
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
|
|
||||||
var message = $"Required claim '{claimType}' is missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
|
|
||||||
throw new InvalidOperationException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetRequiredClaimOfReceiver(this ClaimsPrincipal user, params string[] claimTypes)
|
|
||||||
{
|
|
||||||
foreach (var claimType in claimTypes.Where(t => !string.IsNullOrWhiteSpace(t)).Distinct())
|
|
||||||
{
|
|
||||||
var value = user.FindFirstValue(claimType);
|
|
||||||
if (!string.IsNullOrWhiteSpace(value))
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
var identity = user.Identity;
|
|
||||||
var principalName = identity?.Name ?? "(anonymous)";
|
|
||||||
var authType = identity?.AuthenticationType ?? "(none)";
|
|
||||||
var availableClaims = string.Join(", ", user.Claims.Select(c => $"{c.Type}={c.Value}"));
|
|
||||||
var message = $"Required claim(s) '{string.Join("', '", claimTypes)}' are missing for user '{principalName}' (auth: {authType}). Available claims: [{availableClaims}].";
|
|
||||||
throw new InvalidOperationException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated envelope UUID from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetEnvelopeUuidOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(EnvelopeUuidClaimTypes);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated receiver signature from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetReceiverSignatureOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ReceiverSignatureClaimTypes);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated receiver display name from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetReceiverNameOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.Name);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated receiver email address from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetReceiverMailOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(ClaimTypes.Email);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated envelope title from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetEnvelopeTitleOfReceiver(this ClaimsPrincipal user) => user.GetRequiredClaimOfReceiver(EnvelopeClaimTypes.Title);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated envelope identifier from the claims.
|
|
||||||
/// </summary>
|
|
||||||
public static int GetEnvelopeIdOfReceiver(this ClaimsPrincipal user)
|
|
||||||
{
|
|
||||||
var envIdStr = user.GetRequiredClaimOfReceiver(EnvelopeIdClaimTypes);
|
|
||||||
if (!int.TryParse(envIdStr, out var envId))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Claim '{"envelope_id"}' is not a valid integer.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return envId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="user"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="InvalidOperationException"></exception>
|
|
||||||
public static int GetReceiverIdOfReceiver(this ClaimsPrincipal user)
|
|
||||||
{
|
|
||||||
var rcvIdStr = user.GetRequiredClaimOfReceiver(ReceiverIdClaimTypes);
|
|
||||||
if (!int.TryParse(rcvIdStr, out var rcvId))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Claim '{"receiver_id"}' is not a valid integer.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return rcvId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Signs in an envelope receiver using cookie authentication and attaches envelope claims.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">The current HTTP context.</param>
|
|
||||||
/// <param name="envelopeReceiver">Envelope receiver DTO to extract claims from.</param>
|
|
||||||
/// <param name="receiverRole">Role to attach to the authentication ticket.</param>
|
|
||||||
public static async Task SignInEnvelopeAsync(this HttpContext context, EnvelopeReceiverDto envelopeReceiver, string receiverRole)
|
|
||||||
{
|
|
||||||
var claims = new List<Claim>
|
|
||||||
{
|
|
||||||
new(ClaimTypes.NameIdentifier, envelopeReceiver.Envelope!.Uuid),
|
|
||||||
new(ClaimTypes.Hash, envelopeReceiver.Receiver!.Signature),
|
|
||||||
new(ClaimTypes.Name, envelopeReceiver.Name ?? string.Empty),
|
|
||||||
new(ClaimTypes.Email, envelopeReceiver.Receiver.EmailAddress),
|
|
||||||
new(EnvelopeClaimTypes.Title, envelopeReceiver.Envelope.Title),
|
|
||||||
new(EnvelopeClaimTypes.Id, envelopeReceiver.Envelope.Id.ToString()),
|
|
||||||
new(ClaimTypes.Role, receiverRole)
|
|
||||||
};
|
|
||||||
|
|
||||||
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
|
||||||
|
|
||||||
var authProperties = new AuthenticationProperties
|
|
||||||
{
|
|
||||||
AllowRefresh = false,
|
|
||||||
IsPersistent = false
|
|
||||||
};
|
|
||||||
|
|
||||||
await context.SignInAsync(
|
|
||||||
CookieAuthenticationDefaults.AuthenticationScheme,
|
|
||||||
new ClaimsPrincipal(claimsIdentity),
|
|
||||||
authProperties);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.API.Models;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public class ConnectionString
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public string Value { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace EnvelopeGenerator.API.Models;
|
|
||||||
|
|
||||||
public class CustomImages : Dictionary<string, Image>
|
|
||||||
{
|
|
||||||
public new Image this[string key] => TryGetValue(key, out var img) && img is not null ? img : new();
|
|
||||||
}
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
using DigitalData.Core.API;
|
|
||||||
using DigitalData.Core.Application;
|
|
||||||
using EnvelopeGenerator.Infrastructure;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
||||||
using Microsoft.AspNetCore.Localization;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using System.Globalization;
|
|
||||||
using Scalar.AspNetCore;
|
|
||||||
using Microsoft.OpenApi.Models;
|
|
||||||
using DigitalData.UserManager.DependencyInjection;
|
|
||||||
using EnvelopeGenerator.Application;
|
|
||||||
using DigitalData.Auth.Client;
|
|
||||||
using DigitalData.Core.Abstractions;
|
|
||||||
using EnvelopeGenerator.API.Models;
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using DigitalData.Core.Abstractions.Security.Extensions;
|
|
||||||
using EnvelopeGenerator.API.Middleware;
|
|
||||||
using NLog.Web;
|
|
||||||
using NLog;
|
|
||||||
using DigitalData.Auth.Claims;
|
|
||||||
|
|
||||||
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
|
||||||
logger.Info("Logging initialized!");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
|
||||||
|
|
||||||
builder.Configuration.AddJsonFile("yarp.json", optional: true, reloadOnChange: true);
|
|
||||||
|
|
||||||
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
|
|
||||||
|
|
||||||
if (!builder.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
builder.Logging.ClearProviders();
|
|
||||||
builder.Host.UseNLog();
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = builder.Configuration;
|
|
||||||
|
|
||||||
var deferredProvider = new DeferredServiceProvider();
|
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
|
||||||
builder.Services.AddHttpClient();
|
|
||||||
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
|
||||||
|
|
||||||
// CORS Policy
|
|
||||||
var allowedOrigins = config.GetSection("AllowedOrigins").Get<string[]>() ??
|
|
||||||
throw new InvalidOperationException("AllowedOrigins section is missing in the configuration.");
|
|
||||||
builder.Services.AddCors(options =>
|
|
||||||
{
|
|
||||||
options.AddPolicy("AllowSpecificOriginsPolicy", builder =>
|
|
||||||
{
|
|
||||||
builder.WithOrigins(allowedOrigins)
|
|
||||||
.SetIsOriginAllowedToAllowWildcardSubdomains()
|
|
||||||
.AllowAnyMethod()
|
|
||||||
.AllowAnyHeader()
|
|
||||||
.AllowCredentials();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Swagger
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
|
||||||
builder.Services.AddSwaggerGen(options =>
|
|
||||||
{
|
|
||||||
options.SwaggerDoc("v1", new OpenApiInfo
|
|
||||||
{
|
|
||||||
Version = "v1",
|
|
||||||
Title = "signFLOW Absender-API",
|
|
||||||
Description = "Eine API zur Verwaltung der Erstellung, des Versands und der Nachverfolgung von Umschlägen in der signFLOW-Anwendung.",
|
|
||||||
Contact = new OpenApiContact
|
|
||||||
{
|
|
||||||
Name = "Digital Data GmbH",
|
|
||||||
Url = new Uri("https://digitaldata.works/digitale-signatur#kontakt"),
|
|
||||||
Email = "info-flow@digitaldata.works"
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
|
||||||
{
|
|
||||||
Name = "Authorization",
|
|
||||||
Type = SecuritySchemeType.Http,
|
|
||||||
Scheme = "bearer",
|
|
||||||
BearerFormat = "JWT",
|
|
||||||
In = ParameterLocation.Header,
|
|
||||||
Description = "JWT-Autorisierungs-Header unter Verwendung des Bearer-Schemas.",
|
|
||||||
});
|
|
||||||
|
|
||||||
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
|
||||||
{
|
|
||||||
{
|
|
||||||
new OpenApiSecurityScheme
|
|
||||||
{
|
|
||||||
Reference = new OpenApiReference
|
|
||||||
{
|
|
||||||
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
|
|
||||||
Id = "Bearer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Array.Empty<string>()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml");
|
|
||||||
foreach (var xmlFile in xmlFiles)
|
|
||||||
{
|
|
||||||
options.IncludeXmlComments(xmlFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
options.DocumentFilter<EnvelopeGenerator.API.Documentation.AuthProxyDocumentFilter>();
|
|
||||||
});
|
|
||||||
#if NET9_0_OR_GREATER
|
|
||||||
builder.Services.AddOpenApi();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
//Add EF Core dbcontext
|
|
||||||
var useDbMigration = Environment.GetEnvironmentVariable("MIGRATION_TEST_MODE") == true.ToString() || config.GetValue<bool>("UseDbMigration");
|
|
||||||
var cnnStrName = useDbMigration ? "DbMigrationTest" : "Default";
|
|
||||||
var connStr = config.GetConnectionString(cnnStrName)
|
|
||||||
?? throw new InvalidOperationException($"Connection string '{cnnStrName}' is missing in the application configuration.");
|
|
||||||
|
|
||||||
builder.Services.Configure<ConnectionString>(cs => cs.Value = connStr);
|
|
||||||
|
|
||||||
builder.Services.AddDbContext<EGDbContext>(options => options.UseSqlServer(connStr));
|
|
||||||
|
|
||||||
builder.Services.AddAuthHubClient(config.GetSection("AuthClientParams"));
|
|
||||||
|
|
||||||
var authTokenKeys = config.GetOrDefault<AuthTokenKeys>();
|
|
||||||
|
|
||||||
// Scheme name used for per-envelope receiver JWT authentication.
|
|
||||||
const string EnvelopeReceiverScheme = "EnvelopeReceiverJwt";
|
|
||||||
|
|
||||||
builder.Services.AddAuthentication(options =>
|
|
||||||
{
|
|
||||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
||||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
|
||||||
})
|
|
||||||
.AddJwtBearer(opt =>
|
|
||||||
{
|
|
||||||
opt.TokenValidationParameters = new TokenValidationParameters
|
|
||||||
{
|
|
||||||
ValidateIssuerSigningKey = true,
|
|
||||||
IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) =>
|
|
||||||
{
|
|
||||||
var clientParams = deferredProvider.GetOptions<ClientParams>();
|
|
||||||
var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience);
|
|
||||||
return [publicKey.SecurityKey];
|
|
||||||
},
|
|
||||||
ValidateIssuer = true,
|
|
||||||
ValidIssuer = authTokenKeys.Issuer,
|
|
||||||
ValidateAudience = true,
|
|
||||||
ValidAudience = authTokenKeys.Audience,
|
|
||||||
};
|
|
||||||
|
|
||||||
opt.Events = new JwtBearerEvents
|
|
||||||
{
|
|
||||||
OnMessageReceived = context =>
|
|
||||||
{
|
|
||||||
// if there is no token read related cookie or query string
|
|
||||||
if (context.Token is null) // if there is no token
|
|
||||||
{
|
|
||||||
if (context.Request.Cookies.TryGetValue(authTokenKeys.Cookie, out var cookieToken) && cookieToken is not null)
|
|
||||||
context.Token = cookieToken;
|
|
||||||
else if (context.Request.Query.TryGetValue(authTokenKeys.QueryString, out var queryStrToken))
|
|
||||||
context.Token = queryStrToken;
|
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
// Per-envelope receiver scheme: reads the JWT from the cookie named
|
|
||||||
// AuthTokenSignFLOWReceiver.{envelope_key} where envelope_key is the
|
|
||||||
// last path segment of the request URL.
|
|
||||||
// This enables simultaneous authentication for multiple envelopes
|
|
||||||
// within the same browser session.
|
|
||||||
.AddJwtBearer(EnvelopeReceiverScheme, opt =>
|
|
||||||
{
|
|
||||||
opt.TokenValidationParameters = new TokenValidationParameters
|
|
||||||
{
|
|
||||||
ValidateIssuerSigningKey = true,
|
|
||||||
IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) =>
|
|
||||||
{
|
|
||||||
var clientParams = deferredProvider.GetOptions<ClientParams>();
|
|
||||||
var publicKey = clientParams!.PublicKeys.Get(authTokenKeys.Issuer, authTokenKeys.Audience);
|
|
||||||
return [publicKey.SecurityKey];
|
|
||||||
},
|
|
||||||
ValidateIssuer = true,
|
|
||||||
ValidIssuer = authTokenKeys.Issuer,
|
|
||||||
ValidateAudience = true,
|
|
||||||
ValidAudience = authTokenKeys.Audience,
|
|
||||||
};
|
|
||||||
|
|
||||||
opt.Events = new JwtBearerEvents
|
|
||||||
{
|
|
||||||
OnMessageReceived = context =>
|
|
||||||
{
|
|
||||||
var paths = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
// Derive the envelope key from the last route segment: /{envelope_key}
|
|
||||||
var envelopeKey = paths?.LastOrDefault();
|
|
||||||
|
|
||||||
if (envelopeKey is not null)
|
|
||||||
{
|
|
||||||
var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey);
|
|
||||||
if (context.Request.Cookies.TryGetValue(cookieName, out var cookieToken) && cookieToken is not null)
|
|
||||||
context.Token = cookieToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
},
|
|
||||||
OnTokenValidated = context =>
|
|
||||||
{
|
|
||||||
var paths = context.Request.Path.Value?.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
var envelopeKey = paths?.LastOrDefault();
|
|
||||||
|
|
||||||
var sub = context.Principal?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value
|
|
||||||
?? context.Principal?.FindFirst("sub")?.Value;
|
|
||||||
|
|
||||||
if (envelopeKey is null || sub != envelopeKey)
|
|
||||||
context.Fail("Envelope key in the path does not match the token subject.");
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Authentication
|
|
||||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
|
||||||
.AddCookie(options =>
|
|
||||||
{
|
|
||||||
options.Cookie.HttpOnly = true; // Makes the cookie inaccessible to client-side scripts for security
|
|
||||||
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // Ensures cookies are sent over HTTPS only
|
|
||||||
options.Cookie.SameSite = SameSiteMode.Strict; // Protects against CSRF attacks by restricting how cookies are sent with requests from external sites
|
|
||||||
options.LoginPath = "/api/auth/login";
|
|
||||||
options.LogoutPath = "/api/auth/logout";
|
|
||||||
options.SlidingExpiration = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddAuthorizationBuilder()
|
|
||||||
.AddPolicy(AuthPolicy.SenderOrReceiver, policy =>
|
|
||||||
policy.RequireRole(Role.Sender, Role.Receiver.Full))
|
|
||||||
.AddPolicy(AuthPolicy.Sender, policy =>
|
|
||||||
policy.RequireRole(Role.Sender))
|
|
||||||
// Per-envelope policy: uses the dedicated EnvelopeReceiverJwt scheme so it
|
|
||||||
// never conflicts with the default JwtBearer scheme.
|
|
||||||
.AddPolicy(AuthPolicy.Receiver, policy =>
|
|
||||||
policy
|
|
||||||
.AddAuthenticationSchemes(EnvelopeReceiverScheme)
|
|
||||||
.RequireAuthenticatedUser()
|
|
||||||
.RequireRole(Role.Receiver.Full, "receiver"))
|
|
||||||
.AddPolicy(AuthPolicy.ReceiverTFA, policy =>
|
|
||||||
policy.RequireRole(Role.Receiver.TFA));
|
|
||||||
|
|
||||||
// User manager
|
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
|
||||||
builder.Services.AddUserManager<EGDbContext>();
|
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
|
||||||
|
|
||||||
// LDAP
|
|
||||||
builder.ConfigureBySection<DirectorySearchOptions>();
|
|
||||||
builder.Services.AddDirectorySearchService(config.GetSection("DirectorySearchOptions"));
|
|
||||||
|
|
||||||
// Localizer
|
|
||||||
builder.Services.AddCookieBasedLocalizer();
|
|
||||||
|
|
||||||
// Envelope generator serives
|
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
|
||||||
builder.Services
|
|
||||||
.AddEnvelopeGeneratorInfrastructureServices(opt =>
|
|
||||||
{
|
|
||||||
opt.AddDbTriggerParams(config);
|
|
||||||
opt.AddDbContext((provider, options) =>
|
|
||||||
{
|
|
||||||
var logger = provider.GetRequiredService<ILogger<EGDbContext>>();
|
|
||||||
options.UseSqlServer(connStr)
|
|
||||||
.LogTo(log => logger.LogInformation("{log}", log), Microsoft.Extensions.Logging.LogLevel.Trace)
|
|
||||||
.EnableSensitiveDataLogging()
|
|
||||||
.EnableDetailedErrors();
|
|
||||||
});
|
|
||||||
opt.AddSQLExecutor(executor => executor.ConnectionString = connStr);
|
|
||||||
})
|
|
||||||
.AddEnvelopeGeneratorServices(config);
|
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
deferredProvider.Factory = () => app.Services;
|
|
||||||
|
|
||||||
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
|
||||||
|
|
||||||
#if NET9_0_OR_GREATER
|
|
||||||
app.MapOpenApi();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
|
||||||
if (app.Environment.IsDevelopment() || (app.IsDevOrDiP() && config.GetValue<bool>("UseSwagger")))
|
|
||||||
{
|
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI();
|
|
||||||
app.MapScalarApiReference();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set CORS policy
|
|
||||||
app.UseCors("AllowSpecificOriginsPolicy");
|
|
||||||
|
|
||||||
// Localizer
|
|
||||||
string[] supportedCultureNames = ["de-DE", "en-US"];
|
|
||||||
IList<CultureInfo> list = [.. supportedCultureNames.Select(cn => new CultureInfo(cn))];
|
|
||||||
var cultureInfo = list.FirstOrDefault() ?? throw new InvalidOperationException("There is no supported culture.");
|
|
||||||
var requestLocalizationOptions = new RequestLocalizationOptions
|
|
||||||
{
|
|
||||||
SupportedCultures = list,
|
|
||||||
SupportedUICultures = list
|
|
||||||
};
|
|
||||||
requestLocalizationOptions.RequestCultureProviders.Add(new QueryStringRequestCultureProvider());
|
|
||||||
app.UseRequestLocalization(requestLocalizationOptions);
|
|
||||||
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
|
||||||
|
|
||||||
app.UseDefaultFiles();
|
|
||||||
app.UseStaticFiles();
|
|
||||||
|
|
||||||
app.UseAuthentication();
|
|
||||||
app.UseAuthorization();
|
|
||||||
|
|
||||||
app.MapControllers();
|
|
||||||
|
|
||||||
// Catch-all YARP proxy — only forward requests that are not swagger/scalar/openapi paths.
|
|
||||||
app.MapReverseProxy();
|
|
||||||
|
|
||||||
app.Run();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.Error(ex, "Stopped program because of exception");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
{
|
|
||||||
"ReverseProxy": {
|
|
||||||
"Routes": {
|
|
||||||
"receiver-ui-receiver": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 100,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/receiver/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-login": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 100,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/login/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-sender": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 100,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/sender/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-envelope": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 100,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/envelope/{EnvelopeKey}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
},
|
|
||||||
"Transforms": [
|
|
||||||
{ "PathSet": "/index.html" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"receiver-ui-blazor-framework": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 50,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/_framework/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-blazor-content": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 50,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/_content/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-static-css": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 200,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/css/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
},
|
|
||||||
"Transforms": [
|
|
||||||
{
|
|
||||||
"ResponseHeader": "Cache-Control",
|
|
||||||
"Set": "no-cache, no-store, must-revalidate",
|
|
||||||
"When": "Always"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"receiver-ui-static-js": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 200,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/js/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
},
|
|
||||||
"Transforms": [
|
|
||||||
{
|
|
||||||
"ResponseHeader": "Cache-Control",
|
|
||||||
"Set": "no-cache, no-store, must-revalidate",
|
|
||||||
"When": "Always"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"receiver-ui-fake-data": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 200,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/fake-data/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-appsettings": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 50,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/appsettings.json",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-appsettings-dev": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 50,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/appsettings.Development.json",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-styles": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 50,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/EnvelopeGenerator.ReceiverUI.styles.css",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-fonts": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 200,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/fonts/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receiver-ui-images": {
|
|
||||||
"ClusterId": "receiver-ui",
|
|
||||||
"Order": 200,
|
|
||||||
"Match": {
|
|
||||||
"Path": "/images/{**catch-all}",
|
|
||||||
"Methods": [ "GET", "HEAD" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"auth-login": {
|
|
||||||
"ClusterId": "auth-hub",
|
|
||||||
"Match": {
|
|
||||||
"Path": "/api/auth",
|
|
||||||
"Methods": [ "POST" ]
|
|
||||||
},
|
|
||||||
"Transforms": [
|
|
||||||
{ "PathSet": "/api/auth/sign-flow" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"auth-envelope-receiver-login": {
|
|
||||||
"ClusterId": "auth-hub",
|
|
||||||
"Match": {
|
|
||||||
"Path": "/api/Auth/envelope-receiver/{key}",
|
|
||||||
"Methods": [ "POST" ]
|
|
||||||
},
|
|
||||||
"Transforms": [
|
|
||||||
{ "PathPattern": "/api/auth/envelope-receiver/{key}" },
|
|
||||||
{
|
|
||||||
"QueryValueParameter": "cookie",
|
|
||||||
"Set": "true"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Clusters": {
|
|
||||||
"receiver-ui": {
|
|
||||||
"Destinations": {
|
|
||||||
"primary": {
|
|
||||||
"Address": "https://localhost:52936"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"auth-hub": {
|
|
||||||
"Destinations": {
|
|
||||||
"primary": {
|
|
||||||
"Address": "http://172.24.12.39:9090"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using FluentValidation;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Behaviors;
|
||||||
|
|
||||||
|
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||||
|
where TRequest : notnull
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<IValidator<TRequest>> _validators;
|
||||||
|
|
||||||
|
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
|
||||||
|
{
|
||||||
|
_validators = validators;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TResponse> Handle(
|
||||||
|
TRequest request,
|
||||||
|
RequestHandlerDelegate<TResponse> next,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_validators.Any())
|
||||||
|
{
|
||||||
|
var context = new ValidationContext<TRequest>(request);
|
||||||
|
var failures = _validators
|
||||||
|
.Select(v => v.Validate(context))
|
||||||
|
.SelectMany(r => r.Errors)
|
||||||
|
.Where(f => f is not null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (failures.Count > 0)
|
||||||
|
{
|
||||||
|
var message = string.Join("; ", failures.Select(f => f.ErrorMessage).Distinct());
|
||||||
|
throw new BadRequestException(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await next();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,12 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
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 SignatureDto : ISignature
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unique identifier of the element.
|
/// Gets or sets the unique identifier of the element.
|
||||||
@@ -104,4 +102,24 @@ public class SignatureDto : ISignature
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SenderAppType SenderAppType { get; set; } = SenderAppType.LegacyFormApp;
|
public SenderAppType SenderAppType { get; set; } = SenderAppType.LegacyFormApp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string? FullName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string? Position { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string? Place { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public byte[]? Ink { get; set; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Data Transfer Object representing document metadata and elements without binary payload.
|
||||||
|
/// </summary>
|
||||||
|
public record DocumentDetailsDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the unique identifier of the document.
|
||||||
|
/// </summary>
|
||||||
|
public int Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the envelope ID to which the document belongs.
|
||||||
|
/// </summary>
|
||||||
|
public int EnvelopeId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the date and time when the document was added.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime AddedWhen { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<DocReceiverElementDto>? Elements { get; init; }
|
||||||
|
}
|
||||||
@@ -1,35 +1,27 @@
|
|||||||
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 record DocumentDto : DocumentDetailsDto
|
||||||
public class DocumentDto
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unique identifier of the document.
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; set; }
|
public DocumentDto() { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the envelope ID to which the document belongs.
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int EnvelopeId { get; set; }
|
/// <param name="byteData"></param>
|
||||||
|
/// <param name="details"></param>
|
||||||
/// <summary>
|
public DocumentDto(byte[] byteData, DocumentDetailsDto details) : base(details)
|
||||||
/// Gets or sets the date and time when the document was added.
|
{
|
||||||
/// </summary>
|
ByteData = byteData;
|
||||||
public DateTime AddedWhen { get; set; }
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the binary data of the document, if available.
|
/// Gets or sets the binary data of the document, if available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte[]? ByteData { get; set; }
|
public byte[]? ByteData { get; init; }
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the collection of elements associated with the document for receiver interactions, if any.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<SignatureDto>? Elements { get; set; }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,16 +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.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;
|
|
||||||
|
|
||||||
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>
|
||||||
@@ -87,6 +86,41 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? CertificationType { get; set; }
|
public int? CertificationType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? FinalEmailToCreator { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? FinalEmailToReceivers { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public bool? SendReminderEmails { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? FirstReminderDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? ReminderIntervalDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? ExpiresWhenDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public int? ExpiresWarningWhenDays { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -126,4 +160,14 @@ public record EnvelopeDto : IEnvelope
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<DocumentDto>? Documents { get; set; }
|
public IEnumerable<DocumentDto>? Documents { get; set; }
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
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>
|
||||||
@@ -61,6 +60,11 @@ public record EnvelopeReceiverDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? PrivateMessage { get; init; }
|
public string? PrivateMessage { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public string? AccessCode { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -75,4 +79,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>
|
||||||
|
|||||||
16
EnvelopeGenerator.Application/Common/Dto/PdfPageImageDto.cs
Normal file
16
EnvelopeGenerator.Application/Common/Dto/PdfPageImageDto.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
public sealed class PdfPageImageDto
|
||||||
|
{
|
||||||
|
public int PageNumber { get; init; }
|
||||||
|
|
||||||
|
public double WidthPt { get; init; }
|
||||||
|
|
||||||
|
public double HeightPt { get; init; }
|
||||||
|
|
||||||
|
public int WidthPx { get; init; }
|
||||||
|
|
||||||
|
public int HeightPx { get; init; }
|
||||||
|
|
||||||
|
public string ImageDataUrl { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
public sealed class PdfRenderRequestDto
|
||||||
|
{
|
||||||
|
public byte[]? PdfBytes { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Dynamic;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents PSPDFKit annotation data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Instant">Instant annotation data.</param>
|
||||||
|
/// <param name="Structured">Structured annotation data.</param>
|
||||||
|
[Obsolete("The PSPDFKit library is deprecated.")]
|
||||||
|
public record PsPdfKitAnnotation(ExpandoObject Instant, IEnumerable<AnnotationCreateDto> Structured);
|
||||||
@@ -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>
|
||||||
|
|||||||
65
EnvelopeGenerator.Application/Common/Dto/Signature.cs
Normal file
65
EnvelopeGenerator.Application/Common/Dto/Signature.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
/// <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
|
||||||
|
/// </remarks>
|
||||||
|
public sealed record Signature
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// TBDD_DOCUMENT_RECEIVER_ELEMENT.ID - identifies the specific signature field on the PDF page.
|
||||||
|
/// </summary>
|
||||||
|
public required int Id { get; init; }
|
||||||
|
|
||||||
|
/// <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>Example:</b> "Max Mustermann"
|
||||||
|
/// </summary>
|
||||||
|
public required string FullName { get; init; }
|
||||||
|
|
||||||
|
private readonly string? _position = null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Job title or position of the signer.
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Required:</b> No (optional field)
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Example:</b> "Geschäftsführer" or empty string
|
||||||
|
/// </summary>
|
||||||
|
public string? Position
|
||||||
|
{
|
||||||
|
get => _position;
|
||||||
|
init => _position = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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; }
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using EnvelopeGenerator.Domain.Interfaces.Auditing;
|
using EnvelopeGenerator.Domain.Interfaces.Auditing;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Extensions;
|
namespace EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
|
||||||
@@ -21,4 +22,21 @@ public static class AutoMapperAuditingExtensions
|
|||||||
public static IMappingExpression<TSource, TDestination> MapChangedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
|
public static IMappingExpression<TSource, TDestination> MapChangedWhen<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
|
||||||
where TDestination : IHasChangedWhen
|
where TDestination : IHasChangedWhen
|
||||||
=> expression.ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(_ => DateTime.Now));
|
=> expression.ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(_ => DateTime.Now));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a base64 data URL string to a byte array.
|
||||||
|
/// Handles data URLs in the format: "data:image/png;base64,iVBORw0KG..."
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dataUrl">The base64 data URL string from Canvas.toDataURL()</param>
|
||||||
|
/// <returns>The decoded byte array, or null if the input is null or empty</returns>
|
||||||
|
public static byte[]? MapDataUrlToRequiredBytes(this string dataUrl)
|
||||||
|
{
|
||||||
|
// Remove data URL prefix (e.g., "data:image/png;base64,")
|
||||||
|
var base64Index = dataUrl.IndexOf(',', StringComparison.Ordinal);
|
||||||
|
if (base64Index == -1)
|
||||||
|
throw new ArgumentException("Invalid data URL format. Unable to extract base64 data.", nameof(dataUrl));
|
||||||
|
|
||||||
|
var base64Data = dataUrl[(base64Index + 1)..];
|
||||||
|
return Convert.FromBase64String(base64Data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
||||||
|
|
||||||
|
public interface IPdfPageImageRenderer
|
||||||
|
{
|
||||||
|
Task<IReadOnlyList<PdfPageImageDto>> RenderPagesAsImagesAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -6,6 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Use IRepository")]
|
[Obsolete("Use IRepository")]
|
||||||
public interface IDocumentReceiverElementRepository : ICRUDRepository<Signature, int>
|
public interface IDocumentReceiverElementRepository : ICRUDRepository<DocReceiverElement, int>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,6 @@ namespace EnvelopeGenerator.Application.Common.Interfaces.Services;
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Use MediatR")]
|
[Obsolete("Use MediatR")]
|
||||||
public interface IDocumentReceiverElementService : IBasicCRUDService<SignatureDto, Signature, int>
|
public interface IDocumentReceiverElementService : IBasicCRUDService<DocReceiverElementDto, DocReceiverElement, int>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -23,34 +25,40 @@ public class MappingProfile : Profile
|
|||||||
{
|
{
|
||||||
// Entity to DTO mappings
|
// Entity to DTO mappings
|
||||||
CreateMap<Config, ConfigDto>();
|
CreateMap<Config, ConfigDto>();
|
||||||
CreateMap<Signature, SignatureDto>();
|
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
||||||
CreateMap<DocumentStatus, DocumentStatusDto>();
|
CreateMap<DocumentStatus, DocumentStatusDto>();
|
||||||
CreateMap<EmailTemplate, EmailTemplateDto>();
|
CreateMap<EmailTemplate, EmailTemplateDto>();
|
||||||
CreateMap<Envelope, EnvelopeDto>();
|
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<Document, DocumentDetailsDto>();
|
||||||
CreateMap<Domain.Entities.History, HistoryCreateDto>().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.EnvelopeReceiver, EnvelopeReceiverDto>();
|
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||||
|
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
|
||||||
CreateMap<ConfigDto, Config>();
|
CreateMap<ConfigDto, Config>();
|
||||||
CreateMap<SignatureDto, Signature>();
|
CreateMap<DocReceiverElementDto, DocReceiverElement>();
|
||||||
|
CreateMap<Signature, DocReceiverElement>()
|
||||||
|
.ForMember(dest => dest.Ink, opt => opt.MapFrom(src => src.DataUrl.MapDataUrlToRequiredBytes()))
|
||||||
|
.MapChangedWhen();
|
||||||
CreateMap<DocumentStatusDto, DocumentStatus>();
|
CreateMap<DocumentStatusDto, DocumentStatus>();
|
||||||
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();
|
||||||
|
|
||||||
@@ -61,4 +69,4 @@ public class MappingProfile : Profile
|
|||||||
? new SmsResponse() { Ok = true }
|
? new SmsResponse() { Ok = true }
|
||||||
: new SmsResponse() { Ok = false, Errors = gtxRes });
|
: new SmsResponse() { Ok = false, Errors = gtxRes });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,88 +1,37 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
using EnvelopeGenerator.Application.Common.Notifications.RemoveSignature;
|
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using System.Dynamic;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Notification raised when a document is signed by a receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Instant"></param>
|
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||||
/// <param name="Structured"></param>
|
public record DocSignedNotification : INotification, ISendMailNotification
|
||||||
public record PsPdfKitAnnotation(ExpandoObject Instant, IEnumerable<AnnotationCreateDto> Structured);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Original"></param>
|
|
||||||
public record DocSignedNotification(EnvelopeReceiverDto Original) : EnvelopeReceiverDto(Original), INotification, ISendMailNotification
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// The envelope receiver information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public required EnvelopeReceiverDto EnvelopeReceiver { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The PSPDFKit annotation data.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("The PSPDFKit library is deprecated.")]
|
||||||
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
|
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Gets the email template type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public EmailTemplateType TemplateType => EmailTemplateType.DocumentSigned;
|
public EmailTemplateType TemplateType => EmailTemplateType.DocumentSigned;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Gets the email address of the receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string EmailAddress => Receiver?.EmailAddress
|
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
|
||||||
?? throw new InvalidOperationException($"Receiver is null." +
|
?? throw new InvalidOperationException($"Receiver is null." +
|
||||||
$"DocSignedNotification:\n{this.ToJson(Format.Json.ForDiagnostics)}");
|
$"DocSignedNotification:\n{this.ToJson(Format.Json.ForDiagnostics)}");
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public static class DocSignedNotificationExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converts an <see cref="EnvelopeReceiverDto"/> to a <see cref="DocSignedNotification"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dto">The DTO to convert.</param>
|
|
||||||
/// <param name="psPdfKitAnnotation"></param>
|
|
||||||
/// <returns>A new <see cref="DocSignedNotification"/> instance.</returns>
|
|
||||||
public static DocSignedNotification ToDocSignedNotification(this EnvelopeReceiverDto dto, PsPdfKitAnnotation psPdfKitAnnotation)
|
|
||||||
=> new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation };
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dtoTask"></param>
|
|
||||||
/// <param name="psPdfKitAnnotation"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task<DocSignedNotification?> ToDocSignedNotification(this Task<EnvelopeReceiverDto?> dtoTask, PsPdfKitAnnotation? psPdfKitAnnotation)
|
|
||||||
=> await dtoTask is EnvelopeReceiverDto dto ? new(dto) { PsPdfKitAnnotation = psPdfKitAnnotation } : null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="publisher"></param>
|
|
||||||
/// <param name="notification"></param>
|
|
||||||
/// <param name="cancel"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task PublishSafely(this IPublisher publisher, DocSignedNotification notification, CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await publisher.Publish(notification, cancel);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
await publisher.Publish(new RemoveSignatureNotification()
|
|
||||||
{
|
|
||||||
EnvelopeId = notification.EnvelopeId,
|
|
||||||
ReceiverId = notification.ReceiverId
|
|
||||||
}, cancel);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
||||||
@@ -7,6 +8,7 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("The PSPDFKit library is deprecated.")]
|
||||||
public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
public class AnnotationHandler : INotificationHandler<DocSignedNotification>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using EnvelopeGenerator.Application.DocStatus.Commands;
|
using EnvelopeGenerator.Application.DocStatus.Commands;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -8,6 +9,7 @@ namespace EnvelopeGenerator.Application.Common.Notifications.DocSigned.Handlers;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||||
public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
||||||
{
|
{
|
||||||
private const string BlankAnnotationJson = "{}";
|
private const string BlankAnnotationJson = "{}";
|
||||||
@@ -29,10 +31,11 @@ public class DocStatusHandler : INotificationHandler<DocSignedNotification>
|
|||||||
/// <param name="notification"></param>
|
/// <param name="notification"></param>
|
||||||
/// <param name="cancel"></param>
|
/// <param name="cancel"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||||
public Task Handle(DocSignedNotification notification, CancellationToken cancel) => _sender.Send(new CreateDocStatusCommand()
|
public Task Handle(DocSignedNotification notification, CancellationToken cancel) => _sender.Send(new CreateDocStatusCommand()
|
||||||
{
|
{
|
||||||
EnvelopeId = notification.EnvelopeId,
|
EnvelopeId = notification.EnvelopeReceiver.EnvelopeId,
|
||||||
ReceiverId = notification.ReceiverId,
|
ReceiverId = notification.EnvelopeReceiver.ReceiverId,
|
||||||
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
|
Value = notification.PsPdfKitAnnotation is PsPdfKitAnnotation annot
|
||||||
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
|
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
|
||||||
: BlankAnnotationJson
|
: BlankAnnotationJson
|
||||||
|
|||||||
@@ -29,13 +29,13 @@ public class HistoryHandler : INotificationHandler<DocSignedNotification>
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
public async Task Handle(DocSignedNotification notification, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
if (notification.Receiver is null)
|
if (notification.EnvelopeReceiver.Receiver is null)
|
||||||
throw new InvalidOperationException($"Receiver information is missing in the notification. DocSignedNotification:\n {notification.ToJson(Format.Json.ForDiagnostics)}");
|
throw new InvalidOperationException($"Receiver information is missing in the notification. DocSignedNotification:\n {notification.ToJson(Format.Json.ForDiagnostics)}");
|
||||||
|
|
||||||
await _sender.Send(new CreateHistoryCommand()
|
await _sender.Send(new CreateHistoryCommand()
|
||||||
{
|
{
|
||||||
EnvelopeId = notification.EnvelopeId,
|
EnvelopeId = notification.EnvelopeReceiver.EnvelopeId,
|
||||||
UserReference = notification.Receiver.EmailAddress,
|
UserReference = notification.EnvelopeReceiver.Receiver.EmailAddress,
|
||||||
Status = EnvelopeStatus.DocumentSigned,
|
Status = EnvelopeStatus.DocumentSigned,
|
||||||
}, cancel);
|
}, cancel);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
|
|||||||
protected override void ConfigureEmailOut(DocSignedNotification notification, EmailOut emailOut)
|
protected override void ConfigureEmailOut(DocSignedNotification notification, EmailOut emailOut)
|
||||||
{
|
{
|
||||||
emailOut.ReferenceString = notification.EmailAddress;
|
emailOut.ReferenceString = notification.EmailAddress;
|
||||||
emailOut.ReferenceId = notification.ReceiverId;
|
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -42,11 +42,11 @@ public class SendSignedMailHandler : SendMailHandler<DocSignedNotification>
|
|||||||
{
|
{
|
||||||
var placeHolders = new Dictionary<string, string>()
|
var placeHolders = new Dictionary<string, string>()
|
||||||
{
|
{
|
||||||
{ "[NAME_RECEIVER]", notification.Name ?? string.Empty },
|
{ "[NAME_RECEIVER]", notification.EnvelopeReceiver.Name ?? string.Empty },
|
||||||
{ "[DOCUMENT_TITLE]", notification.Envelope?.Title ?? string.Empty },
|
{ "[DOCUMENT_TITLE]", notification.EnvelopeReceiver.Envelope?.Title ?? string.Empty },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (notification.Envelope.IsReadAndConfirm())
|
if (notification.EnvelopeReceiver.Envelope.IsReadAndConfirm())
|
||||||
{
|
{
|
||||||
placeHolders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen";
|
placeHolders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen";
|
||||||
placeHolders["[DOCUMENT_PROCESS]"] = string.Empty;
|
placeHolders["[DOCUMENT_PROCESS]"] = string.Empty;
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using QRCoder;
|
using QRCoder;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using MediatR;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
|
using EnvelopeGenerator.Application.Common.Behaviors;
|
||||||
|
using FluentValidation;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application;
|
namespace EnvelopeGenerator.Application;
|
||||||
|
|
||||||
@@ -48,16 +53,40 @@ public static class DependencyInjection
|
|||||||
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
||||||
|
|
||||||
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
||||||
services.TryAddSingleton<ISmsSender, GTXSmsSender>();
|
services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped
|
||||||
services.TryAddSingleton<IEnvelopeSmsHandler, EnvelopeSmsHandler>();
|
services.TryAddScoped<IEnvelopeSmsHandler, EnvelopeSmsHandler>(); // Changed: Singleton → Scoped
|
||||||
services.TryAddSingleton<IAuthenticator, Authenticator>();
|
services.TryAddSingleton<IAuthenticator, Authenticator>();
|
||||||
services.TryAddSingleton<QRCodeGenerator>();
|
services.TryAddSingleton<QRCodeGenerator>();
|
||||||
|
|
||||||
services.AddMediatR(cfg =>
|
services.AddMediatR(cfg =>
|
||||||
{
|
{
|
||||||
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
|
||||||
|
|
||||||
|
// Generic request validation
|
||||||
|
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
|
||||||
|
|
||||||
|
// Register SignCommand pipeline behaviors in execution order
|
||||||
|
// 0. EnvelopeReceiverResolutionBehavior - Resolves EnvelopeReceiver from query parameters (executes FIRST)
|
||||||
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
|
||||||
|
|
||||||
|
// 1. AnnotationBehavior - Saves annotations (executes second)
|
||||||
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SaveSignatureBehavior>();
|
||||||
|
|
||||||
|
// 2. AnnotationBehavior - Saves annotations for legacy web flow
|
||||||
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
|
||||||
|
|
||||||
|
// 3. DocStatusBehavior - Creates document status
|
||||||
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
|
||||||
|
|
||||||
|
// 4. HistoryBehavior - Records history
|
||||||
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
|
||||||
|
|
||||||
|
// 5. SendSignedMailBehavior - Sends notification email (executes LAST)
|
||||||
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pipeline behavior that saves annotations.
|
||||||
|
/// Executes first in the signing process.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("The PSPDFKit library is deprecated.")]
|
||||||
|
public class AnnotationBehavior : IPipelineBehavior<SigningCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IRepository<ElementAnnotation> _repo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AnnotationBehavior"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="repository"></param>
|
||||||
|
public AnnotationBehavior(IRepository<ElementAnnotation> repository)
|
||||||
|
{
|
||||||
|
_repo = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="next"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
if(request.ReceiverAppType != ReceiverAppType.LegacyWeb)
|
||||||
|
if(request.PsPdfKitAnnotation is null)
|
||||||
|
return await next(cancel);
|
||||||
|
else
|
||||||
|
throw new BadRequestException("PsPdfKit Annotation are only supported for the legacy web receiver type.");
|
||||||
|
|
||||||
|
if (request.PsPdfKitAnnotation is PsPdfKitAnnotation annot)
|
||||||
|
await _repo.CreateAsync(annot.Structured, cancel);
|
||||||
|
else
|
||||||
|
throw new BadRequestException("Annotation data is missing or invalid.");
|
||||||
|
|
||||||
|
return await next(cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.DocStatus.Commands;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using MediatR;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pipeline behavior that creates document status.
|
||||||
|
/// Executes second in the signing process.
|
||||||
|
/// </summary>
|
||||||
|
public class DocStatusBehavior : IPipelineBehavior<SigningCommand, Unit>
|
||||||
|
{
|
||||||
|
private const string BlankAnnotationJson = "{}";
|
||||||
|
|
||||||
|
private readonly ISender _sender;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender"></param>
|
||||||
|
public DocStatusBehavior(ISender sender)
|
||||||
|
{
|
||||||
|
_sender = sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="next"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||||
|
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _sender.Send(new CreateDocStatusCommand()
|
||||||
|
{
|
||||||
|
EnvelopeId = request.EnvelopeReceiver.EnvelopeId,
|
||||||
|
ReceiverId = request.EnvelopeReceiver.ReceiverId,
|
||||||
|
Value = request.PsPdfKitAnnotation is PsPdfKitAnnotation annot
|
||||||
|
? JsonSerializer.Serialize(annot.Instant, Format.Json.ForAnnotations)
|
||||||
|
: BlankAnnotationJson
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return await next(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pipeline behavior that resolves and validates EnvelopeReceiver.
|
||||||
|
/// Executes FIRST in the signing process - before all other behaviors.
|
||||||
|
/// If EnvelopeReceiver is not provided, it queries the database using EnvelopeReceiverQueryBase parameters.
|
||||||
|
/// </summary>
|
||||||
|
public class EnvelopeReceiverResolutionBehavior : IPipelineBehavior<SigningCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IRepository<EnvelopeReceiver> _erRepo;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="erRepo"></param>
|
||||||
|
/// <param name="mapper"></param>
|
||||||
|
public EnvelopeReceiverResolutionBehavior(IRepository<EnvelopeReceiver> erRepo, IMapper mapper)
|
||||||
|
{
|
||||||
|
_erRepo = erRepo;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="next"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// If EnvelopeReceiver is not provided, query it from database
|
||||||
|
if (request.EnvelopeReceiver is null)
|
||||||
|
{
|
||||||
|
var er = await _erRepo.Query
|
||||||
|
.Where(request, notnull: true)
|
||||||
|
.Include(x => x.Envelope)
|
||||||
|
.Include(x => x.Receiver)
|
||||||
|
.SingleOrDefaultAsync(cancellationToken)
|
||||||
|
?? throw new NotFoundException("EnvelopeReceiver not found");
|
||||||
|
|
||||||
|
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await next(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Histories.Commands;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pipeline behavior that records history.
|
||||||
|
/// Executes third in the signing process.
|
||||||
|
/// </summary>
|
||||||
|
public class HistoryBehavior : IPipelineBehavior<SigningCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly ISender _sender;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender"></param>
|
||||||
|
public HistoryBehavior(ISender sender)
|
||||||
|
{
|
||||||
|
_sender = sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="next"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (request.EnvelopeReceiver.Receiver is null)
|
||||||
|
throw new InvalidOperationException($"Receiver information is missing in the notification. SignCommand:\n {request.ToJson(Format.Json.ForDiagnostics)}");
|
||||||
|
|
||||||
|
await _sender.Send(new CreateHistoryCommand()
|
||||||
|
{
|
||||||
|
EnvelopeId = request.EnvelopeReceiver.EnvelopeId,
|
||||||
|
UserReference = request.EnvelopeReceiver.Receiver.EmailAddress,
|
||||||
|
Status = EnvelopeStatus.DocumentSigned,
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
return await next(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pipeline behavior that creates document status.
|
||||||
|
/// Executes second in the signing process.
|
||||||
|
/// </summary>
|
||||||
|
public class SaveSignatureBehavior : IPipelineBehavior<SigningCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly ISender _sender;
|
||||||
|
|
||||||
|
private readonly IRepository<DocReceiverElement> _elementRepo;
|
||||||
|
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender"></param>
|
||||||
|
/// <param name="elementRepo"></param>
|
||||||
|
/// <param name="mapper"></param>
|
||||||
|
public SaveSignatureBehavior(ISender sender, IRepository<DocReceiverElement> elementRepo, IMapper mapper)
|
||||||
|
{
|
||||||
|
_sender = sender;
|
||||||
|
_elementRepo = elementRepo;
|
||||||
|
_elementRepo = elementRepo;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="next"></param>
|
||||||
|
/// <param name="cancel"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
if (request.ReceiverAppType == ReceiverAppType.LegacyWeb)
|
||||||
|
return await next(cancel);
|
||||||
|
else if(request.Signatures is not IEnumerable<Signature> signatures)
|
||||||
|
throw new BadRequestException($"Signatures are required for saving signature behavior.");
|
||||||
|
|
||||||
|
var elements = await _elementRepo
|
||||||
|
.Where(e => e.Document.EnvelopeId == request.EnvelopeReceiver.EnvelopeId)
|
||||||
|
.Where(e => e.ReceiverId == request.EnvelopeReceiver.ReceiverId)
|
||||||
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
|
foreach (var element in elements)
|
||||||
|
{
|
||||||
|
var signatures = request.Signatures.Where(s => s.Id == element.Id).ToList();
|
||||||
|
if(signatures.Count == 0)
|
||||||
|
throw new BadRequestException("No signature found for element with id {element.Id}.");
|
||||||
|
else if(signatures.Count > 1)
|
||||||
|
throw new BadRequestException("Multiple signatures found for element with id {element.Id}.");
|
||||||
|
|
||||||
|
await _elementRepo.UpdateAsync(signatures.First(), e => e.Id == element.Id, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await next(cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocReceiverElements.Behaviors;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pipeline behavior that sends signed mail notification.
|
||||||
|
/// Executes LAST in the signing process - only if all previous behaviors succeed.
|
||||||
|
/// </summary>
|
||||||
|
public class SendSignedMailBehavior : IPipelineBehavior<SigningCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IRepository<EmailTemplate> _tempRepo;
|
||||||
|
private readonly IRepository<EmailOut> _emailOutRepo;
|
||||||
|
private readonly MailParams _mailParams;
|
||||||
|
private readonly DispatcherParams _dispatcherParams;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tempRepo"></param>
|
||||||
|
/// <param name="emailOutRepo"></param>
|
||||||
|
/// <param name="mailParamsOptions"></param>
|
||||||
|
/// <param name="dispatcherParamsOptions"></param>
|
||||||
|
public SendSignedMailBehavior(
|
||||||
|
IRepository<EmailTemplate> tempRepo,
|
||||||
|
IRepository<EmailOut> emailOutRepo,
|
||||||
|
IOptions<MailParams> mailParamsOptions,
|
||||||
|
IOptions<DispatcherParams> dispatcherParamsOptions)
|
||||||
|
{
|
||||||
|
_tempRepo = tempRepo;
|
||||||
|
_emailOutRepo = emailOutRepo;
|
||||||
|
_mailParams = mailParamsOptions.Value;
|
||||||
|
_dispatcherParams = dispatcherParamsOptions.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="next"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<Unit> Handle(SigningCommand request, RequestHandlerDelegate<Unit> next, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var placeHolders = CreatePlaceHolders(request);
|
||||||
|
|
||||||
|
var temp = await _tempRepo
|
||||||
|
.Where(x => x.Name == EmailTemplateType.DocumentSigned.ToString())
|
||||||
|
.SingleOrDefaultAsync(cancellationToken)
|
||||||
|
?? throw new InvalidOperationException($"Email template not found. SignCommand:\n {request.ToJson(Format.Json.ForDiagnostics)}");
|
||||||
|
|
||||||
|
temp.Subject = ReplacePlaceHolders(temp.Subject, placeHolders, _mailParams.Placeholders);
|
||||||
|
temp.Body = ReplacePlaceHolders(temp.Body, placeHolders, _mailParams.Placeholders);
|
||||||
|
|
||||||
|
var emailOut = new EmailOut
|
||||||
|
{
|
||||||
|
EmailAddress = request.EnvelopeReceiver.Receiver!.EmailAddress,
|
||||||
|
EmailBody = temp.Body,
|
||||||
|
EmailSubj = temp.Subject,
|
||||||
|
AddedWhen = DateTime.Now,
|
||||||
|
AddedWho = _dispatcherParams.AddedWho,
|
||||||
|
SendingProfile = _dispatcherParams.SendingProfile,
|
||||||
|
ReminderTypeId = _dispatcherParams.ReminderTypeId,
|
||||||
|
EmailAttmt1 = _dispatcherParams.EmailAttmt1,
|
||||||
|
WfId = (int)EnvelopeStatus.MessageConfirmationSent,
|
||||||
|
ReferenceString = request.EnvelopeReceiver.Receiver!.EmailAddress,
|
||||||
|
ReferenceId = request.EnvelopeReceiver.ReceiverId
|
||||||
|
};
|
||||||
|
|
||||||
|
await _emailOutRepo.CreateAsync(emailOut, cancellationToken);
|
||||||
|
|
||||||
|
return await next(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, string> CreatePlaceHolders(SigningCommand request)
|
||||||
|
{
|
||||||
|
var placeHolders = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{ "[NAME_RECEIVER]", request.EnvelopeReceiver.Name ?? string.Empty },
|
||||||
|
{ "[DOCUMENT_TITLE]", request.EnvelopeReceiver.Envelope?.Title ?? string.Empty },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (request.EnvelopeReceiver.Envelope.IsReadAndConfirm())
|
||||||
|
{
|
||||||
|
placeHolders["[SIGNATURE_TYPE]"] = "Lesen und bestätigen";
|
||||||
|
placeHolders["[DOCUMENT_PROCESS]"] = string.Empty;
|
||||||
|
placeHolders["[FINAL_STATUS]"] = "Lesebestätigung";
|
||||||
|
placeHolders["[FINAL_ACTION]"] = "Empfänger bestätigt";
|
||||||
|
placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Empfänger abgelehnt!";
|
||||||
|
placeHolders["[RECEIVER_ACTION]"] = "bestätigt";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
placeHolders["[SIGNATURE_TYPE]"] = "Signieren";
|
||||||
|
placeHolders["[DOCUMENT_PROCESS]"] = " und elektronisch unterschreiben";
|
||||||
|
placeHolders["[FINAL_STATUS]"] = "Signatur";
|
||||||
|
placeHolders["[FINAL_ACTION]"] = "Vertragspartner unterzeichnet";
|
||||||
|
placeHolders["[REJECTED_BY_OTHERS]"] = "anderen Vertragspartner abgelehnt! Ihre notwendige Unterzeichnung wurde verworfen.";
|
||||||
|
placeHolders["[RECEIVER_ACTION]"] = "unterschrieben";
|
||||||
|
}
|
||||||
|
|
||||||
|
return placeHolders;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReplacePlaceHolders(string text, params Dictionary<string, string>[] placeHoldersList)
|
||||||
|
{
|
||||||
|
foreach (var placeHolders in placeHoldersList)
|
||||||
|
foreach (var ph in placeHolders)
|
||||||
|
text = text.Replace(ph.Key, ph.Value);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using MediatR;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
using EnvelopeGenerator.Application.Common.Query;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocReceiverElements.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command to sign a document by a receiver.
|
||||||
|
/// </summary>
|
||||||
|
public record SigningCommand : EnvelopeReceiverQueryBase, IRequest
|
||||||
|
{
|
||||||
|
private EnvelopeReceiverDto? _envelopeReceiver;
|
||||||
|
|
||||||
|
internal void SetEnvelopeReceiver(EnvelopeReceiverDto envelopeReceiver)
|
||||||
|
{
|
||||||
|
_envelopeReceiver = envelopeReceiver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The envelope receiver information.
|
||||||
|
/// </summary>
|
||||||
|
public EnvelopeReceiverDto EnvelopeReceiver
|
||||||
|
{
|
||||||
|
get => _envelopeReceiver!;
|
||||||
|
init => _envelopeReceiver = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The PSPDFKit annotation data.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("This notification is deprecated. Use Signature.Commands.SignCommand instead.")]
|
||||||
|
public PsPdfKitAnnotation? PsPdfKitAnnotation { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<Signature>? Signatures { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public ReceiverAppType ReceiverAppType { get; init; } = ReceiverAppType.ReceiverUI;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the sign command. All work is done by pipeline behaviors.
|
||||||
|
/// This handler is intentionally empty - behaviors handle all the processing.
|
||||||
|
/// </summary>
|
||||||
|
public class SignCommandHandler : IRequestHandler<SigningCommand>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the signing command. Pipeline behaviors handle all processing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task Handle(SigningCommand request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public enum ReceiverAppType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
ReceiverUI = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
LegacyWeb = 1,
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.Common.Query;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.DocReceiverElements.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public record ReadDocReceiverElementQuery : EnvelopeReceiverQueryBase, IRequest<IEnumerable<DocReceiverElementDto>>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public class ReadDocReceiverElementQueryHandler : IRequestHandler<ReadDocReceiverElementQuery, IEnumerable<DocReceiverElementDto>>
|
||||||
|
{
|
||||||
|
private readonly IRepository<DocReceiverElement> _repository;
|
||||||
|
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="repository"></param>
|
||||||
|
/// <param name="mapper"></param>
|
||||||
|
public ReadDocReceiverElementQueryHandler(IRepository<DocReceiverElement> repository, IMapper mapper)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="NotImplementedException"></exception>
|
||||||
|
public async Task<IEnumerable<DocReceiverElementDto>> Handle(ReadDocReceiverElementQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var q = _repository.Query;
|
||||||
|
|
||||||
|
if(request.Envelope.Id is int envelopeId)
|
||||||
|
q = q.Where(e => e.Document.EnvelopeId == envelopeId);
|
||||||
|
|
||||||
|
if (request.Envelope.Uuid is string envelopeUuid)
|
||||||
|
q = q.Where(e => e.Document.Envelope.Uuid == envelopeUuid);
|
||||||
|
|
||||||
|
if (request.Receiver.Id is int receiverId)
|
||||||
|
q = q.Where(e => e.ReceiverId == receiverId);
|
||||||
|
|
||||||
|
if (request.Receiver.Signature is string signature)
|
||||||
|
q = q.Where(e => e.Receiver.Signature == signature);
|
||||||
|
|
||||||
|
var elements = await q.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return _mapper.Map<IEnumerable<DocReceiverElementDto>>(elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using AutoMapper.QueryableExtensions;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Documents.Queries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a query to read document metadata/details without binary payload.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="EnvelopeId">The identifier of the envelope associated with the document.</param>
|
||||||
|
/// <param name="UserId">The unique identifier of the sending user. Optional.</param>
|
||||||
|
public record ReadDocumentDetailsQuery(int EnvelopeId, int? UserId = null) : IRequest<DocumentDetailsDto?>;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles queries for reading document details without loading BYTE_DATA.
|
||||||
|
/// </summary>
|
||||||
|
public class ReadDocumentDetailsQueryHandler : IRequestHandler<ReadDocumentDetailsQuery, DocumentDetailsDto?>
|
||||||
|
{
|
||||||
|
private readonly IRepository<Document> _repo;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
public ReadDocumentDetailsQueryHandler(IRepository<Document> repo, IMapper mapper)
|
||||||
|
{
|
||||||
|
_repo = repo;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DocumentDetailsDto?> Handle(ReadDocumentDetailsQuery query, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var docQuery = _repo.Query.AsNoTracking();
|
||||||
|
|
||||||
|
if (query.UserId is int userId)
|
||||||
|
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
|
||||||
|
|
||||||
|
return await docQuery
|
||||||
|
.Where(d => d.EnvelopeId == query.EnvelopeId)
|
||||||
|
.ProjectTo<DocumentDetailsDto>(_mapper.ConfigurationProvider)
|
||||||
|
.FirstOrDefaultAsync(cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Documents.Queries;
|
||||||
|
|
||||||
|
public class ReadDocumentDetailsQueryValidator : AbstractValidator<ReadDocumentDetailsQuery>
|
||||||
|
{
|
||||||
|
public ReadDocumentDetailsQueryValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.EnvelopeId)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.WithMessage("EnvelopeId is required.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Documents.Queries;
|
||||||
|
|
||||||
|
public class ReadDocumentQueryValidator : AbstractValidator<ReadDocumentQuery>
|
||||||
|
{
|
||||||
|
public ReadDocumentQueryValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x)
|
||||||
|
.Must(x => (x.Id ?? 0) > 0 || (x.EnvelopeId ?? 0) > 0)
|
||||||
|
.WithMessage("Id or EnvelopeId is required.");
|
||||||
|
|
||||||
|
RuleFor(x => x.Id)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.When(x => x.Id.HasValue)
|
||||||
|
.WithMessage("Id must be greater than 0.");
|
||||||
|
|
||||||
|
RuleFor(x => x.EnvelopeId)
|
||||||
|
.GreaterThan(0)
|
||||||
|
.When(x => x.EnvelopeId.HasValue)
|
||||||
|
.WithMessage("EnvelopeId must be greater than 0.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -19,9 +19,10 @@
|
|||||||
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
|
<PackageReference Include="DigitalData.Core.Client" Version="2.1.0" />
|
||||||
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
<PackageReference Include="DigitalData.Core.Exceptions" Version="1.1.0" />
|
||||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||||
|
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||||
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
|
||||||
<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 +80,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>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public record CreateEnvelopeReceiverCommand : CreateEnvelopeCommand, IRequest<Cr
|
|||||||
/// <param name="X">X-Position</param>
|
/// <param name="X">X-Position</param>
|
||||||
/// <param name="Y">Y-Position</param>
|
/// <param name="Y">Y-Position</param>
|
||||||
/// <param name="Page">Seite, auf der sie sich befindet</param>
|
/// <param name="Page">Seite, auf der sie sich befindet</param>
|
||||||
public record Signature([Required] double X, [Required] double Y, [Required] int Page);
|
public record DocReceiverElementCreateDto([Required] double X, [Required] double Y, [Required] int Page);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DTO für Empfänger, die erstellt oder abgerufen werden sollen.
|
/// DTO für Empfänger, die erstellt oder abgerufen werden sollen.
|
||||||
@@ -41,7 +41,7 @@ public class ReceiverGetOrCreateCommand
|
|||||||
/// Unterschriften auf Dokumenten.
|
/// Unterschriften auf Dokumenten.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
public List<Signature> Signatures { get; init; } = new();
|
public List<DocReceiverElementCreateDto> DocReceiverElements { get; init; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Der Name, mit dem der Empfänger angesprochen werden soll.
|
/// Der Name, mit dem der Empfänger angesprochen werden soll.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using AutoMapper;
|
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -18,17 +17,14 @@ public record ReadEnvelopeTypesQuery : IRequest<IEnumerable<EnvelopeTypeDto>>;
|
|||||||
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
|
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
|
||||||
{
|
{
|
||||||
private readonly IRepository<EnvelopeType> _repository;
|
private readonly IRepository<EnvelopeType> _repository;
|
||||||
private readonly IMapper _mapper;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="repository"></param>
|
/// <param name="repository"></param>
|
||||||
/// <param name="mapper"></param>
|
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository)
|
||||||
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository, IMapper mapper)
|
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
_mapper = mapper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -39,7 +35,28 @@ public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQu
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
|
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var types = await _repository.Query.ToListAsync(cancellationToken);
|
// TODO: Temporary manual projection to avoid null-materialization crashes from legacy nullable DB columns in EnvelopeType.
|
||||||
return _mapper.Map<IEnumerable<EnvelopeTypeDto>>(types);
|
// Revisit and switch to AutoMapper query projection (ProjectTo) after schema nullability and mapping contracts are stabilized.
|
||||||
|
return await _repository.Query
|
||||||
|
.AsNoTracking()
|
||||||
|
.Select(t => new EnvelopeTypeDto
|
||||||
|
{
|
||||||
|
Id = t.Id,
|
||||||
|
Title = t.Title ?? string.Empty,
|
||||||
|
Language = string.IsNullOrWhiteSpace(t.Language) ? "de-DE" : t.Language,
|
||||||
|
ExpiresDays = t.ExpiresDays,
|
||||||
|
CertificationType = (int?)t.CertificationType,
|
||||||
|
UseAccessCode = t.UseAccessCode,
|
||||||
|
FinalEmailToCreator = (int?)t.FinalEmailToCreator,
|
||||||
|
FinalEmailToReceivers = (int?)t.FinalEmailToReceivers,
|
||||||
|
AddedWhen = t.AddedWhen,
|
||||||
|
ChangedWhen = t.ChangedWhen,
|
||||||
|
ExpiresWarningDays = t.ExpiresWarningDays,
|
||||||
|
SendReminderEmails = t.SendReminderEmails,
|
||||||
|
FirstReminderDays = t.FirstReminderDays,
|
||||||
|
ReminderIntervalDays = t.ReminderIntervalDays,
|
||||||
|
ContractType = t.ContractType
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
@@ -13,23 +12,96 @@ namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Optionale Umschlag-ID fuer Aktualisierungsvorgaenge.
|
||||||
|
/// </summary>
|
||||||
|
public int? EnvelopeId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kennung des ausgewaehlten Umschlagtyps (Vorlage).
|
||||||
|
/// </summary>
|
||||||
|
public int? EnvelopeTypeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sprachcode fuer Umschlaginhalt und Benachrichtigungen.
|
||||||
|
/// </summary>
|
||||||
|
public string? Language { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt an, ob der Zugriffscode fuer Empfaenger aktiviert ist.
|
||||||
|
/// </summary>
|
||||||
|
public bool? UseAccessCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zertifizierungstyp fuer den Signaturprozess.
|
||||||
|
/// </summary>
|
||||||
|
public int? CertificationType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finale E-Mail-Einstellung fuer den Ersteller.
|
||||||
|
/// </summary>
|
||||||
|
public int? FinalEmailToCreator { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finale E-Mail-Einstellung fuer die Empfaenger.
|
||||||
|
/// </summary>
|
||||||
|
public int? FinalEmailToReceivers { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt an, ob Erinnerungs-E-Mails aktiviert sind.
|
||||||
|
/// </summary>
|
||||||
|
public bool? SendReminderEmails { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anzahl der Tage bis zur ersten Erinnerung.
|
||||||
|
/// </summary>
|
||||||
|
public int? FirstReminderDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Intervall in Tagen zwischen Erinnerungs-E-Mails.
|
||||||
|
/// </summary>
|
||||||
|
public int? ReminderIntervalDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anzahl der Tage bis zum Ablauf des Umschlags.
|
||||||
|
/// </summary>
|
||||||
|
public int? ExpiresWhenDays { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anzahl der Tage vor Ablauf, ab denen gewarnt wird.
|
||||||
|
/// </summary>
|
||||||
|
public int? ExpiresWarningWhenDays { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Der Titel des Umschlags. Dies ist ein Pflichtfeld.
|
/// Der Titel des Umschlags. Dies ist ein Pflichtfeld.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
public string? Title { get; set; }
|
||||||
public required string Title { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld.
|
/// Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
public string? Message { get; set; }
|
||||||
public required string Message { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false.
|
/// Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool TFAEnabled { get; set; } = false;
|
public bool TFAEnabled { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt an, ob der Umschlag nach dem Speichern sofort versendet werden soll.
|
||||||
|
/// </summary>
|
||||||
|
public bool Send { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optionaler PDF-Dokumentinhalt als Base64.
|
||||||
|
/// </summary>
|
||||||
|
public CreateEnvelopeDocumentDto? Document { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Empfaengerliste inklusive optionaler Signaturfeld-Koordinaten.
|
||||||
|
/// </summary>
|
||||||
|
public List<CreateEnvelopeReceiverDto> Receivers { get; init; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ID des Absenders
|
/// ID des Absenders
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -55,4 +127,14 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public bool UseSQLExecutor { get; set; } = true;
|
public bool UseSQLExecutor { get; set; } = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record CreateEnvelopeDocumentDto(string DataAsBase64);
|
||||||
|
|
||||||
|
public record CreateEnvelopeFieldDto(double X, double Y, int Page);
|
||||||
|
|
||||||
|
public record CreateEnvelopeReceiverDto(
|
||||||
|
string EmailAddress,
|
||||||
|
string? Name,
|
||||||
|
string? PhoneNumber,
|
||||||
|
List<CreateEnvelopeFieldDto> Fields);
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using DigitalData.Core.Abstraction.Application.Repository;
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
|
using EnvelopeGenerator.Application.Histories.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
@@ -19,6 +29,28 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
|
|
||||||
private IRepository<Envelope> Repository => _provider.GetRequiredService<IRepository<Envelope>>();
|
private IRepository<Envelope> Repository => _provider.GetRequiredService<IRepository<Envelope>>();
|
||||||
|
|
||||||
|
private IRepository<Document> DocumentRepository => _provider.GetRequiredService<IRepository<Document>>();
|
||||||
|
|
||||||
|
private IRepository<EnvelopeReceiver> EnvelopeReceiverRepository => _provider.GetRequiredService<IRepository<EnvelopeReceiver>>();
|
||||||
|
|
||||||
|
private IRepository<EnvelopeType> EnvelopeTypeRepository => _provider.GetRequiredService<IRepository<EnvelopeType>>();
|
||||||
|
|
||||||
|
private IRepository<Receiver> ReceiverRepository => _provider.GetRequiredService<IRepository<Receiver>>();
|
||||||
|
|
||||||
|
private IRepository<DocReceiverElement> DocReceiverElementRepository => _provider.GetRequiredService<IRepository<DocReceiverElement>>();
|
||||||
|
|
||||||
|
private IRepository<EmailTemplate> EmailTemplateRepository => _provider.GetRequiredService<IRepository<EmailTemplate>>();
|
||||||
|
|
||||||
|
private IRepository<EmailOut> EmailOutRepository => _provider.GetRequiredService<IRepository<EmailOut>>();
|
||||||
|
|
||||||
|
private MailParams MailParams => _provider.GetRequiredService<IOptions<MailParams>>().Value;
|
||||||
|
|
||||||
|
private DispatcherParams DispatcherParams => _provider.GetRequiredService<IOptions<DispatcherParams>>().Value;
|
||||||
|
|
||||||
|
private IConfigService ConfigService => _provider.GetRequiredService<IConfigService>();
|
||||||
|
|
||||||
|
private IMediator Mediator => _provider.GetRequiredService<IMediator>();
|
||||||
|
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -40,10 +72,572 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<EnvelopeDto?> Handle(CreateEnvelopeCommand request, CancellationToken cancel)
|
public async Task<EnvelopeDto?> Handle(CreateEnvelopeCommand request, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
var envelope = request.UseSQLExecutor
|
var now = DateTime.Now;
|
||||||
? await Executor.CreateEnvelopeAsync(request.UserId, request.Title, request.Message, request.TFAEnabled, cancel)
|
var title = request.Title?.Trim() ?? string.Empty;
|
||||||
: await Repository.CreateAsync(request, cancel);
|
var message = request.Message?.Trim() ?? string.Empty;
|
||||||
|
var settings = await ResolveEnvelopeSettingsAsync(request, existing: null, cancel);
|
||||||
|
|
||||||
|
Envelope envelope;
|
||||||
|
if (request.EnvelopeId is int envelopeId)
|
||||||
|
{
|
||||||
|
envelope = await Repository.Query
|
||||||
|
.Include(e => e.User)
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == envelopeId, cancel)
|
||||||
|
?? throw new BadRequestException($"Envelope {envelopeId} not found.");
|
||||||
|
|
||||||
|
if (envelope.UserId != request.UserId)
|
||||||
|
throw new ForbiddenException();
|
||||||
|
|
||||||
|
settings = await ResolveEnvelopeSettingsAsync(request, envelope, cancel);
|
||||||
|
|
||||||
|
await Repository.UpdateAsync(e =>
|
||||||
|
{
|
||||||
|
e.Title = title;
|
||||||
|
e.Message = message;
|
||||||
|
e.TfaEnabled = request.TFAEnabled;
|
||||||
|
e.EnvelopeTypeId = settings.EnvelopeTypeId;
|
||||||
|
e.Language = settings.Language;
|
||||||
|
e.UseAccessCode = settings.UseAccessCode;
|
||||||
|
e.CertificationType = settings.CertificationType;
|
||||||
|
e.FinalEmailToCreator = settings.FinalEmailToCreator;
|
||||||
|
e.FinalEmailToReceivers = settings.FinalEmailToReceivers;
|
||||||
|
e.SendReminderEmails = settings.SendReminderEmails;
|
||||||
|
e.FirstReminderDays = settings.FirstReminderDays;
|
||||||
|
e.ReminderIntervalDays = settings.ReminderIntervalDays;
|
||||||
|
e.ExpiresWhenDays = settings.ExpiresWhenDays;
|
||||||
|
e.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
|
||||||
|
e.ContractType = settings.ContractType;
|
||||||
|
e.ExpiresWhen = settings.ExpiresWhen;
|
||||||
|
e.ExpiresWarningWhen = settings.ExpiresWarningWhen;
|
||||||
|
e.Status = EnvelopeStatus.EnvelopeSaved;
|
||||||
|
e.ChangedWhen = now;
|
||||||
|
}, e => e.Id == envelopeId, cancel);
|
||||||
|
|
||||||
|
envelope.Title = title;
|
||||||
|
envelope.Message = message;
|
||||||
|
envelope.TfaEnabled = request.TFAEnabled;
|
||||||
|
envelope.EnvelopeTypeId = settings.EnvelopeTypeId;
|
||||||
|
envelope.Language = settings.Language;
|
||||||
|
envelope.UseAccessCode = settings.UseAccessCode;
|
||||||
|
envelope.CertificationType = settings.CertificationType;
|
||||||
|
envelope.FinalEmailToCreator = settings.FinalEmailToCreator;
|
||||||
|
envelope.FinalEmailToReceivers = settings.FinalEmailToReceivers;
|
||||||
|
envelope.SendReminderEmails = settings.SendReminderEmails;
|
||||||
|
envelope.FirstReminderDays = settings.FirstReminderDays;
|
||||||
|
envelope.ReminderIntervalDays = settings.ReminderIntervalDays;
|
||||||
|
envelope.ExpiresWhenDays = settings.ExpiresWhenDays;
|
||||||
|
envelope.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
|
||||||
|
envelope.ContractType = settings.ContractType;
|
||||||
|
envelope.ExpiresWhen = settings.ExpiresWhen;
|
||||||
|
envelope.ExpiresWarningWhen = settings.ExpiresWarningWhen;
|
||||||
|
envelope.Status = EnvelopeStatus.EnvelopeSaved;
|
||||||
|
envelope.ChangedWhen = now;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
envelope = request.UseSQLExecutor
|
||||||
|
? await Executor.CreateEnvelopeAsync(request.UserId, title, message, request.TFAEnabled, cancel)
|
||||||
|
: await Repository.CreateAsync(new Envelope
|
||||||
|
{
|
||||||
|
UserId = request.UserId,
|
||||||
|
Status = EnvelopeStatus.EnvelopeCreated,
|
||||||
|
Uuid = Guid.NewGuid().ToString(),
|
||||||
|
Title = title,
|
||||||
|
Message = message,
|
||||||
|
AddedWhen = now,
|
||||||
|
ChangedWhen = now,
|
||||||
|
Comment = string.Empty,
|
||||||
|
EnvelopeTypeId = settings.EnvelopeTypeId,
|
||||||
|
Language = settings.Language,
|
||||||
|
TfaEnabled = request.TFAEnabled,
|
||||||
|
UseAccessCode = settings.UseAccessCode,
|
||||||
|
CertificationType = settings.CertificationType,
|
||||||
|
FinalEmailToCreator = settings.FinalEmailToCreator,
|
||||||
|
FinalEmailToReceivers = settings.FinalEmailToReceivers,
|
||||||
|
SendReminderEmails = settings.SendReminderEmails,
|
||||||
|
FirstReminderDays = settings.FirstReminderDays,
|
||||||
|
ReminderIntervalDays = settings.ReminderIntervalDays,
|
||||||
|
ExpiresWhenDays = settings.ExpiresWhenDays,
|
||||||
|
ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays,
|
||||||
|
ExpiresWhen = settings.ExpiresWhen,
|
||||||
|
ExpiresWarningWhen = settings.ExpiresWarningWhen,
|
||||||
|
ContractType = settings.ContractType
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
if (request.UseSQLExecutor)
|
||||||
|
{
|
||||||
|
await Repository.UpdateAsync(e =>
|
||||||
|
{
|
||||||
|
e.Title = title;
|
||||||
|
e.Message = message;
|
||||||
|
e.TfaEnabled = request.TFAEnabled;
|
||||||
|
e.EnvelopeTypeId = settings.EnvelopeTypeId;
|
||||||
|
e.Language = settings.Language;
|
||||||
|
e.UseAccessCode = settings.UseAccessCode;
|
||||||
|
e.CertificationType = settings.CertificationType;
|
||||||
|
e.FinalEmailToCreator = settings.FinalEmailToCreator;
|
||||||
|
e.FinalEmailToReceivers = settings.FinalEmailToReceivers;
|
||||||
|
e.SendReminderEmails = settings.SendReminderEmails;
|
||||||
|
e.FirstReminderDays = settings.FirstReminderDays;
|
||||||
|
e.ReminderIntervalDays = settings.ReminderIntervalDays;
|
||||||
|
e.ExpiresWhenDays = settings.ExpiresWhenDays;
|
||||||
|
e.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
|
||||||
|
e.ExpiresWhen = settings.ExpiresWhen;
|
||||||
|
e.ExpiresWarningWhen = settings.ExpiresWarningWhen;
|
||||||
|
e.ContractType = settings.ContractType;
|
||||||
|
e.Status = EnvelopeStatus.EnvelopeSaved;
|
||||||
|
e.ChangedWhen = now;
|
||||||
|
}, e => e.Id == envelope.Id, cancel);
|
||||||
|
|
||||||
|
envelope.Title = title;
|
||||||
|
envelope.Message = message;
|
||||||
|
envelope.TfaEnabled = request.TFAEnabled;
|
||||||
|
envelope.EnvelopeTypeId = settings.EnvelopeTypeId;
|
||||||
|
envelope.Language = settings.Language;
|
||||||
|
envelope.UseAccessCode = settings.UseAccessCode;
|
||||||
|
envelope.CertificationType = settings.CertificationType;
|
||||||
|
envelope.FinalEmailToCreator = settings.FinalEmailToCreator;
|
||||||
|
envelope.FinalEmailToReceivers = settings.FinalEmailToReceivers;
|
||||||
|
envelope.SendReminderEmails = settings.SendReminderEmails;
|
||||||
|
envelope.FirstReminderDays = settings.FirstReminderDays;
|
||||||
|
envelope.ReminderIntervalDays = settings.ReminderIntervalDays;
|
||||||
|
envelope.ExpiresWhenDays = settings.ExpiresWhenDays;
|
||||||
|
envelope.ExpiresWarningWhenDays = settings.ExpiresWarningWhenDays;
|
||||||
|
envelope.ExpiresWhen = settings.ExpiresWhen;
|
||||||
|
envelope.ExpiresWarningWhen = settings.ExpiresWarningWhen;
|
||||||
|
envelope.ContractType = settings.ContractType;
|
||||||
|
envelope.Status = EnvelopeStatus.EnvelopeSaved;
|
||||||
|
envelope.ChangedWhen = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Mediator.Send(new CreateHistoryCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = envelope.Id,
|
||||||
|
UserReference = await ResolveSenderReferenceAsync(envelope, cancel),
|
||||||
|
Status = EnvelopeStatus.EnvelopeCreated
|
||||||
|
}, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
await UpsertDocumentAsync(envelope.Id, request.Document, now, cancel);
|
||||||
|
await UpsertReceiversAndFieldsAsync(envelope.Id, request.Receivers, now, cancel);
|
||||||
|
|
||||||
|
if (request.Send)
|
||||||
|
await SendEnvelopeAsync(envelope, cancel);
|
||||||
|
|
||||||
|
envelope = await Repository.Query
|
||||||
|
.Include(e => e.User)
|
||||||
|
.Include(e => e.EnvelopeReceivers)
|
||||||
|
.ThenInclude(er => er.Receiver)
|
||||||
|
.FirstAsync(e => e.Id == envelope.Id, cancel);
|
||||||
|
|
||||||
return _mapper.Map<EnvelopeDto>(envelope);
|
return _mapper.Map<EnvelopeDto>(envelope);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private async Task<EnvelopeSettings> ResolveEnvelopeSettingsAsync(CreateEnvelopeCommand request, Envelope? existing, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
EnvelopeType? selectedType = null;
|
||||||
|
if (request.EnvelopeTypeId is int envelopeTypeId && envelopeTypeId > 0)
|
||||||
|
{
|
||||||
|
selectedType = await EnvelopeTypeRepository.Query
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == envelopeTypeId, cancel)
|
||||||
|
?? throw new BadRequestException($"Envelope type {envelopeTypeId} not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiresWhenDays = Math.Max(0,
|
||||||
|
request.ExpiresWhenDays
|
||||||
|
?? selectedType?.ExpiresWhenDays
|
||||||
|
?? existing?.ExpiresWhenDays
|
||||||
|
?? 0);
|
||||||
|
|
||||||
|
var expiresWarningWhenDays = Math.Max(0,
|
||||||
|
request.ExpiresWarningWhenDays
|
||||||
|
?? selectedType?.ExpiresWarningWhenDays
|
||||||
|
?? existing?.ExpiresWarningWhenDays
|
||||||
|
?? 0);
|
||||||
|
|
||||||
|
var useAccessCode = request.UseAccessCode
|
||||||
|
?? selectedType?.UseAccessCode
|
||||||
|
?? existing?.UseAccessCode
|
||||||
|
?? false;
|
||||||
|
|
||||||
|
if (request.TFAEnabled)
|
||||||
|
useAccessCode = true;
|
||||||
|
|
||||||
|
return new EnvelopeSettings(
|
||||||
|
EnvelopeTypeId: request.EnvelopeTypeId ?? existing?.EnvelopeTypeId,
|
||||||
|
Language: ResolveLanguage(request.Language, selectedType?.Language, existing?.Language),
|
||||||
|
UseAccessCode: useAccessCode,
|
||||||
|
CertificationType: request.CertificationType
|
||||||
|
?? (int?)selectedType?.CertificationType
|
||||||
|
?? existing?.CertificationType
|
||||||
|
?? (int)CertificationType.AdvancedElectronicSignature,
|
||||||
|
FinalEmailToCreator: request.FinalEmailToCreator
|
||||||
|
?? (int?)selectedType?.FinalEmailToCreator
|
||||||
|
?? existing?.FinalEmailToCreator
|
||||||
|
?? (int)FinalEmailType.No,
|
||||||
|
FinalEmailToReceivers: request.FinalEmailToReceivers
|
||||||
|
?? (int?)selectedType?.FinalEmailToReceivers
|
||||||
|
?? existing?.FinalEmailToReceivers
|
||||||
|
?? (int)FinalEmailType.No,
|
||||||
|
SendReminderEmails: request.SendReminderEmails
|
||||||
|
?? selectedType?.SendReminderEmails
|
||||||
|
?? existing?.SendReminderEmails
|
||||||
|
?? false,
|
||||||
|
FirstReminderDays: Math.Max(0,
|
||||||
|
request.FirstReminderDays
|
||||||
|
?? selectedType?.FirstReminderDays
|
||||||
|
?? existing?.FirstReminderDays
|
||||||
|
?? 0),
|
||||||
|
ReminderIntervalDays: Math.Max(0,
|
||||||
|
request.ReminderIntervalDays
|
||||||
|
?? selectedType?.ReminderIntervalDays
|
||||||
|
?? existing?.ReminderIntervalDays
|
||||||
|
?? 0),
|
||||||
|
ExpiresWhenDays: expiresWhenDays,
|
||||||
|
ExpiresWarningWhenDays: expiresWarningWhenDays,
|
||||||
|
ExpiresWhen: expiresWhenDays > 0 ? DateTime.Now.AddDays(expiresWhenDays) : null,
|
||||||
|
ExpiresWarningWhen: expiresWarningWhenDays > 0 ? DateTime.Now.AddDays(expiresWarningWhenDays) : null,
|
||||||
|
ContractType: existing?.ContractType
|
||||||
|
?? selectedType?.ContractType
|
||||||
|
?? (int)ContractType.Contract);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveLanguage(string? preferred, string? fallback1, string? fallback2)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(preferred))
|
||||||
|
return preferred.Trim();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(fallback1))
|
||||||
|
return fallback1.Trim();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(fallback2))
|
||||||
|
return fallback2.Trim();
|
||||||
|
|
||||||
|
return "de-DE";
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record EnvelopeSettings(
|
||||||
|
int? EnvelopeTypeId,
|
||||||
|
string Language,
|
||||||
|
bool UseAccessCode,
|
||||||
|
int CertificationType,
|
||||||
|
int FinalEmailToCreator,
|
||||||
|
int FinalEmailToReceivers,
|
||||||
|
bool SendReminderEmails,
|
||||||
|
int FirstReminderDays,
|
||||||
|
int ReminderIntervalDays,
|
||||||
|
int ExpiresWhenDays,
|
||||||
|
int ExpiresWarningWhenDays,
|
||||||
|
DateTime? ExpiresWhen,
|
||||||
|
DateTime? ExpiresWarningWhen,
|
||||||
|
int ContractType);
|
||||||
|
|
||||||
|
private async Task<string> ResolveSenderReferenceAsync(Envelope envelope, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(envelope.User?.Email))
|
||||||
|
return envelope.User.Email;
|
||||||
|
|
||||||
|
var senderEmail = await Repository.Query
|
||||||
|
.Where(e => e.Id == envelope.Id)
|
||||||
|
.Select(e => e.User.Email)
|
||||||
|
.FirstOrDefaultAsync(cancel);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(senderEmail))
|
||||||
|
throw new BadRequestException("Envelope sender email is missing.");
|
||||||
|
|
||||||
|
return senderEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpsertDocumentAsync(int envelopeId, CreateEnvelopeDocumentDto? document, DateTime now, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
if (document is null || string.IsNullOrWhiteSpace(document.DataAsBase64))
|
||||||
|
return;
|
||||||
|
|
||||||
|
byte[] bytes;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bytes = Convert.FromBase64String(document.DataAsBase64);
|
||||||
|
}
|
||||||
|
catch (FormatException)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Document payload is not valid base64.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingDocumentId = await DocumentRepository.Query
|
||||||
|
.Where(d => d.EnvelopeId == envelopeId)
|
||||||
|
.Select(d => (int?)d.Id)
|
||||||
|
.FirstOrDefaultAsync(cancel);
|
||||||
|
|
||||||
|
if (existingDocumentId is null)
|
||||||
|
{
|
||||||
|
await DocumentRepository.CreateAsync(new Document
|
||||||
|
{
|
||||||
|
EnvelopeId = envelopeId,
|
||||||
|
AddedWhen = now,
|
||||||
|
ByteData = bytes,
|
||||||
|
Filename = string.Empty,
|
||||||
|
Filepath = string.Empty,
|
||||||
|
FileNameOriginal = string.Empty
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await DocumentRepository.UpdateAsync(d =>
|
||||||
|
{
|
||||||
|
d.ByteData = bytes;
|
||||||
|
}, d => d.Id == existingDocumentId.Value, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpsertReceiversAndFieldsAsync(int envelopeId, IReadOnlyCollection<CreateEnvelopeReceiverDto> receivers, DateTime now, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var filteredReceivers = receivers
|
||||||
|
.Where(r => !string.IsNullOrWhiteSpace(r.EmailAddress))
|
||||||
|
.Select(r => r with
|
||||||
|
{
|
||||||
|
EmailAddress = r.EmailAddress.Trim().ToLowerInvariant(),
|
||||||
|
Name = r.Name?.Trim(),
|
||||||
|
PhoneNumber = string.IsNullOrWhiteSpace(r.PhoneNumber) ? null : r.PhoneNumber.Trim(),
|
||||||
|
Fields = r.Fields ?? new List<CreateEnvelopeFieldDto>()
|
||||||
|
})
|
||||||
|
.GroupBy(r => r.EmailAddress, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(g => g.First())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (filteredReceivers.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var document = await DocumentRepository.Query
|
||||||
|
.FirstOrDefaultAsync(d => d.EnvelopeId == envelopeId, cancel);
|
||||||
|
|
||||||
|
if (document is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var currentLinks = await EnvelopeReceiverRepository.Query
|
||||||
|
.Include(er => er.Receiver)
|
||||||
|
.Where(er => er.EnvelopeId == envelopeId)
|
||||||
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
|
var linksByEmail = currentLinks
|
||||||
|
.Where(er => !string.IsNullOrWhiteSpace(er.Receiver?.EmailAddress))
|
||||||
|
.GroupBy(er => er.Receiver!.EmailAddress, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(g => g.Key.ToLowerInvariant(), g => g.First(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var sequence = currentLinks.Count > 0 ? currentLinks.Max(er => er.Sequence) : 0;
|
||||||
|
|
||||||
|
foreach (var receiverDraft in filteredReceivers)
|
||||||
|
{
|
||||||
|
var linkResult = await GetOrCreateEnvelopeReceiverAsync(envelopeId, receiverDraft, linksByEmail, now, sequence, cancel);
|
||||||
|
var link = linkResult.Link;
|
||||||
|
sequence = linkResult.NextSequence;
|
||||||
|
|
||||||
|
await DocReceiverElementRepository.DeleteAsync(query => query
|
||||||
|
.Where(e => e.DocumentId == document.Id)
|
||||||
|
.Where(e => e.ReceiverId == link.ReceiverId), cancel);
|
||||||
|
|
||||||
|
foreach (var field in receiverDraft.Fields.Where(f => f.Page > 0))
|
||||||
|
{
|
||||||
|
await DocReceiverElementRepository.CreateAsync(new DocReceiverElement
|
||||||
|
{
|
||||||
|
DocumentId = document.Id,
|
||||||
|
ReceiverId = link.ReceiverId,
|
||||||
|
ElementType = 1,
|
||||||
|
X = field.X,
|
||||||
|
Y = field.Y,
|
||||||
|
Width = 1.77,
|
||||||
|
Height = 1.96,
|
||||||
|
Page = field.Page,
|
||||||
|
Required = true,
|
||||||
|
ReadOnly = false,
|
||||||
|
AnnotationIndex = 0,
|
||||||
|
AddedWhen = now,
|
||||||
|
FullName = link.Name,
|
||||||
|
Position = string.Empty,
|
||||||
|
Place = string.Empty,
|
||||||
|
Tooltip = string.Empty
|
||||||
|
}, cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(EnvelopeReceiver Link, int NextSequence)> GetOrCreateEnvelopeReceiverAsync(
|
||||||
|
int envelopeId,
|
||||||
|
CreateEnvelopeReceiverDto receiverDraft,
|
||||||
|
IDictionary<string, EnvelopeReceiver> linksByEmail,
|
||||||
|
DateTime now,
|
||||||
|
int sequence,
|
||||||
|
CancellationToken cancel)
|
||||||
|
{
|
||||||
|
if (linksByEmail.TryGetValue(receiverDraft.EmailAddress, out var existingLink))
|
||||||
|
{
|
||||||
|
var nextName = string.IsNullOrWhiteSpace(receiverDraft.Name) ? existingLink.Name : receiverDraft.Name!;
|
||||||
|
var nextPhoneNumber = receiverDraft.PhoneNumber ?? existingLink.PhoneNumber;
|
||||||
|
|
||||||
|
await EnvelopeReceiverRepository.UpdateAsync(er =>
|
||||||
|
{
|
||||||
|
er.Name = nextName;
|
||||||
|
er.PhoneNumber = nextPhoneNumber;
|
||||||
|
er.ChangedWhen = now;
|
||||||
|
}, er => er.EnvelopeId == existingLink.EnvelopeId && er.ReceiverId == existingLink.ReceiverId, cancel);
|
||||||
|
|
||||||
|
existingLink.Name = nextName;
|
||||||
|
existingLink.PhoneNumber = nextPhoneNumber;
|
||||||
|
return (existingLink, sequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
var receiver = await ReceiverRepository.Query
|
||||||
|
.FirstOrDefaultAsync(r => r.EmailAddress != null && r.EmailAddress.ToLower() == receiverDraft.EmailAddress, cancel);
|
||||||
|
|
||||||
|
if (receiver is null)
|
||||||
|
{
|
||||||
|
receiver = await ReceiverRepository.CreateAsync(new Receiver
|
||||||
|
{
|
||||||
|
EmailAddress = receiverDraft.EmailAddress,
|
||||||
|
Signature = receiverDraft.EmailAddress.ToUpperInvariant().GetChecksum(),
|
||||||
|
AddedWhen = now,
|
||||||
|
TotpSecretkey = string.Empty,
|
||||||
|
TfaRegDeadline = null
|
||||||
|
}, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
sequence++;
|
||||||
|
var link = await EnvelopeReceiverRepository.CreateAsync(new EnvelopeReceiver
|
||||||
|
{
|
||||||
|
EnvelopeId = envelopeId,
|
||||||
|
ReceiverId = receiver.Id,
|
||||||
|
Sequence = sequence,
|
||||||
|
Name = receiverDraft.Name ?? receiverDraft.EmailAddress,
|
||||||
|
JobTitle = string.Empty,
|
||||||
|
CompanyName = string.Empty,
|
||||||
|
PrivateMessage = string.Empty,
|
||||||
|
AccessCode = string.Empty,
|
||||||
|
AddedWhen = now,
|
||||||
|
ChangedWhen = now,
|
||||||
|
PhoneNumber = receiverDraft.PhoneNumber ?? string.Empty
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
link.Receiver = receiver;
|
||||||
|
linksByEmail[receiverDraft.EmailAddress] = link;
|
||||||
|
return (link, sequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var hasDocument = await DocumentRepository.Query
|
||||||
|
.AnyAsync(d => d.EnvelopeId == envelope.Id && d.ByteData != null && d.ByteData.Length > 0, cancel);
|
||||||
|
|
||||||
|
if (!hasDocument)
|
||||||
|
throw new BadRequestException("Cannot send envelope without a document.");
|
||||||
|
|
||||||
|
var links = await EnvelopeReceiverRepository.Query
|
||||||
|
.Include(er => er.Receiver)
|
||||||
|
.Where(er => er.EnvelopeId == envelope.Id)
|
||||||
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
|
if (links.Count == 0)
|
||||||
|
throw new BadRequestException("Cannot send envelope without receivers.");
|
||||||
|
|
||||||
|
var receiverIds = links.Select(l => l.ReceiverId).Distinct().ToList();
|
||||||
|
var documentIds = await DocumentRepository.Query
|
||||||
|
.Where(d => d.EnvelopeId == envelope.Id)
|
||||||
|
.Select(d => d.Id)
|
||||||
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
|
var receiverFieldCounts = await DocReceiverElementRepository.Query
|
||||||
|
.Where(e => documentIds.Contains(e.DocumentId))
|
||||||
|
.Where(e => receiverIds.Contains(e.ReceiverId))
|
||||||
|
.GroupBy(e => e.ReceiverId)
|
||||||
|
.Select(g => new { ReceiverId = g.Key, Count = g.Count() })
|
||||||
|
.ToDictionaryAsync(x => x.ReceiverId, x => x.Count, cancel);
|
||||||
|
|
||||||
|
var receiversWithoutFields = links
|
||||||
|
.Where(link => !receiverFieldCounts.TryGetValue(link.ReceiverId, out var count) || count <= 0)
|
||||||
|
.Select(link => link.Name)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (receiversWithoutFields.Count > 0)
|
||||||
|
throw new BadRequestException("Cannot send envelope because some receivers have no signature fields.");
|
||||||
|
|
||||||
|
var changedWhen = DateTime.Now;
|
||||||
|
await Repository.UpdateAsync(e =>
|
||||||
|
{
|
||||||
|
e.Status = EnvelopeStatus.EnvelopeQueued;
|
||||||
|
e.ChangedWhen = changedWhen;
|
||||||
|
}, e => e.Id == envelope.Id, cancel);
|
||||||
|
|
||||||
|
var userReference = await ResolveSenderReferenceAsync(envelope, cancel);
|
||||||
|
|
||||||
|
await Mediator.Send(new CreateHistoryCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = envelope.Id,
|
||||||
|
UserReference = userReference,
|
||||||
|
Status = EnvelopeStatus.EnvelopeQueued
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
await QueueInvitationEmailsAsync(envelope, links, userReference, cancel);
|
||||||
|
|
||||||
|
envelope.Status = EnvelopeStatus.EnvelopeQueued;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task QueueInvitationEmailsAsync(
|
||||||
|
Envelope envelope,
|
||||||
|
IReadOnlyCollection<EnvelopeReceiver> links,
|
||||||
|
string senderReference,
|
||||||
|
CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var template = await EmailTemplateRepository.Query
|
||||||
|
.SingleOrDefaultAsync(x => x.Name == EmailTemplateType.DocumentReceived.ToString(), cancel)
|
||||||
|
?? throw new InvalidOperationException($"Email template not found: {EmailTemplateType.DocumentReceived}");
|
||||||
|
|
||||||
|
var signatureHost = await ConfigService.ReadDefaultSignatureHost();
|
||||||
|
var senderName = senderReference;
|
||||||
|
|
||||||
|
foreach (var link in links)
|
||||||
|
{
|
||||||
|
var receiver = link.Receiver;
|
||||||
|
if (receiver is null || string.IsNullOrWhiteSpace(receiver.EmailAddress) || string.IsNullOrWhiteSpace(receiver.Signature))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var envelopeKey = (envelope.Uuid, receiver.Signature).ToEnvelopeKey();
|
||||||
|
var documentLink = $"{signatureHost.TrimEnd('/')}/envelope/login/{Uri.EscapeDataString(envelopeKey)}";
|
||||||
|
|
||||||
|
var placeholders = new Dictionary<string, string>(MailParams.Placeholders)
|
||||||
|
{
|
||||||
|
["[NAME_RECEIVER]"] = link.Name ?? string.Empty,
|
||||||
|
["[NAME_SENDER]"] = senderName ?? string.Empty,
|
||||||
|
["[DOCUMENT_TITLE]"] = envelope.Title ?? string.Empty,
|
||||||
|
["[MESSAGE]"] = envelope.Message ?? string.Empty,
|
||||||
|
["[LINK_TO_DOCUMENT]"] = documentLink,
|
||||||
|
["[LINK_TO_DOCUMENT_TEXT]"] = documentLink[..Math.Min(40, documentLink.Length)] + ".."
|
||||||
|
};
|
||||||
|
|
||||||
|
var subject = ReplacePlaceHolders(template.Subject, placeholders);
|
||||||
|
var body = ReplacePlaceHolders(template.Body, placeholders);
|
||||||
|
|
||||||
|
var emailOut = new EmailOut
|
||||||
|
{
|
||||||
|
EmailAddress = receiver.EmailAddress,
|
||||||
|
EmailSubj = subject,
|
||||||
|
EmailBody = body,
|
||||||
|
AddedWhen = DateTime.Now,
|
||||||
|
AddedWho = DispatcherParams.AddedWho,
|
||||||
|
SendingProfile = DispatcherParams.SendingProfile,
|
||||||
|
ReminderTypeId = DispatcherParams.ReminderTypeId,
|
||||||
|
EmailAttmt1 = DispatcherParams.EmailAttmt1,
|
||||||
|
WfId = (int)EnvelopeStatus.MessageInvitationSent,
|
||||||
|
ReferenceString = receiver.EmailAddress,
|
||||||
|
ReferenceId = link.ReceiverId
|
||||||
|
};
|
||||||
|
|
||||||
|
await EmailOutRepository.CreateAsync(emailOut, cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReplacePlaceHolders(string text, Dictionary<string, string> placeHolders)
|
||||||
|
{
|
||||||
|
foreach (var placeHolder in placeHolders)
|
||||||
|
text = text.Replace(placeHolder.Key, placeHolder.Value);
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,89 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
var changedWhen = DateTime.Now;
|
||||||
|
envelope.Status = targetStatus;
|
||||||
|
envelope.Comment = reason;
|
||||||
|
envelope.ChangedWhen = changedWhen;
|
||||||
|
|
||||||
|
await _envelopeRepository.UpdateAsync(e =>
|
||||||
|
{
|
||||||
|
e.Status = targetStatus;
|
||||||
|
e.Comment = reason;
|
||||||
|
e.ChangedWhen = changedWhen;
|
||||||
|
}, 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>
|
||||||
@@ -22,7 +32,7 @@ public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<Envelo
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optionaler Benutzerfilter; wenn gesetzt, werden nur Umschläge des Benutzers geladen.
|
/// Optionaler Benutzerfilter; wenn gesetzt, werden nur Umschläge des Benutzers geladen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? UserId { get; init; }
|
internal int? UserId { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Setzt den Benutzerkontext für die Abfrage.
|
/// Setzt den Benutzerkontext für die Abfrage.
|
||||||
@@ -132,10 +142,18 @@ 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.Documents)
|
.Include(e => e.Type)
|
||||||
|
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
||||||
|
.Include(e => e.Histories)
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
|
return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,299 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Bestätigungen</value>
|
<value>Bestätigungen</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
<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>
|
||||||
|
<data name="ChangedAt" xml:space="preserve"><value>Geändert am</value></data>
|
||||||
|
<data name="Close" xml:space="preserve"><value>Schließen</value></data>
|
||||||
|
<data name="CloseLanguageMenu" xml:space="preserve"><value>Sprachmenü schließen</value></data>
|
||||||
|
<data name="ConfirmSignature" xml:space="preserve"><value>Unterschrift bestätigen</value></data>
|
||||||
|
<data name="Created" xml:space="preserve"><value>Erstellt</value></data>
|
||||||
|
<data name="Delete" xml:space="preserve"><value>Löschen</value></data>
|
||||||
|
<data name="Document" xml:space="preserve"><value>Dokument</value></data>
|
||||||
|
<data name="DocumentCouldNotBeLoaded" xml:space="preserve"><value>Dokument konnte nicht geladen werden</value></data>
|
||||||
|
<data name="DocumentLoading" xml:space="preserve"><value>Dokument wird geladen...</value></data>
|
||||||
|
<data name="EmailAddress" xml:space="preserve"><value>E-Mail-Adresse</value></data>
|
||||||
|
<data name="Envelope" xml:space="preserve"><value>Umschlag</value></data>
|
||||||
|
<data name="Error" xml:space="preserve"><value>Fehler</value></data>
|
||||||
|
<data name="ErrorLoadingDocument_WithReason" xml:space="preserve"><value>Fehler beim Laden des Dokuments: {0}</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode" xml:space="preserve"><value>Entwicklungsmodus</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Detail" xml:space="preserve"><value>Dies kann dazu führen, dass vertrauliche Informationen aus Ausnahmen Endbenutzern angezeigt werden. Für lokales Debugging setzen Sie ASPNETCORE_ENVIRONMENT auf Development und starten die App neu.</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Info" xml:space="preserve"><value>Im Entwicklungsmodus werden detailliertere Fehlerinformationen angezeigt.</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Warning" xml:space="preserve"><value>Der Entwicklungsmodus sollte in bereitgestellten Anwendungen nicht aktiviert sein.</value></data>
|
||||||
|
<data name="ErrorPage_ProcessingRequest" xml:space="preserve"><value>Bei der Verarbeitung Ihrer Anfrage ist ein Fehler aufgetreten.</value></data>
|
||||||
|
<data name="ErrorPage_RequestId" xml:space="preserve"><value>Anfrage-ID</value></data>
|
||||||
|
<data name="ErrorWithReason" xml:space="preserve"><value>Fehler: {0}</value></data>
|
||||||
|
<data name="From" xml:space="preserve"><value>Von</value></data>
|
||||||
|
<data name="FullName" xml:space="preserve"><value>Vor- und Nachname</value></data>
|
||||||
|
<data name="History" xml:space="preserve"><value>Historie</value></data>
|
||||||
|
<data name="Index_DigitalSignature" xml:space="preserve"><value>Digitale Unterschrift</value></data>
|
||||||
|
<data name="Index_PdfExport" xml:space="preserve"><value>PDF-Export</value></data>
|
||||||
|
<data name="Index_SecureAccess" xml:space="preserve"><value>Sicherer Zugang</value></data>
|
||||||
|
<data name="Index_Subtitle" xml:space="preserve"><value>Willkommen im eSign-Portal</value></data>
|
||||||
|
<data name="Index_ToEnvelopeOverview" xml:space="preserve"><value>Zur Umschlag-Übersicht</value></data>
|
||||||
|
<data name="Languages" xml:space="preserve"><value>Sprachen</value></data>
|
||||||
|
<data name="Loading" xml:space="preserve"><value>Lädt...</value></data>
|
||||||
|
<data name="Login" xml:space="preserve"><value>Anmelden</value></data>
|
||||||
|
<data name="LoginInProgress" xml:space="preserve"><value>Anmelden ...</value></data>
|
||||||
|
<data name="NotAuthenticatedPleaseLoginAgain" xml:space="preserve"><value>Nicht authentifiziert. Bitte melden Sie sich erneut an.</value></data>
|
||||||
|
<data name="Only" xml:space="preserve"><value>Nur</value></data>
|
||||||
|
<data name="OpenDocument" xml:space="preserve"><value>Dokument öffnen</value></data>
|
||||||
|
<data name="Optional" xml:space="preserve"><value>optional</value></data>
|
||||||
|
<data name="Pending" xml:space="preserve"><value>Ausstehend</value></data>
|
||||||
|
<data name="Preview" xml:space="preserve"><value>Vorschau</value></data>
|
||||||
|
<data name="PrivacyPolicy" xml:space="preserve"><value>Datenschutz</value></data>
|
||||||
|
<data name="Receiver" xml:space="preserve"><value>Empfänger</value></data>
|
||||||
|
<data name="ReceiverLogin_AccessCode_Placeholder" xml:space="preserve"><value>Zugangscode eingeben</value></data>
|
||||||
|
<data name="ReceiverLogin_Description" xml:space="preserve"><value>Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.</value></data>
|
||||||
|
<data name="ReceiverLogin_FooterHelp" xml:space="preserve"><value>Bei Problemen wenden Sie sich bitte an den Absender des Dokuments.</value></data>
|
||||||
|
<data name="ReceiverLogin_InvalidCode_Body" xml:space="preserve"><value>Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</value></data>
|
||||||
|
<data name="ReceiverLogin_InvalidCode_Title" xml:space="preserve"><value>Ungültiger Zugangscode.</value></data>
|
||||||
|
<data name="ReceiverLogin_NotFound_Body" xml:space="preserve"><value>Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.</value></data>
|
||||||
|
<data name="ReceiverLogin_NotFound_Title" xml:space="preserve"><value>Dokument nicht gefunden.</value></data>
|
||||||
|
<data name="ReceiverLogin_Subtitle" xml:space="preserve"><value>Sicherer Zugang mit Zugangscode</value></data>
|
||||||
|
<data name="ReceiverSigned_ConfirmBody" xml:space="preserve"><value>Diese Aktion kann nicht rückgängig gemacht werden. Mit der Bestätigung erklären Sie, das oben angezeigte Dokument elektronisch unterzeichnet zu haben. Das unterzeichnete Dokument wird anschließend an alle beteiligten Parteien übermittelt.</value></data>
|
||||||
|
<data name="ReceiverSigned_ConfirmQuestion" xml:space="preserve"><value>Möchten Sie das Dokument verbindlich unterschreiben?</value></data>
|
||||||
|
<data name="ReceiverSigned_EnvelopeKeyMissing" xml:space="preserve"><value>Envelope-Schlüssel fehlt.</value></data>
|
||||||
|
<data name="Refresh" xml:space="preserve"><value>Aktualisieren</value></data>
|
||||||
|
<data name="RequiredField" xml:space="preserve"><value>Pflichtfeld</value></data>
|
||||||
|
<data name="Selected" xml:space="preserve"><value>ausgewählte</value></data>
|
||||||
|
<data name="Send" xml:space="preserve"><value>Senden</value></data>
|
||||||
|
<data name="Sender_ActiveEnvelopes" xml:space="preserve"><value>Aktive Umschläge</value></data>
|
||||||
|
<data name="Sender_CompletedEnvelopes" xml:space="preserve"><value>Abgeschlossene Umschläge</value></data>
|
||||||
|
<data name="Sender_CreateEnvelope" xml:space="preserve"><value>Neuer Umschlag</value></data>
|
||||||
|
<data name="Sender_CreateEnvelope_Title" xml:space="preserve"><value>Neuen Umschlag erstellen</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_Header" xml:space="preserve"><value>Umschlag löschen</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonPlaceholder" xml:space="preserve"><value>Begründung eingeben...</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonPrompt" xml:space="preserve"><value>Bitte geben Sie einen Grund an, warum der Umschlag gelöscht/zurückgezogen werden soll.</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonRequired" xml:space="preserve"><value>Bitte geben Sie einen Löschgrund an.</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_Title" xml:space="preserve"><value>Ausgewählten Umschlag löschen</value></data>
|
||||||
|
<data name="Sender_Edit" xml:space="preserve"><value>Bearbeiten</value></data>
|
||||||
|
<data name="Sender_EditEnvelope_Title" xml:space="preserve"><value>Ausgewählten Umschlag bearbeiten</value></data>
|
||||||
|
<data name="Sender_ExportCurrentView_Title" xml:space="preserve"><value>Aktuelle Ansicht exportieren</value></data>
|
||||||
|
<data name="Sender_ExportFormat_Aria" xml:space="preserve"><value>Exportformat auswählen</value></data>
|
||||||
|
<data name="Sender_ExportView_Title" xml:space="preserve"><value>Export der aktuellen Tabellenansicht</value></data>
|
||||||
|
<data name="Sender_LoadError_Title" xml:space="preserve"><value>Fehler beim Laden der Umschläge</value></data>
|
||||||
|
<data name="Sender_LoadingEnvelopes" xml:space="preserve"><value>Umschläge werden geladen...</value></data>
|
||||||
|
<data name="Sender_NoHistory" xml:space="preserve"><value>Keine Historie vorhanden</value></data>
|
||||||
|
<data name="Sender_NoReceivers" xml:space="preserve"><value>Keine Empfänger</value></data>
|
||||||
|
<data name="Sender_Preview_Title" xml:space="preserve"><value>Ausgewählten Umschlag als PDF anzeigen</value></data>
|
||||||
|
<data name="Sender_PreviewDocumentNotFound" xml:space="preserve"><value>Das Dokument für Umschlag #{0} wurde nicht gefunden oder Sie haben keinen Zugriff darauf.</value></data>
|
||||||
|
<data name="SenderEditor_AddSignature" xml:space="preserve"><value>Signatur hinzufügen</value></data>
|
||||||
|
<data name="SenderEditor_ClickDocumentFor" xml:space="preserve"><value>Klicken Sie auf die Stelle im Dokument für</value></data>
|
||||||
|
<data name="SenderEditor_EmailSuggestionsHint" xml:space="preserve"><value>Bereits verwendete E-Mail-Adressen werden bei der Eingabe vorgeschlagen.</value></data>
|
||||||
|
<data name="SenderEditor_EmailSuggestionsLoading" xml:space="preserve"><value>E-Mail-Vorschläge werden geladen...</value></data>
|
||||||
|
<data name="SenderEditor_ErrorCreatingEnvelope" xml:space="preserve"><value>Fehler beim Erstellen des Umschlags</value></data>
|
||||||
|
<data name="SenderEditor_LoadEnvelopeError" xml:space="preserve"><value>Fehler beim Laden des Umschlags #{0}: {1}</value></data>
|
||||||
|
<data name="SenderEditor_LoadFileError" xml:space="preserve"><value>Fehler beim Laden der Datei: {0}</value></data>
|
||||||
|
<data name="SenderEditor_Message_Placeholder" xml:space="preserve"><value>Bitte unterzeichnen Sie das beigefügte Dokument.</value></data>
|
||||||
|
<data name="SenderEditor_NoDocumentLoaded" xml:space="preserve"><value>Kein Dokument geladen</value></data>
|
||||||
|
<data name="SenderEditor_NoPagesToDisplay" xml:space="preserve"><value>Keine Seiten zur Anzeige verfügbar.</value></data>
|
||||||
|
<data name="SenderEditor_NoReceiversAdded" xml:space="preserve"><value>Noch keine Empfänger hinzugefügt.</value></data>
|
||||||
|
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Seite</value></data>
|
||||||
|
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Signaturfeld platzieren</value></data>
|
||||||
|
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Alle Signaturfelder entfernen</value></data>
|
||||||
|
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Bitte wählen Sie eine PDF-Datei aus.</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>Der bestehende Entwurf wurde versendet.</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Umschlag gesendet</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>Umschlag wurde erfolgreich gesendet.</value></data>
|
||||||
|
<data name="SenderEditor_SignatureField" xml:space="preserve"><value>Signaturfeld</value></data>
|
||||||
|
<data name="SenderEditor_Title_Placeholder" xml:space="preserve"><value>z.B. Arbeitsvertrag 2025</value></data>
|
||||||
|
<data name="SenderEditor_ToOverview" xml:space="preserve"><value>Zur Übersicht</value></data>
|
||||||
|
<data name="SenderEditor_UploadPdfToPlaceFields" xml:space="preserve"><value>Laden Sie eine PDF-Datei hoch, um Signaturfelder zu platzieren.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailDuplicate" xml:space="preserve"><value>Diese E-Mail-Adresse wurde bereits hinzugefügt.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailInvalid" xml:space="preserve"><value>Bitte geben Sie eine gültige E-Mail-Adresse ein.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailRequired" xml:space="preserve"><value>Bitte geben Sie eine E-Mail-Adresse ein.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_FullNameRequired" xml:space="preserve"><value>Bitte geben Sie einen Vor- und Nachnamen ein.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_ReceiverRequired" xml:space="preserve"><value>Bitte fügen Sie mindestens einen Empfänger hinzu.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_SignatureFieldRequired" xml:space="preserve"><value>Bitte platzieren Sie mindestens ein Signaturfeld.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_TitleRequired" xml:space="preserve"><value>Bitte geben Sie einen Titel ein.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_UploadPdf" xml:space="preserve"><value>Bitte laden Sie zuerst ein PDF-Dokument hoch.</value></data>
|
||||||
|
<data name="SenderLogin_Description" xml:space="preserve"><value>Bitte melden Sie sich mit Ihren Zugangsdaten an, um auf das Sender-Dashboard zuzugreifen.</value></data>
|
||||||
|
<data name="SenderLogin_FooterHelp" xml:space="preserve"><value>Bei Problemen wenden Sie sich bitte an den Administrator.</value></data>
|
||||||
|
<data name="SenderLogin_InvalidCredentials_Body" xml:space="preserve"><value>Benutzername oder Passwort ist falsch. Bitte versuchen Sie es erneut.</value></data>
|
||||||
|
<data name="SenderLogin_InvalidCredentials_Title" xml:space="preserve"><value>Ungültige Anmeldedaten.</value></data>
|
||||||
|
<data name="SenderLogin_Password_Placeholder" xml:space="preserve"><value>Passwort eingeben</value></data>
|
||||||
|
<data name="SenderLogin_Subtitle" xml:space="preserve"><value>Sicherer Zugang zum Sender-Dashboard</value></data>
|
||||||
|
<data name="SenderLogin_Title" xml:space="preserve"><value>Sender Anmeldung</value></data>
|
||||||
|
<data name="SenderLogin_Username_Placeholder" xml:space="preserve"><value>Benutzername eingeben</value></data>
|
||||||
|
<data name="ServerError" xml:space="preserve"><value>Serverfehler.</value></data>
|
||||||
|
<data name="Signed" xml:space="preserve"><value>Unterschrieben</value></data>
|
||||||
|
<data name="SignedDocument" xml:space="preserve"><value>Signiertes Dokument</value></data>
|
||||||
|
<data name="System" xml:space="preserve"><value>System</value></data>
|
||||||
|
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</value></data>
|
||||||
|
<data name="UploadPdf" xml:space="preserve"><value>PDF hochladen</value></data>
|
||||||
|
<data name="UserName" xml:space="preserve"><value>Benutzername</value></data>
|
||||||
|
<data name="VerifyInProgress" xml:space="preserve"><value>Überprüfen ...</value></data>
|
||||||
|
</root>
|
||||||
|
|||||||
@@ -477,4 +477,299 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Confirmations</value>
|
<value>Confirmations</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
<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>
|
||||||
|
<data name="ChangedAt" xml:space="preserve"><value>Changed at</value></data>
|
||||||
|
<data name="Close" xml:space="preserve"><value>Close</value></data>
|
||||||
|
<data name="CloseLanguageMenu" xml:space="preserve"><value>Close language menu</value></data>
|
||||||
|
<data name="ConfirmSignature" xml:space="preserve"><value>Confirm signature</value></data>
|
||||||
|
<data name="Created" xml:space="preserve"><value>Created</value></data>
|
||||||
|
<data name="Delete" xml:space="preserve"><value>Delete</value></data>
|
||||||
|
<data name="Document" xml:space="preserve"><value>Document</value></data>
|
||||||
|
<data name="DocumentCouldNotBeLoaded" xml:space="preserve"><value>Document could not be loaded</value></data>
|
||||||
|
<data name="DocumentLoading" xml:space="preserve"><value>Document is loading...</value></data>
|
||||||
|
<data name="EmailAddress" xml:space="preserve"><value>Email address</value></data>
|
||||||
|
<data name="Envelope" xml:space="preserve"><value>Envelope</value></data>
|
||||||
|
<data name="Error" xml:space="preserve"><value>Error</value></data>
|
||||||
|
<data name="ErrorLoadingDocument_WithReason" xml:space="preserve"><value>Error loading document: {0}</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode" xml:space="preserve"><value>Development Mode</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Detail" xml:space="preserve"><value>This can result in sensitive exception information being shown to end users. For local debugging, set ASPNETCORE_ENVIRONMENT to Development and restart the app.</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Info" xml:space="preserve"><value>Switching to Development will display more detailed error information.</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Warning" xml:space="preserve"><value>The Development environment should not be enabled in deployed applications.</value></data>
|
||||||
|
<data name="ErrorPage_ProcessingRequest" xml:space="preserve"><value>An error occurred while processing your request.</value></data>
|
||||||
|
<data name="ErrorPage_RequestId" xml:space="preserve"><value>Request ID</value></data>
|
||||||
|
<data name="ErrorWithReason" xml:space="preserve"><value>Error: {0}</value></data>
|
||||||
|
<data name="From" xml:space="preserve"><value>From</value></data>
|
||||||
|
<data name="FullName" xml:space="preserve"><value>First and last name</value></data>
|
||||||
|
<data name="History" xml:space="preserve"><value>History</value></data>
|
||||||
|
<data name="Index_DigitalSignature" xml:space="preserve"><value>Digital signature</value></data>
|
||||||
|
<data name="Index_PdfExport" xml:space="preserve"><value>PDF export</value></data>
|
||||||
|
<data name="Index_SecureAccess" xml:space="preserve"><value>Secure access</value></data>
|
||||||
|
<data name="Index_Subtitle" xml:space="preserve"><value>Welcome to the eSign portal</value></data>
|
||||||
|
<data name="Index_ToEnvelopeOverview" xml:space="preserve"><value>Go to envelope overview</value></data>
|
||||||
|
<data name="Languages" xml:space="preserve"><value>Languages</value></data>
|
||||||
|
<data name="Loading" xml:space="preserve"><value>Loading...</value></data>
|
||||||
|
<data name="Login" xml:space="preserve"><value>Sign in</value></data>
|
||||||
|
<data name="LoginInProgress" xml:space="preserve"><value>Signing in ...</value></data>
|
||||||
|
<data name="NotAuthenticatedPleaseLoginAgain" xml:space="preserve"><value>Not authenticated. Please sign in again.</value></data>
|
||||||
|
<data name="Only" xml:space="preserve"><value>Only</value></data>
|
||||||
|
<data name="OpenDocument" xml:space="preserve"><value>Open document</value></data>
|
||||||
|
<data name="Optional" xml:space="preserve"><value>optional</value></data>
|
||||||
|
<data name="Pending" xml:space="preserve"><value>Pending</value></data>
|
||||||
|
<data name="Preview" xml:space="preserve"><value>Preview</value></data>
|
||||||
|
<data name="PrivacyPolicy" xml:space="preserve"><value>Privacy policy</value></data>
|
||||||
|
<data name="Receiver" xml:space="preserve"><value>Receiver</value></data>
|
||||||
|
<data name="ReceiverLogin_AccessCode_Placeholder" xml:space="preserve"><value>Enter access code</value></data>
|
||||||
|
<data name="ReceiverLogin_Description" xml:space="preserve"><value>Please enter the access code you received by email to securely open the document.</value></data>
|
||||||
|
<data name="ReceiverLogin_FooterHelp" xml:space="preserve"><value>If you have issues, please contact the document sender.</value></data>
|
||||||
|
<data name="ReceiverLogin_InvalidCode_Body" xml:space="preserve"><value>The entered code is incorrect. Please try again.</value></data>
|
||||||
|
<data name="ReceiverLogin_InvalidCode_Title" xml:space="preserve"><value>Invalid access code.</value></data>
|
||||||
|
<data name="ReceiverLogin_NotFound_Body" xml:space="preserve"><value>The provided access code could not be matched to a document. Please verify the link in your email.</value></data>
|
||||||
|
<data name="ReceiverLogin_NotFound_Title" xml:space="preserve"><value>Document not found.</value></data>
|
||||||
|
<data name="ReceiverLogin_Subtitle" xml:space="preserve"><value>Secure access with access code</value></data>
|
||||||
|
<data name="ReceiverSigned_ConfirmBody" xml:space="preserve"><value>This action cannot be undone. By confirming, you declare that you have electronically signed the document shown above. The signed document will then be delivered to all involved parties.</value></data>
|
||||||
|
<data name="ReceiverSigned_ConfirmQuestion" xml:space="preserve"><value>Do you want to sign this document as binding?</value></data>
|
||||||
|
<data name="ReceiverSigned_EnvelopeKeyMissing" xml:space="preserve"><value>Envelope key is missing.</value></data>
|
||||||
|
<data name="Refresh" xml:space="preserve"><value>Refresh</value></data>
|
||||||
|
<data name="RequiredField" xml:space="preserve"><value>Required field</value></data>
|
||||||
|
<data name="Selected" xml:space="preserve"><value>selected</value></data>
|
||||||
|
<data name="Send" xml:space="preserve"><value>Send</value></data>
|
||||||
|
<data name="Sender_ActiveEnvelopes" xml:space="preserve"><value>Active envelopes</value></data>
|
||||||
|
<data name="Sender_CompletedEnvelopes" xml:space="preserve"><value>Completed envelopes</value></data>
|
||||||
|
<data name="Sender_CreateEnvelope" xml:space="preserve"><value>New envelope</value></data>
|
||||||
|
<data name="Sender_CreateEnvelope_Title" xml:space="preserve"><value>Create new envelope</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_Header" xml:space="preserve"><value>Delete envelope</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonPlaceholder" xml:space="preserve"><value>Enter reason...</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonPrompt" xml:space="preserve"><value>Please provide a reason why the envelope should be deleted/withdrawn.</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonRequired" xml:space="preserve"><value>Please provide a deletion reason.</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_Title" xml:space="preserve"><value>Delete selected envelope</value></data>
|
||||||
|
<data name="Sender_Edit" xml:space="preserve"><value>Edit</value></data>
|
||||||
|
<data name="Sender_EditEnvelope_Title" xml:space="preserve"><value>Edit selected envelope</value></data>
|
||||||
|
<data name="Sender_ExportCurrentView_Title" xml:space="preserve"><value>Export current view</value></data>
|
||||||
|
<data name="Sender_ExportFormat_Aria" xml:space="preserve"><value>Select export format</value></data>
|
||||||
|
<data name="Sender_ExportView_Title" xml:space="preserve"><value>Export current table view</value></data>
|
||||||
|
<data name="Sender_LoadError_Title" xml:space="preserve"><value>Error loading envelopes</value></data>
|
||||||
|
<data name="Sender_LoadingEnvelopes" xml:space="preserve"><value>Loading envelopes...</value></data>
|
||||||
|
<data name="Sender_NoHistory" xml:space="preserve"><value>No history available</value></data>
|
||||||
|
<data name="Sender_NoReceivers" xml:space="preserve"><value>No receivers</value></data>
|
||||||
|
<data name="Sender_Preview_Title" xml:space="preserve"><value>Show selected envelope as PDF</value></data>
|
||||||
|
<data name="Sender_PreviewDocumentNotFound" xml:space="preserve"><value>The document for envelope #{0} was not found or you do not have access.</value></data>
|
||||||
|
<data name="SenderEditor_AddSignature" xml:space="preserve"><value>Add signature</value></data>
|
||||||
|
<data name="SenderEditor_ClickDocumentFor" xml:space="preserve"><value>Click on the location in the document for</value></data>
|
||||||
|
<data name="SenderEditor_EmailSuggestionsHint" xml:space="preserve"><value>Previously used email addresses are suggested while typing.</value></data>
|
||||||
|
<data name="SenderEditor_EmailSuggestionsLoading" xml:space="preserve"><value>Loading email suggestions...</value></data>
|
||||||
|
<data name="SenderEditor_ErrorCreatingEnvelope" xml:space="preserve"><value>Error creating envelope</value></data>
|
||||||
|
<data name="SenderEditor_LoadEnvelopeError" xml:space="preserve"><value>Error loading envelope #{0}: {1}</value></data>
|
||||||
|
<data name="SenderEditor_LoadFileError" xml:space="preserve"><value>Error loading file: {0}</value></data>
|
||||||
|
<data name="SenderEditor_Message_Placeholder" xml:space="preserve"><value>Please sign the attached document.</value></data>
|
||||||
|
<data name="SenderEditor_NoDocumentLoaded" xml:space="preserve"><value>No document loaded</value></data>
|
||||||
|
<data name="SenderEditor_NoPagesToDisplay" xml:space="preserve"><value>No pages to display.</value></data>
|
||||||
|
<data name="SenderEditor_NoReceiversAdded" xml:space="preserve"><value>No receivers added yet.</value></data>
|
||||||
|
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Page</value></data>
|
||||||
|
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Place signature field</value></data>
|
||||||
|
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Remove all signature fields</value></data>
|
||||||
|
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Please select a PDF file.</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>The existing draft has been sent.</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Envelope sent</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>Envelope was sent successfully.</value></data>
|
||||||
|
<data name="SenderEditor_SignatureField" xml:space="preserve"><value>Signature field</value></data>
|
||||||
|
<data name="SenderEditor_Title_Placeholder" xml:space="preserve"><value>e.g. Employment contract 2025</value></data>
|
||||||
|
<data name="SenderEditor_ToOverview" xml:space="preserve"><value>To overview</value></data>
|
||||||
|
<data name="SenderEditor_UploadPdfToPlaceFields" xml:space="preserve"><value>Upload a PDF file to place signature fields.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailDuplicate" xml:space="preserve"><value>This email address has already been added.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailInvalid" xml:space="preserve"><value>Please enter a valid email address.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailRequired" xml:space="preserve"><value>Please enter an email address.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_FullNameRequired" xml:space="preserve"><value>Please enter first and last name.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_ReceiverRequired" xml:space="preserve"><value>Please add at least one receiver.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_SignatureFieldRequired" xml:space="preserve"><value>Please place at least one signature field.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_TitleRequired" xml:space="preserve"><value>Please enter a title.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_UploadPdf" xml:space="preserve"><value>Please upload a PDF document first.</value></data>
|
||||||
|
<data name="SenderLogin_Description" xml:space="preserve"><value>Please sign in with your credentials to access the sender dashboard.</value></data>
|
||||||
|
<data name="SenderLogin_FooterHelp" xml:space="preserve"><value>If you have issues, please contact the administrator.</value></data>
|
||||||
|
<data name="SenderLogin_InvalidCredentials_Body" xml:space="preserve"><value>Username or password is incorrect. Please try again.</value></data>
|
||||||
|
<data name="SenderLogin_InvalidCredentials_Title" xml:space="preserve"><value>Invalid credentials.</value></data>
|
||||||
|
<data name="SenderLogin_Password_Placeholder" xml:space="preserve"><value>Enter password</value></data>
|
||||||
|
<data name="SenderLogin_Subtitle" xml:space="preserve"><value>Secure access to sender dashboard</value></data>
|
||||||
|
<data name="SenderLogin_Title" xml:space="preserve"><value>Sender login</value></data>
|
||||||
|
<data name="SenderLogin_Username_Placeholder" xml:space="preserve"><value>Enter username</value></data>
|
||||||
|
<data name="ServerError" xml:space="preserve"><value>Server error.</value></data>
|
||||||
|
<data name="Signed" xml:space="preserve"><value>Signed</value></data>
|
||||||
|
<data name="SignedDocument" xml:space="preserve"><value>Signed document</value></data>
|
||||||
|
<data name="System" xml:space="preserve"><value>System</value></data>
|
||||||
|
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>An unexpected error occurred. Please try again later.</value></data>
|
||||||
|
<data name="UploadPdf" xml:space="preserve"><value>Upload PDF</value></data>
|
||||||
|
<data name="UserName" xml:space="preserve"><value>Username</value></data>
|
||||||
|
<data name="VerifyInProgress" xml:space="preserve"><value>Verifying ...</value></data>
|
||||||
|
</root>
|
||||||
|
|||||||
@@ -477,4 +477,299 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Confirmations</value>
|
<value>Confirmations</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
<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>
|
||||||
|
<data name="ChangedAt" xml:space="preserve"><value>Modifié le</value></data>
|
||||||
|
<data name="Close" xml:space="preserve"><value>Fermer</value></data>
|
||||||
|
<data name="CloseLanguageMenu" xml:space="preserve"><value>Fermer le menu des langues</value></data>
|
||||||
|
<data name="ConfirmSignature" xml:space="preserve"><value>Confirmer la signature</value></data>
|
||||||
|
<data name="Created" xml:space="preserve"><value>Créé</value></data>
|
||||||
|
<data name="Delete" xml:space="preserve"><value>Supprimer</value></data>
|
||||||
|
<data name="Document" xml:space="preserve"><value>Document</value></data>
|
||||||
|
<data name="DocumentCouldNotBeLoaded" xml:space="preserve"><value>Le document n'a pas pu être chargé</value></data>
|
||||||
|
<data name="DocumentLoading" xml:space="preserve"><value>Chargement du document...</value></data>
|
||||||
|
<data name="EmailAddress" xml:space="preserve"><value>Adresse e-mail</value></data>
|
||||||
|
<data name="Envelope" xml:space="preserve"><value>Enveloppe</value></data>
|
||||||
|
<data name="Error" xml:space="preserve"><value>Erreur</value></data>
|
||||||
|
<data name="ErrorLoadingDocument_WithReason" xml:space="preserve"><value>Erreur lors du chargement du document : {0}</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode" xml:space="preserve"><value>Mode développement</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Detail" xml:space="preserve"><value>Cela peut afficher des informations sensibles d'exception aux utilisateurs finaux. Pour le débogage local, définissez ASPNETCORE_ENVIRONMENT sur Development et redémarrez l'application.</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Info" xml:space="preserve"><value>Le mode développement affiche des informations d'erreur plus détaillées.</value></data>
|
||||||
|
<data name="ErrorPage_DevelopmentMode_Warning" xml:space="preserve"><value>Le mode développement ne doit pas être activé dans les applications déployées.</value></data>
|
||||||
|
<data name="ErrorPage_ProcessingRequest" xml:space="preserve"><value>Une erreur s'est produite lors du traitement de votre demande.</value></data>
|
||||||
|
<data name="ErrorPage_RequestId" xml:space="preserve"><value>ID de requête</value></data>
|
||||||
|
<data name="ErrorWithReason" xml:space="preserve"><value>Erreur : {0}</value></data>
|
||||||
|
<data name="From" xml:space="preserve"><value>De</value></data>
|
||||||
|
<data name="FullName" xml:space="preserve"><value>Nom et prénom</value></data>
|
||||||
|
<data name="History" xml:space="preserve"><value>Historique</value></data>
|
||||||
|
<data name="Index_DigitalSignature" xml:space="preserve"><value>Signature numérique</value></data>
|
||||||
|
<data name="Index_PdfExport" xml:space="preserve"><value>Export PDF</value></data>
|
||||||
|
<data name="Index_SecureAccess" xml:space="preserve"><value>Accès sécurisé</value></data>
|
||||||
|
<data name="Index_Subtitle" xml:space="preserve"><value>Bienvenue sur le portail eSign</value></data>
|
||||||
|
<data name="Index_ToEnvelopeOverview" xml:space="preserve"><value>Aller à l'aperçu des enveloppes</value></data>
|
||||||
|
<data name="Languages" xml:space="preserve"><value>Langues</value></data>
|
||||||
|
<data name="Loading" xml:space="preserve"><value>Chargement...</value></data>
|
||||||
|
<data name="Login" xml:space="preserve"><value>Connexion</value></data>
|
||||||
|
<data name="LoginInProgress" xml:space="preserve"><value>Connexion ...</value></data>
|
||||||
|
<data name="NotAuthenticatedPleaseLoginAgain" xml:space="preserve"><value>Non authentifié. Veuillez vous reconnecter.</value></data>
|
||||||
|
<data name="Only" xml:space="preserve"><value>Uniquement</value></data>
|
||||||
|
<data name="OpenDocument" xml:space="preserve"><value>Ouvrir le document</value></data>
|
||||||
|
<data name="Optional" xml:space="preserve"><value>optionnel</value></data>
|
||||||
|
<data name="Pending" xml:space="preserve"><value>En attente</value></data>
|
||||||
|
<data name="Preview" xml:space="preserve"><value>Aperçu</value></data>
|
||||||
|
<data name="PrivacyPolicy" xml:space="preserve"><value>Politique de confidentialité</value></data>
|
||||||
|
<data name="Receiver" xml:space="preserve"><value>Destinataire</value></data>
|
||||||
|
<data name="ReceiverLogin_AccessCode_Placeholder" xml:space="preserve"><value>Saisir le code d'accès</value></data>
|
||||||
|
<data name="ReceiverLogin_Description" xml:space="preserve"><value>Veuillez saisir le code d'accès reçu par e-mail pour ouvrir le document en toute sécurité.</value></data>
|
||||||
|
<data name="ReceiverLogin_FooterHelp" xml:space="preserve"><value>En cas de problème, veuillez contacter l'expéditeur du document.</value></data>
|
||||||
|
<data name="ReceiverLogin_InvalidCode_Body" xml:space="preserve"><value>Le code saisi est incorrect. Veuillez réessayer.</value></data>
|
||||||
|
<data name="ReceiverLogin_InvalidCode_Title" xml:space="preserve"><value>Code d'accès invalide.</value></data>
|
||||||
|
<data name="ReceiverLogin_NotFound_Body" xml:space="preserve"><value>Le code d'accès indiqué ne correspond à aucun document. Veuillez vérifier le lien dans votre e-mail.</value></data>
|
||||||
|
<data name="ReceiverLogin_NotFound_Title" xml:space="preserve"><value>Document introuvable.</value></data>
|
||||||
|
<data name="ReceiverLogin_Subtitle" xml:space="preserve"><value>Accès sécurisé avec code d'accès</value></data>
|
||||||
|
<data name="ReceiverSigned_ConfirmBody" xml:space="preserve"><value>Cette action est irréversible. En confirmant, vous déclarez avoir signé électroniquement le document affiché ci-dessus. Le document signé sera ensuite transmis à toutes les parties concernées.</value></data>
|
||||||
|
<data name="ReceiverSigned_ConfirmQuestion" xml:space="preserve"><value>Voulez-vous signer ce document de manière contraignante ?</value></data>
|
||||||
|
<data name="ReceiverSigned_EnvelopeKeyMissing" xml:space="preserve"><value>La clé d'enveloppe est manquante.</value></data>
|
||||||
|
<data name="Refresh" xml:space="preserve"><value>Actualiser</value></data>
|
||||||
|
<data name="RequiredField" xml:space="preserve"><value>Champ obligatoire</value></data>
|
||||||
|
<data name="Selected" xml:space="preserve"><value>sélectionnées</value></data>
|
||||||
|
<data name="Send" xml:space="preserve"><value>Envoyer</value></data>
|
||||||
|
<data name="Sender_ActiveEnvelopes" xml:space="preserve"><value>Enveloppes actives</value></data>
|
||||||
|
<data name="Sender_CompletedEnvelopes" xml:space="preserve"><value>Enveloppes terminées</value></data>
|
||||||
|
<data name="Sender_CreateEnvelope" xml:space="preserve"><value>Nouvelle enveloppe</value></data>
|
||||||
|
<data name="Sender_CreateEnvelope_Title" xml:space="preserve"><value>Créer une nouvelle enveloppe</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_Header" xml:space="preserve"><value>Supprimer l'enveloppe</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonPlaceholder" xml:space="preserve"><value>Saisir la raison...</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonPrompt" xml:space="preserve"><value>Veuillez indiquer la raison pour laquelle l'enveloppe doit être supprimée/retirée.</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_ReasonRequired" xml:space="preserve"><value>Veuillez indiquer un motif de suppression.</value></data>
|
||||||
|
<data name="Sender_DeleteEnvelope_Title" xml:space="preserve"><value>Supprimer l'enveloppe sélectionnée</value></data>
|
||||||
|
<data name="Sender_Edit" xml:space="preserve"><value>Modifier</value></data>
|
||||||
|
<data name="Sender_EditEnvelope_Title" xml:space="preserve"><value>Modifier l'enveloppe sélectionnée</value></data>
|
||||||
|
<data name="Sender_ExportCurrentView_Title" xml:space="preserve"><value>Exporter la vue actuelle</value></data>
|
||||||
|
<data name="Sender_ExportFormat_Aria" xml:space="preserve"><value>Sélectionner le format d'export</value></data>
|
||||||
|
<data name="Sender_ExportView_Title" xml:space="preserve"><value>Exporter la vue actuelle du tableau</value></data>
|
||||||
|
<data name="Sender_LoadError_Title" xml:space="preserve"><value>Erreur lors du chargement des enveloppes</value></data>
|
||||||
|
<data name="Sender_LoadingEnvelopes" xml:space="preserve"><value>Chargement des enveloppes...</value></data>
|
||||||
|
<data name="Sender_NoHistory" xml:space="preserve"><value>Aucun historique disponible</value></data>
|
||||||
|
<data name="Sender_NoReceivers" xml:space="preserve"><value>Aucun destinataire</value></data>
|
||||||
|
<data name="Sender_Preview_Title" xml:space="preserve"><value>Afficher l'enveloppe sélectionnée en PDF</value></data>
|
||||||
|
<data name="Sender_PreviewDocumentNotFound" xml:space="preserve"><value>Le document de l'enveloppe #{0} est introuvable ou vous n'avez pas l'autorisation d'y accéder.</value></data>
|
||||||
|
<data name="SenderEditor_AddSignature" xml:space="preserve"><value>Ajouter une signature</value></data>
|
||||||
|
<data name="SenderEditor_ClickDocumentFor" xml:space="preserve"><value>Cliquez sur l'emplacement dans le document pour</value></data>
|
||||||
|
<data name="SenderEditor_EmailSuggestionsHint" xml:space="preserve"><value>Les adresses e-mail déjà utilisées sont proposées pendant la saisie.</value></data>
|
||||||
|
<data name="SenderEditor_EmailSuggestionsLoading" xml:space="preserve"><value>Chargement des suggestions d'e-mail...</value></data>
|
||||||
|
<data name="SenderEditor_ErrorCreatingEnvelope" xml:space="preserve"><value>Erreur lors de la création de l'enveloppe</value></data>
|
||||||
|
<data name="SenderEditor_LoadEnvelopeError" xml:space="preserve"><value>Erreur lors du chargement de l'enveloppe #{0} : {1}</value></data>
|
||||||
|
<data name="SenderEditor_LoadFileError" xml:space="preserve"><value>Erreur lors du chargement du fichier : {0}</value></data>
|
||||||
|
<data name="SenderEditor_Message_Placeholder" xml:space="preserve"><value>Veuillez signer le document ci-joint.</value></data>
|
||||||
|
<data name="SenderEditor_NoDocumentLoaded" xml:space="preserve"><value>Aucun document chargé</value></data>
|
||||||
|
<data name="SenderEditor_NoPagesToDisplay" xml:space="preserve"><value>Aucune page à afficher.</value></data>
|
||||||
|
<data name="SenderEditor_NoReceiversAdded" xml:space="preserve"><value>Aucun destinataire ajouté pour le moment.</value></data>
|
||||||
|
<data name="SenderEditor_PageLabel" xml:space="preserve"><value>Page</value></data>
|
||||||
|
<data name="SenderEditor_PlaceSignatureField" xml:space="preserve"><value>Placer un champ de signature</value></data>
|
||||||
|
<data name="SenderEditor_RemoveAllSignatureFields" xml:space="preserve"><value>Supprimer tous les champs de signature</value></data>
|
||||||
|
<data name="SenderEditor_SelectPdf" xml:space="preserve"><value>Veuillez sélectionner un fichier PDF.</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Body" xml:space="preserve"><value>Le brouillon existant a été envoyé.</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Header" xml:space="preserve"><value>Enveloppe envoyée</value></data>
|
||||||
|
<data name="SenderEditor_Sent_Title" xml:space="preserve"><value>L'enveloppe a été envoyée avec succès.</value></data>
|
||||||
|
<data name="SenderEditor_SignatureField" xml:space="preserve"><value>Champ de signature</value></data>
|
||||||
|
<data name="SenderEditor_Title_Placeholder" xml:space="preserve"><value>p. ex. Contrat de travail 2025</value></data>
|
||||||
|
<data name="SenderEditor_ToOverview" xml:space="preserve"><value>Vers l'aperçu</value></data>
|
||||||
|
<data name="SenderEditor_UploadPdfToPlaceFields" xml:space="preserve"><value>Téléversez un fichier PDF pour placer des champs de signature.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailDuplicate" xml:space="preserve"><value>Cette adresse e-mail a déjà été ajoutée.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailInvalid" xml:space="preserve"><value>Veuillez saisir une adresse e-mail valide.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_EmailRequired" xml:space="preserve"><value>Veuillez saisir une adresse e-mail.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_FullNameRequired" xml:space="preserve"><value>Veuillez saisir le nom et le prénom.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_ReceiverRequired" xml:space="preserve"><value>Veuillez ajouter au moins un destinataire.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_SignatureFieldRequired" xml:space="preserve"><value>Veuillez placer au moins un champ de signature.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_TitleRequired" xml:space="preserve"><value>Veuillez saisir un titre.</value></data>
|
||||||
|
<data name="SenderEditor_Validation_UploadPdf" xml:space="preserve"><value>Veuillez d'abord téléverser un document PDF.</value></data>
|
||||||
|
<data name="SenderLogin_Description" xml:space="preserve"><value>Veuillez vous connecter avec vos identifiants pour accéder au tableau de bord expéditeur.</value></data>
|
||||||
|
<data name="SenderLogin_FooterHelp" xml:space="preserve"><value>En cas de problème, veuillez contacter l'administrateur.</value></data>
|
||||||
|
<data name="SenderLogin_InvalidCredentials_Body" xml:space="preserve"><value>Le nom d'utilisateur ou le mot de passe est incorrect. Veuillez réessayer.</value></data>
|
||||||
|
<data name="SenderLogin_InvalidCredentials_Title" xml:space="preserve"><value>Identifiants invalides.</value></data>
|
||||||
|
<data name="SenderLogin_Password_Placeholder" xml:space="preserve"><value>Saisir le mot de passe</value></data>
|
||||||
|
<data name="SenderLogin_Subtitle" xml:space="preserve"><value>Accès sécurisé au tableau de bord expéditeur</value></data>
|
||||||
|
<data name="SenderLogin_Title" xml:space="preserve"><value>Connexion expéditeur</value></data>
|
||||||
|
<data name="SenderLogin_Username_Placeholder" xml:space="preserve"><value>Saisir le nom d'utilisateur</value></data>
|
||||||
|
<data name="ServerError" xml:space="preserve"><value>Erreur serveur.</value></data>
|
||||||
|
<data name="Signed" xml:space="preserve"><value>Signé</value></data>
|
||||||
|
<data name="SignedDocument" xml:space="preserve"><value>Document signé</value></data>
|
||||||
|
<data name="System" xml:space="preserve"><value>Système</value></data>
|
||||||
|
<data name="UnexpectedErrorTryAgainLater" xml:space="preserve"><value>Une erreur inattendue s'est produite. Veuillez réessayer plus tard.</value></data>
|
||||||
|
<data name="UploadPdf" xml:space="preserve"><value>Téléverser un PDF</value></data>
|
||||||
|
<data name="UserName" xml:space="preserve"><value>Nom d'utilisateur</value></data>
|
||||||
|
<data name="VerifyInProgress" xml:space="preserve"><value>Vérification ...</value></data>
|
||||||
|
</root>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using DigitalData.Core.Application;
|
using DigitalData.Core.Application;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Services;
|
namespace EnvelopeGenerator.Application.Services;
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ namespace EnvelopeGenerator.Application.Services;
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Obsolete("Use MediatR")]
|
[Obsolete("Use MediatR")]
|
||||||
public class DocumentReceiverElementService : BasicCRUDService<IDocumentReceiverElementRepository, SignatureDto, Signature, int>, IDocumentReceiverElementService
|
public class DocumentReceiverElementService : BasicCRUDService<IDocumentReceiverElementRepository, DocReceiverElementDto, DocReceiverElement, int>, IDocumentReceiverElementService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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 Signature)()
|
|
||||||
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()}"
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Return pSourceBuffer
|
Return pSourceBuffer
|
||||||
End If
|
End If
|
||||||
|
|
||||||
Dim sigRepo = scope.ServiceProvider.Repository(Of Signature)()
|
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
|
||||||
Dim elements = sigRepo _
|
Dim elements = sigRepo _
|
||||||
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
||||||
.Include(Function(sig) sig.Annotations) _
|
.Include(Function(sig) sig.Annotations) _
|
||||||
@@ -58,7 +58,7 @@ Namespace Jobs.FinalizeDocument
|
|||||||
End Using
|
End Using
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function BurnElementAnnotsToPDF(pSourceBuffer As Byte(), elements As List(Of Signature)) As Byte()
|
Public Function BurnElementAnnotsToPDF(pSourceBuffer As Byte(), elements As List(Of DocReceiverElement)) As Byte()
|
||||||
' Add background
|
' Add background
|
||||||
Using doc As Pdf(Of MemoryStream, MemoryStream) = Pdf.FromMemory(pSourceBuffer)
|
Using doc As Pdf(Of MemoryStream, MemoryStream) = Pdf.FromMemory(pSourceBuffer)
|
||||||
'TODO: take the length from the largest y
|
'TODO: take the length from the largest y
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ Public Class ElementModel
|
|||||||
MyBase.New(pState)
|
MyBase.New(pState)
|
||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Private Function ToElement(pRow As DataRow) As Signature
|
Private Function ToElement(pRow As DataRow) As DocReceiverElement
|
||||||
Return New Signature() With {
|
Return New DocReceiverElement() With {
|
||||||
.Id = pRow.ItemEx("GUID", 0),
|
.Id = pRow.ItemEx("GUID", 0),
|
||||||
.DocumentId = pRow.ItemEx("DOCUMENT_ID", 0),
|
.DocumentId = pRow.ItemEx("DOCUMENT_ID", 0),
|
||||||
.ReceiverId = pRow.ItemEx("RECEIVER_ID", 0),
|
.ReceiverId = pRow.ItemEx("RECEIVER_ID", 0),
|
||||||
@@ -24,7 +24,7 @@ Public Class ElementModel
|
|||||||
}
|
}
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Private Function ToElements(pTable As DataTable) As List(Of Signature)
|
Private Function ToElements(pTable As DataTable) As List(Of DocReceiverElement)
|
||||||
Return pTable?.Rows.Cast(Of DataRow).
|
Return pTable?.Rows.Cast(Of DataRow).
|
||||||
Select(AddressOf ToElement).
|
Select(AddressOf ToElement).
|
||||||
ToList()
|
ToList()
|
||||||
@@ -80,7 +80,7 @@ Public Class ElementModel
|
|||||||
End Try
|
End Try
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function List(pDocumentId As Integer) As List(Of Signature)
|
Public Function List(pDocumentId As Integer) As List(Of DocReceiverElement)
|
||||||
Try
|
Try
|
||||||
Dim oSql = $"SELECT * FROM [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT] WHERE DOCUMENT_ID = {pDocumentId} ORDER BY PAGE ASC"
|
Dim oSql = $"SELECT * FROM [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT] WHERE DOCUMENT_ID = {pDocumentId} ORDER BY PAGE ASC"
|
||||||
Dim oTable = Database.GetDatatable(oSql)
|
Dim oTable = Database.GetDatatable(oSql)
|
||||||
@@ -93,7 +93,7 @@ Public Class ElementModel
|
|||||||
End Try
|
End Try
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function List(pDocumentId As Integer, pReceiverId As Integer) As List(Of Signature)
|
Public Function List(pDocumentId As Integer, pReceiverId As Integer) As List(Of DocReceiverElement)
|
||||||
Try
|
Try
|
||||||
Dim oReceiverConstraint = ""
|
Dim oReceiverConstraint = ""
|
||||||
If pReceiverId > 0 Then
|
If pReceiverId > 0 Then
|
||||||
@@ -111,7 +111,7 @@ Public Class ElementModel
|
|||||||
End Try
|
End Try
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function Insert(pElement As Signature) As Boolean
|
Public Function Insert(pElement As DocReceiverElement) As Boolean
|
||||||
Try
|
Try
|
||||||
Dim oSql = "INSERT INTO [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT]
|
Dim oSql = "INSERT INTO [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT]
|
||||||
([DOCUMENT_ID]
|
([DOCUMENT_ID]
|
||||||
@@ -161,7 +161,7 @@ Public Class ElementModel
|
|||||||
End Try
|
End Try
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function Update(pElement As Signature) As Boolean
|
Public Function Update(pElement As DocReceiverElement) As Boolean
|
||||||
Try
|
Try
|
||||||
Dim oSql = "UPDATE [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT]
|
Dim oSql = "UPDATE [dbo].[TBSIG_DOCUMENT_RECEIVER_ELEMENT]
|
||||||
SET [POSITION_X] = @POSITION_X
|
SET [POSITION_X] = @POSITION_X
|
||||||
@@ -185,7 +185,7 @@ Public Class ElementModel
|
|||||||
End Try
|
End Try
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Private Function GetElementId(pElement As Signature) As Integer
|
Private Function GetElementId(pElement As DocReceiverElement) As Integer
|
||||||
Try
|
Try
|
||||||
Return Database.GetScalarValue($"SELECT MAX(GUID) FROM TBSIG_DOCUMENT_RECEIVER_ELEMENT
|
Return Database.GetScalarValue($"SELECT MAX(GUID) FROM TBSIG_DOCUMENT_RECEIVER_ELEMENT
|
||||||
WHERE DOCUMENT_ID = {pElement.DocumentId} AND RECEIVER_ID = {pElement.ReceiverId}")
|
WHERE DOCUMENT_ID = {pElement.DocumentId} AND RECEIVER_ID = {pElement.ReceiverId}")
|
||||||
@@ -196,7 +196,7 @@ Public Class ElementModel
|
|||||||
End Try
|
End Try
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function DeleteElement(pElement As Signature) As Boolean
|
Public Function DeleteElement(pElement As DocReceiverElement) As Boolean
|
||||||
Try
|
Try
|
||||||
Dim oSql = $"DELETE FROM TBSIG_DOCUMENT_RECEIVER_ELEMENT WHERE GUID = {pElement.Id}"
|
Dim oSql = $"DELETE FROM TBSIG_DOCUMENT_RECEIVER_ELEMENT WHERE GUID = {pElement.Id}"
|
||||||
Return Database.ExecuteNonQuery(oSql)
|
Return Database.ExecuteNonQuery(oSql)
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ using System.Collections.Generic;
|
|||||||
namespace EnvelopeGenerator.Domain.Entities
|
namespace EnvelopeGenerator.Domain.Entities
|
||||||
{
|
{
|
||||||
[Table("TBSIG_DOCUMENT_RECEIVER_ELEMENT", Schema = "dbo")]
|
[Table("TBSIG_DOCUMENT_RECEIVER_ELEMENT", Schema = "dbo")]
|
||||||
public class Signature : ISignature, IHasReceiver, IHasAddedWhen, IUpdateAuditable
|
public class DocReceiverElement : IDocReceiverElement, IHasReceiver, IHasAddedWhen, IUpdateAuditable
|
||||||
{
|
{
|
||||||
public Signature()
|
public DocReceiverElement()
|
||||||
{
|
{
|
||||||
// TODO: * Check the Form App and remove the default value
|
// TODO: * Check the Form App and remove the default value
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
@@ -126,5 +126,33 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
[NotMapped]
|
[NotMapped]
|
||||||
public double Left => Math.Round(X, 5);
|
public double Left => Math.Round(X, 5);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
[Column("FULL_NAME", TypeName = "nvarchar(100)")]
|
||||||
|
public string
|
||||||
|
#if nullable
|
||||||
|
?
|
||||||
|
# endif
|
||||||
|
FullName { get; set; }
|
||||||
|
|
||||||
|
[Column("POSITION", TypeName = "nvarchar(100)")]
|
||||||
|
public string
|
||||||
|
#if nullable
|
||||||
|
?
|
||||||
|
#endif
|
||||||
|
Position { get; set; }
|
||||||
|
|
||||||
|
[Column("PLACE", TypeName = "nvarchar(100)")]
|
||||||
|
public string
|
||||||
|
#if nullable
|
||||||
|
?
|
||||||
|
#endif
|
||||||
|
Place { get; set; }
|
||||||
|
|
||||||
|
[Column("INK", TypeName = "varbinary(MAX)")]
|
||||||
|
public byte[]
|
||||||
|
#if nullable
|
||||||
|
?
|
||||||
|
#endif
|
||||||
|
Ink { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public Document()
|
public Document()
|
||||||
{
|
{
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
Elements = Enumerable.Empty<Signature>().ToList();
|
Elements = Enumerable.Empty<DocReceiverElement>().ToList();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
FileNameOriginal { get; set; }
|
FileNameOriginal { get; set; }
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public virtual List<Signature>
|
public virtual List<DocReceiverElement>
|
||||||
#if nullable
|
#if nullable
|
||||||
?
|
?
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
ChangedWho { get; set; }
|
ChangedWho { get; set; }
|
||||||
|
|
||||||
[ForeignKey("ElementId")]
|
[ForeignKey("ElementId")]
|
||||||
public virtual Signature
|
public virtual DocReceiverElement
|
||||||
#if nullable
|
#if nullable
|
||||||
?
|
?
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public string Language { get; set; }
|
public string Language { get; set; }
|
||||||
|
|
||||||
[Column("SEND_REMINDER_EMAILS")]
|
[Column("SEND_REMINDER_EMAILS")]
|
||||||
public bool SendReminderEmails { get; set; }
|
public bool? SendReminderEmails { get; set; }
|
||||||
|
|
||||||
[Column("FIRST_REMINDER_DAYS")]
|
[Column("FIRST_REMINDER_DAYS")]
|
||||||
public int? FirstReminderDays { get; set; }
|
public int? FirstReminderDays { get; set; }
|
||||||
@@ -114,7 +114,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public int? CertificationType { get; set; }
|
public int? CertificationType { get; set; }
|
||||||
|
|
||||||
[Column("USE_ACCESS_CODE")]
|
[Column("USE_ACCESS_CODE")]
|
||||||
public bool UseAccessCode { get; set; }
|
public bool? UseAccessCode { get; set; }
|
||||||
|
|
||||||
[Column("FINAL_EMAIL_TO_CREATOR")]
|
[Column("FINAL_EMAIL_TO_CREATOR")]
|
||||||
public int? FinalEmailToCreator { get; set; }
|
public int? FinalEmailToCreator { get; set; }
|
||||||
@@ -132,7 +132,7 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public User User { get; set; }
|
public User User { get; set; }
|
||||||
|
|
||||||
[Column("TFA_ENABLED")]
|
[Column("TFA_ENABLED")]
|
||||||
public bool TfaEnabled { get; set; }
|
public bool? TfaEnabled { get; set; }
|
||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
= false;
|
= false;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -22,41 +22,41 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
public string Language { get; set; }
|
public string Language { get; set; }
|
||||||
|
|
||||||
[Column("EXPIRES_DAYS")]
|
[Column("EXPIRES_DAYS")]
|
||||||
public int ExpiresDays { get; set; }
|
public int? ExpiresDays { get; set; }
|
||||||
|
|
||||||
[Column("CERTIFICATION_TYPE")]
|
[Column("CERTIFICATION_TYPE")]
|
||||||
public Constants.CertificationType CertificationType { get; set; } = Constants.CertificationType.AdvancedElectronicSignature;
|
public Constants.CertificationType? CertificationType { get; set; }
|
||||||
|
|
||||||
[Column("USE_ACCESS_CODE")]
|
[Column("USE_ACCESS_CODE")]
|
||||||
public bool UseAccessCode { get; set; }
|
public bool? UseAccessCode { get; set; }
|
||||||
|
|
||||||
[Column("FINAL_EMAIL_TO_CREATOR")]
|
[Column("FINAL_EMAIL_TO_CREATOR")]
|
||||||
public Constants.FinalEmailType FinalEmailToCreator { get; set; }
|
public Constants.FinalEmailType? FinalEmailToCreator { get; set; }
|
||||||
|
|
||||||
[Column("FINAL_EMAIL_TO_RECEIVERS")]
|
[Column("FINAL_EMAIL_TO_RECEIVERS")]
|
||||||
public Constants.FinalEmailType FinalEmailToReceivers { get; set; }
|
public Constants.FinalEmailType? FinalEmailToReceivers { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column("ADDED_WHEN", TypeName = "datetime")]
|
[Column("ADDED_WHEN", TypeName = "datetime")]
|
||||||
public DateTime AddedWhen { get; set; }
|
public DateTime AddedWhen { get; set; }
|
||||||
|
|
||||||
[Column("CHANGED_WHEN", TypeName = "datetime")]
|
[Column("CHANGED_WHEN", TypeName = "datetime")]
|
||||||
public DateTime ChangedWhen { get; set; }
|
public DateTime? ChangedWhen { get; set; }
|
||||||
|
|
||||||
[Column("EXPIRES_WARNING_DAYS")]
|
[Column("EXPIRES_WARNING_DAYS")]
|
||||||
public int ExpiresWarningDays { get; set; }
|
public int? ExpiresWarningDays { get; set; }
|
||||||
|
|
||||||
[Column("SEND_REMINDER_EMAILS")]
|
[Column("SEND_REMINDER_EMAILS")]
|
||||||
public bool SendReminderEmails { get; set; } = false;
|
public bool? SendReminderEmails { get; set; }
|
||||||
|
|
||||||
[Column("FIRST_REMINDER_DAYS")]
|
[Column("FIRST_REMINDER_DAYS")]
|
||||||
public int FirstReminderDays { get; set; } = 0;
|
public int? FirstReminderDays { get; set; }
|
||||||
|
|
||||||
[Column("REMINDER_INTERVAL_DAYS")]
|
[Column("REMINDER_INTERVAL_DAYS")]
|
||||||
public int ReminderIntervalDays { get; set; } = 0;
|
public int? ReminderIntervalDays { get; set; }
|
||||||
|
|
||||||
[Column("CONTRACT_TYPE")]
|
[Column("CONTRACT_TYPE")]
|
||||||
public int ContractType { get; set; }
|
public int? ContractType { get; set; }
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public bool TFA_Enabled { get; set; } = false;
|
public bool TFA_Enabled { get; set; } = false;
|
||||||
@@ -70,14 +70,14 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
[NotMapped]
|
[NotMapped]
|
||||||
public int ExpiresWhenDays
|
public int ExpiresWhenDays
|
||||||
{
|
{
|
||||||
get => ExpiresDays;
|
get => ExpiresDays ?? 0;
|
||||||
set => ExpiresDays = value;
|
set => ExpiresDays = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public int ExpiresWarningWhenDays
|
public int ExpiresWarningWhenDays
|
||||||
{
|
{
|
||||||
get => ExpiresWarningDays;
|
get => ExpiresWarningDays ?? 0;
|
||||||
set => ExpiresWarningDays = value;
|
set => ExpiresWarningDays = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,4 +86,4 @@ namespace EnvelopeGenerator.Domain.Entities
|
|||||||
return Title;
|
return Title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace EnvelopeGenerator.Domain.Interfaces
|
namespace EnvelopeGenerator.Domain.Interfaces
|
||||||
{
|
{
|
||||||
public interface ISignature
|
public interface IDocReceiverElement
|
||||||
{
|
{
|
||||||
int Page { get; set; }
|
int Page { get; set; }
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|||||||
7
EnvelopeGenerator.Domain/Resources/Model.cs
Normal file
7
EnvelopeGenerator.Domain/Resources/Model.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace EnvelopeGenerator.Domain.Resources
|
||||||
|
{
|
||||||
|
// Marker type for IStringLocalizer lookup of Resources/Model.resx.
|
||||||
|
public class Model
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
116
EnvelopeGenerator.Domain/Resources/Model.fr-FR.resx
Normal file
116
EnvelopeGenerator.Domain/Resources/Model.fr-FR.resx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<data name="AccessCodeIncorrect" xml:space="preserve">
|
||||||
|
<value>Code d'acces incorrect</value>
|
||||||
|
</data>
|
||||||
|
<data name="DocumentRejected" xml:space="preserve">
|
||||||
|
<value>Document rejete</value>
|
||||||
|
</data>
|
||||||
|
<data name="DocumentSigned" xml:space="preserve">
|
||||||
|
<value>Document signe</value>
|
||||||
|
</data>
|
||||||
|
<data name="Draft" xml:space="preserve">
|
||||||
|
<value>Brouillon</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeCompletelySigned" xml:space="preserve">
|
||||||
|
<value>Entierement signe</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeCreated" xml:space="preserve">
|
||||||
|
<value>Enveloppe creee</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeDeleted" xml:space="preserve">
|
||||||
|
<value>Enveloppe supprimee</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopePartlySigned" xml:space="preserve">
|
||||||
|
<value>Partiellement signee</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeQueued" xml:space="preserve">
|
||||||
|
<value>Enveloppe en file d'attente</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeRejected" xml:space="preserve">
|
||||||
|
<value>Enveloppe rejetee</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeSaved" xml:space="preserve">
|
||||||
|
<value>Enregistree</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeSent" xml:space="preserve">
|
||||||
|
<value>Envoyee</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeWithdrawn" xml:space="preserve">
|
||||||
|
<value>Enveloppe retiree</value>
|
||||||
|
</data>
|
||||||
|
<data name="MessageAccessCodeSent" xml:space="preserve">
|
||||||
|
<value>Code d'acces envoye</value>
|
||||||
|
</data>
|
||||||
|
<data name="MessageCompletionSent" xml:space="preserve">
|
||||||
|
<value>E-mail de cloture envoye</value>
|
||||||
|
</data>
|
||||||
|
<data name="MessageConfirmationSent" xml:space="preserve">
|
||||||
|
<value>Confirmation finale envoyee</value>
|
||||||
|
</data>
|
||||||
|
<data name="MessageDeletionSent" xml:space="preserve">
|
||||||
|
<value>Notification de suppression envoyee</value>
|
||||||
|
</data>
|
||||||
|
<data name="MessageInvitationSent" xml:space="preserve">
|
||||||
|
<value>Invitation envoyee</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Public Class FieldEditorController
|
|||||||
Inherits BaseController
|
Inherits BaseController
|
||||||
|
|
||||||
Private ReadOnly Document As Document
|
Private ReadOnly Document As Document
|
||||||
Public Property Elements As New List(Of Signature)
|
Public Property Elements As New List(Of DocReceiverElement)
|
||||||
|
|
||||||
Public Class ElementInfo
|
Public Class ElementInfo
|
||||||
Public ReceiverId As Integer
|
Public ReceiverId As Integer
|
||||||
@@ -36,7 +36,7 @@ Public Class FieldEditorController
|
|||||||
}
|
}
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Private Function GetElementByPosition(pAnnotation As AnnotationStickyNote, pPage As Integer, pReceiverId As Integer) As Signature
|
Private Function GetElementByPosition(pAnnotation As AnnotationStickyNote, pPage As Integer, pReceiverId As Integer) As DocReceiverElement
|
||||||
Dim oElement = Elements.
|
Dim oElement = Elements.
|
||||||
Where(Function(e)
|
Where(Function(e)
|
||||||
Return e.Left = CSng(Math.Round(pAnnotation.Left, 5)) And
|
Return e.Left = CSng(Math.Round(pAnnotation.Left, 5)) And
|
||||||
@@ -47,12 +47,12 @@ Public Class FieldEditorController
|
|||||||
Return oElement
|
Return oElement
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Private Function GetElementByGuid(pGuid As Integer) As Signature
|
Private Function GetElementByGuid(pGuid As Integer) As DocReceiverElement
|
||||||
Dim oElement = Elements.Where(Function(e) pGuid = e.Id).SingleOrDefault()
|
Dim oElement = Elements.Where(Function(e) pGuid = e.Id).SingleOrDefault()
|
||||||
Return oElement
|
Return oElement
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function GetElement(pAnnotation As AnnotationStickyNote) As Signature
|
Public Function GetElement(pAnnotation As AnnotationStickyNote) As DocReceiverElement
|
||||||
Dim oInfo = GetElementInfo(pAnnotation.Tag)
|
Dim oInfo = GetElementInfo(pAnnotation.Tag)
|
||||||
|
|
||||||
If oInfo.Guid = -1 Then
|
If oInfo.Guid = -1 Then
|
||||||
@@ -85,7 +85,7 @@ Public Class FieldEditorController
|
|||||||
Else
|
Else
|
||||||
Dim oInfo = GetElementInfo(pAnnotation.Tag)
|
Dim oInfo = GetElementInfo(pAnnotation.Tag)
|
||||||
|
|
||||||
Elements.Add(New Signature() With {
|
Elements.Add(New DocReceiverElement() With {
|
||||||
.ElementType = Constants.ElementType.Signature,
|
.ElementType = Constants.ElementType.Signature,
|
||||||
.Height = oAnnotationHeight,
|
.Height = oAnnotationHeight,
|
||||||
.Width = oAnnotationWidth,
|
.Width = oAnnotationWidth,
|
||||||
@@ -98,7 +98,7 @@ Public Class FieldEditorController
|
|||||||
End If
|
End If
|
||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Public Function ClearElements(pPage As Integer, pReceiverId As Integer) As IEnumerable(Of Signature)
|
Public Function ClearElements(pPage As Integer, pReceiverId As Integer) As IEnumerable(Of DocReceiverElement)
|
||||||
Return Elements.
|
Return Elements.
|
||||||
Where(Function(e) e.Page <> pPage And e.ReceiverId <> pReceiverId).
|
Where(Function(e) e.Page <> pPage And e.ReceiverId <> pReceiverId).
|
||||||
ToList()
|
ToList()
|
||||||
@@ -120,7 +120,7 @@ Public Class FieldEditorController
|
|||||||
All(Function(pResult) pResult = True)
|
All(Function(pResult) pResult = True)
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function SaveElement(pElement As Signature) As Boolean
|
Public Function SaveElement(pElement As DocReceiverElement) As Boolean
|
||||||
Try
|
Try
|
||||||
If pElement.Id > 0 Then
|
If pElement.Id > 0 Then
|
||||||
Return ElementModel.Update(pElement)
|
Return ElementModel.Update(pElement)
|
||||||
@@ -134,11 +134,11 @@ Public Class FieldEditorController
|
|||||||
End Try
|
End Try
|
||||||
End Function
|
End Function
|
||||||
|
|
||||||
Public Function DeleteElement(pElement As Signature) As Boolean
|
Public Function DeleteElement(pElement As DocReceiverElement) As Boolean
|
||||||
Try
|
Try
|
||||||
' Element aus Datenbank löschen
|
' Element aus Datenbank löschen
|
||||||
If ElementModel.DeleteElement(pElement) Then
|
If ElementModel.DeleteElement(pElement) Then
|
||||||
Dim oElement = New List(Of Signature)() From {pElement}
|
Dim oElement = New List(Of DocReceiverElement)() From {pElement}
|
||||||
Elements = Elements.Except(oElement).ToList()
|
Elements = Elements.Except(oElement).ToList()
|
||||||
Return True
|
Return True
|
||||||
Else
|
Else
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ Partial Public Class frmFieldEditor
|
|||||||
End If
|
End If
|
||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Private Sub LoadAnnotation(pElement As Signature, pReceiverId As Integer)
|
Private Sub LoadAnnotation(pElement As DocReceiverElement, pReceiverId As Integer)
|
||||||
Dim oAnnotation As AnnotationStickyNote = Manager.AddStickyNoteAnnot(0, 0, 0, 0, "SIGNATUR")
|
Dim oAnnotation As AnnotationStickyNote = Manager.AddStickyNoteAnnot(0, 0, 0, 0, "SIGNATUR")
|
||||||
Dim oPage = pElement.Page
|
Dim oPage = pElement.Page
|
||||||
Dim oReceiver = Receivers.Where(Function(r) r.Id = pReceiverId).Single()
|
Dim oReceiver = Receivers.Where(Function(r) r.Id = pReceiverId).Single()
|
||||||
|
|||||||
18
EnvelopeGenerator.Infrastructure.Doc/DependencyInjection.cs
Normal file
18
EnvelopeGenerator.Infrastructure.Doc/DependencyInjection.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Doc;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Infrastructure.Doc;
|
||||||
|
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddDocInfrastructure(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<IPdfPageImageRenderer, PdfPageImageRenderer>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DevExpress.Document.Processor" Version="25.2.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user