Compare commits
234 Commits
a3b33637fd
...
feat/serve
| Author | SHA1 | Date | |
|---|---|---|---|
| 380a5f4c0c | |||
| 0f53d0509e | |||
| 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.GeneratorAPI/ClientApp/envelope-generator-ui/.vscode
|
||||||
/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs
|
/EnvelopeGenerator.Tests.Application/Services/BugFixTests.cs
|
||||||
/EnvelopeGenerator.Tests.Application/annotations.json
|
/EnvelopeGenerator.Tests.Application/annotations.json
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/TekH - SoftHSM Test.md
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/publish-output
|
||||||
|
/EnvelopeGenerator.Server/EnvelopeGenerator.Server/tekh_softHSM_test.md
|
||||||
|
/ignore/EnvelopeGenerator.Form
|
||||||
|
|||||||
324
AGENTS.md
Normal file
324
AGENTS.md
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
# 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:** EnvelopeGenerator.API (ASP.NET Core 8.0)
|
||||||
|
|
||||||
|
**Proxy:** YARP in EnvelopeGenerator.Server routes `/api/*` → `localhost:8088` (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 Both Projects (Required)
|
||||||
|
```powershell
|
||||||
|
# Terminal 1 - API Backend
|
||||||
|
cd EnvelopeGenerator.API
|
||||||
|
dotnet run
|
||||||
|
|
||||||
|
# Terminal 2 - Blazor Frontend
|
||||||
|
cd EnvelopeGenerator.Server\EnvelopeGenerator.Server
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical:** Both must run simultaneously. EnvelopeGenerator.Server proxy forwards `/api/*` to API.
|
||||||
|
|
||||||
|
### 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.API/ # Controllers, endpoints
|
||||||
|
EnvelopeGenerator.Server/
|
||||||
|
EnvelopeGenerator.Server/ # Server-side Blazor components
|
||||||
|
├─ Components/Pages/ # @rendermode InteractiveServer
|
||||||
|
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)
|
||||||
|
|
||||||
|
### Missing Granular Endpoints (Need to Create)
|
||||||
|
```
|
||||||
|
POST /api/Envelope/draft # Create draft envelope
|
||||||
|
PUT /api/Envelope/{id} # Update metadata
|
||||||
|
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
|
||||||
|
POST /api/Envelope/{id}/send # Send to receivers
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Config (`API/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 API (`dotnet run` in `EnvelopeGenerator.API`)
|
||||||
|
2. Start EnvelopeGenerator.Server (`dotnet run` in `EnvelopeGenerator.Server\EnvelopeGenerator.Server`)
|
||||||
|
3. Navigate to `https://localhost:5131` (or check console output for port)
|
||||||
|
4. Test sender login at `/sender/login`
|
||||||
|
5. Test receiver flow at `/envelope/login/{envelopeKey}`
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
**SQL Server** (DD_ECM)
|
||||||
|
- Connection string in `API/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:**
|
||||||
|
- API: `https://localhost:8088`
|
||||||
|
- WebUI: `https://localhost:5131` or `http://localhost:7192`
|
||||||
@@ -1,424 +1,351 @@
|
|||||||
# EnvelopeGenerator — AI Context Reference
|
# EnvelopeGenerator — Current Workspace Context
|
||||||
|
|
||||||
## Purpose
|
## 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)
|
`EnvelopeGenerator.Server` is the current runtime host and contains:
|
||||||
- Runs independently (development & production)
|
- Blazor server host
|
||||||
- **YARP Reverse Proxy** configured via `yarp.json`
|
- WebAssembly host integration
|
||||||
- Proxies requests to:
|
- API controllers
|
||||||
- `EnvelopeGenerator.ReceiverUI` (Blazor WASM)
|
- authentication/authorization setup
|
||||||
- External Auth.API service
|
- Swagger/Scalar setup
|
||||||
- Serves as single entry point for all requests
|
- YARP reverse proxy configuration
|
||||||
|
- DevExpress server-side services
|
||||||
|
- SQL Server distributed cache setup
|
||||||
|
|
||||||
2. **EnvelopeGenerator.ReceiverUI** (Blazor WebAssembly)
|
### Client Project
|
||||||
- Runs on separate host/port
|
**Client UI project:** `EnvelopeGenerator.Server.Client`
|
||||||
- Accessed **only through API proxy** (not directly)
|
|
||||||
- Serves static files (HTML, JS, CSS, WASM)
|
|
||||||
|
|
||||||
**Request Flow:**
|
This project contains:
|
||||||
```
|
- WebAssembly-rendered pages
|
||||||
Client ? API:8088 (YARP Proxy) ? ReceiverUI:52936 (Blazor WASM)
|
- client-side services
|
||||||
? Auth.API:9090 (External Auth Service)
|
- 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` — still exists in the solution, but the current merged app host is `EnvelopeGenerator.Server`
|
||||||
|
|
||||||
|
### Legacy / Do Not Touch
|
||||||
|
- `EnvelopeGenerator.Service`
|
||||||
|
- `EnvelopeGenerator.Form`
|
||||||
|
- `EnvelopeGenerator.BBTests`
|
||||||
|
- `EnvelopeGenerator.CommonServices`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ReceiverUI Route Structure
|
## Current Hosting Model
|
||||||
|
|
||||||
### Root Route
|
`EnvelopeGenerator.Server/Program.cs` currently configures:
|
||||||
| Route | File | Purpose |
|
- `AddRazorComponents()` with both interactive server and interactive WebAssembly components
|
||||||
|---|---|---|
|
- `AddControllers()` and `MapControllers()`
|
||||||
| `/` | `Index.razor` | Application entry point (landing page). |
|
- 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
|
This means the active app is a **merged UI + API host**.
|
||||||
| Route | File | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `/sender/login` | `LoginSenderPage.razor` | Username/password authentication |
|
|
||||||
| `/sender` | `EnvelopeSenderPage.razor` | Sender dashboard (envelope list) |
|
|
||||||
|
|
||||||
### Receiver Routes
|
|
||||||
| Route | File | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `/envelope/login/{EnvelopeKey}` | `LoginReceiverPage.razor` | Access code authentication for specific envelope |
|
|
||||||
| `/envelope/{EnvelopeKey}` | `EnvelopeReceiverPage.razor` | View & sign envelope (PDF.js viewer) |
|
|
||||||
|
|
||||||
**Multi-Envelope Support:** Receivers can login to multiple envelopes simultaneously (per-envelope cookie authentication).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture Evolution
|
## Reverse Proxy
|
||||||
|
|
||||||
### Old Architecture (Deprecated)
|
**Config file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
|
||||||
- **Sender UI:** `EnvelopeGenerator.Web` (Razor Pages + PSPDFKit)
|
|
||||||
- **Receiver UI:** `EnvelopeGenerator.ReceiverUI` (Blazor WASM + PDF.js)
|
|
||||||
- **Backend:** `EnvelopeGenerator.API`
|
|
||||||
|
|
||||||
### Current Architecture
|
Current YARP usage is focused on **AuthHub forwarding**, not a general `/api/* -> EnvelopeGenerator.API` proxy.
|
||||||
- **Unified Frontend:** `EnvelopeGenerator.ReceiverUI` (Blazor WASM) — **Both Senders & Receivers**
|
|
||||||
- **Backend:** `EnvelopeGenerator.API` — **Both Senders & Receivers**
|
Configured routes forward:
|
||||||
- **Libraries:** DevExpress + PDF.js
|
- `POST /api/auth` -> AuthHub `/api/auth/sign-flow`
|
||||||
- **PSPDFKit:** **REMOVED**
|
- `POST /api/Auth/envelope-receiver/{key}` -> AuthHub `/api/auth/envelope-receiver/{key}?cookie=true`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Solution Structure
|
## Active Routes and Files
|
||||||
|
|
||||||
| Project | Target | Purpose |
|
### WebAssembly Pages (`EnvelopeGenerator.Server.Client`)
|
||||||
|---|---|---|
|
| Route | File | Render Mode | Purpose |
|
||||||
| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Backend for **both Senders & Receivers**. Auth, PDF serving, signature endpoints. |
|
|
||||||
| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | **Unified Blazor WebAssembly Frontend**. UI for **both Senders & Receivers**. YARP proxy to API. |
|
|
||||||
| `EnvelopeGenerator.Web` | net7/8/9 | **DEPRECATED.** Legacy Razor Pages (Sender UI). No longer used. |
|
|
||||||
| `EnvelopeGenerator.Application` | multi | MediatR CQRS handlers. Business logic. |
|
|
||||||
| `EnvelopeGenerator.Domain` | multi | Domain models, constants, interfaces. |
|
|
||||||
| `EnvelopeGenerator.Infrastructure` | multi | EF Core repos, DB context. |
|
|
||||||
| `EnvelopeGenerator.PdfEditor` | multi | iText7 utilities (NOT used in ReceiverUI). |
|
|
||||||
| `EnvelopeGenerator.DependencyInjection` | multi | DI registration helpers. |
|
|
||||||
| **VB.NET projects** (Service/Form/BBTests) | net462 | **Legacy. Do NOT touch.** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Files & Routes
|
|
||||||
|
|
||||||
| File | Route/Purpose |
|
|
||||||
|---|---|
|
|
||||||
| `ReceiverUI/Pages/Index.razor` | `/` — Application entry point (landing page). |
|
|
||||||
| `ReceiverUI/Pages/EnvelopeSenderPage.razor` | `/sender` — Sender dashboard (envelope list). |
|
|
||||||
| `ReceiverUI/Pages/EnvelopeReceiverPage.razor` | `/envelope/{key}` — Receiver PDF viewer & signing. |
|
|
||||||
| `ReceiverUI/Pages/LoginSenderPage.razor` | `/sender/login` — Sender username/password auth. |
|
|
||||||
| `ReceiverUI/Pages/LoginReceiverPage.razor` | `/envelope/login/{EnvelopeKey}` — Receiver access code auth. |
|
|
||||||
| `ReceiverUI/wwwroot/js/pdf-viewer.js` | PDF.js wrapper (zoom, pagination, thumbnails). |
|
|
||||||
| `ReceiverUI/wwwroot/js/receiver-signature.js` | Signature pad (draw/type/image). |
|
|
||||||
| `ReceiverUI/wwwroot/css/envelope-viewer.css` | EnvelopeViewer styles. |
|
|
||||||
| `ReceiverUI/Services/AuthService.cs` | Receiver + Sender authentication. |
|
|
||||||
| `ReceiverUI/Services/SignatureCacheService.cs` | Signature caching (Redis/SQL). |
|
|
||||||
| `API/Controllers/CacheController.cs` | Signature cache endpoints. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Coordinate System — CRITICAL
|
|
||||||
|
|
||||||
**Database Format:** INCHES (GdPicture14 native)
|
|
||||||
**Origin:** Top-left corner
|
|
||||||
**Axes:** X right, Y down
|
|
||||||
|
|
||||||
### Conversion Formulas
|
|
||||||
|
|
||||||
| From INCHES to | Formula | Example |
|
|
||||||
|---|---|---|
|
|
||||||
| **DevExpress DX** | `x_DX = x_inches * 100` | 1.5" ? 150 DX |
|
|
||||||
| **PDF Points** | `x_pt = x_inches * 72` | 1.5" ? 108 pt |
|
|
||||||
| **PDF.js Pixels** | Normalize ? scale | `(x_inches / pageWidth) * canvasWidth * scale` |
|
|
||||||
|
|
||||||
**A4 Dimensions:**
|
|
||||||
- Width: 8.27" = 595pt = 827 DX
|
|
||||||
- Height: 11.69" = 842pt = 1169 DX
|
|
||||||
|
|
||||||
### Unit Systems
|
|
||||||
|
|
||||||
| System | Unit | Origin | Y-Axis |
|
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **Database (GdPicture14)** | Inches | Top-left | Down |
|
| `/` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/IndexPage.razor` | WebAssembly | Landing page |
|
||||||
| PDF.js | Pixels | Top-left | Down |
|
| `/sender/login` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginSenderPage.razor` | WebAssembly | Sender login |
|
||||||
| iText7 PDF | Points (1/72") | **Bottom-left** | **Up** (flip required) |
|
| `/sender` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor` | WebAssembly (`prerender: false`) | Sender dashboard |
|
||||||
| ~~PSPDFKit~~ | ~~Points~~ | ~~Top-left~~ | **REMOVED** |
|
| `/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}`
|
The active application exposes controllers from:
|
||||||
**Tech:** PDF.js 3.11.174 + Blazor WASM + configurable quality
|
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers`
|
||||||
**File:** `ReceiverUI/Pages/EnvelopeReceiverPage.razor`
|
|
||||||
|
|
||||||
### Key Features
|
Current controller set includes:
|
||||||
1. HiDPI/Retina support (4x quality)
|
- `AnnotationController`
|
||||||
2. Configurable quality (`appsettings.json`)
|
- `AuthController`
|
||||||
3. Unlimited zoom (50%-300%)
|
- `CacheController`
|
||||||
4. Ctrl+Wheel global zoom
|
- `ConfigController`
|
||||||
5. Resizable thumbnail sidebar (150-400px, localStorage)
|
- `DocumentController`
|
||||||
6. Responsive (desktop/mobile)
|
- `EmailTemplateController`
|
||||||
|
- `EnvelopeController`
|
||||||
|
- `EnvelopeReceiverController`
|
||||||
|
- `EnvelopeTypeController`
|
||||||
|
- `HistoryController`
|
||||||
|
- `LocalizationController`
|
||||||
|
- `ReadOnlyController`
|
||||||
|
- `ReceiverController`
|
||||||
|
- `SignatureController`
|
||||||
|
- `TfaRegistrationController`
|
||||||
|
|
||||||
### Configuration
|
Do not assume API behavior lives only in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
|
||||||
**File:** `ReceiverUI/wwwroot/appsettings.json`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"PdfViewer": {
|
|
||||||
"ThumbnailBaseScale": 0.75,
|
|
||||||
"ThumbnailEnableHiDPI": true,
|
|
||||||
"MainCanvasEnableHiDPI": true,
|
|
||||||
"ZoomStepPercentage": 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript API
|
|
||||||
**File:** `ReceiverUI/wwwroot/js/pdf-viewer.js`
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
window.pdfViewer = {
|
|
||||||
initialize(canvasId, pdfDataUrl, dotNetRef),
|
|
||||||
renderPage(num),
|
|
||||||
renderSignatureButtons(signatures, pageNum, dotNetRef),
|
|
||||||
applySignature(signatureId, dataUrl, fullName, position, place),
|
|
||||||
zoomIn(), zoomOut(), dispose()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Signature Workflow — EnvelopeReceiver
|
## 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:**
|
### Receiver
|
||||||
- Check `SignatureCacheService` for cached signature
|
Receiver authentication is **per envelope**.
|
||||||
- If cached ? skip popup, load signature
|
|
||||||
- If not ? show automatic popup (mandatory)
|
|
||||||
|
|
||||||
2. **Signature Popup (DxPopup):**
|
Key receiver endpoints used by client services:
|
||||||
- **Cannot close** (no X, no ESC, no outside-click)
|
- `POST /api/Auth/envelope-receiver/{envelopeKey}` — submit access code
|
||||||
- **3 Tabs:** Draw (canvas) / Text (font select) / Image (upload)
|
- `GET /api/auth/check/envelope/{envelopeKey}` — check access
|
||||||
- **Required:** Full name, Place
|
- `POST /api/auth/logout/envelope/{envelopeKey}` — logout receiver for one envelope
|
||||||
- **Optional:** Position
|
|
||||||
- **Save ?** Store in `_capturedSignature`, cache via API
|
|
||||||
|
|
||||||
3. **Signature Buttons:**
|
Receiver cookie resolution in server auth uses an envelope-specific cookie name derived from:
|
||||||
- Render purple "Unterschreiben" buttons at signature field positions
|
- `AuthTokenSignFLOWReceiver.{envelopeKey}` pattern
|
||||||
- Coordinates: INCHES ? POINTS ? Pixels (scaled)
|
|
||||||
- File: `pdf-viewer.js` ? `renderSignatureButtons()`
|
|
||||||
|
|
||||||
4. **Apply Signature (Click "Unterschreiben"):**
|
### Receiver Server-Side Authorization
|
||||||
- JS: Remove button, create HTML overlay
|
`EnvelopeReceiverPage.razor` does **not** rely on its own API access-check call for page authorization.
|
||||||
- Format: Image + separator + text (Name, Position, Place, Date)
|
|
||||||
- **NOT stamped on PDF bytes** (visual overlay only)
|
|
||||||
|
|
||||||
5. **Re-rendering:**
|
It uses:
|
||||||
- Zoom/Page change ? recalculate button positions
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
|
||||||
- Session state: `_capturedSignature` (lost on refresh)
|
|
||||||
|
|
||||||
### Data Model
|
Behavior:
|
||||||
**File:** `ReceiverUI/Models/SignatureCaptureDto.cs`
|
- 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
|
```csharp
|
||||||
public sealed record SignatureCaptureDto {
|
public sealed record SignatureCaptureDto {
|
||||||
public required string DataUrl { get; init; } // base64 PNG
|
public required string DataUrl { get; init; }
|
||||||
public required string FullName { get; init; }
|
public required string FullName { get; init; }
|
||||||
public string Position { get; init; } = ""; // Optional
|
public string Position { get; init; } = "";
|
||||||
public required string Place { get; init; }
|
public required string Place { get; init; }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Signature Caching
|
## Signature Cache
|
||||||
|
|
||||||
**Purpose:** Persist signature across page refreshes (distributed cache: Redis/SQL)
|
### Active cache model
|
||||||
|
The current receiver page cache flow is handled directly in the server project through:
|
||||||
|
- `EnvelopeReceiverPageDataService`
|
||||||
|
- `IDistributedCache`
|
||||||
|
- SQL Server distributed cache configuration from `Program.cs`
|
||||||
|
|
||||||
### API Endpoints
|
### Cache key format
|
||||||
**Controller:** `API/Controllers/CacheController.cs`
|
Current server-side key prefix:
|
||||||
|
- `envelope-generator.receiver-ui.signature:{receiverSignature}`
|
||||||
|
|
||||||
- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save
|
This is different from an envelope-key-only cache convention.
|
||||||
- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load
|
|
||||||
- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete
|
|
||||||
|
|
||||||
**Cache Key Format:**
|
### Config
|
||||||
```
|
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/CacheOptions.cs`
|
||||||
signature:91751687-8ae6-4777-bf5f-b8846085e62e:{envelopeKey}
|
- section name: `Cache`
|
||||||
```
|
- option: `SignatureCacheExpiration`
|
||||||
|
|
||||||
**Configuration:** `appsettings.json`
|
### Related controller
|
||||||
```json
|
A cache API controller also exists in:
|
||||||
{
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/CacheController.cs`
|
||||||
"Cache": {
|
|
||||||
"SignatureCacheExpiration": null // or "02:00:00" for 2h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Service
|
|
||||||
**File:** `ReceiverUI/Services/SignatureCacheService.cs`
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class SignatureCacheService {
|
|
||||||
Task SaveSignatureAsync(string envelopeKey, SignatureCaptureDto signature);
|
|
||||||
Task<SignatureCaptureDto?> GetSignatureAsync(string envelopeKey);
|
|
||||||
Task DeleteSignatureAsync(string envelopeKey);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Error Handling:** Fire-and-forget saves, graceful degradation on load failure.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sender Login
|
## Sender Dashboard
|
||||||
|
|
||||||
**Route:** `/sender/login`
|
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor`
|
||||||
**File:** `ReceiverUI/Pages/LoginSenderPage.razor`
|
|
||||||
**Tech:** Bootstrap 5 + DevExpress Blazing Berry theme
|
|
||||||
|
|
||||||
### AuthService Extension
|
Current behavior:
|
||||||
**File:** `ReceiverUI/Services/AuthService.cs`
|
- checks sender access through `AuthService.CheckSenderAccessAsync()`
|
||||||
|
- redirects to `/sender/login` when unauthorized
|
||||||
|
- loads envelope list through client `EnvelopeService`
|
||||||
|
- separates envelopes into active/completed tabs
|
||||||
|
- uses `DevExpress DxGrid`
|
||||||
|
|
||||||
```csharp
|
The sender page is active, but create/edit/delete actions are still marked with TODO behavior in the UI page.
|
||||||
public enum SenderLoginResult { Success, InvalidCredentials, Error }
|
|
||||||
|
|
||||||
public async Task<SenderLoginResult> LoginSenderAsync(string username, string password) {
|
|
||||||
var response = await http.PostAsJsonAsync(
|
|
||||||
$"{_api.BaseUrl}/api/auth?cookie=true",
|
|
||||||
new { username, password });
|
|
||||||
|
|
||||||
return response.StatusCode switch {
|
|
||||||
HttpStatusCode.OK => SenderLoginResult.Success,
|
|
||||||
HttpStatusCode.Unauthorized => SenderLoginResult.InvalidCredentials,
|
|
||||||
_ => SenderLoginResult.Error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Integration
|
|
||||||
**Endpoint:** `POST /api/auth?cookie=true`
|
|
||||||
|
|
||||||
**Request:**
|
|
||||||
```json
|
|
||||||
{ "username": "TekH", "password": "***" }
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
- `200 OK` ? Cookie set, redirect to `/sender`
|
|
||||||
- `401 Unauthorized` ? Show error: "Ungültige Anmeldedaten"
|
|
||||||
- Other ? Show error: "Serverfehler"
|
|
||||||
|
|
||||||
**Cookie:** HTTP-only, Secure (HTTPS), SameSite=Strict
|
|
||||||
|
|
||||||
### UI Flow
|
|
||||||
1. User enters username + password
|
|
||||||
2. Click "Anmelden" or press Enter
|
|
||||||
3. Call `AuthService.LoginSenderAsync()`
|
|
||||||
4. Success ? `Navigation.NavigateTo("/sender", forceLoad: true)`
|
|
||||||
5. Error ? Display alert
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Receiver Login
|
## Localization
|
||||||
|
|
||||||
**Route:** `/envelope/login/{EnvelopeKey}`
|
Current server host localization setup in `Program.cs`:
|
||||||
**File:** `ReceiverUI/Pages/LoginReceiverPage.razor`
|
- supported cultures: `de-DE`, `en-US`
|
||||||
|
- request localization middleware is enabled
|
||||||
|
- `QueryStringRequestCultureProvider` is added
|
||||||
|
- cookie-based localization services are registered via `AddCookieBasedLocalizer()`
|
||||||
|
|
||||||
**Multi-Envelope Support:** Cookies are stored per-envelope (e.g., `AuthTokenSignFLOWReceiver.{envelopeKey}`), allowing simultaneous authentication for multiple envelopes in the same browser session.
|
Do not assume the old ReceiverUI-only `localStorage` culture approach is the current source of truth for the active host.
|
||||||
|
|
||||||
### AuthService Method
|
|
||||||
```csharp
|
|
||||||
public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
|
|
||||||
|
|
||||||
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string key, string accessCode) {
|
|
||||||
var form = new MultipartFormDataContent();
|
|
||||||
form.Add(new StringContent(accessCode), "AccessCode");
|
|
||||||
|
|
||||||
var response = await http.PostAsync(
|
|
||||||
$"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(key)}", form);
|
|
||||||
|
|
||||||
return response.StatusCode switch {
|
|
||||||
HttpStatusCode.OK => EnvelopeLoginResult.Success,
|
|
||||||
HttpStatusCode.Unauthorized => EnvelopeLoginResult.InvalidCode,
|
|
||||||
HttpStatusCode.NotFound => EnvelopeLoginResult.NotFound,
|
|
||||||
_ => EnvelopeLoginResult.Error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Success:** Redirect to `/envelope/{key}`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## NuGet Packages (ReceiverUI)
|
## Coordinate System
|
||||||
|
|
||||||
| Package | Version | Purpose |
|
### Source data
|
||||||
|---|---|---|
|
Database signature coordinates are still based on:
|
||||||
| `DevExpress.Blazor.*` | 25.2.3 | UI components (grids, popups, etc.) |
|
- **unit:** inches
|
||||||
| `SkiaSharp.*` | 3.119.1 | WASM rendering |
|
- **origin:** top-left
|
||||||
| ~~`itext`~~ | ~~8.0.5~~ | **NOT USED** (GPL license) |
|
- **axes:** X right, Y down
|
||||||
|
|
||||||
**External CDN:**
|
### Relevant conversions
|
||||||
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
- inches -> PDF points: `x_pt = x_inches * 72`
|
||||||
|
- inches -> DevExpress DX units: `x_dx = x_inches * 100`
|
||||||
|
|
||||||
|
### Current receiver page behavior
|
||||||
|
The server page data service converts signature placeholders to **points** before sending them into the viewer workflow.
|
||||||
|
|
||||||
|
### Unit systems to keep in mind
|
||||||
|
| System | Unit | Origin | Y-axis |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Database | Inches | Top-left | Down |
|
||||||
|
| PDF.js display | Pixels | Top-left | Down |
|
||||||
|
| PDF points | Points | Depends on PDF model | Depends on consumer |
|
||||||
|
| DevExpress DX | 1/100 inch style coordinates | Top-left-oriented usage in this app | Down-oriented usage |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Mistakes History — Do NOT Repeat
|
## Key Services and Files
|
||||||
|
|
||||||
| Mistake | Why Wrong |
|
### Client services
|
||||||
|---|---|
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs`
|
||||||
| Using iText7 in EnvelopeReceiver | GPL license issue. Use overlay system instead. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs`
|
||||||
| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs`
|
||||||
| Hardcoded quality values in PDF.js | Use `appsettings.json` for configurability. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureCacheService.cs`
|
||||||
| Complex toolbar layouts | User wants simplicity. Keep horizontal layout. |
|
|
||||||
| Over-designed UI (gradients/badges) | User prefers simple text labels. |
|
### Server services
|
||||||
| Ignoring "revert" instructions | Revert HTML structure, not just CSS. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs`
|
||||||
| `BottomMarginBand` for signatures | Repeats on every page. Use DetailBand. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs`
|
||||||
| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand. Calculate per-page. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
|
||||||
|
|
||||||
|
### Server config and host files
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Development Notes
|
## Working Rules for This Workspace
|
||||||
|
|
||||||
### Deprecated Projects
|
- Treat `EnvelopeGenerator.Server` as the active main application host.
|
||||||
**DO NOT USE:**
|
- Treat `EnvelopeGenerator.Server.Client` as the active client UI project.
|
||||||
- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified ReceiverUI
|
- Prefer current `Server` / `Server.Client` paths over old `WebUI` / `ReceiverUI` references.
|
||||||
- PSPDFKit — Removed, use PDF.js + DevExpress instead
|
- 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.
|
||||||
### Legacy Projects (VB.NET)
|
- For receiver PDF/signature work, prefer the current `PDF.js`-based flow in `EnvelopeReceiverPage.razor`.
|
||||||
**DO NOT TOUCH:** `EnvelopeGenerator.Service`, `EnvelopeGenerator.Form`, `EnvelopeGenerator.BBTests`
|
- For DevExpress PDF viewer issues, remember server-side services are registered in `EnvelopeGenerator.Server`.
|
||||||
|
|
||||||
### Signature Coordinate Evidence
|
|
||||||
**File:** `EnvelopeGenerator.Form/frmFieldEditor.vb` (VB.NET)
|
|
||||||
|
|
||||||
```vb
|
|
||||||
Private Const SIGNATURE_WIDTH As Single = 1.77 ' inches
|
|
||||||
Private Const SIGNATURE_HEIGHT As Single = 1.96 ' inches
|
|
||||||
|
|
||||||
Sub LoadAnnotation(pElement As Signature, ...)
|
|
||||||
oAnnotation.Left = CSng(pElement.X) ' Direct INCHES assignment
|
|
||||||
oAnnotation.Top = CSng(pElement.Y)
|
|
||||||
End Sub
|
|
||||||
```
|
|
||||||
|
|
||||||
Proves database uses INCHES natively.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick Reference
|
**Last Updated:** 2026-06-29
|
||||||
|
|
||||||
### When working with coordinates:
|
|
||||||
1. **Database ? UI:** INCHES × 72 = PDF Points
|
|
||||||
2. **UI ? Display:** Points × scale = Pixels
|
|
||||||
3. **iText7 stamping:** Flip Y-axis (top-down ? bottom-up)
|
|
||||||
|
|
||||||
### When adding features:
|
|
||||||
1. Check `Mistakes History` first
|
|
||||||
2. Prefer simplicity over complexity
|
|
||||||
3. Use `appsettings.json` for configuration
|
|
||||||
4. Keep consistent with existing design (Bootstrap 5 + Blazing Berry)
|
|
||||||
5. **Unified frontend:** 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)
|
|
||||||
|
|||||||
@@ -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>
|
/// <summary>
|
||||||
/// Data Transfer Object representing configuration settings.
|
/// Data Transfer Object representing configuration settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class ConfigDto
|
public class ConfigDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class DocReceiverElementDto : IDocReceiverElement
|
public class DocReceiverElementDto : IDocReceiverElement
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class DocumentDto
|
public class DocumentDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing the status of a document for a specific receiver.
|
/// Data Transfer Object representing the status of a document for a specific receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class DocumentStatusDto
|
public class DocumentStatusDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||||
using DigitalData.UserManager.Application.DTOs.User;
|
using DigitalData.UserManager.Application.DTOs.User;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.History;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record EnvelopeDto : IEnvelope
|
public record EnvelopeDto : IEnvelope
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -133,5 +129,10 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<ReceiverDto>? Receivers { get; set; }
|
public IEnumerable<EnvelopeReceiverDto>? EnvelopeReceivers { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<HistoryDto>? Histories { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record EnvelopeReceiverDto
|
public record EnvelopeReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -75,4 +74,13 @@ public record EnvelopeReceiverDto
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasPhoneNumber { get; init; }
|
public bool HasPhoneNumber { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether this receiver has signed the envelope.
|
||||||
|
/// Checks if there is a DocumentSigned history entry for this receiver in the envelope's history.
|
||||||
|
/// </summary>
|
||||||
|
public bool Signed => Envelope?.Histories?.Any(h =>
|
||||||
|
h.Receiver?.Id == ReceiverId &&
|
||||||
|
h.Status == EnvelopeStatus.DocumentSigned
|
||||||
|
) ?? false;
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
|
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
@@ -8,7 +7,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="DateValid"></param>
|
/// <param name="DateValid"></param>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record EnvelopeReceiverReadOnlyCreateDto(
|
public record EnvelopeReceiverReadOnlyCreateDto(
|
||||||
DateTime DateValid)
|
DateTime DateValid)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
|
|
||||||
@@ -8,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
|||||||
/// Represents a read-only Data Transfer Object (DTO) for an envelope receiver.
|
/// Represents a read-only Data Transfer Object (DTO) for an envelope receiver.
|
||||||
/// Contains information about the receiver, associated envelope, and audit details.
|
/// Contains information about the receiver, associated envelope, and audit details.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class EnvelopeReceiverReadOnlyDto
|
public class EnvelopeReceiverReadOnlyDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object for updating a read-only envelope receiver.
|
/// Data Transfer Object for updating a read-only envelope receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class EnvelopeReceiverReadOnlyUpdateDto
|
public class EnvelopeReceiverReadOnlyUpdateDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class EnvelopeTypeDto
|
public class EnvelopeTypeDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class GtxMessagingResponse : Dictionary<string, object?> { }
|
public class GtxMessagingResponse : Dictionary<string, object?> { }
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record SmsResponse
|
public record SmsResponse
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
@@ -7,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class ReceiverDto
|
public class ReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public sealed record Signature
|
|||||||
public string? Position
|
public string? Position
|
||||||
{
|
{
|
||||||
get => _position;
|
get => _position;
|
||||||
init => _position = string.IsNullOrWhiteSpace(value) ? value : null;
|
init => _position = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -62,4 +62,4 @@ public sealed record Signature
|
|||||||
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string Place { get; init; }
|
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.Application.Common.Extensions;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the AutoMapper profile configuration for mapping between
|
/// Represents the AutoMapper profile configuration for mapping between
|
||||||
@@ -26,15 +28,17 @@ public class MappingProfile : Profile
|
|||||||
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
||||||
CreateMap<DocumentStatus, DocumentStatusDto>();
|
CreateMap<DocumentStatus, DocumentStatusDto>();
|
||||||
CreateMap<EmailTemplate, EmailTemplateDto>();
|
CreateMap<EmailTemplate, EmailTemplateDto>();
|
||||||
CreateMap<Envelope, EnvelopeDto>().ForMember(dest => dest.Receivers, opt => opt.MapFrom(src => src.EnvelopeReceivers.Select(er => er.Receiver)));
|
CreateMap<Envelope, EnvelopeDto>()
|
||||||
|
.ForMember(dest => dest.EnvelopeTypeTitle,
|
||||||
|
opt => opt.MapFrom(src => src.Type != null ? src.Type.Title : null));
|
||||||
CreateMap<Document, DocumentDto>();
|
CreateMap<Document, DocumentDto>();
|
||||||
CreateMap<Domain.Entities.History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<Domain.Entities.History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverDto>();
|
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
CreateMap<EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
||||||
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
||||||
CreateMap<Domain.Entities.Receiver, ReceiverDto>();
|
CreateMap<Receiver, ReceiverDto>();
|
||||||
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
CreateMap<EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
||||||
CreateMap<ElementAnnotation, AnnotationDto>();
|
CreateMap<ElementAnnotation, AnnotationDto>();
|
||||||
|
|
||||||
// DTO to Entity mappings
|
// DTO to Entity mappings
|
||||||
@@ -47,13 +51,13 @@ public class MappingProfile : Profile
|
|||||||
CreateMap<EmailTemplateDto, EmailTemplate>();
|
CreateMap<EmailTemplateDto, EmailTemplate>();
|
||||||
CreateMap<EnvelopeDto, Envelope>();
|
CreateMap<EnvelopeDto, Envelope>();
|
||||||
CreateMap<DocumentDto, Document>();
|
CreateMap<DocumentDto, Document>();
|
||||||
CreateMap<HistoryDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
CreateMap<HistoryDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||||
CreateMap<HistoryCreateDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
CreateMap<HistoryCreateDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||||
CreateMap<EnvelopeReceiverDto, Domain.Entities.EnvelopeReceiver>();
|
CreateMap<EnvelopeReceiverDto, EnvelopeReceiver>();
|
||||||
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
||||||
CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
CreateMap<ReceiverDto, Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
||||||
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
CreateMap<EnvelopeReceiverReadOnlyCreateDto, EnvelopeReceiverReadOnly>();
|
||||||
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, EnvelopeReceiverReadOnly>();
|
||||||
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
||||||
.MapAddedWhen();
|
.MapAddedWhen();
|
||||||
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Query;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Query;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
||||||
@@ -29,5 +31,6 @@ public record ReceiverQueryBase
|
|||||||
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
|
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
|
||||||
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
[NotMapped]
|
||||||
|
public virtual bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ public class EnvelopeReceiverAddReadSQL : ISQL<Envelope>
|
|||||||
/// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE,
|
/// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE,
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Raw => @"
|
public string Raw => @"
|
||||||
DECLARE @OUT_RECEIVER_ID int
|
DECLARE @OUT_RECEIVER_ID bigint
|
||||||
|
|
||||||
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
|
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
|
||||||
{0},
|
{0},
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ public static class DependencyInjection
|
|||||||
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
services.Configure<TotpSmsParams>(config.GetSection(nameof(TotpSmsParams)));
|
||||||
|
|
||||||
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
services.AddHttpClientService<GtxMessagingParams>(config.GetSection(nameof(GtxMessagingParams)));
|
||||||
services.TryAddSingleton<ISmsSender, GTXSmsSender>();
|
services.TryAddScoped<ISmsSender, GTXSmsSender>(); // Changed: Singleton → Scoped
|
||||||
services.TryAddSingleton<IEnvelopeSmsHandler, EnvelopeSmsHandler>();
|
services.TryAddScoped<IEnvelopeSmsHandler, EnvelopeSmsHandler>(); // Changed: Singleton → Scoped
|
||||||
services.TryAddSingleton<IAuthenticator, Authenticator>();
|
services.TryAddSingleton<IAuthenticator, Authenticator>();
|
||||||
services.TryAddSingleton<QRCodeGenerator>();
|
services.TryAddSingleton<QRCodeGenerator>();
|
||||||
|
|
||||||
@@ -65,18 +65,21 @@ public static class DependencyInjection
|
|||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, EnvelopeReceiverResolutionBehavior>();
|
||||||
|
|
||||||
// 1. AnnotationBehavior - Saves annotations (executes second)
|
// 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>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, AnnotationBehavior>();
|
||||||
|
|
||||||
// 2. DocStatusBehavior - Creates document status (executes third)
|
// 3. DocStatusBehavior - Creates document status
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, DocStatusBehavior>();
|
||||||
|
|
||||||
// 3. HistoryBehavior - Records history (executes fourth)
|
// 4. HistoryBehavior - Records history
|
||||||
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, HistoryBehavior>();
|
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>();
|
cfg.AddBehavior<IPipelineBehavior<SigningCommand, Unit>, SendSignedMailBehavior>();
|
||||||
});
|
});
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,11 @@ public class EnvelopeReceiverResolutionBehavior : IPipelineBehavior<SigningComma
|
|||||||
// If EnvelopeReceiver is not provided, query it from database
|
// If EnvelopeReceiver is not provided, query it from database
|
||||||
if (request.EnvelopeReceiver is null)
|
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");
|
?? throw new NotFoundException("EnvelopeReceiver not found");
|
||||||
|
|
||||||
request.SetEnvelopeReceiver(_mapper.Map<EnvelopeReceiverDto>(er));
|
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.");
|
throw new BadRequestException($"Signatures are required for saving signature behavior.");
|
||||||
|
|
||||||
var elements = await _elementRepo
|
var elements = await _elementRepo
|
||||||
.Where(e => e.Document.EnvelopeId == request.Envelope.Id)
|
.Where(e => e.Document.EnvelopeId == request.EnvelopeReceiver.EnvelopeId)
|
||||||
.Where(e => e.ReceiverId == request.Receiver.Id)
|
.Where(e => e.ReceiverId == request.EnvelopeReceiver.ReceiverId)
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|
||||||
foreach (var element in elements)
|
foreach (var element in elements)
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ namespace EnvelopeGenerator.Application.Documents.Queries;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Id">The unique identifier of the document. Optional.</param>
|
/// <param name="Id">The unique identifier of the document. Optional.</param>
|
||||||
/// <param name="EnvelopeId">The identifier of the envelope associated with the document. Optional.</param>
|
/// <param name="EnvelopeId">The identifier of the envelope associated with the document. Optional.</param>
|
||||||
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null) : IRequest<DocumentDto>
|
/// <param name="UserId">The unique identifier of the sending user. Optional.</param>
|
||||||
|
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null, int? UserId = null) : IRequest<DocumentDto>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +54,10 @@ public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, Docum
|
|||||||
/// </exception>
|
/// </exception>
|
||||||
public async Task<DocumentDto> Handle(ReadDocumentQuery query, CancellationToken cancel)
|
public async Task<DocumentDto> Handle(ReadDocumentQuery query, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
var docQuery = _repo.Query.Include(doc => doc.Elements).ThenInclude(e => e.Annotations);
|
var docQuery = _repo.Query.Include(doc => doc.Elements).ThenInclude(e => e.Annotations).AsNoTracking();
|
||||||
|
|
||||||
|
if (query.UserId is int userId)
|
||||||
|
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
|
||||||
|
|
||||||
if (query.Id is not null)
|
if (query.Id is not null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Resources\Model.Designer.vb" />
|
<None Remove="Resources\Model.Designer.vb" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -21,7 +21,6 @@
|
|||||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.18" />
|
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||||
@@ -79,25 +78,25 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||||
<PackageReference Include="CommandDotNet">
|
<PackageReference Include="CommandDotNet">
|
||||||
<Version>7.0.5</Version>
|
<Version>7.0.5</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||||
<PackageReference Include="CommandDotNet">
|
<PackageReference Include="CommandDotNet">
|
||||||
<Version>8.1.1</Version>
|
<Version>8.1.1</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||||
<PackageReference Include="CommandDotNet">
|
<PackageReference Include="CommandDotNet">
|
||||||
<Version>8.1.1</Version>
|
<Version>8.1.1</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public bool OnlyActive { get; init; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
public bool OnlyCompleted { get; init; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Abfrage des Include des Umschlags
|
/// Abfrage des Include des Umschlags
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -132,6 +142,12 @@ public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnum
|
|||||||
query = query.Where(e => !status.Ignore.Contains(e.Status));
|
query = query.Where(e => !status.Ignore.Contains(e.Status));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(request is { OnlyActive: true })
|
||||||
|
query = query.Where(e => Status.Active.Contains(e.Status));
|
||||||
|
|
||||||
|
if (request is { OnlyCompleted: true })
|
||||||
|
query = query.Where(e => Status.Completed.Contains(e.Status));
|
||||||
|
|
||||||
var envelopes = await query
|
var envelopes = await query
|
||||||
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using DigitalData.Core.Abstraction.Application.Repository;
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
@@ -14,7 +13,6 @@ namespace EnvelopeGenerator.Application.Receivers.Commands;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public record CreateReceiverCommand : IRequest<(ReceiverDto Receiver, bool AlreadyExists)>
|
public record CreateReceiverCommand : IRequest<(ReceiverDto Receiver, bool AlreadyExists)>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
namespace EnvelopeGenerator.Application.Receivers.Commands;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Receivers.Commands;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object for updating a receiver's information.
|
/// Data Transfer Object for updating a receiver's information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
public class UpdateReceiverCommand
|
public class UpdateReceiverCommand
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using EnvelopeGenerator.Application.Common.Query;
|
|||||||
using MediatR;
|
using MediatR;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Receivers.Queries;
|
namespace EnvelopeGenerator.Application.Receivers.Queries;
|
||||||
|
|
||||||
@@ -12,7 +13,24 @@ namespace EnvelopeGenerator.Application.Receivers.Queries;
|
|||||||
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
||||||
/// um spezifische Informationen über einen Empfänger abzurufen.
|
/// um spezifische Informationen über einen Empfänger abzurufen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>;
|
public record ReadReceiverQuery : ReceiverQueryBase, IRequest<IEnumerable<ReceiverDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Suchbegriff für eine teilweise Übereinstimmung in der E-Mail Adresse des Empfängers
|
||||||
|
/// </summary>
|
||||||
|
public virtual string? EmailAddressSearch { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether any of the specified query criteria have a value.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This property returns <c>true</c> if at least one of the fields
|
||||||
|
/// <see cref="ReceiverQueryBase.Id"/>, <see cref="ReceiverQueryBase.EmailAddress"/>, <see cref="EmailAddressSearch"/>, or <see cref="ReceiverQueryBase.Signature"/> is not null.
|
||||||
|
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
||||||
|
/// </remarks>
|
||||||
|
[NotMapped]
|
||||||
|
public override bool HasAnyCriteria => EmailAddressSearch is not null || base.HasAnyCriteria;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
@@ -53,6 +71,11 @@ public class ReadReceiverQueryHandler : IRequestHandler<ReadReceiverQuery, IEnum
|
|||||||
query = query.Where(r => r.EmailAddress == email);
|
query = query.Where(r => r.EmailAddress == email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.EmailAddressSearch))
|
||||||
|
{
|
||||||
|
query = query.Where(r => EF.Functions.Like(r.EmailAddress, $"%{request.EmailAddressSearch}%"));
|
||||||
|
}
|
||||||
|
|
||||||
if (request.Signature is string signature)
|
if (request.Signature is string signature)
|
||||||
{
|
{
|
||||||
query = query.Where(r => r.Signature == signature);
|
query = query.Where(r => r.Signature == signature);
|
||||||
|
|||||||
@@ -397,4 +397,412 @@ public static class Extensions
|
|||||||
/// <param name="suffix"></param>
|
/// <param name="suffix"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string LockedFooterBody(this IStringLocalizer localizer, string suffix) => localizer[nameof(LockedFooterBody) + suffix].Value;
|
public static string LockedFooterBody(this IStringLocalizer localizer, string suffix) => localizer[nameof(LockedFooterBody) + suffix].Value;
|
||||||
|
|
||||||
|
// Sender-side UI resources
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string NewEnvelope(this IStringLocalizer localizer) => localizer[nameof(NewEnvelope)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string LoadEnvelope(this IStringLocalizer localizer) => localizer[nameof(LoadEnvelope)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteEnvelope(this IStringLocalizer localizer) => localizer[nameof(DeleteEnvelope)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string RefreshData(this IStringLocalizer localizer) => localizer[nameof(RefreshData)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string RefreshedAt(this IStringLocalizer localizer) => localizer[nameof(RefreshedAt)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ShowDocument(this IStringLocalizer localizer) => localizer[nameof(ShowDocument)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ContactReceiver(this IStringLocalizer localizer) => localizer[nameof(ContactReceiver)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EnvelopeId(this IStringLocalizer localizer) => localizer[nameof(EnvelopeId)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string OpenLogDirectory(this IStringLocalizer localizer) => localizer[nameof(OpenLogDirectory)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ShowResultsReport(this IStringLocalizer localizer) => localizer[nameof(ShowResultsReport)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SupportMail(this IStringLocalizer localizer) => localizer[nameof(SupportMail)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ResendInvitation(this IStringLocalizer localizer) => localizer[nameof(ResendInvitation)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Export(this IStringLocalizer localizer) => localizer[nameof(Export)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Receivers(this IStringLocalizer localizer) => localizer[nameof(Receivers)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EmailSalutation(this IStringLocalizer localizer) => localizer[nameof(EmailSalutation)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SignedWhen(this IStringLocalizer localizer) => localizer[nameof(SignedWhen)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AccessCode(this IStringLocalizer localizer) => localizer[nameof(AccessCode)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string User(this IStringLocalizer localizer) => localizer[nameof(User)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Type(this IStringLocalizer localizer) => localizer[nameof(Type)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Title(this IStringLocalizer localizer) => localizer[nameof(Title)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string CreatedOn(this IStringLocalizer localizer) => localizer[nameof(CreatedOn)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string LastModified(this IStringLocalizer localizer) => localizer[nameof(LastModified)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string OpenEnvelopes(this IStringLocalizer localizer) => localizer[nameof(OpenEnvelopes)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string CompletedEnvelopes(this IStringLocalizer localizer) => localizer[nameof(CompletedEnvelopes)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SendAccessCode(this IStringLocalizer localizer) => localizer[nameof(SendAccessCode)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string TwoFactorProperties(this IStringLocalizer localizer) => localizer[nameof(TwoFactorProperties)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Name(this IStringLocalizer localizer) => localizer[nameof(Name)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string PhoneNumber(this IStringLocalizer localizer) => localizer[nameof(PhoneNumber)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AddReceiver(this IStringLocalizer localizer) => localizer[nameof(AddReceiver)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteReceiver(this IStringLocalizer localizer) => localizer[nameof(DeleteReceiver)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AddFile(this IStringLocalizer localizer) => localizer[nameof(AddFile)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string MergeFiles(this IStringLocalizer localizer) => localizer[nameof(MergeFiles)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteFile(this IStringLocalizer localizer) => localizer[nameof(DeleteFile)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ShowFile(this IStringLocalizer localizer) => localizer[nameof(ShowFile)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EditFields(this IStringLocalizer localizer) => localizer[nameof(EditFields)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EditData(this IStringLocalizer localizer) => localizer[nameof(EditData)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Save(this IStringLocalizer localizer) => localizer[nameof(Save)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SendEnvelope(this IStringLocalizer localizer) => localizer[nameof(SendEnvelope)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Cancel(this IStringLocalizer localizer) => localizer[nameof(Cancel)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AddSignature(this IStringLocalizer localizer) => localizer[nameof(AddSignature)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteSignature(this IStringLocalizer localizer) => localizer[nameof(DeleteSignature)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Language(this IStringLocalizer localizer) => localizer[nameof(Language)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string UseAccessCode(this IStringLocalizer localizer) => localizer[nameof(UseAccessCode)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string TwoFactorEnabled(this IStringLocalizer localizer) => localizer[nameof(TwoFactorEnabled)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string CertificationType(this IStringLocalizer localizer) => localizer[nameof(CertificationType)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string FinalEmailToCreator(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToCreator)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string FinalEmailToReceivers(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToReceivers)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string SendReminderEmails(this IStringLocalizer localizer) => localizer[nameof(SendReminderEmails)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string FirstReminderDays(this IStringLocalizer localizer) => localizer[nameof(FirstReminderDays)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ReminderIntervalDays(this IStringLocalizer localizer) => localizer[nameof(ReminderIntervalDays)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ExpiresWhenDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWhenDays)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string ExpiresWarningDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWarningDays)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Message(this IStringLocalizer localizer) => localizer[nameof(Message)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EnvelopeType(this IStringLocalizer localizer) => localizer[nameof(EnvelopeType)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string AllOptions(this IStringLocalizer localizer) => localizer[nameof(AllOptions)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string DeleteReason(this IStringLocalizer localizer) => localizer[nameof(DeleteReason)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string PleaseProvideReason(this IStringLocalizer localizer) => localizer[nameof(PleaseProvideReason)].Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string Status(this IStringLocalizer localizer) => localizer[nameof(Status)].Value;
|
||||||
}
|
}
|
||||||
@@ -477,4 +477,178 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Bestätigungen</value>
|
<value>Bestätigungen</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="NewEnvelope" xml:space="preserve">
|
||||||
|
<value>Neuer Umschlag</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoadEnvelope" xml:space="preserve">
|
||||||
|
<value>Umschlag laden</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteEnvelope" xml:space="preserve">
|
||||||
|
<value>Umschlag zurückrufen/löschen</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshData" xml:space="preserve">
|
||||||
|
<value>Daten Aktualisieren</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshedAt" xml:space="preserve">
|
||||||
|
<value>Aktualisiert: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowDocument" xml:space="preserve">
|
||||||
|
<value>Dokument anzeigen</value>
|
||||||
|
</data>
|
||||||
|
<data name="ContactReceiver" xml:space="preserve">
|
||||||
|
<value>Empfänger kontaktieren</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeId" xml:space="preserve">
|
||||||
|
<value>Umschlag-ID: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenLogDirectory" xml:space="preserve">
|
||||||
|
<value>Öffne Log Verzeichnis</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowResultsReport" xml:space="preserve">
|
||||||
|
<value>Ergebnisbericht anzeigen</value>
|
||||||
|
</data>
|
||||||
|
<data name="SupportMail" xml:space="preserve">
|
||||||
|
<value>Support Mail</value>
|
||||||
|
</data>
|
||||||
|
<data name="ResendInvitation" xml:space="preserve">
|
||||||
|
<value>Einladung manuell versenden</value>
|
||||||
|
</data>
|
||||||
|
<data name="Export" xml:space="preserve">
|
||||||
|
<value>Export</value>
|
||||||
|
</data>
|
||||||
|
<data name="Receivers" xml:space="preserve">
|
||||||
|
<value>Empfänger</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailSalutation" xml:space="preserve">
|
||||||
|
<value>Email Anrede</value>
|
||||||
|
</data>
|
||||||
|
<data name="SignedWhen" xml:space="preserve">
|
||||||
|
<value>Unterschrieben wann</value>
|
||||||
|
</data>
|
||||||
|
<data name="AccessCode" xml:space="preserve">
|
||||||
|
<value>Zugangscode</value>
|
||||||
|
</data>
|
||||||
|
<data name="User" xml:space="preserve">
|
||||||
|
<value>Benutzer</value>
|
||||||
|
</data>
|
||||||
|
<data name="Type" xml:space="preserve">
|
||||||
|
<value>Typ</value>
|
||||||
|
</data>
|
||||||
|
<data name="Title" xml:space="preserve">
|
||||||
|
<value>Titel</value>
|
||||||
|
</data>
|
||||||
|
<data name="CreatedOn" xml:space="preserve">
|
||||||
|
<value>Erstellt am</value>
|
||||||
|
</data>
|
||||||
|
<data name="LastModified" xml:space="preserve">
|
||||||
|
<value>Zuletzt geändert am</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenEnvelopes" xml:space="preserve">
|
||||||
|
<value>Offene Umschläge</value>
|
||||||
|
</data>
|
||||||
|
<data name="CompletedEnvelopes" xml:space="preserve">
|
||||||
|
<value>Abgeschlossene Umschläge</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendAccessCode" xml:space="preserve">
|
||||||
|
<value>Zugangscode senden</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorProperties" xml:space="preserve">
|
||||||
|
<value>2-Faktor Eigenschaften</value>
|
||||||
|
</data>
|
||||||
|
<data name="Name" xml:space="preserve">
|
||||||
|
<value>Name</value>
|
||||||
|
</data>
|
||||||
|
<data name="PhoneNumber" xml:space="preserve">
|
||||||
|
<value>Telefonnummer</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
|
<value>Empfänger hinzufügen</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
|
<value>Empfänger löschen</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddFile" xml:space="preserve">
|
||||||
|
<value>Datei hinzufügen</value>
|
||||||
|
</data>
|
||||||
|
<data name="MergeFiles" xml:space="preserve">
|
||||||
|
<value>Dateien zusammenführen</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteFile" xml:space="preserve">
|
||||||
|
<value>Datei löschen</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowFile" xml:space="preserve">
|
||||||
|
<value>Datei anzeigen</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditFields" xml:space="preserve">
|
||||||
|
<value>Felder bearbeiten</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditData" xml:space="preserve">
|
||||||
|
<value>Daten bearbeiten</value>
|
||||||
|
</data>
|
||||||
|
<data name="Save" xml:space="preserve">
|
||||||
|
<value>Speichern</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendEnvelope" xml:space="preserve">
|
||||||
|
<value>Umschlag versenden</value>
|
||||||
|
</data>
|
||||||
|
<data name="Cancel" xml:space="preserve">
|
||||||
|
<value>Abbrechen</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddSignature" xml:space="preserve">
|
||||||
|
<value>Signatur hinzufügen</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteSignature" xml:space="preserve">
|
||||||
|
<value>Signatur löschen</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language" xml:space="preserve">
|
||||||
|
<value>Sprache</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseAccessCode" xml:space="preserve">
|
||||||
|
<value>Zugangscode verwenden</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorEnabled" xml:space="preserve">
|
||||||
|
<value>2-Faktor-Authentifizierung aktiviert</value>
|
||||||
|
</data>
|
||||||
|
<data name="CertificationType" xml:space="preserve">
|
||||||
|
<value>Zertifizierungstyp</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||||
|
<value>Finale E-Mail an Ersteller</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||||
|
<value>Finale E-Mail an Empfänger</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendReminderEmails" xml:space="preserve">
|
||||||
|
<value>Erinnerungs-E-Mails senden</value>
|
||||||
|
</data>
|
||||||
|
<data name="FirstReminderDays" xml:space="preserve">
|
||||||
|
<value>Erste Erinnerung (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||||
|
<value>Erinnerungsintervall (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||||
|
<value>Läuft ab nach (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWarningDays" xml:space="preserve">
|
||||||
|
<value>Ablaufwarnung (Tage)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message" xml:space="preserve">
|
||||||
|
<value>Nachricht</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeType" xml:space="preserve">
|
||||||
|
<value>Umschlagtyp</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllOptions" xml:space="preserve">
|
||||||
|
<value>Alle Optionen</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReason" xml:space="preserve">
|
||||||
|
<value>Grund für Löschung</value>
|
||||||
|
</data>
|
||||||
|
<data name="PleaseProvideReason" xml:space="preserve">
|
||||||
|
<value>Bitte geben Sie einen Grund an</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status" xml:space="preserve">
|
||||||
|
<value>Status</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -477,4 +477,178 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Confirmations</value>
|
<value>Confirmations</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="NewEnvelope" xml:space="preserve">
|
||||||
|
<value>New Envelope</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoadEnvelope" xml:space="preserve">
|
||||||
|
<value>Load Envelope</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteEnvelope" xml:space="preserve">
|
||||||
|
<value>Delete Envelope</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshData" xml:space="preserve">
|
||||||
|
<value>Reload Data</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshedAt" xml:space="preserve">
|
||||||
|
<value>Refreshed: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowDocument" xml:space="preserve">
|
||||||
|
<value>Show Document</value>
|
||||||
|
</data>
|
||||||
|
<data name="ContactReceiver" xml:space="preserve">
|
||||||
|
<value>Contact Receiver</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeId" xml:space="preserve">
|
||||||
|
<value>Envelope-ID: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenLogDirectory" xml:space="preserve">
|
||||||
|
<value>Open Log Directory</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowResultsReport" xml:space="preserve">
|
||||||
|
<value>Show Results Report</value>
|
||||||
|
</data>
|
||||||
|
<data name="SupportMail" xml:space="preserve">
|
||||||
|
<value>Support Mail</value>
|
||||||
|
</data>
|
||||||
|
<data name="ResendInvitation" xml:space="preserve">
|
||||||
|
<value>Send Invitation Again</value>
|
||||||
|
</data>
|
||||||
|
<data name="Export" xml:space="preserve">
|
||||||
|
<value>Export</value>
|
||||||
|
</data>
|
||||||
|
<data name="Receivers" xml:space="preserve">
|
||||||
|
<value>Receivers</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailSalutation" xml:space="preserve">
|
||||||
|
<value>Email Salutation</value>
|
||||||
|
</data>
|
||||||
|
<data name="SignedWhen" xml:space="preserve">
|
||||||
|
<value>Signed When</value>
|
||||||
|
</data>
|
||||||
|
<data name="AccessCode" xml:space="preserve">
|
||||||
|
<value>Access Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="User" xml:space="preserve">
|
||||||
|
<value>User</value>
|
||||||
|
</data>
|
||||||
|
<data name="Type" xml:space="preserve">
|
||||||
|
<value>Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="Title" xml:space="preserve">
|
||||||
|
<value>Title</value>
|
||||||
|
</data>
|
||||||
|
<data name="CreatedOn" xml:space="preserve">
|
||||||
|
<value>Created On</value>
|
||||||
|
</data>
|
||||||
|
<data name="LastModified" xml:space="preserve">
|
||||||
|
<value>Last Modified</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenEnvelopes" xml:space="preserve">
|
||||||
|
<value>Open Envelopes</value>
|
||||||
|
</data>
|
||||||
|
<data name="CompletedEnvelopes" xml:space="preserve">
|
||||||
|
<value>Completed Envelopes</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendAccessCode" xml:space="preserve">
|
||||||
|
<value>Send Access Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorProperties" xml:space="preserve">
|
||||||
|
<value>2-Factor Properties</value>
|
||||||
|
</data>
|
||||||
|
<data name="Name" xml:space="preserve">
|
||||||
|
<value>Name</value>
|
||||||
|
</data>
|
||||||
|
<data name="PhoneNumber" xml:space="preserve">
|
||||||
|
<value>Phone Number</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
|
<value>Add Receiver</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
|
<value>Delete Receiver</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddFile" xml:space="preserve">
|
||||||
|
<value>Add File</value>
|
||||||
|
</data>
|
||||||
|
<data name="MergeFiles" xml:space="preserve">
|
||||||
|
<value>Merge Files</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteFile" xml:space="preserve">
|
||||||
|
<value>Delete File</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowFile" xml:space="preserve">
|
||||||
|
<value>Show File</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditFields" xml:space="preserve">
|
||||||
|
<value>Edit Fields</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditData" xml:space="preserve">
|
||||||
|
<value>Edit Data</value>
|
||||||
|
</data>
|
||||||
|
<data name="Save" xml:space="preserve">
|
||||||
|
<value>Save</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendEnvelope" xml:space="preserve">
|
||||||
|
<value>Send Envelope</value>
|
||||||
|
</data>
|
||||||
|
<data name="Cancel" xml:space="preserve">
|
||||||
|
<value>Cancel</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddSignature" xml:space="preserve">
|
||||||
|
<value>Add Signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteSignature" xml:space="preserve">
|
||||||
|
<value>Delete Signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language" xml:space="preserve">
|
||||||
|
<value>Language</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseAccessCode" xml:space="preserve">
|
||||||
|
<value>Use Access Code</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorEnabled" xml:space="preserve">
|
||||||
|
<value>2-Factor Authentication Enabled</value>
|
||||||
|
</data>
|
||||||
|
<data name="CertificationType" xml:space="preserve">
|
||||||
|
<value>Certification Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||||
|
<value>Final Email to Creator</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||||
|
<value>Final Email to Receivers</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendReminderEmails" xml:space="preserve">
|
||||||
|
<value>Send Reminder Emails</value>
|
||||||
|
</data>
|
||||||
|
<data name="FirstReminderDays" xml:space="preserve">
|
||||||
|
<value>First Reminder (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||||
|
<value>Reminder Interval (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||||
|
<value>Expires After (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWarningDays" xml:space="preserve">
|
||||||
|
<value>Expiry Warning (Days)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message" xml:space="preserve">
|
||||||
|
<value>Message</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeType" xml:space="preserve">
|
||||||
|
<value>Envelope Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllOptions" xml:space="preserve">
|
||||||
|
<value>All Options</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReason" xml:space="preserve">
|
||||||
|
<value>Deletion Reason</value>
|
||||||
|
</data>
|
||||||
|
<data name="PleaseProvideReason" xml:space="preserve">
|
||||||
|
<value>Please provide a reason</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status" xml:space="preserve">
|
||||||
|
<value>Status</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -477,4 +477,178 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Confirmations</value>
|
<value>Confirmations</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="NewEnvelope" xml:space="preserve">
|
||||||
|
<value>Nouvelle enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="LoadEnvelope" xml:space="preserve">
|
||||||
|
<value>Charger l'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteEnvelope" xml:space="preserve">
|
||||||
|
<value>Supprimer l'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshData" xml:space="preserve">
|
||||||
|
<value>Actualiser les données</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshedAt" xml:space="preserve">
|
||||||
|
<value>Actualisé : {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowDocument" xml:space="preserve">
|
||||||
|
<value>Afficher le document</value>
|
||||||
|
</data>
|
||||||
|
<data name="ContactReceiver" xml:space="preserve">
|
||||||
|
<value>Contacter le destinataire</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeId" xml:space="preserve">
|
||||||
|
<value>ID d'enveloppe : {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenLogDirectory" xml:space="preserve">
|
||||||
|
<value>Ouvrir le répertoire des logs</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowResultsReport" xml:space="preserve">
|
||||||
|
<value>Afficher le rapport de résultats</value>
|
||||||
|
</data>
|
||||||
|
<data name="SupportMail" xml:space="preserve">
|
||||||
|
<value>E-mail de support</value>
|
||||||
|
</data>
|
||||||
|
<data name="ResendInvitation" xml:space="preserve">
|
||||||
|
<value>Renvoyer l'invitation</value>
|
||||||
|
</data>
|
||||||
|
<data name="Export" xml:space="preserve">
|
||||||
|
<value>Exporter</value>
|
||||||
|
</data>
|
||||||
|
<data name="Receivers" xml:space="preserve">
|
||||||
|
<value>Destinataires</value>
|
||||||
|
</data>
|
||||||
|
<data name="EmailSalutation" xml:space="preserve">
|
||||||
|
<value>Formule de politesse</value>
|
||||||
|
</data>
|
||||||
|
<data name="SignedWhen" xml:space="preserve">
|
||||||
|
<value>Signé quand</value>
|
||||||
|
</data>
|
||||||
|
<data name="AccessCode" xml:space="preserve">
|
||||||
|
<value>Code d'accès</value>
|
||||||
|
</data>
|
||||||
|
<data name="User" xml:space="preserve">
|
||||||
|
<value>Utilisateur</value>
|
||||||
|
</data>
|
||||||
|
<data name="Type" xml:space="preserve">
|
||||||
|
<value>Type</value>
|
||||||
|
</data>
|
||||||
|
<data name="Title" xml:space="preserve">
|
||||||
|
<value>Titre</value>
|
||||||
|
</data>
|
||||||
|
<data name="CreatedOn" xml:space="preserve">
|
||||||
|
<value>Créé le</value>
|
||||||
|
</data>
|
||||||
|
<data name="LastModified" xml:space="preserve">
|
||||||
|
<value>Dernière modification</value>
|
||||||
|
</data>
|
||||||
|
<data name="OpenEnvelopes" xml:space="preserve">
|
||||||
|
<value>Enveloppes ouvertes</value>
|
||||||
|
</data>
|
||||||
|
<data name="CompletedEnvelopes" xml:space="preserve">
|
||||||
|
<value>Enveloppes terminées</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendAccessCode" xml:space="preserve">
|
||||||
|
<value>Envoyer le code d'accès</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorProperties" xml:space="preserve">
|
||||||
|
<value>Propriétés 2-facteurs</value>
|
||||||
|
</data>
|
||||||
|
<data name="Name" xml:space="preserve">
|
||||||
|
<value>Nom</value>
|
||||||
|
</data>
|
||||||
|
<data name="PhoneNumber" xml:space="preserve">
|
||||||
|
<value>Numéro de téléphone</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddReceiver" xml:space="preserve">
|
||||||
|
<value>Ajouter un destinataire</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReceiver" xml:space="preserve">
|
||||||
|
<value>Supprimer le destinataire</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddFile" xml:space="preserve">
|
||||||
|
<value>Ajouter un fichier</value>
|
||||||
|
</data>
|
||||||
|
<data name="MergeFiles" xml:space="preserve">
|
||||||
|
<value>Fusionner les fichiers</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteFile" xml:space="preserve">
|
||||||
|
<value>Supprimer le fichier</value>
|
||||||
|
</data>
|
||||||
|
<data name="ShowFile" xml:space="preserve">
|
||||||
|
<value>Afficher le fichier</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditFields" xml:space="preserve">
|
||||||
|
<value>Modifier les champs</value>
|
||||||
|
</data>
|
||||||
|
<data name="EditData" xml:space="preserve">
|
||||||
|
<value>Modifier les données</value>
|
||||||
|
</data>
|
||||||
|
<data name="Save" xml:space="preserve">
|
||||||
|
<value>Enregistrer</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendEnvelope" xml:space="preserve">
|
||||||
|
<value>Envoyer l'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="Cancel" xml:space="preserve">
|
||||||
|
<value>Annuler</value>
|
||||||
|
</data>
|
||||||
|
<data name="AddSignature" xml:space="preserve">
|
||||||
|
<value>Ajouter une signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteSignature" xml:space="preserve">
|
||||||
|
<value>Supprimer la signature</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language" xml:space="preserve">
|
||||||
|
<value>Langue</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseAccessCode" xml:space="preserve">
|
||||||
|
<value>Utiliser un code d'accès</value>
|
||||||
|
</data>
|
||||||
|
<data name="TwoFactorEnabled" xml:space="preserve">
|
||||||
|
<value>Authentification à 2 facteurs activée</value>
|
||||||
|
</data>
|
||||||
|
<data name="CertificationType" xml:space="preserve">
|
||||||
|
<value>Type de certification</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||||
|
<value>E-mail final au créateur</value>
|
||||||
|
</data>
|
||||||
|
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||||
|
<value>E-mail final aux destinataires</value>
|
||||||
|
</data>
|
||||||
|
<data name="SendReminderEmails" xml:space="preserve">
|
||||||
|
<value>Envoyer des e-mails de rappel</value>
|
||||||
|
</data>
|
||||||
|
<data name="FirstReminderDays" xml:space="preserve">
|
||||||
|
<value>Premier rappel (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||||
|
<value>Intervalle de rappel (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||||
|
<value>Expire après (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="ExpiresWarningDays" xml:space="preserve">
|
||||||
|
<value>Avertissement d'expiration (jours)</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message" xml:space="preserve">
|
||||||
|
<value>Message</value>
|
||||||
|
</data>
|
||||||
|
<data name="EnvelopeType" xml:space="preserve">
|
||||||
|
<value>Type d'enveloppe</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllOptions" xml:space="preserve">
|
||||||
|
<value>Toutes les options</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeleteReason" xml:space="preserve">
|
||||||
|
<value>Motif de suppression</value>
|
||||||
|
</data>
|
||||||
|
<data name="PleaseProvideReason" xml:space="preserve">
|
||||||
|
<value>Veuillez indiquer une raison</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status" xml:space="preserve">
|
||||||
|
<value>Statut</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
Imports System.IO
|
Imports System.IO
|
||||||
Imports DigitalData.Modules.Database
|
Imports DigitalData.Modules.Database
|
||||||
Imports DigitalData.Modules.Logging
|
Imports DigitalData.Modules.Logging
|
||||||
|
Imports EnvelopeGenerator.CommonServices
|
||||||
Imports EnvelopeGenerator.CommonServices.Jobs
|
Imports EnvelopeGenerator.CommonServices.Jobs
|
||||||
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument
|
Imports EnvelopeGenerator.CommonServices.Jobs.FinalizeDocument
|
||||||
Imports GdPicture14
|
Imports GdPicture14
|
||||||
@@ -145,64 +146,91 @@ Public Class frmFinalizePDF
|
|||||||
log.AppendLine($"Document bytes: {oBuffer.Length}")
|
log.AppendLine($"Document bytes: {oBuffer.Length}")
|
||||||
|
|
||||||
' 3. Check what BurnAnnotsToPDF will do internally
|
' 3. Check what BurnAnnotsToPDF will do internally
|
||||||
|
Dim oEnvelopeEntity As Envelope = Nothing
|
||||||
Using scope = Factory.Shared.ScopeFactory.CreateScope()
|
Using scope = Factory.Shared.ScopeFactory.CreateScope()
|
||||||
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
|
Dim envRepo = scope.ServiceProvider.Repository(Of Envelope)()
|
||||||
Dim envelope = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
|
oEnvelopeEntity = envRepo.Where(Function(env) env.Id = envelopeId).FirstOrDefault()
|
||||||
|
|
||||||
If envelope Is Nothing Then
|
If oEnvelopeEntity Is Nothing Then
|
||||||
log.AppendLine("ERROR: Envelope not found in EF Core!")
|
log.AppendLine("ERROR: Envelope not found in EF Core!")
|
||||||
txtResult.Text = log.ToString()
|
txtResult.Text = log.ToString()
|
||||||
Return
|
Return
|
||||||
End If
|
End If
|
||||||
|
|
||||||
log.AppendLine($"Envelope found: Id={envelope.Id}, EnvelopeTypeId={envelope.EnvelopeTypeId}")
|
log.AppendLine($"Envelope found: Id={oEnvelopeEntity.Id}, EnvelopeTypeId={oEnvelopeEntity.EnvelopeTypeId}")
|
||||||
log.AppendLine($"ReadOnly (IsReadAndConfirm): {envelope.ReadOnly}")
|
log.AppendLine($"ReadOnly (IsReadAndConfirm): {oEnvelopeEntity.ReadOnly}")
|
||||||
|
|
||||||
If envelope.ReadOnly Then
|
If oEnvelopeEntity.ReadOnly Then
|
||||||
log.AppendLine(">>> EARLY RETURN: ReadOnly=True, original PDF returned without burning")
|
log.AppendLine(">>> ReadOnly=True: original PDF will be used without burning (annotations skipped)")
|
||||||
txtResult.Text = log.ToString()
|
|
||||||
Return
|
|
||||||
End If
|
|
||||||
|
|
||||||
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
|
|
||||||
Dim elements = sigRepo _
|
|
||||||
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
|
||||||
.Include(Function(sig) sig.Annotations) _
|
|
||||||
.ToList()
|
|
||||||
|
|
||||||
log.AppendLine($"Elements (Signature) count: {elements.Count}")
|
|
||||||
|
|
||||||
If elements.Any() Then
|
|
||||||
log.AppendLine(">>> PATH: BurnElementAnnotsToPDF (new element-based path)")
|
|
||||||
For Each elem In elements
|
|
||||||
Dim annotCount = If(elem.Annotations IsNot Nothing, elem.Annotations.Count(), 0)
|
|
||||||
log.AppendLine($" Element Id={elem.Id}, Page={elem.Page}, X={elem.X}, Y={elem.Y}, W={elem.Width}, H={elem.Height}, Annotations={annotCount}")
|
|
||||||
If elem.Annotations IsNot Nothing Then
|
|
||||||
For Each annot In elem.Annotations
|
|
||||||
log.AppendLine($" Annot: Name={annot.Name}, Type={annot.Type}, X={annot.X}, Y={annot.Y}, W={annot.Width}, H={annot.Height}")
|
|
||||||
Next
|
|
||||||
End If
|
|
||||||
Next
|
|
||||||
Else
|
Else
|
||||||
log.AppendLine(">>> PATH: BurnInstantJSONAnnotsToPDF (old JSON-based path)")
|
Dim sigRepo = scope.ServiceProvider.Repository(Of DocReceiverElement)()
|
||||||
|
Dim elements = sigRepo _
|
||||||
|
.Where(Function(sig) sig.Document.EnvelopeId = envelopeId) _
|
||||||
|
.Include(Function(sig) sig.Annotations) _
|
||||||
|
.ToList()
|
||||||
|
|
||||||
|
log.AppendLine($"Elements (Signature) count: {elements.Count}")
|
||||||
|
|
||||||
|
If elements.Any() Then
|
||||||
|
log.AppendLine(">>> PATH: BurnElementAnnotsToPDF (new element-based path)")
|
||||||
|
For Each elem In elements
|
||||||
|
Dim annotCount = If(elem.Annotations IsNot Nothing, elem.Annotations.Count(), 0)
|
||||||
|
log.AppendLine($" Element Id={elem.Id}, Page={elem.Page}, X={elem.X}, Y={elem.Y}, W={elem.Width}, H={elem.Height}, Annotations={annotCount}")
|
||||||
|
If elem.Annotations IsNot Nothing Then
|
||||||
|
For Each annot In elem.Annotations
|
||||||
|
log.AppendLine($" Annot: Name={annot.Name}, Type={annot.Type}, X={annot.X}, Y={annot.Y}, W={annot.Width}, H={annot.Height}")
|
||||||
|
Next
|
||||||
|
End If
|
||||||
|
Next
|
||||||
|
Else
|
||||||
|
log.AppendLine(">>> PATH: BurnInstantJSONAnnotsToPDF (old JSON-based path)")
|
||||||
|
End If
|
||||||
End If
|
End If
|
||||||
End Using
|
End Using
|
||||||
|
|
||||||
' 4. Actually call BurnAnnotsToPDF (same as Service)
|
' 4. Burn annotations (ReadOnly envelopes return original PDF unchanged)
|
||||||
log.AppendLine("")
|
log.AppendLine("")
|
||||||
log.AppendLine("Calling BurnAnnotsToPDF...")
|
log.AppendLine("Calling BurnAnnotsToPDF...")
|
||||||
Dim oNewBuffer = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
|
Dim oBurnedBuffer As Byte() = PDFBurner.BurnAnnotsToPDF(oBuffer, oJsonList, envelopeId)
|
||||||
log.AppendLine($"Result bytes: {oNewBuffer.Length}")
|
log.AppendLine($"Burned bytes: {oBurnedBuffer.Length}")
|
||||||
log.AppendLine($"Same as input: {oBuffer.Length = oNewBuffer.Length AndAlso oBuffer.SequenceEqual(oNewBuffer)}")
|
log.AppendLine($"Unchanged (ReadOnly): {oBuffer.Length = oBurnedBuffer.Length AndAlso oBuffer.SequenceEqual(oBurnedBuffer)}")
|
||||||
|
|
||||||
' 5. Write output
|
' 5. Build State and load Envelope domain model for ReportCreator
|
||||||
|
log.AppendLine("")
|
||||||
|
log.AppendLine("Loading envelope domain model for report...")
|
||||||
|
Dim oState As New State() With {
|
||||||
|
.Database = Database,
|
||||||
|
.LogConfig = LogConfig
|
||||||
|
}
|
||||||
|
Dim oEnvelopeModel As New EnvelopeModel(oState)
|
||||||
|
Dim oEnvelope = oEnvelopeModel.GetById(envelopeId)
|
||||||
|
If oEnvelope Is Nothing Then
|
||||||
|
log.AppendLine("ERROR: Envelope not found via EnvelopeModel!")
|
||||||
|
txtResult.Text = log.ToString()
|
||||||
|
Return
|
||||||
|
End If
|
||||||
|
log.AppendLine($"Envelope loaded: {oEnvelope.Id}")
|
||||||
|
|
||||||
|
' 6. Create signing certificate report
|
||||||
|
log.AppendLine("Creating signing certificate report...")
|
||||||
|
Dim oReportCreator As New ReportCreator(LogConfig, oState)
|
||||||
|
Dim oReportBuffer As Byte() = oReportCreator.CreateReport(oEnvelope)
|
||||||
|
log.AppendLine($"Report bytes: {oReportBuffer.Length}")
|
||||||
|
|
||||||
|
' 7. Merge burned document + report (last page)
|
||||||
|
log.AppendLine("Merging documents...")
|
||||||
|
Dim oMerger As New PDFMerger(LogConfig, pGDPictureLicenseKey)
|
||||||
|
Dim oMergedBuffer As Byte() = oMerger.MergeDocuments(oBurnedBuffer, oReportBuffer)
|
||||||
|
log.AppendLine($"Merged bytes: {oMergedBuffer.Length}")
|
||||||
|
|
||||||
|
' 8. Save and open
|
||||||
Dim desktopPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
|
Dim desktopPath As String = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
|
||||||
Dim oNewPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.burned.pdf")
|
Dim oMergedPath = Path.Combine(desktopPath, $"E{txtEnvelope.Text}_FullTest.merged.pdf")
|
||||||
File.WriteAllBytes(oNewPath, oNewBuffer)
|
File.WriteAllBytes(oMergedPath, oMergedBuffer)
|
||||||
log.AppendLine($"Output: {oNewPath}")
|
log.AppendLine($"Output: {oMergedPath}")
|
||||||
|
|
||||||
txtResult.Text = log.ToString()
|
txtResult.Text = log.ToString()
|
||||||
Process.Start(oNewPath)
|
Process.Start(oMergedPath)
|
||||||
|
|
||||||
Catch ex As Exception
|
Catch ex As Exception
|
||||||
txtResult.Text = $"ERROR: {ex.Message}{vbCrLf}{vbCrLf}{ex.ToString()}"
|
txtResult.Text = $"ERROR: {ex.Message}{vbCrLf}{vbCrLf}{ex.ToString()}"
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Dim y = frameY + yOffsetofFF
|
Dim y = frameY + yOffsetofFF
|
||||||
|
|
||||||
If annot.Type = AnnotationType.FormField Then
|
If annot.Type = AnnotationType.FormField Then
|
||||||
AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value)
|
AddFormFieldValue(annot.X / inchFactor, y, annot.Width / inchFactor, annot.Height / inchFactor, element.Page, annot.Value, annot.Name)
|
||||||
ElseIf annot.Type = AnnotationType.Image Then
|
ElseIf annot.Type = AnnotationType.Image Then
|
||||||
AddImageAnnotation(
|
AddImageAnnotation(
|
||||||
annot.X / inchFactor,
|
annot.X / inchFactor,
|
||||||
@@ -269,11 +269,14 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Next
|
Next
|
||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Private Sub AddFormFieldValue(x As Double, y As Double, width As Double, height As Double, page As Integer, value As String)
|
Private Sub AddFormFieldValue(x As Double, y As Double, width As Double, height As Double, page As Integer, value As String, Optional egName As String = "")
|
||||||
Manager.SelectPage(page)
|
Manager.SelectPage(page)
|
||||||
|
|
||||||
|
' Truncate if needed
|
||||||
|
Dim displayValue As String = TruncateText(value, egName)
|
||||||
|
|
||||||
' Add the text annotation
|
' Add the text annotation
|
||||||
Dim ant = Manager.AddTextAnnot(x, y, width, height, value)
|
Dim ant = Manager.AddTextAnnot(x, y, width, height, displayValue)
|
||||||
|
|
||||||
' Set the font properties
|
' Set the font properties
|
||||||
ant.FontName = _pdfBurnerParams.FontName
|
ant.FontName = _pdfBurnerParams.FontName
|
||||||
@@ -294,8 +297,9 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Dim oHeight = oBounds.Item(3)
|
Dim oHeight = oBounds.Item(3)
|
||||||
|
|
||||||
Manager.SelectPage(pAnnotation.pageIndex + 1)
|
Manager.SelectPage(pAnnotation.pageIndex + 1)
|
||||||
' Add the text annotation
|
' Add the text annotation (truncate if needed)
|
||||||
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, formFieldValue.value)
|
Dim displayValue As String = TruncateText(formFieldValue.value, pAnnotation.egName)
|
||||||
|
Dim ant = Manager.AddTextAnnot(oX, oY, oWidth, oHeight, displayValue)
|
||||||
|
|
||||||
' Set the font properties
|
' Set the font properties
|
||||||
ant.FontName = _pdfBurnerParams.FontName
|
ant.FontName = _pdfBurnerParams.FontName
|
||||||
@@ -306,6 +310,48 @@ Namespace Jobs.FinalizeDocument
|
|||||||
#End Region
|
#End Region
|
||||||
|
|
||||||
#Region "Helpers"
|
#Region "Helpers"
|
||||||
|
''' <summary>
|
||||||
|
''' Truncates a field value so its rendered width fits within the configured max width (inches).
|
||||||
|
''' Uses Graphics.MeasureString with the configured font for pixel-accurate measurement.
|
||||||
|
''' Appends TruncationSuffix when the value is cut.
|
||||||
|
''' </summary>
|
||||||
|
Private Function TruncateText(value As String, fieldEgName As String) As String
|
||||||
|
If String.IsNullOrEmpty(value) Then Return value
|
||||||
|
If _pdfBurnerParams.TextMaxWidths Is Nothing Then Return value
|
||||||
|
|
||||||
|
Dim maxWidthInches As Single = 0
|
||||||
|
If Not _pdfBurnerParams.TextMaxWidths.TryGetValue(fieldEgName, maxWidthInches) Then Return value
|
||||||
|
If maxWidthInches <= 0 Then Return value
|
||||||
|
|
||||||
|
Dim suffix As String = If(_pdfBurnerParams.TruncationSuffix, "")
|
||||||
|
|
||||||
|
Using bmp As New Bitmap(1, 1)
|
||||||
|
Using g As Graphics = Graphics.FromImage(bmp)
|
||||||
|
Dim font As New Font(_pdfBurnerParams.FontName, _pdfBurnerParams.FontSize, _pdfBurnerParams.FontStyle, GraphicsUnit.Point)
|
||||||
|
Dim maxWidthPx As Single = maxWidthInches * g.DpiX
|
||||||
|
|
||||||
|
' Already fits — no truncation needed
|
||||||
|
If g.MeasureString(value, font).Width <= maxWidthPx Then Return value
|
||||||
|
|
||||||
|
' Binary search for the longest prefix that fits (with suffix)
|
||||||
|
Dim lo As Integer = 0
|
||||||
|
Dim hi As Integer = value.Length
|
||||||
|
While lo < hi
|
||||||
|
Dim mid As Integer = (lo + hi + 1) \ 2
|
||||||
|
Dim candidate As String = value.Substring(0, mid) & suffix
|
||||||
|
If g.MeasureString(candidate, font).Width <= maxWidthPx Then
|
||||||
|
lo = mid
|
||||||
|
Else
|
||||||
|
hi = mid - 1
|
||||||
|
End If
|
||||||
|
End While
|
||||||
|
|
||||||
|
Return value.Substring(0, lo) & suffix
|
||||||
|
End Using
|
||||||
|
End Using
|
||||||
|
End Function
|
||||||
|
|
||||||
|
|
||||||
Private Function ToPointF(pPoints As List(Of Single)) As PointF
|
Private Function ToPointF(pPoints As List(Of Single)) As PointF
|
||||||
Dim oPoints = pPoints.Select(AddressOf ToInches).ToList()
|
Dim oPoints = pPoints.Select(AddressOf ToInches).ToList()
|
||||||
Return New PointF(oPoints.Item(0), oPoints.Item(1))
|
Return New PointF(oPoints.Item(0), oPoints.Item(1))
|
||||||
|
|||||||
@@ -12,5 +12,24 @@ Namespace Jobs.FinalizeDocument
|
|||||||
Public Property FontSize As Integer = 8
|
Public Property FontSize As Integer = 8
|
||||||
|
|
||||||
Public Property FontStyle As FontStyle = FontStyle.Italic
|
Public Property FontStyle As FontStyle = FontStyle.Italic
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' The suffix appended to a truncated field value. Defaults to "..".
|
||||||
|
''' Can be changed to any string (e.g. "…", "...", "").
|
||||||
|
''' </summary>
|
||||||
|
Public Property TruncationSuffix As String = ".."
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Per-field maximum render width in inches.
|
||||||
|
''' Text is measured using the configured Font/FontSize via Graphics.MeasureString (pixel-accurate).
|
||||||
|
''' If the rendered width exceeds the limit, the text is cut and TruncationSuffix is appended.
|
||||||
|
''' Keys must match egName values used in PDFBurner (e.g. "name", "position", "city", "date").
|
||||||
|
''' Set a value to 0 to disable truncation for that field.
|
||||||
|
''' </summary>
|
||||||
|
Public Property TextMaxWidths As New Dictionary(Of String, Single) From {
|
||||||
|
{"position", 1.41F},
|
||||||
|
{"city", 1.41F},
|
||||||
|
{"date", 2.0F}
|
||||||
|
}
|
||||||
End Class
|
End Class
|
||||||
End Namespace
|
End Namespace
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
Imports EnvelopeGenerator.Domain.Constants
|
Imports EnvelopeGenerator.Domain.Constants
|
||||||
Imports EnvelopeGenerator.Domain.Entities
|
Imports EnvelopeGenerator.Domain.Entities
|
||||||
|
Imports EnvelopeGenerator.Domain.Interfaces
|
||||||
|
|
||||||
Public Class ReportItem
|
Public Class ReportItem
|
||||||
|
|
||||||
@@ -11,8 +12,15 @@ Public Class ReportItem
|
|||||||
Public Property ItemStatus As EnvelopeStatus
|
Public Property ItemStatus As EnvelopeStatus
|
||||||
Public ReadOnly Property ItemStatusTranslated As String
|
Public ReadOnly Property ItemStatusTranslated As String
|
||||||
Get
|
Get
|
||||||
Dim oStatus = ItemStatus.ToString()
|
Dim oStatusKey = ItemStatus.ToString()
|
||||||
Return My.Resources.Model.ResourceManager.GetString(oStatus)
|
|
||||||
|
If ItemStatus = EnvelopeStatus.DocumentSigned AndAlso
|
||||||
|
Envelope IsNot Nothing AndAlso
|
||||||
|
Envelope.IsReadAndConfirm() Then
|
||||||
|
oStatusKey = "DocumentSignedRaC"
|
||||||
|
End If
|
||||||
|
|
||||||
|
Return My.Resources.Model.ResourceManager.GetString(oStatusKey)
|
||||||
End Get
|
End Get
|
||||||
End Property
|
End Property
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,9 @@
|
|||||||
<data name="DocumentSigned" xml:space="preserve">
|
<data name="DocumentSigned" xml:space="preserve">
|
||||||
<value>Document signed</value>
|
<value>Document signed</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||||
|
<value>Read and confirmed</value>
|
||||||
|
</data>
|
||||||
<data name="Draft" xml:space="preserve">
|
<data name="Draft" xml:space="preserve">
|
||||||
<value>Draft</value>
|
<value>Draft</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -168,6 +168,9 @@
|
|||||||
<data name="DocumentSigned" xml:space="preserve">
|
<data name="DocumentSigned" xml:space="preserve">
|
||||||
<value>Dokument unterzeichnet</value>
|
<value>Dokument unterzeichnet</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||||
|
<value>Gelesen und bestätigt</value>
|
||||||
|
</data>
|
||||||
<data name="Draft" xml:space="preserve">
|
<data name="Draft" xml:space="preserve">
|
||||||
<value>Entwurf</value>
|
<value>Entwurf</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Domain.Constants
|
namespace EnvelopeGenerator.Domain.Constants
|
||||||
{
|
{
|
||||||
// http://wiki.dd/xwiki13/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/SignFlow/Envelope%20Status/
|
// http://wiki.dd/xwiki_prod/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/signFLOW/signFLOW%20-%20Enwickler-Handbuch/4.%20Anhang/4.3%20Historie%20und%20Status%20der%20Umschl%C3%A4ge/
|
||||||
public enum EnvelopeStatus
|
public enum EnvelopeStatus
|
||||||
{
|
{
|
||||||
Invalid = 0,
|
Invalid = 0,
|
||||||
@@ -49,5 +51,28 @@ namespace EnvelopeGenerator.Domain.Constants
|
|||||||
EnvelopeStatus.EnvelopeCreated,
|
EnvelopeStatus.EnvelopeCreated,
|
||||||
EnvelopeStatus.DocumentMod_Rotation
|
EnvelopeStatus.DocumentMod_Rotation
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static readonly List<EnvelopeStatus> Active = Enum.GetValues(typeof(EnvelopeStatus))
|
||||||
|
.Cast<EnvelopeStatus>()
|
||||||
|
.Where(status => status.IsActive())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
public static readonly List<EnvelopeStatus> Completed = Enum.GetValues(typeof(EnvelopeStatus))
|
||||||
|
.Cast<EnvelopeStatus>()
|
||||||
|
.Where(status => status.IsCompleted())
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EnvelopeStatusExtensions
|
||||||
|
{
|
||||||
|
public static bool IsActive(this EnvelopeStatus status)
|
||||||
|
{
|
||||||
|
return status >= EnvelopeStatus.EnvelopeCreated && status < EnvelopeStatus.EnvelopePartlySigned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsCompleted(this EnvelopeStatus status)
|
||||||
|
{
|
||||||
|
return status >= EnvelopeStatus.EnvelopeCompletelySigned && status <= EnvelopeStatus.EnvelopeWithdrawn;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#if NETFRAMEWORK
|
#if NETFRAMEWORK
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Domain.Constants
|
namespace EnvelopeGenerator.Domain.Constants
|
||||||
@@ -15,7 +16,11 @@ namespace EnvelopeGenerator.Domain.Constants
|
|||||||
public static class Receiver
|
public static class Receiver
|
||||||
{
|
{
|
||||||
public const string TFA = "EGReceiverTFA";
|
public const string TFA = "EGReceiverTFA";
|
||||||
|
|
||||||
|
[Obsolete("Use Receiver.Authorized")]
|
||||||
public const string Full = "EGReceiver";
|
public const string Full = "EGReceiver";
|
||||||
|
|
||||||
|
public static readonly IReadOnlyList<string> Authorized = new[] { "receiver", "EGReceiver" };
|
||||||
}
|
}
|
||||||
|
|
||||||
public const string Sender = "EGSender";
|
public const string Sender = "EGSender";
|
||||||
|
|||||||
@@ -213,6 +213,15 @@ namespace My.Resources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to Gelesen und bestätigt.
|
||||||
|
/// </summary>
|
||||||
|
public static string DocumentSignedRaC {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DocumentSignedRaC", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Entwurf.
|
/// Looks up a localized string similar to Entwurf.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -168,6 +168,9 @@
|
|||||||
<data name="DocumentSigned" xml:space="preserve">
|
<data name="DocumentSigned" xml:space="preserve">
|
||||||
<value>Dokument unterzeichnet</value>
|
<value>Dokument unterzeichnet</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="DocumentSignedRaC" xml:space="preserve">
|
||||||
|
<value>Gelesen und bestätigt</value>
|
||||||
|
</data>
|
||||||
<data name="Draft" xml:space="preserve">
|
<data name="Draft" xml:space="preserve">
|
||||||
<value>Entwurf</value>
|
<value>Entwurf</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using EnvelopeGenerator.Infrastructure.Repositories;
|
|||||||
using EnvelopeGenerator.Infrastructure.Executor;
|
using EnvelopeGenerator.Infrastructure.Executor;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
using EnvelopeGenerator.Application.Common.Interfaces.Repositories;
|
||||||
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
using EnvelopeGenerator.Application.Common.Interfaces.SQLExecutor;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
#elif NETFRAMEWORK
|
#elif NETFRAMEWORK
|
||||||
using System;
|
using System;
|
||||||
@@ -55,6 +56,9 @@ namespace EnvelopeGenerator.Infrastructure
|
|||||||
services.TryAddScoped<IEnvelopeTypeRepository, EnvelopeTypeRepository>();
|
services.TryAddScoped<IEnvelopeTypeRepository, EnvelopeTypeRepository>();
|
||||||
services.TryAddScoped<IReceiverRepository, ReceiverRepository>();
|
services.TryAddScoped<IReceiverRepository, ReceiverRepository>();
|
||||||
services.TryAddScoped<IEnvelopeReceiverReadOnlyRepository, EnvelopeReceiverReadOnlyRepository>();
|
services.TryAddScoped<IEnvelopeReceiverReadOnlyRepository, EnvelopeReceiverReadOnlyRepository>();
|
||||||
|
|
||||||
|
// User Preferences — distributed cache (dbo.TBDD_CACHE)
|
||||||
|
services.TryAddScoped<IUserPreferencesService, UserPreferencesService>();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
services.AddDbRepository(opt =>
|
services.AddDbRepository(opt =>
|
||||||
|
|||||||
@@ -204,16 +204,24 @@ public abstract class EGDbContextBase : DbContext
|
|||||||
|
|
||||||
#region Trigger
|
#region Trigger
|
||||||
// Configure entities to handle database triggers
|
// Configure entities to handle database triggers
|
||||||
void AddTrigger<T>() where T : class => _triggers
|
void AddTrigger<T>() where T : class
|
||||||
.Where(t => t.Key == typeof(T).Name)
|
{
|
||||||
.SelectMany(t => t.Value)
|
var entityName = typeof(T).Name;
|
||||||
.ForEach(tName =>
|
|
||||||
|
var configuredTriggerNames = _triggers
|
||||||
|
.Where(t => t.Key == entityName)
|
||||||
|
.SelectMany(t => t.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var triggerName in configuredTriggerNames)
|
||||||
{
|
{
|
||||||
#if NET
|
#if NET
|
||||||
modelBuilder.Entity<T>().ToTable(tb => tb.HasTrigger(tName));
|
modelBuilder.Entity<T>().ToTable(tb => tb.HasTrigger(triggerName));
|
||||||
#endif
|
#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
|
// TODO: call add trigger methods with attributes and reflection
|
||||||
AddTrigger<Config>();
|
AddTrigger<Config>();
|
||||||
@@ -243,4 +251,4 @@ public abstract class EGDbContextBase : DbContext
|
|||||||
|
|
||||||
#if NETFRAMEWORK
|
#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="DigitalData.Core.Infrastructure" Version="2.6.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.6" />
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||||
<PackageReference Include="QuestPDF" Version="2025.7.1" />
|
<PackageReference Include="QuestPDF" Version="2025.7.1" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
|
|||||||
46
EnvelopeGenerator.Infrastructure/UserPreferencesService.cs
Normal file
46
EnvelopeGenerator.Infrastructure/UserPreferencesService.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#if NET
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Infrastructure;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implements user preferences storage using the distributed cache (dbo.TBDD_CACHE).
|
||||||
|
/// No DB schema changes required — uses the existing SQL Server distributed cache table.
|
||||||
|
/// Cache key schema: "signflow.user-preferences:{username}"
|
||||||
|
/// Expiration: indefinite (no TTL) — preferences persist until explicitly overwritten.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UserPreferencesService(IDistributedCache cache) : IUserPreferencesService
|
||||||
|
{
|
||||||
|
private const string KeyPrefix = "signflow.user-preferences:";
|
||||||
|
|
||||||
|
private static string BuildKey(string id) => $"{KeyPrefix}{id}";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<UserPreferencesDto> GetOrCreateAsync(string id, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var json = await cache.GetStringAsync(BuildKey(id), ct);
|
||||||
|
if(json is not null && JsonSerializer.Deserialize<UserPreferencesDto>(json) is UserPreferencesDto pref)
|
||||||
|
{
|
||||||
|
return pref;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// init new
|
||||||
|
await SaveAsync(id, UserPreferencesDto.Default, ct);
|
||||||
|
return UserPreferencesDto.Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task SaveAsync(string id, UserPreferencesDto preferences, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(preferences);
|
||||||
|
|
||||||
|
// No expiration — preferences are stored indefinitely.
|
||||||
|
await cache.SetStringAsync(BuildKey(id), json, new DistributedCacheEntryOptions(), ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<Router AppAssembly="@typeof(Program).Assembly">
|
|
||||||
<Found Context="routeData">
|
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
|
||||||
</Found>
|
|
||||||
<NotFound>
|
|
||||||
<LayoutView Layout="@typeof(MainLayout)">
|
|
||||||
<p>Sorry, there's nothing at this address.</p>
|
|
||||||
</LayoutView>
|
|
||||||
</NotFound>
|
|
||||||
</Router>
|
|
||||||
@@ -1,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,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,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 class Adjustment
|
||||||
{
|
{
|
||||||
public static Adjustment CreateBalanceForward(DateTime dt, int random)
|
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;
|
using DevExpress.DataAccess.Sql.DataApi;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
namespace EnvelopeGenerator.Server.Client.Data {
|
||||||
public class Customer {
|
public class Customer {
|
||||||
static List<Customer> currentCustomers = new List<Customer>();
|
static List<Customer> currentCustomers = new List<Customer>();
|
||||||
|
|
||||||
@@ -61,4 +61,4 @@ namespace EnvelopeGenerator.ReceiverUI.Data {
|
|||||||
public string Fax { get; set; }
|
public string Fax { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
namespace EnvelopeGenerator.Server.Client.Data {
|
||||||
public class DataItem {
|
public class DataItem {
|
||||||
static readonly string[] accountType = new string[] { "Energy", "Manufacturing", "Estate", "Food", "Services" };
|
static readonly string[] accountType = new string[] { "Energy", "Manufacturing", "Estate", "Food", "Services" };
|
||||||
public string CustomerID { get; set; }
|
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 {
|
public class DataItemList : IList<DataItem>, IList {
|
||||||
readonly int rowCount;
|
readonly int rowCount;
|
||||||
|
|
||||||
@@ -67,4 +67,4 @@ namespace EnvelopeGenerator.ReceiverUI.Data {
|
|||||||
CopyTo(array, arrayIndex);
|
CopyTo(array, arrayIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
namespace EnvelopeGenerator.Server.Client.Data {
|
||||||
class DeterministicRandom {
|
class DeterministicRandom {
|
||||||
const int randomCount = 10000;
|
const int randomCount = 10000;
|
||||||
static readonly int[] deterministicRandomNumbers;
|
static readonly int[] deterministicRandomNumbers;
|
||||||
@@ -57,4 +57,4 @@
|
|||||||
this.rnd = i + (i >> 10) + (i >> 20);
|
this.rnd = i + (i >> 10) + (i >> 20);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Data {
|
namespace EnvelopeGenerator.Server.Client.Data {
|
||||||
public struct Term {
|
public struct Term {
|
||||||
public static readonly Term[] Terms = new Term[] {
|
public static readonly Term[] Terms = new Term[] {
|
||||||
new Term("Payment seven days after invoice date" ),
|
new Term("Payment seven days after invoice date" ),
|
||||||
@@ -12,4 +12,4 @@
|
|||||||
this.currentName = currentName;
|
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>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
|
@inherits LayoutComponentBase
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
@inject ThemeService ThemeService
|
||||||
|
|
||||||
|
<HeadContent>
|
||||||
|
<link href="@ThemeService.ThemeCssHref" rel="stylesheet" />
|
||||||
|
</HeadContent>
|
||||||
|
|
||||||
|
<div class="page" data-bs-theme="@ThemeService.BootstrapTheme">
|
||||||
|
<main>
|
||||||
|
<article class="content">
|
||||||
|
@Body
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<footer class="receiver-footer">
|
||||||
|
<div class="receiver-footer__left">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ThemeControls />
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Inject] IHttpClientFactory HttpClientFactory { get; set; } = default!;
|
||||||
|
|
||||||
|
private Action? _themeChangeHandler;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await FontLoader.LoadFonts(HttpClientFactory, ["opensans.ttf"]);
|
||||||
|
_themeChangeHandler = () => InvokeAsync(StateHasChanged);
|
||||||
|
ThemeService.OnChange += _themeChangeHandler;
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
=> ThemeService.OnChange -= _themeChangeHandler;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
|
@rendermode InteractiveWebAssembly
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
@inject ThemeService ThemeService
|
||||||
|
|
||||||
|
<div class="sf-theme-slider">
|
||||||
|
|
||||||
|
@* Moon — left end (blazing-dark) *@
|
||||||
|
<svg class="sf-theme-icon @(ThemeService.ThemeName == "blazing-dark" ? "sf-theme-icon--active" : "")"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 24 24"
|
||||||
|
title="Blazing Dark">
|
||||||
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
@* Track wrapper: range input + left/mid/right ticks *@
|
||||||
|
<div class="sf-theme-track-wrapper">
|
||||||
|
@* Left tick *@
|
||||||
|
<div class="sf-theme-tick sf-theme-tick--left"></div>
|
||||||
|
@* Mid tick (marks the middle position) *@
|
||||||
|
<div class="sf-theme-tick sf-theme-tick--mid"></div>
|
||||||
|
@* Right tick *@
|
||||||
|
<div class="sf-theme-tick sf-theme-tick--right"></div>
|
||||||
|
|
||||||
|
<input type="range"
|
||||||
|
class="sf-theme-range"
|
||||||
|
min="0" max="2" step="1"
|
||||||
|
value="@SliderValue"
|
||||||
|
@oninput="OnSliderChanged"
|
||||||
|
title="@ThemeService.AvailableThemes[SliderValue].Label" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@* Sun — right end (purple) *@
|
||||||
|
<svg class="sf-theme-icon @(ThemeService.ThemeName == "purple" ? "sf-theme-icon--active" : (ThemeService.ThemeName == "blazing-berry" ? "sf-theme-icon--mid" : ""))"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor" stroke-width="1.5"
|
||||||
|
title="Blazing Berry / Purple">
|
||||||
|
<circle cx="12" cy="12" r="4" fill="currentColor" stroke="none"/>
|
||||||
|
<line x1="12" y1="2" x2="12" y2="5"/>
|
||||||
|
<line x1="12" y1="19" x2="12" y2="22"/>
|
||||||
|
<line x1="4.22" y1="4.22" x2="6.34" y2="6.34"/>
|
||||||
|
<line x1="17.66" y1="17.66" x2="19.78" y2="19.78"/>
|
||||||
|
<line x1="2" y1="12" x2="5" y2="12"/>
|
||||||
|
<line x1="19" y1="12" x2="22" y2="12"/>
|
||||||
|
<line x1="4.22" y1="19.78" x2="6.34" y2="17.66"/>
|
||||||
|
<line x1="17.66" y1="6.34" x2="19.78" y2="4.22"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private Action? _themeChangeHandler;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps the active theme to a 0-based slider step.
|
||||||
|
/// 0 = blazing-dark (🌙 left)
|
||||||
|
/// 1 = blazing-berry (middle)
|
||||||
|
/// 2 = purple (☀️ right)
|
||||||
|
/// </summary>
|
||||||
|
private int SliderValue => ThemeService.ThemeName switch
|
||||||
|
{
|
||||||
|
"blazing-dark" => 0,
|
||||||
|
"purple" => 2,
|
||||||
|
_ => 1
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
_themeChangeHandler = () => InvokeAsync(StateHasChanged);
|
||||||
|
ThemeService.OnChange += _themeChangeHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
await ThemeService.InitializeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSliderChanged(ChangeEventArgs e)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(e.Value?.ToString(), out var step)) return;
|
||||||
|
|
||||||
|
var themeName = step switch
|
||||||
|
{
|
||||||
|
0 => "blazing-dark",
|
||||||
|
2 => "purple",
|
||||||
|
_ => "blazing-berry"
|
||||||
|
};
|
||||||
|
|
||||||
|
await ThemeService.SetThemeAsync(themeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
=> ThemeService.OnChange -= _themeChangeHandler;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
namespace EnvelopeGenerator.Server.Client.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a pre-assigned signature annotation position on a specific page.
|
/// Represents a pre-assigned signature annotation position on a specific page.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Models.Constants
|
namespace EnvelopeGenerator.Server.Client.Models.Constants
|
||||||
{
|
{
|
||||||
public enum SenderAppType
|
public enum SenderAppType
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Models.Constants;
|
namespace EnvelopeGenerator.Server.Client.Models.Constants;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the unit of measurement for coordinate values in signature positioning.
|
/// Represents the unit of measurement for coordinate values in signature positioning.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client-side DTO mirroring CustomImageEntry on the server.
|
||||||
|
/// Height is a CSS value string (e.g. "28px") set in appsettings.json.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record CustomImageDto(string Src, string Height, Dictionary<string, string> Classes)
|
||||||
|
{
|
||||||
|
public CustomImageDto() : this(string.Empty, "28px", new()) { }
|
||||||
|
|
||||||
|
/// <summary>Returns the CSS class for the given context key, or empty string if absent.</summary>
|
||||||
|
public string GetClass(string key) =>
|
||||||
|
Classes.TryGetValue(key, out var cls) && cls is not null ? cls : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Client-side DTO for the full CustomImages config (App + Company logos).</summary>
|
||||||
|
public sealed record CustomImagesDto(CustomImageDto App, CustomImageDto Company)
|
||||||
|
{
|
||||||
|
public CustomImagesDto() : this(new(), new()) { }
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
namespace EnvelopeGenerator.Server.Client.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Client-side model for the envelope receiver returned by
|
/// Client-side model for the envelope receiver returned by
|
||||||
@@ -102,5 +102,4 @@ public record ReceiverClientDto
|
|||||||
public string? Signature { get; init; }
|
public string? Signature { get; init; }
|
||||||
public DateTime AddedWhen { get; init; }
|
public DateTime AddedWhen { get; init; }
|
||||||
public DateTime? TfaRegDeadline { get; init; }
|
public DateTime? TfaRegDeadline { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
namespace EnvelopeGenerator.Server.Client.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a captured signature with metadata created by the receiver in the signature popup.
|
/// Represents a captured signature with metadata created by the receiver in the signature popup.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user