Compare commits
73 Commits
71e375d6ea
...
feat/migr-
| Author | SHA1 | Date | |
|---|---|---|---|
| 732fe92952 | |||
| 99fbb33f1c | |||
| a10ee590c9 | |||
| 03367ebc4a | |||
| 1ac7188466 | |||
| db593cb46a | |||
| a5e4f97397 | |||
| 6ca03a50eb | |||
| 96a84ba1a5 | |||
| ec0ea72890 | |||
| 7b912387e7 | |||
| 6c142eba08 | |||
| 489d2808a1 | |||
| 7466fd78f6 | |||
| e34b5ddbbe | |||
| b56f906848 | |||
| fe09c5c7ae | |||
| 0763d82f6e | |||
| 5a30bc050b | |||
| a4b218b9f3 | |||
| 67798b35da | |||
| b5bb2bbaae | |||
| 85a0736106 | |||
| de9c9da176 | |||
| f4571320ce | |||
| 2abfffdeba | |||
| bfd1a9d060 | |||
| 78ed49a077 | |||
| 6aa97adf84 | |||
| 7456babe0d | |||
| 6fe99d0cd0 | |||
| 45018d04b1 | |||
| b5af3e61ed | |||
| 314608f27f | |||
| ba9f233993 | |||
| 9d962708c4 | |||
| c93a056ca5 | |||
| a88a26c248 | |||
| 1e963ea215 | |||
| 02b857382c | |||
| ca4ec7cb6f | |||
| f2356b3ce4 | |||
| d61fe79613 | |||
| 714cb9555f | |||
| 315a022cb8 | |||
| 746635979b | |||
| 31548728cd | |||
| 06c8af2ed8 | |||
| 9f57baf2e5 | |||
| 73d793f0a0 | |||
| 65bb68feef | |||
| c5e97ee30b | |||
| 3a4f449b59 | |||
| 6ca7767e4d | |||
| 4237f0a815 | |||
| 3302be9348 | |||
| 4572e20c51 | |||
| b3a70d7259 | |||
| bb81920d44 | |||
| 3b66de0691 | |||
| 9f6004ba8c | |||
| ef246bae32 | |||
| e4ebb29969 | |||
| 83cdb9dfe9 | |||
| c5db676e01 | |||
| 95c8e15887 | |||
| 561b844e59 | |||
| 011960be75 | |||
| 151c785af9 | |||
| fa354a05cc | |||
| 1326407462 | |||
| a3c653ddb3 | |||
| 8d736cdc5e |
@@ -1,477 +1,419 @@
|
||||
# EnvelopeGenerator — AI Context Reference
|
||||
# EnvelopeGenerator — Current Workspace Context
|
||||
|
||||
## Purpose
|
||||
Digital document signing system with **unified Blazor Auto (Server+WASM hybrid) frontend** for both Senders and Receivers. Senders create envelopes and place signature fields. Receivers view PDFs, sign documents, export stamped PDFs.
|
||||
Digital document signing system for senders and receivers.
|
||||
|
||||
**Primary Libraries:** DevExpress + PDF.js (PSPDFKit removed)
|
||||
- Senders authenticate, view envelope lists, and manage envelope workflows.
|
||||
- Receivers authenticate per envelope, open PDFs, create signatures, and apply them in the viewer.
|
||||
- The active UI stack is `Blazor Auto` with server-side and WebAssembly render modes.
|
||||
- Primary UI/PDF libraries are `DevExpress` and `PDF.js`.
|
||||
|
||||
---
|
||||
|
||||
## Migration Notice
|
||||
## Active Application Structure
|
||||
|
||||
**EnvelopeGenerator.ReceiverUI ? EnvelopeGenerator.WebUI Migration**
|
||||
### Main Host
|
||||
**Primary active application:** `EnvelopeGenerator.Server`
|
||||
|
||||
The project has been migrated from pure Blazor WebAssembly (`ReceiverUI`) to **Blazor Auto (Server+WASM hybrid)** architecture (`WebUI`) to resolve DevExpress `DxPdfViewer` compatibility issues.
|
||||
`EnvelopeGenerator.Server` is the current runtime host and contains:
|
||||
- Blazor server host
|
||||
- WebAssembly host integration
|
||||
- API controllers
|
||||
- authentication/authorization setup
|
||||
- Swagger/Scalar setup
|
||||
- YARP reverse proxy configuration
|
||||
- DevExpress server-side services
|
||||
- SQL Server distributed cache setup
|
||||
|
||||
**Reason:** DevExpress `DxPdfViewer` requires backend server-side rendering services that are NOT available in pure WebAssembly projects.
|
||||
### Client Project
|
||||
**Client UI project:** `EnvelopeGenerator.Server.Client`
|
||||
|
||||
**New Structure:**
|
||||
- **WebUI** (Server project): Hosts server-side components, YARP proxy, DevExpress backend services
|
||||
- **WebUI.Client** (WASM project): Client-side components, business logic, services
|
||||
This project contains:
|
||||
- WebAssembly-rendered pages
|
||||
- client-side services
|
||||
- client models and options
|
||||
- sender and receiver login flows
|
||||
|
||||
**Migration Details:** See `MIGRATION_CONTEXT.md`
|
||||
### Other Projects
|
||||
- `EnvelopeGenerator.Application` — MediatR/CQRS handlers and business logic
|
||||
- `EnvelopeGenerator.Domain` — domain models, constants, shared abstractions
|
||||
- `EnvelopeGenerator.Infrastructure` — EF Core and infrastructure services
|
||||
- `EnvelopeGenerator.PdfEditor` — PDF-related backend utilities
|
||||
- `EnvelopeGenerator.API` — still exists in the solution, but the current merged app host is `EnvelopeGenerator.Server`
|
||||
|
||||
### Legacy / Do Not Touch
|
||||
- `EnvelopeGenerator.Service`
|
||||
- `EnvelopeGenerator.Form`
|
||||
- `EnvelopeGenerator.BBTests`
|
||||
- `EnvelopeGenerator.CommonServices`
|
||||
|
||||
---
|
||||
|
||||
## Deployment Architecture
|
||||
## Current Hosting Model
|
||||
|
||||
**Two Presentation Projects (Both Required):**
|
||||
`EnvelopeGenerator.Server/Program.cs` currently configures:
|
||||
- `AddRazorComponents()` with both interactive server and interactive WebAssembly components
|
||||
- `AddControllers()` and `MapControllers()`
|
||||
- JWT authentication for sender and receiver flows
|
||||
- cookie authentication
|
||||
- authorization policies using `AuthScheme.Sender`, `AuthScheme.Receiver`, `AuthPolicy.Sender`, `AuthPolicy.Receiver`
|
||||
- `AddReverseProxy()` with `yarp.json`
|
||||
- Swagger / OpenAPI / Scalar
|
||||
- distributed SQL Server cache
|
||||
- DevExpress Blazor and DevExpress PDF Viewer server-side services
|
||||
- request localization middleware
|
||||
|
||||
1. **EnvelopeGenerator.API** (ASP.NET Core Web API)
|
||||
- Runs independently (development & production)
|
||||
- Backend services for document management, authentication, signature endpoints
|
||||
- Serves as API endpoint for WebUI
|
||||
This means the active app is a **merged UI + API host**.
|
||||
|
||||
2. **EnvelopeGenerator.WebUI** (Blazor Auto - Server+WASM Hybrid)
|
||||
- **Server Project (`EnvelopeGenerator.WebUI`):**
|
||||
- **YARP Reverse Proxy** configured via `yarp.json`
|
||||
- Proxies `/api/*` requests to `API:8088`
|
||||
- Hosts server-side components (`@rendermode InteractiveServer`)
|
||||
- DevExpress server-side services (DxPdfViewer backend)
|
||||
- **Client Project (`EnvelopeGenerator.WebUI.Client`):**
|
||||
- Client-side components (`@rendermode InteractiveWebAssembly`)
|
||||
- Business logic services (AuthService, DocumentService, etc.)
|
||||
- WASM runtime
|
||||
---
|
||||
|
||||
**Request Flow:**
|
||||
```
|
||||
Client ? WebUI:XXXX (Blazor Auto)
|
||||
?? Server-side Pages (DxPdfViewer)
|
||||
?? Client-side Pages (WASM)
|
||||
?? YARP Proxy: /api/* ? API:8088
|
||||
## Reverse Proxy
|
||||
|
||||
**Config file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
|
||||
|
||||
Current YARP usage is focused on **AuthHub forwarding**, not a general `/api/* -> EnvelopeGenerator.API` proxy.
|
||||
|
||||
Configured routes forward:
|
||||
- `POST /api/auth` -> AuthHub `/api/auth/sign-flow`
|
||||
- `POST /api/Auth/envelope-receiver/{key}` -> AuthHub `/api/auth/envelope-receiver/{key}?cookie=true`
|
||||
|
||||
---
|
||||
|
||||
## Active Routes and Files
|
||||
|
||||
### WebAssembly Pages (`EnvelopeGenerator.Server.Client`)
|
||||
| Route | File | Render Mode | Purpose |
|
||||
|---|---|---|---|
|
||||
| `/` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/IndexPage.razor` | WebAssembly | Landing page |
|
||||
| `/sender/login` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginSenderPage.razor` | WebAssembly | Sender login |
|
||||
| `/sender` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor` | WebAssembly (`prerender: false`) | Sender dashboard |
|
||||
| `/envelope/login/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor` | WebAssembly | Receiver login |
|
||||
|
||||
### Server Pages (`EnvelopeGenerator.Server`)
|
||||
| Route | File | Render Mode | Purpose |
|
||||
|---|---|---|---|
|
||||
| `/envelope/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` | InteractiveServer | Main receiver PDF viewer and signing page |
|
||||
| `/envelope/DxPdfViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor` | InteractiveServer | DevExpress PDF Viewer test page |
|
||||
| `/envelope/{EnvelopeKey}/DxReportViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxReportViewer.razor` | InteractiveServer | DevExpress report-based PDF rendering |
|
||||
| `/envelope/Embed` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_embed.razor` | InteractiveServer | Embedded browser PDF view test page |
|
||||
|
||||
---
|
||||
|
||||
## Current API Location
|
||||
|
||||
The active application exposes controllers from:
|
||||
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers`
|
||||
|
||||
Current controller set includes:
|
||||
- `AnnotationController`
|
||||
- `AuthController`
|
||||
- `CacheController`
|
||||
- `ConfigController`
|
||||
- `DocumentController`
|
||||
- `EmailTemplateController`
|
||||
- `EnvelopeController`
|
||||
- `EnvelopeReceiverController`
|
||||
- `EnvelopeTypeController`
|
||||
- `HistoryController`
|
||||
- `LocalizationController`
|
||||
- `ReadOnlyController`
|
||||
- `ReceiverController`
|
||||
- `SignatureController`
|
||||
- `TfaRegistrationController`
|
||||
|
||||
Do not assume API behavior lives only in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
|
||||
|
||||
---
|
||||
|
||||
## Authentication Model
|
||||
|
||||
### Sender
|
||||
Client login page uses `EnvelopeGenerator.Server.Client/Services/AuthService.cs`.
|
||||
|
||||
Key sender endpoints:
|
||||
- `POST /api/auth?cookie=true` — login
|
||||
- `GET /api/auth/check` — current sender access check
|
||||
- `POST /api/auth/logout` — logout
|
||||
|
||||
### Receiver
|
||||
Receiver authentication is **per envelope**.
|
||||
|
||||
Key receiver endpoints used by client services:
|
||||
- `POST /api/Auth/envelope-receiver/{envelopeKey}` — submit access code
|
||||
- `GET /api/auth/check/envelope/{envelopeKey}` — check access
|
||||
- `POST /api/auth/logout/envelope/{envelopeKey}` — logout receiver for one envelope
|
||||
|
||||
Receiver cookie resolution in server auth uses an envelope-specific cookie name derived from:
|
||||
- `AuthTokenSignFLOWReceiver.{envelopeKey}` pattern
|
||||
|
||||
### Receiver Server-Side Authorization
|
||||
`EnvelopeReceiverPage.razor` does **not** rely on its own API access-check call for page authorization.
|
||||
|
||||
It uses:
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
|
||||
|
||||
Behavior:
|
||||
- tries the current `HttpContext.User`
|
||||
- if needed, reads the per-envelope receiver cookie directly
|
||||
- validates the JWT with the receiver auth scheme
|
||||
- verifies the token subject matches the route envelope key
|
||||
|
||||
---
|
||||
|
||||
## Receiver Page Data Loading
|
||||
|
||||
Main server-side page data service:
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
|
||||
|
||||
This service loads directly via MediatR and distributed cache:
|
||||
- document bytes
|
||||
- receiver envelope data
|
||||
- signature placeholders
|
||||
- cached signature data
|
||||
|
||||
For signature placeholders, the service:
|
||||
- reads document receiver elements
|
||||
- filters them for the authenticated receiver
|
||||
- converts coordinates to `UnitOfLength.Point` before UI use
|
||||
|
||||
---
|
||||
|
||||
## Receiver PDF Viewer
|
||||
|
||||
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
|
||||
|
||||
Current receiver viewer characteristics:
|
||||
- route: `/envelope/{EnvelopeKey}`
|
||||
- render mode: `InteractiveServer`
|
||||
- PDF rendering: **Migration in progress from `PDF.js` to `DxPdfViewer`**
|
||||
- toolbar: page navigation, zoom, thumbnail toggle, signature navigation, signature reset
|
||||
- signature popup: `DxPopup`
|
||||
- thumbnail sidebar: resizable and stored in `localStorage`
|
||||
|
||||
### ⚠️ CRITICAL: DevExpress DxPdfViewer Control Requirements
|
||||
|
||||
**Verified API for installed `DevExpress.Blazor.PdfViewer` v25.2.3:**
|
||||
|
||||
| Property | Access | Notes |
|
||||
|----------|--------|-------|
|
||||
| `DocumentContent` | `[Parameter]` GET/SET | Feed PDF as `byte[]` |
|
||||
| `ZoomLevel` | `[Parameter]` GET/SET | **Factor** (not percentage): `1.5` = 150% |
|
||||
| `IsSinglePagePreview` | `[Parameter]` GET/SET | Single page mode |
|
||||
| `CssClass` | `[Parameter]` GET/SET | CSS class |
|
||||
| `DocumentName` | `[Parameter]` GET/SET | Download filename |
|
||||
| `SizeMode` | `[Parameter]` GET/SET | `Small` / `Medium` / `Large` |
|
||||
| `PageCount` | Read-only GET | Total pages — **no JS call needed** |
|
||||
| `ActivePageIndex` | Read-only GET | Current page (0-based) — **cannot SET** |
|
||||
| `CustomizeToolbar` | Event | Only available toolbar event |
|
||||
|
||||
**Does NOT exist in v25.2.3 — do NOT use:**
|
||||
- `GoToPageAsync()` ❌
|
||||
- `GoToNextPageAsync()` ❌
|
||||
- `ZoomAsync()` ❌
|
||||
- `PageNumberChanged` event ❌
|
||||
- `ZoomLevelChanged` event ❌
|
||||
- `ToolbarVisible` property ❌
|
||||
|
||||
**Correct approach:**
|
||||
```razor
|
||||
<DxPdfViewer @ref="_pdfViewer"
|
||||
DocumentContent="@_pdfDocumentContent"
|
||||
ZoomLevel="@_viewerZoomLevel"
|
||||
IsSinglePagePreview="true"
|
||||
CustomizeToolbar="OnCustomizeToolbar" />
|
||||
```
|
||||
|
||||
**Configuration:** `EnvelopeGenerator.WebUI/yarp.json`
|
||||
```csharp
|
||||
// ZoomLevel: always divide by 100 (factor, not percentage)
|
||||
_viewerZoomLevel = _currentZoom / 100d; // 150 -> 1.5
|
||||
|
||||
---
|
||||
// PageCount: read directly, no JS needed
|
||||
_totalPages = _pdfViewer.PageCount;
|
||||
|
||||
## WebUI Route Structure
|
||||
|
||||
### Root Route
|
||||
| Route | File | Location | Render Mode |
|
||||
|---|---|---|---|
|
||||
| `/` | `Index.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
||||
|
||||
### Sender Routes
|
||||
| Route | File | Location | Render Mode |
|
||||
|---|---|---|---|
|
||||
| `/sender/login` | `LoginSenderPage.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
||||
| `/sender` | `EnvelopeSenderPage.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
||||
|
||||
### Receiver Routes (PDF Viewers)
|
||||
| Route | File | Location | Render Mode |
|
||||
|---|---|---|---|
|
||||
| `/envelope/login/{EnvelopeKey}` | `LoginReceiverPage.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
||||
| `/envelope/{EnvelopeKey}` | `EnvelopeReceiverPage.razor` | `WebUI/Components/Pages/` | `@rendermode InteractiveServer` |
|
||||
| `/envelope/DxPdfViewer` | `EnvelopeReceiverPage_DxPdfViewer.razor` | `WebUI/Components/Pages/` | `@rendermode InteractiveServer` |
|
||||
| `/envelope/{EnvelopeKey}/DxReportViewer` | `EnvelopeReceiverPage_DxReportViewer.razor` | `WebUI/Components/Pages/` | `@rendermode InteractiveServer` |
|
||||
| `/envelope/Embed` | `EnvelopeReceiverPage_embed.razor` | `WebUI/Components/Pages/` | `@rendermode InteractiveServer` |
|
||||
|
||||
**Multi-Envelope Support:** Receivers can login to multiple envelopes simultaneously (per-envelope cookie authentication).
|
||||
|
||||
**Render Mode Strategy:**
|
||||
- **Client-side Pages (WASM):** Login, Sender dashboard, Index (no DevExpress backend required)
|
||||
- **Server-side Pages (Server):** PDF viewers (DevExpress DxPdfViewer requires backend)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Evolution
|
||||
|
||||
### Old Architecture (Deprecated v1)
|
||||
- **Sender UI:** `EnvelopeGenerator.Web` (Razor Pages + PSPDFKit)
|
||||
- **Receiver UI:** Separate project
|
||||
- **Backend:** `EnvelopeGenerator.API`
|
||||
|
||||
### Intermediate Architecture (Deprecated v2)
|
||||
- **Unified Frontend:** `EnvelopeGenerator.ReceiverUI` (Pure Blazor WASM)
|
||||
- **Backend:** `EnvelopeGenerator.API`
|
||||
- **Issue:** DevExpress `DxPdfViewer` displayed blank screen (no backend services in WASM)
|
||||
|
||||
### Current Architecture (Active)
|
||||
- **Frontend:** `EnvelopeGenerator.WebUI` (Blazor Auto - Server+WASM Hybrid)
|
||||
- **WebUI** (Server): Server-side components, YARP proxy, DevExpress backend
|
||||
- **WebUI.Client** (WASM): Client-side components, services, business logic
|
||||
- **Backend:** `EnvelopeGenerator.API`
|
||||
- **Libraries:** DevExpress + PDF.js
|
||||
- **PSPDFKit:** **REMOVED**
|
||||
|
||||
---
|
||||
|
||||
## Solution Structure
|
||||
|
||||
| Project | Target | Purpose |
|
||||
|---|---|---|
|
||||
| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Backend for **both Senders & Receivers**. Auth, PDF serving, signature endpoints. |
|
||||
| `EnvelopeGenerator.WebUI` | net8.0 | **Blazor Auto Server Project**. YARP proxy, server-side components, DevExpress backend services. |
|
||||
| `EnvelopeGenerator.WebUI.Client` | net8.0 WASM | **Blazor Auto Client Project**. Client-side components, services, business logic. |
|
||||
| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | **DEPRECATED.** Pure Blazor WASM (migrated to WebUI). |
|
||||
| `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 WebUI). |
|
||||
| `EnvelopeGenerator.DependencyInjection` | multi | DI registration helpers. |
|
||||
| **VB.NET projects** (Service/Form/BBTests) | net462 | **Legacy. Do NOT touch.** |
|
||||
|
||||
---
|
||||
|
||||
## Key Files & Routes
|
||||
|
||||
### Client-Side Pages (WebUI.Client)
|
||||
| File | Route | Purpose |
|
||||
|---|---|---|
|
||||
| `WebUI.Client/Pages/Index.razor` | `/` | Application entry point (landing page). |
|
||||
| `WebUI.Client/Pages/EnvelopeSenderPage.razor` | `/sender` | Sender dashboard (envelope list). |
|
||||
| `WebUI.Client/Pages/LoginSenderPage.razor` | `/sender/login` | Sender username/password auth. |
|
||||
| `WebUI.Client/Pages/LoginReceiverPage.razor` | `/envelope/login/{EnvelopeKey}` | Receiver access code auth. |
|
||||
|
||||
### Server-Side Pages (WebUI)
|
||||
| File | Route | Purpose |
|
||||
|---|---|---|
|
||||
| `WebUI/Components/Pages/EnvelopeReceiverPage.razor` | `/envelope/{key}` | Receiver PDF viewer & signing (PDF.js). |
|
||||
| `WebUI/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor` | `/envelope/DxPdfViewer` | DevExpress PDF Viewer (test page). |
|
||||
| `WebUI/Components/Pages/EnvelopeReceiverPage_DxReportViewer.razor` | `/envelope/{key}/DxReportViewer` | DevExpress Report Viewer. |
|
||||
| `WebUI/Components/Pages/EnvelopeReceiverPage_embed.razor` | `/envelope/Embed` | Embedded PDF viewer (iframe). |
|
||||
|
||||
### Services & Assets
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `WebUI.Client/Services/AuthService.cs` | Receiver + Sender authentication. |
|
||||
| `WebUI.Client/Services/SignatureCacheService.cs` | Signature caching (Redis/SQL). |
|
||||
| `WebUI.Client/Services/DocumentService.cs` | PDF document retrieval. |
|
||||
| `WebUI/wwwroot/js/pdf-viewer.js` | PDF.js wrapper (zoom, pagination, thumbnails). |
|
||||
| `WebUI/wwwroot/js/receiver-signature.js` | Signature pad (draw/type/image). |
|
||||
| `WebUI/wwwroot/css/envelope-viewer.css` | EnvelopeViewer styles. |
|
||||
| `API/Controllers/CacheController.cs` | Signature cache endpoints. |
|
||||
|
||||
---
|
||||
|
||||
## Coordinate System — CRITICAL
|
||||
|
||||
**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 |
|
||||
| PDF.js | Pixels | Top-left | Down |
|
||||
| iText7 PDF | Points (1/72") | **Bottom-left** | **Up** (flip required) |
|
||||
| ~~PSPDFKit~~ | ~~Points~~ | ~~Top-left~~ | **REMOVED** |
|
||||
|
||||
---
|
||||
|
||||
## EnvelopeReceiver — PDF.js Viewer & Signing
|
||||
|
||||
**Route:** `/envelope/{EnvelopeKey}`
|
||||
**Tech:** PDF.js 3.11.174 + Blazor Server (`@rendermode InteractiveServer`) + configurable quality
|
||||
**File:** `WebUI/Components/Pages/EnvelopeReceiverPage.razor`
|
||||
|
||||
### Key Features
|
||||
1. HiDPI/Retina support (4x quality)
|
||||
2. Configurable quality (`appsettings.json`)
|
||||
3. Unlimited zoom (50%-300%)
|
||||
4. Ctrl+Wheel global zoom
|
||||
5. Resizable thumbnail sidebar (150-400px, localStorage)
|
||||
6. Responsive (desktop/mobile)
|
||||
|
||||
### Configuration
|
||||
**File:** `WebUI/wwwroot/appsettings.json`
|
||||
|
||||
```json
|
||||
// Page navigation: only via CustomizeToolbar buttons
|
||||
protected void OnCustomizeToolbar(ToolbarModel toolbarModel)
|
||||
{
|
||||
"PdfViewer": {
|
||||
"ThumbnailBaseScale": 0.75,
|
||||
"ThumbnailEnableHiDPI": true,
|
||||
"MainCanvasEnableHiDPI": true,
|
||||
"ZoomStepPercentage": 5
|
||||
}
|
||||
toolbarModel.AllItems.Clear();
|
||||
var nextButton = new ToolbarItem
|
||||
{
|
||||
IconCssClass = "dx-icon-chevronnext",
|
||||
Enabled = _currentPage < _totalPages,
|
||||
Click = async (args) =>
|
||||
{
|
||||
_currentPage++;
|
||||
_viewerZoomLevel = _currentZoom / 100d;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await RenderSignatureButtonsAsync();
|
||||
}
|
||||
};
|
||||
toolbarModel.AllItems.Add(nextButton);
|
||||
}
|
||||
```
|
||||
|
||||
### JavaScript API
|
||||
**File:** `WebUI/wwwroot/js/pdf-viewer.js`
|
||||
**JavaScript role after migration:**
|
||||
- Overlay geometry calculations only
|
||||
- Thumbnail rendering via PDF.js helper
|
||||
- Custom UI interactions (sidebar resize, signature canvas)
|
||||
- **NOT** for controlling DxPdfViewer page or zoom
|
||||
|
||||
```javascript
|
||||
window.pdfViewer = {
|
||||
initialize(canvasId, pdfDataUrl, dotNetRef),
|
||||
renderPage(num),
|
||||
renderSignatureButtons(signatures, pageNum, dotNetRef),
|
||||
applySignature(signatureId, dataUrl, fullName, position, place),
|
||||
zoomIn(), zoomOut(), dispose()
|
||||
}
|
||||
```
|
||||
See `DEVEXPRESS_V25_LIMITATIONS.md` for complete verified API reference.
|
||||
|
||||
### 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 — EnvelopeReceiver
|
||||
## Signature Workflow
|
||||
|
||||
**IMPORTANT:** iText7 NOT used (GPL license issue). Client-side overlay system only.
|
||||
Receiver signatures are handled as a **viewer overlay workflow**.
|
||||
|
||||
### Workflow Steps
|
||||
### 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.
|
||||
|
||||
1. **Page Load:**
|
||||
- Check `SignatureCacheService` for cached signature
|
||||
- If cached ? skip popup, load signature
|
||||
- If not ? show automatic popup (mandatory)
|
||||
### 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.
|
||||
|
||||
2. **Signature Popup (DxPopup):**
|
||||
- **Cannot close** (no X, no ESC, no outside-click)
|
||||
- **3 Tabs:** Draw (canvas) / Text (font select) / Image (upload)
|
||||
- **Required:** Full name, Place
|
||||
- **Optional:** Position
|
||||
- **Save ?** Store in `_capturedSignature`, cache via API
|
||||
|
||||
3. **Signature Buttons:**
|
||||
- Render purple "Unterschreiben" buttons at signature field positions
|
||||
- Coordinates: INCHES ? POINTS ? Pixels (scaled)
|
||||
- File: `pdf-viewer.js` ? `renderSignatureButtons()`
|
||||
|
||||
4. **Apply Signature (Click "Unterschreiben"):**
|
||||
- JS: Remove button, create HTML overlay
|
||||
- Format: Image + separator + text (Name, Position, Place, Date)
|
||||
- **NOT stamped on PDF bytes** (visual overlay only)
|
||||
|
||||
5. **Re-rendering:**
|
||||
- Zoom/Page change ? recalculate button positions
|
||||
- Session state: `_capturedSignature` (lost on refresh)
|
||||
|
||||
### Data Model
|
||||
**File:** `WebUI.Client/Models/SignatureCaptureDto.cs`
|
||||
### Signature DTO
|
||||
`EnvelopeGenerator.Server.Client/Models/SignatureCaptureDto.cs`
|
||||
|
||||
```csharp
|
||||
public sealed record SignatureCaptureDto {
|
||||
public required string DataUrl { get; init; } // base64 PNG
|
||||
public required string DataUrl { get; init; }
|
||||
public required string FullName { get; init; }
|
||||
public string Position { get; init; } = ""; // Optional
|
||||
public string Position { get; init; } = "";
|
||||
public required string Place { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Signature Caching
|
||||
## Signature Cache
|
||||
|
||||
**Purpose:** Persist signature across page refreshes (distributed cache: Redis/SQL)
|
||||
### Active cache model
|
||||
The current receiver page cache flow is handled directly in the server project through:
|
||||
- `EnvelopeReceiverPageDataService`
|
||||
- `IDistributedCache`
|
||||
- SQL Server distributed cache configuration from `Program.cs`
|
||||
|
||||
### API Endpoints
|
||||
**Controller:** `API/Controllers/CacheController.cs`
|
||||
### Cache key format
|
||||
Current server-side key prefix:
|
||||
- `envelope-generator.receiver-ui.signature:{receiverSignature}`
|
||||
|
||||
- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save
|
||||
- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load
|
||||
- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete
|
||||
This is different from an envelope-key-only cache convention.
|
||||
|
||||
**Cache Key Format:**
|
||||
```
|
||||
signature:91751687-8ae6-4777-bf5f-b8846085e62e:{envelopeKey}
|
||||
```
|
||||
### Config
|
||||
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/CacheOptions.cs`
|
||||
- section name: `Cache`
|
||||
- option: `SignatureCacheExpiration`
|
||||
|
||||
**Configuration:** `appsettings.json`
|
||||
```json
|
||||
{
|
||||
"Cache": {
|
||||
"SignatureCacheExpiration": null // or "02:00:00" for 2h
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service
|
||||
**File:** `WebUI.Client/Services/SignatureCacheService.cs`
|
||||
|
||||
```csharp
|
||||
public class SignatureCacheService {
|
||||
Task SaveSignatureAsync(string envelopeKey, SignatureCaptureDto signature);
|
||||
Task<SignatureCaptureDto?> GetSignatureAsync(string envelopeKey);
|
||||
Task DeleteSignatureAsync(string envelopeKey);
|
||||
}
|
||||
```
|
||||
|
||||
**Error Handling:** Fire-and-forget saves, graceful degradation on load failure.
|
||||
### Related controller
|
||||
A cache API controller also exists in:
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/CacheController.cs`
|
||||
|
||||
---
|
||||
|
||||
## Sender Login
|
||||
## Sender Dashboard
|
||||
|
||||
**Route:** `/sender/login`
|
||||
**File:** `WebUI.Client/Pages/LoginSenderPage.razor`
|
||||
**Tech:** Bootstrap 5 + DevExpress Blazing Berry theme
|
||||
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor`
|
||||
|
||||
### AuthService Extension
|
||||
**File:** `WebUI.Client/Services/AuthService.cs`
|
||||
Current behavior:
|
||||
- checks sender access through `AuthService.CheckSenderAccessAsync()`
|
||||
- redirects to `/sender/login` when unauthorized
|
||||
- loads envelope list through client `EnvelopeService`
|
||||
- separates envelopes into active/completed tabs
|
||||
- uses `DevExpress DxGrid`
|
||||
|
||||
```csharp
|
||||
public enum SenderLoginResult { Success, InvalidCredentials, Error }
|
||||
|
||||
public async Task<SenderLoginResult> LoginSenderAsync(string username, string password) {
|
||||
var response = await http.PostAsJsonAsync(
|
||||
$"{_api.BaseUrl}/api/auth?cookie=true",
|
||||
new { username, password });
|
||||
|
||||
return response.StatusCode switch {
|
||||
HttpStatusCode.OK => SenderLoginResult.Success,
|
||||
HttpStatusCode.Unauthorized => SenderLoginResult.InvalidCredentials,
|
||||
_ => SenderLoginResult.Error
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### API Integration
|
||||
**Endpoint:** `POST /api/auth?cookie=true`
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{ "username": "TekH", "password": "***" }
|
||||
```
|
||||
|
||||
**Response:**
|
||||
- `200 OK` ? Cookie set, redirect to `/sender`
|
||||
- `401 Unauthorized` ? Show error: "Ungültige Anmeldedaten"
|
||||
- Other ? Show error: "Serverfehler"
|
||||
|
||||
**Cookie:** HTTP-only, Secure (HTTPS), SameSite=Strict
|
||||
|
||||
### UI Flow
|
||||
1. User enters username + password
|
||||
2. Click "Anmelden" or press Enter
|
||||
3. Call `AuthService.LoginSenderAsync()`
|
||||
4. Success ? `Navigation.NavigateTo("/sender", forceLoad: true)`
|
||||
5. Error ? Display alert
|
||||
The sender page is active, but create/edit/delete actions are still marked with TODO behavior in the UI page.
|
||||
|
||||
---
|
||||
|
||||
## Receiver Login
|
||||
## Localization
|
||||
|
||||
**Route:** `/envelope/login/{EnvelopeKey}`
|
||||
**File:** `WebUI.Client/Pages/LoginReceiverPage.razor`
|
||||
Current server host localization setup in `Program.cs`:
|
||||
- supported cultures: `de-DE`, `en-US`
|
||||
- request localization middleware is enabled
|
||||
- `QueryStringRequestCultureProvider` is added
|
||||
- cookie-based localization services are registered via `AddCookieBasedLocalizer()`
|
||||
|
||||
**Multi-Envelope Support:** Cookies are stored per-envelope (e.g., `AuthTokenSignFLOWReceiver.{envelopeKey}`), allowing simultaneous authentication for multiple envelopes in the same browser session.
|
||||
|
||||
### AuthService Method
|
||||
```csharp
|
||||
public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
|
||||
|
||||
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string key, string accessCode) {
|
||||
var form = new MultipartFormDataContent();
|
||||
form.Add(new StringContent(accessCode), "AccessCode");
|
||||
|
||||
var response = await http.PostAsync(
|
||||
$"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(key)}", form);
|
||||
|
||||
return response.StatusCode switch {
|
||||
HttpStatusCode.OK => EnvelopeLoginResult.Success,
|
||||
HttpStatusCode.Unauthorized => EnvelopeLoginResult.InvalidCode,
|
||||
HttpStatusCode.NotFound => EnvelopeLoginResult.NotFound,
|
||||
_ => EnvelopeLoginResult.Error
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Success:** Redirect to `/envelope/{key}`
|
||||
Do not assume the old ReceiverUI-only `localStorage` culture approach is the current source of truth for the active host.
|
||||
|
||||
---
|
||||
|
||||
## NuGet Packages (WebUI.Client)
|
||||
## Coordinate System
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---|---|---|
|
||||
| `DevExpress.Blazor.*` | 25.2.3 | UI components (grids, popups, etc.) |
|
||||
| `SkiaSharp.*` | 3.119.1 | WASM rendering |
|
||||
| ~~`itext`~~ | ~~8.0.5~~ | **NOT USED** (GPL license) |
|
||||
### Source data
|
||||
Database signature coordinates are still based on:
|
||||
- **unit:** inches
|
||||
- **origin:** top-left
|
||||
- **axes:** X right, Y down
|
||||
|
||||
**External CDN:**
|
||||
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
||||
### Relevant conversions
|
||||
- inches -> PDF points: `x_pt = x_inches * 72`
|
||||
- inches -> DevExpress DX units: `x_dx = x_inches * 100`
|
||||
|
||||
### Current receiver page behavior
|
||||
The server page data service converts signature placeholders to **points** before sending them into the viewer workflow.
|
||||
|
||||
### Unit systems to keep in mind
|
||||
| System | Unit | Origin | Y-axis |
|
||||
|---|---|---|---|
|
||||
| Database | Inches | Top-left | Down |
|
||||
| PDF.js display | Pixels | Top-left | Down |
|
||||
| PDF points | Points | Depends on PDF model | Depends on consumer |
|
||||
| DevExpress DX | 1/100 inch style coordinates | Top-left-oriented usage in this app | Down-oriented usage |
|
||||
|
||||
---
|
||||
|
||||
## Mistakes History — Do NOT Repeat
|
||||
## Key Services and Files
|
||||
|
||||
| Mistake | Why Wrong |
|
||||
|---|---|
|
||||
| Using iText7 in EnvelopeReceiver | GPL license issue. Use overlay system instead. |
|
||||
| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. |
|
||||
| Hardcoded quality values in PDF.js | Use `appsettings.json` for configurability. |
|
||||
| Complex toolbar layouts | User wants simplicity. Keep horizontal layout. |
|
||||
| Over-designed UI (gradients/badges) | User prefers simple text labels. |
|
||||
| Ignoring "revert" instructions | Revert HTML structure, not just CSS. |
|
||||
| `BottomMarginBand` for signatures | Repeats on every page. Use DetailBand. |
|
||||
| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand. Calculate per-page. |
|
||||
### Client services
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureCacheService.cs`
|
||||
|
||||
### Server services
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
|
||||
|
||||
### Server config and host files
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
|
||||
|
||||
---
|
||||
|
||||
## Development Notes
|
||||
## Working Rules for This Workspace
|
||||
|
||||
### Deprecated Projects
|
||||
**DO NOT USE:**
|
||||
- `EnvelopeGenerator.ReceiverUI` (Pure Blazor WASM) — Migrated to WebUI (DevExpress compatibility issue)
|
||||
- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified WebUI
|
||||
- PSPDFKit — Removed, use PDF.js + DevExpress instead
|
||||
|
||||
### Legacy Projects (VB.NET)
|
||||
**DO NOT TOUCH:** `EnvelopeGenerator.Service`, `EnvelopeGenerator.Form`, `EnvelopeGenerator.BBTests`
|
||||
|
||||
### Signature Coordinate Evidence
|
||||
**File:** `EnvelopeGenerator.Form/frmFieldEditor.vb` (VB.NET)
|
||||
|
||||
```vb
|
||||
Private Const SIGNATURE_WIDTH As Single = 1.77 ' inches
|
||||
Private Const SIGNATURE_HEIGHT As Single = 1.96 ' inches
|
||||
|
||||
Sub LoadAnnotation(pElement As Signature, ...)
|
||||
oAnnotation.Left = CSng(pElement.X) ' Direct INCHES assignment
|
||||
oAnnotation.Top = CSng(pElement.Y)
|
||||
End Sub
|
||||
```
|
||||
|
||||
Proves database uses INCHES natively.
|
||||
- Treat `EnvelopeGenerator.Server` as the active main application host.
|
||||
- Treat `EnvelopeGenerator.Server.Client` as the active client UI project.
|
||||
- Prefer current `Server` / `Server.Client` paths over old `WebUI` / `ReceiverUI` references.
|
||||
- Do not use `EnvelopeGenerator.Web` or `EnvelopeGenerator.ReceiverUI` as the primary implementation target unless explicitly asked.
|
||||
- Do not modify the legacy VB.NET projects unless explicitly requested.
|
||||
- For receiver PDF/signature work, prefer the current `PDF.js`-based flow in `EnvelopeReceiverPage.razor`.
|
||||
- For DevExpress PDF viewer issues, remember server-side services are registered in `EnvelopeGenerator.Server`.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### When working with coordinates:
|
||||
1. **Database ? UI:** INCHES × 72 = PDF Points
|
||||
2. **UI ? Display:** Points × scale = Pixels
|
||||
3. **iText7 stamping:** Flip Y-axis (top-down ? bottom-up)
|
||||
|
||||
### When adding features:
|
||||
1. Check `Mistakes History` first
|
||||
2. Prefer simplicity over complexity
|
||||
3. Use `appsettings.json` for configuration
|
||||
4. Keep consistent with existing design (Bootstrap 5 + Blazing Berry)
|
||||
5. **Unified frontend:** WebUI serves both Senders and Receivers
|
||||
6. **Render mode:** Client-side (WASM) for login/dashboard, Server-side for PDF viewers
|
||||
|
||||
### When debugging:
|
||||
1. **Coordinates:** Always check unit system (inches/points/pixels)
|
||||
2. **Authentication:** Check cookie name/domain/SameSite
|
||||
3. **Cache:** Check Redis/SQL connection + key format
|
||||
4. **Frontend confusion:** Only use WebUI (ReceiverUI/Web are deprecated)
|
||||
5. **Blank DxPdfViewer:** Ensure page has `@rendermode InteractiveServer`
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-01-27 (ReceiverUI ? WebUI migration complete)
|
||||
**Last Updated:** 2026-06-29
|
||||
|
||||
300
DEBUG_NOTES.md
Normal file
300
DEBUG_NOTES.md
Normal file
@@ -0,0 +1,300 @@
|
||||
# Debug Tools for DevExpress DxPdfViewer Integration
|
||||
|
||||
## Purpose
|
||||
This document describes temporary debug tools added to diagnose DevExpress DOM structure and page navigation issues.
|
||||
|
||||
## IMPORTANT: TEMPORARY DEBUG CODE
|
||||
|
||||
**These debug tools are TEMPORARY and should be REMOVED after resolving the page navigation issue.**
|
||||
|
||||
---
|
||||
|
||||
## Debug Tools Added
|
||||
|
||||
### 1. Debug UI Button (Toolbar)
|
||||
|
||||
**Location:** `EnvelopeReceiverPage.razor` - Toolbar Section
|
||||
|
||||
**Visual:** Orange button with "?" icon in the PDF viewer toolbar
|
||||
|
||||
**What it does:**
|
||||
- Opens a floating overlay panel showing DevExpress DOM analysis
|
||||
- Displays all input elements found in DxPdfViewer
|
||||
- Shows which CSS selectors successfully find the page input
|
||||
- Provides a "Test: Go to Page 2" button for live testing
|
||||
|
||||
**Code Location:**
|
||||
```razor
|
||||
@* DEBUG: DevExpress DOM Inspector *@
|
||||
<div class="pdf-toolbar__section">
|
||||
<button class="pdf-toolbar__btn" @onclick="ShowDebugUI" ...>
|
||||
```
|
||||
|
||||
**C# Method:**
|
||||
```csharp
|
||||
async Task ShowDebugUI()
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("dxPdfViewerShowDebugUI");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. JavaScript Debug Functions
|
||||
|
||||
**Location:** `pdf-viewer.js`
|
||||
|
||||
**Functions Added:**
|
||||
|
||||
#### `window.dxPdfViewerDebugDOM()`
|
||||
- Console-based debug function
|
||||
- Logs detailed DOM analysis to browser console
|
||||
- Returns analysis object for programmatic inspection
|
||||
|
||||
#### `window.dxPdfViewerShowDebugUI()`
|
||||
- HTML overlay-based debug function
|
||||
- Creates visual debug panel without console interaction
|
||||
- No security warnings (no need to paste code)
|
||||
|
||||
---
|
||||
|
||||
## How to Use
|
||||
|
||||
### Step 1: Run Application
|
||||
```powershell
|
||||
dotnet run --project EnvelopeGenerator.Server/EnvelopeGenerator.Server
|
||||
```
|
||||
|
||||
### Step 2: Open Receiver Page
|
||||
Navigate to: `https://localhost:8088/envelope/{EnvelopeKey}`
|
||||
|
||||
### Step 3: Click Debug Button
|
||||
- Look for the **orange "?" button** in the PDF toolbar (left side, after thumbnails toggle)
|
||||
- Click it to open the debug overlay
|
||||
|
||||
### Step 4: Review Debug Information
|
||||
|
||||
The overlay shows:
|
||||
- **Total Inputs**: Number of input elements found
|
||||
- **Input Elements**: Details of each input (type, class, ID, value)
|
||||
- **Selector Tests**: Which CSS selectors work (✓) and which don't (✗)
|
||||
- **Toolbar**: Whether toolbar element was found
|
||||
- **DxWidget**: Whether DevExpress widget element was found
|
||||
|
||||
### Step 5: Test Page Navigation
|
||||
Click the **"Test: Go to Page 2"** button in the overlay
|
||||
|
||||
### Step 6: Report Results
|
||||
|
||||
**Copy the following information:**
|
||||
|
||||
1. **Total Inputs**: X
|
||||
2. **Input Details**: (type, className, id for each input)
|
||||
3. **Selector Test Results**: (which selectors show ✓ FOUND)
|
||||
4. **Test Result**: Did PDF actually navigate to page 2? (Yes/No)
|
||||
5. **Console Messages**: Any errors or warnings in F12 console
|
||||
|
||||
---
|
||||
|
||||
## What to Look For
|
||||
|
||||
### ✓ Success Indicators
|
||||
- At least one selector shows **✓ FOUND**
|
||||
- "Test: Go to Page 2" button actually changes PDF page
|
||||
- Console shows: `✓ Found page input with selector: "..."`
|
||||
|
||||
### ✗ Problem Indicators
|
||||
- All selectors show **✗ NOT FOUND**
|
||||
- "Test: Go to Page 2" does nothing
|
||||
- Console shows: `✗ Page input not found`
|
||||
|
||||
---
|
||||
|
||||
## After Diagnosis
|
||||
|
||||
Once the correct selector is identified:
|
||||
|
||||
### 1. Update `window.dxPdfViewerGoToPage()`
|
||||
Update the `selectors` array in `pdf-viewer.js` to prioritize the working selector:
|
||||
|
||||
```javascript
|
||||
const selectors = [
|
||||
'WORKING_SELECTOR_HERE', // ✓ Move this to top
|
||||
'input[type="number"]',
|
||||
// ... rest
|
||||
];
|
||||
```
|
||||
|
||||
### 2. Remove Debug Code
|
||||
|
||||
**Files to clean up:**
|
||||
|
||||
#### `EnvelopeReceiverPage.razor`
|
||||
Remove:
|
||||
```razor
|
||||
@* DEBUG: DevExpress DOM Inspector *@
|
||||
<div class="pdf-toolbar__section">
|
||||
<button class="pdf-toolbar__btn" @onclick="ShowDebugUI" ...>
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
Remove C# method:
|
||||
```csharp
|
||||
async Task ShowDebugUI() { ... }
|
||||
```
|
||||
|
||||
#### `pdf-viewer.js`
|
||||
Remove:
|
||||
```javascript
|
||||
// ⚠ AUTO-DEBUG: Display results in HTML overlay
|
||||
window.dxPdfViewerShowDebugUI = function() { ... }
|
||||
```
|
||||
|
||||
Keep:
|
||||
- `window.dxPdfViewerDebugDOM()` - can be useful for future debugging (optional)
|
||||
- `window.dxPdfViewerGoToPage()` - this is permanent (after fixing selector)
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Debug UI doesn't open
|
||||
- Check browser console (F12) for JavaScript errors
|
||||
- Ensure `pdf-viewer.js` is loaded
|
||||
- Verify DxPdfViewer has finished rendering
|
||||
|
||||
### "Page input not found" error
|
||||
- DevExpress may not have rendered toolbar yet
|
||||
- Try waiting 2-3 seconds after page load
|
||||
- Check if DxPdfViewer is visible on screen
|
||||
|
||||
### Selector works but page doesn't change
|
||||
- DevExpress may require different event sequence
|
||||
- Try adding more events (focus, click, etc.)
|
||||
- May need to find DevExpress client API instead
|
||||
|
||||
---
|
||||
|
||||
## SOLUTION: CustomizeToolbar + Manual State Tracking
|
||||
|
||||
**Identified root cause:**
|
||||
- DevExpress v25.2.3 has no event support
|
||||
- `PageNumberChanged` event does not exist
|
||||
- `ZoomLevelChanged` event does not exist
|
||||
- `ToolbarVisible` property does not exist
|
||||
- `GoToPageAsync()` method does not exist
|
||||
- Only `CustomizeToolbar` event is available
|
||||
|
||||
**Verified working API (v25.2.3):**
|
||||
- `DocumentContent` byte[] – for feeding PDF ✓
|
||||
- `ZoomLevel` double – zoom factor (1.5 = 150%) ✓
|
||||
- `IsSinglePagePreview` bool – single page mode ✓
|
||||
- `PageCount` int (GET only) – **replaces JS call** ✓
|
||||
- `ActivePageIndex` int (GET only) – current page index ✓
|
||||
- `CssClass`, `DocumentName`, `SizeMode` ✓
|
||||
|
||||
**Implemented strategy:**
|
||||
- Create custom navigation/zoom buttons via `CustomizeToolbar`
|
||||
- Manual state tracking with `_currentPage`, `_currentZoom`, `_viewerZoomLevel`
|
||||
- Manually trigger overlay refresh after button clicks
|
||||
- Replace JS getTotalPages() call with `_totalPages = _pdfViewer.PageCount`
|
||||
|
||||
**Correct code example:**
|
||||
|
||||
```csharp
|
||||
protected void OnCustomizeToolbar(ToolbarModel toolbarModel)
|
||||
{
|
||||
toolbarModel.AllItems.Clear();
|
||||
|
||||
var prevButton = new ToolbarItem
|
||||
{
|
||||
Text = "Previous",
|
||||
IconCssClass = "dx-icon-chevronprev",
|
||||
Enabled = _currentPage > 1,
|
||||
Click = async (args) =>
|
||||
{
|
||||
if (_currentPage > 1)
|
||||
{
|
||||
_currentPage--;
|
||||
_viewerZoomLevel = _currentZoom / 100d; // 150 -> 1.5
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await RenderSignatureButtonsAsync();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var nextButton = new ToolbarItem
|
||||
{
|
||||
Text = "Next",
|
||||
IconCssClass = "dx-icon-chevronnext",
|
||||
Enabled = _currentPage < _totalPages,
|
||||
Click = async (args) =>
|
||||
{
|
||||
if (_currentPage < _totalPages)
|
||||
{
|
||||
_currentPage++;
|
||||
_viewerZoomLevel = _currentZoom / 100d; // 150 -> 1.5
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await RenderSignatureButtonsAsync();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
toolbarModel.AllItems.Add(prevButton);
|
||||
toolbarModel.AllItems.Add(nextButton);
|
||||
}
|
||||
```
|
||||
|
||||
**PageCount usage (instead of JS):**
|
||||
|
||||
```csharp
|
||||
// In OnAfterRenderAsync
|
||||
if (_pdfViewer is not null && _pdfViewer.PageCount > 0)
|
||||
{
|
||||
_totalPages = _pdfViewer.PageCount; // JS getTotalPages() no longer needed
|
||||
_pdfLoaded = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
```
|
||||
|
||||
**Known limitations:**
|
||||
1. If user scrolls PDF, C# receives no notification, overlays may desync
|
||||
2. Thumbnail navigation only updates state, cannot move viewer
|
||||
3. Cross-page signature navigation limited without programmatic page switching
|
||||
|
||||
**See:** `DEVEXPRESS_V25_LIMITATIONS.md` – complete verified API reference
|
||||
|
||||
---
|
||||
|
||||
## Expected Timeline
|
||||
|
||||
1. ✓ **Day 1**: Add debug tools (DONE)
|
||||
2. ✓ **Day 1**: Collect DOM analysis data (DONE)
|
||||
3. ✓ **Day 1**: Identify root cause (DONE - v25.2.3 has no events)
|
||||
4. ✓ **Day 1**: Define workaround strategy (DONE - Custom toolbar with manual tracking)
|
||||
5. ✓ **Day 1**: Implement workaround (DONE)
|
||||
6. ⚠ **Day 2**: Test and document limitations
|
||||
7. ⚠ **Day 2**: Consider DevExpress upgrade or accept limitations
|
||||
|
||||
---
|
||||
|
||||
## Related Files
|
||||
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
|
||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
|
||||
- `RECEIVER_PDF_VIEWER_CONTEXT.md` (main context document - **UPDATED with new strategy**)
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Debug UI uses inline styles to avoid CSS conflicts
|
||||
- Overlay is positioned at `z-index: 99999` to appear above everything
|
||||
- Close button removes overlay from DOM completely
|
||||
- All debug output also goes to browser console for advanced inspection
|
||||
- **Debug findings led to complete strategy change - see RECEIVER_PDF_VIEWER_CONTEXT.md section 12-14**
|
||||
|
||||
---
|
||||
|
||||
**Remember: This is TEMPORARY debugging code. Delete after completing the new implementation strategy!**
|
||||
231
DEVEXPRESS_V25_LIMITATIONS.md
Normal file
231
DEVEXPRESS_V25_LIMITATIONS.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# DevExpress Blazor PdfViewer v25.2.3 - Verified API Reference
|
||||
|
||||
> **Source:** All information in this document has been verified from the actual source code of `DevExpress.Blazor.PdfViewer` v25.2.3 package.
|
||||
> AI-generated API suggestions (GoToPageAsync, PageNumberChanged, etc.) are NOT real – do not use them.
|
||||
|
||||
---
|
||||
|
||||
## Verified Available Parameters
|
||||
|
||||
| Property | Type | Access | Default | Description |
|
||||
|----------|------|--------|---------|-------------|
|
||||
| `DocumentContent` | `byte[]` | `[Parameter]` GET/SET | – | Feeds PDF content as byte array |
|
||||
| `CssClass` | `string` | `[Parameter]` GET/SET | – | Assigns CSS class to component |
|
||||
| `DocumentName` | `string` | `[Parameter]` GET/SET | `"Document"` | Download filename |
|
||||
| `IsSinglePagePreview` | `bool` | `[Parameter]` GET/SET | `false` | Single page mode |
|
||||
| `SizeMode` | `SizeMode?` | `[Parameter]` GET/SET | `null` | `Small`, `Medium`, `Large` |
|
||||
| `ZoomLevel` | `double` | `[Parameter]` GET/SET | `-1` | **Factor** (not percentage). `1.5` = 150% |
|
||||
| `ActivePageIndex` | `int` | GET only | – | Active page index (0-based). No SET. |
|
||||
| `PageCount` | `int` | GET only | – | Total page count in document |
|
||||
|
||||
---
|
||||
|
||||
## Available Events
|
||||
|
||||
- **`CustomizeToolbar`** – Allows toolbar customization
|
||||
- **`ZoomLevelChanged`** – Fires when ZoomLevel property changes (EventCallback<double>)
|
||||
|
||||
## Missing Events (NOT AVAILABLE in v25.2.3)
|
||||
|
||||
- **`PageNumberChanged`** / **`ActivePageIndexChanged`** – Not available
|
||||
- User scrolling or native toolbar page changes do not trigger C# code
|
||||
|
||||
---
|
||||
|
||||
## Missing Properties (NOT AVAILABLE in v25.2.3)
|
||||
|
||||
- **`ToolbarVisible`** – Not available (toolbar cannot be completely hidden)
|
||||
- **`ActivePageIndex` (settable)** – Read-only; no programmatic page navigation
|
||||
|
||||
---
|
||||
|
||||
## Missing Methods (NOT AVAILABLE in v25.2.3)
|
||||
|
||||
- **`GoToPageAsync()`** – Not available
|
||||
- **`GoToNextPageAsync()`** – Not available
|
||||
- **`ZoomAsync()`** – Not available
|
||||
|
||||
---
|
||||
|
||||
## Critical Integration Notes
|
||||
|
||||
### ZoomLevel takes factor, not percentage
|
||||
|
||||
```csharp
|
||||
// CORRECT
|
||||
_viewerZoomLevel = 1.5; // viewer displays "150%"
|
||||
_viewerZoomLevel = _currentZoom / 100d; // _currentZoom=150 -> 1.5
|
||||
|
||||
// WRONG
|
||||
_viewerZoomLevel = 150; // viewer displays "15000%"
|
||||
```
|
||||
|
||||
### PageCount replaces JS call
|
||||
|
||||
```csharp
|
||||
// CORRECT - read directly from component (no JS needed)
|
||||
_totalPages = _pdfViewer.PageCount;
|
||||
|
||||
// OLD method (no longer needed for this purpose)
|
||||
// _totalPages = await JSRuntime.InvokeAsync<int>("pdfViewer.getTotalPages");
|
||||
```
|
||||
|
||||
### ActivePageIndex is read-only
|
||||
|
||||
```csharp
|
||||
// CORRECT - read for state synchronization
|
||||
var currentPage = _pdfViewer.ActivePageIndex + 1; // convert to 1-based
|
||||
|
||||
// COMPILE ERROR - no setter
|
||||
// _pdfViewer.ActivePageIndex = 3; // COMPILE ERROR
|
||||
```
|
||||
|
||||
### DocumentContent byte[] feeding
|
||||
|
||||
```razor
|
||||
<DxPdfViewer @ref="_pdfViewer"
|
||||
DocumentContent="@_pdfDocumentContent"
|
||||
ZoomLevel="@_viewerZoomLevel"
|
||||
IsSinglePagePreview="true" />
|
||||
|
||||
@code {
|
||||
DxPdfViewer? _pdfViewer;
|
||||
byte[]? _pdfDocumentContent; // populate in OnInitializedAsync
|
||||
double _viewerZoomLevel = 1.5; // 150%
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Impact on EnvelopeReceiverPage
|
||||
|
||||
### Features That Don't Work
|
||||
1. **Event-driven overlay updates** – No page/zoom change events
|
||||
2. **Thumbnail click navigation** – Cannot navigate viewer to specific page via C# API
|
||||
3. **Cross-page signature navigation** – No programmatic page change API
|
||||
4. **Automatic overlay synchronization** – User scroll/native toolbar doesn't trigger C#
|
||||
|
||||
### Features That Work
|
||||
1. **ZoomLevel binding** – Custom zoom buttons can update viewer zoom
|
||||
2. **PageCount** – Total pages can be read directly from component
|
||||
3. **IsSinglePagePreview** – Single page mode works
|
||||
4. **DocumentContent** – byte[] feeding works perfectly
|
||||
5. **CustomizeToolbar** – Only way to add custom buttons to toolbar
|
||||
|
||||
---
|
||||
|
||||
## Workaround Strategy
|
||||
|
||||
CustomizeToolbar event is used to add custom navigation/zoom buttons.
|
||||
Manual state tracking (`_currentPage`, `_currentZoom`, `_viewerZoomLevel`) is kept in C#.
|
||||
Overlay refresh is manually triggered only after button clicks.
|
||||
|
||||
```csharp
|
||||
protected void OnCustomizeToolbar(ToolbarModel toolbarModel)
|
||||
{
|
||||
toolbarModel.AllItems.Clear();
|
||||
|
||||
var prevButton = new ToolbarItem
|
||||
{
|
||||
Text = "Previous",
|
||||
IconCssClass = "dx-icon-chevronprev",
|
||||
Enabled = _currentPage > 1,
|
||||
Click = async (args) =>
|
||||
{
|
||||
if (_currentPage > 1)
|
||||
{
|
||||
_currentPage--;
|
||||
_viewerZoomLevel = _currentZoom / 100d;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await RenderSignatureButtonsAsync();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var nextButton = new ToolbarItem
|
||||
{
|
||||
Text = "Next",
|
||||
IconCssClass = "dx-icon-chevronnext",
|
||||
Enabled = _currentPage < _totalPages,
|
||||
Click = async (args) =>
|
||||
{
|
||||
if (_currentPage < _totalPages)
|
||||
{
|
||||
_currentPage++;
|
||||
_viewerZoomLevel = _currentZoom / 100d;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await RenderSignatureButtonsAsync();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var zoomInButton = new ToolbarItem
|
||||
{
|
||||
IconCssClass = "dx-icon-plus",
|
||||
Enabled = _currentZoom < 300,
|
||||
Click = async (args) =>
|
||||
{
|
||||
_currentZoom = Math.Min(_currentZoom + 10, 300);
|
||||
_viewerZoomLevel = _currentZoom / 100d; // 150 -> 1.5
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await RenderSignatureButtonsAsync();
|
||||
}
|
||||
};
|
||||
|
||||
var zoomOutButton = new ToolbarItem
|
||||
{
|
||||
IconCssClass = "dx-icon-minus",
|
||||
Enabled = _currentZoom > 50,
|
||||
Click = async (args) =>
|
||||
{
|
||||
_currentZoom = Math.Max(_currentZoom - 10, 50);
|
||||
_viewerZoomLevel = _currentZoom / 100d; // 150 -> 1.5
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await RenderSignatureButtonsAsync();
|
||||
}
|
||||
};
|
||||
|
||||
toolbarModel.AllItems.Add(prevButton);
|
||||
toolbarModel.AllItems.Add(nextButton);
|
||||
toolbarModel.AllItems.Add(zoomInButton);
|
||||
toolbarModel.AllItems.Add(zoomOutButton);
|
||||
}
|
||||
```
|
||||
|
||||
### PageCount reading example (in OnAfterRenderAsync)
|
||||
|
||||
```csharp
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!_pdfLoaded && _pdfDocumentContent is { Length: > 0 })
|
||||
{
|
||||
await Task.Delay(300); // wait for DxPdfViewer to load
|
||||
|
||||
if (_pdfViewer is not null && _pdfViewer.PageCount > 0)
|
||||
{
|
||||
_totalPages = _pdfViewer.PageCount; // read directly instead of JS
|
||||
_pdfLoaded = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await RenderThumbnailsAsync();
|
||||
await RenderSignatureButtonsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Known Acceptable Limitations
|
||||
|
||||
1. If user scrolls PDF, C# `_currentPage` does not synchronize
|
||||
2. Thumbnail clicks update state but cannot move DevExpress viewer to target page
|
||||
3. Browser zoom gestures do not trigger overlay updates
|
||||
4. Custom toolbar buttons correctly trigger overlay updates
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- DevExpress official documentation: https://docs.devexpress.com/Blazor/DevExpress.Blazor.PdfViewer.DxPdfViewer
|
||||
- Verified package: `DevExpress.Blazor.PdfViewer` v25.2.3
|
||||
- **Note:** AI-suggested APIs (GoToPageAsync, PageNumberChanged, ActivePageIndexChanged, ZoomAsync, ToolbarVisible) are NOT real. Do not use.
|
||||
@@ -40,7 +40,7 @@ public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions,
|
||||
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</response>
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
[HttpPost("logout")]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
@@ -69,7 +69,7 @@ public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions,
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||
[HttpGet("check")]
|
||||
[Authorize]
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
public IActionResult Check(string? role = null)
|
||||
=> role is not null && !User.IsInRole(role)
|
||||
? Unauthorized()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -13,7 +14,7 @@ namespace EnvelopeGenerator.API.Controllers;
|
||||
/// </remarks>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
|
||||
{
|
||||
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
|
||||
|
||||
@@ -15,14 +15,14 @@ namespace EnvelopeGenerator.API.Controllers;
|
||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class SignatureController : ControllerBase
|
||||
public class DocReceiverElementController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="SignatureController"/>.
|
||||
/// Initializes a new instance of <see cref="DocReceiverElementController"/>.
|
||||
/// </summary>
|
||||
public SignatureController(IMediator mediator)
|
||||
public DocReceiverElementController(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
@@ -34,6 +34,7 @@
|
||||
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
|
||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.28" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
|
||||
<PackageReference Include="itext" Version="8.0.5" />
|
||||
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
|
||||
|
||||
@@ -13,7 +13,6 @@ using EnvelopeGenerator.Application;
|
||||
using DigitalData.Auth.Client;
|
||||
using DigitalData.Core.Abstractions;
|
||||
using EnvelopeGenerator.API.Models;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using DigitalData.Core.Abstractions.Security.Extensions;
|
||||
using EnvelopeGenerator.API.Middleware;
|
||||
@@ -22,6 +21,7 @@ using NLog.Web;
|
||||
using NLog;
|
||||
using DigitalData.Auth.Claims;
|
||||
using EnvelopeGenerator.API;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
|
||||
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||
logger.Info("Logging initialized!");
|
||||
@@ -44,7 +44,11 @@ try
|
||||
|
||||
var deferredProvider = new DeferredServiceProvider();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddControllers()
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
|
||||
});
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
||||
|
||||
@@ -238,8 +242,9 @@ try
|
||||
});
|
||||
|
||||
builder.Services.AddAuthorizationBuilder()
|
||||
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy.RequireRole(Role.Sender, Role.Receiver.Full))
|
||||
|
||||
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy
|
||||
.RequireRole(Role.Sender, Role.Receiver.Full)
|
||||
.AddAuthenticationSchemes(AuthScheme.Sender, AuthScheme.Receiver))
|
||||
.AddPolicy(AuthPolicy.Sender, policy => policy
|
||||
.RequireRole(Role.Sender)
|
||||
.AddAuthenticationSchemes(AuthScheme.Sender))
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"launchUrl": "swagger",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "sender",
|
||||
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing configuration settings.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class ConfigDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class DocReceiverElementDto : IDocReceiverElement
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class DocumentDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing the status of a document for a specific receiver.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class DocumentStatusDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||
using DigitalData.UserManager.Application.DTOs.User;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Application.Common.Dto.History;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using EnvelopeGenerator.Domain.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record EnvelopeDto : IEnvelope
|
||||
{
|
||||
/// <summary>
|
||||
@@ -133,5 +129,10 @@ public record EnvelopeDto : IEnvelope
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public IEnumerable<ReceiverDto>? Receivers { get; set; }
|
||||
public IEnumerable<EnvelopeReceiverDto>? EnvelopeReceivers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Envelope history entries tracking actions like DocumentSigned, EnvelopeOpened, etc.
|
||||
/// </summary>
|
||||
public IEnumerable<HistoryDto>? Histories { get; set; }
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record EnvelopeReceiverDto
|
||||
{
|
||||
/// <summary>
|
||||
@@ -75,4 +74,13 @@ public record EnvelopeReceiverDto
|
||||
///
|
||||
/// </summary>
|
||||
public bool HasPhoneNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether this receiver has signed the envelope.
|
||||
/// Checks if there is a DocumentSigned history entry for this receiver in the envelope's history.
|
||||
/// </summary>
|
||||
public bool Signed => Envelope?.Histories?.Any(h =>
|
||||
h.Receiver?.Id == ReceiverId &&
|
||||
h.Status == EnvelopeStatus.DocumentSigned
|
||||
) ?? false;
|
||||
}
|
||||
@@ -1,11 +1,8 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
@@ -8,7 +7,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="DateValid"></param>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record EnvelopeReceiverReadOnlyCreateDto(
|
||||
DateTime DateValid)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
|
||||
@@ -8,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
/// Represents a read-only Data Transfer Object (DTO) for an envelope receiver.
|
||||
/// Contains information about the receiver, associated envelope, and audit details.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class EnvelopeReceiverReadOnlyDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for updating a read-only envelope receiver.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class EnvelopeReceiverReadOnlyUpdateDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class EnvelopeTypeDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class GtxMessagingResponse : Dictionary<string, object?> { }
|
||||
@@ -1,11 +1,8 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record SmsResponse
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
@@ -7,7 +6,6 @@ namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class ReceiverDto
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -7,7 +7,9 @@ using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Application.Common.Extensions;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the AutoMapper profile configuration for mapping between
|
||||
@@ -26,15 +28,15 @@ public class MappingProfile : Profile
|
||||
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
||||
CreateMap<DocumentStatus, DocumentStatusDto>();
|
||||
CreateMap<EmailTemplate, EmailTemplateDto>();
|
||||
CreateMap<Envelope, EnvelopeDto>().ForMember(dest => dest.Receivers, opt => opt.MapFrom(src => src.EnvelopeReceivers.Select(er => er.Receiver)));
|
||||
CreateMap<Envelope, EnvelopeDto>();
|
||||
CreateMap<Document, DocumentDto>();
|
||||
CreateMap<Domain.Entities.History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||
CreateMap<Domain.Entities.History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
||||
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||
CreateMap<History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||
CreateMap<EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
||||
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
||||
CreateMap<Domain.Entities.Receiver, ReceiverDto>();
|
||||
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
||||
CreateMap<Receiver, ReceiverDto>();
|
||||
CreateMap<EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
||||
CreateMap<ElementAnnotation, AnnotationDto>();
|
||||
|
||||
// DTO to Entity mappings
|
||||
@@ -47,13 +49,13 @@ public class MappingProfile : Profile
|
||||
CreateMap<EmailTemplateDto, EmailTemplate>();
|
||||
CreateMap<EnvelopeDto, Envelope>();
|
||||
CreateMap<DocumentDto, Document>();
|
||||
CreateMap<HistoryDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||
CreateMap<HistoryCreateDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||
CreateMap<EnvelopeReceiverDto, Domain.Entities.EnvelopeReceiver>();
|
||||
CreateMap<HistoryDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||
CreateMap<HistoryCreateDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||
CreateMap<EnvelopeReceiverDto, EnvelopeReceiver>();
|
||||
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
||||
CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
||||
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
||||
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
||||
CreateMap<ReceiverDto, Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
||||
CreateMap<EnvelopeReceiverReadOnlyCreateDto, EnvelopeReceiverReadOnly>();
|
||||
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, EnvelopeReceiverReadOnly>();
|
||||
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
||||
.MapAddedWhen();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\Model.Designer.vb" />
|
||||
</ItemGroup>
|
||||
@@ -21,7 +21,6 @@
|
||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.18" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||
@@ -79,25 +78,25 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||
<PackageReference Include="CommandDotNet">
|
||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||
<PackageReference Include="CommandDotNet">
|
||||
<Version>7.0.5</Version>
|
||||
</PackageReference>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||
<PackageReference Include="CommandDotNet">
|
||||
<Version>8.1.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||
<PackageReference Include="CommandDotNet">
|
||||
<Version>8.1.1</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||
<PackageReference Include="CommandDotNet">
|
||||
<Version>8.1.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||
<PackageReference Include="CommandDotNet">
|
||||
<Version>8.1.1</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -14,6 +14,16 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
||||
/// </summary>
|
||||
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool OnlyActive { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool OnlyCompleted { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Abfrage des Include des Umschlags
|
||||
/// </summary>
|
||||
@@ -132,6 +142,12 @@ public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnum
|
||||
query = query.Where(e => !status.Ignore.Contains(e.Status));
|
||||
}
|
||||
|
||||
if(request is { OnlyActive: true })
|
||||
query = query.Where(e => Status.Active.Contains(e.Status));
|
||||
|
||||
if (request is { OnlyCompleted: true })
|
||||
query = query.Where(e => Status.Completed.Contains(e.Status));
|
||||
|
||||
var envelopes = await query
|
||||
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
||||
.ToListAsync(cancel);
|
||||
|
||||
@@ -3,7 +3,6 @@ using DigitalData.Core.Abstraction.Application.Repository;
|
||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||
using EnvelopeGenerator.Domain.Entities;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Cryptography;
|
||||
@@ -14,7 +13,6 @@ namespace EnvelopeGenerator.Application.Receivers.Commands;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public record CreateReceiverCommand : IRequest<(ReceiverDto Receiver, bool AlreadyExists)>
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace EnvelopeGenerator.Application.Receivers.Commands;
|
||||
namespace EnvelopeGenerator.Application.Receivers.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Data Transfer Object for updating a receiver's information.
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class UpdateReceiverCommand
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -397,4 +397,412 @@ public static class Extensions
|
||||
/// <param name="suffix"></param>
|
||||
/// <returns></returns>
|
||||
public static string LockedFooterBody(this IStringLocalizer localizer, string suffix) => localizer[nameof(LockedFooterBody) + suffix].Value;
|
||||
|
||||
// Sender-side UI resources
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string NewEnvelope(this IStringLocalizer localizer) => localizer[nameof(NewEnvelope)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string LoadEnvelope(this IStringLocalizer localizer) => localizer[nameof(LoadEnvelope)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string DeleteEnvelope(this IStringLocalizer localizer) => localizer[nameof(DeleteEnvelope)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string RefreshData(this IStringLocalizer localizer) => localizer[nameof(RefreshData)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string RefreshedAt(this IStringLocalizer localizer) => localizer[nameof(RefreshedAt)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string ShowDocument(this IStringLocalizer localizer) => localizer[nameof(ShowDocument)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string ContactReceiver(this IStringLocalizer localizer) => localizer[nameof(ContactReceiver)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string EnvelopeId(this IStringLocalizer localizer) => localizer[nameof(EnvelopeId)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string OpenLogDirectory(this IStringLocalizer localizer) => localizer[nameof(OpenLogDirectory)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string ShowResultsReport(this IStringLocalizer localizer) => localizer[nameof(ShowResultsReport)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string SupportMail(this IStringLocalizer localizer) => localizer[nameof(SupportMail)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string ResendInvitation(this IStringLocalizer localizer) => localizer[nameof(ResendInvitation)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string Export(this IStringLocalizer localizer) => localizer[nameof(Export)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string Receivers(this IStringLocalizer localizer) => localizer[nameof(Receivers)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string EmailSalutation(this IStringLocalizer localizer) => localizer[nameof(EmailSalutation)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string SignedWhen(this IStringLocalizer localizer) => localizer[nameof(SignedWhen)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string AccessCode(this IStringLocalizer localizer) => localizer[nameof(AccessCode)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string User(this IStringLocalizer localizer) => localizer[nameof(User)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string Type(this IStringLocalizer localizer) => localizer[nameof(Type)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string Title(this IStringLocalizer localizer) => localizer[nameof(Title)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string CreatedOn(this IStringLocalizer localizer) => localizer[nameof(CreatedOn)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string LastModified(this IStringLocalizer localizer) => localizer[nameof(LastModified)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string OpenEnvelopes(this IStringLocalizer localizer) => localizer[nameof(OpenEnvelopes)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string CompletedEnvelopes(this IStringLocalizer localizer) => localizer[nameof(CompletedEnvelopes)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string SendAccessCode(this IStringLocalizer localizer) => localizer[nameof(SendAccessCode)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string TwoFactorProperties(this IStringLocalizer localizer) => localizer[nameof(TwoFactorProperties)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string Name(this IStringLocalizer localizer) => localizer[nameof(Name)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string PhoneNumber(this IStringLocalizer localizer) => localizer[nameof(PhoneNumber)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string AddReceiver(this IStringLocalizer localizer) => localizer[nameof(AddReceiver)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string DeleteReceiver(this IStringLocalizer localizer) => localizer[nameof(DeleteReceiver)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string AddFile(this IStringLocalizer localizer) => localizer[nameof(AddFile)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string MergeFiles(this IStringLocalizer localizer) => localizer[nameof(MergeFiles)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string DeleteFile(this IStringLocalizer localizer) => localizer[nameof(DeleteFile)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string ShowFile(this IStringLocalizer localizer) => localizer[nameof(ShowFile)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string EditFields(this IStringLocalizer localizer) => localizer[nameof(EditFields)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string EditData(this IStringLocalizer localizer) => localizer[nameof(EditData)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string Save(this IStringLocalizer localizer) => localizer[nameof(Save)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string SendEnvelope(this IStringLocalizer localizer) => localizer[nameof(SendEnvelope)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string Cancel(this IStringLocalizer localizer) => localizer[nameof(Cancel)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string AddSignature(this IStringLocalizer localizer) => localizer[nameof(AddSignature)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string DeleteSignature(this IStringLocalizer localizer) => localizer[nameof(DeleteSignature)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string Language(this IStringLocalizer localizer) => localizer[nameof(Language)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string UseAccessCode(this IStringLocalizer localizer) => localizer[nameof(UseAccessCode)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string TwoFactorEnabled(this IStringLocalizer localizer) => localizer[nameof(TwoFactorEnabled)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string CertificationType(this IStringLocalizer localizer) => localizer[nameof(CertificationType)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string FinalEmailToCreator(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToCreator)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string FinalEmailToReceivers(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToReceivers)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string SendReminderEmails(this IStringLocalizer localizer) => localizer[nameof(SendReminderEmails)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string FirstReminderDays(this IStringLocalizer localizer) => localizer[nameof(FirstReminderDays)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string ReminderIntervalDays(this IStringLocalizer localizer) => localizer[nameof(ReminderIntervalDays)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string ExpiresWhenDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWhenDays)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string ExpiresWarningDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWarningDays)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string Message(this IStringLocalizer localizer) => localizer[nameof(Message)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string EnvelopeType(this IStringLocalizer localizer) => localizer[nameof(EnvelopeType)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string AllOptions(this IStringLocalizer localizer) => localizer[nameof(AllOptions)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string DeleteReason(this IStringLocalizer localizer) => localizer[nameof(DeleteReason)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string PleaseProvideReason(this IStringLocalizer localizer) => localizer[nameof(PleaseProvideReason)].Value;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="localizer"></param>
|
||||
/// <returns></returns>
|
||||
public static string Status(this IStringLocalizer localizer) => localizer[nameof(Status)].Value;
|
||||
}
|
||||
@@ -477,4 +477,178 @@
|
||||
<data name="Confirmations" xml:space="preserve">
|
||||
<value>Bestätigungen</value>
|
||||
</data>
|
||||
<data name="NewEnvelope" xml:space="preserve">
|
||||
<value>Neuer Umschlag</value>
|
||||
</data>
|
||||
<data name="LoadEnvelope" xml:space="preserve">
|
||||
<value>Umschlag laden</value>
|
||||
</data>
|
||||
<data name="DeleteEnvelope" xml:space="preserve">
|
||||
<value>Umschlag zurückrufen/löschen</value>
|
||||
</data>
|
||||
<data name="RefreshData" xml:space="preserve">
|
||||
<value>Daten Aktualisieren</value>
|
||||
</data>
|
||||
<data name="RefreshedAt" xml:space="preserve">
|
||||
<value>Aktualisiert: {0}</value>
|
||||
</data>
|
||||
<data name="ShowDocument" xml:space="preserve">
|
||||
<value>Dokument anzeigen</value>
|
||||
</data>
|
||||
<data name="ContactReceiver" xml:space="preserve">
|
||||
<value>Empfänger kontaktieren</value>
|
||||
</data>
|
||||
<data name="EnvelopeId" xml:space="preserve">
|
||||
<value>Umschlag-ID: {0}</value>
|
||||
</data>
|
||||
<data name="OpenLogDirectory" xml:space="preserve">
|
||||
<value>Öffne Log Verzeichnis</value>
|
||||
</data>
|
||||
<data name="ShowResultsReport" xml:space="preserve">
|
||||
<value>Ergebnisbericht anzeigen</value>
|
||||
</data>
|
||||
<data name="SupportMail" xml:space="preserve">
|
||||
<value>Support Mail</value>
|
||||
</data>
|
||||
<data name="ResendInvitation" xml:space="preserve">
|
||||
<value>Einladung manuell versenden</value>
|
||||
</data>
|
||||
<data name="Export" xml:space="preserve">
|
||||
<value>Export</value>
|
||||
</data>
|
||||
<data name="Receivers" xml:space="preserve">
|
||||
<value>Empfänger</value>
|
||||
</data>
|
||||
<data name="EmailSalutation" xml:space="preserve">
|
||||
<value>Email Anrede</value>
|
||||
</data>
|
||||
<data name="SignedWhen" xml:space="preserve">
|
||||
<value>Unterschrieben wann</value>
|
||||
</data>
|
||||
<data name="AccessCode" xml:space="preserve">
|
||||
<value>Zugangscode</value>
|
||||
</data>
|
||||
<data name="User" xml:space="preserve">
|
||||
<value>Benutzer</value>
|
||||
</data>
|
||||
<data name="Type" xml:space="preserve">
|
||||
<value>Typ</value>
|
||||
</data>
|
||||
<data name="Title" xml:space="preserve">
|
||||
<value>Titel</value>
|
||||
</data>
|
||||
<data name="CreatedOn" xml:space="preserve">
|
||||
<value>Erstellt am</value>
|
||||
</data>
|
||||
<data name="LastModified" xml:space="preserve">
|
||||
<value>Zuletzt geändert am</value>
|
||||
</data>
|
||||
<data name="OpenEnvelopes" xml:space="preserve">
|
||||
<value>Offene Umschläge</value>
|
||||
</data>
|
||||
<data name="CompletedEnvelopes" xml:space="preserve">
|
||||
<value>Abgeschlossene Umschläge</value>
|
||||
</data>
|
||||
<data name="SendAccessCode" xml:space="preserve">
|
||||
<value>Zugangscode senden</value>
|
||||
</data>
|
||||
<data name="TwoFactorProperties" xml:space="preserve">
|
||||
<value>2-Faktor Eigenschaften</value>
|
||||
</data>
|
||||
<data name="Name" xml:space="preserve">
|
||||
<value>Name</value>
|
||||
</data>
|
||||
<data name="PhoneNumber" xml:space="preserve">
|
||||
<value>Telefonnummer</value>
|
||||
</data>
|
||||
<data name="AddReceiver" xml:space="preserve">
|
||||
<value>Empfänger hinzufügen</value>
|
||||
</data>
|
||||
<data name="DeleteReceiver" xml:space="preserve">
|
||||
<value>Empfänger löschen</value>
|
||||
</data>
|
||||
<data name="AddFile" xml:space="preserve">
|
||||
<value>Datei hinzufügen</value>
|
||||
</data>
|
||||
<data name="MergeFiles" xml:space="preserve">
|
||||
<value>Dateien zusammenführen</value>
|
||||
</data>
|
||||
<data name="DeleteFile" xml:space="preserve">
|
||||
<value>Datei löschen</value>
|
||||
</data>
|
||||
<data name="ShowFile" xml:space="preserve">
|
||||
<value>Datei anzeigen</value>
|
||||
</data>
|
||||
<data name="EditFields" xml:space="preserve">
|
||||
<value>Felder bearbeiten</value>
|
||||
</data>
|
||||
<data name="EditData" xml:space="preserve">
|
||||
<value>Daten bearbeiten</value>
|
||||
</data>
|
||||
<data name="Save" xml:space="preserve">
|
||||
<value>Speichern</value>
|
||||
</data>
|
||||
<data name="SendEnvelope" xml:space="preserve">
|
||||
<value>Umschlag versenden</value>
|
||||
</data>
|
||||
<data name="Cancel" xml:space="preserve">
|
||||
<value>Abbrechen</value>
|
||||
</data>
|
||||
<data name="AddSignature" xml:space="preserve">
|
||||
<value>Signatur hinzufügen</value>
|
||||
</data>
|
||||
<data name="DeleteSignature" xml:space="preserve">
|
||||
<value>Signatur löschen</value>
|
||||
</data>
|
||||
<data name="Language" xml:space="preserve">
|
||||
<value>Sprache</value>
|
||||
</data>
|
||||
<data name="UseAccessCode" xml:space="preserve">
|
||||
<value>Zugangscode verwenden</value>
|
||||
</data>
|
||||
<data name="TwoFactorEnabled" xml:space="preserve">
|
||||
<value>2-Faktor-Authentifizierung aktiviert</value>
|
||||
</data>
|
||||
<data name="CertificationType" xml:space="preserve">
|
||||
<value>Zertifizierungstyp</value>
|
||||
</data>
|
||||
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||
<value>Finale E-Mail an Ersteller</value>
|
||||
</data>
|
||||
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||
<value>Finale E-Mail an Empfänger</value>
|
||||
</data>
|
||||
<data name="SendReminderEmails" xml:space="preserve">
|
||||
<value>Erinnerungs-E-Mails senden</value>
|
||||
</data>
|
||||
<data name="FirstReminderDays" xml:space="preserve">
|
||||
<value>Erste Erinnerung (Tage)</value>
|
||||
</data>
|
||||
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||
<value>Erinnerungsintervall (Tage)</value>
|
||||
</data>
|
||||
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||
<value>Läuft ab nach (Tage)</value>
|
||||
</data>
|
||||
<data name="ExpiresWarningDays" xml:space="preserve">
|
||||
<value>Ablaufwarnung (Tage)</value>
|
||||
</data>
|
||||
<data name="Message" xml:space="preserve">
|
||||
<value>Nachricht</value>
|
||||
</data>
|
||||
<data name="EnvelopeType" xml:space="preserve">
|
||||
<value>Umschlagtyp</value>
|
||||
</data>
|
||||
<data name="AllOptions" xml:space="preserve">
|
||||
<value>Alle Optionen</value>
|
||||
</data>
|
||||
<data name="DeleteReason" xml:space="preserve">
|
||||
<value>Grund für Löschung</value>
|
||||
</data>
|
||||
<data name="PleaseProvideReason" xml:space="preserve">
|
||||
<value>Bitte geben Sie einen Grund an</value>
|
||||
</data>
|
||||
<data name="Status" xml:space="preserve">
|
||||
<value>Status</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -477,4 +477,178 @@
|
||||
<data name="Confirmations" xml:space="preserve">
|
||||
<value>Confirmations</value>
|
||||
</data>
|
||||
<data name="NewEnvelope" xml:space="preserve">
|
||||
<value>New Envelope</value>
|
||||
</data>
|
||||
<data name="LoadEnvelope" xml:space="preserve">
|
||||
<value>Load Envelope</value>
|
||||
</data>
|
||||
<data name="DeleteEnvelope" xml:space="preserve">
|
||||
<value>Delete Envelope</value>
|
||||
</data>
|
||||
<data name="RefreshData" xml:space="preserve">
|
||||
<value>Reload Data</value>
|
||||
</data>
|
||||
<data name="RefreshedAt" xml:space="preserve">
|
||||
<value>Refreshed: {0}</value>
|
||||
</data>
|
||||
<data name="ShowDocument" xml:space="preserve">
|
||||
<value>Show Document</value>
|
||||
</data>
|
||||
<data name="ContactReceiver" xml:space="preserve">
|
||||
<value>Contact Receiver</value>
|
||||
</data>
|
||||
<data name="EnvelopeId" xml:space="preserve">
|
||||
<value>Envelope-ID: {0}</value>
|
||||
</data>
|
||||
<data name="OpenLogDirectory" xml:space="preserve">
|
||||
<value>Open Log Directory</value>
|
||||
</data>
|
||||
<data name="ShowResultsReport" xml:space="preserve">
|
||||
<value>Show Results Report</value>
|
||||
</data>
|
||||
<data name="SupportMail" xml:space="preserve">
|
||||
<value>Support Mail</value>
|
||||
</data>
|
||||
<data name="ResendInvitation" xml:space="preserve">
|
||||
<value>Send Invitation Again</value>
|
||||
</data>
|
||||
<data name="Export" xml:space="preserve">
|
||||
<value>Export</value>
|
||||
</data>
|
||||
<data name="Receivers" xml:space="preserve">
|
||||
<value>Receivers</value>
|
||||
</data>
|
||||
<data name="EmailSalutation" xml:space="preserve">
|
||||
<value>Email Salutation</value>
|
||||
</data>
|
||||
<data name="SignedWhen" xml:space="preserve">
|
||||
<value>Signed When</value>
|
||||
</data>
|
||||
<data name="AccessCode" xml:space="preserve">
|
||||
<value>Access Code</value>
|
||||
</data>
|
||||
<data name="User" xml:space="preserve">
|
||||
<value>User</value>
|
||||
</data>
|
||||
<data name="Type" xml:space="preserve">
|
||||
<value>Type</value>
|
||||
</data>
|
||||
<data name="Title" xml:space="preserve">
|
||||
<value>Title</value>
|
||||
</data>
|
||||
<data name="CreatedOn" xml:space="preserve">
|
||||
<value>Created On</value>
|
||||
</data>
|
||||
<data name="LastModified" xml:space="preserve">
|
||||
<value>Last Modified</value>
|
||||
</data>
|
||||
<data name="OpenEnvelopes" xml:space="preserve">
|
||||
<value>Open Envelopes</value>
|
||||
</data>
|
||||
<data name="CompletedEnvelopes" xml:space="preserve">
|
||||
<value>Completed Envelopes</value>
|
||||
</data>
|
||||
<data name="SendAccessCode" xml:space="preserve">
|
||||
<value>Send Access Code</value>
|
||||
</data>
|
||||
<data name="TwoFactorProperties" xml:space="preserve">
|
||||
<value>2-Factor Properties</value>
|
||||
</data>
|
||||
<data name="Name" xml:space="preserve">
|
||||
<value>Name</value>
|
||||
</data>
|
||||
<data name="PhoneNumber" xml:space="preserve">
|
||||
<value>Phone Number</value>
|
||||
</data>
|
||||
<data name="AddReceiver" xml:space="preserve">
|
||||
<value>Add Receiver</value>
|
||||
</data>
|
||||
<data name="DeleteReceiver" xml:space="preserve">
|
||||
<value>Delete Receiver</value>
|
||||
</data>
|
||||
<data name="AddFile" xml:space="preserve">
|
||||
<value>Add File</value>
|
||||
</data>
|
||||
<data name="MergeFiles" xml:space="preserve">
|
||||
<value>Merge Files</value>
|
||||
</data>
|
||||
<data name="DeleteFile" xml:space="preserve">
|
||||
<value>Delete File</value>
|
||||
</data>
|
||||
<data name="ShowFile" xml:space="preserve">
|
||||
<value>Show File</value>
|
||||
</data>
|
||||
<data name="EditFields" xml:space="preserve">
|
||||
<value>Edit Fields</value>
|
||||
</data>
|
||||
<data name="EditData" xml:space="preserve">
|
||||
<value>Edit Data</value>
|
||||
</data>
|
||||
<data name="Save" xml:space="preserve">
|
||||
<value>Save</value>
|
||||
</data>
|
||||
<data name="SendEnvelope" xml:space="preserve">
|
||||
<value>Send Envelope</value>
|
||||
</data>
|
||||
<data name="Cancel" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
</data>
|
||||
<data name="AddSignature" xml:space="preserve">
|
||||
<value>Add Signature</value>
|
||||
</data>
|
||||
<data name="DeleteSignature" xml:space="preserve">
|
||||
<value>Delete Signature</value>
|
||||
</data>
|
||||
<data name="Language" xml:space="preserve">
|
||||
<value>Language</value>
|
||||
</data>
|
||||
<data name="UseAccessCode" xml:space="preserve">
|
||||
<value>Use Access Code</value>
|
||||
</data>
|
||||
<data name="TwoFactorEnabled" xml:space="preserve">
|
||||
<value>2-Factor Authentication Enabled</value>
|
||||
</data>
|
||||
<data name="CertificationType" xml:space="preserve">
|
||||
<value>Certification Type</value>
|
||||
</data>
|
||||
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||
<value>Final Email to Creator</value>
|
||||
</data>
|
||||
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||
<value>Final Email to Receivers</value>
|
||||
</data>
|
||||
<data name="SendReminderEmails" xml:space="preserve">
|
||||
<value>Send Reminder Emails</value>
|
||||
</data>
|
||||
<data name="FirstReminderDays" xml:space="preserve">
|
||||
<value>First Reminder (Days)</value>
|
||||
</data>
|
||||
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||
<value>Reminder Interval (Days)</value>
|
||||
</data>
|
||||
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||
<value>Expires After (Days)</value>
|
||||
</data>
|
||||
<data name="ExpiresWarningDays" xml:space="preserve">
|
||||
<value>Expiry Warning (Days)</value>
|
||||
</data>
|
||||
<data name="Message" xml:space="preserve">
|
||||
<value>Message</value>
|
||||
</data>
|
||||
<data name="EnvelopeType" xml:space="preserve">
|
||||
<value>Envelope Type</value>
|
||||
</data>
|
||||
<data name="AllOptions" xml:space="preserve">
|
||||
<value>All Options</value>
|
||||
</data>
|
||||
<data name="DeleteReason" xml:space="preserve">
|
||||
<value>Deletion Reason</value>
|
||||
</data>
|
||||
<data name="PleaseProvideReason" xml:space="preserve">
|
||||
<value>Please provide a reason</value>
|
||||
</data>
|
||||
<data name="Status" xml:space="preserve">
|
||||
<value>Status</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -477,4 +477,178 @@
|
||||
<data name="Confirmations" xml:space="preserve">
|
||||
<value>Confirmations</value>
|
||||
</data>
|
||||
<data name="NewEnvelope" xml:space="preserve">
|
||||
<value>Nouvelle enveloppe</value>
|
||||
</data>
|
||||
<data name="LoadEnvelope" xml:space="preserve">
|
||||
<value>Charger l'enveloppe</value>
|
||||
</data>
|
||||
<data name="DeleteEnvelope" xml:space="preserve">
|
||||
<value>Supprimer l'enveloppe</value>
|
||||
</data>
|
||||
<data name="RefreshData" xml:space="preserve">
|
||||
<value>Actualiser les données</value>
|
||||
</data>
|
||||
<data name="RefreshedAt" xml:space="preserve">
|
||||
<value>Actualisé : {0}</value>
|
||||
</data>
|
||||
<data name="ShowDocument" xml:space="preserve">
|
||||
<value>Afficher le document</value>
|
||||
</data>
|
||||
<data name="ContactReceiver" xml:space="preserve">
|
||||
<value>Contacter le destinataire</value>
|
||||
</data>
|
||||
<data name="EnvelopeId" xml:space="preserve">
|
||||
<value>ID d'enveloppe : {0}</value>
|
||||
</data>
|
||||
<data name="OpenLogDirectory" xml:space="preserve">
|
||||
<value>Ouvrir le répertoire des logs</value>
|
||||
</data>
|
||||
<data name="ShowResultsReport" xml:space="preserve">
|
||||
<value>Afficher le rapport de résultats</value>
|
||||
</data>
|
||||
<data name="SupportMail" xml:space="preserve">
|
||||
<value>E-mail de support</value>
|
||||
</data>
|
||||
<data name="ResendInvitation" xml:space="preserve">
|
||||
<value>Renvoyer l'invitation</value>
|
||||
</data>
|
||||
<data name="Export" xml:space="preserve">
|
||||
<value>Exporter</value>
|
||||
</data>
|
||||
<data name="Receivers" xml:space="preserve">
|
||||
<value>Destinataires</value>
|
||||
</data>
|
||||
<data name="EmailSalutation" xml:space="preserve">
|
||||
<value>Formule de politesse</value>
|
||||
</data>
|
||||
<data name="SignedWhen" xml:space="preserve">
|
||||
<value>Signé quand</value>
|
||||
</data>
|
||||
<data name="AccessCode" xml:space="preserve">
|
||||
<value>Code d'accès</value>
|
||||
</data>
|
||||
<data name="User" xml:space="preserve">
|
||||
<value>Utilisateur</value>
|
||||
</data>
|
||||
<data name="Type" xml:space="preserve">
|
||||
<value>Type</value>
|
||||
</data>
|
||||
<data name="Title" xml:space="preserve">
|
||||
<value>Titre</value>
|
||||
</data>
|
||||
<data name="CreatedOn" xml:space="preserve">
|
||||
<value>Créé le</value>
|
||||
</data>
|
||||
<data name="LastModified" xml:space="preserve">
|
||||
<value>Dernière modification</value>
|
||||
</data>
|
||||
<data name="OpenEnvelopes" xml:space="preserve">
|
||||
<value>Enveloppes ouvertes</value>
|
||||
</data>
|
||||
<data name="CompletedEnvelopes" xml:space="preserve">
|
||||
<value>Enveloppes terminées</value>
|
||||
</data>
|
||||
<data name="SendAccessCode" xml:space="preserve">
|
||||
<value>Envoyer le code d'accès</value>
|
||||
</data>
|
||||
<data name="TwoFactorProperties" xml:space="preserve">
|
||||
<value>Propriétés 2-facteurs</value>
|
||||
</data>
|
||||
<data name="Name" xml:space="preserve">
|
||||
<value>Nom</value>
|
||||
</data>
|
||||
<data name="PhoneNumber" xml:space="preserve">
|
||||
<value>Numéro de téléphone</value>
|
||||
</data>
|
||||
<data name="AddReceiver" xml:space="preserve">
|
||||
<value>Ajouter un destinataire</value>
|
||||
</data>
|
||||
<data name="DeleteReceiver" xml:space="preserve">
|
||||
<value>Supprimer le destinataire</value>
|
||||
</data>
|
||||
<data name="AddFile" xml:space="preserve">
|
||||
<value>Ajouter un fichier</value>
|
||||
</data>
|
||||
<data name="MergeFiles" xml:space="preserve">
|
||||
<value>Fusionner les fichiers</value>
|
||||
</data>
|
||||
<data name="DeleteFile" xml:space="preserve">
|
||||
<value>Supprimer le fichier</value>
|
||||
</data>
|
||||
<data name="ShowFile" xml:space="preserve">
|
||||
<value>Afficher le fichier</value>
|
||||
</data>
|
||||
<data name="EditFields" xml:space="preserve">
|
||||
<value>Modifier les champs</value>
|
||||
</data>
|
||||
<data name="EditData" xml:space="preserve">
|
||||
<value>Modifier les données</value>
|
||||
</data>
|
||||
<data name="Save" xml:space="preserve">
|
||||
<value>Enregistrer</value>
|
||||
</data>
|
||||
<data name="SendEnvelope" xml:space="preserve">
|
||||
<value>Envoyer l'enveloppe</value>
|
||||
</data>
|
||||
<data name="Cancel" xml:space="preserve">
|
||||
<value>Annuler</value>
|
||||
</data>
|
||||
<data name="AddSignature" xml:space="preserve">
|
||||
<value>Ajouter une signature</value>
|
||||
</data>
|
||||
<data name="DeleteSignature" xml:space="preserve">
|
||||
<value>Supprimer la signature</value>
|
||||
</data>
|
||||
<data name="Language" xml:space="preserve">
|
||||
<value>Langue</value>
|
||||
</data>
|
||||
<data name="UseAccessCode" xml:space="preserve">
|
||||
<value>Utiliser un code d'accès</value>
|
||||
</data>
|
||||
<data name="TwoFactorEnabled" xml:space="preserve">
|
||||
<value>Authentification à 2 facteurs activée</value>
|
||||
</data>
|
||||
<data name="CertificationType" xml:space="preserve">
|
||||
<value>Type de certification</value>
|
||||
</data>
|
||||
<data name="FinalEmailToCreator" xml:space="preserve">
|
||||
<value>E-mail final au créateur</value>
|
||||
</data>
|
||||
<data name="FinalEmailToReceivers" xml:space="preserve">
|
||||
<value>E-mail final aux destinataires</value>
|
||||
</data>
|
||||
<data name="SendReminderEmails" xml:space="preserve">
|
||||
<value>Envoyer des e-mails de rappel</value>
|
||||
</data>
|
||||
<data name="FirstReminderDays" xml:space="preserve">
|
||||
<value>Premier rappel (jours)</value>
|
||||
</data>
|
||||
<data name="ReminderIntervalDays" xml:space="preserve">
|
||||
<value>Intervalle de rappel (jours)</value>
|
||||
</data>
|
||||
<data name="ExpiresWhenDays" xml:space="preserve">
|
||||
<value>Expire après (jours)</value>
|
||||
</data>
|
||||
<data name="ExpiresWarningDays" xml:space="preserve">
|
||||
<value>Avertissement d'expiration (jours)</value>
|
||||
</data>
|
||||
<data name="Message" xml:space="preserve">
|
||||
<value>Message</value>
|
||||
</data>
|
||||
<data name="EnvelopeType" xml:space="preserve">
|
||||
<value>Type d'enveloppe</value>
|
||||
</data>
|
||||
<data name="AllOptions" xml:space="preserve">
|
||||
<value>Toutes les options</value>
|
||||
</data>
|
||||
<data name="DeleteReason" xml:space="preserve">
|
||||
<value>Motif de suppression</value>
|
||||
</data>
|
||||
<data name="PleaseProvideReason" xml:space="preserve">
|
||||
<value>Veuillez indiquer une raison</value>
|
||||
</data>
|
||||
<data name="Status" xml:space="preserve">
|
||||
<value>Statut</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace EnvelopeGenerator.Domain.Constants
|
||||
{
|
||||
// http://wiki.dd/xwiki13/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/SignFlow/Envelope%20Status/
|
||||
// http://wiki.dd/xwiki_prod/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/signFLOW/signFLOW%20-%20Enwickler-Handbuch/4.%20Anhang/4.3%20Historie%20und%20Status%20der%20Umschl%C3%A4ge/
|
||||
public enum EnvelopeStatus
|
||||
{
|
||||
Invalid = 0,
|
||||
@@ -49,5 +51,28 @@ namespace EnvelopeGenerator.Domain.Constants
|
||||
EnvelopeStatus.EnvelopeCreated,
|
||||
EnvelopeStatus.DocumentMod_Rotation
|
||||
};
|
||||
|
||||
public static readonly List<EnvelopeStatus> Active = Enum.GetValues(typeof(EnvelopeStatus))
|
||||
.Cast<EnvelopeStatus>()
|
||||
.Where(status => status.IsActive())
|
||||
.ToList();
|
||||
|
||||
public static readonly List<EnvelopeStatus> Completed = Enum.GetValues(typeof(EnvelopeStatus))
|
||||
.Cast<EnvelopeStatus>()
|
||||
.Where(status => status.IsCompleted())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static class EnvelopeStatusExtensions
|
||||
{
|
||||
public static bool IsActive(this EnvelopeStatus status)
|
||||
{
|
||||
return status >= EnvelopeStatus.EnvelopeCreated && status < EnvelopeStatus.EnvelopePartlySigned;
|
||||
}
|
||||
|
||||
public static bool IsCompleted(this EnvelopeStatus status)
|
||||
{
|
||||
return status >= EnvelopeStatus.EnvelopeCompletelySigned && status <= EnvelopeStatus.EnvelopeWithdrawn;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
#if NET
|
||||
using EnvelopeGenerator.Application.Common.Configurations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.Infrastructure
|
||||
{
|
||||
public class EGDbContextFactory : IDesignTimeDbContextFactory<EGDbContext>
|
||||
{
|
||||
public EGDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.migration.json")
|
||||
.Build();
|
||||
|
||||
// create DbContextOptions
|
||||
var optionsBuilder = new DbContextOptionsBuilder<EGDbContext>();
|
||||
optionsBuilder.UseSqlServer(config.GetConnectionString("Default"));
|
||||
|
||||
// create DbTriggerParams
|
||||
var triggerLists = config.GetSection("DbTriggerParams").Get<Dictionary<string, List<string>>>();
|
||||
var dbTriggerParams = new DbTriggerParams();
|
||||
if (triggerLists is not null)
|
||||
foreach (var triggerList in triggerLists)
|
||||
{
|
||||
if (triggerList.Value.Count == 0)
|
||||
continue; // Skip empty trigger lists
|
||||
|
||||
var tableName = triggerList.Key;
|
||||
|
||||
dbTriggerParams[tableName] = new List<string>();
|
||||
|
||||
foreach (var trigger in triggerList.Value)
|
||||
{
|
||||
dbTriggerParams[tableName].Add(trigger);
|
||||
}
|
||||
}
|
||||
|
||||
var dbContext = new EGDbContext(optionsBuilder.Options, Options.Create(dbTriggerParams));
|
||||
dbContext.IsMigration = true;
|
||||
return dbContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,4 +1,6 @@
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
@using System.Globalization
|
||||
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
@@ -7,4 +9,4 @@
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
</Router>
|
||||
|
||||
@@ -24,11 +24,13 @@
|
||||
|
||||
</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="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" />
|
||||
@@ -40,6 +42,9 @@
|
||||
<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>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a pre-assigned signature annotation position on a specific page.
|
||||
/// <br/><br/>
|
||||
/// <b>Coordinate unit (X, Y):</b> Inches (GdPicture14 native unit),
|
||||
/// origin at the <b>top-left</b> corner of the page, both axes increase downward/rightward.
|
||||
/// <br/><br/>
|
||||
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
|
||||
/// Convert: <c>xDX = xInches * 100.0</c>
|
||||
/// <br/>
|
||||
/// <b>Conversion to PDF Points:</b> Multiply by 72 (1 inch = 72 points).
|
||||
/// Convert: <c>xPt = xInches * 72.0</c>
|
||||
/// <br/>
|
||||
/// <b>Y-axis for PDF (bottom-left origin):</b> Flip required for iText7.
|
||||
/// Convert: <c>yPt = (pageHeightInches - yInches - elemHeightInches) * 72.0</c>
|
||||
/// </summary>
|
||||
[Obsolete("Use SignatureDto with SignatureService.")]
|
||||
public record AnnotationDto
|
||||
{
|
||||
/// <summary>Unique identifier of the annotation.</summary>
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>1-based page number within the document.</summary>
|
||||
public int Page { get; init; }
|
||||
|
||||
/// <summary>Horizontal position in INCHES from the left edge of the page.</summary>
|
||||
public double X { get; init; }
|
||||
|
||||
/// <summary>Vertical position in INCHES from the top edge of the page.</summary>
|
||||
public double Y { get; init; }
|
||||
}
|
||||
39
EnvelopeGenerator.ReceiverUI/Models/EnvelopeDto.cs
Normal file
39
EnvelopeGenerator.ReceiverUI/Models/EnvelopeDto.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
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; }
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
@inject IOptions<ApiOptions> AppOptions
|
||||
@inject IOptions<PdfViewerOptions> PdfViewerOptions
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject SignatureService SignatureService
|
||||
@inject DocReceiverElementService SignatureService
|
||||
@inject SignatureCacheService SignatureCacheService
|
||||
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
|
||||
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService
|
||||
|
||||
@@ -1,7 +1,442 @@
|
||||
@page "/sender"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize(Policy = "Sender")]
|
||||
|
||||
<h3>EnvelopeSender</h3>
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
@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
|
||||
@@ -301,7 +302,10 @@ Shown="OnPopupShownAsync">
|
||||
bool IsLoggingOut;
|
||||
|
||||
IReadOnlyList<AnnotationDto> _annotations = [];
|
||||
IEnumerable<int> AnnotationPages => _annotations.Select(a => a.Page).Distinct().OrderBy(p => p);
|
||||
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;
|
||||
|
||||
@@ -7,6 +7,9 @@ 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");
|
||||
@@ -20,9 +23,14 @@ builder.Services.AddScoped<DocumentService>();
|
||||
builder.Services.AddScoped<AuthService>();
|
||||
builder.Services.AddScoped<AnnotationService>();
|
||||
builder.Services.AddScoped<EnvelopeReceiverService>();
|
||||
builder.Services.AddScoped<SignatureService>();
|
||||
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();
|
||||
@@ -41,5 +49,30 @@ 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();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"profiles": {
|
||||
"EnvelopeGenerator.ReceiverUI": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchBrowser": false,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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;
|
||||
|
||||
@@ -58,6 +58,16 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
/// <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> CheckSenderAsync(CancellationToken cancel = default)
|
||||
{
|
||||
var response = await http.GetAsync($"{_api.BaseUrl}/api/auth/check", cancel);
|
||||
return response.StatusCode == HttpStatusCode.OK;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates a sender user with username and password.
|
||||
/// Calls POST /api/auth?cookie=true with JSON body.
|
||||
@@ -78,4 +88,16 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
_ => 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;
|
||||
}
|
||||
}
|
||||
|
||||
74
EnvelopeGenerator.ReceiverUI/Services/CultureService.cs
Normal file
74
EnvelopeGenerator.ReceiverUI/Services/CultureService.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing application culture/localization.
|
||||
/// </summary>
|
||||
public class CultureService
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
private const string CULTURE_KEY = "AppCulture";
|
||||
|
||||
public CultureService(IJSRuntime jsRuntime)
|
||||
{
|
||||
_jsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of supported cultures.
|
||||
/// </summary>
|
||||
public static CultureInfo[] SupportedCultures { get; } = new[]
|
||||
{
|
||||
new CultureInfo("de-DE"),
|
||||
new CultureInfo("en-US"),
|
||||
new CultureInfo("fr-FR")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Sets the application culture and stores it in localStorage.
|
||||
/// </summary>
|
||||
public async Task SetCultureAsync(string culture)
|
||||
{
|
||||
if (!SupportedCultures.Any(c => c.Name == culture))
|
||||
throw new ArgumentException($"Culture '{culture}' is not supported.", nameof(culture));
|
||||
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", CULTURE_KEY, culture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stored culture from localStorage.
|
||||
/// </summary>
|
||||
public async Task<string?> GetCultureAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", CULTURE_KEY);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the culture from localStorage or browser settings.
|
||||
/// </summary>
|
||||
public async Task<CultureInfo> InitializeCultureAsync()
|
||||
{
|
||||
var storedCulture = await GetCultureAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(storedCulture) &&
|
||||
SupportedCultures.Any(c => c.Name == storedCulture))
|
||||
{
|
||||
return new CultureInfo(storedCulture);
|
||||
}
|
||||
|
||||
// Fallback to browser culture or default
|
||||
var browserCulture = CultureInfo.CurrentCulture.Name;
|
||||
var matchedCulture = SupportedCultures.FirstOrDefault(c => c.Name == browserCulture);
|
||||
|
||||
return matchedCulture ?? SupportedCultures[0]; // Default to German
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,13 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
||||
|
||||
public class SignatureService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
public class DocReceiverElementService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public async Task<IReadOnlyList<SignatureDto>> GetAsync(string envelopeKey, CancellationToken cancel = default)
|
||||
{
|
||||
var url = $"{apiOptions.Value.BaseUrl}/api/Signature/{Uri.EscapeDataString(envelopeKey)}";
|
||||
var url = $"{apiOptions.Value.BaseUrl}/api/DocReceiverElement/{Uri.EscapeDataString(envelopeKey)}";
|
||||
var response = await http.GetAsync(url, cancel);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
72
EnvelopeGenerator.ReceiverUI/Services/EnvelopeService.cs
Normal file
72
EnvelopeGenerator.ReceiverUI/Services/EnvelopeService.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using EnvelopeGenerator.ReceiverUI.Models;
|
||||
using EnvelopeGenerator.ReceiverUI.Options;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves <see cref="EnvelopeDto"/>s from the API.
|
||||
/// </summary>
|
||||
public class EnvelopeService
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly ApiOptions _apiOptions;
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public EnvelopeService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||
{
|
||||
_http = http;
|
||||
_apiOptions = apiOptions.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches envelopes from the API with optional filters.
|
||||
/// </summary>
|
||||
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
|
||||
public async Task<IEnumerable<EnvelopeDto>?> GetAsync(
|
||||
int? id = null,
|
||||
string? uuid = null,
|
||||
bool? onlyActive = null,
|
||||
bool? onlyCompleted = null,
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
var baseUrl = $"{_apiOptions.BaseUrl}/api/Envelope";
|
||||
var queryParams = new Dictionary<string, string?>();
|
||||
|
||||
if (id.HasValue)
|
||||
{
|
||||
queryParams["Id"] = id.Value.ToString();
|
||||
}
|
||||
if (!string.IsNullOrEmpty(uuid))
|
||||
{
|
||||
queryParams["Uuid"] = uuid;
|
||||
}
|
||||
if (onlyActive.HasValue)
|
||||
{
|
||||
queryParams["OnlyActive"] = onlyActive.Value.ToString();
|
||||
}
|
||||
if (onlyCompleted.HasValue)
|
||||
{
|
||||
queryParams["OnlyCompleted"] = onlyCompleted.Value.ToString();
|
||||
}
|
||||
|
||||
var url = QueryHelpers.AddQueryString(baseUrl, queryParams);
|
||||
|
||||
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 envelopes. Status: {statusCode} ({reasonPhrase})",
|
||||
null,
|
||||
response.StatusCode);
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<IEnumerable<EnvelopeDto>>(_jsonOptions, cancel);
|
||||
}
|
||||
}
|
||||
80
EnvelopeGenerator.ReceiverUI/Shared/LanguageSelector.razor
Normal file
80
EnvelopeGenerator.ReceiverUI/Shared/LanguageSelector.razor
Normal file
@@ -0,0 +1,80 @@
|
||||
@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"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,12 @@
|
||||
</article>
|
||||
</main>
|
||||
<footer class="receiver-footer">
|
||||
<span>© SignFlow 2023-2024 <a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a></span>
|
||||
<span class="receiver-footer__sep">|</span>
|
||||
<a href="docs/privacy-policy.de-DE.html" target="_blank" rel="noopener">Datenschutz</a>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -15,3 +15,18 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -365,4 +365,75 @@ article {
|
||||
|
||||
.receiver-footer__sep {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Language Selector (Footer) ──────────────────────────────────────────── */
|
||||
.language-selector {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.language-selector__trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.language-selector__trigger:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.language-selector__arrow {
|
||||
font-size: 0.6rem;
|
||||
transition: transform 0.2s ease;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.language-selector__dropdown {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 0.5rem);
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
min-width: 160px;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.language-selector__option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.65rem 1rem;
|
||||
background: white;
|
||||
border: none;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: background-color 0.2s ease;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.language-selector__option:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.language-selector__option .fi {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.language-selector__option span:last-child {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
297
EnvelopeGenerator.ReceiverUI/wwwroot/css/sender-page.css
Normal file
297
EnvelopeGenerator.ReceiverUI/wwwroot/css/sender-page.css
Normal file
@@ -0,0 +1,297 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
<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;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<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" />
|
||||
@@ -28,6 +29,10 @@
|
||||
<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>
|
||||
|
||||
@@ -4,7 +4,5 @@ public class ApiOptions
|
||||
{
|
||||
public const string SectionName = "Api";
|
||||
|
||||
public string BaseUrl { get; set; } = string.Empty;
|
||||
|
||||
public bool UsePredefinedReports { get; set; } = false;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,446 @@
|
||||
@page "/sender"
|
||||
@rendermode InteractiveWebAssembly
|
||||
@page "/sender"
|
||||
@rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false))
|
||||
@using System.Text.Json
|
||||
@using EnvelopeGenerator.Domain.Constants
|
||||
@using EnvelopeGenerator.Server.Client.Models
|
||||
@using DevExpress.Blazor
|
||||
@using EnvelopeGenerator.Server.Client.Services
|
||||
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService
|
||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject AppVersionService AppVersion
|
||||
@using EnvelopeGenerator.Application.Common.Dto
|
||||
@inject EnvelopeGenerator.Server.Client.Services.EnvelopeService EnvelopeService
|
||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject AppVersionService AppVersion
|
||||
|
||||
<h3>EnvelopeSender</h3>
|
||||
<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?.ToList() ?? [];
|
||||
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.Receiver?.EmailAddress</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?.ToList() ?? [];
|
||||
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.Receiver?.EmailAddress</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.CheckSenderAccessAsync();
|
||||
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(EnvelopeStatus status)
|
||||
{
|
||||
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 +1,4 @@
|
||||
@page "/"
|
||||
@rendermode InteractiveWebAssembly
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||
@@ -59,8 +58,8 @@
|
||||
@code {
|
||||
private const string HomePageDescription =
|
||||
"Das digitale Unterschriftenportal ist eine Plattform, die entwickelt wurde, um Ihre Dokumente sicher zu unterschreiben und zu verwalten. " +
|
||||
"Mit seiner benutzerfreundlichen Oberfläche können Sie Ihre Dokumente schnell hochladen, die Unterschriftsprozesse verfolgen und Ihre digitalen Unterschriftenanwendungen einfach durchführen. " +
|
||||
"Dieses Portal beschleunigt Ihren Arbeitsablauf mit rechtlich gültigen Unterschriften und erhöht gleichzeitig die Sicherheit Ihrer Dokumente.";
|
||||
"Mit seiner benutzerfreundlichen Oberfläche können Sie Ihre Dokumente schnell hochladen, die Unterschriftsprozesse verfolgen und Ihre digitalen Unterschriftenanwendungen einfach durchführen. " +
|
||||
"Dieses Portal beschleunigt Ihren Arbeitsablauf mit rechtlich gültigen Unterschriften und erhöht gleichzeitig die Sicherheit Ihrer Dokumente.";
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
@@ -32,6 +32,7 @@ builder.Services.AddScoped<EnvelopeReceiverService>();
|
||||
builder.Services.AddScoped<SignatureService>();
|
||||
builder.Services.AddScoped<SignatureCacheService>();
|
||||
builder.Services.AddSingleton<AppVersionService>();
|
||||
builder.Services.AddScoped<EnvelopeService>();
|
||||
|
||||
// DevExpress WASM
|
||||
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
@@ -10,6 +9,7 @@ public enum SenderLoginResult { Success, InvalidCredentials, Error }
|
||||
|
||||
public class AuthService(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
private HttpClient CreateDefaultClient() => httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the current user holds a valid receiver token for the given envelope key.
|
||||
@@ -17,11 +17,22 @@ public class AuthService(IHttpClientFactory httpClientFactory)
|
||||
/// </summary>
|
||||
public async Task<bool> CheckEnvelopeAccessAsync(string envelopeKey, CancellationToken cancel = default)
|
||||
{
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Submits the access code for the given envelope key.
|
||||
/// Calls POST /api/Auth/envelope-receiver/{key} with multipart/form-data.
|
||||
@@ -29,9 +40,11 @@ public class AuthService(IHttpClientFactory httpClientFactory)
|
||||
/// </summary>
|
||||
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string envelopeKey, string accessCode, CancellationToken cancel = default)
|
||||
{
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var form = new MultipartFormDataContent();
|
||||
form.Add(new StringContent(accessCode), "AccessCode");
|
||||
using var http = CreateDefaultClient();
|
||||
var form = new MultipartFormDataContent
|
||||
{
|
||||
{ new StringContent(accessCode), "AccessCode" }
|
||||
};
|
||||
|
||||
var response = await http.PostAsync(
|
||||
$"/api/Auth/envelope-receiver/{Uri.EscapeDataString(envelopeKey)}",
|
||||
@@ -52,13 +65,26 @@ public class AuthService(IHttpClientFactory httpClientFactory)
|
||||
/// </summary>
|
||||
public async Task<bool> LogoutEnvelopeReceiverAsync(string envelopeKey, CancellationToken cancel = default)
|
||||
{
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
using var http = CreateDefaultClient();
|
||||
var response = await http.PostAsync(
|
||||
$"/api/auth/logout/envelope/{Uri.EscapeDataString(envelopeKey)}",
|
||||
null, cancel);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the per-envelope receiver cookie for the given envelope key.
|
||||
/// Calls POST /api/auth/logout/envelope/{envelopeKey}.
|
||||
/// </summary>
|
||||
public async Task<bool> LogoutSenderAsync(CancellationToken cancel = default)
|
||||
{
|
||||
using var http = CreateDefaultClient();
|
||||
var response = await http.PostAsync(
|
||||
$"/api/auth/logout",
|
||||
null, cancel);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates a sender user with username and password.
|
||||
/// Calls POST /api/auth?cookie=true with JSON body.
|
||||
@@ -66,7 +92,7 @@ public class AuthService(IHttpClientFactory httpClientFactory)
|
||||
/// </summary>
|
||||
public async Task<SenderLoginResult> LoginSenderAsync(string username, string password, CancellationToken cancel = default)
|
||||
{
|
||||
using var http = httpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
using var http = CreateDefaultClient();
|
||||
var requestBody = new { username, password };
|
||||
|
||||
var response = await http.PostAsJsonAsync(
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing application culture/localization.
|
||||
/// </summary>
|
||||
public class CultureService
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
private const string CULTURE_KEY = "AppCulture";
|
||||
|
||||
public CultureService(IJSRuntime jsRuntime)
|
||||
{
|
||||
_jsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of supported cultures.
|
||||
/// </summary>
|
||||
public static CultureInfo[] SupportedCultures { get; } = new[]
|
||||
{
|
||||
new CultureInfo("de-DE"),
|
||||
new CultureInfo("en-US"),
|
||||
new CultureInfo("fr-FR")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Sets the application culture and stores it in localStorage.
|
||||
/// </summary>
|
||||
public async Task SetCultureAsync(string culture)
|
||||
{
|
||||
if (!SupportedCultures.Any(c => c.Name == culture))
|
||||
throw new ArgumentException($"Culture '{culture}' is not supported.", nameof(culture));
|
||||
|
||||
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", CULTURE_KEY, culture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stored culture from localStorage.
|
||||
/// </summary>
|
||||
public async Task<string?> GetCultureAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", CULTURE_KEY);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the culture from localStorage or browser settings.
|
||||
/// </summary>
|
||||
public async Task<CultureInfo> InitializeCultureAsync()
|
||||
{
|
||||
var storedCulture = await GetCultureAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(storedCulture) &&
|
||||
SupportedCultures.Any(c => c.Name == storedCulture))
|
||||
{
|
||||
return new CultureInfo(storedCulture);
|
||||
}
|
||||
|
||||
// Fallback to browser culture or default
|
||||
var browserCulture = CultureInfo.CurrentCulture.Name;
|
||||
var matchedCulture = SupportedCultures.FirstOrDefault(c => c.Name == browserCulture);
|
||||
|
||||
return matchedCulture ?? SupportedCultures[0]; // Default to German
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using EnvelopeGenerator.Server.Client.Models;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
public class DocReceiverElementService(IHttpClientFactory clientFactory)
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public async Task<IReadOnlyList<SignatureDto>> GetAsync(string envelopeKey, CancellationToken cancel = default)
|
||||
{
|
||||
var url = $"/api/DocReceiverElement/{Uri.EscapeDataString(envelopeKey)}";
|
||||
|
||||
var http = clientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var response = await http.GetAsync(url, cancel);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new HttpRequestException($"Failed to retrieve signatures for envelope {envelopeKey}: {response.StatusCode} {response.ReasonPhrase}");
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<List<SignatureDto>>(_jsonOptions, cancel);
|
||||
return result ?? [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves <see cref="EnvelopeDto"/>s from the API.
|
||||
/// </summary>
|
||||
public class EnvelopeService(IHttpClientFactory clientFactory)
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
/// <summary>
|
||||
/// Fetches envelopes from the API with optional filters.
|
||||
/// </summary>
|
||||
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
|
||||
public async Task<IEnumerable<EnvelopeDto>?> GetAsync(
|
||||
int? id = null,
|
||||
string? uuid = null,
|
||||
bool? onlyActive = null,
|
||||
bool? onlyCompleted = null,
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
var baseUrl = $"/api/Envelope";
|
||||
var queryParams = new Dictionary<string, string?>();
|
||||
|
||||
if (id.HasValue)
|
||||
{
|
||||
queryParams["Id"] = id.Value.ToString();
|
||||
}
|
||||
if (!string.IsNullOrEmpty(uuid))
|
||||
{
|
||||
queryParams["Uuid"] = uuid;
|
||||
}
|
||||
if (onlyActive.HasValue)
|
||||
{
|
||||
queryParams["OnlyActive"] = onlyActive.Value.ToString();
|
||||
}
|
||||
if (onlyCompleted.HasValue)
|
||||
{
|
||||
queryParams["OnlyCompleted"] = onlyCompleted.Value.ToString();
|
||||
}
|
||||
|
||||
var url = QueryHelpers.AddQueryString(baseUrl, queryParams);
|
||||
|
||||
var httpClient = clientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
var response = await httpClient.GetAsync(url, cancel);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var statusCode = (int)response.StatusCode;
|
||||
var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
|
||||
throw new HttpRequestException(
|
||||
$"Failed to load envelopes. Status: {statusCode} ({reasonPhrase})",
|
||||
null,
|
||||
response.StatusCode);
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<IEnumerable<EnvelopeDto>>(_jsonOptions, cancel);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
@page "/envelope/DxPdfViewer"
|
||||
@rendermode InteractiveServer
|
||||
@using System.IO
|
||||
@using DevExpress.Blazor
|
||||
@using System.Reflection
|
||||
@using DevExpress.Blazor.PdfViewer
|
||||
|
||||
|
||||
@@ -10,3 +10,4 @@
|
||||
@using EnvelopeGenerator.Server.Client
|
||||
@using EnvelopeGenerator.Server.Components
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.Blazor.PdfViewer
|
||||
|
||||
@@ -40,17 +40,11 @@ public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions,
|
||||
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</response>
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||
[Authorize(Policy = AuthPolicy.Sender)]
|
||||
[HttpPost("logout")]
|
||||
public async Task<IActionResult> Logout()
|
||||
public IActionResult Logout()
|
||||
{
|
||||
if (await this.IsUserInPolicyAsync(AuthPolicy.Sender))
|
||||
Response.Cookies.Delete(authTokenKeys.Cookie);
|
||||
else if (await this.IsUserInPolicyAsync(AuthPolicy.ReceiverOrReceiverTFA))
|
||||
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
else
|
||||
return Unauthorized();
|
||||
|
||||
Response.Cookies.Delete(authTokenKeys.Cookie);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -69,7 +63,7 @@ public partial class AuthController(IOptions<AuthTokenKeys> authTokenKeyOptions,
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||
[HttpGet("check")]
|
||||
[Authorize]
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
public IActionResult Check(string? role = null)
|
||||
=> role is not null && !User.IsInRole(role)
|
||||
? Unauthorized()
|
||||
|
||||
@@ -30,35 +30,16 @@ public class DocumentController(IMediator mediator, IAuthorizationService authSe
|
||||
/// <param name="query">Encoded envelope key.</param>
|
||||
/// <param name="cancel">Cancellation token.</param>
|
||||
[HttpGet]
|
||||
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||
[Authorize(Policy = AuthPolicy.Sender)]
|
||||
public async Task<IActionResult> GetDocument(CancellationToken cancel, [FromQuery] ReadDocumentQuery? query = null)
|
||||
{
|
||||
// Sender: expects query with envelope key
|
||||
if (await this.IsUserInPolicyAsync(AuthPolicy.Sender))
|
||||
{
|
||||
if (query is null)
|
||||
return BadRequest("Missing document query.");
|
||||
if (query is null)
|
||||
return BadRequest("Missing document query.");
|
||||
|
||||
var senderDoc = await mediator.Send(query, cancel);
|
||||
return senderDoc.ByteData is byte[] senderDocByte
|
||||
? File(senderDocByte, "application/octet-stream")
|
||||
: NotFound("Document is empty.");
|
||||
}
|
||||
|
||||
// Receiver: resolve envelope id from claims
|
||||
if (await this.IsUserInPolicyAsync(AuthPolicy.Receiver))
|
||||
{
|
||||
if (query is not null)
|
||||
return BadRequest("Query parameters are not allowed for receiver role.");
|
||||
|
||||
var envelopeId = User.EnvelopeId();
|
||||
var receiverDoc = await mediator.Send(new ReadDocumentQuery { EnvelopeId = envelopeId }, cancel);
|
||||
return receiverDoc.ByteData is byte[] receiverDocByte
|
||||
? File(receiverDocByte, "application/octet-stream")
|
||||
: NotFound("Document is empty.");
|
||||
}
|
||||
|
||||
return Unauthorized();
|
||||
var senderDoc = await mediator.Send(query, cancel);
|
||||
return senderDoc.ByteData is byte[] senderDocByte
|
||||
? File(senderDocByte, "application/octet-stream")
|
||||
: NotFound("Document is empty.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -94,7 +94,7 @@ public class EnvelopeController : ControllerBase
|
||||
/// <param name="command"></param>
|
||||
/// <returns></returns>
|
||||
[NonAction]
|
||||
[Authorize]
|
||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateAsync([FromBody] CreateEnvelopeCommand command)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
namespace EnvelopeGenerator.Server.Models;
|
||||
|
||||
[Obsolete("Use auth DTO")]
|
||||
public record Auth(string? AccessCode = null, string? SmsCode = null, string? AuthenticatorCode = null, bool UserSelectSMS = default)
|
||||
{
|
||||
public bool HasAccessCode => AccessCode is not null;
|
||||
|
||||
@@ -10,6 +10,7 @@ using EnvelopeGenerator.Domain.Constants;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Globalization;
|
||||
using Scalar.AspNetCore;
|
||||
using Microsoft.OpenApi.Models;
|
||||
@@ -53,7 +54,11 @@ try
|
||||
.AddInteractiveWebAssemblyComponents();
|
||||
|
||||
// Add API Controllers
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddControllers()
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles;
|
||||
});
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
// YARP Reverse Proxy (for forwarding auth requests to AuthHub)
|
||||
@@ -255,7 +260,6 @@ try
|
||||
|
||||
// Authorization Policies
|
||||
builder.Services.AddAuthorizationBuilder()
|
||||
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy.RequireRole(Role.Sender, Role.Receiver.Full))
|
||||
.AddPolicy(AuthPolicy.Sender, policy => policy
|
||||
.RequireRole(Role.Sender)
|
||||
.AddAuthenticationSchemes(AuthScheme.Sender))
|
||||
@@ -319,8 +323,16 @@ try
|
||||
builder.Services.AddScoped<SignatureCacheService>();
|
||||
builder.Services.AddSingleton<AppVersionService>();
|
||||
|
||||
// EnvelopeService with HttpClient factory (for SSR scenarios)
|
||||
builder.Services.AddScoped<EnvelopeService>();
|
||||
|
||||
// DocReceiverElementService (SignatureService alternative)
|
||||
builder.Services.AddScoped<DocReceiverElementService>();
|
||||
|
||||
// SSR Authentication Service (for Envelope Receiver pages)
|
||||
builder.Services.AddScoped<EnvelopeGenerator.Server.Services.IEnvelopeAuthService, EnvelopeGenerator.Server.Services.EnvelopeAuthService>();
|
||||
builder.Services.AddScoped<EnvelopeGenerator.Server.Services.EnvelopeReceiverAuthorizationService>();
|
||||
builder.Services.AddScoped<EnvelopeGenerator.Server.Services.EnvelopeReceiverPageDataService>();
|
||||
|
||||
// DevExpress Server-Side Services (CRITICAL for DxPdfViewer)
|
||||
builder.Services.AddDevExpressBlazor();
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using DigitalData.Auth.Claims;
|
||||
using EnvelopeGenerator.Domain.Constants;
|
||||
using EnvelopeGenerator.Server.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Authorizes receiver access for interactive server pages without calling a controller endpoint.
|
||||
/// </summary>
|
||||
public class EnvelopeReceiverAuthorizationService(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IAuthorizationService authorizationService,
|
||||
IOptions<AuthTokenKeys> authTokenKeyOptions,
|
||||
IOptionsMonitor<JwtBearerOptions> jwtBearerOptionsMonitor,
|
||||
ILogger<EnvelopeReceiverAuthorizationService> logger)
|
||||
{
|
||||
private readonly AuthTokenKeys _authTokenKeys = authTokenKeyOptions.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the authenticated receiver principal for the specified envelope key when authorization succeeds.
|
||||
/// </summary>
|
||||
public async Task<ClaimsPrincipal?> AuthorizeAsync(string envelopeKey, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(envelopeKey))
|
||||
return null;
|
||||
|
||||
var httpContext = httpContextAccessor.HttpContext;
|
||||
if (httpContext is null)
|
||||
return null;
|
||||
|
||||
if (await IsAuthorizedReceiverAsync(httpContext.User, envelopeKey, cancellationToken))
|
||||
return httpContext.User;
|
||||
|
||||
var cookieName = CookieNames.GetEnvelopeReceiverCookieName(_authTokenKeys.Cookie, envelopeKey);
|
||||
if (!httpContext.Request.Cookies.TryGetValue(cookieName, out var token) || string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
logger.LogDebug("Receiver cookie '{CookieName}' was not found for envelope '{EnvelopeKey}'.", cookieName, envelopeKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
var principal = ValidateReceiverToken(token);
|
||||
if (principal is null)
|
||||
return null;
|
||||
|
||||
if (!await IsAuthorizedReceiverAsync(principal, envelopeKey, cancellationToken))
|
||||
return null;
|
||||
|
||||
httpContext.User = principal;
|
||||
|
||||
return principal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the current request is authorized for the specified envelope key.
|
||||
/// </summary>
|
||||
public async Task<bool> IsAuthorizedAsync(string envelopeKey, CancellationToken cancellationToken = default)
|
||||
=> await AuthorizeAsync(envelopeKey, cancellationToken) is not null;
|
||||
|
||||
private async Task<bool> IsAuthorizedReceiverAsync(ClaimsPrincipal? principal, string envelopeKey, CancellationToken cancellationToken)
|
||||
{
|
||||
if (principal?.Identity?.IsAuthenticated != true)
|
||||
return false;
|
||||
|
||||
var authorizationResult = await authorizationService.AuthorizeAsync(principal, AuthPolicy.Receiver);
|
||||
if (!authorizationResult.Succeeded)
|
||||
return false;
|
||||
|
||||
var subject = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value
|
||||
?? principal.FindFirst("sub")?.Value;
|
||||
|
||||
return string.Equals(subject, envelopeKey, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private ClaimsPrincipal? ValidateReceiverToken(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tokenValidationParameters = jwtBearerOptionsMonitor.Get(AuthScheme.Receiver).TokenValidationParameters.Clone();
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
return tokenHandler.ValidateToken(token, tokenValidationParameters, out _);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogDebug(ex, "Receiver token validation failed.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using EnvelopeGenerator.Application.Common.Dto;
|
||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||
using EnvelopeGenerator.Application.Documents.Queries;
|
||||
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
||||
using EnvelopeGenerator.Server.Client.Models;
|
||||
using EnvelopeGenerator.Server.Client.Models.Constants;
|
||||
using EnvelopeGenerator.Server.Extensions;
|
||||
using EnvelopeGenerator.Server.Options;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ApplicationEnvelopeReceiverDto = EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto;
|
||||
|
||||
namespace EnvelopeGenerator.Server.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Loads receiver page data directly from MediatR and distributed cache.
|
||||
/// </summary>
|
||||
public class EnvelopeReceiverPageDataService(
|
||||
IMediator mediator,
|
||||
IDistributedCache cache,
|
||||
IOptions<CacheOptions> cacheOptions)
|
||||
{
|
||||
private const string SignatureCacheKeyPrefix = "envelope-generator.receiver-ui.signature:";
|
||||
|
||||
/// <summary>
|
||||
/// Loads the PDF document bytes for the authenticated receiver.
|
||||
/// </summary>
|
||||
public async Task<byte[]?> GetDocumentAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var document = await mediator.Send(new ReadDocumentQuery(EnvelopeId: user.EnvelopeId()), cancellationToken);
|
||||
return document.ByteData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the current receiver's signature placeholders.
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<SignatureDto>> GetSignaturesAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var receiverId = user.ReceiverId();
|
||||
var document = await mediator.Send(new ReadDocumentQuery(EnvelopeId: user.EnvelopeId()), cancellationToken);
|
||||
|
||||
if (document.Elements is not IEnumerable<DocReceiverElementDto> elements)
|
||||
return [];
|
||||
|
||||
var signatures = elements
|
||||
.Where(element => element.ReceiverId == receiverId)
|
||||
.Select(MapSignature)
|
||||
.ToList();
|
||||
|
||||
return signatures.Convert(UnitOfLength.Point);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the envelope receiver data for the specified envelope key.
|
||||
/// </summary>
|
||||
public async Task<ApplicationEnvelopeReceiverDto?> GetEnvelopeReceiverAsync(string envelopeKey, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await mediator.Send(new ReadEnvelopeReceiverQuery { Key = envelopeKey }, cancellationToken);
|
||||
return result.SingleOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the cached signature for the authenticated receiver.
|
||||
/// </summary>
|
||||
public async Task<SignatureCaptureDto?> GetCachedSignatureAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var json = await cache.GetStringAsync(GetSignatureCacheKey(user), cancellationToken);
|
||||
return json is null ? null : JsonSerializer.Deserialize<SignatureCaptureDto>(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the cached signature for the authenticated receiver.
|
||||
/// </summary>
|
||||
public async Task SaveCachedSignatureAsync(ClaimsPrincipal user, SignatureCaptureDto signature, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(signature);
|
||||
var options = cacheOptions.Value.SignatureCacheExpiration.HasValue
|
||||
? new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheOptions.Value.SignatureCacheExpiration.Value }
|
||||
: new DistributedCacheEntryOptions();
|
||||
|
||||
await cache.SetStringAsync(GetSignatureCacheKey(user), json, options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the cached signature for the authenticated receiver.
|
||||
/// </summary>
|
||||
public Task DeleteCachedSignatureAsync(ClaimsPrincipal user, CancellationToken cancellationToken = default)
|
||||
=> cache.RemoveAsync(GetSignatureCacheKey(user), cancellationToken);
|
||||
|
||||
private static string GetSignatureCacheKey(ClaimsPrincipal user)
|
||||
=> $"{SignatureCacheKeyPrefix}{user.ReceiverSignature()}";
|
||||
|
||||
private static SignatureDto MapSignature(DocReceiverElementDto element) => new()
|
||||
{
|
||||
Id = element.Id,
|
||||
X = element.X,
|
||||
Y = element.Y,
|
||||
Page = element.Page,
|
||||
SenderAppType = (EnvelopeGenerator.Server.Client.Models.Constants.SenderAppType)element.SenderAppType
|
||||
};
|
||||
}
|
||||
@@ -584,6 +584,32 @@ body.resizing {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.envelope-dx-viewer-host {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 720px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.envelope-dx-pdf-viewer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 720px;
|
||||
}
|
||||
|
||||
.envelope-dx-pdf-viewer .dxbl-pdf-viewer,
|
||||
.envelope-dx-pdf-viewer .dxbl-pdfviewer,
|
||||
.envelope-dx-pdf-viewer .dxbl-pdf-viewer-container,
|
||||
.envelope-dx-pdf-viewer .dxbl-scroll-viewer,
|
||||
.envelope-dx-pdf-viewer .dxbl-scroll-viewer-content,
|
||||
.envelope-dx-pdf-viewer .dxbl-pdf-viewer-content,
|
||||
.envelope-dx-pdf-viewer .dxbl-pdfviewer-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pdf-page-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
@@ -641,6 +667,11 @@ body.resizing {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.pdf-signature-layer--dx {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pdf-signature-layer .signature-button {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@
|
||||
"auth-hub": {
|
||||
"Destinations": {
|
||||
"primary": {
|
||||
"Address": "https://localhost:9090"
|
||||
"Address": "http://172.24.12.39:9090"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B29
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
COPILOT_CONTEXT.md = COPILOT_CONTEXT.md
|
||||
FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md
|
||||
OPEN_SSR_TASK.md = OPEN_SSR_TASK.md
|
||||
RECEIVER_PDF_VIEWER_CONTEXT.md = RECEIVER_PDF_VIEWER_CONTEXT.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CBC2432-A561-4440-89BC-671B66A24146}"
|
||||
|
||||
454
MIGRATION_PLAN.md
Normal file
454
MIGRATION_PLAN.md
Normal file
@@ -0,0 +1,454 @@
|
||||
# DevExpress DxPdfViewer Migration Plan
|
||||
## EnvelopeReceiverPage.razor - PDF.js to DevExpress v25.2.3
|
||||
|
||||
**Created:** June 30, 2026
|
||||
**Target File:** `EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
|
||||
**Current Implementation:** PDF.js 3.11.174 with custom JavaScript overlays
|
||||
**Target Implementation:** DevExpress DxPdfViewer v25.2.3 with hybrid approach
|
||||
|
||||
---
|
||||
|
||||
## Confirmed DevExpress v25.2.3 API
|
||||
|
||||
### Available Properties
|
||||
- `DocumentContent` (byte[]) - Two-way bindable
|
||||
- `ZoomLevel` (double) - Two-way bindable, **factor not percentage** (1.5 = 150%)
|
||||
- `ActivePageIndex` (int) - **Read-only**, 0-based
|
||||
- `PageCount` (int) - **Read-only**
|
||||
- `IsSinglePagePreview` (bool)
|
||||
- `CssClass` (string)
|
||||
- `DocumentName` (string)
|
||||
- `SizeMode` (SizeMode?)
|
||||
|
||||
### Available Events
|
||||
- `CustomizeToolbar` - Toolbar customization
|
||||
- `ZoomLevelChanged` - EventCallback<double> when zoom changes
|
||||
|
||||
### Available Methods
|
||||
- `PrintAsync()` - Browser print dialog
|
||||
- `DownloadAsync()` - Download PDF
|
||||
|
||||
### NOT Available (Critical Gaps)
|
||||
- ❌ No `GoToPageAsync()` or any programmatic page navigation
|
||||
- ❌ No `ActivePageIndexChanged` or `PageNumberChanged` event
|
||||
- ❌ `ActivePageIndex` is read-only (cannot set programmatically)
|
||||
- ❌ No way to detect user scrolling between pages
|
||||
|
||||
---
|
||||
|
||||
## Migration Strategy: Hybrid Approach
|
||||
|
||||
Given the API limitations, we'll use a **hybrid approach**:
|
||||
|
||||
1. **DevExpress DxPdfViewer** for PDF rendering
|
||||
2. **Custom toolbar** via `CustomizeToolbar` event
|
||||
3. **Manual state tracking** for current page/zoom
|
||||
4. **ZoomLevelChanged event** for zoom synchronization
|
||||
5. **JavaScript overlays** for signature buttons (same as PDF.js implementation)
|
||||
6. **Graceful degradation** for features that can't be implemented
|
||||
|
||||
### What Works
|
||||
✅ PDF rendering with DevExpress
|
||||
✅ Custom zoom controls via toolbar
|
||||
✅ Zoom level synchronization via `ZoomLevelChanged` event
|
||||
✅ Signature button overlays (JavaScript, same as current)
|
||||
✅ Signature capture workflow (unchanged)
|
||||
✅ Page count display
|
||||
|
||||
### What Has Limitations
|
||||
⚠️ **Page navigation** - Custom toolbar buttons only (no thumbnail click navigation to viewer)
|
||||
⚠️ **Page tracking** - Manual state only (no event when user scrolls in native viewer)
|
||||
⚠️ **Thumbnail navigation** - Updates state but cannot move viewer to that page
|
||||
⚠️ **Signature button clicks** - Cannot navigate viewer to signature page programmatically
|
||||
|
||||
---
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Update Component Structure
|
||||
|
||||
**Current:**
|
||||
```razor
|
||||
<canvas id="pdfCanvas" style="@CanvasStyle"></canvas>
|
||||
```
|
||||
|
||||
**New:**
|
||||
```razor
|
||||
<DxPdfViewer @ref="_pdfViewer"
|
||||
DocumentContent="@_pdfDocumentContent"
|
||||
@bind-ZoomLevel="_viewerZoomLevel"
|
||||
ZoomLevelChanged="OnZoomLevelChanged"
|
||||
CustomizeToolbar="OnCustomizeToolbar"
|
||||
IsSinglePagePreview="true"
|
||||
CssClass="receiver-pdf-viewer" />
|
||||
```
|
||||
|
||||
### Step 2: Add Component Fields
|
||||
|
||||
```csharp
|
||||
private DxPdfViewer? _pdfViewer;
|
||||
private byte[]? _pdfDocumentContent;
|
||||
private double _viewerZoomLevel = 1.0; // DevExpress expects factor (1.0 = 100%)
|
||||
private int _currentPage = 1; // Manual tracking (1-based)
|
||||
private int _currentZoom = 100; // Manual tracking (percentage)
|
||||
private int _totalPages = 0;
|
||||
```
|
||||
|
||||
### Step 3: Implement CustomizeToolbar
|
||||
|
||||
```csharp
|
||||
protected void OnCustomizeToolbar(ToolbarModel toolbarModel)
|
||||
{
|
||||
toolbarModel.AllItems.Clear();
|
||||
|
||||
// Previous Page Button
|
||||
toolbarModel.AllItems.Add(new ToolbarItem
|
||||
{
|
||||
Text = "Previous",
|
||||
IconCssClass = "dx-icon-chevronprev",
|
||||
Enabled = _currentPage > 1,
|
||||
Click = async (args) =>
|
||||
{
|
||||
if (_currentPage > 1)
|
||||
{
|
||||
_currentPage--;
|
||||
await RefreshOverlaysAsync();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Page Info Display
|
||||
toolbarModel.AllItems.Add(new ToolbarItem
|
||||
{
|
||||
Text = $"Page {_currentPage} of {_totalPages}",
|
||||
BeginGroup = true
|
||||
});
|
||||
|
||||
// Next Page Button
|
||||
toolbarModel.AllItems.Add(new ToolbarItem
|
||||
{
|
||||
Text = "Next",
|
||||
IconCssClass = "dx-icon-chevronnext",
|
||||
Enabled = _currentPage < _totalPages,
|
||||
Click = async (args) =>
|
||||
{
|
||||
if (_currentPage < _totalPages)
|
||||
{
|
||||
_currentPage++;
|
||||
await RefreshOverlaysAsync();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Zoom Out Button
|
||||
toolbarModel.AllItems.Add(new ToolbarItem
|
||||
{
|
||||
IconCssClass = "dx-icon-minus",
|
||||
BeginGroup = true,
|
||||
Enabled = _currentZoom > 50,
|
||||
Click = async (args) =>
|
||||
{
|
||||
_currentZoom = Math.Max(_currentZoom - 10, 50);
|
||||
_viewerZoomLevel = _currentZoom / 100.0;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
});
|
||||
|
||||
// Zoom Display
|
||||
toolbarModel.AllItems.Add(new ToolbarItem
|
||||
{
|
||||
Text = $"{_currentZoom}%"
|
||||
});
|
||||
|
||||
// Zoom In Button
|
||||
toolbarModel.AllItems.Add(new ToolbarItem
|
||||
{
|
||||
IconCssClass = "dx-icon-plus",
|
||||
Enabled = _currentZoom < 300,
|
||||
Click = async (args) =>
|
||||
{
|
||||
_currentZoom = Math.Min(_currentZoom + 10, 300);
|
||||
_viewerZoomLevel = _currentZoom / 100.0;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
});
|
||||
|
||||
// Download Button
|
||||
toolbarModel.AllItems.Add(new ToolbarItem
|
||||
{
|
||||
IconCssClass = "dx-icon-download",
|
||||
BeginGroup = true,
|
||||
Click = async (args) =>
|
||||
{
|
||||
if (_pdfViewer != null)
|
||||
{
|
||||
await _pdfViewer.DownloadAsync();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Implement ZoomLevelChanged Event
|
||||
|
||||
```csharp
|
||||
private async Task OnZoomLevelChanged(double newZoomLevel)
|
||||
{
|
||||
// Synchronize manual tracking with DevExpress viewer
|
||||
_currentZoom = (int)Math.Round(newZoomLevel * 100);
|
||||
|
||||
// Refresh signature overlays with new zoom
|
||||
await RefreshOverlaysAsync();
|
||||
|
||||
// Force toolbar update to show new zoom percentage
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Load PDF Document
|
||||
|
||||
```csharp
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(_envelopeKey))
|
||||
{
|
||||
// Existing envelope loading logic...
|
||||
var envelope = await EnvelopeService.GetEnvelopeByKeyAsync(_envelopeKey);
|
||||
|
||||
if (envelope?.Document?.BinaryContent != null)
|
||||
{
|
||||
_pdfDocumentContent = envelope.Document.BinaryContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 6: Read PageCount After Render
|
||||
|
||||
```csharp
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (!_pdfLoaded && _pdfDocumentContent is { Length: > 0 })
|
||||
{
|
||||
// Wait for DevExpress to load PDF
|
||||
await Task.Delay(300);
|
||||
|
||||
if (_pdfViewer is not null && _pdfViewer.PageCount > 0)
|
||||
{
|
||||
_totalPages = _pdfViewer.PageCount;
|
||||
_pdfLoaded = true;
|
||||
|
||||
// Initial overlay render
|
||||
await RefreshOverlaysAsync();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 7: Signature Overlay Rendering (Keep JavaScript)
|
||||
|
||||
```csharp
|
||||
private async Task RefreshOverlaysAsync()
|
||||
{
|
||||
if (!_pdfLoaded || _signatures == null) return;
|
||||
|
||||
// Filter signatures for current page
|
||||
var currentPageSignatures = _signatures
|
||||
.Where(s => s.PageNumber == _currentPage)
|
||||
.ToList();
|
||||
|
||||
// Call JavaScript to render overlays (same as PDF.js implementation)
|
||||
await JSRuntime.InvokeVoidAsync(
|
||||
"pdfViewer.renderSignatureButtons",
|
||||
currentPageSignatures,
|
||||
_currentPage,
|
||||
DotNetObjectReference.Create(this)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 8: Handle Thumbnail Clicks (Best Effort)
|
||||
|
||||
```csharp
|
||||
[JSInvokable]
|
||||
public async Task OnThumbnailClick(int pageNumber)
|
||||
{
|
||||
if (pageNumber < 1 || pageNumber > _totalPages) return;
|
||||
|
||||
// Update manual state
|
||||
_currentPage = pageNumber;
|
||||
|
||||
// Refresh overlays
|
||||
await RefreshOverlaysAsync();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
// NOTE: DevExpress viewer will NOT navigate to this page
|
||||
// User must use custom toolbar buttons to navigate
|
||||
// This is a known limitation of v25.2.3
|
||||
}
|
||||
```
|
||||
|
||||
### Step 9: Handle Signature Button Clicks (Best Effort)
|
||||
|
||||
```csharp
|
||||
[JSInvokable]
|
||||
public async Task OnSignatureButtonClick(string signatureId)
|
||||
{
|
||||
var signature = _signatures?.FirstOrDefault(s => s.Id == signatureId);
|
||||
if (signature == null) return;
|
||||
|
||||
// Update to signature's page
|
||||
_currentPage = signature.PageNumber;
|
||||
|
||||
// Open signature modal
|
||||
_showSignatureModal = true;
|
||||
_selectedSignatureId = signatureId;
|
||||
|
||||
await RefreshOverlaysAsync();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
// NOTE: DevExpress viewer will NOT navigate to signature page
|
||||
// User sees modal but viewer stays on current page
|
||||
// This is a known limitation of v25.2.3
|
||||
}
|
||||
```
|
||||
|
||||
### Step 10: Update CSS for DevExpress
|
||||
|
||||
```css
|
||||
.receiver-pdf-viewer {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Signature overlay buttons (same as PDF.js) */
|
||||
.signature-button {
|
||||
position: absolute;
|
||||
border: 2px solid #0d6efd;
|
||||
background-color: rgba(13, 110, 253, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.signature-button:hover {
|
||||
background-color: rgba(13, 110, 253, 0.3);
|
||||
border-color: #0a58ca;
|
||||
}
|
||||
|
||||
.signature-button.signed {
|
||||
border-color: #198754;
|
||||
background-color: rgba(25, 135, 84, 0.1);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Phase 1: Basic PDF Rendering
|
||||
- [ ] PDF loads in DevExpress viewer
|
||||
- [ ] PageCount is correctly read
|
||||
- [ ] Initial zoom is 100%
|
||||
- [ ] Custom toolbar appears with all buttons
|
||||
|
||||
### Phase 2: Navigation
|
||||
- [ ] Previous button navigates (updates state)
|
||||
- [ ] Next button navigates (updates state)
|
||||
- [ ] Page info displays correctly
|
||||
- [ ] Buttons disable at first/last page
|
||||
|
||||
### Phase 3: Zoom
|
||||
- [ ] Zoom in button increases zoom
|
||||
- [ ] Zoom out button decreases zoom
|
||||
- [ ] Zoom display shows correct percentage
|
||||
- [ ] ZoomLevelChanged event fires
|
||||
- [ ] Buttons disable at 50%/300%
|
||||
|
||||
### Phase 4: Signature Overlays
|
||||
- [ ] Signature buttons render on correct positions
|
||||
- [ ] Overlays update when page changes (custom toolbar)
|
||||
- [ ] Overlays update when zoom changes
|
||||
- [ ] Click opens signature modal
|
||||
- [ ] Signed signatures show green border
|
||||
|
||||
### Phase 5: Signature Workflow
|
||||
- [ ] Draw signature works
|
||||
- [ ] Type signature works
|
||||
- [ ] Upload image works
|
||||
- [ ] Signature applies to PDF
|
||||
- [ ] Overlay updates to "signed" state
|
||||
|
||||
### Phase 6: Edge Cases
|
||||
- [ ] Multi-page PDF (10+ pages)
|
||||
- [ ] PDF with no signatures
|
||||
- [ ] PDF with multiple signatures on same page
|
||||
- [ ] Browser refresh preserves state
|
||||
- [ ] Mobile responsive layout
|
||||
|
||||
### Known Limitations to Document
|
||||
- [ ] User scrolling in viewer doesn't update custom toolbar page number
|
||||
- [ ] Thumbnail clicks don't navigate viewer (state updates only)
|
||||
- [ ] Signature button clicks don't navigate viewer to that page
|
||||
- [ ] Native DevExpress toolbar is hidden (custom toolbar only)
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If migration fails or critical issues are discovered:
|
||||
|
||||
1. Keep PDF.js files in `wwwroot/js/`
|
||||
2. Create branch `feature/devexpress-migration` before starting
|
||||
3. Master branch keeps PDF.js implementation
|
||||
4. Can revert by checking out master
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
Migration is successful if:
|
||||
|
||||
1. ✅ PDF renders correctly in DevExpress viewer
|
||||
2. ✅ Custom toolbar navigation works
|
||||
3. ✅ Zoom controls work and synchronize
|
||||
4. ✅ Signature overlays render correctly
|
||||
5. ✅ Signature capture and application works
|
||||
6. ✅ Performance is acceptable (no lag on 20+ page PDFs)
|
||||
7. ✅ Mobile/tablet layout works
|
||||
8. ⚠️ User is informed about navigation limitations (documentation/tooltips)
|
||||
|
||||
---
|
||||
|
||||
## Timeline Estimate
|
||||
|
||||
- **Step 1-2:** Component structure update - 30 minutes
|
||||
- **Step 3:** CustomizeToolbar implementation - 1 hour
|
||||
- **Step 4:** ZoomLevelChanged event - 30 minutes
|
||||
- **Step 5-6:** PDF loading and PageCount - 30 minutes
|
||||
- **Step 7:** Signature overlays - 1 hour (testing positioning)
|
||||
- **Step 8-9:** Thumbnail/signature navigation - 1 hour
|
||||
- **Step 10:** CSS updates - 30 minutes
|
||||
- **Testing:** Full checklist - 2 hours
|
||||
- **Documentation:** User-facing limitations - 30 minutes
|
||||
|
||||
**Total Estimated Time:** 7-8 hours
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. ✅ Verify DevExpress API capabilities (DONE - this document)
|
||||
2. ⬜ Create feature branch `feature/devexpress-migration`
|
||||
3. ⬜ Backup current EnvelopeReceiverPage.razor
|
||||
4. ⬜ Implement Steps 1-10
|
||||
5. ⬜ Complete testing checklist
|
||||
6. ⬜ Manual testing with real envelopes
|
||||
7. ⬜ Document known limitations for users
|
||||
8. ⬜ Merge to master or rollback based on results
|
||||
553
OPEN_SSR_TASK.md
553
OPEN_SSR_TASK.md
@@ -1,553 +0,0 @@
|
||||
# SSR Authentication Migration — Implementation Notes
|
||||
|
||||
## Overview
|
||||
Migration from WASM client-side authentication to SSR (Server-Side Rendering) authentication for `EnvelopeReceiverPage.razor` to fix authentication issues in Blazor InteractiveServer mode.
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
### Issue
|
||||
`EnvelopeReceiverPage.razor` uses `@rendermode InteractiveServer` but was calling **WASM client service** `AuthService.CheckEnvelopeAccessAsync()`:
|
||||
|
||||
```razor
|
||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||
|
||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||||
```
|
||||
|
||||
**Why This Failed:**
|
||||
- `AuthService` is a **WASM client service** that uses `IHttpClientFactory`
|
||||
- In SSR context, `HttpContext` is required to configure the base address
|
||||
- `CheckEnvelopeAccessAsync()` makes an HTTP request to `/api/auth/check/envelope/{key}`
|
||||
- This request **goes to itself** (server calling its own endpoint), causing issues
|
||||
- Returns `false` even when user is authenticated
|
||||
|
||||
---
|
||||
|
||||
## Solution Architecture
|
||||
|
||||
### Created New SSR Authentication Service
|
||||
|
||||
**Files Created:**
|
||||
1. `EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs` (Interface)
|
||||
2. `EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs` (Implementation)
|
||||
|
||||
**Purpose:** Direct `HttpContext.User` validation without HTTP requests
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. IEnvelopeAuthService Interface
|
||||
|
||||
**Location:** `EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs`
|
||||
|
||||
```csharp
|
||||
namespace EnvelopeGenerator.Server.Services;
|
||||
|
||||
public interface IEnvelopeAuthService
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if the current user is authenticated for the given envelope key.
|
||||
/// Validates both that the user is authenticated AND that the envelope key matches their claims.
|
||||
/// </summary>
|
||||
bool IsAuthenticated(string envelopeKey);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authenticated envelope key from the current user's claims (NameIdentifier or "sub" claim).
|
||||
/// </summary>
|
||||
string? GetAuthenticatedEnvelopeKey();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current HttpContext user principal.
|
||||
/// </summary>
|
||||
ClaimsPrincipal? GetCurrentUser();
|
||||
}
|
||||
```
|
||||
|
||||
**Key Methods:**
|
||||
- `IsAuthenticated(string envelopeKey)`: Validates user auth + envelope key match
|
||||
- `GetAuthenticatedEnvelopeKey()`: Extracts envelope key from claims
|
||||
- `GetCurrentUser()`: Returns `ClaimsPrincipal` for advanced scenarios
|
||||
|
||||
---
|
||||
|
||||
### 2. EnvelopeAuthService Implementation
|
||||
|
||||
**Location:** `EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs`
|
||||
|
||||
**Dependencies:**
|
||||
- `IHttpContextAccessor`: Access current HTTP context
|
||||
- `ILogger<EnvelopeAuthService>`: Structured logging
|
||||
|
||||
**Logic:**
|
||||
```csharp
|
||||
public bool IsAuthenticated(string envelopeKey)
|
||||
{
|
||||
// 1. Validate envelope key parameter
|
||||
if (string.IsNullOrWhiteSpace(envelopeKey))
|
||||
return false;
|
||||
|
||||
// 2. Get HttpContext
|
||||
var context = _httpContextAccessor.HttpContext;
|
||||
|
||||
// 3. Check if user is authenticated
|
||||
if (context?.User?.Identity?.IsAuthenticated != true)
|
||||
return false;
|
||||
|
||||
// 4. Extract envelope key from claims
|
||||
var sub = GetEnvelopeKeyFromClaims(context.User);
|
||||
|
||||
// 5. Verify match
|
||||
return sub == envelopeKey;
|
||||
}
|
||||
|
||||
private string? GetEnvelopeKeyFromClaims(ClaimsPrincipal user)
|
||||
{
|
||||
// Try standard claim first
|
||||
var sub = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
// Fallback to JWT "sub" claim
|
||||
if (string.IsNullOrWhiteSpace(sub))
|
||||
sub = user.FindFirst("sub")?.Value;
|
||||
|
||||
return sub;
|
||||
}
|
||||
```
|
||||
|
||||
**Claim Priority:**
|
||||
1. `ClaimTypes.NameIdentifier` (standard .NET claim)
|
||||
2. `"sub"` (JWT standard claim)
|
||||
|
||||
---
|
||||
|
||||
### 3. Service Registration
|
||||
|
||||
**Location:** `EnvelopeGenerator.Server/Program.cs`
|
||||
|
||||
**Added:**
|
||||
```csharp
|
||||
// SSR Authentication Service (for Envelope Receiver pages)
|
||||
builder.Services.AddScoped<EnvelopeGenerator.Server.Services.IEnvelopeAuthService,
|
||||
EnvelopeGenerator.Server.Services.EnvelopeAuthService>();
|
||||
```
|
||||
|
||||
**Lifetime:** `Scoped` (per-request, matches `IHttpContextAccessor`)
|
||||
|
||||
---
|
||||
|
||||
### 4. EnvelopeReceiverPage.razor Changes
|
||||
|
||||
**Changes Made (REVERTED - To Be Re-Applied):**
|
||||
|
||||
#### 4.1 Using Statements
|
||||
```razor
|
||||
@using EnvelopeGenerator.Server.Services
|
||||
```
|
||||
|
||||
#### 4.2 Dependency Injection
|
||||
**Old:**
|
||||
```razor
|
||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||
```
|
||||
|
||||
**New:**
|
||||
```razor
|
||||
@inject IEnvelopeAuthService EnvelopeAuth
|
||||
@inject IHttpClientFactory HttpClientFactory
|
||||
```
|
||||
|
||||
#### 4.3 Authentication Check in `OnInitializedAsync()`
|
||||
**Old:**
|
||||
```csharp
|
||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||||
if (!hasAccess) {
|
||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**New:**
|
||||
```csharp
|
||||
// ? SSR Authentication check via service
|
||||
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
|
||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ? Synchronous (no HTTP overhead)
|
||||
- ? Direct `HttpContext.User` access
|
||||
- ? No self-referencing HTTP calls
|
||||
- ? Works in SSR context
|
||||
|
||||
#### 4.4 Logout Method
|
||||
**Old:**
|
||||
```csharp
|
||||
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey);
|
||||
```
|
||||
|
||||
**New:**
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
// ? SSR: Direct HTTP call instead of WASM client service
|
||||
using var http = HttpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
await http.PostAsync($"/api/auth/logout/envelope/{Uri.EscapeDataString(EnvelopeKey)}", null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Logout failed for envelope {EnvelopeKey}", EnvelopeKey);
|
||||
}
|
||||
|
||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||
```
|
||||
|
||||
**Why Changed:**
|
||||
- WASM `AuthService.LogoutEnvelopeReceiverAsync()` doesn't work in SSR
|
||||
- Use named HttpClient `"EnvelopeGenerator.Server"` (configured in `Program.cs`)
|
||||
- Graceful error handling (logout errors shouldn't block redirect)
|
||||
|
||||
---
|
||||
|
||||
## Remaining Tasks
|
||||
|
||||
### ? Completed
|
||||
1. ? Created `IEnvelopeAuthService` interface
|
||||
2. ? Implemented `EnvelopeAuthService` with `HttpContext` access
|
||||
3. ? Registered service in `Program.cs`
|
||||
4. ?? **REVERTED** `EnvelopeReceiverPage.razor` changes (merge conflict)
|
||||
|
||||
### ? TODO (Next Agent)
|
||||
|
||||
#### 1. Re-apply EnvelopeReceiverPage.razor Changes
|
||||
**File:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
|
||||
|
||||
**Steps:**
|
||||
1. Add using statement:
|
||||
```razor
|
||||
@using EnvelopeGenerator.Server.Services
|
||||
```
|
||||
|
||||
2. Replace injection:
|
||||
```razor
|
||||
@inject IEnvelopeAuthService EnvelopeAuth
|
||||
@inject IHttpClientFactory HttpClientFactory
|
||||
```
|
||||
|
||||
Remove:
|
||||
```razor
|
||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||
```
|
||||
|
||||
3. Update `OnInitializedAsync()` authentication check:
|
||||
```csharp
|
||||
// Replace this:
|
||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||||
if (!hasAccess) {
|
||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||
return;
|
||||
}
|
||||
|
||||
// With this:
|
||||
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
|
||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
4. Update `LogoutAsync()` method:
|
||||
```csharp
|
||||
async Task LogoutAsync() {
|
||||
if (string.IsNullOrWhiteSpace(EnvelopeKey) || _isLoggingOut) return;
|
||||
_isLoggingOut = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
// ? SSR: Direct HTTP call instead of WASM client service
|
||||
using var http = HttpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
||||
await http.PostAsync($"/api/auth/logout/envelope/{Uri.EscapeDataString(EnvelopeKey)}", null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Logout failed for envelope {EnvelopeKey}", EnvelopeKey);
|
||||
}
|
||||
|
||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Test Authentication Flow
|
||||
**Scenarios:**
|
||||
- ? Valid cookie ? Page loads
|
||||
- ? Invalid cookie ? Redirect to login
|
||||
- ? No cookie ? Redirect to login
|
||||
- ? Envelope key mismatch ? Redirect to login
|
||||
- ? Logout ? Cookie cleared, redirect to login
|
||||
|
||||
#### 3. Remove WASM Client Services from SSR Pages
|
||||
**Optional Cleanup:**
|
||||
- Review other SSR pages (`EnvelopeReceiverPage_DxPdfViewer.razor`, etc.)
|
||||
- Replace WASM client services with SSR equivalents where applicable
|
||||
- Document which services are WASM-only vs SSR-compatible
|
||||
|
||||
---
|
||||
|
||||
## Authentication Flow Comparison
|
||||
|
||||
### ? Old Flow (WASM Client Service in SSR)
|
||||
```
|
||||
EnvelopeReceiverPage (@rendermode InteractiveServer)
|
||||
?
|
||||
AuthService.CheckEnvelopeAccessAsync() (WASM client)
|
||||
?
|
||||
IHttpClientFactory.CreateClient("EnvelopeGenerator.Server")
|
||||
?
|
||||
GET /api/auth/check/envelope/{key}
|
||||
?
|
||||
[SELF-REFERENCING REQUEST - FAILS]
|
||||
?
|
||||
Returns false even when authenticated
|
||||
```
|
||||
|
||||
### ? New Flow (SSR Service)
|
||||
```
|
||||
EnvelopeReceiverPage (@rendermode InteractiveServer)
|
||||
?
|
||||
IEnvelopeAuthService.IsAuthenticated(envelopeKey)
|
||||
?
|
||||
IHttpContextAccessor.HttpContext.User (Direct access)
|
||||
?
|
||||
ClaimsPrincipal.FindFirst("sub" or NameIdentifier)
|
||||
?
|
||||
Compare with envelopeKey
|
||||
?
|
||||
Return true/false (synchronous, no HTTP)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Technical Decisions
|
||||
|
||||
### Why Not Use `[Authorize]` Attribute?
|
||||
- Blazor SSR components **don't support** `[Authorize]` at component level
|
||||
- Would require `<AuthorizeView>` component (less clean)
|
||||
- Custom service provides more control + logging
|
||||
|
||||
### Why Scoped Lifetime?
|
||||
- `IHttpContextAccessor` is scoped (per-request)
|
||||
- `EnvelopeAuthService` depends on `IHttpContextAccessor`
|
||||
- Scoped ensures same `HttpContext` throughout request
|
||||
|
||||
### Why Two Claims (`NameIdentifier` + `"sub"`)?
|
||||
- **`NameIdentifier`**: Standard .NET claim type
|
||||
- **`"sub"`**: JWT standard claim
|
||||
- Fallback ensures compatibility with different token formats
|
||||
|
||||
---
|
||||
|
||||
## Logging & Debugging
|
||||
|
||||
### Log Levels
|
||||
- **Debug:** Successful authentication
|
||||
- **Warning:** Null envelope key, key mismatch
|
||||
- **Error:** (Reserved for future exceptions)
|
||||
|
||||
### Sample Logs
|
||||
```
|
||||
[Debug] User authenticated for envelope 517bb9c5-6082-4e61-aaa5-9846386e67ee
|
||||
[Warning] Envelope key mismatch: Expected abc123, Got 517bb9c5-6082-4e61-aaa5-9846386e67ee
|
||||
[Warning] IsAuthenticated called with null or empty envelope key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Unit Tests (TODO)
|
||||
```csharp
|
||||
// EnvelopeGenerator.Tests/Services/EnvelopeAuthServiceTests.cs
|
||||
[Fact]
|
||||
public void IsAuthenticated_ValidUser_ReturnsTrue() { ... }
|
||||
|
||||
[Fact]
|
||||
public void IsAuthenticated_InvalidKey_ReturnsFalse() { ... }
|
||||
|
||||
[Fact]
|
||||
public void IsAuthenticated_UnauthenticatedUser_ReturnsFalse() { ... }
|
||||
|
||||
[Fact]
|
||||
public void GetAuthenticatedEnvelopeKey_ValidUser_ReturnsKey() { ... }
|
||||
```
|
||||
|
||||
### Integration Tests (Manual)
|
||||
1. ? Login with valid access code ? Cookie set
|
||||
2. ? Navigate to `/envelope/{key}` ? Page loads
|
||||
3. ? Logout ? Cookie cleared, redirect
|
||||
4. ? Try accessing `/envelope/{key}` without cookie ? Redirect to login
|
||||
5. ? Try accessing `/envelope/{wrongKey}` with valid cookie ? Redirect to login
|
||||
|
||||
---
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
- [x] Create `IEnvelopeAuthService` interface
|
||||
- [x] Implement `EnvelopeAuthService`
|
||||
- [x] Register service in `Program.cs`
|
||||
- [ ] **Re-apply** `EnvelopeReceiverPage.razor` changes (after merge)
|
||||
- [ ] Test authentication flow
|
||||
- [ ] Add unit tests
|
||||
- [ ] Update other SSR pages (if needed)
|
||||
- [ ] Document in `COPILOT_CONTEXT.md`
|
||||
|
||||
---
|
||||
|
||||
## Documentation Updates Needed
|
||||
|
||||
### COPILOT_CONTEXT.md
|
||||
|
||||
**Add Section:**
|
||||
```markdown
|
||||
## SSR Authentication Service
|
||||
|
||||
**Purpose:** Server-side authentication for Blazor InteractiveServer pages.
|
||||
|
||||
**Location:** `EnvelopeGenerator.Server/Services/`
|
||||
|
||||
**Service:** `IEnvelopeAuthService` / `EnvelopeAuthService`
|
||||
|
||||
**Usage:**
|
||||
```razor
|
||||
@inject IEnvelopeAuthService EnvelopeAuth
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
|
||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why Not Use WASM Client Services in SSR?**
|
||||
- WASM client services use `IHttpClientFactory` with base address configuration
|
||||
- SSR context requires `HttpContext` to configure base address
|
||||
- Calling API endpoints from server-side component creates self-referencing requests
|
||||
- Use `IEnvelopeAuthService` for direct `HttpContext.User` access instead
|
||||
|
||||
**Authentication Flow:**
|
||||
1. JWT token stored in per-envelope cookie (`AuthTokenSignFLOWReceiver.{envelopeKey}`)
|
||||
2. JWT middleware validates token, sets `HttpContext.User`
|
||||
3. `EnvelopeAuthService` checks `ClaimsPrincipal.FindFirst("sub")` or `NameIdentifier`
|
||||
4. Compares claim value with route parameter `{EnvelopeKey}`
|
||||
|
||||
**Claim Priority:**
|
||||
1. `ClaimTypes.NameIdentifier` (standard .NET)
|
||||
2. `"sub"` (JWT standard)
|
||||
|
||||
**Service Lifetime:** Scoped (per-request)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
### ? Don't Do This
|
||||
```csharp
|
||||
// SSR page using WASM client service
|
||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
||||
|
||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
||||
```
|
||||
|
||||
**Why Wrong:**
|
||||
- Creates self-referencing HTTP request
|
||||
- WASM client service doesn't work in SSR context
|
||||
- Always returns `false` even when authenticated
|
||||
|
||||
### ? Do This Instead
|
||||
```csharp
|
||||
// SSR page using SSR authentication service
|
||||
@inject IEnvelopeAuthService EnvelopeAuth
|
||||
|
||||
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
|
||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**Why Correct:**
|
||||
- Direct `HttpContext.User` access
|
||||
- Synchronous (no HTTP overhead)
|
||||
- Works in SSR context
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
### Related Files
|
||||
- `EnvelopeGenerator.Server/Program.cs` (Service registration, JWT middleware)
|
||||
- `EnvelopeGenerator.Server.Client/Services/AuthService.cs` (WASM client version)
|
||||
- `EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor` (WASM login page)
|
||||
- `EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` (SSR viewer page)
|
||||
|
||||
### JWT Configuration
|
||||
**File:** `EnvelopeGenerator.Server/Program.cs`
|
||||
|
||||
```csharp
|
||||
.AddJwtBearer(AuthScheme.Receiver, opt =>
|
||||
{
|
||||
opt.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
var envelopeKey = context.Request.Path.Value?.Split('/').LastOrDefault();
|
||||
if (envelopeKey is not null)
|
||||
{
|
||||
var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey);
|
||||
if (context.Request.Cookies.TryGetValue(cookieName, out var cookieToken))
|
||||
context.Token = cookieToken;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
OnTokenValidated = context =>
|
||||
{
|
||||
var envelopeKey = context.Request.Path.Value?.Split('/').LastOrDefault();
|
||||
var sub = context.Principal?.FindFirst("sub")?.Value;
|
||||
|
||||
if (envelopeKey is null || sub != envelopeKey)
|
||||
context.Fail("Envelope key mismatch");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**What Was Done:**
|
||||
1. Created SSR authentication service (`IEnvelopeAuthService` / `EnvelopeAuthService`)
|
||||
2. Registered service in DI container
|
||||
3. Updated `EnvelopeReceiverPage.razor` (REVERTED due to merge)
|
||||
|
||||
**What's Left:**
|
||||
1. **Re-apply** `EnvelopeReceiverPage.razor` changes after merge
|
||||
2. Test authentication flow
|
||||
3. Add unit tests
|
||||
4. Update documentation
|
||||
|
||||
**Key Insight:**
|
||||
- **WASM client services ? SSR server services**
|
||||
- Use `IHttpContextAccessor` for direct `HttpContext.User` access in SSR
|
||||
- Avoid HTTP requests from server-side components to own endpoints
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** 2025-01-27
|
||||
**Status:** ?? Partial (Service created, page changes reverted for merge)
|
||||
**Next Agent:** Re-apply `EnvelopeReceiverPage.razor` changes + testing
|
||||
1272
RECEIVER_PDF_VIEWER_CONTEXT.md
Normal file
1272
RECEIVER_PDF_VIEWER_CONTEXT.md
Normal file
File diff suppressed because it is too large
Load Diff
290
SESSION_SUMMARY.md
Normal file
290
SESSION_SUMMARY.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# Session Summary - DevExpress Migration Research
|
||||
**Date:** June 30, 2026
|
||||
**Task:** Research and plan PDF.js to DevExpress DxPdfViewer migration
|
||||
|
||||
---
|
||||
|
||||
## What We Accomplished
|
||||
|
||||
### 1. Documentation Review & Translation ✅
|
||||
- Translated all Turkish sections in `DEBUG_NOTES.md` to English
|
||||
- Translated all Turkish sections in `DEVEXPRESS_V25_LIMITATIONS.md` to English
|
||||
- Translated all Turkish sections in `TESTING_CHECKLIST.md` to English
|
||||
- Fixed character encoding issues (? symbols → proper characters)
|
||||
|
||||
### 2. API Verification ✅
|
||||
- Verified DevExpress DxPdfViewer v25.2.3 API from official documentation
|
||||
- **Discovered:** `ZoomLevelChanged` event IS available (contrary to initial documentation)
|
||||
- **Confirmed:** No `ActivePageIndexChanged` or `PageNumberChanged` events
|
||||
- **Confirmed:** `ActivePageIndex` is read-only (no programmatic page navigation)
|
||||
- **Confirmed:** No `GoToPageAsync()` or similar methods
|
||||
|
||||
### 3. Complete API Documentation ✅
|
||||
|
||||
**Available in v25.2.3:**
|
||||
- `DocumentContent` (byte[]) - Two-way bindable
|
||||
- `ZoomLevel` (double) - Two-way bindable, factor not percentage
|
||||
- `ActivePageIndex` (int) - Read-only, 0-based
|
||||
- `PageCount` (int) - Read-only
|
||||
- `CustomizeToolbar` event - Toolbar customization
|
||||
- `ZoomLevelChanged` event - EventCallback<double>
|
||||
- `PrintAsync()` method
|
||||
- `DownloadAsync()` method
|
||||
|
||||
**NOT Available:**
|
||||
- ❌ No page navigation API (GoToPageAsync, etc.)
|
||||
- ❌ No ActivePageIndexChanged event
|
||||
- ❌ ActivePageIndex is read-only (cannot set)
|
||||
|
||||
### 4. Migration Strategy Defined ✅
|
||||
|
||||
**Chosen Approach:** Hybrid Implementation
|
||||
- DevExpress DxPdfViewer for PDF rendering
|
||||
- Custom toolbar via `CustomizeToolbar` event
|
||||
- Manual state tracking for current page/zoom
|
||||
- `ZoomLevelChanged` event for zoom synchronization
|
||||
- JavaScript overlays for signature buttons (keep existing)
|
||||
- Graceful degradation for unavailable features
|
||||
|
||||
**What Works:**
|
||||
- ✅ PDF rendering with DevExpress
|
||||
- ✅ Custom zoom controls via toolbar
|
||||
- ✅ Zoom level synchronization via event
|
||||
- ✅ Signature overlays (JavaScript, unchanged)
|
||||
- ✅ Signature capture workflow (unchanged)
|
||||
|
||||
**Known Limitations:**
|
||||
- ⚠️ User scrolling in viewer doesn't trigger C# page tracking
|
||||
- ⚠️ Thumbnail clicks can't navigate viewer (only state updates)
|
||||
- ⚠️ Signature button clicks can't navigate viewer to that page
|
||||
- ⚠️ Must use custom toolbar for navigation
|
||||
|
||||
### 5. Complete Implementation Plan Created ✅
|
||||
|
||||
**Created:** `MIGRATION_PLAN.md` with:
|
||||
- Step-by-step implementation guide (10 steps)
|
||||
- Complete code examples for each step
|
||||
- Testing checklist (30+ items)
|
||||
- Success criteria
|
||||
- Rollback plan
|
||||
- Timeline estimate: 7-8 hours
|
||||
|
||||
### 6. Updated Documentation ✅
|
||||
|
||||
**Updated:** `DEVEXPRESS_V25_LIMITATIONS.md`
|
||||
- Removed `ZoomLevelChanged` from "Missing Events" section
|
||||
- Added to "Available Events" section
|
||||
- Corrected notes about AI-suggested APIs
|
||||
|
||||
---
|
||||
|
||||
## Key Decisions Made
|
||||
|
||||
### Decision 1: Hybrid Approach vs Multi-Instance
|
||||
**Chosen:** Hybrid (single DxPdfViewer + custom toolbar)
|
||||
**Rejected:** Multi-instance (separate viewer per page)
|
||||
**Reason:** Memory/performance concerns for 30+ page PDFs
|
||||
|
||||
### Decision 2: CustomizeToolbar for Navigation
|
||||
**Chosen:** Custom toolbar buttons for page navigation
|
||||
**Alternative:** Extract single page to byte[] and swap DocumentContent
|
||||
**Reason:** More maintainable, better UX, proven pattern
|
||||
|
||||
### Decision 3: Keep JavaScript Overlays
|
||||
**Chosen:** Continue using PDF.js overlay JavaScript
|
||||
**Alternative:** Pure Blazor overlays with absolute positioning
|
||||
**Reason:** Already working, coordinate system already solved, no rework needed
|
||||
|
||||
### Decision 4: ZoomLevelChanged Event Utilization
|
||||
**Chosen:** Use `ZoomLevelChanged` to synchronize overlays
|
||||
**Alternative:** Manual zoom tracking only
|
||||
**Reason:** Native zoom controls in DevExpress toolbar will trigger event, keeping overlays in sync
|
||||
|
||||
---
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
### Created:
|
||||
1. `MIGRATION_PLAN.md` - Complete step-by-step migration guide
|
||||
2. `SESSION_SUMMARY.md` - This file
|
||||
|
||||
### Modified:
|
||||
1. `DEBUG_NOTES.md` - Translated Turkish → English
|
||||
2. `DEVEXPRESS_V25_LIMITATIONS.md` - Translated + corrected ZoomLevelChanged availability
|
||||
3. `TESTING_CHECKLIST.md` - Translated Turkish → English
|
||||
|
||||
---
|
||||
|
||||
## Critical Findings
|
||||
|
||||
### Finding 1: ZoomLevelChanged Event Available
|
||||
**Impact:** Medium-High
|
||||
**Details:** Initial documentation incorrectly stated this event was missing. It IS available in v25.2.3, which improves zoom synchronization capabilities.
|
||||
|
||||
**Benefit:** Can detect when user uses native DevExpress zoom controls and update signature overlays accordingly.
|
||||
|
||||
### Finding 2: No Page Navigation API
|
||||
**Impact:** High
|
||||
**Details:** Absolutely no way to programmatically navigate viewer to specific page. `ActivePageIndex` is read-only, no methods available.
|
||||
|
||||
**Workaround:** Custom toolbar buttons that update manual state tracking. User must use these buttons instead of scrolling or native viewer navigation.
|
||||
|
||||
### Finding 3: No Page Change Event
|
||||
**Impact:** Medium
|
||||
**Details:** When user scrolls in native viewer, no C# event fires. Cannot detect page changes.
|
||||
|
||||
**Workaround:** Disable native scrolling via `IsSinglePagePreview="true"`, force use of custom toolbar buttons only.
|
||||
|
||||
---
|
||||
|
||||
## Remaining Work
|
||||
|
||||
### Immediate Next Steps:
|
||||
1. Create feature branch `feature/devexpress-migration`
|
||||
2. Backup current `EnvelopeReceiverPage.razor`
|
||||
3. Begin implementation following `MIGRATION_PLAN.md` steps 1-10
|
||||
|
||||
### Testing Required:
|
||||
1. Basic PDF rendering (Phase 1)
|
||||
2. Navigation with custom toolbar (Phase 2)
|
||||
3. Zoom synchronization (Phase 3)
|
||||
4. Signature overlays (Phase 4)
|
||||
5. Full signature workflow (Phase 5)
|
||||
6. Edge cases and limitations (Phase 6)
|
||||
|
||||
### Documentation Required:
|
||||
1. User-facing documentation about navigation limitations
|
||||
2. Update `RECEIVER_PDF_VIEWER_CONTEXT.md` with DevExpress details
|
||||
3. Add tooltips/help text in UI explaining custom toolbar usage
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
### Low Risk ✅
|
||||
- PDF rendering - DevExpress is proven
|
||||
- Signature capture - No changes to existing modal
|
||||
- Zoom controls - ZoomLevelChanged event available
|
||||
- Signature overlays - Keep existing JavaScript
|
||||
|
||||
### Medium Risk ⚠️
|
||||
- Custom toolbar UX - Users must learn new navigation pattern
|
||||
- Page tracking - Manual state only, can desync if user finds way to scroll
|
||||
- Performance - DevExpress rendering speed vs PDF.js unknown
|
||||
|
||||
### High Risk ❌
|
||||
- None identified - Rollback plan exists if critical issues found
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
**Migration is successful if:**
|
||||
1. All signature workflows complete successfully
|
||||
2. PDF rendering performance is acceptable (<2s for 20-page PDF)
|
||||
3. No critical bugs in signature placement
|
||||
4. Custom toolbar navigation works reliably
|
||||
5. Mobile/tablet layout works
|
||||
6. Users can complete signing workflow without confusion
|
||||
|
||||
**Migration is failed if:**
|
||||
1. Signature positioning is broken
|
||||
2. Performance is significantly worse than PDF.js
|
||||
3. Critical workflow steps are blocked
|
||||
4. Mobile layout is unusable
|
||||
|
||||
→ If failed, rollback to PDF.js implementation on master branch
|
||||
|
||||
---
|
||||
|
||||
## Questions Answered
|
||||
|
||||
### Q1: Can we use DevExpress DxPdfViewer in v25.2.3?
|
||||
**A:** Yes, but with significant API limitations requiring workarounds.
|
||||
|
||||
### Q2: Can we programmatically navigate to specific pages?
|
||||
**A:** No. Must use custom toolbar buttons with manual state tracking.
|
||||
|
||||
### Q3: Can we detect when user changes pages or zoom?
|
||||
**A:** Zoom yes (`ZoomLevelChanged` event), page changes no.
|
||||
|
||||
### Q4: Do we need to rewrite signature overlay logic?
|
||||
**A:** No. Keep existing PDF.js JavaScript for overlays.
|
||||
|
||||
### Q5: How long will migration take?
|
||||
**A:** Estimated 7-8 hours implementation + testing.
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### For Immediate Action:
|
||||
1. ✅ Proceed with migration using hybrid approach
|
||||
2. ✅ Use `MIGRATION_PLAN.md` as implementation guide
|
||||
3. ✅ Create feature branch before starting
|
||||
4. ✅ Test thoroughly with multi-page PDFs and multiple signatures
|
||||
|
||||
### For Future Consideration:
|
||||
1. Monitor DevExpress v26.x for API improvements (page navigation, events)
|
||||
2. Consider user feedback on custom toolbar navigation
|
||||
3. Evaluate if `IsSinglePagePreview="true"` provides best UX
|
||||
4. Consider adding help tooltips for navigation buttons
|
||||
|
||||
### Not Recommended:
|
||||
1. ❌ Multi-instance approach (memory concerns)
|
||||
2. ❌ Full rewrite of signature overlay logic (unnecessary)
|
||||
3. ❌ Trying to hack `ActivePageIndex` with reflection (breaks on updates)
|
||||
4. ❌ JavaScript interop to control DevExpress viewer (unsupported)
|
||||
|
||||
---
|
||||
|
||||
## Technical Debt Notes
|
||||
|
||||
### Introduced by Migration:
|
||||
1. Manual page state tracking (desync possible if native scrolling enabled)
|
||||
2. Custom toolbar implementation (must maintain alongside DevExpress updates)
|
||||
3. Mixed Blazor/JavaScript architecture (overlays in JS, viewer in Blazor)
|
||||
|
||||
### Mitigated by Migration:
|
||||
1. PDF.js version management (DevExpress handles PDF rendering)
|
||||
2. PDF.js security updates (DevExpress responsibility)
|
||||
3. Cross-browser PDF rendering quirks (DevExpress tested)
|
||||
|
||||
### Neutral:
|
||||
1. Signature capture logic (unchanged)
|
||||
2. Coordinate system complexity (unchanged, still INCHES in DB)
|
||||
3. Redis/SQL signature caching (unchanged)
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Research Phase: COMPLETE ✅**
|
||||
|
||||
We have thoroughly researched the DevExpress DxPdfViewer v25.2.3 API, identified all limitations, designed a viable hybrid migration strategy, and created a complete implementation plan.
|
||||
|
||||
**Key Insight:** The migration is feasible with acceptable tradeoffs. The main limitation (no programmatic page navigation) can be worked around with custom toolbar buttons. The availability of `ZoomLevelChanged` event is a positive discovery that improves the solution.
|
||||
|
||||
**Recommendation:** Proceed with migration following `MIGRATION_PLAN.md`.
|
||||
|
||||
**Next Session:** Begin implementation starting with Steps 1-2 (component structure).
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Session Statistics
|
||||
|
||||
- Documentation pages reviewed: 5+ DevExpress documentation pages
|
||||
- Files translated: 3 (DEBUG_NOTES.md, DEVEXPRESS_V25_LIMITATIONS.md, TESTING_CHECKLIST.md)
|
||||
- Files created: 2 (MIGRATION_PLAN.md, SESSION_SUMMARY.md)
|
||||
- Files updated: 3
|
||||
- API endpoints verified: 15+
|
||||
- Code examples written: 10+
|
||||
- Test cases defined: 30+
|
||||
- Estimated implementation time: 7-8 hours
|
||||
- Estimated testing time: 2 hours
|
||||
|
||||
**Total Documentation:** ~500 lines of comprehensive migration guidance
|
||||
|
||||
---
|
||||
|
||||
**End of Session Summary**
|
||||
161
TESTING_CHECKLIST.md
Normal file
161
TESTING_CHECKLIST.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# DevExpress v25.2.3 - Testing Checklist
|
||||
|
||||
> **Important:** This checklist has been updated according to the verified real API for v25.2.3.
|
||||
> `GoToPageAsync()`, `PageNumberChanged`, `ZoomLevelChanged`, `ToolbarVisible` do NOT exist.
|
||||
|
||||
## Build Status
|
||||
- **Build:** Successful
|
||||
- **DevExpress Version:** 25.2.3
|
||||
- **Strategy:** `CustomizeToolbar` + manual state tracking
|
||||
|
||||
---
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### 1. PDF Loading
|
||||
- [ ] PDF document loads successfully
|
||||
- [ ] DevExpress PdfViewer displays the document
|
||||
- [ ] `_pdfViewer.PageCount > 0` check passes
|
||||
- [ ] `_totalPages = _pdfViewer.PageCount` gets correct value (no JS call needed)
|
||||
- [ ] `_pdfLoaded = true` is set
|
||||
- [ ] Toolbar is visible and shows correct page count (e.g., "Page 1 / 5")
|
||||
- [ ] Zoom level displays correctly (e.g., "150%") – if `_viewerZoomLevel = 1.5`
|
||||
|
||||
### 2. CustomizeToolbar Navigation
|
||||
- [ ] **Previous Page button** (◀) works and updates page counter
|
||||
- [ ] **Next Page button** (▶) works and updates page counter
|
||||
- [ ] Previous button is **disabled on page 1**
|
||||
- [ ] Next button is **disabled on last page**
|
||||
- [ ] Page counter updates correctly (e.g., "Page 2 / 5")
|
||||
- [ ] Each navigation button's Click handler calls `RenderSignatureButtonsAsync()`
|
||||
|
||||
### 3. CustomizeToolbar Zoom
|
||||
- [ ] **Zoom In button** (+) increases zoom
|
||||
- [ ] **Zoom Out button** (−) decreases zoom
|
||||
- [ ] `_viewerZoomLevel = _currentZoom / 100d` is calculated (150 → 1.5)
|
||||
- [ ] Viewer does NOT display **"15000%"** (incorrect value detection)
|
||||
- [ ] Zoom is constrained to 50% - 300% range
|
||||
- [ ] PDF viewer zoom level changes visually
|
||||
|
||||
### 4. Signature Overlay Rendering
|
||||
- [ ] **Signature placeholders** appear on correct pages
|
||||
- [ ] Overlays are positioned correctly over the PDF
|
||||
- [ ] Overlays **re-render after page changes**
|
||||
- [ ] Overlays **re-render after zoom changes**
|
||||
- [ ] Overlay sizes scale with zoom level
|
||||
|
||||
### 5. Signature Navigation
|
||||
- [ ] Custom signature toolbar is visible (if signatures exist)
|
||||
- [ ] Previous/Next signature buttons work
|
||||
- [ ] Signature counter shows correct values (e.g., "0 / 3")
|
||||
- [ ] "X open" badge shows unsigned count
|
||||
- [ ] **Cross-page navigation:** `_currentPage` updates and overlays refresh
|
||||
- [ ] ⚠ **Known limitation:** DxPdfViewer visible page cannot be changed programmatically
|
||||
|
||||
### 6. Thumbnail Sidebar
|
||||
- [ ] Thumbnails render (may take a few seconds)
|
||||
- [ ] Thumbnail click **updates `_currentPage` state**
|
||||
- [ ] Thumbnail click **refreshes overlays**
|
||||
- [ ] ⚠ **Known limitation:** Thumbnail click does not navigate DevExpress viewer
|
||||
- [ ] Active thumbnail is highlighted correctly
|
||||
|
||||
### 7. Signature Capture & Application
|
||||
- [ ] Signature popup opens on first load (if no cache)
|
||||
- [ ] Draw signature works
|
||||
- [ ] Text signature works
|
||||
- [ ] Image upload signature works
|
||||
- [ ] Clicking signature placeholder applies signature
|
||||
- [ ] Applied signature overlays are positioned correctly
|
||||
- [ ] Counter updates after signature applied (e.g., "1 / 3")
|
||||
|
||||
### 8. Known Limitations (v25.2.3 API Limit)
|
||||
- [ ] ⚠ **User cannot scroll PDF to change pages** (only CustomizeToolbar buttons)
|
||||
- [ ] ⚠ Thumbnail clicks do not navigate viewer (only update state)
|
||||
- [ ] ⚠ Browser zoom gestures do not trigger overlay updates
|
||||
- [ ] ✓ Custom toolbar buttons correctly trigger overlay updates
|
||||
- [ ] ✓ `_pdfViewer.PageCount` eliminates need for JS `getTotalPages()` call
|
||||
- [ ] ✓ `ZoomLevel = _currentZoom / 100d` calculates correct zoom factor
|
||||
|
||||
---
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Issue: Toolbar not visible
|
||||
**Reason:** `_pdfLoaded = false` or `_totalPages = 0`
|
||||
**Solution:** In `OnAfterRenderAsync`, check `_pdfViewer.PageCount > 0`, set `_pdfLoaded = true`
|
||||
|
||||
```csharp
|
||||
if (_pdfViewer is not null && _pdfViewer.PageCount > 0)
|
||||
{
|
||||
_totalPages = _pdfViewer.PageCount; // no JS call needed
|
||||
_pdfLoaded = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
```
|
||||
|
||||
### Issue: Signature overlays not visible
|
||||
**Reason:** `RenderSignatureButtonsAsync()` not called after page/zoom change
|
||||
**Solution:** Verify each button Click handler in `OnCustomizeToolbar` calls `RenderSignatureButtonsAsync()`
|
||||
|
||||
### Issue: Page navigation not working
|
||||
**Reason:** `OnCustomizeToolbar` button Click lambdas not updating `_currentPage`
|
||||
**Solution:** Check that each button updates `_currentPage` and calls `StateHasChanged()` and `RenderSignatureButtonsAsync()`
|
||||
|
||||
### Issue: Zoom not working or showing "15000%"
|
||||
**Reason:** Incorrect value assigned to `_viewerZoomLevel`
|
||||
**Solution:** `_viewerZoomLevel = _currentZoom / 100d` – ZoomLevel takes **factor**, not percentage
|
||||
|
||||
```csharp
|
||||
// CORRECT
|
||||
_currentZoom = 150; // UI display: "150%"
|
||||
_viewerZoomLevel = 150 / 100d; // Pass to DxPdfViewer: 1.5
|
||||
|
||||
// WRONG
|
||||
_viewerZoomLevel = 150; // DxPdfViewer displays "15000%"
|
||||
```
|
||||
|
||||
### Issue: Total page count is 0
|
||||
**Reason:** JS call made before timing or fails
|
||||
**Solution:** Use `_pdfViewer.PageCount` directly, no need for `pdfViewer.getTotalPages()` JS call
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
**Minimum working functionality:**
|
||||
- PDF loads and displays
|
||||
- `_totalPages = _pdfViewer.PageCount` gets correct page count
|
||||
- Custom toolbar navigation works (previous/next/zoom)
|
||||
- Signature overlays render on current page
|
||||
- Signature capture and application works
|
||||
- Overlays update after navigation/zoom
|
||||
|
||||
**Known acceptable limitations (v25.2.3 API limit):**
|
||||
- Thumbnail clicks do not navigate viewer (only update state)
|
||||
- User scroll/native toolbar navigation does not update C# state
|
||||
- Cross-page signature navigation is limited
|
||||
|
||||
---
|
||||
|
||||
## Next Steps If All Tests Pass
|
||||
|
||||
1. Remove debug toolbar button (if present)
|
||||
2. Clean up unused code comments
|
||||
3. Update user documentation about navigation limitations
|
||||
|
||||
---
|
||||
|
||||
## Architecture Reference: Verified v25.2.3 API
|
||||
|
||||
| Property | Access | Usage |
|
||||
|----------|--------|-------|
|
||||
| `DocumentContent` | `[Parameter]` GET/SET | Feed PDF with `byte[]` |
|
||||
| `ZoomLevel` | `[Parameter]` GET/SET | **Factor**: `_currentZoom / 100d` |
|
||||
| `IsSinglePagePreview` | `[Parameter]` GET/SET | `true` = single page mode |
|
||||
| `PageCount` | GET only | Total pages – no JS needed |
|
||||
| `ActivePageIndex` | GET only | Active page index (0-based) – no SET |
|
||||
| `CssClass` | `[Parameter]` GET/SET | Assign CSS class |
|
||||
| `SizeMode` | `[Parameter]` GET/SET | `Small` / `Medium` / `Large` |
|
||||
| `DocumentName` | `[Parameter]` GET/SET | Download filename |
|
||||
|
||||
**Not available:** `GoToPageAsync()`, `PageNumberChanged`, `ZoomLevelChanged`, `ToolbarVisible`
|
||||
Reference in New Issue
Block a user