Compare commits
192 Commits
6fe99d0cd0
...
feat/remov
| Author | SHA1 | Date | |
|---|---|---|---|
| b46c3c8ebe | |||
| 9f712a6b8c | |||
| 4449743105 | |||
| 435be882c9 | |||
| 09257d7766 | |||
| 16fa4b460f | |||
| f4aae8c787 | |||
| ed1468194d | |||
| f9f94c900e | |||
| 18df6221c9 | |||
| b15f95755c | |||
| 4be9e38a9e | |||
| 11c8886d6f | |||
| 1ad673904a | |||
| 32a9d008a4 | |||
| 454dd0b6cc | |||
| aa2a31e993 | |||
| ecc509919c | |||
| f3751274b1 | |||
| adc231f10e | |||
| 999fac7dac | |||
| 86758bb127 | |||
| 498d59a4ce | |||
| ce430c1b80 | |||
| eb219291de | |||
| 80733ab1f7 | |||
| 3d9153da23 | |||
| a0bfd405f0 | |||
| ec3a15a467 | |||
| 80f08b393c | |||
| b22d57d7da | |||
| 01cd8b2607 | |||
| 310d2efc6e | |||
| e5402bafc9 | |||
| f517ab4cfb | |||
| 6d63f9db2b | |||
| 46c7c31e03 | |||
| 8f6db435bb | |||
| 9bf7676464 | |||
| d9556c18ca | |||
| 5b7157f422 | |||
| 119ee3ec16 | |||
| b536b31a4b | |||
| a80efee075 | |||
| 86d6daafbd | |||
| aefa1a8883 | |||
| b3e7f77165 | |||
| bac9bfd036 | |||
| 6effb8a0d6 | |||
| a9ff0d88f2 | |||
| 0fdfe991ea | |||
| 7fc2c14f9c | |||
| 5098deedd1 | |||
| 2e634361c5 | |||
| 60ebebc0d8 | |||
| c2d8b6531c | |||
| 665266457f | |||
| 14d581acac | |||
| eb533a1dc2 | |||
| 36025fad7b | |||
| 0606efabf9 | |||
| 2cb1e266f7 | |||
| 35c1374b9c | |||
| feaea1626b | |||
| babd96200a | |||
| f9cfcf1f19 | |||
| ffb9e07466 | |||
| 5384a08905 | |||
| dda5ff28e1 | |||
| 043349a47f | |||
| c36f7ccf98 | |||
| cf3e85bcd7 | |||
| 77379265c2 | |||
| 9b68c09b0b | |||
| cd6bf05352 | |||
| b2c205b160 | |||
| 33439f4a65 | |||
| b5cdf79799 | |||
| 72915ac001 | |||
| e4d7a0d8f3 | |||
| d5f9de06f3 | |||
| bbc129178f | |||
| 40c95100ff | |||
| 1b2731b4b2 | |||
| ae56902758 | |||
| e80059d34e | |||
| a2443032c5 | |||
| 3d2fe532e5 | |||
| 5e83aa26c9 | |||
| 2c789cd4c0 | |||
| 9ecfe08e2e | |||
| 74da6e37b0 | |||
| 10f65e583a | |||
| 0cdc8f1191 | |||
| 56ccab6377 | |||
| 4d069cdaa0 | |||
| bbbfa4de01 | |||
| 3ff3373b27 | |||
| 2af8815cf6 | |||
| ca24a96084 | |||
| db368b889a | |||
| 8f451b9c2c | |||
| 6120e6062e | |||
| cc2aea90ed | |||
| 762a9e8bca | |||
| 6ed4caea4f | |||
| d94821433a | |||
| 278b9964f1 | |||
| e6722803bb | |||
| 47bc7675c9 | |||
| 789e312316 | |||
| 2a9bbb3fe5 | |||
| bc34317720 | |||
| 76ff3e47e1 | |||
| 2d22bfcd06 | |||
| 185c783824 | |||
| b957b4b4bb | |||
| df154d83cc | |||
| 49ec9fbead | |||
| 01fc29f59e | |||
| 733b70cca2 | |||
| 8f4b751303 | |||
| a5e4f97397 | |||
| 6ca03a50eb | |||
| 96a84ba1a5 | |||
| ec0ea72890 | |||
| 7b912387e7 | |||
| 6c142eba08 | |||
| 489d2808a1 | |||
| 7466fd78f6 | |||
| e34b5ddbbe | |||
| b56f906848 | |||
| fe09c5c7ae | |||
| 0763d82f6e | |||
| 5a30bc050b | |||
| a4b218b9f3 | |||
| 67798b35da | |||
| b5bb2bbaae | |||
| 85a0736106 | |||
| de9c9da176 | |||
| f4571320ce | |||
| 2abfffdeba | |||
| bfd1a9d060 | |||
| 78ed49a077 | |||
| 6aa97adf84 | |||
| 7456babe0d | |||
| 71e375d6ea | |||
| 05f64e2b61 | |||
| ed17852542 | |||
| 9947774ba8 | |||
| c6c1decd2a | |||
| 0fdaa1a38d | |||
| 5d66de9f32 | |||
| b6ec5307b6 | |||
| 106e62a912 | |||
| 27940f5d34 | |||
| e776c2edb4 | |||
| 3f0f5d7fb9 | |||
| e11bc9df8e | |||
| 4dca17d39c | |||
| 8baf6b5553 | |||
| 3ca99fdd83 | |||
| 9e37bf1fe2 | |||
| 9a0837caa9 | |||
| 030646f33d | |||
| 88317e40f5 | |||
| 3a2fa77862 | |||
| cfa6dbd2de | |||
| eb2603f389 | |||
| 456178bee1 | |||
| 2c41c74510 | |||
| bb73795d68 | |||
| 207992d95a | |||
| d6bafc64a6 | |||
| 3090711892 | |||
| 9dbd8f7952 | |||
| 48a41f2987 | |||
| 96688a951c | |||
| 6f07de3ec4 | |||
| 4611266224 | |||
| c529d03129 | |||
| 829fab9647 | |||
| b2e3605b54 | |||
| 8cbdee2491 | |||
| 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,489 +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.** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Localization & Culture Management
|
|
||||||
|
|
||||||
**Current Architecture:** Blazor WebAssembly (client-side culture management)
|
|
||||||
|
|
||||||
### Implementation Details
|
|
||||||
|
|
||||||
**Culture Storage:**
|
|
||||||
- Culture preference stored in browser's `localStorage` (key: `AppCulture`)
|
|
||||||
- Managed by `CultureService.cs` (ReceiverUI/Services)
|
|
||||||
- Supported cultures: `de-DE`, `en-US`, `fr-FR`
|
|
||||||
|
|
||||||
**Culture Initialization:**
|
|
||||||
- **Location:** `Program.cs` (lines 53-57)
|
|
||||||
- Sets `CultureInfo.DefaultThreadCurrentCulture/UICulture` **before** app runs
|
|
||||||
- **WASM-Safe:** Each user has isolated browser instance
|
|
||||||
|
|
||||||
**Language Selector:**
|
|
||||||
- **Component:** `LanguageSelector.razor` (ReceiverUI/Shared)
|
|
||||||
- Displays flag icon + language name
|
|
||||||
- Changes culture via `CultureService.SetCultureAsync()`
|
|
||||||
- Navigates with `forceLoad: false` (smooth transition, no page reload)
|
|
||||||
|
|
||||||
### ⚠️ MIGRATION WARNING: Blazor Server/Auto
|
|
||||||
|
|
||||||
**Current approach is WASM-specific and will break in Server/Auto render modes!**
|
|
||||||
|
|
||||||
**Why it breaks:**
|
|
||||||
- `Program.cs:53-57` sets **global** `DefaultThreadCurrentCulture`
|
|
||||||
- In Server/Auto, one app instance serves **all users**
|
|
||||||
- User A selects German → User B sees German too (shared state)
|
|
||||||
- Thread-safety issues and culture conflicts
|
|
||||||
|
|
||||||
**Migration Checklist (when moving to Server/Auto):**
|
|
||||||
|
|
||||||
1. **Remove global culture initialization** from `Program.cs` (lines 53-57)
|
|
||||||
- See detailed warning comment in the code
|
|
||||||
|
|
||||||
2. **Add RequestLocalizationMiddleware** (Server-side approach):
|
|
||||||
```csharp
|
|
||||||
app.UseRequestLocalization(options => {
|
|
||||||
options.SupportedCultures = new[] { "de-DE", "en-US", "fr-FR" };
|
|
||||||
options.SupportedUICultures = options.SupportedCultures;
|
|
||||||
options.RequestCultureProviders.Insert(0, new CookieRequestCultureProvider());
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **OR** Use **per-circuit culture** (Blazor Server approach):
|
|
||||||
- Store culture in circuit-scoped service
|
|
||||||
- Use `CascadingParameter` to distribute to components
|
|
||||||
- See: https://learn.microsoft.com/aspnet/core/blazor/globalization-localization
|
|
||||||
|
|
||||||
4. **Update `LanguageSelector.razor`:**
|
|
||||||
- Remove manual `CultureInfo.DefaultThreadCurrentCulture` assignment
|
|
||||||
- Use middleware/circuit culture provider instead
|
|
||||||
|
|
||||||
5. **Update `CultureService.cs`:**
|
|
||||||
- Integrate with Server-side culture provider
|
|
||||||
- May need to store in cookies instead of localStorage
|
|
||||||
|
|
||||||
**References:**
|
|
||||||
- Microsoft Docs: [Blazor Globalization/Localization](https://learn.microsoft.com/aspnet/core/blazor/globalization-localization)
|
|
||||||
- Current implementation: `Program.cs`, `CultureService.cs`, `LanguageSelector.razor`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ namespace EnvelopeGenerator.API.Controllers;
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public class EnvelopeController : ControllerBase
|
public class EnvelopeController : ControllerBase
|
||||||
{
|
{
|
||||||
|
public record DeleteEnvelopeRequest(string Reason);
|
||||||
|
|
||||||
private readonly ILogger<EnvelopeController> _logger;
|
private readonly ILogger<EnvelopeController> _logger;
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
@@ -108,4 +110,17 @@ public class EnvelopeController : ControllerBase
|
|||||||
else
|
else
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||||
|
[HttpDelete("{id:int}")]
|
||||||
|
public async Task<IActionResult> DeleteAsync([FromRoute] int id, [FromBody] DeleteEnvelopeRequest request)
|
||||||
|
{
|
||||||
|
var result = await _mediator.Send(new DeleteEnvelopeCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = id,
|
||||||
|
Reason = request?.Reason ?? string.Empty
|
||||||
|
}.WithAuth(User.GetId()));
|
||||||
|
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -214,6 +214,10 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
if (reader.Read())
|
if (reader.Read())
|
||||||
{
|
{
|
||||||
bool outSuccess = reader.GetBoolean(0);
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
|
if (!outSuccess)
|
||||||
|
_logger.LogWarning(
|
||||||
|
"PRSIG_API_ADD_DOC_RECEIVER_ELEM returned OUT_SUCCESS=false. DOC_ID={DocId}, RECEIVER_ID={ReceiverId}, Page={Page}",
|
||||||
|
document.Id, rcv.Id, sign.Page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
@@ -221,8 +225,6 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
#region Create history
|
#region Create history
|
||||||
// ENV_UID, STATUS_ID, USER_ID,
|
// ENV_UID, STATUS_ID, USER_ID,
|
||||||
string sql_hist = @"
|
string sql_hist = @"
|
||||||
USE [DD_ECM]
|
|
||||||
|
|
||||||
DECLARE @OUT_SUCCESS bit;
|
DECLARE @OUT_SUCCESS bit;
|
||||||
|
|
||||||
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
EXEC [dbo].[PRSIG_API_ADD_HISTORY_STATE]
|
||||||
@@ -244,6 +246,10 @@ public class EnvelopeReceiverController : ControllerBase
|
|||||||
if (reader.Read())
|
if (reader.Read())
|
||||||
{
|
{
|
||||||
bool outSuccess = reader.GetBoolean(0);
|
bool outSuccess = reader.GetBoolean(0);
|
||||||
|
if (!outSuccess)
|
||||||
|
_logger.LogWarning(
|
||||||
|
"PRSIG_API_ADD_HISTORY_STATE returned OUT_SUCCESS=false. EnvelopeUuid={EnvelopeUuid}",
|
||||||
|
envelope.Uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ public class ExceptionHandlingMiddleware
|
|||||||
message = notFoundEx.Message;
|
message = notFoundEx.Message;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ForbiddenException forbiddenEx:
|
||||||
|
context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
|
||||||
|
message = forbiddenEx.Message;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
logger.LogError(exception, "Unhandled exception occurred.");
|
logger.LogError(exception, "Unhandled exception occurred.");
|
||||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
|
|||||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||||
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
|
<ProjectGuid>5e0e17c0-ff5a-4246-bf87-1add85376a27</ProjectGuid>
|
||||||
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\API\net8\$(Version)\EnvelopeGenerator.API.zip</DesktopBuildPackageLocation>
|
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\signFLOW\PreRelease\API\net8\$(Version)\EnvelopeGenerator.API.zip</DesktopBuildPackageLocation>
|
||||||
<PackageAsSingleFile>true</PackageAsSingleFile>
|
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||||
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
|
<DeployIisAppPath>EnvelopeGenerator</DeployIisAppPath>
|
||||||
<_TargetId>IISWebDeployPackage</_TargetId>
|
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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.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;
|
||||||
@@ -129,4 +130,9 @@ public record EnvelopeDto : IEnvelope
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<EnvelopeReceiverDto>? EnvelopeReceivers { 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,5 +1,6 @@
|
|||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
|
||||||
@@ -73,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;
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -28,7 +28,9 @@ 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>();
|
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<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<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));
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||||
|
|
||||||
|
public record EnvelopeDeletedNotification : INotification, ISendMailNotification
|
||||||
|
{
|
||||||
|
public required Envelope Envelope { get; init; }
|
||||||
|
|
||||||
|
public required EnvelopeReceiver EnvelopeReceiver { get; init; }
|
||||||
|
|
||||||
|
public required string Reason { get; init; }
|
||||||
|
|
||||||
|
public required string SenderReference { get; init; }
|
||||||
|
|
||||||
|
public EmailTemplateType TemplateType => EmailTemplateType.DocumentDeleted;
|
||||||
|
|
||||||
|
public string EmailAddress => EnvelopeReceiver.Receiver?.EmailAddress
|
||||||
|
?? throw new InvalidOperationException("Receiver email is missing for EnvelopeDeletedNotification.");
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Entities;
|
||||||
|
using EnvelopeGenerator.Application.Common.Configurations;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted.Handlers;
|
||||||
|
|
||||||
|
public class SendDeletedMailHandler : SendMailHandler<EnvelopeDeletedNotification>
|
||||||
|
{
|
||||||
|
public SendDeletedMailHandler(
|
||||||
|
IRepository<EmailTemplate> tempRepo,
|
||||||
|
IRepository<EmailOut> emailOutRepo,
|
||||||
|
IOptions<MailParams> mailParamsOptions,
|
||||||
|
IOptions<DispatcherParams> dispatcherParamsOptions)
|
||||||
|
: base(tempRepo, emailOutRepo, mailParamsOptions, dispatcherParamsOptions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ConfigureEmailOut(EnvelopeDeletedNotification notification, EmailOut emailOut)
|
||||||
|
{
|
||||||
|
emailOut.ReferenceString = notification.EmailAddress;
|
||||||
|
emailOut.ReferenceId = notification.EnvelopeReceiver.ReceiverId;
|
||||||
|
emailOut.WfId = (int)EnvelopeStatus.MessageDeletionSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Dictionary<string, string> CreatePlaceHolders(EnvelopeDeletedNotification notification)
|
||||||
|
{
|
||||||
|
var senderName = notification.Envelope.User?.Email;
|
||||||
|
if (string.IsNullOrWhiteSpace(senderName))
|
||||||
|
senderName = notification.SenderReference;
|
||||||
|
|
||||||
|
return new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["[NAME_RECEIVER]"] = notification.EnvelopeReceiver.Name ?? string.Empty,
|
||||||
|
["[NAME_SENDER]"] = senderName ?? string.Empty,
|
||||||
|
["[DOCUMENT_TITLE]"] = notification.Envelope.Title ?? string.Empty,
|
||||||
|
["[REASON]"] = notification.Reason
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Query;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Query;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
/// Stellt eine Abfrage dar, um die Details eines Empfängers zu lesen.
|
||||||
@@ -29,5 +31,6 @@ public record ReceiverQueryBase
|
|||||||
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
|
/// <see cref="Id"/>, <see cref="EmailAddress"/>, or <see cref="Signature"/> is not null.
|
||||||
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
/// <para>Usage example: The query can be executed only if at least one criterion is specified.</para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
[NotMapped]
|
||||||
|
public virtual bool HasAnyCriteria => Id is not null || EmailAddress is not null || Signature is not null;
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ public class EnvelopeReceiverAddReadSQL : ISQL<Envelope>
|
|||||||
/// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE,
|
/// ENV_UID, EMAIL_ADRESS, SALUTATION, PHONE,
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Raw => @"
|
public string Raw => @"
|
||||||
DECLARE @OUT_RECEIVER_ID int
|
DECLARE @OUT_RECEIVER_ID bigint
|
||||||
|
|
||||||
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
|
EXEC [dbo].[PRSIG_API_CREATE_RECEIVER]
|
||||||
{0},
|
{0},
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ namespace EnvelopeGenerator.Application.Documents.Queries;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="Id">The unique identifier of the document. Optional.</param>
|
/// <param name="Id">The unique identifier of the document. Optional.</param>
|
||||||
/// <param name="EnvelopeId">The identifier of the envelope associated with the document. Optional.</param>
|
/// <param name="EnvelopeId">The identifier of the envelope associated with the document. Optional.</param>
|
||||||
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null) : IRequest<DocumentDto>
|
/// <param name="UserId">The unique identifier of the sending user. Optional.</param>
|
||||||
|
public record ReadDocumentQuery(int? Id = null, int? EnvelopeId = null, int? UserId = null) : IRequest<DocumentDto>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +54,10 @@ public class ReadDocumentQueryHandler : IRequestHandler<ReadDocumentQuery, Docum
|
|||||||
/// </exception>
|
/// </exception>
|
||||||
public async Task<DocumentDto> Handle(ReadDocumentQuery query, CancellationToken cancel)
|
public async Task<DocumentDto> Handle(ReadDocumentQuery query, CancellationToken cancel)
|
||||||
{
|
{
|
||||||
var docQuery = _repo.Query.Include(doc => doc.Elements).ThenInclude(e => e.Annotations);
|
var docQuery = _repo.Query.Include(doc => doc.Elements).ThenInclude(e => e.Annotations).AsNoTracking();
|
||||||
|
|
||||||
|
if (query.UserId is int userId)
|
||||||
|
docQuery = docQuery.Where(doc => doc.Envelope.UserId == userId);
|
||||||
|
|
||||||
if (query.Id is not null)
|
if (query.Id is not null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
|
public record DeleteEnvelopeResultDto(int EnvelopeId, EnvelopeStatus AppliedStatus, bool IsNoOp);
|
||||||
|
|
||||||
|
public record DeleteEnvelopeCommand : IRequest<DeleteEnvelopeResultDto>
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public int EnvelopeId { get; init; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MinLength(1)]
|
||||||
|
public string Reason { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
internal int UserId { get; private set; }
|
||||||
|
|
||||||
|
public DeleteEnvelopeCommand WithAuth(int userId)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using EnvelopeGenerator.Application.Common.Notifications.EnvelopeDeleted;
|
||||||
|
using DigitalData.Core.Abstraction.Application.Repository;
|
||||||
|
using DigitalData.Core.Exceptions;
|
||||||
|
using EnvelopeGenerator.Application.Histories.Commands;
|
||||||
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Envelopes.Commands;
|
||||||
|
|
||||||
|
public class DeleteEnvelopeCommandHandler : IRequestHandler<DeleteEnvelopeCommand, DeleteEnvelopeResultDto>
|
||||||
|
{
|
||||||
|
private readonly IRepository<Envelope> _envelopeRepository;
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public DeleteEnvelopeCommandHandler(
|
||||||
|
IRepository<Envelope> envelopeRepository,
|
||||||
|
IMediator mediator)
|
||||||
|
{
|
||||||
|
_envelopeRepository = envelopeRepository;
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DeleteEnvelopeResultDto> Handle(DeleteEnvelopeCommand request, CancellationToken cancel)
|
||||||
|
{
|
||||||
|
var reason = request.Reason?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(reason))
|
||||||
|
throw new BadRequestException("Please provide a reason.");
|
||||||
|
|
||||||
|
var envelope = await _envelopeRepository.Query
|
||||||
|
.Include(e => e.User)
|
||||||
|
.Include(e => e.EnvelopeReceivers)
|
||||||
|
.ThenInclude(er => er.Receiver)
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == request.EnvelopeId, cancel)
|
||||||
|
?? throw new NotFoundException($"Envelope {request.EnvelopeId} not found.");
|
||||||
|
|
||||||
|
if (envelope.UserId != request.UserId)
|
||||||
|
throw new ForbiddenException();
|
||||||
|
|
||||||
|
if (envelope.Status is EnvelopeStatus.EnvelopeDeleted or EnvelopeStatus.EnvelopeWithdrawn)
|
||||||
|
return new DeleteEnvelopeResultDto(envelope.Id, envelope.Status, IsNoOp: true);
|
||||||
|
|
||||||
|
var targetStatus = envelope.Status > EnvelopeStatus.EnvelopeSaved
|
||||||
|
? EnvelopeStatus.EnvelopeWithdrawn
|
||||||
|
: EnvelopeStatus.EnvelopeDeleted;
|
||||||
|
|
||||||
|
envelope.Status = targetStatus;
|
||||||
|
envelope.Comment = reason;
|
||||||
|
envelope.ChangedWhen = DateTime.Now;
|
||||||
|
|
||||||
|
await _envelopeRepository.UpdateAsync(envelope, e => e.Id == envelope.Id, cancel);
|
||||||
|
|
||||||
|
var userReference = envelope.User?.Email;
|
||||||
|
if (string.IsNullOrWhiteSpace(userReference))
|
||||||
|
throw new BadRequestException("Envelope sender email is missing.");
|
||||||
|
|
||||||
|
await _mediator.Send(new CreateHistoryCommand
|
||||||
|
{
|
||||||
|
EnvelopeId = envelope.Id,
|
||||||
|
UserReference = userReference,
|
||||||
|
Status = targetStatus,
|
||||||
|
Comment = reason
|
||||||
|
}, cancel);
|
||||||
|
|
||||||
|
foreach (var envelopeReceiver in envelope.EnvelopeReceivers ?? Enumerable.Empty<EnvelopeReceiver>())
|
||||||
|
{
|
||||||
|
if (envelopeReceiver.Receiver is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
await _mediator.Publish(new EnvelopeDeletedNotification
|
||||||
|
{
|
||||||
|
Envelope = envelope,
|
||||||
|
EnvelopeReceiver = envelopeReceiver,
|
||||||
|
Reason = reason,
|
||||||
|
SenderReference = userReference
|
||||||
|
}, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DeleteEnvelopeResultDto(envelope.Id, targetStatus, IsNoOp: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,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 =>
|
||||||
|
|||||||
@@ -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,12 +0,0 @@
|
|||||||
@using System.Globalization
|
|
||||||
|
|
||||||
<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,56 +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.8" />
|
|
||||||
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.8" />
|
|
||||||
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.8" />
|
|
||||||
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.8" />
|
|
||||||
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.28" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.11" />
|
|
||||||
<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>
|
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
|
||||||
</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,39 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
|
||||||
|
|
||||||
public class EnvelopeDto
|
|
||||||
{
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("uuid")]
|
|
||||||
public string? Uuid { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("title")]
|
|
||||||
public string? Title { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("status")]
|
|
||||||
public int Status { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("docResult")]
|
|
||||||
public byte[]? DocResult { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("envelopeReceivers")]
|
|
||||||
public List<EnvelopeReceiverSimpleDto> EnvelopeReceivers { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Simplified receiver model for envelope list display
|
|
||||||
/// </summary>
|
|
||||||
public class EnvelopeReceiverSimpleDto
|
|
||||||
{
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string? Name { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("email")]
|
|
||||||
public string? Email { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("signed")]
|
|
||||||
public bool Signed { get; set; }
|
|
||||||
}
|
|
||||||
@@ -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,442 +0,0 @@
|
|||||||
@page "/sender"
|
|
||||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize(Policy = "Sender")]
|
|
||||||
|
|
||||||
@using System.Text.Json
|
|
||||||
@using EnvelopeGenerator.Domain.Constants
|
|
||||||
@using EnvelopeGenerator.ReceiverUI.Models
|
|
||||||
@using DevExpress.Blazor
|
|
||||||
@using EnvelopeGenerator.ReceiverUI.Services
|
|
||||||
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeService EnvelopeService
|
|
||||||
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
@inject IJSRuntime JSRuntime
|
|
||||||
@inject AppVersionService AppVersion
|
|
||||||
|
|
||||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
|
||||||
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
|
||||||
<link href="@AppVersion.GetVersionedUrl("css/sender-page.css")" rel="stylesheet" />
|
|
||||||
|
|
||||||
<div class="sender-dashboard-layout">
|
|
||||||
<div class="sender-action-bar">
|
|
||||||
<div class="sender-action-bar__inner">
|
|
||||||
<div class="sender-title-section">
|
|
||||||
<div class="sender-logo">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" 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="sender-title">Umschlag-Übersicht</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-toolbar">
|
|
||||||
<button class="sender-btn sender-btn--primary" @onclick="CreateEnvelope" title="Neuen Umschlag erstellen">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
||||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
|
||||||
</svg>
|
|
||||||
Neuer Umschlag
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="sender-btn" @onclick="EditEnvelope" disabled="@(_selectedEnvelope == null || IsEnvelopeSent(_selectedEnvelope))" title="Ausgewählten Umschlag bearbeiten">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" 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>
|
|
||||||
Bearbeiten
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="sender-btn sender-btn--danger" @onclick="DeleteEnvelope" disabled="@(_selectedEnvelope == null)" title="Ausgewählten Umschlag löschen">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
||||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
|
||||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
|
||||||
</svg>
|
|
||||||
Löschen
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="sender-btn" @onclick="RefreshEnvelopes" disabled="@_isLoading" title="Aktualisieren">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
||||||
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
|
|
||||||
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
|
|
||||||
</svg>
|
|
||||||
@if (_isLoading) {
|
|
||||||
<span class="spinner-border spinner-border-sm" style="width: 14px; height: 14px;"></span>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="sender-btn sender-btn--logout" @onclick="LogoutAsync" disabled="@_isLoggingOut" title="Abmelden">
|
|
||||||
@if (_isLoggingOut) {
|
|
||||||
<span class="spinner-border spinner-border-sm" style="width: 14px; height: 14px;"></span>
|
|
||||||
} else {
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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 class="sender-content">
|
|
||||||
@if (_isLoading && _allEnvelopes == null) {
|
|
||||||
<div class="d-flex justify-content-center align-items-center h-100">
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="spinner-border text-white mb-3" style="width: 3.5rem; height: 3.5rem;" role="status">
|
|
||||||
<span class="visually-hidden">Lädt...</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-white fw-semibold">Umschläge werden geladen...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
} else if (_errorMessage != null) {
|
|
||||||
<div class="error-container">
|
|
||||||
<div class="alert alert-danger shadow-lg">
|
|
||||||
<div class="d-flex align-items-start">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="me-3 flex-shrink-0" viewBox="0 0 16 16">
|
|
||||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
|
||||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<h5 class="mb-2">Fehler beim Laden der Umschläge</h5>
|
|
||||||
<p class="mb-0">@_errorMessage</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<div class="sender-grid-container">
|
|
||||||
<div class="sender-tabs">
|
|
||||||
<button class="sender-tab @(_activeTab == "active" ? "sender-tab--active" : "")" @onclick='() => _activeTab = "active"'>
|
|
||||||
<span>Aktive Umschläge</span>
|
|
||||||
@if (_activeEnvelopes != null) {
|
|
||||||
<span style="opacity: 0.6; margin-left: 0.5rem;">(@_activeEnvelopes.Count())</span>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
<button class="sender-tab @(_activeTab == "completed" ? "sender-tab--active" : "")" @onclick='() => _activeTab = "completed"'>
|
|
||||||
<span>Abgeschlossene Umschläge</span>
|
|
||||||
@if (_completedEnvelopes != null) {
|
|
||||||
<span style="opacity: 0.6; margin-left: 0.5rem;">(@_completedEnvelopes.Count())</span>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sender-grid-wrapper">
|
|
||||||
@if (_activeTab == "active") {
|
|
||||||
<DxGrid Data="@_activeEnvelopes"
|
|
||||||
@ref="_gridActive"
|
|
||||||
ShowFilterRow="true"
|
|
||||||
ShowSearchBox="true"
|
|
||||||
AllowColumnReorder="true"
|
|
||||||
AllowSort=true
|
|
||||||
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
|
||||||
PageSize="20"
|
|
||||||
PagerVisible="true"
|
|
||||||
SelectionMode="GridSelectionMode.Single"
|
|
||||||
SelectedDataItem="@_selectedEnvelope"
|
|
||||||
SelectedDataItemChanged="@OnSelectedEnvelopeChanged"
|
|
||||||
CustomizeElement="OnCustomizeElement">
|
|
||||||
<Columns>
|
|
||||||
<DxGridDataColumn FieldName="Id" Caption="ID">
|
|
||||||
<CellDisplayTemplate Context="cellContext">
|
|
||||||
@((cellContext.DataItem as EnvelopeDto)?.Id)
|
|
||||||
</CellDisplayTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="Title" Caption="Titel">
|
|
||||||
<CellDisplayTemplate Context="cellContext">
|
|
||||||
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
|
|
||||||
</CellDisplayTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="Status" Caption="Status">
|
|
||||||
<CellDisplayTemplate Context="cellContext">
|
|
||||||
@{
|
|
||||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
|
||||||
if (envelope != null) {
|
|
||||||
var statusInfo = GetStatusInfo(envelope.Status);
|
|
||||||
<div class="status-badge status-badge--@statusInfo.CssClass">
|
|
||||||
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
|
|
||||||
@statusInfo.Label
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</CellDisplayTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="Empfänger">
|
|
||||||
<CellDisplayTemplate Context="cellContext">
|
|
||||||
@{
|
|
||||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
|
||||||
if (envelope != null) {
|
|
||||||
var receivers = envelope.EnvelopeReceivers ?? new List<EnvelopeReceiverSimpleDto>();
|
|
||||||
var signed = receivers.Count(r => r.Signed);
|
|
||||||
var total = receivers.Count;
|
|
||||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
||||||
<span style="font-size: 0.875rem; color: #6b7280;">
|
|
||||||
@signed / @total unterschrieben
|
|
||||||
</span>
|
|
||||||
@if (total > 0) {
|
|
||||||
<div style="flex: 1; min-width: 60px; max-width: 120px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden;">
|
|
||||||
<div style="height: 100%; background: linear-gradient(90deg, #81c784 0%, #66bb6a 100%); width: @((signed * 100.0 / total).ToString("F0"))%;"></div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</CellDisplayTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
</Columns>
|
|
||||||
<DetailRowTemplate Context="detailContext">
|
|
||||||
<div style="padding: 1rem; background: #f9fafb;">
|
|
||||||
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">Empfänger</h6>
|
|
||||||
@{
|
|
||||||
var envelope = detailContext.DataItem as EnvelopeDto;
|
|
||||||
if (envelope?.EnvelopeReceivers?.Any() == true) {
|
|
||||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
|
||||||
@foreach (var receiver in envelope.EnvelopeReceivers) {
|
|
||||||
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
|
|
||||||
<span class="receiver-badge receiver-badge--@(receiver.Signed ? "signed" : "unsigned")" style="min-width: 100px;">
|
|
||||||
@if (receiver.Signed) {
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" 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>Unterschrieben</span>
|
|
||||||
} else {
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
|
|
||||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
|
||||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
|
||||||
</svg>
|
|
||||||
<span>Ausstehend</span>
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
<div style="flex: 1; font-size: 0.875rem;">
|
|
||||||
<strong style="color: #1f2937;">@receiver.Name</strong>
|
|
||||||
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Email</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">Keine Empfänger</p>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</DetailRowTemplate>
|
|
||||||
</DxGrid>
|
|
||||||
} else {
|
|
||||||
<DxGrid Data="@_completedEnvelopes"
|
|
||||||
@ref="_gridCompleted"
|
|
||||||
ShowFilterRow="true"
|
|
||||||
ShowSearchBox="true"
|
|
||||||
PageSize="20"
|
|
||||||
PagerVisible="true"
|
|
||||||
SelectionMode="GridSelectionMode.Single"
|
|
||||||
SelectedDataItem="@_selectedEnvelope"
|
|
||||||
SelectedDataItemChanged="@OnSelectedEnvelopeChanged"
|
|
||||||
CustomizeElement="OnCustomizeElement">
|
|
||||||
<Columns>
|
|
||||||
<DxGridDataColumn FieldName="Id" Caption="ID">
|
|
||||||
<CellDisplayTemplate Context="cellContext">
|
|
||||||
@((cellContext.DataItem as EnvelopeDto)?.Id)
|
|
||||||
</CellDisplayTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="Title" Caption="Titel">
|
|
||||||
<CellDisplayTemplate Context="cellContext">
|
|
||||||
<strong>@((cellContext.DataItem as EnvelopeDto)?.Title)</strong>
|
|
||||||
</CellDisplayTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="Status" Caption="Status">
|
|
||||||
<CellDisplayTemplate Context="cellContext">
|
|
||||||
@{
|
|
||||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
|
||||||
if (envelope != null) {
|
|
||||||
var statusInfo = GetStatusInfo(envelope.Status);
|
|
||||||
<div class="status-badge status-badge--@statusInfo.CssClass">
|
|
||||||
<span class="status-dot status-dot--@statusInfo.DotColor"></span>
|
|
||||||
@statusInfo.Label
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</CellDisplayTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
<DxGridDataColumn FieldName="EnvelopeReceivers" Caption="Empfänger">
|
|
||||||
<CellDisplayTemplate Context="cellContext">
|
|
||||||
@{
|
|
||||||
var envelope = cellContext.DataItem as EnvelopeDto;
|
|
||||||
if (envelope != null) {
|
|
||||||
var receivers = envelope.EnvelopeReceivers ?? new List<EnvelopeReceiverSimpleDto>();
|
|
||||||
var signed = receivers.Count(r => r.Signed);
|
|
||||||
var total = receivers.Count;
|
|
||||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
||||||
<span style="font-size: 0.875rem; color: #6b7280;">
|
|
||||||
@signed / @total unterschrieben
|
|
||||||
</span>
|
|
||||||
@if (total > 0) {
|
|
||||||
<div style="flex: 1; min-width: 60px; max-width: 120px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden;">
|
|
||||||
<div style="height: 100%; background: linear-gradient(90deg, #81c784 0%, #66bb6a 100%); width: @((signed * 100.0 / total).ToString("F0"))%;"></div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</CellDisplayTemplate>
|
|
||||||
</DxGridDataColumn>
|
|
||||||
</Columns>
|
|
||||||
<DetailRowTemplate Context="detailContext">
|
|
||||||
<div style="padding: 1rem; background: #f9fafb;">
|
|
||||||
<h6 style="font-weight: 600; color: #374151; margin-bottom: 0.75rem;">Empfänger</h6>
|
|
||||||
@{
|
|
||||||
var envelope = detailContext.DataItem as EnvelopeDto;
|
|
||||||
if (envelope?.EnvelopeReceivers?.Any() == true) {
|
|
||||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
|
||||||
@foreach (var receiver in envelope.EnvelopeReceivers) {
|
|
||||||
<div style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; background: white; border-radius: 6px; border: 1px solid #e5e7eb;">
|
|
||||||
<span class="receiver-badge receiver-badge--@(receiver.Signed ? "signed" : "unsigned")" style="min-width: 100px;">
|
|
||||||
@if (receiver.Signed) {
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" 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>Unterschrieben</span>
|
|
||||||
} else {
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" viewBox="0 0 16 16">
|
|
||||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
|
||||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
|
||||||
</svg>
|
|
||||||
<span>Ausstehend</span>
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
<div style="flex: 1; font-size: 0.875rem;">
|
|
||||||
<strong style="color: #1f2937;">@receiver.Name</strong>
|
|
||||||
<span style="color: #6b7280; margin-left: 0.5rem;">@receiver.Email</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<p style="color: #9ca3af; font-size: 0.875rem; margin: 0;">Keine Empfänger</p>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</DetailRowTemplate>
|
|
||||||
</DxGrid>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private IEnumerable<EnvelopeDto>? _allEnvelopes;
|
|
||||||
private IEnumerable<EnvelopeDto>? _activeEnvelopes;
|
|
||||||
private IEnumerable<EnvelopeDto>? _completedEnvelopes;
|
|
||||||
private EnvelopeDto? _selectedEnvelope;
|
|
||||||
private string _activeTab = "active";
|
|
||||||
private bool _isLoading = true;
|
|
||||||
private bool _isLoggingOut = false;
|
|
||||||
private string? _errorMessage;
|
|
||||||
private DxGrid? _gridActive;
|
|
||||||
private DxGrid? _gridCompleted;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
var hasAccess = await AuthService.CheckSenderAsync();
|
|
||||||
if (!hasAccess)
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo($"/sender/login");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await LoadEnvelopesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task LoadEnvelopesAsync()
|
|
||||||
{
|
|
||||||
_isLoading = true;
|
|
||||||
_errorMessage = null;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_allEnvelopes = await EnvelopeService.GetAsync() ?? [];
|
|
||||||
|
|
||||||
// Split into active and completed based on status
|
|
||||||
var envelopes = _allEnvelopes.ToList();
|
|
||||||
_activeEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsActive()).ToList();
|
|
||||||
_completedEnvelopes = envelopes.Where(e => ((EnvelopeStatus)e.Status).IsCompleted()).ToList();
|
|
||||||
|
|
||||||
await JSRuntime.InvokeVoidAsync("console.log", $"Loaded {_activeEnvelopes.Count()} active and {_completedEnvelopes.Count()} completed envelopes");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_errorMessage = ex.Message;
|
|
||||||
await JSRuntime.InvokeVoidAsync("console.error", "Fehler beim Laden der Umschläge:", ex.ToString());
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_isLoading = false;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task RefreshEnvelopes()
|
|
||||||
{
|
|
||||||
await LoadEnvelopesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
void CreateEnvelope()
|
|
||||||
{
|
|
||||||
// TODO: Navigate to envelope creation page
|
|
||||||
JSRuntime.InvokeVoidAsync("console.log", "Create envelope clicked - not yet implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
void EditEnvelope()
|
|
||||||
{
|
|
||||||
if (_selectedEnvelope == null) return;
|
|
||||||
// TODO: Navigate to envelope editor
|
|
||||||
JSRuntime.InvokeVoidAsync("console.log", $"Edit envelope {_selectedEnvelope.Id} clicked - not yet implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
void DeleteEnvelope()
|
|
||||||
{
|
|
||||||
if (_selectedEnvelope == null) return;
|
|
||||||
// TODO: Show delete confirmation dialog
|
|
||||||
JSRuntime.InvokeVoidAsync("console.log", $"Delete envelope {_selectedEnvelope.Id} clicked - not yet implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task LogoutAsync()
|
|
||||||
{
|
|
||||||
_isLoggingOut = true;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
await AuthService.LogoutSenderAsync();
|
|
||||||
Navigation.NavigateTo("/sender/login", forceLoad: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsEnvelopeSent(EnvelopeDto envelope)
|
|
||||||
{
|
|
||||||
var status = (EnvelopeStatus)envelope.Status;
|
|
||||||
return status >= EnvelopeStatus.EnvelopeQueued;
|
|
||||||
}
|
|
||||||
|
|
||||||
(string Label, string CssClass, string DotColor) GetStatusInfo(int statusCode)
|
|
||||||
{
|
|
||||||
var status = (EnvelopeStatus)statusCode;
|
|
||||||
return status switch
|
|
||||||
{
|
|
||||||
EnvelopeStatus.EnvelopePartlySigned => ("Teilweise unterschrieben", "partly-signed", "green"),
|
|
||||||
EnvelopeStatus.EnvelopeQueued => ("In Warteschlange", "queued", "orange"),
|
|
||||||
EnvelopeStatus.EnvelopeSent => ("Gesendet", "sent", "orange"),
|
|
||||||
EnvelopeStatus.EnvelopeCompletelySigned => ("Vollständig unterschrieben", "completed", "green"),
|
|
||||||
EnvelopeStatus.EnvelopeDeleted => ("Gelöscht", "deleted", "red"),
|
|
||||||
EnvelopeStatus.EnvelopeRejected => ("Abgelehnt", "rejected", "red"),
|
|
||||||
EnvelopeStatus.EnvelopeWithdrawn => ("Zurückgezogen", "withdrawn", "red"),
|
|
||||||
EnvelopeStatus.EnvelopeCreated => ("Erstellt", "created", "blue"),
|
|
||||||
EnvelopeStatus.EnvelopeSaved => ("Gespeichert", "saved", "blue"),
|
|
||||||
_ => ("Unbekannt", "unknown", "blue")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnCustomizeElement(GridCustomizeElementEventArgs e)
|
|
||||||
{
|
|
||||||
// Future: Add custom row coloring based on status if needed
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnSelectedEnvelopeChanged(object envelope)
|
|
||||||
{
|
|
||||||
_selectedEnvelope = envelope as EnvelopeDto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,750 +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 EnvelopeGenerator.Application.Common.Dto
|
|
||||||
@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 ?? throw new InvalidOperationException($"Annotation page is missing for annotation ID {a.Id}. Annotation details: X={a.X}, Y={a.Y}"))
|
|
||||||
.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,78 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
|
||||||
using EnvelopeGenerator.ReceiverUI;
|
|
||||||
using DevExpress.DataAccess.Web;
|
|
||||||
using EnvelopeGenerator.ReceiverUI.Services;
|
|
||||||
using EnvelopeGenerator.ReceiverUI.Options;
|
|
||||||
using DevExpress.XtraReports.Services;
|
|
||||||
using DevExpress.Blazor.Reporting;
|
|
||||||
using DevExpress.XtraReports.Web.Extensions;
|
|
||||||
using EnvelopeGenerator.Application.Resources;
|
|
||||||
using Microsoft.Extensions.Localization;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
|
||||||
builder.RootComponents.Add<App>("#app");
|
|
||||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
|
||||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
|
||||||
builder.Services.Configure<ApiOptions>(opts =>
|
|
||||||
builder.Configuration.GetSection(ApiOptions.SectionName).Bind(opts));
|
|
||||||
builder.Services.Configure<PdfViewerOptions>(opts =>
|
|
||||||
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
|
|
||||||
builder.Services.AddScoped<DocumentService>();
|
|
||||||
builder.Services.AddScoped<AuthService>();
|
|
||||||
builder.Services.AddScoped<AnnotationService>();
|
|
||||||
builder.Services.AddScoped<EnvelopeReceiverService>();
|
|
||||||
builder.Services.AddScoped<DocReceiverElementService>();
|
|
||||||
builder.Services.AddScoped<SignatureCacheService>();
|
|
||||||
builder.Services.AddSingleton<AppVersionService>();
|
|
||||||
builder.Services.AddScoped<EnvelopeService>();
|
|
||||||
builder.Services.AddScoped<CultureService>();
|
|
||||||
|
|
||||||
// Localization services
|
|
||||||
builder.Services.AddLocalization();
|
|
||||||
|
|
||||||
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
|
|
||||||
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
|
|
||||||
|
|
||||||
builder.Services.AddDevExpressBlazorReportingWebAssembly(configure => {
|
|
||||||
configure.UseDevelopmentMode();
|
|
||||||
});
|
|
||||||
builder.Services.AddScoped<IDataSourceWizardJsonConnectionStorage, CustomDataSourceWizardJsonDataConnectionStorage>();
|
|
||||||
builder.Services.AddScoped<IJsonDataConnectionProviderFactory, CustomJsonDataConnectionProviderFactory>();
|
|
||||||
builder.Services.AddScoped<IObjectDataSourceWizardTypeProvider, ObjectDataSourceWizardCustomTypeProvider>();
|
|
||||||
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.ReceiverUI.Data.DataItemList));
|
|
||||||
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.ReceiverUI.PredefinedReports.Report));
|
|
||||||
builder.Services.AddSingleton<InMemoryReportStorageWebExtension>();
|
|
||||||
builder.Services.AddSingleton<ReportStorageWebExtension>(sp => sp.GetRequiredService<InMemoryReportStorageWebExtension>());
|
|
||||||
builder.Services.AddScoped<IReportProviderAsync, CustomReportProvider>();
|
|
||||||
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
|
|
||||||
|
|
||||||
var host = builder.Build();
|
|
||||||
|
|
||||||
// ⚠️ IMPORTANT: BLAZOR WASM-SPECIFIC CULTURE INITIALIZATION
|
|
||||||
// This approach sets DefaultThreadCurrentCulture globally, which is SAFE for WebAssembly
|
|
||||||
// because each user runs their own isolated app instance in their browser.
|
|
||||||
//
|
|
||||||
// ⚠️ TODO: REMOVE/REFACTOR WHEN MIGRATING TO BLAZOR SERVER/AUTO
|
|
||||||
// In Server/Auto render modes, this is DANGEROUS because:
|
|
||||||
// - Server runs a single shared instance for all users
|
|
||||||
// - Setting global culture affects ALL connected users simultaneously
|
|
||||||
// - Race conditions and culture conflicts will occur
|
|
||||||
//
|
|
||||||
// Migration Guide:
|
|
||||||
// - Option 1: Use RequestLocalizationMiddleware for per-request culture
|
|
||||||
// - Option 2: Use CascadingParameter with per-circuit culture state
|
|
||||||
// - See: https://learn.microsoft.com/aspnet/core/blazor/globalization-localization
|
|
||||||
//
|
|
||||||
// Related files to update on migration:
|
|
||||||
// - LanguageSelector.razor (remove manual culture setting)
|
|
||||||
// - App.razor (may need CascadingValue for culture)
|
|
||||||
// - Startup/Program.cs (add middleware)
|
|
||||||
var cultureService = host.Services.GetRequiredService<CultureService>();
|
|
||||||
var culture = await cultureService.InitializeCultureAsync();
|
|
||||||
CultureInfo.DefaultThreadCurrentCulture = culture;
|
|
||||||
CultureInfo.DefaultThreadCurrentUICulture = culture;
|
|
||||||
|
|
||||||
await FontLoader.LoadFonts(host.Services.GetRequiredService<HttpClient>(), new List<string> { "opensans.ttf" });
|
|
||||||
await host.RunAsync();
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"profiles": {
|
|
||||||
"EnvelopeGenerator.ReceiverUI": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"launchBrowser": false,
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
},
|
|
||||||
"applicationUrl": "https://localhost:52936;http://localhost:52937"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using System.Net.Http.Json;
|
|
||||||
using System.Text.Json;
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
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,80 +0,0 @@
|
|||||||
@using System.Globalization
|
|
||||||
@using EnvelopeGenerator.ReceiverUI.Services
|
|
||||||
@inject IJSRuntime JSRuntime
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
@inject CultureService CultureService
|
|
||||||
|
|
||||||
<div class="language-selector">
|
|
||||||
<button class="language-selector__trigger" @onclick="ToggleDropdown" aria-label="Select Language">
|
|
||||||
<span class="fi fi-@GetFlagCode(CurrentCulture)"></span>
|
|
||||||
<span class="language-selector__arrow">@GetLanguageName(CurrentCulture)</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@if (isOpen)
|
|
||||||
{
|
|
||||||
<div class="language-selector__dropdown">
|
|
||||||
<button class="language-selector__option" @onclick="@(() => ChangeLanguageAsync("de-DE"))">
|
|
||||||
<span class="fi fi-de"></span>
|
|
||||||
<span>Deutsch</span>
|
|
||||||
</button>
|
|
||||||
<button class="language-selector__option" @onclick="@(() => ChangeLanguageAsync("en-US"))">
|
|
||||||
<span class="fi fi-us"></span>
|
|
||||||
<span>English</span>
|
|
||||||
</button>
|
|
||||||
<button class="language-selector__option" @onclick="@(() => ChangeLanguageAsync("fr-FR"))">
|
|
||||||
<span class="fi fi-fr"></span>
|
|
||||||
<span>Français</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private bool isOpen = false;
|
|
||||||
private string CurrentCulture => CultureInfo.CurrentCulture.Name;
|
|
||||||
|
|
||||||
private void ToggleDropdown()
|
|
||||||
{
|
|
||||||
isOpen = !isOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ChangeLanguageAsync(string culture)
|
|
||||||
{
|
|
||||||
if (CultureInfo.CurrentCulture.Name != culture)
|
|
||||||
{
|
|
||||||
await CultureService.SetCultureAsync(culture);
|
|
||||||
|
|
||||||
// Set culture without page reload
|
|
||||||
var cultureInfo = new CultureInfo(culture);
|
|
||||||
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
|
|
||||||
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
|
|
||||||
|
|
||||||
// Navigate without reload to trigger re-render
|
|
||||||
Navigation.NavigateTo(Navigation.Uri, forceLoad: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
isOpen = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetFlagCode(string culture)
|
|
||||||
{
|
|
||||||
return culture switch
|
|
||||||
{
|
|
||||||
"de-DE" => "de",
|
|
||||||
"en-US" => "us",
|
|
||||||
"fr-FR" => "fr",
|
|
||||||
_ => "de"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetLanguageName(string culture)
|
|
||||||
{
|
|
||||||
return culture switch
|
|
||||||
{
|
|
||||||
"de-DE" => "Deutsch",
|
|
||||||
"en-US" => "English",
|
|
||||||
"fr-FR" => "Français",
|
|
||||||
_ => "Deutsch"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
@using EnvelopeGenerator.ReceiverUI.Services;
|
|
||||||
@inherits LayoutComponentBase
|
|
||||||
|
|
||||||
<div class="page">
|
|
||||||
<main>
|
|
||||||
<article class="content">
|
|
||||||
@Body
|
|
||||||
</article>
|
|
||||||
</main>
|
|
||||||
<footer class="receiver-footer">
|
|
||||||
<div class="receiver-footer__content">
|
|
||||||
<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>
|
|
||||||
<LanguageSelector />
|
|
||||||
</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,32 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.receiver-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.receiver-footer__content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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,297 +0,0 @@
|
|||||||
.sender-dashboard-layout {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #7e22ce 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-action-bar {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
border-bottom: 3px solid rgba(126, 34, 206, 0.3);
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-action-bar__inner {
|
|
||||||
max-width: 1600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-title-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-logo svg {
|
|
||||||
filter: drop-shadow(0 2px 4px rgba(126, 34, 206, 0.3));
|
|
||||||
color: #7e22ce;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-title {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1e293b;
|
|
||||||
letter-spacing: -0.025em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.625rem 1.125rem;
|
|
||||||
background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%);
|
|
||||||
border: 1px solid rgba(126, 34, 206, 0.2);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1e293b;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-btn:hover:not(:disabled) {
|
|
||||||
background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%);
|
|
||||||
border-color: rgba(126, 34, 206, 0.4);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(126, 34, 206, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-btn:disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
background: rgba(0, 0, 0, 0.02);
|
|
||||||
border-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-btn--primary {
|
|
||||||
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
|
|
||||||
border-color: transparent;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-btn--primary:hover:not(:disabled) {
|
|
||||||
background: linear-gradient(135deg, #6b1cb0 0%, #1e3a72 100%);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 16px rgba(126, 34, 206, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-btn--danger {
|
|
||||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(220, 38, 38, 0.08) 100%);
|
|
||||||
border-color: rgba(239, 68, 68, 0.3);
|
|
||||||
color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-btn--danger:hover:not(:disabled) {
|
|
||||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
||||||
border-color: transparent;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-btn--logout {
|
|
||||||
padding: 0.5rem;
|
|
||||||
min-width: 38px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-content {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
padding: 1.5rem;
|
|
||||||
position: relative;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-grid-container {
|
|
||||||
background: rgba(255, 255, 255, 0.98);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
max-width: 1600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-grid-container::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 4px;
|
|
||||||
background: linear-gradient(90deg, #7e22ce 0%, #2a5298 100%);
|
|
||||||
z-index: 1;
|
|
||||||
border-radius: 16px 16px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-tabs {
|
|
||||||
display: flex;
|
|
||||||
border-bottom: 2px solid rgba(126, 34, 206, 0.1);
|
|
||||||
padding: 0 2rem;
|
|
||||||
background: rgba(126, 34, 206, 0.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-tab {
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #6b7280;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 3px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-tab:hover {
|
|
||||||
color: #7e22ce;
|
|
||||||
background: rgba(126, 34, 206, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-tab--active {
|
|
||||||
color: #7e22ce;
|
|
||||||
border-bottom-color: #7e22ce;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-grid-wrapper {
|
|
||||||
padding: 1.5rem 2rem 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide DevExpress empty cells */
|
|
||||||
.dxbl-grid-empty-cell {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.375rem;
|
|
||||||
padding: 0.25rem 0.625rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge--partly-signed,
|
|
||||||
.status-badge--completed {
|
|
||||||
background: rgba(129, 199, 132, 0.15);
|
|
||||||
color: #2e7d32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge--queued,
|
|
||||||
.status-badge--sent {
|
|
||||||
background: rgba(255, 183, 77, 0.15);
|
|
||||||
color: #e65100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge--deleted,
|
|
||||||
.status-badge--rejected,
|
|
||||||
.status-badge--withdrawn {
|
|
||||||
background: rgba(229, 115, 115, 0.15);
|
|
||||||
color: #c62828;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge--created,
|
|
||||||
.status-badge--saved {
|
|
||||||
background: rgba(100, 181, 246, 0.15);
|
|
||||||
color: #1565c0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot--green {
|
|
||||||
background: #81c784;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot--orange {
|
|
||||||
background: #ffb74d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot--red {
|
|
||||||
background: #e57373;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot--blue {
|
|
||||||
background: #64b5f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.receiver-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
padding: 0.125rem 0.5rem;
|
|
||||||
background: #f3f4f6;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #374151;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.receiver-badge--signed {
|
|
||||||
background: rgba(129, 199, 132, 0.15);
|
|
||||||
color: #2e7d32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.receiver-badge--unsigned {
|
|
||||||
background: rgba(229, 115, 115, 0.15);
|
|
||||||
color: #c62828;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@media (max-width: 768px) {
|
|
||||||
.sender-action-bar {
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-action-bar__inner {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-toolbar {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-title {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-content {
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-grid-wrapper {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-tabs {
|
|
||||||
padding: 0 1rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-tab {
|
|
||||||
padding: 0.875rem 1rem;
|
|
||||||
font-size: 0.813rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +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" />
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css" />
|
|
||||||
<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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace EnvelopeGenerator.Server.Client.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a pre-assigned signature annotation position on a specific page.
|
||||||
|
/// <br/><br/>
|
||||||
|
/// <b>Coordinate unit (X, Y):</b> Inches (GdPicture14 native unit),
|
||||||
|
/// origin at the <b>top-left</b> corner of the page, both axes increase downward/rightward.
|
||||||
|
/// <br/><br/>
|
||||||
|
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
|
||||||
|
/// Convert: <c>xDX = xInches * 100.0</c>
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Conversion to PDF Points:</b> Multiply by 72 (1 inch = 72 points).
|
||||||
|
/// Convert: <c>xPt = xInches * 72.0</c>
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Y-axis for PDF (bottom-left origin):</b> Flip required for iText7.
|
||||||
|
/// Convert: <c>yPt = (pageHeightInches - yInches - elemHeightInches) * 72.0</c>
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Use SignatureDto with SignatureService.")]
|
||||||
|
public record AnnotationDto
|
||||||
|
{
|
||||||
|
/// <summary>Unique identifier of the annotation.</summary>
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>1-based page number within the document.</summary>
|
||||||
|
public int Page { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Horizontal position in INCHES from the left edge of the page.</summary>
|
||||||
|
public double X { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Vertical position in INCHES from the top edge of the page.</summary>
|
||||||
|
public double Y { get; init; }
|
||||||
|
}
|
||||||
@@ -1,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.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using EnvelopeGenerator.ReceiverUI.Models.Constants;
|
using EnvelopeGenerator.Server.Client.Models.Constants;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
namespace EnvelopeGenerator.Server.Client.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a signature position on a PDF page.
|
/// Represents a signature position on a PDF page.
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Options;
|
namespace EnvelopeGenerator.Server.Client.Options;
|
||||||
|
|
||||||
public class ApiOptions
|
public class ApiOptions
|
||||||
{
|
{
|
||||||
public const string SectionName = "Api";
|
public const string SectionName = "Api";
|
||||||
|
|
||||||
public string BaseUrl { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public bool UsePredefinedReports { get; set; } = false;
|
public bool UsePredefinedReports { get; set; } = false;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Options;
|
namespace EnvelopeGenerator.Server.Client.Options;
|
||||||
|
|
||||||
public class PdfViewerOptions
|
public class PdfViewerOptions
|
||||||
{
|
{
|
||||||
@@ -1,15 +1,27 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
|
@inject NavigationManager Navigation
|
||||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
@inject EnvelopeGenerator.Server.Client.Services.CustomImagesService CustomImagesService
|
||||||
|
@rendermode InteractiveWebAssembly
|
||||||
|
|
||||||
<div class="home-page-wrapper">
|
<div class="home-page-wrapper">
|
||||||
|
|
||||||
<div class="home-hero-header">
|
<div class="home-hero-header">
|
||||||
<div class="home-hero-header__inner">
|
<div class="home-hero-header__inner">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="home-hero-header__icon" viewBox="0 0 16 16">
|
@* signFLOW logo from appsettings.json CustomImages.App — fallback to SVG if not loaded *@
|
||||||
<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"/>
|
@if (_appLogo is not null && !string.IsNullOrEmpty(_appLogo.Src))
|
||||||
</svg>
|
{
|
||||||
|
<img src="@_appLogo.Src"
|
||||||
|
style="height: @_appLogo.Height"
|
||||||
|
class="home-hero-header__icon @_appLogo.GetClass("Main")"
|
||||||
|
alt="signFLOW" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="home-hero-header__icon" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
<div>
|
<div>
|
||||||
<h1 class="home-hero-header__title">SignFlow</h1>
|
<h1 class="home-hero-header__title">SignFlow</h1>
|
||||||
<p class="home-hero-header__subtitle">Willkommen im eSign-Portal</p>
|
<p class="home-hero-header__subtitle">Willkommen im eSign-Portal</p>
|
||||||
@@ -25,7 +37,18 @@
|
|||||||
<span id="home-description"></span>
|
<span id="home-description"></span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mt-4 pt-3 border-top">
|
<div class="mt-4 pt-3 border-top d-flex flex-column align-items-center gap-4">
|
||||||
|
|
||||||
|
<button class="home-btn-primary btn px-4 py-2 d-flex align-items-center gap-2" @onclick='() => Navigation.NavigateTo("/sender")'>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" 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>
|
||||||
|
Zur Umschlag-Übersicht
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="d-flex flex-wrap justify-content-center gap-3">
|
<div class="d-flex flex-wrap justify-content-center gap-3">
|
||||||
<div class="home-feature-badge">
|
<div class="home-feature-badge">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
@@ -47,6 +70,7 @@
|
|||||||
PDF-Export
|
PDF-Export
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -61,11 +85,18 @@
|
|||||||
"Mit seiner benutzerfreundlichen Oberfläche können Sie Ihre Dokumente schnell hochladen, die Unterschriftsprozesse verfolgen und Ihre digitalen Unterschriftenanwendungen einfach durchführen. " +
|
"Mit seiner benutzerfreundlichen Oberfläche können Sie Ihre Dokumente schnell hochladen, die Unterschriftsprozesse verfolgen und Ihre digitalen Unterschriftenanwendungen einfach durchführen. " +
|
||||||
"Dieses Portal beschleunigt Ihren Arbeitsablauf mit rechtlich gültigen Unterschriften und erhöht gleichzeitig die Sicherheit Ihrer Dokumente.";
|
"Dieses Portal beschleunigt Ihren Arbeitsablauf mit rechtlich gültigen Unterschriften und erhöht gleichzeitig die Sicherheit Ihrer Dokumente.";
|
||||||
|
|
||||||
|
private EnvelopeGenerator.Server.Client.Services.CustomImageDto? _appLogo;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
// Load logo config early — 401 here is normal (not yet logged in), service returns defaults silently
|
||||||
|
var logos = await CustomImagesService.GetAsync();
|
||||||
|
_appLogo = logos.App;
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (!firstRender) return;
|
||||||
{
|
await JS.InvokeVoidAsync("receiverSignature.startTyped", "home-description", HomePageDescription, 15);
|
||||||
await JS.InvokeVoidAsync("receiverSignature.startTyped", "home-description", HomePageDescription, 15);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,58 +1,62 @@
|
|||||||
@page "/envelope/login/{EnvelopeKey}"
|
@page "/envelope/login/{EnvelopeKey}"
|
||||||
@using EnvelopeGenerator.ReceiverUI.Services
|
@rendermode InteractiveWebAssembly
|
||||||
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
@inject AuthService AuthService
|
@inject AuthService AuthService
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
|
||||||
|
|
||||||
<div class="login-page-wrapper d-flex align-items-center justify-content-center min-vh-100">
|
<div class="login-page-wrapper d-flex align-items-center justify-content-center min-vh-100">
|
||||||
<div class="login-card card shadow border-0" style="max-width: 440px; width: 100%;">
|
<div class="login-card card shadow border-0" style="max-width: 440px; width: 100%;">
|
||||||
|
|
||||||
<div class="card-header text-white text-center py-4 border-0" style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border-radius: calc(0.375rem - 1px) calc(0.375rem - 1px) 0 0;">
|
<div class="card-header text-white text-center py-4 border-0" style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border-radius: calc(0.375rem - 1px) calc(0.375rem - 1px) 0 0;">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="mb-0 fw-semibold">Dokument öffnen</h5>
|
<h5 class="mb-0 fw-semibold">Dokument öffnen</h5>
|
||||||
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">Sicherer Zugang mit Zugangscode</p>
|
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">Sicherer Zugang mit Zugangscode</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4">
|
||||||
|
|
||||||
<p class="text-muted mb-4" style="font-size: 0.875rem; line-height: 1.5;">
|
<p class="text-muted mb-4" style="font-size: 0.875rem; line-height: 1.5;">
|
||||||
Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.
|
Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@if (LoginResult == EnvelopeLoginResult.NotFound) {
|
@if (LoginResult == EnvelopeLoginResult.NotFound)
|
||||||
|
{
|
||||||
<div class="alert alert-warning d-flex align-items-start gap-2 py-2" role="alert">
|
<div class="alert alert-warning d-flex align-items-start gap-2 py-2" role="alert">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
||||||
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<strong>Dokument nicht gefunden.</strong><br />
|
<strong>Dokument nicht gefunden.</strong><br />
|
||||||
<span style="font-size:0.85rem;">Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.</span>
|
<span style="font-size:0.85rem;">Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} else if (LoginResult == EnvelopeLoginResult.InvalidCode) {
|
}
|
||||||
|
else if (LoginResult == EnvelopeLoginResult.InvalidCode)
|
||||||
|
{
|
||||||
<div class="alert alert-danger d-flex align-items-start gap-2 py-2" role="alert">
|
<div class="alert alert-danger d-flex align-items-start gap-2 py-2" role="alert">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
||||||
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<strong>Ungültiger Zugangscode.</strong><br />
|
<strong>Ungültiger Zugangscode.</strong><br />
|
||||||
<span style="font-size:0.85rem;">Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</span>
|
<span style="font-size:0.85rem;">Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} else if (LoginResult == EnvelopeLoginResult.Error) {
|
}
|
||||||
|
else if (LoginResult == EnvelopeLoginResult.Error)
|
||||||
|
{
|
||||||
<div class="alert alert-secondary d-flex align-items-start gap-2 py-2" role="alert">
|
<div class="alert alert-secondary d-flex align-items-start gap-2 py-2" role="alert">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
||||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||||
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
|
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<strong>Serverfehler.</strong><br />
|
<strong>Serverfehler.</strong><br />
|
||||||
<span style="font-size:0.85rem;">Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</span>
|
<span style="font-size:0.85rem;">Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -65,7 +69,7 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text bg-light border-end-0">
|
<span class="input-group-text bg-light border-end-0">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#6c757d" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#6c757d" viewBox="0 0 16 16">
|
||||||
<path d="M3.5 11.5a3.5 3.5 0 1 1 3.163-5H14L15.5 8 14 9.5l-1-1-1 1-1-1-1 1-1-1-1.837 1.337A3.5 3.5 0 0 1 3.5 11.5zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
|
<path d="M3.5 11.5a3.5 3.5 0 1 1 3.163-5H14L15.5 8 14 9.5l-1-1-1 1-1-1-1 1-1-1-1.837 1.337A3.5 3.5 0 0 1 3.5 11.5zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<input id="login-access-code"
|
<input id="login-access-code"
|
||||||
@@ -82,17 +86,20 @@
|
|||||||
style="border-left: none;"
|
style="border-left: none;"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@onclick="() => ShowCode = !ShowCode">
|
@onclick="() => ShowCode = !ShowCode">
|
||||||
@if (ShowCode) {
|
@if (ShowCode)
|
||||||
|
{
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/>
|
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z" />
|
||||||
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z"/>
|
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z" />
|
||||||
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709z"/>
|
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709z" />
|
||||||
<path fill-rule="evenodd" d="M13.646 14.354l-12-12 .708-.708 12 12-.708.708z"/>
|
<path fill-rule="evenodd" d="M13.646 14.354l-12-12 .708-.708 12 12-.708.708z" />
|
||||||
</svg>
|
</svg>
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
|
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z" />
|
||||||
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
|
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
@@ -103,14 +110,17 @@
|
|||||||
style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border: none;"
|
style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border: none;"
|
||||||
@onclick="SubmitAsync"
|
@onclick="SubmitAsync"
|
||||||
disabled="@(IsLoading || string.IsNullOrWhiteSpace(AccessCode))">
|
disabled="@(IsLoading || string.IsNullOrWhiteSpace(AccessCode))">
|
||||||
@if (IsLoading) {
|
@if (IsLoading)
|
||||||
|
{
|
||||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||||
<span>Überprüfen …</span>
|
<span>Überprüfen …</span>
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
|
||||||
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
<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>
|
</svg>
|
||||||
<span>Dokument öffnen</span>
|
<span>Dokument öffnen</span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -131,12 +141,14 @@
|
|||||||
bool IsLoading;
|
bool IsLoading;
|
||||||
EnvelopeLoginResult? LoginResult;
|
EnvelopeLoginResult? LoginResult;
|
||||||
|
|
||||||
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) {
|
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e)
|
||||||
|
{
|
||||||
if (e.Key == "Enter")
|
if (e.Key == "Enter")
|
||||||
await SubmitAsync();
|
await SubmitAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task SubmitAsync() {
|
async Task SubmitAsync()
|
||||||
|
{
|
||||||
if (string.IsNullOrWhiteSpace(AccessCode) || IsLoading) return;
|
if (string.IsNullOrWhiteSpace(AccessCode) || IsLoading) return;
|
||||||
|
|
||||||
IsLoading = true;
|
IsLoading = true;
|
||||||
@@ -145,7 +157,8 @@
|
|||||||
|
|
||||||
var result = await AuthService.LoginEnvelopeReceiverAsync(EnvelopeKey, AccessCode.Trim());
|
var result = await AuthService.LoginEnvelopeReceiverAsync(EnvelopeKey, AccessCode.Trim());
|
||||||
|
|
||||||
if (result == EnvelopeLoginResult.Success) {
|
if (result == EnvelopeLoginResult.Success)
|
||||||
|
{
|
||||||
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
@page "/sender/login"
|
@page "/sender/login"
|
||||||
@using EnvelopeGenerator.ReceiverUI.Services
|
@rendermode InteractiveWebAssembly
|
||||||
|
@using EnvelopeGenerator.Server.Client.Services
|
||||||
@inject AuthService AuthService
|
@inject AuthService AuthService
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
|
||||||
|
|
||||||
<div class="login-page-wrapper d-flex align-items-center justify-content-center min-vh-100">
|
<div class="login-page-wrapper d-flex align-items-center justify-content-center min-vh-100">
|
||||||
<div class="login-card card shadow border-0" style="max-width: 440px; width: 100%;">
|
<div class="login-card card shadow border-0" style="max-width: 440px; width: 100%;">
|
||||||
|
|
||||||
@@ -169,4 +168,3 @@
|
|||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using DevExpress.XtraReports.UI;
|
using DevExpress.XtraReports.UI;
|
||||||
namespace EnvelopeGenerator.ReceiverUI.PredefinedReports {
|
namespace EnvelopeGenerator.Server.Client.PredefinedReports {
|
||||||
public class Report : XtraReport {
|
public class Report : XtraReport {
|
||||||
private TopMarginBand topMarginBand1;
|
private TopMarginBand topMarginBand1;
|
||||||
private XRPageInfo xrPageInfo4;
|
private XRPageInfo xrPageInfo4;
|
||||||
@@ -1081,7 +1081,7 @@ namespace EnvelopeGenerator.ReceiverUI.PredefinedReports {
|
|||||||
objectConstructorInfo1.Parameters.AddRange(new DevExpress.DataAccess.ObjectBinding.Parameter[] {
|
objectConstructorInfo1.Parameters.AddRange(new DevExpress.DataAccess.ObjectBinding.Parameter[] {
|
||||||
parameter1});
|
parameter1});
|
||||||
this.objectDataSource1.Constructor = objectConstructorInfo1;
|
this.objectDataSource1.Constructor = objectConstructorInfo1;
|
||||||
this.objectDataSource1.DataSource = typeof(EnvelopeGenerator.ReceiverUI.Data.DataItemList);
|
this.objectDataSource1.DataSource = typeof(EnvelopeGenerator.Server.Client.Data.DataItemList);
|
||||||
this.objectDataSource1.Name = "objectDataSource1";
|
this.objectDataSource1.Name = "objectDataSource1";
|
||||||
//
|
//
|
||||||
// Title
|
// Title
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using DevExpress.XtraReports.UI;
|
using DevExpress.XtraReports.UI;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.PredefinedReports {
|
namespace EnvelopeGenerator.Server.Client.PredefinedReports {
|
||||||
public static class ReportsFactory
|
public static class ReportsFactory
|
||||||
{
|
{
|
||||||
public static readonly Dictionary<string, Func<XtraReport>> Reports = new() {
|
public static readonly Dictionary<string, Func<XtraReport>> Reports = new() {
|
||||||
@@ -11,4 +11,4 @@ namespace EnvelopeGenerator.ReceiverUI.PredefinedReports {
|
|||||||
return Reports[reportName]();
|
return Reports[reportName]();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
|
using EnvelopeGenerator.Server.Client.Services;
|
||||||
|
using EnvelopeGenerator.Server.Client.Options;
|
||||||
|
using DevExpress.Blazor.Reporting;
|
||||||
|
using DevExpress.XtraReports.Web.Extensions;
|
||||||
|
using DevExpress.DataAccess.Web;
|
||||||
|
using DevExpress.XtraReports.Services;
|
||||||
|
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
|
||||||
|
// Named HttpClient for API calls (both for services and DevExpress components)
|
||||||
|
builder.Services.AddHttpClient("EnvelopeGenerator.Server", client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default HttpClient (DevExpress PdfViewer requires this)
|
||||||
|
builder.Services.AddScoped(sp => new HttpClient
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configuration Options
|
||||||
|
builder.Services.Configure<PdfViewerOptions>(opts =>
|
||||||
|
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
|
||||||
|
|
||||||
|
// Business Services
|
||||||
|
builder.Services.AddScoped<DocumentService>();
|
||||||
|
builder.Services.AddScoped<AuthService>();
|
||||||
|
builder.Services.AddScoped<AnnotationService>();
|
||||||
|
builder.Services.AddScoped<EnvelopeReceiverService>();
|
||||||
|
builder.Services.AddScoped<SignatureService>();
|
||||||
|
builder.Services.AddScoped<SignatureCacheService>();
|
||||||
|
builder.Services.AddSingleton<AppVersionService>();
|
||||||
|
builder.Services.AddScoped<EnvelopeService>();
|
||||||
|
|
||||||
|
// UI Preferences & Theme
|
||||||
|
builder.Services.AddScoped<UserPreferencesService>();
|
||||||
|
builder.Services.AddScoped<ThemeService>();
|
||||||
|
|
||||||
|
// Logo configuration (loaded once after login, in-memory cached)
|
||||||
|
builder.Services.AddScoped<CustomImagesService>();
|
||||||
|
|
||||||
|
// DevExpress WASM
|
||||||
|
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
|
||||||
|
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
|
||||||
|
|
||||||
|
builder.Services.AddDevExpressBlazorReportingWebAssembly(configure => {
|
||||||
|
configure.UseDevelopmentMode();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reporting Services
|
||||||
|
builder.Services.AddScoped<IDataSourceWizardJsonConnectionStorage, CustomDataSourceWizardJsonDataConnectionStorage>();
|
||||||
|
builder.Services.AddScoped<IJsonDataConnectionProviderFactory, CustomJsonDataConnectionProviderFactory>();
|
||||||
|
builder.Services.AddScoped<IObjectDataSourceWizardTypeProvider, ObjectDataSourceWizardCustomTypeProvider>();
|
||||||
|
|
||||||
|
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.Server.Client.Data.DataItemList));
|
||||||
|
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.Server.Client.PredefinedReports.Report));
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<InMemoryReportStorageWebExtension>();
|
||||||
|
builder.Services.AddSingleton<ReportStorageWebExtension>(sp => sp.GetRequiredService<InMemoryReportStorageWebExtension>());
|
||||||
|
builder.Services.AddScoped<IReportProviderAsync, CustomReportProvider>();
|
||||||
|
|
||||||
|
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
await FontLoader.LoadFonts(host.Services.GetRequiredService<IHttpClientFactory>(), new List<string> { "opensans.ttf" });
|
||||||
|
await host.RunAsync();
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<Router AppAssembly="typeof(Program).Assembly">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||||
|
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||||
|
</Found>
|
||||||
|
</Router>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using EnvelopeGenerator.Server.Client.Models;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves annotation positions from the API.
|
||||||
|
/// Uses relative paths (/api/Annotation/{envelopeKey}).
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Use SignatureService.")]
|
||||||
|
public class AnnotationService(IHttpClientFactory httpClientFactory)
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<AnnotationDto>> GetAnnotationsAsync(string envelopeKey, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
var url = $"/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,4 +1,4 @@
|
|||||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides application version for cache busting static assets.
|
/// Provides application version for cache busting static assets.
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using EnvelopeGenerator.ReceiverUI.Options;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
|
public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
|
||||||
|
|
||||||
public enum SenderLoginResult { Success, InvalidCredentials, Error }
|
public enum SenderLoginResult { Success, InvalidCredentials, Error }
|
||||||
|
|
||||||
public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
public class AuthService(IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
private readonly ApiOptions _api = apiOptions.Value;
|
private HttpClient CreateDefaultClient() => httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks whether the current user holds a valid receiver token for the given envelope key.
|
/// Checks whether the current user holds a valid receiver token for the given envelope key.
|
||||||
@@ -19,7 +17,19 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<bool> CheckEnvelopeAccessAsync(string envelopeKey, CancellationToken cancel = default)
|
public async Task<bool> CheckEnvelopeAccessAsync(string envelopeKey, CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
var response = await http.GetAsync($"{_api.BaseUrl}/api/auth/check/envelope/{Uri.EscapeDataString(envelopeKey)}", cancel);
|
using var http = CreateDefaultClient();
|
||||||
|
var response = await http.GetAsync($"/api/auth/check/envelope/{Uri.EscapeDataString(envelopeKey)}", cancel);
|
||||||
|
return response.StatusCode == HttpStatusCode.OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the current user holds a valid receiver token for the given envelope key.
|
||||||
|
/// Calls GET /api/auth/check/envelope/{envelopeKey}.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> CheckSenderAccessAsync(CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
using var http = CreateDefaultClient();
|
||||||
|
var response = await http.GetAsync($"/api/auth/check", cancel);
|
||||||
return response.StatusCode == HttpStatusCode.OK;
|
return response.StatusCode == HttpStatusCode.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,11 +40,14 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string envelopeKey, string accessCode, CancellationToken cancel = default)
|
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string envelopeKey, string accessCode, CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
var form = new MultipartFormDataContent();
|
using var http = CreateDefaultClient();
|
||||||
form.Add(new StringContent(accessCode), "AccessCode");
|
var form = new MultipartFormDataContent
|
||||||
|
{
|
||||||
|
{ new StringContent(accessCode), "AccessCode" }
|
||||||
|
};
|
||||||
|
|
||||||
var response = await http.PostAsync(
|
var response = await http.PostAsync(
|
||||||
$"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(envelopeKey)}",
|
$"/api/Auth/envelope-receiver/{Uri.EscapeDataString(envelopeKey)}",
|
||||||
form, cancel);
|
form, cancel);
|
||||||
|
|
||||||
return response.StatusCode switch
|
return response.StatusCode switch
|
||||||
@@ -52,20 +65,24 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<bool> LogoutEnvelopeReceiverAsync(string envelopeKey, CancellationToken cancel = default)
|
public async Task<bool> LogoutEnvelopeReceiverAsync(string envelopeKey, CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
|
using var http = CreateDefaultClient();
|
||||||
var response = await http.PostAsync(
|
var response = await http.PostAsync(
|
||||||
$"{_api.BaseUrl}/api/auth/logout/envelope/{Uri.EscapeDataString(envelopeKey)}",
|
$"/api/auth/logout/envelope/{Uri.EscapeDataString(envelopeKey)}",
|
||||||
null, cancel);
|
null, cancel);
|
||||||
return response.IsSuccessStatusCode;
|
return response.IsSuccessStatusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks whether the current user holds a valid receiver token for the given envelope key.
|
/// Removes the per-envelope receiver cookie for the given envelope key.
|
||||||
/// Calls GET /api/auth/check/envelope/{envelopeKey}.
|
/// Calls POST /api/auth/logout/envelope/{envelopeKey}.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<bool> CheckSenderAsync(CancellationToken cancel = default)
|
public async Task<bool> LogoutSenderAsync(CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
var response = await http.GetAsync($"{_api.BaseUrl}/api/auth/check", cancel);
|
using var http = CreateDefaultClient();
|
||||||
return response.StatusCode == HttpStatusCode.OK;
|
var response = await http.PostAsync(
|
||||||
|
$"/api/auth/logout",
|
||||||
|
null, cancel);
|
||||||
|
return response.IsSuccessStatusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -75,10 +92,11 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<SenderLoginResult> LoginSenderAsync(string username, string password, CancellationToken cancel = default)
|
public async Task<SenderLoginResult> LoginSenderAsync(string username, string password, CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
|
using var http = CreateDefaultClient();
|
||||||
var requestBody = new { username, password };
|
var requestBody = new { username, password };
|
||||||
|
|
||||||
var response = await http.PostAsJsonAsync(
|
var response = await http.PostAsJsonAsync(
|
||||||
$"{_api.BaseUrl}/api/auth?cookie=true",
|
$"/api/auth?cookie=true",
|
||||||
requestBody, cancel);
|
requestBody, cancel);
|
||||||
|
|
||||||
return response.StatusCode switch
|
return response.StatusCode switch
|
||||||
@@ -88,16 +106,4 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
|||||||
_ => SenderLoginResult.Error
|
_ => SenderLoginResult.Error
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Logs out the sender user by removing the authentication cookie.
|
|
||||||
/// Calls POST /api/auth/logout.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<bool> LogoutSenderAsync(CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
var response = await http.PostAsync(
|
|
||||||
$"{_api.BaseUrl}/api/auth/logout",
|
|
||||||
null, cancel);
|
|
||||||
return response.IsSuccessStatusCode;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Service for managing application culture/localization.
|
/// Service for managing application culture/localization.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using DevExpress.DataAccess.Json;
|
||||||
|
using DevExpress.DataAccess.Web;
|
||||||
|
using DevExpress.DataAccess.Wizard.Services;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Client.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) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Client.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches and caches the CustomImages configuration (signFLOW + company logos)
|
||||||
|
/// from GET /api/CustomImages.
|
||||||
|
///
|
||||||
|
/// Config changes rarely (deployment-time only), so results are cached in-memory
|
||||||
|
/// for the lifetime of the service (WASM app session). No TTL needed.
|
||||||
|
/// </summary>
|
||||||
|
public class CustomImagesService(IHttpClientFactory clientFactory)
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions _json = new(JsonSerializerDefaults.Web);
|
||||||
|
private const string Endpoint = "/api/CustomImages";
|
||||||
|
|
||||||
|
private CustomImagesDto? _cached;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns logo configuration. Fetches once from server; all subsequent calls
|
||||||
|
/// are served from memory with zero network overhead.
|
||||||
|
/// Returns defaults (empty Src) on auth failure (401) — components must handle gracefully.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<CustomImagesDto> GetAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (_cached is not null)
|
||||||
|
return _cached;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = clientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||||
|
var response = await client.GetAsync(Endpoint, ct);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
// Not authenticated yet (login page) — return defaults silently.
|
||||||
|
_cached = new CustomImagesDto();
|
||||||
|
return _cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cached = await response.Content.ReadFromJsonAsync<CustomImagesDto>(_json, ct)
|
||||||
|
?? new CustomImagesDto();
|
||||||
|
return _cached;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Network error or server unavailable — return defaults so UI still renders.
|
||||||
|
return new CustomImagesDto();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Call after logout to force re-fetch on next login.</summary>
|
||||||
|
public void Invalidate() => _cached = null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using DevExpress.DataAccess.Json;
|
||||||
|
using DevExpress.DataAccess.Web;
|
||||||
|
namespace EnvelopeGenerator.Server.Client.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using DevExpress.XtraReports.UI;
|
||||||
|
using DevExpress.XtraReports.Services;
|
||||||
|
using EnvelopeGenerator.Server.Client.PredefinedReports;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Server.Client.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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user