Compare commits
267 Commits
a3b33637fd
...
feat/serve
| Author | SHA1 | Date | |
|---|---|---|---|
| 96c7cd6e8a | |||
| 4047ba7c6b | |||
| 897860b2bf | |||
| 8933cff558 | |||
| 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 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -365,3 +365,8 @@ FodyWeavers.xsd
|
||||
/EnvelopeGenerator.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode
|
||||
/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs
|
||||
/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
|
||||
|
||||
322
AGENTS.md
Normal file
322
AGENTS.md
Normal file
@@ -0,0 +1,322 @@
|
||||
# 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.
|
||||
|
||||
## 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`
|
||||
@@ -1,424 +1,351 @@
|
||||
# EnvelopeGenerator — AI Context Reference
|
||||
# EnvelopeGenerator — Current Workspace Context
|
||||
|
||||
## Purpose
|
||||
Digital document signing system with **unified Blazor WASM frontend** for both Senders and Receivers. Senders create envelopes and place signature fields. Receivers view PDFs, sign documents, export stamped PDFs.
|
||||
Digital document signing system for senders and receivers.
|
||||
|
||||
**Primary Libraries:** DevExpress + PDF.js (PSPDFKit removed)
|
||||
- Senders authenticate, view envelope lists, and manage envelope workflows.
|
||||
- Receivers authenticate per envelope, open PDFs, create signatures, and apply them in the viewer.
|
||||
- The active UI stack is `Blazor Auto` with server-side and WebAssembly render modes.
|
||||
- Primary UI/PDF libraries are `DevExpress` and `PDF.js`.
|
||||
|
||||
---
|
||||
|
||||
## Deployment Architecture
|
||||
## Active Application Structure
|
||||
|
||||
**Two Presentation Projects (Both Required):**
|
||||
### Main Host
|
||||
**Primary active application:** `EnvelopeGenerator.Server`
|
||||
|
||||
1. **EnvelopeGenerator.API** (ASP.NET Core Web API)
|
||||
- Runs independently (development & production)
|
||||
- **YARP Reverse Proxy** configured via `yarp.json`
|
||||
- Proxies requests to:
|
||||
- `EnvelopeGenerator.ReceiverUI` (Blazor WASM)
|
||||
- External Auth.API service
|
||||
- Serves as single entry point for all requests
|
||||
`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
|
||||
|
||||
2. **EnvelopeGenerator.ReceiverUI** (Blazor WebAssembly)
|
||||
- Runs on separate host/port
|
||||
- Accessed **only through API proxy** (not directly)
|
||||
- Serves static files (HTML, JS, CSS, WASM)
|
||||
### Client Project
|
||||
**Client UI project:** `EnvelopeGenerator.Server.Client`
|
||||
|
||||
**Request Flow:**
|
||||
```
|
||||
Client ? API:8088 (YARP Proxy) ? ReceiverUI:52936 (Blazor WASM)
|
||||
? Auth.API:9090 (External Auth Service)
|
||||
```
|
||||
This project contains:
|
||||
- WebAssembly-rendered pages
|
||||
- client-side services
|
||||
- client models and options
|
||||
- sender and receiver login flows
|
||||
|
||||
**Configuration:** `EnvelopeGenerator.API/yarp.json`
|
||||
### 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`
|
||||
|
||||
---
|
||||
|
||||
## ReceiverUI Route Structure
|
||||
## Current Hosting Model
|
||||
|
||||
### Root Route
|
||||
| Route | File | Purpose |
|
||||
|---|---|---|
|
||||
| `/` | `Index.razor` | Application entry point (landing page). |
|
||||
`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
|
||||
|
||||
### Sender Routes
|
||||
| Route | File | Purpose |
|
||||
|---|---|---|
|
||||
| `/sender/login` | `LoginSenderPage.razor` | Username/password authentication |
|
||||
| `/sender` | `EnvelopeSenderPage.razor` | Sender dashboard (envelope list) |
|
||||
|
||||
### Receiver Routes
|
||||
| Route | File | Purpose |
|
||||
|---|---|---|
|
||||
| `/envelope/login/{EnvelopeKey}` | `LoginReceiverPage.razor` | Access code authentication for specific envelope |
|
||||
| `/envelope/{EnvelopeKey}` | `EnvelopeReceiverPage.razor` | View & sign envelope (PDF.js viewer) |
|
||||
|
||||
**Multi-Envelope Support:** Receivers can login to multiple envelopes simultaneously (per-envelope cookie authentication).
|
||||
This means the active app is a **merged UI + API host**.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Evolution
|
||||
## Reverse Proxy
|
||||
|
||||
### Old Architecture (Deprecated)
|
||||
- **Sender UI:** `EnvelopeGenerator.Web` (Razor Pages + PSPDFKit)
|
||||
- **Receiver UI:** `EnvelopeGenerator.ReceiverUI` (Blazor WASM + PDF.js)
|
||||
- **Backend:** `EnvelopeGenerator.API`
|
||||
**Config file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
|
||||
|
||||
### Current Architecture
|
||||
- **Unified Frontend:** `EnvelopeGenerator.ReceiverUI` (Blazor WASM) — **Both Senders & Receivers**
|
||||
- **Backend:** `EnvelopeGenerator.API` — **Both Senders & Receivers**
|
||||
- **Libraries:** DevExpress + PDF.js
|
||||
- **PSPDFKit:** **REMOVED**
|
||||
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`
|
||||
|
||||
---
|
||||
|
||||
## Solution Structure
|
||||
## Active Routes and Files
|
||||
|
||||
| Project | Target | Purpose |
|
||||
|---|---|---|
|
||||
| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Backend for **both Senders & Receivers**. Auth, PDF serving, signature endpoints. |
|
||||
| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | **Unified Blazor WebAssembly Frontend**. UI for **both Senders & Receivers**. YARP proxy to API. |
|
||||
| `EnvelopeGenerator.Web` | net7/8/9 | **DEPRECATED.** Legacy Razor Pages (Sender UI). No longer used. |
|
||||
| `EnvelopeGenerator.Application` | multi | MediatR CQRS handlers. Business logic. |
|
||||
| `EnvelopeGenerator.Domain` | multi | Domain models, constants, interfaces. |
|
||||
| `EnvelopeGenerator.Infrastructure` | multi | EF Core repos, DB context. |
|
||||
| `EnvelopeGenerator.PdfEditor` | multi | iText7 utilities (NOT used in ReceiverUI). |
|
||||
| `EnvelopeGenerator.DependencyInjection` | multi | DI registration helpers. |
|
||||
| **VB.NET projects** (Service/Form/BBTests) | net462 | **Legacy. Do NOT touch.** |
|
||||
|
||||
---
|
||||
|
||||
## Key Files & Routes
|
||||
|
||||
| File | Route/Purpose |
|
||||
|---|---|
|
||||
| `ReceiverUI/Pages/Index.razor` | `/` — Application entry point (landing page). |
|
||||
| `ReceiverUI/Pages/EnvelopeSenderPage.razor` | `/sender` — Sender dashboard (envelope list). |
|
||||
| `ReceiverUI/Pages/EnvelopeReceiverPage.razor` | `/envelope/{key}` — Receiver PDF viewer & signing. |
|
||||
| `ReceiverUI/Pages/LoginSenderPage.razor` | `/sender/login` — Sender username/password auth. |
|
||||
| `ReceiverUI/Pages/LoginReceiverPage.razor` | `/envelope/login/{EnvelopeKey}` — Receiver access code auth. |
|
||||
| `ReceiverUI/wwwroot/js/pdf-viewer.js` | PDF.js wrapper (zoom, pagination, thumbnails). |
|
||||
| `ReceiverUI/wwwroot/js/receiver-signature.js` | Signature pad (draw/type/image). |
|
||||
| `ReceiverUI/wwwroot/css/envelope-viewer.css` | EnvelopeViewer styles. |
|
||||
| `ReceiverUI/Services/AuthService.cs` | Receiver + Sender authentication. |
|
||||
| `ReceiverUI/Services/SignatureCacheService.cs` | Signature caching (Redis/SQL). |
|
||||
| `API/Controllers/CacheController.cs` | Signature cache endpoints. |
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System — CRITICAL
|
||||
|
||||
**Database Format:** INCHES (GdPicture14 native)
|
||||
**Origin:** Top-left corner
|
||||
**Axes:** X right, Y down
|
||||
|
||||
### Conversion Formulas
|
||||
|
||||
| From INCHES to | Formula | Example |
|
||||
|---|---|---|
|
||||
| **DevExpress DX** | `x_DX = x_inches * 100` | 1.5" ? 150 DX |
|
||||
| **PDF Points** | `x_pt = x_inches * 72` | 1.5" ? 108 pt |
|
||||
| **PDF.js Pixels** | Normalize ? scale | `(x_inches / pageWidth) * canvasWidth * scale` |
|
||||
|
||||
**A4 Dimensions:**
|
||||
- Width: 8.27" = 595pt = 827 DX
|
||||
- Height: 11.69" = 842pt = 1169 DX
|
||||
|
||||
### Unit Systems
|
||||
|
||||
| System | Unit | Origin | Y-Axis |
|
||||
### WebAssembly Pages (`EnvelopeGenerator.Server.Client`)
|
||||
| Route | File | Render Mode | Purpose |
|
||||
|---|---|---|---|
|
||||
| **Database (GdPicture14)** | Inches | Top-left | Down |
|
||||
| PDF.js | Pixels | Top-left | Down |
|
||||
| iText7 PDF | Points (1/72") | **Bottom-left** | **Up** (flip required) |
|
||||
| ~~PSPDFKit~~ | ~~Points~~ | ~~Top-left~~ | **REMOVED** |
|
||||
| `/` | `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 |
|
||||
|
||||
---
|
||||
|
||||
## EnvelopeReceiver — PDF.js Viewer & Signing
|
||||
## Current API Location
|
||||
|
||||
**Route:** `/envelope/{EnvelopeKey}`
|
||||
**Tech:** PDF.js 3.11.174 + Blazor WASM + configurable quality
|
||||
**File:** `ReceiverUI/Pages/EnvelopeReceiverPage.razor`
|
||||
The active application exposes controllers from:
|
||||
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers`
|
||||
|
||||
### Key Features
|
||||
1. HiDPI/Retina support (4x quality)
|
||||
2. Configurable quality (`appsettings.json`)
|
||||
3. Unlimited zoom (50%-300%)
|
||||
4. Ctrl+Wheel global zoom
|
||||
5. Resizable thumbnail sidebar (150-400px, localStorage)
|
||||
6. Responsive (desktop/mobile)
|
||||
Current controller set includes:
|
||||
- `AnnotationController`
|
||||
- `AuthController`
|
||||
- `CacheController`
|
||||
- `ConfigController`
|
||||
- `DocumentController`
|
||||
- `EmailTemplateController`
|
||||
- `EnvelopeController`
|
||||
- `EnvelopeReceiverController`
|
||||
- `EnvelopeTypeController`
|
||||
- `HistoryController`
|
||||
- `LocalizationController`
|
||||
- `ReadOnlyController`
|
||||
- `ReceiverController`
|
||||
- `SignatureController`
|
||||
- `TfaRegistrationController`
|
||||
|
||||
### Configuration
|
||||
**File:** `ReceiverUI/wwwroot/appsettings.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"PdfViewer": {
|
||||
"ThumbnailBaseScale": 0.75,
|
||||
"ThumbnailEnableHiDPI": true,
|
||||
"MainCanvasEnableHiDPI": true,
|
||||
"ZoomStepPercentage": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### JavaScript API
|
||||
**File:** `ReceiverUI/wwwroot/js/pdf-viewer.js`
|
||||
|
||||
```javascript
|
||||
window.pdfViewer = {
|
||||
initialize(canvasId, pdfDataUrl, dotNetRef),
|
||||
renderPage(num),
|
||||
renderSignatureButtons(signatures, pageNum, dotNetRef),
|
||||
applySignature(signatureId, dataUrl, fullName, position, place),
|
||||
zoomIn(), zoomOut(), dispose()
|
||||
}
|
||||
```
|
||||
Do not assume API behavior lives in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
|
||||
|
||||
---
|
||||
|
||||
## Signature Workflow — EnvelopeReceiver
|
||||
## Authentication Model
|
||||
|
||||
**IMPORTANT:** iText7 NOT used (GPL license issue). Client-side overlay system only.
|
||||
### Sender
|
||||
Client login page uses `EnvelopeGenerator.Server.Client/Services/AuthService.cs`.
|
||||
|
||||
### Workflow Steps
|
||||
Key sender endpoints:
|
||||
- `POST /api/auth?cookie=true` — login
|
||||
- `GET /api/auth/check` — current sender access check
|
||||
- `POST /api/auth/logout` — logout
|
||||
|
||||
1. **Page Load:**
|
||||
- Check `SignatureCacheService` for cached signature
|
||||
- If cached ? skip popup, load signature
|
||||
- If not ? show automatic popup (mandatory)
|
||||
### Receiver
|
||||
Receiver authentication is **per envelope**.
|
||||
|
||||
2. **Signature Popup (DxPopup):**
|
||||
- **Cannot close** (no X, no ESC, no outside-click)
|
||||
- **3 Tabs:** Draw (canvas) / Text (font select) / Image (upload)
|
||||
- **Required:** Full name, Place
|
||||
- **Optional:** Position
|
||||
- **Save ?** Store in `_capturedSignature`, cache via API
|
||||
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
|
||||
|
||||
3. **Signature Buttons:**
|
||||
- Render purple "Unterschreiben" buttons at signature field positions
|
||||
- Coordinates: INCHES ? POINTS ? Pixels (scaled)
|
||||
- File: `pdf-viewer.js` ? `renderSignatureButtons()`
|
||||
Receiver cookie resolution in server auth uses an envelope-specific cookie name derived from:
|
||||
- `AuthTokenSignFLOWReceiver.{envelopeKey}` pattern
|
||||
|
||||
4. **Apply Signature (Click "Unterschreiben"):**
|
||||
- JS: Remove button, create HTML overlay
|
||||
- Format: Image + separator + text (Name, Position, Place, Date)
|
||||
- **NOT stamped on PDF bytes** (visual overlay only)
|
||||
### Receiver Server-Side Authorization
|
||||
`EnvelopeReceiverPage.razor` does **not** rely on its own API access-check call for page authorization.
|
||||
|
||||
5. **Re-rendering:**
|
||||
- Zoom/Page change ? recalculate button positions
|
||||
- Session state: `_capturedSignature` (lost on refresh)
|
||||
It uses:
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
|
||||
|
||||
### Data Model
|
||||
**File:** `ReceiverUI/Models/SignatureCaptureDto.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; } // base64 PNG
|
||||
public required string DataUrl { get; init; }
|
||||
public required string FullName { get; init; }
|
||||
public string Position { get; init; } = ""; // Optional
|
||||
public string Position { get; init; } = "";
|
||||
public required string Place { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Signature Caching
|
||||
## Signature Cache
|
||||
|
||||
**Purpose:** Persist signature across page refreshes (distributed cache: Redis/SQL)
|
||||
### Active cache model
|
||||
The current receiver page cache flow is handled directly in the server project through:
|
||||
- `EnvelopeReceiverPageDataService`
|
||||
- `IDistributedCache`
|
||||
- SQL Server distributed cache configuration from `Program.cs`
|
||||
|
||||
### API Endpoints
|
||||
**Controller:** `API/Controllers/CacheController.cs`
|
||||
### Cache key format
|
||||
Current server-side key prefix:
|
||||
- `envelope-generator.receiver-ui.signature:{receiverSignature}`
|
||||
|
||||
- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save
|
||||
- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load
|
||||
- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete
|
||||
This is different from an envelope-key-only cache convention.
|
||||
|
||||
**Cache Key Format:**
|
||||
```
|
||||
signature:91751687-8ae6-4777-bf5f-b8846085e62e:{envelopeKey}
|
||||
```
|
||||
### Config
|
||||
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/CacheOptions.cs`
|
||||
- section name: `Cache`
|
||||
- option: `SignatureCacheExpiration`
|
||||
|
||||
**Configuration:** `appsettings.json`
|
||||
```json
|
||||
{
|
||||
"Cache": {
|
||||
"SignatureCacheExpiration": null // or "02:00:00" for 2h
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service
|
||||
**File:** `ReceiverUI/Services/SignatureCacheService.cs`
|
||||
|
||||
```csharp
|
||||
public class SignatureCacheService {
|
||||
Task SaveSignatureAsync(string envelopeKey, SignatureCaptureDto signature);
|
||||
Task<SignatureCaptureDto?> GetSignatureAsync(string envelopeKey);
|
||||
Task DeleteSignatureAsync(string envelopeKey);
|
||||
}
|
||||
```
|
||||
|
||||
**Error Handling:** Fire-and-forget saves, graceful degradation on load failure.
|
||||
### Related controller
|
||||
A cache API controller also exists in:
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/CacheController.cs`
|
||||
|
||||
---
|
||||
|
||||
## Sender Login
|
||||
## Sender Dashboard
|
||||
|
||||
**Route:** `/sender/login`
|
||||
**File:** `ReceiverUI/Pages/LoginSenderPage.razor`
|
||||
**Tech:** Bootstrap 5 + DevExpress Blazing Berry theme
|
||||
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor`
|
||||
|
||||
### AuthService Extension
|
||||
**File:** `ReceiverUI/Services/AuthService.cs`
|
||||
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`
|
||||
|
||||
```csharp
|
||||
public enum SenderLoginResult { Success, InvalidCredentials, Error }
|
||||
|
||||
public async Task<SenderLoginResult> LoginSenderAsync(string username, string password) {
|
||||
var response = await http.PostAsJsonAsync(
|
||||
$"{_api.BaseUrl}/api/auth?cookie=true",
|
||||
new { username, password });
|
||||
|
||||
return response.StatusCode switch {
|
||||
HttpStatusCode.OK => SenderLoginResult.Success,
|
||||
HttpStatusCode.Unauthorized => SenderLoginResult.InvalidCredentials,
|
||||
_ => SenderLoginResult.Error
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### API Integration
|
||||
**Endpoint:** `POST /api/auth?cookie=true`
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{ "username": "TekH", "password": "***" }
|
||||
```
|
||||
|
||||
**Response:**
|
||||
- `200 OK` ? Cookie set, redirect to `/sender`
|
||||
- `401 Unauthorized` ? Show error: "Ungültige Anmeldedaten"
|
||||
- Other ? Show error: "Serverfehler"
|
||||
|
||||
**Cookie:** HTTP-only, Secure (HTTPS), SameSite=Strict
|
||||
|
||||
### UI Flow
|
||||
1. User enters username + password
|
||||
2. Click "Anmelden" or press Enter
|
||||
3. Call `AuthService.LoginSenderAsync()`
|
||||
4. Success ? `Navigation.NavigateTo("/sender", forceLoad: true)`
|
||||
5. Error ? Display alert
|
||||
The sender page is active, but create/edit/delete actions are still marked with TODO behavior in the UI page.
|
||||
|
||||
---
|
||||
|
||||
## Receiver Login
|
||||
## Localization
|
||||
|
||||
**Route:** `/envelope/login/{EnvelopeKey}`
|
||||
**File:** `ReceiverUI/Pages/LoginReceiverPage.razor`
|
||||
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()`
|
||||
|
||||
**Multi-Envelope Support:** Cookies are stored per-envelope (e.g., `AuthTokenSignFLOWReceiver.{envelopeKey}`), allowing simultaneous authentication for multiple envelopes in the same browser session.
|
||||
|
||||
### AuthService Method
|
||||
```csharp
|
||||
public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
|
||||
|
||||
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string key, string accessCode) {
|
||||
var form = new MultipartFormDataContent();
|
||||
form.Add(new StringContent(accessCode), "AccessCode");
|
||||
|
||||
var response = await http.PostAsync(
|
||||
$"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(key)}", form);
|
||||
|
||||
return response.StatusCode switch {
|
||||
HttpStatusCode.OK => EnvelopeLoginResult.Success,
|
||||
HttpStatusCode.Unauthorized => EnvelopeLoginResult.InvalidCode,
|
||||
HttpStatusCode.NotFound => EnvelopeLoginResult.NotFound,
|
||||
_ => EnvelopeLoginResult.Error
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Success:** Redirect to `/envelope/{key}`
|
||||
Do not assume the old ReceiverUI-only `localStorage` culture approach is the current source of truth for the active host.
|
||||
|
||||
---
|
||||
|
||||
## NuGet Packages (ReceiverUI)
|
||||
## Coordinate System
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---|---|---|
|
||||
| `DevExpress.Blazor.*` | 25.2.3 | UI components (grids, popups, etc.) |
|
||||
| `SkiaSharp.*` | 3.119.1 | WASM rendering |
|
||||
| ~~`itext`~~ | ~~8.0.5~~ | **NOT USED** (GPL license) |
|
||||
### Source data
|
||||
Database signature coordinates are still based on:
|
||||
- **unit:** inches
|
||||
- **origin:** top-left
|
||||
- **axes:** X right, Y down
|
||||
|
||||
**External CDN:**
|
||||
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
||||
### 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 |
|
||||
|
||||
---
|
||||
|
||||
## Mistakes History — Do NOT Repeat
|
||||
## Key Services and Files
|
||||
|
||||
| Mistake | Why Wrong |
|
||||
|---|---|
|
||||
| Using iText7 in EnvelopeReceiver | GPL license issue. Use overlay system instead. |
|
||||
| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. |
|
||||
| Hardcoded quality values in PDF.js | Use `appsettings.json` for configurability. |
|
||||
| Complex toolbar layouts | User wants simplicity. Keep horizontal layout. |
|
||||
| Over-designed UI (gradients/badges) | User prefers simple text labels. |
|
||||
| Ignoring "revert" instructions | Revert HTML structure, not just CSS. |
|
||||
| `BottomMarginBand` for signatures | Repeats on every page. Use DetailBand. |
|
||||
| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand. Calculate per-page. |
|
||||
### 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`
|
||||
|
||||
---
|
||||
|
||||
## Development Notes
|
||||
## Working Rules for This Workspace
|
||||
|
||||
### Deprecated Projects
|
||||
**DO NOT USE:**
|
||||
- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified ReceiverUI
|
||||
- PSPDFKit — Removed, use PDF.js + DevExpress instead
|
||||
|
||||
### Legacy Projects (VB.NET)
|
||||
**DO NOT TOUCH:** `EnvelopeGenerator.Service`, `EnvelopeGenerator.Form`, `EnvelopeGenerator.BBTests`
|
||||
|
||||
### Signature Coordinate Evidence
|
||||
**File:** `EnvelopeGenerator.Form/frmFieldEditor.vb` (VB.NET)
|
||||
|
||||
```vb
|
||||
Private Const SIGNATURE_WIDTH As Single = 1.77 ' inches
|
||||
Private Const SIGNATURE_HEIGHT As Single = 1.96 ' inches
|
||||
|
||||
Sub LoadAnnotation(pElement As Signature, ...)
|
||||
oAnnotation.Left = CSng(pElement.X) ' Direct INCHES assignment
|
||||
oAnnotation.Top = CSng(pElement.Y)
|
||||
End Sub
|
||||
```
|
||||
|
||||
Proves database uses INCHES natively.
|
||||
- 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`.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### When working with coordinates:
|
||||
1. **Database ? UI:** INCHES × 72 = PDF Points
|
||||
2. **UI ? Display:** Points × scale = Pixels
|
||||
3. **iText7 stamping:** Flip Y-axis (top-down ? bottom-up)
|
||||
|
||||
### When adding features:
|
||||
1. Check `Mistakes History` first
|
||||
2. Prefer simplicity over complexity
|
||||
3. Use `appsettings.json` for configuration
|
||||
4. Keep consistent with existing design (Bootstrap 5 + Blazing Berry)
|
||||
5. **Unified frontend:** ReceiverUI serves both Senders and Receivers
|
||||
|
||||
### When debugging:
|
||||
1. **Coordinates:** Always check unit system (inches/points/pixels)
|
||||
2. **Authentication:** Check cookie name/domain/SameSite
|
||||
3. **Cache:** Check Redis/SQL connection + key format
|
||||
4. **Frontend confusion:** Only use ReceiverUI (Web is deprecated)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** Session 19 (Razor file naming convention + Index route proxy)
|
||||
**Last Updated:** 2026-06-29
|
||||
|
||||
@@ -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> Get(string envelopeKey, CancellationToken cancel)
|
||||
{
|
||||
int envelopeId = User.EnvelopeId();
|
||||
|
||||
int receiverId = User.ReceiverId();
|
||||
|
||||
var doc = await _mediator.Send(new ReadDocumentQuery() { EnvelopeId = envelopeId }, cancel);
|
||||
|
||||
if (doc.Elements is not IEnumerable<DocReceiverElementDto> 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,83 +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="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
|
||||
<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,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,185 +0,0 @@
|
||||
{
|
||||
"ReverseProxy": {
|
||||
"Routes": {
|
||||
"receiver-ui-root": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 300,
|
||||
"Match": {
|
||||
"Path": "/",
|
||||
"Methods": [ "GET", "HEAD" ]
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathSet": "/index.html" }
|
||||
]
|
||||
},
|
||||
"receiver-ui-sender": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 100,
|
||||
"Match": {
|
||||
"Path": "/sender/{**catch-all}",
|
||||
"Methods": [ "GET", "HEAD" ]
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathSet": "/index.html" }
|
||||
]
|
||||
},
|
||||
"receiver-ui-envelope": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 100,
|
||||
"Match": {
|
||||
"Path": "/envelope/{EnvelopeKey}",
|
||||
"Methods": [ "GET", "HEAD" ]
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathSet": "/index.html" }
|
||||
]
|
||||
},
|
||||
"receiver-ui-envelope-dxreportviewer": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 90,
|
||||
"Match": {
|
||||
"Path": "/envelope/{EnvelopeKey}/DxReportViewer",
|
||||
"Methods": [ "GET", "HEAD" ]
|
||||
},
|
||||
"Transforms": [
|
||||
{ "PathSet": "/index.html" }
|
||||
]
|
||||
},
|
||||
"receiver-ui-blazor-framework": {
|
||||
"ClusterId": "receiver-ui",
|
||||
"Order": 50,
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing configuration settings.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class ConfigDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class DocReceiverElementDto : IDocReceiverElement
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class DocumentDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing the status of a document for a specific receiver.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class DocumentStatusDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||
using DigitalData.UserManager.Application.DTOs.User;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Application.Common.Dto.History;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record EnvelopeDto : IEnvelope
|
||||
{
|
||||
/// <summary>
|
||||
@@ -133,5 +129,10 @@ public record EnvelopeDto : IEnvelope
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public IEnumerable<ReceiverDto>? Receivers { get; set; }
|
||||
public IEnumerable<EnvelopeReceiverDto>? EnvelopeReceivers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc.
|
||||
/// </summary>
|
||||
public IEnumerable<HistoryDto>? Histories { get; set; }
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record EnvelopeReceiverDto
|
||||
{
|
||||
/// <summary>
|
||||
@@ -61,6 +60,11 @@ public record EnvelopeReceiverDto
|
||||
/// </summary>
|
||||
public string? PrivateMessage { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string? AccessCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
@@ -75,4 +79,13 @@ public record EnvelopeReceiverDto
|
||||
///
|
||||
/// </summary>
|
||||
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>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
@@ -8,7 +7,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="DateValid"></param>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record EnvelopeReceiverReadOnlyCreateDto(
|
||||
DateTime DateValid)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
|
||||
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.
|
||||
/// Contains information about the receiver, associated envelope, and audit details.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class EnvelopeReceiverReadOnlyDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for updating a read-only envelope receiver.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class EnvelopeReceiverReadOnlyUpdateDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class EnvelopeTypeDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
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>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record SmsResponse
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
@@ -7,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class ReceiverDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -49,7 +49,7 @@ public sealed record Signature
|
||||
public string? Position
|
||||
{
|
||||
get => _position;
|
||||
init => _position = string.IsNullOrWhiteSpace(value) ? value : null;
|
||||
init => _position = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -62,4 +62,4 @@ public sealed record Signature
|
||||
/// <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();
|
||||
}
|
||||
@@ -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.Domain.Entities;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the AutoMapper profile configuration for mapping between
|
||||
@@ -26,15 +28,17 @@ public class MappingProfile : Profile
|
||||
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
||||
CreateMap<DocumentStatus, DocumentStatusDto>();
|
||||
CreateMap<EmailTemplate, EmailTemplateDto>();
|
||||
CreateMap<Envelope, EnvelopeDto>().ForMember(dest => dest.Receivers, opt => opt.MapFrom(src => src.EnvelopeReceivers.Select(er => er.Receiver)));
|
||||
CreateMap<Envelope, EnvelopeDto>()
|
||||
.ForMember(dest => dest.EnvelopeTypeTitle,
|
||||
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
|
||||
CreateMap<Document, DocumentDto>();
|
||||
CreateMap<Domain.Entities.History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||
CreateMap<Domain.Entities.History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
||||
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||
CreateMap<EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
||||
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
||||
CreateMap<Domain.Entities.Receiver, ReceiverDto>();
|
||||
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
||||
CreateMap<Receiver, ReceiverDto>();
|
||||
CreateMap<EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
||||
CreateMap<ElementAnnotation, AnnotationDto>();
|
||||
|
||||
// DTO to Entity mappings
|
||||
@@ -47,13 +51,13 @@ public class MappingProfile : Profile
|
||||
CreateMap<EmailTemplateDto, EmailTemplate>();
|
||||
CreateMap<EnvelopeDto, Envelope>();
|
||||
CreateMap<DocumentDto, Document>();
|
||||
CreateMap<HistoryDto, Domain.Entities.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<EnvelopeReceiverDto, Domain.Entities.EnvelopeReceiver>();
|
||||
CreateMap<HistoryDto, 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, EnvelopeReceiver>();
|
||||
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
||||
CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
||||
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
||||
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
||||
CreateMap<ReceiverDto, Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
||||
CreateMap<EnvelopeReceiverReadOnlyCreateDto, EnvelopeReceiverReadOnly>();
|
||||
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, EnvelopeReceiverReadOnly>();
|
||||
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
||||
.MapAddedWhen();
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||
|
||||
public record EnvelopeDeletedNotification : INotification, ISendMailNotification
|
||||
{
|
||||
public required Envelope Envelope { get; init; }
|
||||
|
||||
public required EnvelopeReceiver EnvelopeReceiver { get; init; }
|
||||
|
||||
public required string Reason { get; init; }
|
||||
|
||||
public required string SenderReference { get; init; }
|
||||
|
||||
public EmailTemplateType TemplateType => EmailTemplateType.DocumentDeleted;
|
||||
|
||||
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
|
||||
?? throw new InvalidOperationException("Receiver email is missing for EnvelopeDeletedNotification.");
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||
using EnvelopeGenerator.Application.Common.Configurations;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted.Handlers;
|
||||
|
||||
public class SendDeletedMailHandler : SendMailHandler<EnvelopeDeletedNotification>
|
||||
{
|
||||
public SendDeletedMailHandler(
|
||||
IRepository<EmailTemplate> tempRepo,
|
||||
IRepository<EmailOut> emailOutRepo,
|
||||
IOptions<MailParams> mailParamsOptions,
|
||||
IOptions<DispatcherParams> dispatcherParamsOptions)
|
||||
: base(tempRepo, emailOutRepo, mailParamsOptions, dispatcherParamsOptions)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void ConfigureEmailOut(EnvelopeDeletedNotification notification, EmailOut emailOut)
|
||||
{
|
||||
emailOut.ReferenceString = notification.EmailAddress;
|
||||
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
|
||||
emailOut.WfId = (int)EnvelopeStatus.MessageDeletionSent;
|
||||
}
|
||||
|
||||
protected override Dictionary<string, string> CreatePlaceHolders(EnvelopeDeletedNotification notification)
|
||||
{
|
||||
var senderName = notification.Envelope.User?.Email;
|
||||
if (string.IsNullOrWhiteSpace(senderName))
|
||||
senderName = notification.SenderReference;
|
||||
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
["[NAME_RECEIVER]"] = notification.EnvelopeReceiver.Name ?? string.Empty,
|
||||
["[NAME_SENDER]"] = senderName ?? string.Empty,
|
||||
["[DOCUMENT_TITLE]"] = notification.Envelope.Title ?? string.Empty,
|
||||
["[REASON]"] = notification.Reason
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace EnvelopeGenerator.Application.Common.Query;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Query;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
||||
/// </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,
|
||||
/// </summary>
|
||||
public string Raw => @"
|
||||
DECLARE @OUT_RECEIVER_ID int
|
||||
DECLARE @OUT_RECEIVER_ID bigint
|
||||
|
||||
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
|
||||
{0},
|
||||
|
||||
@@ -51,8 +51,8 @@ public static class DependencyInjection
|
||||
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
||||
|
||||
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
||||
services.TryAddSingleton<ISmsSender, GTXSmsSender>();
|
||||
services.TryAddSingleton<IEnvelopeSmsHandler, EnvelopeSmsHandler>();
|
||||
services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped
|
||||
services.TryAddScoped<IEnvelopeSmsHandler, EnvelopeSmsHandler>(); // Changed: Singleton → Scoped
|
||||
services.TryAddSingleton<IAuthenticator, Authenticator>();
|
||||
services.TryAddSingleton<QRCodeGenerator>();
|
||||
|
||||
@@ -65,18 +65,21 @@ public static class DependencyInjection
|
||||
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>();
|
||||
|
||||
// 2. DocStatusBehavior - Creates document status (executes third)
|
||||
// 3. DocStatusBehavior - Creates document status
|
||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
|
||||
|
||||
// 3. HistoryBehavior - Records history (executes fourth)
|
||||
// 4. HistoryBehavior - Records history
|
||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
|
||||
|
||||
// 4. SendSignedMailBehavior - Sends notification email (executes LAST, only if all previous succeed)
|
||||
// 5. SendSignedMailBehavior - Sends notification email (executes LAST)
|
||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,11 @@ public class EnvelopeReceiverResolutionBehavior : IPipelineBehavior<SigningComma
|
||||
// If EnvelopeReceiver is not provided, query it from database
|
||||
if (request.EnvelopeReceiver is null)
|
||||
{
|
||||
var er = await _erRepo.Query.Where(request, notnull: true).SingleOrDefaultAsync(cancellationToken)
|
||||
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));
|
||||
|
||||
@@ -50,8 +50,8 @@ public class SaveSignatureBehavior : IPipelineBehavior<SigningCommand, Unit>
|
||||
throw new BadRequestException($"Signatures are required for saving signature behavior.");
|
||||
|
||||
var elements = await _elementRepo
|
||||
.Where(e => e.Document.EnvelopeId == request.Envelope.Id)
|
||||
.Where(e => e.ReceiverId == request.Receiver.Id)
|
||||
.Where(e => e.Document.EnvelopeId == request.EnvelopeReceiver.EnvelopeId)
|
||||
.Where(e => e.ReceiverId == request.EnvelopeReceiver.ReceiverId)
|
||||
.ToListAsync(cancel);
|
||||
|
||||
foreach (var element in elements)
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace EnvelopeGenerator.Application.Documents.Queries;
|
||||
/// </summary>
|
||||
/// <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>
|
||||
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>
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\Model.Designer.vb" />
|
||||
</ItemGroup>
|
||||
@@ -21,7 +21,6 @@
|
||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||
<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="Otp.NET" Version="1.4.0" />
|
||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||
@@ -79,25 +78,25 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||
<PackageReference Include="CommandDotNet">
|
||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||
<PackageReference Include="CommandDotNet">
|
||||
<Version>7.0.5</Version>
|
||||
</PackageReference>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||
<PackageReference Include="CommandDotNet">
|
||||
<Version>8.1.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||
<PackageReference Include="CommandDotNet">
|
||||
<Version>8.1.1</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||
<PackageReference Include="CommandDotNet">
|
||||
<Version>8.1.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||
<PackageReference Include="CommandDotNet">
|
||||
<Version>8.1.1</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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 MediatR;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
@@ -13,23 +12,29 @@ namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
/// </summary>
|
||||
public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
||||
{
|
||||
public int? EnvelopeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Der Titel des Umschlags. Dies ist ein Pflichtfeld.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Title { get; set; }
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public required string Message { get; set; }
|
||||
public string? Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false.
|
||||
/// </summary>
|
||||
public bool TFAEnabled { get; set; } = false;
|
||||
|
||||
public bool Send { get; init; }
|
||||
|
||||
public CreateEnvelopeDocumentDto? Document { get; init; }
|
||||
|
||||
public List<CreateEnvelopeReceiverDto> Receivers { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// ID des Absenders
|
||||
/// </summary>
|
||||
@@ -55,4 +60,14 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
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 MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||
using DigitalData.Core.Exceptions;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
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.Histories.Commands;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||
|
||||
@@ -19,6 +29,26 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
||||
|
||||
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<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;
|
||||
|
||||
/// <summary>
|
||||
@@ -40,10 +70,385 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
|
||||
/// <returns></returns>
|
||||
public async Task<EnvelopeDto?> Handle(CreateEnvelopeCommand request, CancellationToken cancel)
|
||||
{
|
||||
var envelope = request.UseSQLExecutor
|
||||
? await Executor.CreateEnvelopeAsync(request.UserId, request.Title, request.Message, request.TFAEnabled, cancel)
|
||||
: await Repository.CreateAsync(request, cancel);
|
||||
var now = DateTime.Now;
|
||||
var title = request.Title?.Trim() ?? string.Empty;
|
||||
var message = request.Message?.Trim() ?? string.Empty;
|
||||
|
||||
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();
|
||||
|
||||
await Repository.UpdateAsync(e =>
|
||||
{
|
||||
e.Title = title;
|
||||
e.Message = message;
|
||||
e.TfaEnabled = request.TFAEnabled;
|
||||
e.Status = EnvelopeStatus.EnvelopeSaved;
|
||||
e.ChangedWhen = now;
|
||||
}, e => e.Id == envelopeId, cancel);
|
||||
|
||||
envelope.Title = title;
|
||||
envelope.Message = message;
|
||||
envelope.TfaEnabled = request.TFAEnabled;
|
||||
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,
|
||||
Language = "de-DE",
|
||||
TfaEnabled = request.TFAEnabled,
|
||||
UseAccessCode = false,
|
||||
SendReminderEmails = false
|
||||
}, cancel);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool OnlyActive { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool OnlyCompleted { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Abfrage des Include des Umschlags
|
||||
/// </summary>
|
||||
@@ -132,10 +142,18 @@ public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnum
|
||||
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
|
||||
.Include(e => e.Type)
|
||||
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
||||
.Include(e => e.Histories)
|
||||
.ToListAsync(cancel);
|
||||
|
||||
return _mapper.Map<IEnumerable<EnvelopeDto>>(envelopes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Cryptography;
|
||||
@@ -14,7 +13,6 @@ namespace EnvelopeGenerator.Application.Receivers.Commands;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record CreateReceiverCommand : IRequest<(ReceiverDto Receiver, bool AlreadyExists)>
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Receivers.Commands;
|
||||
namespace EnvelopeGenerator.Application.Receivers.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for updating a receiver's information.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class UpdateReceiverCommand
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -5,6 +5,7 @@ using EnvelopeGenerator.Application.Common.Query;
|
||||
using MediatR;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
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.
|
||||
/// um spezifische Informationen über einen Empfänger abzurufen.
|
||||
/// </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>
|
||||
///
|
||||
@@ -53,6 +71,11 @@ public class ReadReceiverQueryHandler : IRequestHandler<ReadReceiverQuery, IEnum
|
||||
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)
|
||||
{
|
||||
query = query.Where(r => r.Signature == signature);
|
||||
|
||||
@@ -397,4 +397,412 @@ public static class Extensions
|
||||
/// <param name="suffix"></param>
|
||||
/// <returns></returns>
|
||||
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,297 @@
|
||||
<data name="Confirmations" xml:space="preserve">
|
||||
<value>Bestätigungen</value>
|
||||
</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_NoReceiversAdded" xml:space="preserve"><value>Noch keine Empfänger hinzugefügt.</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,297 @@
|
||||
<data name="Confirmations" xml:space="preserve">
|
||||
<value>Confirmations</value>
|
||||
</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_NoReceiversAdded" xml:space="preserve"><value>No receivers added yet.</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,297 @@
|
||||
<data name="Confirmations" xml:space="preserve">
|
||||
<value>Confirmations</value>
|
||||
</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_NoReceiversAdded" xml:space="preserve"><value>Aucun destinataire ajouté pour le moment.</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,6 +1,7 @@
|
||||
Imports System.IO
|
||||
Imports DigitalData.Modules.Database
|
||||
Imports DigitalData.Modules.Logging
|
||||
Imports EnvelopeGenerator.CommonServices
|
||||
Imports EnvelopeGenerator.CommonServices.Jobs
|
||||
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument
|
||||
Imports GdPicture14
|
||||
@@ -145,64 +146,91 @@ Public Class frmFinalizePDF
|
||||
log.AppendLine($"Document bytes: {oBuffer.Length}")
|
||||
|
||||
' 3. Check what BurnAnnotsToPDF will do internally
|
||||
Dim oEnvelopeEntity As Envelope = Nothing
|
||||
Using scope = Factory.Shared.ScopeFactory.CreateScope()
|
||||
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!")
|
||||
txtResult.Text = log.ToString()
|
||||
Return
|
||||
End If
|
||||
|
||||
log.AppendLine($"Envelope found: Id={envelope.Id}, EnvelopeTypeId={envelope.EnvelopeTypeId}")
|
||||
log.AppendLine($"ReadOnly (IsReadAndConfirm): {envelope.ReadOnly}")
|
||||
log.AppendLine($"Envelope found: Id={oEnvelopeEntity.Id}, EnvelopeTypeId={oEnvelopeEntity.EnvelopeTypeId}")
|
||||
log.AppendLine($"ReadOnly (IsReadAndConfirm): {oEnvelopeEntity.ReadOnly}")
|
||||
|
||||
If envelope.ReadOnly Then
|
||||
log.AppendLine(">>> EARLY RETURN: ReadOnly=True, original PDF returned without burning")
|
||||
txtResult.Text = log.ToString()
|
||||
Return
|
||||
End If
|
||||
|
||||
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
|
||||
Dim elements = sigRepo _
|
||||
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
||||
.Include(Function(sig) sig.Annotations) _
|
||||
.ToList()
|
||||
|
||||
log.AppendLine($"Elements (Signature) count: {elements.Count}")
|
||||
|
||||
If elements.Any() Then
|
||||
log.AppendLine(">>> PATH: BurnElementAnnotsToPDF (new element-based path)")
|
||||
For Each elem In elements
|
||||
Dim annotCount = If(elem.Annotations IsNot Nothing, elem.Annotations.Count(), 0)
|
||||
log.AppendLine($" Element Id={elem.Id}, Page={elem.Page}, X={elem.X}, Y={elem.Y}, W={elem.Width}, H={elem.Height}, Annotations={annotCount}")
|
||||
If elem.Annotations IsNot Nothing Then
|
||||
For Each annot In elem.Annotations
|
||||
log.AppendLine($" Annot: Name={annot.Name}, Type={annot.Type}, X={annot.X}, Y={annot.Y}, W={annot.Width}, H={annot.Height}")
|
||||
Next
|
||||
End If
|
||||
Next
|
||||
If oEnvelopeEntity.ReadOnly Then
|
||||
log.AppendLine(">>> ReadOnly=True: original PDF will be used without burning (annotations skipped)")
|
||||
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 Using
|
||||
|
||||
' 4. Actually call BurnAnnotsToPDF (same as Service)
|
||||
' 4. Burn annotations (ReadOnly envelopes return original PDF unchanged)
|
||||
log.AppendLine("")
|
||||
log.AppendLine("Calling BurnAnnotsToPDF...")
|
||||
Dim oNewBuffer = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
|
||||
log.AppendLine($"Result bytes: {oNewBuffer.Length}")
|
||||
log.AppendLine($"Same as input: {oBuffer.Length = oNewBuffer.Length AndAlso oBuffer.SequenceEqual(oNewBuffer)}")
|
||||
Dim oBurnedBuffer As Byte() = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
|
||||
log.AppendLine($"Burned bytes: {oBurnedBuffer.Length}")
|
||||
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 oNewPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.burned.pdf")
|
||||
File.WriteAllBytes(oNewPath, oNewBuffer)
|
||||
log.AppendLine($"Output: {oNewPath}")
|
||||
Dim oMergedPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.merged.pdf")
|
||||
File.WriteAllBytes(oMergedPath, oMergedBuffer)
|
||||
log.AppendLine($"Output: {oMergedPath}")
|
||||
|
||||
txtResult.Text = log.ToString()
|
||||
Process.Start(oNewPath)
|
||||
Process.Start(oMergedPath)
|
||||
|
||||
Catch ex As Exception
|
||||
txtResult.Text = $"ERROR: {ex.Message}{vbCrLf}{vbCrLf}{ex.ToString()}"
|
||||
|
||||
@@ -119,7 +119,7 @@ Namespace Jobs.FinalizeDocument
|
||||
Dim y = frameY + yOffsetofFF
|
||||
|
||||
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
|
||||
AddImageAnnotation(
|
||||
annot.X / inchFactor,
|
||||
@@ -269,11 +269,14 @@ Namespace Jobs.FinalizeDocument
|
||||
Next
|
||||
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)
|
||||
|
||||
' Truncate if needed
|
||||
Dim displayValue As String = TruncateText(value, egName)
|
||||
|
||||
' 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
|
||||
ant.FontName = _pdfBurnerParams.FontName
|
||||
@@ -294,8 +297,9 @@ Namespace Jobs.FinalizeDocument
|
||||
Dim oHeight = oBounds.Item(3)
|
||||
|
||||
Manager.SelectPage(pAnnotation.pageIndex + 1)
|
||||
' Add the text annotation
|
||||
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, formFieldValue.value)
|
||||
' Add the text annotation (truncate if needed)
|
||||
Dim displayValue As String = TruncateText(formFieldValue.value, pAnnotation.egName)
|
||||
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, displayValue)
|
||||
|
||||
' Set the font properties
|
||||
ant.FontName = _pdfBurnerParams.FontName
|
||||
@@ -306,6 +310,48 @@ Namespace Jobs.FinalizeDocument
|
||||
#End Region
|
||||
|
||||
#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
|
||||
Dim oPoints = pPoints.Select(AddressOf ToInches).ToList()
|
||||
Return New PointF(oPoints.Item(0), oPoints.Item(1))
|
||||
|
||||
@@ -12,5 +12,24 @@ Namespace Jobs.FinalizeDocument
|
||||
Public Property FontSize As Integer = 8
|
||||
|
||||
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 Namespace
|
||||
@@ -1,5 +1,6 @@
|
||||
Imports EnvelopeGenerator.Domain.Constants
|
||||
Imports EnvelopeGenerator.Domain.Entities
|
||||
Imports EnvelopeGenerator.Domain.Interfaces
|
||||
|
||||
Public Class ReportItem
|
||||
|
||||
@@ -11,8 +12,15 @@ Public Class ReportItem
|
||||
Public Property ItemStatus As EnvelopeStatus
|
||||
Public ReadOnly Property ItemStatusTranslated As String
|
||||
Get
|
||||
Dim oStatus = ItemStatus.ToString()
|
||||
Return My.Resources.Model.ResourceManager.GetString(oStatus)
|
||||
Dim oStatusKey = ItemStatus.ToString()
|
||||
|
||||
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 Property
|
||||
|
||||
|
||||
@@ -168,6 +168,9 @@
|
||||
<data name="DocumentSigned" xml:space="preserve">
|
||||
<value>Document signed</value>
|
||||
</data>
|
||||
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||
<value>Read and confirmed</value>
|
||||
</data>
|
||||
<data name="Draft" xml:space="preserve">
|
||||
<value>Draft</value>
|
||||
</data>
|
||||
|
||||
@@ -168,6 +168,9 @@
|
||||
<data name="DocumentSigned" xml:space="preserve">
|
||||
<value>Dokument unterzeichnet</value>
|
||||
</data>
|
||||
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||
<value>Gelesen und bestätigt</value>
|
||||
</data>
|
||||
<data name="Draft" xml:space="preserve">
|
||||
<value>Entwurf</value>
|
||||
</data>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
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
|
||||
{
|
||||
Invalid = 0,
|
||||
@@ -49,5 +51,28 @@ namespace EnvelopeGenerator.Domain.Constants
|
||||
EnvelopeStatus.EnvelopeCreated,
|
||||
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
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
#endif
|
||||
|
||||
namespace EnvelopeGenerator.Domain.Constants
|
||||
@@ -15,7 +16,11 @@ namespace EnvelopeGenerator.Domain.Constants
|
||||
public static class Receiver
|
||||
{
|
||||
public const string TFA = "EGReceiverTFA";
|
||||
|
||||
[Obsolete("Use Receiver.Authorized")]
|
||||
public const string Full = "EGReceiver";
|
||||
|
||||
public static readonly IReadOnlyList<string> Authorized = new[] { "receiver", "EGReceiver" };
|
||||
}
|
||||
|
||||
public const string Sender = "EGSender";
|
||||
|
||||
@@ -213,6 +213,15 @@ namespace My.Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Gelesen und bestätigt.
|
||||
/// </summary>
|
||||
public static string DocumentSignedRaC {
|
||||
get {
|
||||
return ResourceManager.GetString("DocumentSignedRaC", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Entwurf.
|
||||
/// </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">
|
||||
<value>Dokument unterzeichnet</value>
|
||||
</data>
|
||||
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||
<value>Gelesen und bestätigt</value>
|
||||
</data>
|
||||
<data name="Draft" xml:space="preserve">
|
||||
<value>Entwurf</value>
|
||||
</data>
|
||||
|
||||
@@ -14,6 +14,7 @@ using EnvelopeGenerator.Infrastructure.Repositories;
|
||||
using EnvelopeGenerator.Infrastructure.Executor;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||
#elif NETFRAMEWORK
|
||||
using System;
|
||||
@@ -55,6 +56,9 @@ namespace EnvelopeGenerator.Infrastructure
|
||||
services.TryAddScoped<IEnvelopeTypeRepository, EnvelopeTypeRepository>();
|
||||
services.TryAddScoped<IReceiverRepository, ReceiverRepository>();
|
||||
services.TryAddScoped<IEnvelopeReceiverReadOnlyRepository, EnvelopeReceiverReadOnlyRepository>();
|
||||
|
||||
// User Preferences — distributed cache (dbo.TBDD_CACHE)
|
||||
services.TryAddScoped<IUserPreferencesService, UserPreferencesService>();
|
||||
#endif
|
||||
|
||||
services.AddDbRepository(opt =>
|
||||
@@ -210,4 +214,4 @@ namespace EnvelopeGenerator.Infrastructure
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,16 +204,24 @@ public abstract class EGDbContextBase : DbContext
|
||||
|
||||
#region Trigger
|
||||
// Configure entities to handle database triggers
|
||||
void AddTrigger<T>() where T : class => _triggers
|
||||
.Where(t => t.Key == typeof(T).Name)
|
||||
.SelectMany(t => t.Value)
|
||||
.ForEach(tName =>
|
||||
void AddTrigger<T>() where T : class
|
||||
{
|
||||
var entityName = typeof(T).Name;
|
||||
|
||||
var configuredTriggerNames = _triggers
|
||||
.Where(t => t.Key == entityName)
|
||||
.SelectMany(t => t.Value)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
foreach (var triggerName in configuredTriggerNames)
|
||||
{
|
||||
#if NET
|
||||
modelBuilder.Entity<T>().ToTable(tb => tb.HasTrigger(tName));
|
||||
modelBuilder.Entity<T>().ToTable(tb => tb.HasTrigger(triggerName));
|
||||
#endif
|
||||
_logger?.LogInformation("Trigger '{triggerName}' has been added to the '{entityName}' entity.", tName, typeof(T).Name);
|
||||
});
|
||||
_logger?.LogInformation("Trigger '{triggerName}' has been added to the '{entityName}' entity.", triggerName, entityName);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: call add trigger methods with attributes and reflection
|
||||
AddTrigger<Config>();
|
||||
@@ -243,4 +251,4 @@ public abstract class EGDbContextBase : DbContext
|
||||
|
||||
#if NETFRAMEWORK
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
#if NET
|
||||
using EnvelopeGenerator.Application.Common.Configurations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.Infrastructure
|
||||
{
|
||||
public class EGDbContextFactory : IDesignTimeDbContextFactory<EGDbContext>
|
||||
{
|
||||
public EGDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.migration.json")
|
||||
.Build();
|
||||
|
||||
// create DbContextOptions
|
||||
var optionsBuilder = new DbContextOptionsBuilder<EGDbContext>();
|
||||
optionsBuilder.UseSqlServer(config.GetConnectionString("Default"));
|
||||
|
||||
// create DbTriggerParams
|
||||
var triggerLists = config.GetSection("DbTriggerParams").Get<Dictionary<string, List<string>>>();
|
||||
var dbTriggerParams = new DbTriggerParams();
|
||||
if (triggerLists is not null)
|
||||
foreach (var triggerList in triggerLists)
|
||||
{
|
||||
if (triggerList.Value.Count == 0)
|
||||
continue; // Skip empty trigger lists
|
||||
|
||||
var tableName = triggerList.Key;
|
||||
|
||||
dbTriggerParams[tableName] = new List<string>();
|
||||
|
||||
foreach (var trigger in triggerList.Value)
|
||||
{
|
||||
dbTriggerParams[tableName].Add(trigger);
|
||||
}
|
||||
}
|
||||
|
||||
var dbContext = new EGDbContext(optionsBuilder.Options, Options.Create(dbTriggerParams));
|
||||
dbContext.IsMigration = true;
|
||||
return dbContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -26,6 +26,7 @@
|
||||
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.6.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||
<PackageReference Include="QuestPDF" Version="2025.7.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
|
||||
46
EnvelopeGenerator.Infrastructure/UserPreferencesService.cs
Normal file
46
EnvelopeGenerator.Infrastructure/UserPreferencesService.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
#if NET
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||
|
||||
namespace EnvelopeGenerator.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Implements user preferences storage using the distributed cache (dbo.TBDD_CACHE).
|
||||
/// No DB schema changes required — uses the existing SQL Server distributed cache table.
|
||||
/// Cache key schema: "signflow.user-preferences:{username}"
|
||||
/// Expiration: indefinite (no TTL) — preferences persist until explicitly overwritten.
|
||||
/// </summary>
|
||||
public sealed class UserPreferencesService(IDistributedCache cache) : IUserPreferencesService
|
||||
{
|
||||
private const string KeyPrefix = "signflow.user-preferences:";
|
||||
|
||||
private static string BuildKey(string id) => $"{KeyPrefix}{id}";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<UserPreferencesDto> GetOrCreateAsync(string id, CancellationToken ct = default)
|
||||
{
|
||||
var json = await cache.GetStringAsync(BuildKey(id), ct);
|
||||
if(json is not null && JsonSerializer.Deserialize<UserPreferencesDto>(json) is UserPreferencesDto pref)
|
||||
{
|
||||
return pref;
|
||||
}
|
||||
else
|
||||
{
|
||||
// init new
|
||||
await SaveAsync(id, UserPreferencesDto.Default, ct);
|
||||
return UserPreferencesDto.Default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SaveAsync(string id, UserPreferencesDto preferences, CancellationToken ct = default)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(preferences);
|
||||
|
||||
// No expiration — preferences are stored indefinitely.
|
||||
await cache.SetStringAsync(BuildKey(id), json, new DistributedCacheEntryOptions(), ct);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,10 +0,0 @@
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
@@ -1,51 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<WasmBuildNative>true</WasmBuildNative>
|
||||
<InvariantGlobalization>false</InvariantGlobalization>
|
||||
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
|
||||
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageId>EnvelopeGenerator.ReceiverUI</PackageId>
|
||||
<Authors>Digital Data GmbH</Authors>
|
||||
<Company>Digital Data GmbH</Company>
|
||||
<Product>EnvelopeGenerator.ReceiverUI</Product>
|
||||
<PackageIcon>Assets\icon.ico</PackageIcon>
|
||||
<PackageTags>digital data envelope generator web</PackageTags>
|
||||
<Description>EnvelopeGenerator.ReceiverUI is a Blazor WebAssembly application developed to manage signing processes. It uses Entity Framework Core (EF Core) for database operations. The user interface for signing processes is developed with Razor View Engine (.cshtml files) and JavaScript under wwwroot, integrated with PSPDFKit. This integration allows users to view and sign documents seamlessly.</Description>
|
||||
<Version>1.4.2</Version>
|
||||
<!-- NuGet package version -->
|
||||
<AssemblyVersion>1.4.2.0</AssemblyVersion>
|
||||
<!-- Assembly version for API compatibility -->
|
||||
<FileVersion>1.4.2.0</FileVersion>
|
||||
<!-- Windows file version -->
|
||||
<Copyright>Copyright © 2026 Digital Data GmbH. All rights reserved.</Copyright>
|
||||
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.3" />
|
||||
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.1" />
|
||||
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
|
||||
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.11" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Properties\PublishProfiles\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Update="wwwroot\docs\privacy-policy.en-US.html">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="wwwroot\docs\privacy-policy.fr-FR.html">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,12 +0,0 @@
|
||||
The following open source libraries are used and included within thеse sample/demonstration projects:
|
||||
|
||||
Bootstrap (Open Source – MIT License)
|
||||
Copyright (c) 2011-2021 Twitter, Inc. / Copyright (c) 2011-2021 The Bootstrap Authors
|
||||
https://github.com/twbs/bootstrap/blob/main/LICENSE
|
||||
|
||||
open-iconic (Open Source – MIT License and SIL Open Font License)
|
||||
Copyright (c) 2014 Waybury
|
||||
https://github.com/iconic/open-iconic/blob/master/ICON-LICENSE
|
||||
https://github.com/iconic/open-iconic/blob/master/FONT-LICENSE
|
||||
|
||||
The open source libraries included in these sample/demonstration projects are done so pursuant to each individual open source library license and subject to the disclaimers and limitations on liability set forth in each open source library license.
|
||||
@@ -1,7 +0,0 @@
|
||||
@page "/sender"
|
||||
|
||||
<h3>EnvelopeSender</h3>
|
||||
|
||||
@code {
|
||||
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
@page "/example/documentviewer"
|
||||
|
||||
<DxDocumentViewer ReportName="LargeDatasetReport" CssClass="dx-blazor-reporting-container" Height="@null" Width="@null">
|
||||
<DxDocumentViewerTabPanelSettings Width="340" />
|
||||
</DxDocumentViewer>
|
||||
@@ -1,19 +0,0 @@
|
||||
@page "/example/sender"
|
||||
@using DevExpress.DataAccess.Json;
|
||||
@using EnvelopeGenerator.ReceiverUI.Services;
|
||||
|
||||
<DxReportDesigner ReportName="LargeDatasetReport" CssClass="dx-blazor-reporting-container" Height="@null" Width="@null" AllowMDI="true" DataSources="DataSources">
|
||||
<DxReportDesignerWizardSettings UseFullscreenWizard="true" />
|
||||
</DxReportDesigner>
|
||||
|
||||
@code {
|
||||
Dictionary<string, object> DataSources = new Dictionary<string, object>();
|
||||
protected override async Task OnInitializedAsync() {
|
||||
await base.OnInitializedAsync();
|
||||
var connection = CustomDataSourceWizardJsonDataConnectionStorage.GetDefaultConnection();
|
||||
JsonDataSource jsonDataSource = new JsonDataSource();
|
||||
jsonDataSource.JsonSource = connection.GetJsonSource();
|
||||
await jsonDataSource.FillAsync();
|
||||
DataSources.Add("Northwind", jsonDataSource);
|
||||
}
|
||||
}
|
||||
@@ -1,746 +0,0 @@
|
||||
@page "/example/receiver/{EnvelopeKey}"
|
||||
@page "/example/report-viewer/{EnvelopeKey}"
|
||||
@using System.Drawing
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.Drawing
|
||||
@using DevExpress.Utils
|
||||
@using DevExpress.XtraPrinting
|
||||
@using DevExpress.XtraPrinting.Drawing
|
||||
@using Microsoft.JSInterop
|
||||
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
|
||||
@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand
|
||||
@using XRLabel = DevExpress.XtraReports.UI.XRLabel
|
||||
@using XRPictureBox = DevExpress.XtraReports.UI.XRPictureBox
|
||||
@using XRControl = DevExpress.XtraReports.UI.XRControl
|
||||
@using ImageSizeMode = DevExpress.XtraPrinting.ImageSizeMode
|
||||
@using EnvelopeGenerator.ReceiverUI.Services
|
||||
@using DevExpress.Blazor.Reporting
|
||||
@using Microsoft.Extensions.Options
|
||||
@using EnvelopeGenerator.ReceiverUI.Options
|
||||
@using EnvelopeGenerator.ReceiverUI.Models
|
||||
@implements IDisposable
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject AnnotationService AnnotationService
|
||||
@inject IOptions<ApiOptions> AppOptions
|
||||
@inject NavigationManager Navigation
|
||||
@inject InMemoryReportStorageWebExtension ReportStorage
|
||||
@inject EnvelopeGenerator.ReceiverUI.Services.DocumentService DocumentService
|
||||
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
|
||||
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
<link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" />
|
||||
|
||||
<div class="receiver-page-layout">
|
||||
|
||||
<div class="receiver-signature-panel">
|
||||
|
||||
@* ?? Envelope info header ???????????????????????????????????????????????? *@
|
||||
@if (_envelopeReceiver is not null) {
|
||||
<div class="receiver-info-header">
|
||||
<div class="receiver-info-header__gradient">
|
||||
<div class="receiver-info-header__left">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="currentColor" class="receiver-info-header__icon" viewBox="0 0 16 16">
|
||||
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<div class="receiver-info-header__title">@(_envelopeReceiver.Envelope?.Title ?? "Dokument")</div>
|
||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName) || !string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) {
|
||||
<div class="receiver-info-header__sender">
|
||||
Von
|
||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName)) {
|
||||
<strong>@_envelopeReceiver.Envelope!.User!.FullName</strong>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) {
|
||||
<span class="opacity-75"><@_envelopeReceiver.Envelope!.User!.Email></span>
|
||||
}
|
||||
· @_envelopeReceiver.Envelope?.AddedWhen.ToString("dd.MM.yyyy")
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="receiver-info-header__badges">
|
||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Name)) {
|
||||
<span class="receiver-info-badge">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Z"/>
|
||||
</svg>
|
||||
@_envelopeReceiver.Name
|
||||
</span>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.CompanyName)) {
|
||||
<span class="receiver-info-badge">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M14.763.075A.5.5 0 0 1 15 .5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5V14h-1v1.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V10a.5.5 0 0 1 .342-.474L6 7.64V4.5a.5.5 0 0 1 .276-.447l8-4a.5.5 0 0 1 .487.022Z"/>
|
||||
</svg>
|
||||
@_envelopeReceiver.CompanyName
|
||||
</span>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.JobTitle)) {
|
||||
<span class="receiver-info-badge receiver-info-badge--muted">@_envelopeReceiver.JobTitle</span>
|
||||
}
|
||||
@{
|
||||
var docElements = _envelopeReceiver.Envelope?.Documents?.FirstOrDefault()?.Elements;
|
||||
int sigCount = docElements?.Count() ?? _annotations.Count;
|
||||
}
|
||||
@if (sigCount > 0) {
|
||||
<span class="receiver-info-badge receiver-info-badge--accent">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
</svg>
|
||||
@sigCount @(sigCount == 1 ? "Unterschriftsfeld" : "Unterschriftsfelder")
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message)) {
|
||||
<div class="receiver-info-message">@_envelopeReceiver.Envelope!.Message</div>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage)) {
|
||||
<div class="receiver-info-private-message">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1 flex-shrink-0" viewBox="0 0 16 16">
|
||||
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||||
</svg>
|
||||
@_envelopeReceiver.PrivateMessage
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ?? Signature action bar ???????????????????????????????????????????????? *@
|
||||
<div class="receiver-action-bar">
|
||||
<div class="receiver-action-bar__inner">
|
||||
<button class="btn btn-sm @(_capturedSignature is not null ? "btn-outline-success" : "btn-primary")" @onclick="OpenSignaturePopupAsync">
|
||||
@if (_capturedSignature is not null) {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
||||
</svg>
|
||||
<span>Unterschrift gespeichert</span>
|
||||
} else {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
</svg>
|
||||
<span>Unterschrift erstellen</span>
|
||||
}
|
||||
</button>
|
||||
|
||||
@if (_annotations.Count > 0 && !SignatureApplied) {
|
||||
<div class="receiver-action-bar__progress">
|
||||
<div class="progress" style="height:5px; min-width:70px; width:90px;">
|
||||
<div class="progress-bar @(_checkedAnnotations.Count == _annotations.Count ? "bg-success" : "bg-primary")"
|
||||
style="width:@(_annotations.Count > 0 ? (_checkedAnnotations.Count * 100 / _annotations.Count) : 0)%"
|
||||
role="progressbar"></div>
|
||||
</div>
|
||||
<span class="text-muted" style="font-size:0.75rem;">
|
||||
@_checkedAnnotations.Count / @_annotations.Count
|
||||
Seite@(AnnotationPages.Count() == 1 ? "" : "n") @string.Join(",", AnnotationPages)
|
||||
</span>
|
||||
@if (_capturedSignature is null) {
|
||||
<span class="text-muted fst-italic" style="font-size:0.72rem;">Zuerst Unterschrift erstellen</span>
|
||||
} else if (_checkedAnnotations.Count == _annotations.Count) {
|
||||
<span class="text-success fw-semibold" style="font-size:0.72rem;">✓ Wird angewendet…</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(SignatureValidationMessage)) {
|
||||
<span class="text-danger" style="font-size:0.78rem;">@SignatureValidationMessage</span>
|
||||
}
|
||||
@if (SignatureApplied) {
|
||||
<span class="text-success" style="font-size:0.78rem;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
|
||||
</svg>
|
||||
Unterschrift angewendet
|
||||
</span>
|
||||
}
|
||||
|
||||
<div class="ms-auto d-flex align-items-center gap-2">
|
||||
<button class="btn btn-sm btn-success" disabled="@(!SignatureApplied)" @onclick="ExportSignedPdfAsync">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||
</svg>
|
||||
PDF exportieren
|
||||
</button>
|
||||
@if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||||
<button class="btn btn-sm btn-outline-danger" @onclick="LogoutAsync" disabled="@IsLoggingOut" title="Abmelden">
|
||||
@if (IsLoggingOut) {
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
} else {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
|
||||
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<DxPopup @bind-Visible="SignaturePopupVisible"
|
||||
HeaderText="Unterschrift erfassen"
|
||||
Width="620px"
|
||||
ShowFooter="true"
|
||||
CloseOnEscape="false"
|
||||
ShowCloseButton="false"
|
||||
CloseOnOutsideClick="false"
|
||||
Shown="OnPopupShownAsync">
|
||||
<BodyContentTemplate>
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item">
|
||||
<button type="button" class="nav-link @(ActiveSignatureTab == SignatureTabDraw ? "active" : null)" @onclick="() => SetSignatureTabAsync(SignatureTabDraw)">Zeichnen</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button type="button" class="nav-link @(ActiveSignatureTab == SignatureTabText ? "active" : null)" @onclick="() => SetSignatureTabAsync(SignatureTabText)">Text</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button type="button" class="nav-link @(ActiveSignatureTab == SignatureTabImage ? "active" : null)" @onclick="() => SetSignatureTabAsync(SignatureTabImage)">Bild</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@if(ActiveSignatureTab == SignatureTabDraw) {
|
||||
<p class="text-muted mb-2">Bitte unterschreiben Sie im folgenden Feld.</p>
|
||||
<canvas id="receiver-signature-pad" width="520" height="160" class="border rounded bg-white w-100" style="max-width: 520px; touch-action: none;"></canvas>
|
||||
} else if(ActiveSignatureTab == SignatureTabText) {
|
||||
<p class="text-muted mb-2">Geben Sie Ihre Unterschrift als Text ein und waehlen Sie eine Schriftart.</p>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-12 col-md-7">
|
||||
<input class="form-control" placeholder="Ihre Unterschrift" value="@TypedSignatureText" @oninput="OnTypedSignatureChanged" />
|
||||
</div>
|
||||
<div class="col-12 col-md-5">
|
||||
<select class="form-select" value="@TypedSignatureFont" @onchange="OnTypedSignatureFontChanged">
|
||||
@foreach(var font in TypedSignatureFonts) {
|
||||
<option value="@font.Value" style="font-family: @font.Value">@font.Text</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="receiver-typed-signature-pad" width="520" height="160" class="border rounded bg-white w-100" style="max-width: 520px;"></canvas>
|
||||
} else {
|
||||
<p class="text-muted mb-2">Laden Sie ein Bild Ihrer Unterschrift hoch.</p>
|
||||
<input id="receiver-signature-image-input" class="form-control mb-2" type="file" accept="image/png,image/jpeg,image/webp" />
|
||||
<canvas id="receiver-image-signature-pad" width="520" height="160" class="border rounded bg-white w-100" style="max-width: 520px;"></canvas>
|
||||
}
|
||||
|
||||
|
||||
<div class="border-top mt-3 pt-3">
|
||||
<p class="text-muted mb-2">Bitte geben Sie die folgenden Angaben ein. Das Datum wird automatisch hinzugefuegt.</p>
|
||||
<div class="row g-2">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="receiver-signer-name">Vor- und Nachname *</label>
|
||||
<input id="receiver-signer-name" class="form-control" value="@SignerFullName" @oninput="args => SignerFullName = args.Value?.ToString() ?? string.Empty" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="receiver-signer-position">Position</label>
|
||||
<input id="receiver-signer-position" class="form-control" value="@SignerPosition" @oninput="args => SignerPosition = args.Value?.ToString() ?? string.Empty" />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label" for="receiver-signature-place">Ort *</label>
|
||||
<input id="receiver-signature-place" class="form-control" value="@SignaturePlace" @oninput="args => SignaturePlace = args.Value?.ToString() ?? string.Empty" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!string.IsNullOrWhiteSpace(PopupValidationMessage)) {
|
||||
<div class="text-danger mt-2">@PopupValidationMessage</div>
|
||||
}
|
||||
</BodyContentTemplate>
|
||||
<FooterContentTemplate>
|
||||
<div class="d-flex gap-2 flex-wrap justify-content-end w-100">
|
||||
<button class="btn btn-outline-secondary" @onclick="RenewSignatureAsync">Unterschrift erneuern</button>
|
||||
<button class="btn btn-primary" @onclick="SaveSignatureAsync">Speichern</button>
|
||||
</div>
|
||||
</FooterContentTemplate>
|
||||
</DxPopup>
|
||||
|
||||
<div class="receiver-viewer-wrapper">
|
||||
@if(Report is not null) {
|
||||
<DxReportViewer @key="ViewerKey" @ref="reportViewer" Report="Report" RootCssClasses="w-100 h-100" Zoom="1.3" />
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
const string SignatureTabDraw = "draw";
|
||||
const string SignatureTabText = "text";
|
||||
const string SignatureTabImage = "image";
|
||||
const string DrawCanvasId = "receiver-signature-pad";
|
||||
const string TypedCanvasId = "receiver-typed-signature-pad";
|
||||
const string ImageInputId = "receiver-signature-image-input";
|
||||
const string ImageCanvasId = "receiver-image-signature-pad";
|
||||
|
||||
readonly (string Text, string Value)[] TypedSignatureFonts = {
|
||||
("Brush Script", "'Brush Script MT', cursive"),
|
||||
("Segoe Script", "'Segoe Script', cursive"),
|
||||
("Lucida Handwriting", "'Lucida Handwriting', cursive"),
|
||||
("Comic Sans", "'Comic Sans MS', cursive"),
|
||||
("Cursive", "cursive")
|
||||
};
|
||||
|
||||
[Parameter] public string EnvelopeKey { get; set; } = string.Empty;
|
||||
|
||||
DxReportViewer? reportViewer;
|
||||
XtraReport? Report;
|
||||
bool SignatureApplied;
|
||||
bool SignaturePopupVisible;
|
||||
string? SignatureValidationMessage;
|
||||
string? PopupValidationMessage;
|
||||
string ActiveSignatureTab = SignatureTabDraw;
|
||||
string TypedSignatureText = string.Empty;
|
||||
string TypedSignatureFont = "'Brush Script MT', cursive";
|
||||
string SignerFullName = string.Empty;
|
||||
string SignerPosition = string.Empty;
|
||||
string SignaturePlace = string.Empty;
|
||||
int ViewerKey;
|
||||
bool IsLoggingOut;
|
||||
|
||||
IReadOnlyList<AnnotationDto> _annotations = [];
|
||||
IEnumerable<int> AnnotationPages => _annotations.Select(a => a.Page).Distinct().OrderBy(p => p);
|
||||
EnvelopeReceiverDto? _envelopeReceiver;
|
||||
record SignatureCapture(string DataUrl, string FullName, string Position, string Place);
|
||||
SignatureCapture? _capturedSignature;
|
||||
byte[]? _basePdfBytes;
|
||||
// annotation IDs the user has checked via overlay checkboxes
|
||||
readonly HashSet<long> _checkedAnnotations = [];
|
||||
DotNetObjectReference<ReportViewer>? _dotNetRef;
|
||||
int _lastOverlayViewerKey = -1;
|
||||
|
||||
async Task LogoutAsync() {
|
||||
if (string.IsNullOrWhiteSpace(EnvelopeKey) || IsLoggingOut) return;
|
||||
IsLoggingOut = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey);
|
||||
Navigation.NavigateTo($"/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
|
||||
// ? REDIRECT: /receiver/{key} -> /envelope/{key} (NEW PDF.js viewer)
|
||||
if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||||
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||||
if (!hasAccess) {
|
||||
Navigation.NavigateTo($"/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
ActiveSignatureTab = SignatureTabDraw;
|
||||
SignaturePopupVisible = true;
|
||||
SignatureValidationMessage = null;
|
||||
PopupValidationMessage = null;
|
||||
}
|
||||
}
|
||||
|
||||
_annotations = await AnnotationService.GetAnnotationsAsync(EnvelopeKey);
|
||||
|
||||
try {
|
||||
_envelopeReceiver = await EnvelopeReceiverService.GetAsync(EnvelopeKey);
|
||||
} catch (HttpRequestException ex) {
|
||||
// Log error but continue - UI will handle null envelope receiver gracefully
|
||||
Console.WriteLine($"Failed to load envelope receiver: {ex.Message}");
|
||||
}
|
||||
|
||||
if (!AppOptions.Value.UsePredefinedReports && !string.IsNullOrWhiteSpace(EnvelopeKey)) {
|
||||
try {
|
||||
var pdfBytes = await DocumentService.GetDocumentAsync(EnvelopeKey);
|
||||
if (pdfBytes is { Length: > 0 })
|
||||
_basePdfBytes = pdfBytes;
|
||||
} catch (HttpRequestException ex) {
|
||||
// Log error but continue - will use predefined report instead
|
||||
Console.WriteLine($"Failed to load document: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var initialReport = BuildFreshBaseReport();
|
||||
Report = initialReport;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
||||
if (firstRender)
|
||||
_dotNetRef = DotNetObjectReference.Create(this);
|
||||
|
||||
|
||||
if (Report is not null && _annotations.Count > 0
|
||||
&& _capturedSignature is not null && !SignatureApplied
|
||||
&& _lastOverlayViewerKey != ViewerKey) {
|
||||
_lastOverlayViewerKey = ViewerKey;
|
||||
await JSRuntime.InvokeVoidAsync(
|
||||
"receiverSignature.installAnnotationCheckboxes",
|
||||
_annotations, _checkedAnnotations.ToArray(), _dotNetRef);
|
||||
}
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public async Task OnAnnotationToggled(long annotationId, bool isChecked) {
|
||||
if (isChecked)
|
||||
_checkedAnnotations.Add(annotationId);
|
||||
else
|
||||
_checkedAnnotations.Remove(annotationId);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
if (_capturedSignature is not null
|
||||
&& !SignatureApplied
|
||||
&& _annotations.Count > 0
|
||||
&& _checkedAnnotations.Count == _annotations.Count) {
|
||||
// K?sa bekleme: kullan?c? son tick'in görsel feedback'ini görsün
|
||||
await Task.Delay(400);
|
||||
await SubmitSignaturesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
void OpenSignaturePopupAsync() {
|
||||
ActiveSignatureTab = SignatureTabDraw;
|
||||
SignaturePopupVisible = true;
|
||||
SignatureValidationMessage = null;
|
||||
PopupValidationMessage = null;
|
||||
}
|
||||
|
||||
async Task OnPopupShownAsync() {
|
||||
await InitializeActiveSignatureTabAsync();
|
||||
}
|
||||
|
||||
async Task SetSignatureTabAsync(string tab) {
|
||||
ActiveSignatureTab = tab;
|
||||
PopupValidationMessage = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await Task.Delay(50);
|
||||
await InitializeActiveSignatureTabAsync();
|
||||
}
|
||||
|
||||
async Task InitializeActiveSignatureTabAsync() {
|
||||
if(ActiveSignatureTab == SignatureTabDraw) {
|
||||
await JSRuntime.InvokeVoidAsync("receiverSignature.initialize", DrawCanvasId);
|
||||
} else if(ActiveSignatureTab == SignatureTabText) {
|
||||
await JSRuntime.InvokeVoidAsync("receiverSignature.initializeTyped", TypedCanvasId);
|
||||
await RenderTypedSignatureAsync();
|
||||
} else {
|
||||
await JSRuntime.InvokeVoidAsync("receiverSignature.initializeImage", ImageInputId, ImageCanvasId);
|
||||
}
|
||||
}
|
||||
|
||||
async Task RenewSignatureAsync() {
|
||||
PopupValidationMessage = null;
|
||||
|
||||
if(ActiveSignatureTab == SignatureTabDraw) {
|
||||
await JSRuntime.InvokeVoidAsync("receiverSignature.clear", DrawCanvasId);
|
||||
} else if(ActiveSignatureTab == SignatureTabText) {
|
||||
TypedSignatureText = string.Empty;
|
||||
await JSRuntime.InvokeVoidAsync("receiverSignature.clearTyped", TypedCanvasId);
|
||||
} else {
|
||||
await JSRuntime.InvokeVoidAsync("receiverSignature.clearImage", ImageInputId, ImageCanvasId);
|
||||
}
|
||||
}
|
||||
|
||||
void CloseSignaturePopup() {
|
||||
PopupValidationMessage = null;
|
||||
SignaturePopupVisible = false;
|
||||
}
|
||||
|
||||
async Task OnTypedSignatureChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) {
|
||||
TypedSignatureText = args.Value?.ToString() ?? string.Empty;
|
||||
await RenderTypedSignatureAsync();
|
||||
}
|
||||
|
||||
async Task OnTypedSignatureFontChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) {
|
||||
TypedSignatureFont = args.Value?.ToString() ?? TypedSignatureFont;
|
||||
await RenderTypedSignatureAsync();
|
||||
}
|
||||
|
||||
async Task RenderTypedSignatureAsync() {
|
||||
await JSRuntime.InvokeVoidAsync("receiverSignature.renderTypedSignature", TypedCanvasId, TypedSignatureText, TypedSignatureFont);
|
||||
}
|
||||
|
||||
async Task SaveSignatureAsync() {
|
||||
if (string.IsNullOrWhiteSpace(SignerFullName)) {
|
||||
PopupValidationMessage = "Bitte geben Sie Vor- und Nachname ein.";
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(SignaturePlace)) {
|
||||
PopupValidationMessage = "Bitte geben Sie den Ort ein.";
|
||||
return;
|
||||
}
|
||||
var signatureDataUrl = await GetActiveSignatureDataUrlAsync();
|
||||
if (string.IsNullOrWhiteSpace(signatureDataUrl)) {
|
||||
PopupValidationMessage = "Die Unterschrift ist fuer den PDF-Export erforderlich.";
|
||||
return;
|
||||
}
|
||||
|
||||
PopupValidationMessage = null;
|
||||
SignatureValidationMessage = null;
|
||||
_capturedSignature = new(signatureDataUrl, SignerFullName.Trim(), SignerPosition.Trim(), SignaturePlace.Trim());
|
||||
|
||||
// If no annotations, apply immediately (no checkbox step needed)
|
||||
if (_annotations.Count == 0) {
|
||||
var freshReport = BuildFreshBaseReport();
|
||||
AddSignature(freshReport, _capturedSignature.DataUrl, _capturedSignature.FullName, _capturedSignature.Position, _capturedSignature.Place);
|
||||
Report = freshReport;
|
||||
SignatureApplied = true;
|
||||
SignaturePopupVisible = false;
|
||||
ViewerKey++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Close popup; checkboxes will appear on the PDF via OnAfterRenderAsync
|
||||
SignaturePopupVisible = false;
|
||||
_lastOverlayViewerKey = -1; // force overlay reinstall
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
async Task SubmitSignaturesAsync() {
|
||||
if (_checkedAnnotations.Count == 0) {
|
||||
SignatureValidationMessage = "Bitte markieren Sie mindestens ein Unterschriftsfeld im Dokument.";
|
||||
return;
|
||||
}
|
||||
if (_checkedAnnotations.Count < _annotations.Count) {
|
||||
SignatureValidationMessage = $"Bitte markieren Sie alle {_annotations.Count} Unterschriftsfelder. Noch {_annotations.Count - _checkedAnnotations.Count} offen.";
|
||||
return;
|
||||
}
|
||||
|
||||
SignatureValidationMessage = null;
|
||||
var freshReport = BuildFreshBaseReport();
|
||||
foreach (var ann in _annotations)
|
||||
AddSignatureAtAnnotation(freshReport, ann, _capturedSignature!.DataUrl, _capturedSignature.FullName, _capturedSignature.Position, _capturedSignature.Place);
|
||||
|
||||
Report = freshReport;
|
||||
SignatureApplied = true;
|
||||
ViewerKey++;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
async Task<string?> GetActiveSignatureDataUrlAsync() {
|
||||
if(ActiveSignatureTab == SignatureTabDraw)
|
||||
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getDataUrl", DrawCanvasId);
|
||||
|
||||
if(ActiveSignatureTab == SignatureTabText) {
|
||||
await RenderTypedSignatureAsync();
|
||||
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getTypedDataUrl", TypedCanvasId);
|
||||
}
|
||||
|
||||
return await JSRuntime.InvokeAsync<string?>("receiverSignature.getImageDataUrl", ImageCanvasId);
|
||||
}
|
||||
|
||||
async Task ExportSignedPdfAsync() {
|
||||
if(!SignatureApplied || Report is null) {
|
||||
SignatureValidationMessage = "Bitte fuegen Sie die Unterschrift zuerst zum Bericht hinzu.";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SignatureValidationMessage = null;
|
||||
await reportViewer!.ExportToAsync(ExportFormat.Pdf);
|
||||
} catch(Exception) {
|
||||
SignatureValidationMessage = "Das signierte PDF konnte nicht exportiert werden. Bitte laden Sie die Seite neu und versuchen Sie es erneut.";
|
||||
}
|
||||
}
|
||||
|
||||
XtraReport BuildFreshBaseReport() {
|
||||
if (_basePdfBytes is { Length: > 0 }) {
|
||||
var report = new XtraReport();
|
||||
var detail = new DevExpress.XtraReports.UI.DetailBand();
|
||||
report.Bands.Add(detail);
|
||||
detail.Controls.Add(new DevExpress.XtraReports.UI.XRPdfContent { Source = _basePdfBytes, GenerateOwnPages = true });
|
||||
return report;
|
||||
}
|
||||
return CreateReportInstance();
|
||||
}
|
||||
|
||||
static void AddAnnotationPlaceholders(XtraReport report, IReadOnlyList<AnnotationDto> annotations) {
|
||||
var bottomMargin = report.Bands.OfType<BottomMarginBand>().FirstOrDefault();
|
||||
if (bottomMargin is null) {
|
||||
bottomMargin = new BottomMarginBand();
|
||||
report.Bands.Add(bottomMargin);
|
||||
}
|
||||
|
||||
const float sigWidth = 230F;
|
||||
const float sigHeight = 154F;
|
||||
const float bottomPad = 6F;
|
||||
const float defaultTopPad = 8F;
|
||||
const float maxBandHeight = 210F;
|
||||
|
||||
float requiredHeight = defaultTopPad + sigHeight + bottomPad;
|
||||
bottomMargin.HeightF = Math.Min(maxBandHeight, Math.Max(bottomMargin.HeightF, requiredHeight));
|
||||
float topPad = Math.Max(0F, bottomMargin.HeightF - bottomPad - sigHeight);
|
||||
|
||||
foreach (var ann in annotations) {
|
||||
float sigX = (float)(ann.X);
|
||||
var annotId = ann.Id.ToString();
|
||||
|
||||
var placeholder = new XRLabel {
|
||||
Name = $"receiverSignaturePlaceholder_{annotId}",
|
||||
Text = "\u270e Bitte unterschreiben",
|
||||
BoundsF = new RectangleF(sigX, topPad, sigWidth, sigHeight),
|
||||
Borders = BorderSide.All,
|
||||
BorderColor = System.Drawing.Color.FromArgb(230, 81, 0),
|
||||
BackColor = System.Drawing.Color.FromArgb(30, 255, 236, 153),
|
||||
Font = new DXFont("Open Sans", 10F, DXFontStyle.Regular),
|
||||
ForeColor = System.Drawing.Color.FromArgb(94, 38, 0),
|
||||
TextAlignment = TextAlignment.MiddleCenter
|
||||
};
|
||||
|
||||
bottomMargin.Controls.Add(placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
XtraReport CreateReportInstance() {
|
||||
return ReportStorage.TryGetReport("LargeDatasetReport", out var savedReport)
|
||||
? savedReport
|
||||
: PredefinedReports.ReportsFactory.GetReport("LargeDatasetReport");
|
||||
}
|
||||
|
||||
static void AddSignature(XtraReport report, string signatureDataUrl, string signerFullName, string signerPosition, string signaturePlace) {
|
||||
var imageBytes = Convert.FromBase64String(signatureDataUrl[(signatureDataUrl.IndexOf(',') + 1)..]);
|
||||
using var imageStream = new MemoryStream(imageBytes);
|
||||
var imageSource = new ImageSource(DXImage.FromStream(imageStream));
|
||||
var bottomMargin = report.Bands.OfType<BottomMarginBand>().FirstOrDefault();
|
||||
|
||||
if(bottomMargin is null) {
|
||||
bottomMargin = new BottomMarginBand();
|
||||
report.Bands.Add(bottomMargin);
|
||||
}
|
||||
|
||||
RemoveExistingSignature(bottomMargin);
|
||||
|
||||
// Layout constants
|
||||
const float sigX = 390F;
|
||||
const float sigWidth = 230F;
|
||||
const float sigImgHeight = 70F;
|
||||
const float infoHeight = 65F; // up to 4 lines at 8pt
|
||||
const float innerGap = 5F;
|
||||
const float bottomPad = 6F;
|
||||
const float defaultTopPad = 8F;
|
||||
const float maxBandHeight = 210F;
|
||||
|
||||
float requiredHeight = defaultTopPad + sigImgHeight + innerGap + infoHeight + bottomPad;
|
||||
|
||||
// Grow band if needed, but cap at maxBandHeight to avoid overlapping page content
|
||||
bottomMargin.HeightF = Math.Min(maxBandHeight, Math.Max(bottomMargin.HeightF, requiredHeight));
|
||||
|
||||
// If band is tighter than required, compress top padding so content still fits
|
||||
float topPad = Math.Max(0F, bottomMargin.HeightF - bottomPad - infoHeight - innerGap - sigImgHeight);
|
||||
|
||||
float imageY = topPad;
|
||||
float labelY = imageY + sigImgHeight + innerGap;
|
||||
|
||||
var signatureInformation = string.IsNullOrWhiteSpace(signerPosition)
|
||||
? $"{signerFullName}\n{signaturePlace}, {DateTime.Now:d}"
|
||||
: $"{signerFullName}\n{signerPosition}\n{signaturePlace}, {DateTime.Now:d}";
|
||||
|
||||
var signature = new XRPictureBox {
|
||||
Name = "receiverSignatureImage",
|
||||
ImageSource = imageSource,
|
||||
BoundsF = new RectangleF(sigX, imageY, sigWidth, sigImgHeight),
|
||||
Sizing = ImageSizeMode.ZoomImage,
|
||||
Borders = BorderSide.Bottom,
|
||||
BorderColor = System.Drawing.Color.FromArgb(73, 80, 87)
|
||||
};
|
||||
|
||||
var signatureLabel = new XRLabel {
|
||||
Name = "receiverSignatureLabel",
|
||||
Text = signatureInformation,
|
||||
Multiline = true,
|
||||
BoundsF = new RectangleF(sigX, labelY, sigWidth, infoHeight),
|
||||
Font = new DXFont("Open Sans", 8F, DXFontStyle.Regular),
|
||||
ForeColor = System.Drawing.Color.FromArgb(73, 80, 87),
|
||||
TextAlignment = TextAlignment.TopLeft
|
||||
};
|
||||
|
||||
bottomMargin.Controls.AddRange(new XRControl[] { signature, signatureLabel });
|
||||
}
|
||||
|
||||
static void RemoveExistingSignature(BottomMarginBand bottomMargin) {
|
||||
var controls = bottomMargin.Controls
|
||||
.Cast<XRControl>()
|
||||
.Where(control => control.Name is "receiverSignatureLabel" or "receiverSignatureImage")
|
||||
.ToArray();
|
||||
|
||||
foreach(var control in controls)
|
||||
bottomMargin.Controls.Remove(control);
|
||||
}
|
||||
|
||||
static void AddSignatureAtAnnotation(XtraReport report, AnnotationDto? annotation, string signatureDataUrl, string signerFullName, string signerPosition, string signaturePlace) {
|
||||
var imageBytes = Convert.FromBase64String(signatureDataUrl[(signatureDataUrl.IndexOf(',') + 1)..]);
|
||||
using var imageStream = new MemoryStream(imageBytes);
|
||||
var imageSource = new ImageSource(DXImage.FromStream(imageStream));
|
||||
|
||||
var detail = report.Bands.OfType<DevExpress.XtraReports.UI.DetailBand>().FirstOrDefault();
|
||||
if (detail is null) return;
|
||||
|
||||
var annotId = annotation?.Id.ToString() ?? "0";
|
||||
RemoveExistingSignatureById(detail, annotId);
|
||||
|
||||
const float sigWidth = 230F;
|
||||
const float sigImgHeight = 70F;
|
||||
const float infoHeight = 48.75F;
|
||||
const float innerGap = 5F;
|
||||
|
||||
float sigX = (float)(annotation?.X ?? 390.0);
|
||||
float imageY = (float)(annotation?.Y ?? 900.0);
|
||||
float labelY = imageY + sigImgHeight + innerGap;
|
||||
int targetPage = annotation?.Page ?? 1;
|
||||
|
||||
var signatureInformation = string.IsNullOrWhiteSpace(signerPosition)
|
||||
? $"{signerFullName}\n{signaturePlace}, {DateTime.Now:d}"
|
||||
: $"{signerFullName}\n{signerPosition}\n{signaturePlace}, {DateTime.Now:d}";
|
||||
|
||||
var signature = new XRPictureBox {
|
||||
Name = $"receiverSignatureImage_{annotId}",
|
||||
ImageSource = imageSource,
|
||||
BoundsF = new RectangleF(sigX, imageY, sigWidth, sigImgHeight),
|
||||
Sizing = ImageSizeMode.ZoomImage,
|
||||
Borders = BorderSide.Bottom,
|
||||
BorderColor = System.Drawing.Color.FromArgb(73, 80, 87),
|
||||
BackColor = Color.FromArgb(219, 219, 219)
|
||||
};
|
||||
|
||||
var signatureLabel = new XRLabel {
|
||||
Name = $"receiverSignatureLabel_{annotId}",
|
||||
Text = signatureInformation,
|
||||
Multiline = true,
|
||||
BoundsF = new RectangleF(sigX, labelY - 5, sigWidth, infoHeight),
|
||||
Font = new DXFont("Open Sans", 8F, DXFontStyle.Regular),
|
||||
ForeColor = System.Drawing.Color.FromArgb(73, 80, 87),
|
||||
TextAlignment = TextAlignment.TopCenter,
|
||||
BackColor = Color.FromArgb(219, 219, 219)
|
||||
};
|
||||
|
||||
// Show each control only on the target page using an independent print counter
|
||||
int sigPrintCount = 0;
|
||||
signature.BeforePrint += (_, e) => {
|
||||
sigPrintCount++;
|
||||
e.Cancel = sigPrintCount != targetPage;
|
||||
};
|
||||
|
||||
int lblPrintCount = 0;
|
||||
signatureLabel.BeforePrint += (_, e) => {
|
||||
lblPrintCount++;
|
||||
e.Cancel = lblPrintCount != targetPage;
|
||||
};
|
||||
|
||||
detail.Controls.AddRange(new XRControl[] { signature, signatureLabel });
|
||||
}
|
||||
|
||||
static void RemoveExistingSignatureById(DevExpress.XtraReports.UI.DetailBand detail, string annotId) {
|
||||
var controls = detail.Controls
|
||||
.Cast<XRControl>()
|
||||
.Where(c => c.Name == $"receiverSignatureImage_{annotId}" || c.Name == $"receiverSignatureLabel_{annotId}")
|
||||
.ToArray();
|
||||
foreach (var c in controls)
|
||||
detail.Controls.Remove(c);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_dotNetRef?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
@page "/example/test"
|
||||
@using System.Drawing
|
||||
@using DevExpress.Drawing
|
||||
@using DevExpress.Utils
|
||||
@using DevExpress.XtraPrinting
|
||||
@using DevExpress.XtraPrinting.Drawing
|
||||
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
|
||||
@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand
|
||||
@using XRLabel = DevExpress.XtraReports.UI.XRLabel
|
||||
@using XRPictureBox = DevExpress.XtraReports.UI.XRPictureBox
|
||||
@using XRControl = DevExpress.XtraReports.UI.XRControl
|
||||
@using ImageSizeMode = DevExpress.XtraPrinting.ImageSizeMode
|
||||
@using EnvelopeGenerator.ReceiverUI.Services
|
||||
@using DevExpress.Blazor.Reporting
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject InMemoryReportStorageWebExtension ReportStorage
|
||||
@inject EnvelopeGenerator.ReceiverUI.Services.DocumentService DocumentService
|
||||
@inject Microsoft.Extensions.Configuration.IConfiguration Configuration
|
||||
@inject HttpClient Http
|
||||
@using System;
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
<link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" />
|
||||
|
||||
<div class="receiver-page-layout">
|
||||
|
||||
<div class="receiver-viewer-wrapper">
|
||||
<embed class="w-100 h-50" src="/docs/Document.pdf" type="application/pdf" />
|
||||
@if (PdfBytes is { Length: > 0 }) {
|
||||
<DxPdfViewer DocumentContent="PdfBytes" CssClass="w-100 h-50" />
|
||||
}
|
||||
else{
|
||||
<div>Not found</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
const string SignatureTabDraw = "draw";
|
||||
const string SignatureTabText = "text";
|
||||
const string SignatureTabImage = "image";
|
||||
const string DrawCanvasId = "receiver-signature-pad";
|
||||
const string TypedCanvasId = "receiver-typed-signature-pad";
|
||||
const string ImageInputId = "receiver-signature-image-input";
|
||||
const string ImageCanvasId = "receiver-image-signature-pad";
|
||||
|
||||
readonly (string Text, string Value)[] TypedSignatureFonts = {
|
||||
("Brush Script", "'Brush Script MT', cursive"),
|
||||
("Segoe Script", "'Segoe Script', cursive"),
|
||||
("Lucida Handwriting", "'Lucida Handwriting', cursive"),
|
||||
("Comic Sans", "'Comic Sans MS', cursive"),
|
||||
("Cursive", "cursive")
|
||||
};
|
||||
|
||||
[Parameter] public string? EnvelopeKey { get; set; }
|
||||
|
||||
DxReportViewer? reportViewer;
|
||||
XtraReport? Report;
|
||||
string PdfViewerUrl = string.Empty;
|
||||
byte[]? PdfBytes;
|
||||
byte[]? SignedPdfBytes;
|
||||
bool SignatureApplied;
|
||||
bool SignaturePopupVisible;
|
||||
string? PopupValidationMessage;
|
||||
string ActiveSignatureTab = SignatureTabDraw;
|
||||
string TypedSignatureText = string.Empty;
|
||||
string TypedSignatureFont = "'Brush Script MT', cursive";
|
||||
string SignerFullName = string.Empty;
|
||||
string SignerPosition = string.Empty;
|
||||
string SignaturePlace = string.Empty;
|
||||
int ViewerKey;
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
PdfBytes = await Http.GetByteArrayAsync("/docs/Document.pdf");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
@page "/"
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
|
||||
<div class="home-page-wrapper">
|
||||
|
||||
<div class="home-hero-header">
|
||||
<div class="home-hero-header__inner">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="home-hero-header__icon" viewBox="0 0 16 16">
|
||||
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<h1 class="home-hero-header__title">SignFlow</h1>
|
||||
<p class="home-hero-header__subtitle">Willkommen im eSign-Portal</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="home-content">
|
||||
<div class="home-card card shadow border-0">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
|
||||
<p class="text-muted mb-4" style="font-size: 0.92rem; line-height: 1.7; text-align: justify; text-align-last: left; min-height: calc(0.92rem * 1.7 * 9);">
|
||||
<span id="home-description"></span>
|
||||
</p>
|
||||
|
||||
<div class="mt-4 pt-3 border-top">
|
||||
<div class="d-flex flex-wrap justify-content-center gap-3">
|
||||
<div class="home-feature-badge">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||||
</svg>
|
||||
Sicherer Zugang
|
||||
</div>
|
||||
<div class="home-feature-badge">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
||||
</svg>
|
||||
Digitale Unterschrift
|
||||
</div>
|
||||
<div class="home-feature-badge">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||
</svg>
|
||||
PDF-Export
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private const string HomePageDescription =
|
||||
"Das digitale Unterschriftenportal ist eine Plattform, die entwickelt wurde, um Ihre Dokumente sicher zu unterschreiben und zu verwalten. " +
|
||||
"Mit seiner benutzerfreundlichen Oberfläche können Sie Ihre Dokumente schnell hochladen, die Unterschriftsprozesse verfolgen und Ihre digitalen Unterschriftenanwendungen einfach durchführen. " +
|
||||
"Dieses Portal beschleunigt Ihren Arbeitsablauf mit rechtlich gültigen Unterschriften und erhöht gleichzeitig die Sicherheit Ihrer Dokumente.";
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await JS.InvokeVoidAsync("receiverSignature.startTyped", "home-description", HomePageDescription, 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using EnvelopeGenerator.ReceiverUI;
|
||||
using DevExpress.DataAccess.Web;
|
||||
using EnvelopeGenerator.ReceiverUI.Services;
|
||||
using EnvelopeGenerator.ReceiverUI.Options;
|
||||
using DevExpress.XtraReports.Services;
|
||||
using DevExpress.Blazor.Reporting;
|
||||
using DevExpress.XtraReports.Web.Extensions;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
builder.Services.Configure<ApiOptions>(opts =>
|
||||
builder.Configuration.GetSection(ApiOptions.SectionName).Bind(opts));
|
||||
builder.Services.Configure<PdfViewerOptions>(opts =>
|
||||
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
|
||||
builder.Services.AddScoped<DocumentService>();
|
||||
builder.Services.AddScoped<AuthService>();
|
||||
builder.Services.AddScoped<AnnotationService>();
|
||||
builder.Services.AddScoped<EnvelopeReceiverService>();
|
||||
builder.Services.AddScoped<SignatureService>();
|
||||
builder.Services.AddScoped<SignatureCacheService>();
|
||||
builder.Services.AddSingleton<AppVersionService>();
|
||||
|
||||
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
|
||||
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
|
||||
|
||||
builder.Services.AddDevExpressBlazorReportingWebAssembly(configure => {
|
||||
configure.UseDevelopmentMode();
|
||||
});
|
||||
builder.Services.AddScoped<IDataSourceWizardJsonConnectionStorage, CustomDataSourceWizardJsonDataConnectionStorage>();
|
||||
builder.Services.AddScoped<IJsonDataConnectionProviderFactory, CustomJsonDataConnectionProviderFactory>();
|
||||
builder.Services.AddScoped<IObjectDataSourceWizardTypeProvider, ObjectDataSourceWizardCustomTypeProvider>();
|
||||
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.ReceiverUI.Data.DataItemList));
|
||||
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.ReceiverUI.PredefinedReports.Report));
|
||||
builder.Services.AddSingleton<InMemoryReportStorageWebExtension>();
|
||||
builder.Services.AddSingleton<ReportStorageWebExtension>(sp => sp.GetRequiredService<InMemoryReportStorageWebExtension>());
|
||||
builder.Services.AddScoped<IReportProviderAsync, CustomReportProvider>();
|
||||
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
|
||||
|
||||
var host = builder.Build();
|
||||
await FontLoader.LoadFonts(host.Services.GetRequiredService<HttpClient>(), new List<string> { "opensans.ttf" });
|
||||
await host.RunAsync();
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<WebPublishMethod>Package</WebPublishMethod>
|
||||
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
|
||||
<LastUsedPlatform>Any CPU</LastUsedPlatform>
|
||||
<SiteUrlToLaunchAfterPublish />
|
||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||
<ProjectGuid>fb2d306b-1042-4a70-31ed-f991a1599371</ProjectGuid>
|
||||
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\ReceiverUI\net8\$(Version)\EnvelopeGenerator.ReceiverUI.zip</DesktopBuildPackageLocation>
|
||||
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||
<DeployIisAppPath>EnvelopeGenerator.ReceiverUI</DeployIisAppPath>
|
||||
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"profiles": {
|
||||
"EnvelopeGenerator.ReceiverUI": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:52936;http://localhost:52937"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using EnvelopeGenerator.ReceiverUI.Models;
|
||||
using EnvelopeGenerator.ReceiverUI.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves annotation positions from the API.
|
||||
/// The URL is composed as <c>{BaseUrl}/api/Annotation/{envelopeKey}</c>.
|
||||
/// During development, <c>BaseUrl</c> is empty so the request resolves to the
|
||||
/// YARP-proxied route on the same origin, which currently serves
|
||||
/// <c>fake-data/annotations.json</c>. To switch to real data, update the
|
||||
/// YARP route in <c>yarp.json</c> — no code change required.
|
||||
/// </summary>
|
||||
[Obsolete("Use SignatureService.")]
|
||||
public class AnnotationService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public async Task<IReadOnlyList<AnnotationDto>> GetAnnotationsAsync(string envelopeKey, CancellationToken cancel = default)
|
||||
{
|
||||
var url = $"{apiOptions.Value.BaseUrl}/api/Annotation/{Uri.EscapeDataString(envelopeKey)}";
|
||||
var response = await http.GetAsync(url, cancel);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return [];
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<List<AnnotationDto>>(_jsonOptions, cancel);
|
||||
return result ?? [];
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using DevExpress.DataAccess.Json;
|
||||
using DevExpress.DataAccess.Web;
|
||||
using DevExpress.DataAccess.Wizard.Services;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services
|
||||
{
|
||||
public class CustomDataSourceWizardJsonDataConnectionStorage : IDataSourceWizardJsonConnectionStorage
|
||||
{
|
||||
public static JsonDataConnection GetDefaultConnection() {
|
||||
var uriJsonSource = new UriJsonSource() {
|
||||
Uri = new Uri(@"https://raw.githubusercontent.com/DevExpress-Examples/DataSources/master/JSON/customers.json"),
|
||||
};
|
||||
return new JsonDataConnection(uriJsonSource) { StoreConnectionNameOnly = true, Name = "NWindProductsJson" };
|
||||
}
|
||||
public static List<JsonDataConnection> GetConnections() {
|
||||
var connections = new List<JsonDataConnection> {
|
||||
GetDefaultConnection()
|
||||
};
|
||||
return connections;
|
||||
}
|
||||
|
||||
bool IJsonConnectionStorageService.CanSaveConnection => false;
|
||||
bool IJsonConnectionStorageService.ContainsConnection(string connectionName) {
|
||||
return GetConnections().Any(x => x.Name == connectionName);
|
||||
}
|
||||
|
||||
IEnumerable<JsonDataConnection> IJsonConnectionStorageService.GetConnections() {
|
||||
return GetConnections();
|
||||
}
|
||||
|
||||
JsonDataConnection IJsonDataConnectionProviderService.GetJsonDataConnection(string name) {
|
||||
var connection = GetConnections().FirstOrDefault(x => x.Name == name);
|
||||
if(connection == null)
|
||||
throw new InvalidOperationException();
|
||||
return connection;
|
||||
}
|
||||
|
||||
void IJsonConnectionStorageService.SaveConnection(string connectionName, JsonDataConnection connection, bool saveCredentials) { }
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using DevExpress.DataAccess.Json;
|
||||
using DevExpress.DataAccess.Web;
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services
|
||||
{
|
||||
public class CustomJsonDataConnectionProviderFactory : IJsonDataConnectionProviderFactory {
|
||||
public IJsonDataConnectionProviderService Create() {
|
||||
return new WebDocumentViewerJsonDataConnectionProvider(CustomDataSourceWizardJsonDataConnectionStorage.GetConnections());
|
||||
}
|
||||
}
|
||||
|
||||
public class WebDocumentViewerJsonDataConnectionProvider : IJsonDataConnectionProviderService
|
||||
{
|
||||
readonly List<JsonDataConnection> jsonDataConnections;
|
||||
public WebDocumentViewerJsonDataConnectionProvider(List<JsonDataConnection> jsonDataConnections) {
|
||||
this.jsonDataConnections = jsonDataConnections;
|
||||
}
|
||||
public JsonDataConnection GetJsonDataConnection(string name) {
|
||||
var connection = jsonDataConnections.FirstOrDefault(x => x.Name == name);
|
||||
if(connection == null)
|
||||
throw new InvalidOperationException();
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using DevExpress.XtraReports.UI;
|
||||
using DevExpress.XtraReports.Services;
|
||||
using EnvelopeGenerator.ReceiverUI.PredefinedReports;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services
|
||||
{
|
||||
public class CustomReportProvider : IReportProviderAsync {
|
||||
private readonly InMemoryReportStorageWebExtension reportStorage;
|
||||
|
||||
public CustomReportProvider(InMemoryReportStorageWebExtension reportStorage) {
|
||||
this.reportStorage = reportStorage;
|
||||
}
|
||||
|
||||
public Task<XtraReport> GetReportAsync(string id, ReportProviderContext context) {
|
||||
if(reportStorage.TryGetReport(id, out var savedReport))
|
||||
return Task.FromResult(savedReport);
|
||||
|
||||
return Task.FromResult(ReportsFactory.GetReport(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using EnvelopeGenerator.ReceiverUI.Models;
|
||||
using EnvelopeGenerator.ReceiverUI.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver
|
||||
/// from <c>GET api/EnvelopeReceiver/{envelopeKey}</c>.
|
||||
/// </summary>
|
||||
public class EnvelopeReceiverService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the envelope receiver data for the given envelope key from the API.
|
||||
/// Throws HttpRequestException on failure with appropriate status code.
|
||||
/// </summary>
|
||||
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
|
||||
public async Task<EnvelopeReceiverDto?> GetAsync(string envelopeKey, CancellationToken cancel = default)
|
||||
{
|
||||
var url = $"{apiOptions.Value.BaseUrl}/api/EnvelopeReceiver/{Uri.EscapeDataString(envelopeKey)}";
|
||||
var response = await http.GetAsync(url, cancel);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var statusCode = (int)response.StatusCode;
|
||||
var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
|
||||
throw new HttpRequestException(
|
||||
$"Failed to load envelope receiver data. Status: {statusCode} ({reasonPhrase})",
|
||||
null,
|
||||
response.StatusCode);
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<EnvelopeReceiverDto>(_jsonOptions, cancel);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using DevExpress.Drawing;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services {
|
||||
public static class FontLoader {
|
||||
public async static Task LoadFonts(HttpClient httpClient, List<string> fontNames) {
|
||||
foreach(var fontName in fontNames) {
|
||||
var fontBytes = await httpClient.GetByteArrayAsync($"fonts/{fontName}");
|
||||
DXFontRepository.Instance.AddFont(fontBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using DevExpress.DataAccess.Web;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services {
|
||||
public class ObjectDataSourceWizardCustomTypeProvider : IObjectDataSourceWizardTypeProvider {
|
||||
public IEnumerable<Type> GetAvailableTypes(string context) {
|
||||
return new[] { typeof(Data.DataItemList) };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
.navbar.header-navbar {
|
||||
flex-grow: 0;
|
||||
flex-wrap: nowrap;
|
||||
border: none;
|
||||
background-color: inherit;
|
||||
border-radius: 0;
|
||||
height: 3.5rem;
|
||||
min-height: 3.5rem;
|
||||
box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.12);
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.header-navbar .navbar-toggler {
|
||||
outline: none;
|
||||
border-radius: 0;
|
||||
padding-left: .75rem;
|
||||
padding-right: .75rem;
|
||||
box-shadow: none;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.header-navbar .navbar-toggler .navbar-toggler-icon {
|
||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255,255,255, 1)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E");
|
||||
background-color: transparent !important;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.1rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 350px) {
|
||||
.title {
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
@using EnvelopeGenerator.ReceiverUI.Services;
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<main>
|
||||
<article class="content">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
<footer class="receiver-footer">
|
||||
<span>© SignFlow 2023-2024 <a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a></span>
|
||||
<span class="receiver-footer__sep">|</span>
|
||||
<a href="docs/privacy-policy.de-DE.html" target="_blank" rel="noopener">Datenschutz</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject] HttpClient Http { get; set; }
|
||||
List<string> RequiredFonts = new() {
|
||||
"opensans.ttf"
|
||||
};
|
||||
|
||||
protected async override Task OnInitializedAsync() {
|
||||
await FontLoader.LoadFonts(Http, RequiredFonts);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
article {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.oi {
|
||||
width: 2rem;
|
||||
font-size: 1.1rem;
|
||||
vertical-align: text-top;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.25);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar .collapse {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
@using System.Net.Http
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.JSInterop
|
||||
@using EnvelopeGenerator.ReceiverUI
|
||||
@using EnvelopeGenerator.ReceiverUI.Shared
|
||||
@using DevExpress.Blazor.Reporting
|
||||
@using DevExpress.Blazor.PdfViewer
|
||||
@using DevExpress.Blazor
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"DevExpress": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="d-flex flex-column justify-content-center align-items-center vh-100">
|
||||
<div class="d-flex">
|
||||
<img class="mt-2 me-4" src="images/sad.svg" width="60" height="60" />
|
||||
<div>
|
||||
<div class="h1">Your browser is not supported.</div>
|
||||
<p style="font-size: 1rem; opacity: 0.75;" class="m-0">In .NET 5.0, Blazor does not support Microsoft Internet Explorer and Microsoft Edge Legacy (refer to <a target="_blank" href="https://docs.devexpress.com/Blazor/401588/common-concepts/supported-browsers">Supported Browsers</a>).<br />Please use a different browser to run EnvelopeGenerator.ReceiverUI.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,368 +0,0 @@
|
||||
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
|
||||
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
main, .page {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
article {
|
||||
height: calc(100vh - 36px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.receiver-page-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.receiver-signature-panel {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.receiver-viewer-wrapper {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.dx-blazor-reporting-container {
|
||||
height: calc(100vh - 166px) !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* ── Force DevExpress viewer pages into a single centered column ─────────── */
|
||||
.dxbrv-report-preview-content {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
/* ── Annotation signature checkbox overlays ─────────────────────────────── */
|
||||
.annot-sig-cb-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
font-family: "Segoe UI", Arial, sans-serif;
|
||||
padding: 0 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
||||
border: none;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(44, 62, 80, 0.35);
|
||||
transition: box-shadow 0.18s, filter 0.18s;
|
||||
}
|
||||
|
||||
.annot-sig-cb-wrapper:hover {
|
||||
filter: brightness(1.12);
|
||||
box-shadow: 0 4px 14px rgba(44, 62, 80, 0.45);
|
||||
}
|
||||
|
||||
.annot-sig-cb-wrapper--checked {
|
||||
background: linear-gradient(135deg, #1a6b2a 0%, #27ae60 100%);
|
||||
box-shadow: 0 2px 8px rgba(26, 107, 42, 0.35);
|
||||
}
|
||||
|
||||
.annot-sig-cb-wrapper--checked:hover {
|
||||
filter: brightness(1.1);
|
||||
box-shadow: 0 4px 14px rgba(26, 107, 42, 0.45);
|
||||
}
|
||||
|
||||
.annot-sig-cb {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.annot-sig-cb__label {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
pointer-events: none;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
/* ── Envelope info header ────────────────────────────────────────────────── */
|
||||
.receiver-info-header {
|
||||
border-bottom: 1px solid rgba(0,0,0,.08);
|
||||
}
|
||||
|
||||
.receiver-info-header__gradient {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 10px 16px 8px;
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.receiver-info-header__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.receiver-info-header__icon {
|
||||
flex-shrink: 0;
|
||||
opacity: .85;
|
||||
}
|
||||
|
||||
.receiver-info-header__title {
|
||||
font-size: 0.92rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 340px;
|
||||
}
|
||||
|
||||
.receiver-info-header__sender {
|
||||
font-size: 0.72rem;
|
||||
opacity: .8;
|
||||
margin-top: 1px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.receiver-info-header__badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.receiver-info-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: rgba(255,255,255,.18);
|
||||
color: #fff;
|
||||
border-radius: 20px;
|
||||
padding: 2px 9px;
|
||||
font-size: 0.70rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.receiver-info-badge--muted {
|
||||
background: rgba(255,255,255,.10);
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.receiver-info-badge--accent {
|
||||
background: rgba(39,174,96,.35);
|
||||
border: 1px solid rgba(39,174,96,.5);
|
||||
}
|
||||
|
||||
.receiver-info-message {
|
||||
padding: 7px 16px;
|
||||
font-size: 0.78rem;
|
||||
color: #444;
|
||||
border-bottom: 1px solid rgba(0,0,0,.05);
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.receiver-info-private-message {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
padding: 5px 16px 6px;
|
||||
font-size: 0.75rem;
|
||||
color: #5a5a72;
|
||||
background: #f8f7ff;
|
||||
border-top: 1px solid rgba(90,80,180,.12);
|
||||
}
|
||||
|
||||
/* ── Signature action bar ────────────────────────────────────────────────── */
|
||||
.receiver-action-bar {
|
||||
border-bottom: 1px solid rgba(0,0,0,.08);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.receiver-action-bar__inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 7px 14px;
|
||||
}
|
||||
|
||||
.receiver-action-bar__progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ── Home page ───────────────────────────────────────────────────────────── */
|
||||
.home-page-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-height: calc(100vh - 36px);
|
||||
background: linear-gradient(160deg, #1e2e3e 0%, #2c3e50 40%, #3498db 100%);
|
||||
padding-bottom: 36px;
|
||||
}
|
||||
|
||||
.home-hero-header {
|
||||
width: 100%;
|
||||
padding: 48px 24px 36px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.home-hero-header__inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.home-hero-header__icon {
|
||||
opacity: 0.90;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.home-hero-header__title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.home-hero-header__subtitle {
|
||||
font-size: 0.92rem;
|
||||
margin: 4px 0 0;
|
||||
color: rgba(255,255,255,0.75);
|
||||
}
|
||||
|
||||
.home-content {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.home-card {
|
||||
border-radius: 12px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.home-btn-primary {
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
transition: filter 0.15s, box-shadow 0.15s;
|
||||
box-shadow: 0 2px 10px rgba(44,62,80,0.30);
|
||||
}
|
||||
|
||||
.home-btn-primary:hover {
|
||||
filter: brightness(1.12);
|
||||
box-shadow: 0 4px 16px rgba(44,62,80,0.40);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.home-feature-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: rgba(52,152,219,0.10);
|
||||
color: #3498db;
|
||||
border: 1px solid rgba(52,152,219,0.25);
|
||||
border-radius: 20px;
|
||||
padding: 4px 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── Footer ──────────────────────────────────────────────────────────────── */
|
||||
.receiver-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 0 20px;
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: 0.72rem;
|
||||
z-index: 100;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.receiver-footer a {
|
||||
color: rgba(255, 255, 255, 0.90);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.receiver-footer a:hover {
|
||||
color: #ffffff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.receiver-footer__sep {
|
||||
opacity: 0.4;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<title>EnvelopeGenerator.ReceiverUI</title>
|
||||
<base href="/" />
|
||||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="EnvelopeGenerator.ReceiverUI.styles.css" rel="stylesheet" />
|
||||
<link href="css/app.css" rel="stylesheet" />
|
||||
<style type="text/css">
|
||||
.splash-screen {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-flow: column nowrap;
|
||||
height: 100vh;
|
||||
font-family: "Segoe UI",Roboto,"Helvetica Neue","-apple-system",BlinkMacSystemFont,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
|
||||
font-size: .88rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.splash-screen .spinner-border {
|
||||
border: .2em solid;
|
||||
border-color: #5f368d #bfbfbf #bfbfbf;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.splash-screen-caption {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.splash-screen-text {
|
||||
color: #a1a1a1;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<script type="text/javascript">
|
||||
if(/MSIE \d|Trident.*rv:|Edge\//.test(window.navigator.userAgent))
|
||||
window.location.href = "browserNotSupported.html";
|
||||
</script>
|
||||
<div class="splash-screen">
|
||||
<div class="spinner-border"></div>
|
||||
<div class="splash-screen-caption">EnvelopeGenerator.ReceiverUI</div>
|
||||
<div class="splash-screen-text">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">X</a>
|
||||
</div>
|
||||
<script src="_content/DevExpress.Blazor.Resources/js/preload-script.js"></script>
|
||||
<script src="js/typed.umd.js"></script>
|
||||
<script src="js/receiver-signature.js?v=9"></script>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
namespace EnvelopeGenerator.Server.Client.Data {
|
||||
public class Adjustment
|
||||
{
|
||||
public static Adjustment CreateBalanceForward(DateTime dt, int random)
|
||||
@@ -41,4 +41,4 @@
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using DevExpress.DataAccess.Sql;
|
||||
using DevExpress.DataAccess.Sql;
|
||||
using DevExpress.DataAccess.Sql.DataApi;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
namespace EnvelopeGenerator.Server.Client.Data {
|
||||
public class Customer {
|
||||
static List<Customer> currentCustomers = new List<Customer>();
|
||||
|
||||
@@ -61,4 +61,4 @@ namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
public string Fax { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
namespace EnvelopeGenerator.Server.Client.Data {
|
||||
public class DataItem {
|
||||
static readonly string[] accountType = new string[] { "Energy", "Manufacturing", "Estate", "Food", "Services" };
|
||||
public string CustomerID { get; set; }
|
||||
@@ -68,4 +68,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections;
|
||||
using System.Collections;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
namespace EnvelopeGenerator.Server.Client.Data {
|
||||
public class DataItemList : IList<DataItem>, IList {
|
||||
readonly int rowCount;
|
||||
|
||||
@@ -67,4 +67,4 @@ namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
CopyTo(array, arrayIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
namespace EnvelopeGenerator.Server.Client.Data {
|
||||
class DeterministicRandom {
|
||||
const int randomCount = 10000;
|
||||
static readonly int[] deterministicRandomNumbers;
|
||||
@@ -57,4 +57,4 @@
|
||||
this.rnd = i + (i >> 10) + (i >> 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
||||
namespace EnvelopeGenerator.Server.Client.Data {
|
||||
public struct Term {
|
||||
public static readonly Term[] Terms = new Term[] {
|
||||
new Term("Payment seven days after invoice date" ),
|
||||
@@ -12,4 +12,4 @@
|
||||
this.currentName = currentName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
||||
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
|
||||
<WasmBuildNative>true</WasmBuildNative>
|
||||
<InvariantGlobalization>false</InvariantGlobalization>
|
||||
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.3" />
|
||||
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.28" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.9" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.1" />
|
||||
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
|
||||
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.22" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.11" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="PredefinedReports\Report.cs">
|
||||
<SubType>XtraReport</SubType>
|
||||
</Compile>
|
||||
</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