Compare commits
26 Commits
master
...
3a2fa77862
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a2fa77862 | |||
| cfa6dbd2de | |||
| eb2603f389 | |||
| 456178bee1 | |||
| 2c41c74510 | |||
| bb73795d68 | |||
| 207992d95a | |||
| d6bafc64a6 | |||
| 3090711892 | |||
| 9dbd8f7952 | |||
| 48a41f2987 | |||
| 96688a951c | |||
| 6f07de3ec4 | |||
| 4611266224 | |||
| c529d03129 | |||
| 829fab9647 | |||
| b2e3605b54 | |||
| 8cbdee2491 | |||
| 4281eaeb22 | |||
| 150fca5f47 | |||
| 1f889d8b58 | |||
| d599fe3156 | |||
| 6c40c48ac8 | |||
| 536b8ef5da | |||
| d35a35c75e | |||
| 7fb1a87cf2 |
@@ -1,72 +1,107 @@
|
|||||||
# EnvelopeGenerator — AI Context Reference
|
# EnvelopeGenerator — AI Context Reference
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
Digital document signing system with **unified Blazor WASM frontend** for both Senders and Receivers. Senders create envelopes and place signature fields. Receivers view PDFs, sign documents, export stamped PDFs.
|
Digital document signing system 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.
|
||||||
|
|
||||||
**Primary Libraries:** DevExpress + PDF.js (PSPDFKit removed)
|
**Primary Libraries:** DevExpress + PDF.js (PSPDFKit removed)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Migration Notice
|
||||||
|
|
||||||
|
**EnvelopeGenerator.ReceiverUI ? EnvelopeGenerator.WebUI Migration**
|
||||||
|
|
||||||
|
The project has been migrated from pure Blazor WebAssembly (`ReceiverUI`) to **Blazor Auto (Server+WASM hybrid)** architecture (`WebUI`) to resolve DevExpress `DxPdfViewer` compatibility issues.
|
||||||
|
|
||||||
|
**Reason:** DevExpress `DxPdfViewer` requires backend server-side rendering services that are NOT available in pure WebAssembly projects.
|
||||||
|
|
||||||
|
**New Structure:**
|
||||||
|
- **WebUI** (Server project): Hosts server-side components, YARP proxy, DevExpress backend services
|
||||||
|
- **WebUI.Client** (WASM project): Client-side components, business logic, services
|
||||||
|
|
||||||
|
**Migration Details:** See `MIGRATION_CONTEXT.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Deployment Architecture
|
## Deployment Architecture
|
||||||
|
|
||||||
**Two Presentation Projects (Both Required):**
|
**Two Presentation Projects (Both Required):**
|
||||||
|
|
||||||
1. **EnvelopeGenerator.API** (ASP.NET Core Web API)
|
1. **EnvelopeGenerator.API** (ASP.NET Core Web API)
|
||||||
- Runs independently (development & production)
|
- Runs independently (development & production)
|
||||||
- **YARP Reverse Proxy** configured via `yarp.json`
|
- Backend services for document management, authentication, signature endpoints
|
||||||
- Proxies requests to:
|
- Serves as API endpoint for WebUI
|
||||||
- `EnvelopeGenerator.ReceiverUI` (Blazor WASM)
|
|
||||||
- External Auth.API service
|
|
||||||
- Serves as single entry point for all requests
|
|
||||||
|
|
||||||
2. **EnvelopeGenerator.ReceiverUI** (Blazor WebAssembly)
|
2. **EnvelopeGenerator.WebUI** (Blazor Auto - Server+WASM Hybrid)
|
||||||
- Runs on separate host/port
|
- **Server Project (`EnvelopeGenerator.WebUI`):**
|
||||||
- Accessed **only through API proxy** (not directly)
|
- **YARP Reverse Proxy** configured via `yarp.json`
|
||||||
- Serves static files (HTML, JS, CSS, WASM)
|
- 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:**
|
**Request Flow:**
|
||||||
```
|
```
|
||||||
Client ? API:8088 (YARP Proxy) ? ReceiverUI:52936 (Blazor WASM)
|
Client ? WebUI:XXXX (Blazor Auto)
|
||||||
? Auth.API:9090 (External Auth Service)
|
?? Server-side Pages (DxPdfViewer)
|
||||||
|
?? Client-side Pages (WASM)
|
||||||
|
?? YARP Proxy: /api/* ? API:8088
|
||||||
```
|
```
|
||||||
|
|
||||||
**Configuration:** `EnvelopeGenerator.API/yarp.json`
|
**Configuration:** `EnvelopeGenerator.WebUI/yarp.json`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ReceiverUI Route Structure
|
## WebUI Route Structure
|
||||||
|
|
||||||
### Root Route
|
### Root Route
|
||||||
| Route | File | Purpose |
|
| Route | File | Location | Render Mode |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| `/` | `Index.razor` | Application entry point (landing page). |
|
| `/` | `Index.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
||||||
|
|
||||||
### Sender Routes
|
### Sender Routes
|
||||||
| Route | File | Purpose |
|
| Route | File | Location | Render Mode |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| `/sender/login` | `LoginSenderPage.razor` | Username/password authentication |
|
| `/sender/login` | `LoginSenderPage.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
||||||
| `/sender` | `EnvelopeSenderPage.razor` | Sender dashboard (envelope list) |
|
| `/sender` | `EnvelopeSenderPage.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
||||||
|
|
||||||
### Receiver Routes
|
### Receiver Routes (PDF Viewers)
|
||||||
| Route | File | Purpose |
|
| Route | File | Location | Render Mode |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| `/envelope/login/{EnvelopeKey}` | `LoginReceiverPage.razor` | Access code authentication for specific envelope |
|
| `/envelope/login/{EnvelopeKey}` | `LoginReceiverPage.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
||||||
| `/envelope/{EnvelopeKey}` | `EnvelopeReceiverPage.razor` | View & sign envelope (PDF.js viewer) |
|
| `/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).
|
**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
|
## Architecture Evolution
|
||||||
|
|
||||||
### Old Architecture (Deprecated)
|
### Old Architecture (Deprecated v1)
|
||||||
- **Sender UI:** `EnvelopeGenerator.Web` (Razor Pages + PSPDFKit)
|
- **Sender UI:** `EnvelopeGenerator.Web` (Razor Pages + PSPDFKit)
|
||||||
- **Receiver UI:** `EnvelopeGenerator.ReceiverUI` (Blazor WASM + PDF.js)
|
- **Receiver UI:** Separate project
|
||||||
- **Backend:** `EnvelopeGenerator.API`
|
- **Backend:** `EnvelopeGenerator.API`
|
||||||
|
|
||||||
### Current Architecture
|
### Intermediate Architecture (Deprecated v2)
|
||||||
- **Unified Frontend:** `EnvelopeGenerator.ReceiverUI` (Blazor WASM) — **Both Senders & Receivers**
|
- **Unified Frontend:** `EnvelopeGenerator.ReceiverUI` (Pure Blazor WASM)
|
||||||
- **Backend:** `EnvelopeGenerator.API` — **Both Senders & Receivers**
|
- **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
|
- **Libraries:** DevExpress + PDF.js
|
||||||
- **PSPDFKit:** **REMOVED**
|
- **PSPDFKit:** **REMOVED**
|
||||||
|
|
||||||
@@ -77,101 +112,51 @@ Client ? API:8088 (YARP Proxy) ? ReceiverUI:52936 (Blazor WASM)
|
|||||||
| Project | Target | Purpose |
|
| Project | Target | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Backend for **both Senders & Receivers**. Auth, PDF serving, signature endpoints. |
|
| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Backend for **both Senders & Receivers**. Auth, PDF serving, signature endpoints. |
|
||||||
| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | **Unified Blazor WebAssembly Frontend**. UI for **both Senders & Receivers**. YARP proxy to API. |
|
| `EnvelopeGenerator.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.Web` | net7/8/9 | **DEPRECATED.** Legacy Razor Pages (Sender UI). No longer used. |
|
||||||
| `EnvelopeGenerator.Application` | multi | MediatR CQRS handlers. Business logic. |
|
| `EnvelopeGenerator.Application` | multi | MediatR CQRS handlers. Business logic. |
|
||||||
| `EnvelopeGenerator.Domain` | multi | Domain models, constants, interfaces. |
|
| `EnvelopeGenerator.Domain` | multi | Domain models, constants, interfaces. |
|
||||||
| `EnvelopeGenerator.Infrastructure` | multi | EF Core repos, DB context. |
|
| `EnvelopeGenerator.Infrastructure` | multi | EF Core repos, DB context. |
|
||||||
| `EnvelopeGenerator.PdfEditor` | multi | iText7 utilities (NOT used in ReceiverUI). |
|
| `EnvelopeGenerator.PdfEditor` | multi | iText7 utilities (NOT used in WebUI). |
|
||||||
| `EnvelopeGenerator.DependencyInjection` | multi | DI registration helpers. |
|
| `EnvelopeGenerator.DependencyInjection` | multi | DI registration helpers. |
|
||||||
| **VB.NET projects** (Service/Form/BBTests) | net462 | **Legacy. Do NOT touch.** |
|
| **VB.NET projects** (Service/Form/BBTests) | net462 | **Legacy. Do NOT touch.** |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Localization & Culture Management
|
|
||||||
|
|
||||||
**Current Architecture:** Blazor WebAssembly (client-side culture management)
|
|
||||||
|
|
||||||
### Implementation Details
|
|
||||||
|
|
||||||
**Culture Storage:**
|
|
||||||
- Culture preference stored in browser's `localStorage` (key: `AppCulture`)
|
|
||||||
- Managed by `CultureService.cs` (ReceiverUI/Services)
|
|
||||||
- Supported cultures: `de-DE`, `en-US`, `fr-FR`
|
|
||||||
|
|
||||||
**Culture Initialization:**
|
|
||||||
- **Location:** `Program.cs` (lines 53-57)
|
|
||||||
- Sets `CultureInfo.DefaultThreadCurrentCulture/UICulture` **before** app runs
|
|
||||||
- **WASM-Safe:** Each user has isolated browser instance
|
|
||||||
|
|
||||||
**Language Selector:**
|
|
||||||
- **Component:** `LanguageSelector.razor` (ReceiverUI/Shared)
|
|
||||||
- Displays flag icon + language name
|
|
||||||
- Changes culture via `CultureService.SetCultureAsync()`
|
|
||||||
- Navigates with `forceLoad: false` (smooth transition, no page reload)
|
|
||||||
|
|
||||||
### ⚠️ MIGRATION WARNING: Blazor Server/Auto
|
|
||||||
|
|
||||||
**Current approach is WASM-specific and will break in Server/Auto render modes!**
|
|
||||||
|
|
||||||
**Why it breaks:**
|
|
||||||
- `Program.cs:53-57` sets **global** `DefaultThreadCurrentCulture`
|
|
||||||
- In Server/Auto, one app instance serves **all users**
|
|
||||||
- User A selects German → User B sees German too (shared state)
|
|
||||||
- Thread-safety issues and culture conflicts
|
|
||||||
|
|
||||||
**Migration Checklist (when moving to Server/Auto):**
|
|
||||||
|
|
||||||
1. **Remove global culture initialization** from `Program.cs` (lines 53-57)
|
|
||||||
- See detailed warning comment in the code
|
|
||||||
|
|
||||||
2. **Add RequestLocalizationMiddleware** (Server-side approach):
|
|
||||||
```csharp
|
|
||||||
app.UseRequestLocalization(options => {
|
|
||||||
options.SupportedCultures = new[] { "de-DE", "en-US", "fr-FR" };
|
|
||||||
options.SupportedUICultures = options.SupportedCultures;
|
|
||||||
options.RequestCultureProviders.Insert(0, new CookieRequestCultureProvider());
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **OR** Use **per-circuit culture** (Blazor Server approach):
|
|
||||||
- Store culture in circuit-scoped service
|
|
||||||
- Use `CascadingParameter` to distribute to components
|
|
||||||
- See: https://learn.microsoft.com/aspnet/core/blazor/globalization-localization
|
|
||||||
|
|
||||||
4. **Update `LanguageSelector.razor`:**
|
|
||||||
- Remove manual `CultureInfo.DefaultThreadCurrentCulture` assignment
|
|
||||||
- Use middleware/circuit culture provider instead
|
|
||||||
|
|
||||||
5. **Update `CultureService.cs`:**
|
|
||||||
- Integrate with Server-side culture provider
|
|
||||||
- May need to store in cookies instead of localStorage
|
|
||||||
|
|
||||||
**References:**
|
|
||||||
- Microsoft Docs: [Blazor Globalization/Localization](https://learn.microsoft.com/aspnet/core/blazor/globalization-localization)
|
|
||||||
- Current implementation: `Program.cs`, `CultureService.cs`, `LanguageSelector.razor`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Files & Routes
|
## Key Files & Routes
|
||||||
|
|
||||||
| File | Route/Purpose |
|
### 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 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `ReceiverUI/Pages/Index.razor` | `/` — Application entry point (landing page). |
|
| `WebUI.Client/Services/AuthService.cs` | Receiver + Sender authentication. |
|
||||||
| `ReceiverUI/Pages/EnvelopeSenderPage.razor` | `/sender` — Sender dashboard (envelope list). |
|
| `WebUI.Client/Services/SignatureCacheService.cs` | Signature caching (Redis/SQL). |
|
||||||
| `ReceiverUI/Pages/EnvelopeReceiverPage.razor` | `/envelope/{key}` — Receiver PDF viewer & signing. |
|
| `WebUI.Client/Services/DocumentService.cs` | PDF document retrieval. |
|
||||||
| `ReceiverUI/Pages/LoginSenderPage.razor` | `/sender/login` — Sender username/password auth. |
|
| `WebUI/wwwroot/js/pdf-viewer.js` | PDF.js wrapper (zoom, pagination, thumbnails). |
|
||||||
| `ReceiverUI/Pages/LoginReceiverPage.razor` | `/envelope/login/{EnvelopeKey}` — Receiver access code auth. |
|
| `WebUI/wwwroot/js/receiver-signature.js` | Signature pad (draw/type/image). |
|
||||||
| `ReceiverUI/wwwroot/js/pdf-viewer.js` | PDF.js wrapper (zoom, pagination, thumbnails). |
|
| `WebUI/wwwroot/css/envelope-viewer.css` | EnvelopeViewer styles. |
|
||||||
| `ReceiverUI/wwwroot/js/receiver-signature.js` | Signature pad (draw/type/image). |
|
|
||||||
| `ReceiverUI/wwwroot/css/envelope-viewer.css` | EnvelopeViewer styles. |
|
|
||||||
| `ReceiverUI/Services/AuthService.cs` | Receiver + Sender authentication. |
|
|
||||||
| `ReceiverUI/Services/SignatureCacheService.cs` | Signature caching (Redis/SQL). |
|
|
||||||
| `API/Controllers/CacheController.cs` | Signature cache endpoints. |
|
| `API/Controllers/CacheController.cs` | Signature cache endpoints. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Coordinate System — CRITICAL
|
## Coordinate System — CRITICAL
|
||||||
|
|
||||||
**Database Format:** INCHES (GdPicture14 native)
|
**Database Format:** INCHES (GdPicture14 native)
|
||||||
**Origin:** Top-left corner
|
**Origin:** Top-left corner
|
||||||
@@ -200,11 +185,11 @@ Client ? API:8088 (YARP Proxy) ? ReceiverUI:52936 (Blazor WASM)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## EnvelopeReceiver — PDF.js Viewer & Signing
|
## EnvelopeReceiver — PDF.js Viewer & Signing
|
||||||
|
|
||||||
**Route:** `/envelope/{EnvelopeKey}`
|
**Route:** `/envelope/{EnvelopeKey}`
|
||||||
**Tech:** PDF.js 3.11.174 + Blazor WASM + configurable quality
|
**Tech:** PDF.js 3.11.174 + Blazor Server (`@rendermode InteractiveServer`) + configurable quality
|
||||||
**File:** `ReceiverUI/Pages/EnvelopeReceiverPage.razor`
|
**File:** `WebUI/Components/Pages/EnvelopeReceiverPage.razor`
|
||||||
|
|
||||||
### Key Features
|
### Key Features
|
||||||
1. HiDPI/Retina support (4x quality)
|
1. HiDPI/Retina support (4x quality)
|
||||||
@@ -215,7 +200,7 @@ Client ? API:8088 (YARP Proxy) ? ReceiverUI:52936 (Blazor WASM)
|
|||||||
6. Responsive (desktop/mobile)
|
6. Responsive (desktop/mobile)
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
**File:** `ReceiverUI/wwwroot/appsettings.json`
|
**File:** `WebUI/wwwroot/appsettings.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -229,7 +214,7 @@ Client ? API:8088 (YARP Proxy) ? ReceiverUI:52936 (Blazor WASM)
|
|||||||
```
|
```
|
||||||
|
|
||||||
### JavaScript API
|
### JavaScript API
|
||||||
**File:** `ReceiverUI/wwwroot/js/pdf-viewer.js`
|
**File:** `WebUI/wwwroot/js/pdf-viewer.js`
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
window.pdfViewer = {
|
window.pdfViewer = {
|
||||||
@@ -243,7 +228,7 @@ window.pdfViewer = {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Signature Workflow — EnvelopeReceiver
|
## Signature Workflow — EnvelopeReceiver
|
||||||
|
|
||||||
**IMPORTANT:** iText7 NOT used (GPL license issue). Client-side overlay system only.
|
**IMPORTANT:** iText7 NOT used (GPL license issue). Client-side overlay system only.
|
||||||
|
|
||||||
@@ -276,7 +261,7 @@ window.pdfViewer = {
|
|||||||
- Session state: `_capturedSignature` (lost on refresh)
|
- Session state: `_capturedSignature` (lost on refresh)
|
||||||
|
|
||||||
### Data Model
|
### Data Model
|
||||||
**File:** `ReceiverUI/Models/SignatureCaptureDto.cs`
|
**File:** `WebUI.Client/Models/SignatureCaptureDto.cs`
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public sealed record SignatureCaptureDto {
|
public sealed record SignatureCaptureDto {
|
||||||
@@ -296,9 +281,9 @@ public sealed record SignatureCaptureDto {
|
|||||||
### API Endpoints
|
### API Endpoints
|
||||||
**Controller:** `API/Controllers/CacheController.cs`
|
**Controller:** `API/Controllers/CacheController.cs`
|
||||||
|
|
||||||
- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save
|
- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save
|
||||||
- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load
|
- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load
|
||||||
- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete
|
- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete
|
||||||
|
|
||||||
**Cache Key Format:**
|
**Cache Key Format:**
|
||||||
```
|
```
|
||||||
@@ -315,7 +300,7 @@ signature:91751687-8ae6-4777-bf5f-b8846085e62e:{envelopeKey}
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Service
|
### Service
|
||||||
**File:** `ReceiverUI/Services/SignatureCacheService.cs`
|
**File:** `WebUI.Client/Services/SignatureCacheService.cs`
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public class SignatureCacheService {
|
public class SignatureCacheService {
|
||||||
@@ -332,11 +317,11 @@ public class SignatureCacheService {
|
|||||||
## Sender Login
|
## Sender Login
|
||||||
|
|
||||||
**Route:** `/sender/login`
|
**Route:** `/sender/login`
|
||||||
**File:** `ReceiverUI/Pages/LoginSenderPage.razor`
|
**File:** `WebUI.Client/Pages/LoginSenderPage.razor`
|
||||||
**Tech:** Bootstrap 5 + DevExpress Blazing Berry theme
|
**Tech:** Bootstrap 5 + DevExpress Blazing Berry theme
|
||||||
|
|
||||||
### AuthService Extension
|
### AuthService Extension
|
||||||
**File:** `ReceiverUI/Services/AuthService.cs`
|
**File:** `WebUI.Client/Services/AuthService.cs`
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public enum SenderLoginResult { Success, InvalidCredentials, Error }
|
public enum SenderLoginResult { Success, InvalidCredentials, Error }
|
||||||
@@ -364,7 +349,7 @@ public async Task<SenderLoginResult> LoginSenderAsync(string username, string pa
|
|||||||
|
|
||||||
**Response:**
|
**Response:**
|
||||||
- `200 OK` ? Cookie set, redirect to `/sender`
|
- `200 OK` ? Cookie set, redirect to `/sender`
|
||||||
- `401 Unauthorized` ? Show error: "Ungültige Anmeldedaten"
|
- `401 Unauthorized` ? Show error: "Ungültige Anmeldedaten"
|
||||||
- Other ? Show error: "Serverfehler"
|
- Other ? Show error: "Serverfehler"
|
||||||
|
|
||||||
**Cookie:** HTTP-only, Secure (HTTPS), SameSite=Strict
|
**Cookie:** HTTP-only, Secure (HTTPS), SameSite=Strict
|
||||||
@@ -381,7 +366,7 @@ public async Task<SenderLoginResult> LoginSenderAsync(string username, string pa
|
|||||||
## Receiver Login
|
## Receiver Login
|
||||||
|
|
||||||
**Route:** `/envelope/login/{EnvelopeKey}`
|
**Route:** `/envelope/login/{EnvelopeKey}`
|
||||||
**File:** `ReceiverUI/Pages/LoginReceiverPage.razor`
|
**File:** `WebUI.Client/Pages/LoginReceiverPage.razor`
|
||||||
|
|
||||||
**Multi-Envelope Support:** Cookies are stored per-envelope (e.g., `AuthTokenSignFLOWReceiver.{envelopeKey}`), allowing simultaneous authentication for multiple envelopes in the same browser session.
|
**Multi-Envelope Support:** Cookies are stored per-envelope (e.g., `AuthTokenSignFLOWReceiver.{envelopeKey}`), allowing simultaneous authentication for multiple envelopes in the same browser session.
|
||||||
|
|
||||||
@@ -409,7 +394,7 @@ public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string key, st
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## NuGet Packages (ReceiverUI)
|
## NuGet Packages (WebUI.Client)
|
||||||
|
|
||||||
| Package | Version | Purpose |
|
| Package | Version | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -422,7 +407,7 @@ public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string key, st
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Mistakes History — Do NOT Repeat
|
## Mistakes History — Do NOT Repeat
|
||||||
|
|
||||||
| Mistake | Why Wrong |
|
| Mistake | Why Wrong |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -441,8 +426,9 @@ public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string key, st
|
|||||||
|
|
||||||
### Deprecated Projects
|
### Deprecated Projects
|
||||||
**DO NOT USE:**
|
**DO NOT USE:**
|
||||||
- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified ReceiverUI
|
- `EnvelopeGenerator.ReceiverUI` (Pure Blazor WASM) — Migrated to WebUI (DevExpress compatibility issue)
|
||||||
- PSPDFKit — Removed, use PDF.js + DevExpress instead
|
- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified WebUI
|
||||||
|
- PSPDFKit — Removed, use PDF.js + DevExpress instead
|
||||||
|
|
||||||
### Legacy Projects (VB.NET)
|
### Legacy Projects (VB.NET)
|
||||||
**DO NOT TOUCH:** `EnvelopeGenerator.Service`, `EnvelopeGenerator.Form`, `EnvelopeGenerator.BBTests`
|
**DO NOT TOUCH:** `EnvelopeGenerator.Service`, `EnvelopeGenerator.Form`, `EnvelopeGenerator.BBTests`
|
||||||
@@ -467,8 +453,8 @@ Proves database uses INCHES natively.
|
|||||||
## Quick Reference
|
## Quick Reference
|
||||||
|
|
||||||
### When working with coordinates:
|
### When working with coordinates:
|
||||||
1. **Database ? UI:** INCHES × 72 = PDF Points
|
1. **Database ? UI:** INCHES × 72 = PDF Points
|
||||||
2. **UI ? Display:** Points × scale = Pixels
|
2. **UI ? Display:** Points × scale = Pixels
|
||||||
3. **iText7 stamping:** Flip Y-axis (top-down ? bottom-up)
|
3. **iText7 stamping:** Flip Y-axis (top-down ? bottom-up)
|
||||||
|
|
||||||
### When adding features:
|
### When adding features:
|
||||||
@@ -476,14 +462,16 @@ Proves database uses INCHES natively.
|
|||||||
2. Prefer simplicity over complexity
|
2. Prefer simplicity over complexity
|
||||||
3. Use `appsettings.json` for configuration
|
3. Use `appsettings.json` for configuration
|
||||||
4. Keep consistent with existing design (Bootstrap 5 + Blazing Berry)
|
4. Keep consistent with existing design (Bootstrap 5 + Blazing Berry)
|
||||||
5. **Unified frontend:** ReceiverUI serves both Senders and Receivers
|
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:
|
### When debugging:
|
||||||
1. **Coordinates:** Always check unit system (inches/points/pixels)
|
1. **Coordinates:** Always check unit system (inches/points/pixels)
|
||||||
2. **Authentication:** Check cookie name/domain/SameSite
|
2. **Authentication:** Check cookie name/domain/SameSite
|
||||||
3. **Cache:** Check Redis/SQL connection + key format
|
3. **Cache:** Check Redis/SQL connection + key format
|
||||||
4. **Frontend confusion:** Only use ReceiverUI (Web is deprecated)
|
4. **Frontend confusion:** Only use WebUI (ReceiverUI/Web are deprecated)
|
||||||
|
5. **Blank DxPdfViewer:** Ensure page has `@rendermode InteractiveServer`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated:** Session 19 (Razor file naming convention + Index route proxy)
|
**Last Updated:** 2025-01-27 (ReceiverUI ? WebUI migration complete)
|
||||||
|
|||||||
@@ -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>
|
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</response>
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
||||||
[HttpPost("logout")]
|
[HttpPost("logout")]
|
||||||
public async Task<IActionResult> 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.Status200OK)]
|
||||||
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
|
||||||
[HttpGet("check")]
|
[HttpGet("check")]
|
||||||
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
|
[Authorize]
|
||||||
public IActionResult Check(string? role = null)
|
public IActionResult Check(string? role = null)
|
||||||
=> role is not null && !User.IsInRole(role)
|
=> role is not null && !User.IsInRole(role)
|
||||||
? Unauthorized()
|
? Unauthorized()
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
using EnvelopeGenerator.API.Models.PsPdfKitAnnotation;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@@ -14,7 +13,7 @@ namespace EnvelopeGenerator.API.Controllers;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Authorize(Policy = AuthPolicy.SenderOrReceiver)]
|
[Authorize]
|
||||||
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
|
public class ConfigController(IOptionsMonitor<AnnotationParams> annotationParamsOptions) : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
|
private readonly AnnotationParams _annotationParams = annotationParamsOptions.CurrentValue;
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ namespace EnvelopeGenerator.API.Controllers;
|
|||||||
[Authorize(Policy = AuthPolicy.Receiver)]
|
[Authorize(Policy = AuthPolicy.Receiver)]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
public class DocReceiverElementController : ControllerBase
|
public class SignatureController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of <see cref="DocReceiverElementController"/>.
|
/// Initializes a new instance of <see cref="SignatureController"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DocReceiverElementController(IMediator mediator)
|
public SignatureController(IMediator mediator)
|
||||||
{
|
{
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,6 @@
|
|||||||
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
|
<PackageReference Include="DigitalData.Auth.Client" Version="1.3.7" />
|
||||||
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
<PackageReference Include="DigitalData.Core.API" Version="2.2.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<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="Microsoft.Extensions.Caching.SqlServer" Version="8.0.11" Condition="'$(TargetFramework)' == 'net8.0'" />
|
||||||
<PackageReference Include="itext" Version="8.0.5" />
|
<PackageReference Include="itext" Version="8.0.5" />
|
||||||
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
|
<PackageReference Include="itext.bouncy-castle-adapter" Version="8.0.5" />
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using EnvelopeGenerator.Application;
|
|||||||
using DigitalData.Auth.Client;
|
using DigitalData.Auth.Client;
|
||||||
using DigitalData.Core.Abstractions;
|
using DigitalData.Core.Abstractions;
|
||||||
using EnvelopeGenerator.API.Models;
|
using EnvelopeGenerator.API.Models;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using DigitalData.Core.Abstractions.Security.Extensions;
|
using DigitalData.Core.Abstractions.Security.Extensions;
|
||||||
using EnvelopeGenerator.API.Middleware;
|
using EnvelopeGenerator.API.Middleware;
|
||||||
@@ -21,7 +22,6 @@ using NLog.Web;
|
|||||||
using NLog;
|
using NLog;
|
||||||
using DigitalData.Auth.Claims;
|
using DigitalData.Auth.Claims;
|
||||||
using EnvelopeGenerator.API;
|
using EnvelopeGenerator.API;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
|
|
||||||
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
|
||||||
logger.Info("Logging initialized!");
|
logger.Info("Logging initialized!");
|
||||||
@@ -44,11 +44,7 @@ try
|
|||||||
|
|
||||||
var deferredProvider = new DeferredServiceProvider();
|
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.AddHttpClient();
|
||||||
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
||||||
|
|
||||||
@@ -242,9 +238,8 @@ try
|
|||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddAuthorizationBuilder()
|
builder.Services.AddAuthorizationBuilder()
|
||||||
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy
|
.AddPolicy(AuthPolicy.SenderOrReceiver, policy => policy.RequireRole(Role.Sender, Role.Receiver.Full))
|
||||||
.RequireRole(Role.Sender, Role.Receiver.Full)
|
|
||||||
.AddAuthenticationSchemes(AuthScheme.Sender, AuthScheme.Receiver))
|
|
||||||
.AddPolicy(AuthPolicy.Sender, policy => policy
|
.AddPolicy(AuthPolicy.Sender, policy => policy
|
||||||
.RequireRole(Role.Sender)
|
.RequireRole(Role.Sender)
|
||||||
.AddAuthenticationSchemes(AuthScheme.Sender))
|
.AddAuthenticationSchemes(AuthScheme.Sender))
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
"https": {
|
"https": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": false,
|
||||||
"launchUrl": "sender",
|
"launchUrl": "swagger",
|
||||||
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
"applicationUrl": "https://localhost:8088;http://localhost:5131",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing configuration settings.
|
/// Data Transfer Object representing configuration settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class ConfigDto
|
public class ConfigDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
/// Data Transfer Object representing a positioned element assigned to a document receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class DocReceiverElementDto : IDocReceiverElement
|
public class DocReceiverElementDto : IDocReceiverElement
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
/// Data Transfer Object representing a document within an envelope, including optional binary data and form elements.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class DocumentDto
|
public class DocumentDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing the status of a document for a specific receiver.
|
/// Data Transfer Object representing the status of a document for a specific receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class DocumentStatusDto
|
public class DocumentStatusDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||||
using DigitalData.UserManager.Application.DTOs.User;
|
using DigitalData.UserManager.Application.DTOs.User;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using EnvelopeGenerator.Domain.Constants;
|
using EnvelopeGenerator.Domain.Constants;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using EnvelopeGenerator.Domain.Interfaces;
|
using EnvelopeGenerator.Domain.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public record EnvelopeDto : IEnvelope
|
public record EnvelopeDto : IEnvelope
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -128,5 +133,5 @@ public record EnvelopeDto : IEnvelope
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<EnvelopeReceiverDto>? EnvelopeReceivers { get; set; }
|
public IEnumerable<ReceiverDto>? Receivers { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
using DigitalData.EmailProfilerDispatcher.Abstraction.Attributes;
|
||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public record EnvelopeReceiverDto
|
public record EnvelopeReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
|
public record EnvelopeReceiverSecretDto : EnvelopeReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
@@ -7,6 +8,7 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
|||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="DateValid"></param>
|
/// <param name="DateValid"></param>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public record EnvelopeReceiverReadOnlyCreateDto(
|
public record EnvelopeReceiverReadOnlyCreateDto(
|
||||||
DateTime DateValid)
|
DateTime DateValid)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
|
|
||||||
@@ -6,6 +8,7 @@ namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
|||||||
/// Represents a read-only Data Transfer Object (DTO) for an envelope receiver.
|
/// Represents a read-only Data Transfer Object (DTO) for an envelope receiver.
|
||||||
/// Contains information about the receiver, associated envelope, and audit details.
|
/// Contains information about the receiver, associated envelope, and audit details.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class EnvelopeReceiverReadOnlyDto
|
public class EnvelopeReceiverReadOnlyDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiverReadOnly;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object for updating a read-only envelope receiver.
|
/// Data Transfer Object for updating a read-only envelope receiver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class EnvelopeReceiverReadOnlyUpdateDto
|
public class EnvelopeReceiverReadOnlyUpdateDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
/// Data Transfer Object representing a type of envelope with its configuration settings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class EnvelopeTypeDto
|
public class EnvelopeTypeDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|||||||
using EnvelopeGenerator.Application.Common.Extensions;
|
using EnvelopeGenerator.Application.Common.Extensions;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
|
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
namespace EnvelopeGenerator.Application.Common.Dto;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the AutoMapper profile configuration for mapping between
|
/// Represents the AutoMapper profile configuration for mapping between
|
||||||
@@ -28,15 +26,15 @@ public class MappingProfile : Profile
|
|||||||
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
CreateMap<DocReceiverElement, DocReceiverElementDto>();
|
||||||
CreateMap<DocumentStatus, DocumentStatusDto>();
|
CreateMap<DocumentStatus, DocumentStatusDto>();
|
||||||
CreateMap<EmailTemplate, EmailTemplateDto>();
|
CreateMap<EmailTemplate, EmailTemplateDto>();
|
||||||
CreateMap<Envelope, EnvelopeDto>();
|
CreateMap<Envelope, EnvelopeDto>().ForMember(dest => dest.Receivers, opt => opt.MapFrom(src => src.EnvelopeReceivers.Select(er => er.Receiver)));
|
||||||
CreateMap<Document, DocumentDto>();
|
CreateMap<Document, DocumentDto>();
|
||||||
CreateMap<History, HistoryDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
CreateMap<Domain.Entities.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<Domain.Entities.History, HistoryCreateDto>().ForMember(dest => dest.ActionDate, opt => opt.MapFrom(src => src.ChangedWhen));
|
||||||
CreateMap<EnvelopeReceiver, EnvelopeReceiverDto>();
|
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverDto>();
|
||||||
CreateMap<EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverSecretDto>();
|
||||||
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
CreateMap<EnvelopeType, EnvelopeTypeDto>();
|
||||||
CreateMap<Receiver, ReceiverDto>();
|
CreateMap<Domain.Entities.Receiver, ReceiverDto>();
|
||||||
CreateMap<EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
|
||||||
CreateMap<ElementAnnotation, AnnotationDto>();
|
CreateMap<ElementAnnotation, AnnotationDto>();
|
||||||
|
|
||||||
// DTO to Entity mappings
|
// DTO to Entity mappings
|
||||||
@@ -49,13 +47,13 @@ public class MappingProfile : Profile
|
|||||||
CreateMap<EmailTemplateDto, EmailTemplate>();
|
CreateMap<EmailTemplateDto, EmailTemplate>();
|
||||||
CreateMap<EnvelopeDto, Envelope>();
|
CreateMap<EnvelopeDto, Envelope>();
|
||||||
CreateMap<DocumentDto, Document>();
|
CreateMap<DocumentDto, Document>();
|
||||||
CreateMap<HistoryDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
CreateMap<HistoryDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||||
CreateMap<HistoryCreateDto, History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
CreateMap<HistoryCreateDto, Domain.Entities.History>().ForMember(dest => dest.ChangedWhen, opt => opt.MapFrom(src => src.ActionDate));
|
||||||
CreateMap<EnvelopeReceiverDto, EnvelopeReceiver>();
|
CreateMap<EnvelopeReceiverDto, Domain.Entities.EnvelopeReceiver>();
|
||||||
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
CreateMap<EnvelopeTypeDto, EnvelopeType>();
|
||||||
CreateMap<ReceiverDto, Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
CreateMap<ReceiverDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
|
||||||
CreateMap<EnvelopeReceiverReadOnlyCreateDto, EnvelopeReceiverReadOnly>();
|
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
||||||
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, EnvelopeReceiverReadOnly>();
|
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
|
||||||
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
CreateMap<AnnotationCreateDto, ElementAnnotation>()
|
||||||
.MapAddedWhen();
|
.MapAddedWhen();
|
||||||
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class GtxMessagingResponse : Dictionary<string, object?> { }
|
public class GtxMessagingResponse : Dictionary<string, object?> { }
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Common.Dto.Messaging;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public record SmsResponse
|
public record SmsResponse
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
using EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
@@ -6,6 +7,7 @@ namespace EnvelopeGenerator.Application.Common.Dto.Receiver;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class ReceiverDto
|
public class ReceiverDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Resources\Model.Designer.vb" />
|
<None Remove="Resources\Model.Designer.vb" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
|
||||||
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
|
||||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.18" />
|
||||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
<PackageReference Include="Microsoft.Identity.Client" Version="4.82.1" />
|
||||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||||
@@ -78,25 +79,25 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
|
||||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||||
<PackageReference Include="CommandDotNet">
|
<PackageReference Include="CommandDotNet">
|
||||||
<Version>7.0.5</Version>
|
<Version>7.0.5</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||||
<PackageReference Include="CommandDotNet">
|
<PackageReference Include="CommandDotNet">
|
||||||
<Version>8.1.1</Version>
|
<Version>8.1.1</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
|
||||||
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
<PackageReference Include="AutoMapper" Version="14.0.0" />
|
||||||
<PackageReference Include="CommandDotNet">
|
<PackageReference Include="CommandDotNet">
|
||||||
<Version>8.1.1</Version>
|
<Version>8.1.1</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -14,16 +14,6 @@ namespace EnvelopeGenerator.Application.Envelopes.Queries;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
public record ReadEnvelopeQuery : EnvelopeQueryBase, IRequest<IEnumerable<EnvelopeDto>>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public bool OnlyActive { get; init; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
public bool OnlyCompleted { get; init; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Abfrage des Include des Umschlags
|
/// Abfrage des Include des Umschlags
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -142,12 +132,6 @@ public class ReadEnvelopeQueryHandler : IRequestHandler<ReadEnvelopeQuery, IEnum
|
|||||||
query = query.Where(e => !status.Ignore.Contains(e.Status));
|
query = query.Where(e => !status.Ignore.Contains(e.Status));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(request is { OnlyActive: true })
|
|
||||||
query = query.Where(e => Status.Active.Contains(e.Status));
|
|
||||||
|
|
||||||
if (request is { OnlyCompleted: true })
|
|
||||||
query = query.Where(e => Status.Completed.Contains(e.Status));
|
|
||||||
|
|
||||||
var envelopes = await query
|
var envelopes = await query
|
||||||
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
.Include(e => e.EnvelopeReceivers).ThenInclude(er => er.Receiver)
|
||||||
.ToListAsync(cancel);
|
.ToListAsync(cancel);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using DigitalData.Core.Abstraction.Application.Repository;
|
|||||||
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
using EnvelopeGenerator.Application.Common.Dto.Receiver;
|
||||||
using EnvelopeGenerator.Domain.Entities;
|
using EnvelopeGenerator.Domain.Entities;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
@@ -13,6 +14,7 @@ namespace EnvelopeGenerator.Application.Receivers.Commands;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public record CreateReceiverCommand : IRequest<(ReceiverDto Receiver, bool AlreadyExists)>
|
public record CreateReceiverCommand : IRequest<(ReceiverDto Receiver, bool AlreadyExists)>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
namespace EnvelopeGenerator.Application.Receivers.Commands;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.Application.Receivers.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data Transfer Object for updating a receiver's information.
|
/// Data Transfer Object for updating a receiver's information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)]
|
||||||
public class UpdateReceiverCommand
|
public class UpdateReceiverCommand
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -397,412 +397,4 @@ public static class Extensions
|
|||||||
/// <param name="suffix"></param>
|
/// <param name="suffix"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static string LockedFooterBody(this IStringLocalizer localizer, string suffix) => localizer[nameof(LockedFooterBody) + suffix].Value;
|
public static string LockedFooterBody(this IStringLocalizer localizer, string suffix) => localizer[nameof(LockedFooterBody) + suffix].Value;
|
||||||
|
|
||||||
// Sender-side UI resources
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string NewEnvelope(this IStringLocalizer localizer) => localizer[nameof(NewEnvelope)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string LoadEnvelope(this IStringLocalizer localizer) => localizer[nameof(LoadEnvelope)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string DeleteEnvelope(this IStringLocalizer localizer) => localizer[nameof(DeleteEnvelope)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string RefreshData(this IStringLocalizer localizer) => localizer[nameof(RefreshData)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string RefreshedAt(this IStringLocalizer localizer) => localizer[nameof(RefreshedAt)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string ShowDocument(this IStringLocalizer localizer) => localizer[nameof(ShowDocument)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string ContactReceiver(this IStringLocalizer localizer) => localizer[nameof(ContactReceiver)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string EnvelopeId(this IStringLocalizer localizer) => localizer[nameof(EnvelopeId)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string OpenLogDirectory(this IStringLocalizer localizer) => localizer[nameof(OpenLogDirectory)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string ShowResultsReport(this IStringLocalizer localizer) => localizer[nameof(ShowResultsReport)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string SupportMail(this IStringLocalizer localizer) => localizer[nameof(SupportMail)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string ResendInvitation(this IStringLocalizer localizer) => localizer[nameof(ResendInvitation)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string Export(this IStringLocalizer localizer) => localizer[nameof(Export)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string Receivers(this IStringLocalizer localizer) => localizer[nameof(Receivers)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string EmailSalutation(this IStringLocalizer localizer) => localizer[nameof(EmailSalutation)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string SignedWhen(this IStringLocalizer localizer) => localizer[nameof(SignedWhen)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string AccessCode(this IStringLocalizer localizer) => localizer[nameof(AccessCode)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string User(this IStringLocalizer localizer) => localizer[nameof(User)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string Type(this IStringLocalizer localizer) => localizer[nameof(Type)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string Title(this IStringLocalizer localizer) => localizer[nameof(Title)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string CreatedOn(this IStringLocalizer localizer) => localizer[nameof(CreatedOn)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string LastModified(this IStringLocalizer localizer) => localizer[nameof(LastModified)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string OpenEnvelopes(this IStringLocalizer localizer) => localizer[nameof(OpenEnvelopes)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string CompletedEnvelopes(this IStringLocalizer localizer) => localizer[nameof(CompletedEnvelopes)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string SendAccessCode(this IStringLocalizer localizer) => localizer[nameof(SendAccessCode)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string TwoFactorProperties(this IStringLocalizer localizer) => localizer[nameof(TwoFactorProperties)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string Name(this IStringLocalizer localizer) => localizer[nameof(Name)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string PhoneNumber(this IStringLocalizer localizer) => localizer[nameof(PhoneNumber)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string AddReceiver(this IStringLocalizer localizer) => localizer[nameof(AddReceiver)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string DeleteReceiver(this IStringLocalizer localizer) => localizer[nameof(DeleteReceiver)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string AddFile(this IStringLocalizer localizer) => localizer[nameof(AddFile)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string MergeFiles(this IStringLocalizer localizer) => localizer[nameof(MergeFiles)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string DeleteFile(this IStringLocalizer localizer) => localizer[nameof(DeleteFile)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string ShowFile(this IStringLocalizer localizer) => localizer[nameof(ShowFile)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string EditFields(this IStringLocalizer localizer) => localizer[nameof(EditFields)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string EditData(this IStringLocalizer localizer) => localizer[nameof(EditData)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string Save(this IStringLocalizer localizer) => localizer[nameof(Save)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string SendEnvelope(this IStringLocalizer localizer) => localizer[nameof(SendEnvelope)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string Cancel(this IStringLocalizer localizer) => localizer[nameof(Cancel)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string AddSignature(this IStringLocalizer localizer) => localizer[nameof(AddSignature)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string DeleteSignature(this IStringLocalizer localizer) => localizer[nameof(DeleteSignature)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string Language(this IStringLocalizer localizer) => localizer[nameof(Language)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string UseAccessCode(this IStringLocalizer localizer) => localizer[nameof(UseAccessCode)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string TwoFactorEnabled(this IStringLocalizer localizer) => localizer[nameof(TwoFactorEnabled)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string CertificationType(this IStringLocalizer localizer) => localizer[nameof(CertificationType)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string FinalEmailToCreator(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToCreator)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string FinalEmailToReceivers(this IStringLocalizer localizer) => localizer[nameof(FinalEmailToReceivers)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string SendReminderEmails(this IStringLocalizer localizer) => localizer[nameof(SendReminderEmails)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string FirstReminderDays(this IStringLocalizer localizer) => localizer[nameof(FirstReminderDays)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string ReminderIntervalDays(this IStringLocalizer localizer) => localizer[nameof(ReminderIntervalDays)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string ExpiresWhenDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWhenDays)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string ExpiresWarningDays(this IStringLocalizer localizer) => localizer[nameof(ExpiresWarningDays)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string Message(this IStringLocalizer localizer) => localizer[nameof(Message)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string EnvelopeType(this IStringLocalizer localizer) => localizer[nameof(EnvelopeType)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string AllOptions(this IStringLocalizer localizer) => localizer[nameof(AllOptions)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string DeleteReason(this IStringLocalizer localizer) => localizer[nameof(DeleteReason)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string PleaseProvideReason(this IStringLocalizer localizer) => localizer[nameof(PleaseProvideReason)].Value;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localizer"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string Status(this IStringLocalizer localizer) => localizer[nameof(Status)].Value;
|
|
||||||
}
|
}
|
||||||
@@ -477,178 +477,4 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Bestätigungen</value>
|
<value>Bestätigungen</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="NewEnvelope" xml:space="preserve">
|
|
||||||
<value>Neuer Umschlag</value>
|
|
||||||
</data>
|
|
||||||
<data name="LoadEnvelope" xml:space="preserve">
|
|
||||||
<value>Umschlag laden</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeleteEnvelope" xml:space="preserve">
|
|
||||||
<value>Umschlag zurückrufen/löschen</value>
|
|
||||||
</data>
|
|
||||||
<data name="RefreshData" xml:space="preserve">
|
|
||||||
<value>Daten Aktualisieren</value>
|
|
||||||
</data>
|
|
||||||
<data name="RefreshedAt" xml:space="preserve">
|
|
||||||
<value>Aktualisiert: {0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="ShowDocument" xml:space="preserve">
|
|
||||||
<value>Dokument anzeigen</value>
|
|
||||||
</data>
|
|
||||||
<data name="ContactReceiver" xml:space="preserve">
|
|
||||||
<value>Empfänger kontaktieren</value>
|
|
||||||
</data>
|
|
||||||
<data name="EnvelopeId" xml:space="preserve">
|
|
||||||
<value>Umschlag-ID: {0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="OpenLogDirectory" xml:space="preserve">
|
|
||||||
<value>Öffne Log Verzeichnis</value>
|
|
||||||
</data>
|
|
||||||
<data name="ShowResultsReport" xml:space="preserve">
|
|
||||||
<value>Ergebnisbericht anzeigen</value>
|
|
||||||
</data>
|
|
||||||
<data name="SupportMail" xml:space="preserve">
|
|
||||||
<value>Support Mail</value>
|
|
||||||
</data>
|
|
||||||
<data name="ResendInvitation" xml:space="preserve">
|
|
||||||
<value>Einladung manuell versenden</value>
|
|
||||||
</data>
|
|
||||||
<data name="Export" xml:space="preserve">
|
|
||||||
<value>Export</value>
|
|
||||||
</data>
|
|
||||||
<data name="Receivers" xml:space="preserve">
|
|
||||||
<value>Empfänger</value>
|
|
||||||
</data>
|
|
||||||
<data name="EmailSalutation" xml:space="preserve">
|
|
||||||
<value>Email Anrede</value>
|
|
||||||
</data>
|
|
||||||
<data name="SignedWhen" xml:space="preserve">
|
|
||||||
<value>Unterschrieben wann</value>
|
|
||||||
</data>
|
|
||||||
<data name="AccessCode" xml:space="preserve">
|
|
||||||
<value>Zugangscode</value>
|
|
||||||
</data>
|
|
||||||
<data name="User" xml:space="preserve">
|
|
||||||
<value>Benutzer</value>
|
|
||||||
</data>
|
|
||||||
<data name="Type" xml:space="preserve">
|
|
||||||
<value>Typ</value>
|
|
||||||
</data>
|
|
||||||
<data name="Title" xml:space="preserve">
|
|
||||||
<value>Titel</value>
|
|
||||||
</data>
|
|
||||||
<data name="CreatedOn" xml:space="preserve">
|
|
||||||
<value>Erstellt am</value>
|
|
||||||
</data>
|
|
||||||
<data name="LastModified" xml:space="preserve">
|
|
||||||
<value>Zuletzt geändert am</value>
|
|
||||||
</data>
|
|
||||||
<data name="OpenEnvelopes" xml:space="preserve">
|
|
||||||
<value>Offene Umschläge</value>
|
|
||||||
</data>
|
|
||||||
<data name="CompletedEnvelopes" xml:space="preserve">
|
|
||||||
<value>Abgeschlossene Umschläge</value>
|
|
||||||
</data>
|
|
||||||
<data name="SendAccessCode" xml:space="preserve">
|
|
||||||
<value>Zugangscode senden</value>
|
|
||||||
</data>
|
|
||||||
<data name="TwoFactorProperties" xml:space="preserve">
|
|
||||||
<value>2-Faktor Eigenschaften</value>
|
|
||||||
</data>
|
|
||||||
<data name="Name" xml:space="preserve">
|
|
||||||
<value>Name</value>
|
|
||||||
</data>
|
|
||||||
<data name="PhoneNumber" xml:space="preserve">
|
|
||||||
<value>Telefonnummer</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddReceiver" xml:space="preserve">
|
|
||||||
<value>Empfänger hinzufügen</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeleteReceiver" xml:space="preserve">
|
|
||||||
<value>Empfänger löschen</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddFile" xml:space="preserve">
|
|
||||||
<value>Datei hinzufügen</value>
|
|
||||||
</data>
|
|
||||||
<data name="MergeFiles" xml:space="preserve">
|
|
||||||
<value>Dateien zusammenführen</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeleteFile" xml:space="preserve">
|
|
||||||
<value>Datei löschen</value>
|
|
||||||
</data>
|
|
||||||
<data name="ShowFile" xml:space="preserve">
|
|
||||||
<value>Datei anzeigen</value>
|
|
||||||
</data>
|
|
||||||
<data name="EditFields" xml:space="preserve">
|
|
||||||
<value>Felder bearbeiten</value>
|
|
||||||
</data>
|
|
||||||
<data name="EditData" xml:space="preserve">
|
|
||||||
<value>Daten bearbeiten</value>
|
|
||||||
</data>
|
|
||||||
<data name="Save" xml:space="preserve">
|
|
||||||
<value>Speichern</value>
|
|
||||||
</data>
|
|
||||||
<data name="SendEnvelope" xml:space="preserve">
|
|
||||||
<value>Umschlag versenden</value>
|
|
||||||
</data>
|
|
||||||
<data name="Cancel" xml:space="preserve">
|
|
||||||
<value>Abbrechen</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddSignature" xml:space="preserve">
|
|
||||||
<value>Signatur hinzufügen</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeleteSignature" xml:space="preserve">
|
|
||||||
<value>Signatur löschen</value>
|
|
||||||
</data>
|
|
||||||
<data name="Language" xml:space="preserve">
|
|
||||||
<value>Sprache</value>
|
|
||||||
</data>
|
|
||||||
<data name="UseAccessCode" xml:space="preserve">
|
|
||||||
<value>Zugangscode verwenden</value>
|
|
||||||
</data>
|
|
||||||
<data name="TwoFactorEnabled" xml:space="preserve">
|
|
||||||
<value>2-Faktor-Authentifizierung aktiviert</value>
|
|
||||||
</data>
|
|
||||||
<data name="CertificationType" xml:space="preserve">
|
|
||||||
<value>Zertifizierungstyp</value>
|
|
||||||
</data>
|
|
||||||
<data name="FinalEmailToCreator" xml:space="preserve">
|
|
||||||
<value>Finale E-Mail an Ersteller</value>
|
|
||||||
</data>
|
|
||||||
<data name="FinalEmailToReceivers" xml:space="preserve">
|
|
||||||
<value>Finale E-Mail an Empfänger</value>
|
|
||||||
</data>
|
|
||||||
<data name="SendReminderEmails" xml:space="preserve">
|
|
||||||
<value>Erinnerungs-E-Mails senden</value>
|
|
||||||
</data>
|
|
||||||
<data name="FirstReminderDays" xml:space="preserve">
|
|
||||||
<value>Erste Erinnerung (Tage)</value>
|
|
||||||
</data>
|
|
||||||
<data name="ReminderIntervalDays" xml:space="preserve">
|
|
||||||
<value>Erinnerungsintervall (Tage)</value>
|
|
||||||
</data>
|
|
||||||
<data name="ExpiresWhenDays" xml:space="preserve">
|
|
||||||
<value>Läuft ab nach (Tage)</value>
|
|
||||||
</data>
|
|
||||||
<data name="ExpiresWarningDays" xml:space="preserve">
|
|
||||||
<value>Ablaufwarnung (Tage)</value>
|
|
||||||
</data>
|
|
||||||
<data name="Message" xml:space="preserve">
|
|
||||||
<value>Nachricht</value>
|
|
||||||
</data>
|
|
||||||
<data name="EnvelopeType" xml:space="preserve">
|
|
||||||
<value>Umschlagtyp</value>
|
|
||||||
</data>
|
|
||||||
<data name="AllOptions" xml:space="preserve">
|
|
||||||
<value>Alle Optionen</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeleteReason" xml:space="preserve">
|
|
||||||
<value>Grund für Löschung</value>
|
|
||||||
</data>
|
|
||||||
<data name="PleaseProvideReason" xml:space="preserve">
|
|
||||||
<value>Bitte geben Sie einen Grund an</value>
|
|
||||||
</data>
|
|
||||||
<data name="Status" xml:space="preserve">
|
|
||||||
<value>Status</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
</root>
|
||||||
@@ -477,178 +477,4 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Confirmations</value>
|
<value>Confirmations</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="NewEnvelope" xml:space="preserve">
|
|
||||||
<value>New Envelope</value>
|
|
||||||
</data>
|
|
||||||
<data name="LoadEnvelope" xml:space="preserve">
|
|
||||||
<value>Load Envelope</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeleteEnvelope" xml:space="preserve">
|
|
||||||
<value>Delete Envelope</value>
|
|
||||||
</data>
|
|
||||||
<data name="RefreshData" xml:space="preserve">
|
|
||||||
<value>Reload Data</value>
|
|
||||||
</data>
|
|
||||||
<data name="RefreshedAt" xml:space="preserve">
|
|
||||||
<value>Refreshed: {0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="ShowDocument" xml:space="preserve">
|
|
||||||
<value>Show Document</value>
|
|
||||||
</data>
|
|
||||||
<data name="ContactReceiver" xml:space="preserve">
|
|
||||||
<value>Contact Receiver</value>
|
|
||||||
</data>
|
|
||||||
<data name="EnvelopeId" xml:space="preserve">
|
|
||||||
<value>Envelope-ID: {0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="OpenLogDirectory" xml:space="preserve">
|
|
||||||
<value>Open Log Directory</value>
|
|
||||||
</data>
|
|
||||||
<data name="ShowResultsReport" xml:space="preserve">
|
|
||||||
<value>Show Results Report</value>
|
|
||||||
</data>
|
|
||||||
<data name="SupportMail" xml:space="preserve">
|
|
||||||
<value>Support Mail</value>
|
|
||||||
</data>
|
|
||||||
<data name="ResendInvitation" xml:space="preserve">
|
|
||||||
<value>Send Invitation Again</value>
|
|
||||||
</data>
|
|
||||||
<data name="Export" xml:space="preserve">
|
|
||||||
<value>Export</value>
|
|
||||||
</data>
|
|
||||||
<data name="Receivers" xml:space="preserve">
|
|
||||||
<value>Receivers</value>
|
|
||||||
</data>
|
|
||||||
<data name="EmailSalutation" xml:space="preserve">
|
|
||||||
<value>Email Salutation</value>
|
|
||||||
</data>
|
|
||||||
<data name="SignedWhen" xml:space="preserve">
|
|
||||||
<value>Signed When</value>
|
|
||||||
</data>
|
|
||||||
<data name="AccessCode" xml:space="preserve">
|
|
||||||
<value>Access Code</value>
|
|
||||||
</data>
|
|
||||||
<data name="User" xml:space="preserve">
|
|
||||||
<value>User</value>
|
|
||||||
</data>
|
|
||||||
<data name="Type" xml:space="preserve">
|
|
||||||
<value>Type</value>
|
|
||||||
</data>
|
|
||||||
<data name="Title" xml:space="preserve">
|
|
||||||
<value>Title</value>
|
|
||||||
</data>
|
|
||||||
<data name="CreatedOn" xml:space="preserve">
|
|
||||||
<value>Created On</value>
|
|
||||||
</data>
|
|
||||||
<data name="LastModified" xml:space="preserve">
|
|
||||||
<value>Last Modified</value>
|
|
||||||
</data>
|
|
||||||
<data name="OpenEnvelopes" xml:space="preserve">
|
|
||||||
<value>Open Envelopes</value>
|
|
||||||
</data>
|
|
||||||
<data name="CompletedEnvelopes" xml:space="preserve">
|
|
||||||
<value>Completed Envelopes</value>
|
|
||||||
</data>
|
|
||||||
<data name="SendAccessCode" xml:space="preserve">
|
|
||||||
<value>Send Access Code</value>
|
|
||||||
</data>
|
|
||||||
<data name="TwoFactorProperties" xml:space="preserve">
|
|
||||||
<value>2-Factor Properties</value>
|
|
||||||
</data>
|
|
||||||
<data name="Name" xml:space="preserve">
|
|
||||||
<value>Name</value>
|
|
||||||
</data>
|
|
||||||
<data name="PhoneNumber" xml:space="preserve">
|
|
||||||
<value>Phone Number</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddReceiver" xml:space="preserve">
|
|
||||||
<value>Add Receiver</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeleteReceiver" xml:space="preserve">
|
|
||||||
<value>Delete Receiver</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddFile" xml:space="preserve">
|
|
||||||
<value>Add File</value>
|
|
||||||
</data>
|
|
||||||
<data name="MergeFiles" xml:space="preserve">
|
|
||||||
<value>Merge Files</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeleteFile" xml:space="preserve">
|
|
||||||
<value>Delete File</value>
|
|
||||||
</data>
|
|
||||||
<data name="ShowFile" xml:space="preserve">
|
|
||||||
<value>Show File</value>
|
|
||||||
</data>
|
|
||||||
<data name="EditFields" xml:space="preserve">
|
|
||||||
<value>Edit Fields</value>
|
|
||||||
</data>
|
|
||||||
<data name="EditData" xml:space="preserve">
|
|
||||||
<value>Edit Data</value>
|
|
||||||
</data>
|
|
||||||
<data name="Save" xml:space="preserve">
|
|
||||||
<value>Save</value>
|
|
||||||
</data>
|
|
||||||
<data name="SendEnvelope" xml:space="preserve">
|
|
||||||
<value>Send Envelope</value>
|
|
||||||
</data>
|
|
||||||
<data name="Cancel" xml:space="preserve">
|
|
||||||
<value>Cancel</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddSignature" xml:space="preserve">
|
|
||||||
<value>Add Signature</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeleteSignature" xml:space="preserve">
|
|
||||||
<value>Delete Signature</value>
|
|
||||||
</data>
|
|
||||||
<data name="Language" xml:space="preserve">
|
|
||||||
<value>Language</value>
|
|
||||||
</data>
|
|
||||||
<data name="UseAccessCode" xml:space="preserve">
|
|
||||||
<value>Use Access Code</value>
|
|
||||||
</data>
|
|
||||||
<data name="TwoFactorEnabled" xml:space="preserve">
|
|
||||||
<value>2-Factor Authentication Enabled</value>
|
|
||||||
</data>
|
|
||||||
<data name="CertificationType" xml:space="preserve">
|
|
||||||
<value>Certification Type</value>
|
|
||||||
</data>
|
|
||||||
<data name="FinalEmailToCreator" xml:space="preserve">
|
|
||||||
<value>Final Email to Creator</value>
|
|
||||||
</data>
|
|
||||||
<data name="FinalEmailToReceivers" xml:space="preserve">
|
|
||||||
<value>Final Email to Receivers</value>
|
|
||||||
</data>
|
|
||||||
<data name="SendReminderEmails" xml:space="preserve">
|
|
||||||
<value>Send Reminder Emails</value>
|
|
||||||
</data>
|
|
||||||
<data name="FirstReminderDays" xml:space="preserve">
|
|
||||||
<value>First Reminder (Days)</value>
|
|
||||||
</data>
|
|
||||||
<data name="ReminderIntervalDays" xml:space="preserve">
|
|
||||||
<value>Reminder Interval (Days)</value>
|
|
||||||
</data>
|
|
||||||
<data name="ExpiresWhenDays" xml:space="preserve">
|
|
||||||
<value>Expires After (Days)</value>
|
|
||||||
</data>
|
|
||||||
<data name="ExpiresWarningDays" xml:space="preserve">
|
|
||||||
<value>Expiry Warning (Days)</value>
|
|
||||||
</data>
|
|
||||||
<data name="Message" xml:space="preserve">
|
|
||||||
<value>Message</value>
|
|
||||||
</data>
|
|
||||||
<data name="EnvelopeType" xml:space="preserve">
|
|
||||||
<value>Envelope Type</value>
|
|
||||||
</data>
|
|
||||||
<data name="AllOptions" xml:space="preserve">
|
|
||||||
<value>All Options</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeleteReason" xml:space="preserve">
|
|
||||||
<value>Deletion Reason</value>
|
|
||||||
</data>
|
|
||||||
<data name="PleaseProvideReason" xml:space="preserve">
|
|
||||||
<value>Please provide a reason</value>
|
|
||||||
</data>
|
|
||||||
<data name="Status" xml:space="preserve">
|
|
||||||
<value>Status</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
</root>
|
||||||
@@ -477,178 +477,4 @@
|
|||||||
<data name="Confirmations" xml:space="preserve">
|
<data name="Confirmations" xml:space="preserve">
|
||||||
<value>Confirmations</value>
|
<value>Confirmations</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="NewEnvelope" xml:space="preserve">
|
|
||||||
<value>Nouvelle enveloppe</value>
|
|
||||||
</data>
|
|
||||||
<data name="LoadEnvelope" xml:space="preserve">
|
|
||||||
<value>Charger l'enveloppe</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeleteEnvelope" xml:space="preserve">
|
|
||||||
<value>Supprimer l'enveloppe</value>
|
|
||||||
</data>
|
|
||||||
<data name="RefreshData" xml:space="preserve">
|
|
||||||
<value>Actualiser les données</value>
|
|
||||||
</data>
|
|
||||||
<data name="RefreshedAt" xml:space="preserve">
|
|
||||||
<value>Actualisé : {0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="ShowDocument" xml:space="preserve">
|
|
||||||
<value>Afficher le document</value>
|
|
||||||
</data>
|
|
||||||
<data name="ContactReceiver" xml:space="preserve">
|
|
||||||
<value>Contacter le destinataire</value>
|
|
||||||
</data>
|
|
||||||
<data name="EnvelopeId" xml:space="preserve">
|
|
||||||
<value>ID d'enveloppe : {0}</value>
|
|
||||||
</data>
|
|
||||||
<data name="OpenLogDirectory" xml:space="preserve">
|
|
||||||
<value>Ouvrir le répertoire des logs</value>
|
|
||||||
</data>
|
|
||||||
<data name="ShowResultsReport" xml:space="preserve">
|
|
||||||
<value>Afficher le rapport de résultats</value>
|
|
||||||
</data>
|
|
||||||
<data name="SupportMail" xml:space="preserve">
|
|
||||||
<value>E-mail de support</value>
|
|
||||||
</data>
|
|
||||||
<data name="ResendInvitation" xml:space="preserve">
|
|
||||||
<value>Renvoyer l'invitation</value>
|
|
||||||
</data>
|
|
||||||
<data name="Export" xml:space="preserve">
|
|
||||||
<value>Exporter</value>
|
|
||||||
</data>
|
|
||||||
<data name="Receivers" xml:space="preserve">
|
|
||||||
<value>Destinataires</value>
|
|
||||||
</data>
|
|
||||||
<data name="EmailSalutation" xml:space="preserve">
|
|
||||||
<value>Formule de politesse</value>
|
|
||||||
</data>
|
|
||||||
<data name="SignedWhen" xml:space="preserve">
|
|
||||||
<value>Signé quand</value>
|
|
||||||
</data>
|
|
||||||
<data name="AccessCode" xml:space="preserve">
|
|
||||||
<value>Code d'accès</value>
|
|
||||||
</data>
|
|
||||||
<data name="User" xml:space="preserve">
|
|
||||||
<value>Utilisateur</value>
|
|
||||||
</data>
|
|
||||||
<data name="Type" xml:space="preserve">
|
|
||||||
<value>Type</value>
|
|
||||||
</data>
|
|
||||||
<data name="Title" xml:space="preserve">
|
|
||||||
<value>Titre</value>
|
|
||||||
</data>
|
|
||||||
<data name="CreatedOn" xml:space="preserve">
|
|
||||||
<value>Créé le</value>
|
|
||||||
</data>
|
|
||||||
<data name="LastModified" xml:space="preserve">
|
|
||||||
<value>Dernière modification</value>
|
|
||||||
</data>
|
|
||||||
<data name="OpenEnvelopes" xml:space="preserve">
|
|
||||||
<value>Enveloppes ouvertes</value>
|
|
||||||
</data>
|
|
||||||
<data name="CompletedEnvelopes" xml:space="preserve">
|
|
||||||
<value>Enveloppes terminées</value>
|
|
||||||
</data>
|
|
||||||
<data name="SendAccessCode" xml:space="preserve">
|
|
||||||
<value>Envoyer le code d'accès</value>
|
|
||||||
</data>
|
|
||||||
<data name="TwoFactorProperties" xml:space="preserve">
|
|
||||||
<value>Propriétés 2-facteurs</value>
|
|
||||||
</data>
|
|
||||||
<data name="Name" xml:space="preserve">
|
|
||||||
<value>Nom</value>
|
|
||||||
</data>
|
|
||||||
<data name="PhoneNumber" xml:space="preserve">
|
|
||||||
<value>Numéro de téléphone</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddReceiver" xml:space="preserve">
|
|
||||||
<value>Ajouter un destinataire</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeleteReceiver" xml:space="preserve">
|
|
||||||
<value>Supprimer le destinataire</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddFile" xml:space="preserve">
|
|
||||||
<value>Ajouter un fichier</value>
|
|
||||||
</data>
|
|
||||||
<data name="MergeFiles" xml:space="preserve">
|
|
||||||
<value>Fusionner les fichiers</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeleteFile" xml:space="preserve">
|
|
||||||
<value>Supprimer le fichier</value>
|
|
||||||
</data>
|
|
||||||
<data name="ShowFile" xml:space="preserve">
|
|
||||||
<value>Afficher le fichier</value>
|
|
||||||
</data>
|
|
||||||
<data name="EditFields" xml:space="preserve">
|
|
||||||
<value>Modifier les champs</value>
|
|
||||||
</data>
|
|
||||||
<data name="EditData" xml:space="preserve">
|
|
||||||
<value>Modifier les données</value>
|
|
||||||
</data>
|
|
||||||
<data name="Save" xml:space="preserve">
|
|
||||||
<value>Enregistrer</value>
|
|
||||||
</data>
|
|
||||||
<data name="SendEnvelope" xml:space="preserve">
|
|
||||||
<value>Envoyer l'enveloppe</value>
|
|
||||||
</data>
|
|
||||||
<data name="Cancel" xml:space="preserve">
|
|
||||||
<value>Annuler</value>
|
|
||||||
</data>
|
|
||||||
<data name="AddSignature" xml:space="preserve">
|
|
||||||
<value>Ajouter une signature</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeleteSignature" xml:space="preserve">
|
|
||||||
<value>Supprimer la signature</value>
|
|
||||||
</data>
|
|
||||||
<data name="Language" xml:space="preserve">
|
|
||||||
<value>Langue</value>
|
|
||||||
</data>
|
|
||||||
<data name="UseAccessCode" xml:space="preserve">
|
|
||||||
<value>Utiliser un code d'accès</value>
|
|
||||||
</data>
|
|
||||||
<data name="TwoFactorEnabled" xml:space="preserve">
|
|
||||||
<value>Authentification à 2 facteurs activée</value>
|
|
||||||
</data>
|
|
||||||
<data name="CertificationType" xml:space="preserve">
|
|
||||||
<value>Type de certification</value>
|
|
||||||
</data>
|
|
||||||
<data name="FinalEmailToCreator" xml:space="preserve">
|
|
||||||
<value>E-mail final au créateur</value>
|
|
||||||
</data>
|
|
||||||
<data name="FinalEmailToReceivers" xml:space="preserve">
|
|
||||||
<value>E-mail final aux destinataires</value>
|
|
||||||
</data>
|
|
||||||
<data name="SendReminderEmails" xml:space="preserve">
|
|
||||||
<value>Envoyer des e-mails de rappel</value>
|
|
||||||
</data>
|
|
||||||
<data name="FirstReminderDays" xml:space="preserve">
|
|
||||||
<value>Premier rappel (jours)</value>
|
|
||||||
</data>
|
|
||||||
<data name="ReminderIntervalDays" xml:space="preserve">
|
|
||||||
<value>Intervalle de rappel (jours)</value>
|
|
||||||
</data>
|
|
||||||
<data name="ExpiresWhenDays" xml:space="preserve">
|
|
||||||
<value>Expire après (jours)</value>
|
|
||||||
</data>
|
|
||||||
<data name="ExpiresWarningDays" xml:space="preserve">
|
|
||||||
<value>Avertissement d'expiration (jours)</value>
|
|
||||||
</data>
|
|
||||||
<data name="Message" xml:space="preserve">
|
|
||||||
<value>Message</value>
|
|
||||||
</data>
|
|
||||||
<data name="EnvelopeType" xml:space="preserve">
|
|
||||||
<value>Type d'enveloppe</value>
|
|
||||||
</data>
|
|
||||||
<data name="AllOptions" xml:space="preserve">
|
|
||||||
<value>Toutes les options</value>
|
|
||||||
</data>
|
|
||||||
<data name="DeleteReason" xml:space="preserve">
|
|
||||||
<value>Motif de suppression</value>
|
|
||||||
</data>
|
|
||||||
<data name="PleaseProvideReason" xml:space="preserve">
|
|
||||||
<value>Veuillez indiquer une raison</value>
|
|
||||||
</data>
|
|
||||||
<data name="Status" xml:space="preserve">
|
|
||||||
<value>Statut</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
</root>
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.Domain.Constants
|
namespace EnvelopeGenerator.Domain.Constants
|
||||||
{
|
{
|
||||||
// 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/
|
// http://wiki.dd/xwiki13/bin/view/Anwendungen/Produkt-Handbuch/Sonstiges/SignFlow/Envelope%20Status/
|
||||||
public enum EnvelopeStatus
|
public enum EnvelopeStatus
|
||||||
{
|
{
|
||||||
Invalid = 0,
|
Invalid = 0,
|
||||||
@@ -51,28 +49,5 @@ namespace EnvelopeGenerator.Domain.Constants
|
|||||||
EnvelopeStatus.EnvelopeCreated,
|
EnvelopeStatus.EnvelopeCreated,
|
||||||
EnvelopeStatus.DocumentMod_Rotation
|
EnvelopeStatus.DocumentMod_Rotation
|
||||||
};
|
};
|
||||||
|
|
||||||
public static readonly List<EnvelopeStatus> Active = Enum.GetValues(typeof(EnvelopeStatus))
|
|
||||||
.Cast<EnvelopeStatus>()
|
|
||||||
.Where(status => status.IsActive())
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
public static readonly List<EnvelopeStatus> Completed = Enum.GetValues(typeof(EnvelopeStatus))
|
|
||||||
.Cast<EnvelopeStatus>()
|
|
||||||
.Where(status => status.IsCompleted())
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class EnvelopeStatusExtensions
|
|
||||||
{
|
|
||||||
public static bool IsActive(this EnvelopeStatus status)
|
|
||||||
{
|
|
||||||
return status >= EnvelopeStatus.EnvelopeCreated && status < EnvelopeStatus.EnvelopePartlySigned;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsCompleted(this EnvelopeStatus status)
|
|
||||||
{
|
|
||||||
return status >= EnvelopeStatus.EnvelopeCompletelySigned && status <= EnvelopeStatus.EnvelopeWithdrawn;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
48
EnvelopeGenerator.Infrastructure/EGDbContextFactory.cs
Normal file
48
EnvelopeGenerator.Infrastructure/EGDbContextFactory.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#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,6 +1,4 @@
|
|||||||
@using System.Globalization
|
<Router AppAssembly="@typeof(Program).Assembly">
|
||||||
|
|
||||||
<Router AppAssembly="@typeof(Program).Assembly">
|
|
||||||
<Found Context="routeData">
|
<Found Context="routeData">
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||||
</Found>
|
</Found>
|
||||||
@@ -9,4 +7,4 @@
|
|||||||
<p>Sorry, there's nothing at this address.</p>
|
<p>Sorry, there's nothing at this address.</p>
|
||||||
</LayoutView>
|
</LayoutView>
|
||||||
</NotFound>
|
</NotFound>
|
||||||
</Router>
|
</Router>
|
||||||
@@ -24,13 +24,11 @@
|
|||||||
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.8" />
|
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
|
||||||
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.8" />
|
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.3" />
|
||||||
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.8" />
|
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
||||||
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.8" />
|
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.3" />
|
||||||
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
|
<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.NativeAssets.WebAssembly" Version="3.119.1" />
|
||||||
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
|
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
|
||||||
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />
|
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />
|
||||||
@@ -42,9 +40,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Properties\PublishProfiles\" />
|
<Folder Include="Properties\PublishProfiles\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\EnvelopeGenerator.Application\EnvelopeGenerator.Application.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Update="wwwroot\docs\privacy-policy.en-US.html">
|
<Content Update="wwwroot\docs\privacy-policy.en-US.html">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
|||||||
32
EnvelopeGenerator.ReceiverUI/Models/AnnotationDto.cs
Normal file
32
EnvelopeGenerator.ReceiverUI/Models/AnnotationDto.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.Models;
|
|
||||||
|
|
||||||
public class EnvelopeDto
|
|
||||||
{
|
|
||||||
[JsonPropertyName("id")]
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("uuid")]
|
|
||||||
public string? Uuid { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("title")]
|
|
||||||
public string? Title { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("status")]
|
|
||||||
public int Status { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("docResult")]
|
|
||||||
public byte[]? DocResult { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("envelopeReceivers")]
|
|
||||||
public List<EnvelopeReceiverSimpleDto> EnvelopeReceivers { get; set; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Simplified receiver model for envelope list display
|
|
||||||
/// </summary>
|
|
||||||
public class EnvelopeReceiverSimpleDto
|
|
||||||
{
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string? Name { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("email")]
|
|
||||||
public string? Email { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("signed")]
|
|
||||||
public bool Signed { get; set; }
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
@inject IOptions<ApiOptions> AppOptions
|
@inject IOptions<ApiOptions> AppOptions
|
||||||
@inject IOptions<PdfViewerOptions> PdfViewerOptions
|
@inject IOptions<PdfViewerOptions> PdfViewerOptions
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@inject DocReceiverElementService SignatureService
|
@inject SignatureService SignatureService
|
||||||
@inject SignatureCacheService SignatureCacheService
|
@inject SignatureCacheService SignatureCacheService
|
||||||
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
|
@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService
|
||||||
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService
|
@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService
|
||||||
|
|||||||
@@ -1,442 +1,7 @@
|
|||||||
@page "/sender"
|
@page "/sender"
|
||||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize(Policy = "Sender")]
|
|
||||||
|
|
||||||
@using System.Text.Json
|
<h3>EnvelopeSender</h3>
|
||||||
@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 {
|
@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,7 +6,6 @@
|
|||||||
@using DevExpress.Utils
|
@using DevExpress.Utils
|
||||||
@using DevExpress.XtraPrinting
|
@using DevExpress.XtraPrinting
|
||||||
@using DevExpress.XtraPrinting.Drawing
|
@using DevExpress.XtraPrinting.Drawing
|
||||||
@using EnvelopeGenerator.Application.Common.Dto
|
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
|
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
|
||||||
@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand
|
@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand
|
||||||
@@ -302,10 +301,7 @@ Shown="OnPopupShownAsync">
|
|||||||
bool IsLoggingOut;
|
bool IsLoggingOut;
|
||||||
|
|
||||||
IReadOnlyList<AnnotationDto> _annotations = [];
|
IReadOnlyList<AnnotationDto> _annotations = [];
|
||||||
IEnumerable<int> AnnotationPages => _annotations
|
IEnumerable<int> AnnotationPages => _annotations.Select(a => a.Page).Distinct().OrderBy(p => p);
|
||||||
.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;
|
EnvelopeReceiverDto? _envelopeReceiver;
|
||||||
record SignatureCapture(string DataUrl, string FullName, string Position, string Place);
|
record SignatureCapture(string DataUrl, string FullName, string Position, string Place);
|
||||||
SignatureCapture? _capturedSignature;
|
SignatureCapture? _capturedSignature;
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ using EnvelopeGenerator.ReceiverUI.Options;
|
|||||||
using DevExpress.XtraReports.Services;
|
using DevExpress.XtraReports.Services;
|
||||||
using DevExpress.Blazor.Reporting;
|
using DevExpress.Blazor.Reporting;
|
||||||
using DevExpress.XtraReports.Web.Extensions;
|
using DevExpress.XtraReports.Web.Extensions;
|
||||||
using EnvelopeGenerator.Application.Resources;
|
|
||||||
using Microsoft.Extensions.Localization;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
builder.RootComponents.Add<App>("#app");
|
builder.RootComponents.Add<App>("#app");
|
||||||
@@ -23,14 +20,9 @@ builder.Services.AddScoped<DocumentService>();
|
|||||||
builder.Services.AddScoped<AuthService>();
|
builder.Services.AddScoped<AuthService>();
|
||||||
builder.Services.AddScoped<AnnotationService>();
|
builder.Services.AddScoped<AnnotationService>();
|
||||||
builder.Services.AddScoped<EnvelopeReceiverService>();
|
builder.Services.AddScoped<EnvelopeReceiverService>();
|
||||||
builder.Services.AddScoped<DocReceiverElementService>();
|
builder.Services.AddScoped<SignatureService>();
|
||||||
builder.Services.AddScoped<SignatureCacheService>();
|
builder.Services.AddScoped<SignatureCacheService>();
|
||||||
builder.Services.AddSingleton<AppVersionService>();
|
builder.Services.AddSingleton<AppVersionService>();
|
||||||
builder.Services.AddScoped<EnvelopeService>();
|
|
||||||
builder.Services.AddScoped<CultureService>();
|
|
||||||
|
|
||||||
// Localization services
|
|
||||||
builder.Services.AddLocalization();
|
|
||||||
|
|
||||||
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
|
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
|
||||||
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
|
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
|
||||||
@@ -49,30 +41,5 @@ builder.Services.AddScoped<IReportProviderAsync, CustomReportProvider>();
|
|||||||
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
|
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
|
||||||
|
|
||||||
var host = builder.Build();
|
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 FontLoader.LoadFonts(host.Services.GetRequiredService<HttpClient>(), new List<string> { "opensans.ttf" });
|
||||||
await host.RunAsync();
|
await host.RunAsync();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"profiles": {
|
"profiles": {
|
||||||
"EnvelopeGenerator.ReceiverUI": {
|
"EnvelopeGenerator.ReceiverUI": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"launchBrowser": false,
|
"launchBrowser": true,
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using EnvelopeGenerator.Application.Common.Dto;
|
|
||||||
using EnvelopeGenerator.ReceiverUI.Models;
|
using EnvelopeGenerator.ReceiverUI.Models;
|
||||||
using EnvelopeGenerator.ReceiverUI.Options;
|
using EnvelopeGenerator.ReceiverUI.Options;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|||||||
@@ -58,16 +58,6 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
|||||||
return response.IsSuccessStatusCode;
|
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>
|
/// <summary>
|
||||||
/// Authenticates a sender user with username and password.
|
/// Authenticates a sender user with username and password.
|
||||||
/// Calls POST /api/auth?cookie=true with JSON body.
|
/// Calls POST /api/auth?cookie=true with JSON body.
|
||||||
@@ -88,16 +78,4 @@ public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
|||||||
_ => SenderLoginResult.Error
|
_ => SenderLoginResult.Error
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Logs out the sender user by removing the authentication cookie.
|
|
||||||
/// Calls POST /api/auth/logout.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<bool> LogoutSenderAsync(CancellationToken cancel = default)
|
|
||||||
{
|
|
||||||
var response = await http.PostAsync(
|
|
||||||
$"{_api.BaseUrl}/api/auth/logout",
|
|
||||||
null, cancel);
|
|
||||||
return response.IsSuccessStatusCode;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,13 +6,13 @@ using Microsoft.Extensions.Options;
|
|||||||
|
|
||||||
namespace EnvelopeGenerator.ReceiverUI.Services;
|
namespace EnvelopeGenerator.ReceiverUI.Services;
|
||||||
|
|
||||||
public class DocReceiverElementService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
public class SignatureService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
public async Task<IReadOnlyList<SignatureDto>> GetAsync(string envelopeKey, CancellationToken cancel = default)
|
public async Task<IReadOnlyList<SignatureDto>> GetAsync(string envelopeKey, CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
var url = $"{apiOptions.Value.BaseUrl}/api/DocReceiverElement/{Uri.EscapeDataString(envelopeKey)}";
|
var url = $"{apiOptions.Value.BaseUrl}/api/Signature/{Uri.EscapeDataString(envelopeKey)}";
|
||||||
var response = await http.GetAsync(url, cancel);
|
var response = await http.GetAsync(url, cancel);
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
@using System.Globalization
|
|
||||||
@using EnvelopeGenerator.ReceiverUI.Services
|
|
||||||
@inject IJSRuntime JSRuntime
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
@inject CultureService CultureService
|
|
||||||
|
|
||||||
<div class="language-selector">
|
|
||||||
<button class="language-selector__trigger" @onclick="ToggleDropdown" aria-label="Select Language">
|
|
||||||
<span class="fi fi-@GetFlagCode(CurrentCulture)"></span>
|
|
||||||
<span class="language-selector__arrow">@GetLanguageName(CurrentCulture)</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@if (isOpen)
|
|
||||||
{
|
|
||||||
<div class="language-selector__dropdown">
|
|
||||||
<button class="language-selector__option" @onclick="@(() => ChangeLanguageAsync("de-DE"))">
|
|
||||||
<span class="fi fi-de"></span>
|
|
||||||
<span>Deutsch</span>
|
|
||||||
</button>
|
|
||||||
<button class="language-selector__option" @onclick="@(() => ChangeLanguageAsync("en-US"))">
|
|
||||||
<span class="fi fi-us"></span>
|
|
||||||
<span>English</span>
|
|
||||||
</button>
|
|
||||||
<button class="language-selector__option" @onclick="@(() => ChangeLanguageAsync("fr-FR"))">
|
|
||||||
<span class="fi fi-fr"></span>
|
|
||||||
<span>Français</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
private bool isOpen = false;
|
|
||||||
private string CurrentCulture => CultureInfo.CurrentCulture.Name;
|
|
||||||
|
|
||||||
private void ToggleDropdown()
|
|
||||||
{
|
|
||||||
isOpen = !isOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ChangeLanguageAsync(string culture)
|
|
||||||
{
|
|
||||||
if (CultureInfo.CurrentCulture.Name != culture)
|
|
||||||
{
|
|
||||||
await CultureService.SetCultureAsync(culture);
|
|
||||||
|
|
||||||
// Set culture without page reload
|
|
||||||
var cultureInfo = new CultureInfo(culture);
|
|
||||||
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
|
|
||||||
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
|
|
||||||
|
|
||||||
// Navigate without reload to trigger re-render
|
|
||||||
Navigation.NavigateTo(Navigation.Uri, forceLoad: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
isOpen = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetFlagCode(string culture)
|
|
||||||
{
|
|
||||||
return culture switch
|
|
||||||
{
|
|
||||||
"de-DE" => "de",
|
|
||||||
"en-US" => "us",
|
|
||||||
"fr-FR" => "fr",
|
|
||||||
_ => "de"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetLanguageName(string culture)
|
|
||||||
{
|
|
||||||
return culture switch
|
|
||||||
{
|
|
||||||
"de-DE" => "Deutsch",
|
|
||||||
"en-US" => "English",
|
|
||||||
"fr-FR" => "Français",
|
|
||||||
_ => "Deutsch"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,12 +8,9 @@
|
|||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
<footer class="receiver-footer">
|
<footer class="receiver-footer">
|
||||||
<div class="receiver-footer__content">
|
<span>© SignFlow 2023-2024 <a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a></span>
|
||||||
<span>© SignFlow 2023-2024 <a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a></span>
|
<span class="receiver-footer__sep">|</span>
|
||||||
<span class="receiver-footer__sep">|</span>
|
<a href="docs/privacy-policy.de-DE.html" target="_blank" rel="noopener">Datenschutz</a>
|
||||||
<a href="docs/privacy-policy.de-DE.html" target="_blank" rel="noopener">Datenschutz</a>
|
|
||||||
</div>
|
|
||||||
<LanguageSelector />
|
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -15,18 +15,3 @@ article {
|
|||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
margin: 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,75 +365,4 @@ article {
|
|||||||
|
|
||||||
.receiver-footer__sep {
|
.receiver-footer__sep {
|
||||||
opacity: 0.4;
|
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;
|
|
||||||
}
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
.sender-dashboard-layout {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #7e22ce 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-action-bar {
|
|
||||||
background: rgba(255, 255, 255, 0.95);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
border-bottom: 3px solid rgba(126, 34, 206, 0.3);
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-action-bar__inner {
|
|
||||||
max-width: 1600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-title-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-logo svg {
|
|
||||||
filter: drop-shadow(0 2px 4px rgba(126, 34, 206, 0.3));
|
|
||||||
color: #7e22ce;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-title {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1e293b;
|
|
||||||
letter-spacing: -0.025em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.625rem 1.125rem;
|
|
||||||
background: linear-gradient(135deg, rgba(126, 34, 206, 0.05) 0%, rgba(42, 82, 152, 0.05) 100%);
|
|
||||||
border: 1px solid rgba(126, 34, 206, 0.2);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1e293b;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-btn:hover:not(:disabled) {
|
|
||||||
background: linear-gradient(135deg, rgba(126, 34, 206, 0.1) 0%, rgba(42, 82, 152, 0.1) 100%);
|
|
||||||
border-color: rgba(126, 34, 206, 0.4);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 12px rgba(126, 34, 206, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-btn:disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
background: rgba(0, 0, 0, 0.02);
|
|
||||||
border-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-btn--primary {
|
|
||||||
background: linear-gradient(135deg, #7e22ce 0%, #2a5298 100%);
|
|
||||||
border-color: transparent;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-btn--primary:hover:not(:disabled) {
|
|
||||||
background: linear-gradient(135deg, #6b1cb0 0%, #1e3a72 100%);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 16px rgba(126, 34, 206, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-btn--danger {
|
|
||||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(220, 38, 38, 0.08) 100%);
|
|
||||||
border-color: rgba(239, 68, 68, 0.3);
|
|
||||||
color: #dc2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-btn--danger:hover:not(:disabled) {
|
|
||||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
||||||
border-color: transparent;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-btn--logout {
|
|
||||||
padding: 0.5rem;
|
|
||||||
min-width: 38px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-content {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
padding: 1.5rem;
|
|
||||||
position: relative;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-grid-container {
|
|
||||||
background: rgba(255, 255, 255, 0.98);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
max-width: 1600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-grid-container::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 4px;
|
|
||||||
background: linear-gradient(90deg, #7e22ce 0%, #2a5298 100%);
|
|
||||||
z-index: 1;
|
|
||||||
border-radius: 16px 16px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-tabs {
|
|
||||||
display: flex;
|
|
||||||
border-bottom: 2px solid rgba(126, 34, 206, 0.1);
|
|
||||||
padding: 0 2rem;
|
|
||||||
background: rgba(126, 34, 206, 0.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-tab {
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #6b7280;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 3px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-tab:hover {
|
|
||||||
color: #7e22ce;
|
|
||||||
background: rgba(126, 34, 206, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-tab--active {
|
|
||||||
color: #7e22ce;
|
|
||||||
border-bottom-color: #7e22ce;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-grid-wrapper {
|
|
||||||
padding: 1.5rem 2rem 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide DevExpress empty cells */
|
|
||||||
.dxbl-grid-empty-cell {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.375rem;
|
|
||||||
padding: 0.25rem 0.625rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge--partly-signed,
|
|
||||||
.status-badge--completed {
|
|
||||||
background: rgba(129, 199, 132, 0.15);
|
|
||||||
color: #2e7d32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge--queued,
|
|
||||||
.status-badge--sent {
|
|
||||||
background: rgba(255, 183, 77, 0.15);
|
|
||||||
color: #e65100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge--deleted,
|
|
||||||
.status-badge--rejected,
|
|
||||||
.status-badge--withdrawn {
|
|
||||||
background: rgba(229, 115, 115, 0.15);
|
|
||||||
color: #c62828;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge--created,
|
|
||||||
.status-badge--saved {
|
|
||||||
background: rgba(100, 181, 246, 0.15);
|
|
||||||
color: #1565c0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot--green {
|
|
||||||
background: #81c784;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot--orange {
|
|
||||||
background: #ffb74d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot--red {
|
|
||||||
background: #e57373;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot--blue {
|
|
||||||
background: #64b5f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.receiver-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
padding: 0.125rem 0.5rem;
|
|
||||||
background: #f3f4f6;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #374151;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.receiver-badge--signed {
|
|
||||||
background: rgba(129, 199, 132, 0.15);
|
|
||||||
color: #2e7d32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.receiver-badge--unsigned {
|
|
||||||
background: rgba(229, 115, 115, 0.15);
|
|
||||||
color: #c62828;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@media (max-width: 768px) {
|
|
||||||
.sender-action-bar {
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-action-bar__inner {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-toolbar {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-title {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-content {
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-grid-wrapper {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-tabs {
|
|
||||||
padding: 0 1rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sender-tab {
|
|
||||||
padding: 0.875rem 1rem;
|
|
||||||
font-size: 0.813rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||||
<link href="EnvelopeGenerator.ReceiverUI.styles.css" rel="stylesheet" />
|
<link href="EnvelopeGenerator.ReceiverUI.styles.css" rel="stylesheet" />
|
||||||
<link href="css/app.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">
|
<style type="text/css">
|
||||||
.splash-screen {
|
.splash-screen {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
namespace EnvelopeGenerator.WebUI.Client.Data {
|
||||||
|
public class Adjustment
|
||||||
|
{
|
||||||
|
public static Adjustment CreateBalanceForward(DateTime dt, int random)
|
||||||
|
{
|
||||||
|
var rnd = new DeterministicRandom(random);
|
||||||
|
Adjustment res = new Adjustment();
|
||||||
|
res.currentDateTime = dt;
|
||||||
|
res.currentDescription = "Balance Forward";
|
||||||
|
res.currentAmount = rnd.Random(10, 300) * 10;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
public static Adjustment CreatePayment(DateTime dt, int random)
|
||||||
|
{
|
||||||
|
var rnd = new DeterministicRandom(random);
|
||||||
|
Adjustment res = new Adjustment();
|
||||||
|
res.currentDateTime = dt;
|
||||||
|
res.currentDescription = "Payment";
|
||||||
|
res.currentAmount = -rnd.Random(1, 40) * 10;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
public static Adjustment CreateCharge(DateTime dt, int random)
|
||||||
|
{
|
||||||
|
var rnd = new DeterministicRandom(random);
|
||||||
|
Adjustment res = new Adjustment();
|
||||||
|
res.currentDateTime = dt;
|
||||||
|
res.currentDescription = rnd.GetRandomItem(bills);
|
||||||
|
res.currentAmount = rnd.Random(10, 50) * 10;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime currentDateTime;
|
||||||
|
string currentDescription = "";
|
||||||
|
double currentAmount = 0;
|
||||||
|
static readonly string[] bills = new string[] { "Bill - Insurance", "Bill - Electricity", "Bill - Rent", "Bill - Phone", "Bill - Office Supplies" };
|
||||||
|
public DateTime Date { get { return currentDateTime; } }
|
||||||
|
public string Description { get { return currentDescription; } }
|
||||||
|
public double Amount { get { return currentAmount; } }
|
||||||
|
|
||||||
|
public Adjustment()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using DevExpress.DataAccess.Sql;
|
||||||
|
using DevExpress.DataAccess.Sql.DataApi;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.WebUI.Client.Data {
|
||||||
|
public class Customer {
|
||||||
|
static List<Customer> currentCustomers = new List<Customer>();
|
||||||
|
|
||||||
|
public static List<Customer> Customers { get { return currentCustomers; } }
|
||||||
|
static Customer() {
|
||||||
|
try {
|
||||||
|
SqlDataSource ds = new SqlDataSource("NWindConnectionString");
|
||||||
|
SelectQuery query = SelectQueryFluentBuilder
|
||||||
|
.AddTable("Customers")
|
||||||
|
.SelectAllColumns()
|
||||||
|
.Build("Customers");
|
||||||
|
ds.Queries.Add(query);
|
||||||
|
ds.RebuildResultSchema();
|
||||||
|
ds.Fill();
|
||||||
|
ITable src = ds.Result["Customers"];
|
||||||
|
foreach(var row in src) {
|
||||||
|
currentCustomers.Add(new Customer() {
|
||||||
|
CustomerID = row.GetValue<string>("CustomerID"),
|
||||||
|
Address = row.GetValue<string>("Address"),
|
||||||
|
CompanyName = row.GetValue<string>("CompanyName"),
|
||||||
|
ContactName = row.GetValue<string>("ContactName"),
|
||||||
|
ContactTitle = row.GetValue<string>("ContactTitle"),
|
||||||
|
Country = row.GetValue<string>("Country"),
|
||||||
|
City = row.GetValue<string>("City"),
|
||||||
|
Fax = row.GetValue<string>("Fax"),
|
||||||
|
Phone = row.GetValue<string>("Phone"),
|
||||||
|
PostalCode = row.GetValue<string>("PostalCode"),
|
||||||
|
Region = row.GetValue<string>("Region")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
currentCustomers.Add(new Customer() {
|
||||||
|
Address = "Obere Str. 57",
|
||||||
|
City = "Berlin",
|
||||||
|
CompanyName = "Alfreds Futterkiste",
|
||||||
|
ContactName = "Maria Anders",
|
||||||
|
ContactTitle = "Sales Representative",
|
||||||
|
Country = "Germany",
|
||||||
|
CustomerID = "ALFKI",
|
||||||
|
Fax = "030-0076545",
|
||||||
|
Phone = "030-0074321",
|
||||||
|
PostalCode = "12209"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CustomerID { get; set; }
|
||||||
|
public string CompanyName { get; set; }
|
||||||
|
public string ContactName { get; set; }
|
||||||
|
public string ContactTitle { get; set; }
|
||||||
|
public string Address { get; set; }
|
||||||
|
public string City { get; set; }
|
||||||
|
public string PostalCode { get; set; }
|
||||||
|
public string Region { get; set; }
|
||||||
|
public string Country { get; set; }
|
||||||
|
public string Phone { get; set; }
|
||||||
|
public string Fax { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
namespace EnvelopeGenerator.WebUI.Client.Data {
|
||||||
|
public class DataItem {
|
||||||
|
static readonly string[] accountType = new string[] { "Energy", "Manufacturing", "Estate", "Food", "Services" };
|
||||||
|
public string CustomerID { get; set; }
|
||||||
|
public string CompanyName { get; set; }
|
||||||
|
public string ContactName { get; set; }
|
||||||
|
public string ContactTitle { get; set; }
|
||||||
|
public string Address { get; set; }
|
||||||
|
public string City { get; set; }
|
||||||
|
public string PostalCode { get; set; }
|
||||||
|
public string Region { get; set; }
|
||||||
|
public string Country { get; set; }
|
||||||
|
public string Phone { get; set; }
|
||||||
|
public string Fax { get; set; }
|
||||||
|
public string Email { get; set; }
|
||||||
|
public string Invoice { get; set; }
|
||||||
|
public string CustomerAccount { get; set; }
|
||||||
|
public string CustomerIdentifiers { get; set; }
|
||||||
|
public DateTime BillingDate { get; set; }
|
||||||
|
public DateTime BillingPeriodStart { get; set; }
|
||||||
|
public DateTime BillingPeriodEnd { get; set; }
|
||||||
|
public string Terms { get; set; }
|
||||||
|
public string TermsID { get; set; }
|
||||||
|
public Adjustment[] Adjustments { get; set; }
|
||||||
|
|
||||||
|
public DataItem(int i) {
|
||||||
|
var rnd = new DeterministicRandom(i);
|
||||||
|
Customer c = rnd.GetRandomItem(Customer.Customers);
|
||||||
|
CustomerID = c.CustomerID;
|
||||||
|
CompanyName = c.CompanyName;
|
||||||
|
ContactName = c.ContactName;
|
||||||
|
ContactTitle = c.ContactTitle;
|
||||||
|
Address = c.Address;
|
||||||
|
City = c.City;
|
||||||
|
PostalCode = c.PostalCode;
|
||||||
|
Region = c.Region;
|
||||||
|
Country = c.Country;
|
||||||
|
Phone = c.Phone;
|
||||||
|
Fax = c.Fax;
|
||||||
|
Email = ContactName.Split(' ')[0].Replace(' ', '.').ToLower() + "@" + CompanyName.Split(' ')[0].ToLower() + ".com";
|
||||||
|
Invoice = string.Format("{0}{1}-{2}", rnd.RandomChar, rnd.Random(100, 1000), rnd.Random(100, 1000));
|
||||||
|
CustomerAccount = rnd.GetRandomItem(accountType);
|
||||||
|
CustomerIdentifiers = string.Format("{0}-{1}", rnd.Random(1000, 10000), rnd.Random(10, 100));
|
||||||
|
BillingPeriodStart = rnd.RandomTime();
|
||||||
|
BillingPeriodEnd = rnd.RandomTime(BillingPeriodStart, 7 * 24, 30 * 24);
|
||||||
|
BillingDate = rnd.RandomTime(BillingPeriodEnd, 7 * 24, 30 * 24);
|
||||||
|
Term currentTerm = rnd.GetRandomItem(Term.Terms);
|
||||||
|
Terms = currentTerm.Name;
|
||||||
|
|
||||||
|
int adjustmentsCount = rnd.Random(6) + 4;
|
||||||
|
Adjustments = new Adjustment[adjustmentsCount];
|
||||||
|
int h = (int)((BillingPeriodEnd - BillingPeriodStart).TotalHours / adjustmentsCount);
|
||||||
|
|
||||||
|
Adjustments[0] = Adjustment.CreateBalanceForward(rnd.RandomTime(BillingPeriodStart, 0, h), rnd.Random(10000));
|
||||||
|
|
||||||
|
int[] items = rnd.RandomList(adjustmentsCount - 1, 2);
|
||||||
|
|
||||||
|
for(int j = 1; j < Adjustments.Length; j++) {
|
||||||
|
DateTime nextDate = rnd.RandomTime(BillingPeriodStart.AddHours(h * j), 0, h);
|
||||||
|
switch(items[j - 1]) {
|
||||||
|
case 0:
|
||||||
|
Adjustments[j] = Adjustment.CreateCharge(nextDate, rnd.Random(10000));
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
Adjustments[j] = Adjustment.CreatePayment(nextDate, rnd.Random(10000));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.WebUI.Client.Data {
|
||||||
|
public class DataItemList : IList<DataItem>, IList {
|
||||||
|
readonly int rowCount;
|
||||||
|
|
||||||
|
public DataItem this[int index] { get { return new DataItem(index); } set { } }
|
||||||
|
public int Count { get { return rowCount; } }
|
||||||
|
public bool IsReadOnly { get { return false; } }
|
||||||
|
public bool IsFixedSize { get { return false; } }
|
||||||
|
public object SyncRoot { get { return true; } }
|
||||||
|
public bool IsSynchronized { get { return true; } }
|
||||||
|
object IList.this[int index] { get { return new DataItem(index); } set { } }
|
||||||
|
|
||||||
|
public DataItemList(int rowCount) {
|
||||||
|
this.rowCount = rowCount;
|
||||||
|
}
|
||||||
|
public IEnumerator<DataItem> GetEnumerator() {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
public int Add(object value) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
public bool Contains(object value) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
public void Clear() {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
public int IndexOf(object value) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
public void Insert(int index, object value) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
public void Remove(object value) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
public void RemoveAt(int index) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
public void CopyTo(Array array, int index) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
public int IndexOf(DataItem item) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
public void Insert(int index, DataItem item) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
public void Add(DataItem item) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
public bool Contains(DataItem item) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
public void CopyTo(DataItem[] array, int arrayIndex) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
public bool Remove(DataItem item) {
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
void ICollection<DataItem>.CopyTo(DataItem[] array, int arrayIndex) {
|
||||||
|
CopyTo(array, arrayIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
namespace EnvelopeGenerator.WebUI.Client.Data {
|
||||||
|
class DeterministicRandom {
|
||||||
|
const int randomCount = 10000;
|
||||||
|
static readonly int[] deterministicRandomNumbers;
|
||||||
|
static readonly DateTime time;
|
||||||
|
int rnd;
|
||||||
|
int Next {
|
||||||
|
get {
|
||||||
|
rnd = deterministicRandomNumbers[rnd % randomCount];
|
||||||
|
return rnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public char RandomChar {
|
||||||
|
get {
|
||||||
|
return (char)((int)'A' + Random(0, 26));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public int[] RandomList(int count, int to) {
|
||||||
|
int[] res = new int[count];
|
||||||
|
for(int i = 0; i < Math.Min(count, to); i++)
|
||||||
|
res[i] = i;
|
||||||
|
for(int i = to; i < count; i++)
|
||||||
|
res[i] = Random(to);
|
||||||
|
|
||||||
|
for(int i = 0; i < count; i++) {
|
||||||
|
int ind = Random(count);
|
||||||
|
int temp = res[ind];
|
||||||
|
res[ind] = res[i];
|
||||||
|
res[i] = temp;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
public int Random(int to) {
|
||||||
|
return Random(0, to);
|
||||||
|
}
|
||||||
|
public int Random(int from, int to) {
|
||||||
|
return Next % Math.Max(1, to - from) + from;
|
||||||
|
}
|
||||||
|
public T GetRandomItem<T>(IList<T> list) {
|
||||||
|
return list[Next % list.Count];
|
||||||
|
}
|
||||||
|
public DateTime RandomTime() {
|
||||||
|
return RandomTime(time, 0, 30 * 24);
|
||||||
|
}
|
||||||
|
public DateTime RandomTime(DateTime from, int fromHours, int toHours) {
|
||||||
|
return from.AddHours(Next % (toHours - fromHours) + fromHours);
|
||||||
|
}
|
||||||
|
|
||||||
|
static DeterministicRandom() {
|
||||||
|
time = DateTime.Now.AddDays(-62);
|
||||||
|
Random currentRandom = new Random(randomCount);
|
||||||
|
deterministicRandomNumbers = new int[randomCount];
|
||||||
|
for(int i = 0; i < randomCount; i++)
|
||||||
|
deterministicRandomNumbers[i] = currentRandom.Next(randomCount);
|
||||||
|
}
|
||||||
|
public DeterministicRandom(int i) {
|
||||||
|
this.rnd = i + (i >> 10) + (i >> 20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace EnvelopeGenerator.WebUI.Client.Data {
|
||||||
|
public struct Term {
|
||||||
|
public static readonly Term[] Terms = new Term[] {
|
||||||
|
new Term("Payment seven days after invoice date" ),
|
||||||
|
new Term("Payment ten days after invoice date" ),
|
||||||
|
new Term("End of month" ),
|
||||||
|
new Term("21st of the month following invoice date" ),
|
||||||
|
};
|
||||||
|
readonly string currentName;
|
||||||
|
public string Name { get { return currentName; } }
|
||||||
|
public Term(string currentName) {
|
||||||
|
this.currentName = currentName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
||||||
|
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
|
||||||
|
<WasmBuildNative>true</WasmBuildNative>
|
||||||
|
<InvariantGlobalization>false</InvariantGlobalization>
|
||||||
|
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="DevExpress.Blazor.PdfViewer" Version="25.2.3" />
|
||||||
|
<PackageReference Include="DevExpress.Blazor.Reporting.JSBasedControls" Version="25.2.3" />
|
||||||
|
<PackageReference Include="DevExpress.Blazor.Reporting.Viewer" Version="25.2.3" />
|
||||||
|
<PackageReference Include="DevExpress.Drawing.Skia" Version="25.2.3" />
|
||||||
|
<PackageReference Include="HarfBuzzSharp.NativeAssets.WebAssembly" Version="8.3.1.2" />
|
||||||
|
<PackageReference Include="SkiaSharp.NativeAssets.WebAssembly" Version="3.119.1" />
|
||||||
|
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.1" />
|
||||||
|
<NativeFileReference Include="$(HarfBuzzSharpStaticLibraryPath)\2.0.23\*.a" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.22" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.11" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Update="PredefinedReports\Report.cs">
|
||||||
|
<SubType>XtraReport</SubType>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<nav class="navbar header-navbar p-0">
|
||||||
|
<button class="navbar-toggler bg-primary d-block" @onclick="OnToggleClick">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="ms-3 fw-bold title pe-4">EnvelopeGenerator.ReceiverUI</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public bool ToggleOn { get; set; }
|
||||||
|
[Parameter] public EventCallback<bool> ToggleOnChanged { get; set; }
|
||||||
|
|
||||||
|
async Task OnToggleClick() => await Toggle();
|
||||||
|
|
||||||
|
async Task Toggle(bool? value = null) {
|
||||||
|
var newValue = value ?? !ToggleOn;
|
||||||
|
if(ToggleOn != newValue) {
|
||||||
|
ToggleOn = newValue;
|
||||||
|
await ToggleOnChanged.InvokeAsync(ToggleOn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
@using EnvelopeGenerator.WebUI.Client.Services;
|
||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
<main>
|
||||||
|
<article class="content">
|
||||||
|
@Body
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<footer class="receiver-footer">
|
||||||
|
<span>© SignFlow 2023-2024 <a href="https://digitaldata.works" target="_blank" rel="noopener">Digital Data GmbH</a></span>
|
||||||
|
<span class="receiver-footer__sep">|</span>
|
||||||
|
<a href="docs/privacy-policy.de-DE.html" target="_blank" rel="noopener">Datenschutz</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Inject] HttpClient Http { get; set; }
|
||||||
|
List<string> RequiredFonts = new() {
|
||||||
|
"opensans.ttf"
|
||||||
|
};
|
||||||
|
|
||||||
|
protected async override Task OnInitializedAsync() {
|
||||||
|
await FontLoader.LoadFonts(Http, RequiredFonts);
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#blazor-error-ui {
|
||||||
|
background: lightyellow;
|
||||||
|
bottom: 0;
|
||||||
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
display: none;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui .dismiss {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<div class="top-row ps-3 navbar navbar-dark">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="">EnvelopeGenerator.ReceiverUI</a>
|
||||||
|
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
|
||||||
|
<nav class="flex-column">
|
||||||
|
@*
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||||
|
<span class="oi oi-home" aria-hidden="true"></span> Home
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="documentviewer">
|
||||||
|
<span class="oi oi-plus" aria-hidden="true"></span> Document Viewer (JS-Based)
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
*@
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="receiver">
|
||||||
|
<span class="oi oi-plus" aria-hidden="true"></span> Empfänger-UI
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="sender">
|
||||||
|
<span class="oi oi-plus" aria-hidden="true"></span> Umschlag-UI
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private bool collapseNavMenu = true;
|
||||||
|
|
||||||
|
private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
||||||
|
|
||||||
|
private void ToggleNavMenu()
|
||||||
|
{
|
||||||
|
collapseNavMenu = !collapseNavMenu;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace EnvelopeGenerator.WebUI.Client.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a pre-assigned signature annotation position on a specific page.
|
||||||
|
/// <br/><br/>
|
||||||
|
/// <b>Coordinate unit (X, Y):</b> Inches (GdPicture14 native unit),
|
||||||
|
/// origin at the <b>top-left</b> corner of the page, both axes increase downward/rightward.
|
||||||
|
/// <br/><br/>
|
||||||
|
/// <b>Conversion to DevExpress:</b> Multiply by 100 (DX uses 1/100 inch).
|
||||||
|
/// Convert: <c>xDX = xInches * 100.0</c>
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Conversion to PDF Points:</b> Multiply by 72 (1 inch = 72 points).
|
||||||
|
/// Convert: <c>xPt = xInches * 72.0</c>
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Y-axis for PDF (bottom-left origin):</b> Flip required for iText7.
|
||||||
|
/// Convert: <c>yPt = (pageHeightInches - yInches - elemHeightInches) * 72.0</c>
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Use SignatureDto with SignatureService.")]
|
||||||
|
public record AnnotationDto
|
||||||
|
{
|
||||||
|
/// <summary>Unique identifier of the annotation.</summary>
|
||||||
|
public long Id { get; init; }
|
||||||
|
|
||||||
|
/// <summary>1-based page number within the document.</summary>
|
||||||
|
public int Page { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Horizontal position in INCHES from the left edge of the page.</summary>
|
||||||
|
public double X { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Vertical position in INCHES from the top edge of the page.</summary>
|
||||||
|
public double Y { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace EnvelopeGenerator.WebUI.Client.Models.Constants
|
||||||
|
{
|
||||||
|
public enum SenderAppType
|
||||||
|
{
|
||||||
|
LegacyFormApp = 0,
|
||||||
|
ReceiverUIBlazorApp = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
namespace EnvelopeGenerator.WebUI.Client.Models.Constants;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the unit of measurement for coordinate values in signature positioning.
|
||||||
|
/// Used for converting coordinates between different systems (GdPicture14, PDF.js, iText7).
|
||||||
|
/// </summary>
|
||||||
|
public enum UnitOfLength
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Inch unit (1 inch = 25.4 mm).
|
||||||
|
/// This is the native unit used by GdPicture14 (EnvelopeGenerator.Form - Legacy VB.NET app).
|
||||||
|
/// Database stores all coordinates (X, Y, Width, Height) in INCHES.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <b>Source:</b> GdPicture14.Annotations.AnnotationStickyNote uses INCHES natively.
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Evidence:</b> VB.NET code directly assigns database values to annotation properties without conversion:
|
||||||
|
/// <code>
|
||||||
|
/// oAnnotation.Left = CSng(pElement.X) ' Direct assignment → INCHES
|
||||||
|
/// oAnnotation.Top = CSng(pElement.Y)
|
||||||
|
/// </code>
|
||||||
|
/// <b>Standard Page Dimensions:</b>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>A4: 8.27" × 11.69" (210mm × 297mm)</item>
|
||||||
|
/// <item>Letter: 8.5" × 11"</item>
|
||||||
|
/// </list>
|
||||||
|
/// </remarks>
|
||||||
|
Inch = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PDF Point unit (1 point = 1/72 inch).
|
||||||
|
/// This is the standard unit used by PDF specification and PDF.js viewer.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <b>Definition:</b> According to PDF specification and Microsoft documentation:
|
||||||
|
/// <br/>
|
||||||
|
/// <i>"PDF pages are sized in point units. 1 pt == 1/72 inch"</i>
|
||||||
|
/// <br/><br/>
|
||||||
|
/// <b>Conversion Formula:</b>
|
||||||
|
/// <code>
|
||||||
|
/// points = inches * 72.0
|
||||||
|
/// inches = points / 72.0
|
||||||
|
/// </code>
|
||||||
|
/// <b>Important:</b> Point ≠ Pixel!
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><b>Point (pt):</b> Device-independent unit (always 1/72 inch)</item>
|
||||||
|
/// <item><b>Pixel (px):</b> Device-dependent unit (varies with screen DPI)</item>
|
||||||
|
/// <item>At 72 DPI: 1 point = 1 pixel (coincidence)</item>
|
||||||
|
/// <item>At 96 DPI: 1 point ≈ 1.33 pixels</item>
|
||||||
|
/// <item>At 300 DPI: 1 point ≈ 4.17 pixels</item>
|
||||||
|
/// </list>
|
||||||
|
/// <b>Standard Page Dimensions (in points):</b>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>A4: 595 × 842 points (8.27" × 11.69" × 72)</item>
|
||||||
|
/// <item>Letter: 612 × 792 points (8.5" × 11" × 72)</item>
|
||||||
|
/// </list>
|
||||||
|
/// <b>Usage in EnvelopeGenerator:</b>
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>PDF.js viewer expects coordinates in points</item>
|
||||||
|
/// <item>iText7 library uses points for PDF manipulation</item>
|
||||||
|
/// <item>PSPDFKit (Web) uses points for annotation placement</item>
|
||||||
|
/// </list>
|
||||||
|
/// </remarks>
|
||||||
|
Point
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
namespace EnvelopeGenerator.WebUI.Client.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client-side model for the envelope receiver returned by
|
||||||
|
/// <c>GET api/EnvelopeReceiver/{envelopeKey}</c>.
|
||||||
|
/// </summary>
|
||||||
|
public record EnvelopeReceiverDto
|
||||||
|
{
|
||||||
|
public int EnvelopeId { get; init; }
|
||||||
|
public int ReceiverId { get; init; }
|
||||||
|
public int Sequence { get; init; }
|
||||||
|
|
||||||
|
public string? Name { get; init; }
|
||||||
|
public string? JobTitle { get; init; }
|
||||||
|
public string? CompanyName { get; init; }
|
||||||
|
public string? PrivateMessage { get; init; }
|
||||||
|
|
||||||
|
public DateTime AddedWhen { get; init; }
|
||||||
|
public DateTime? ChangedWhen { get; init; }
|
||||||
|
public bool HasPhoneNumber { get; init; }
|
||||||
|
|
||||||
|
public EnvelopeClientDto? Envelope { get; init; }
|
||||||
|
public ReceiverClientDto? Receiver { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client-side model for the envelope data embedded in <see cref="EnvelopeReceiverDto"/>.
|
||||||
|
/// </summary>
|
||||||
|
public record EnvelopeClientDto
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public int UserId { get; init; }
|
||||||
|
public int Status { get; init; }
|
||||||
|
public string StatusName { get; init; } = string.Empty;
|
||||||
|
public string Uuid { get; init; } = string.Empty;
|
||||||
|
public string Title { get; init; } = string.Empty;
|
||||||
|
public string Message { get; init; } = string.Empty;
|
||||||
|
public DateTime AddedWhen { get; init; }
|
||||||
|
public DateTime? ChangedWhen { get; init; }
|
||||||
|
public string Language { get; init; } = "de-DE";
|
||||||
|
public int? EnvelopeTypeId { get; init; }
|
||||||
|
public string? EnvelopeTypeTitle { get; init; }
|
||||||
|
public int? ContractType { get; init; }
|
||||||
|
public int? CertificationType { get; init; }
|
||||||
|
public bool UseAccessCode { get; init; }
|
||||||
|
public bool TFAEnabled { get; init; }
|
||||||
|
public IEnumerable<DocumentClientDto>? Documents { get; init; }
|
||||||
|
public EnvelopeSenderDto? User { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sender (user) information embedded in <see cref="EnvelopeClientDto"/>.
|
||||||
|
/// </summary>
|
||||||
|
public record EnvelopeSenderDto
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public string? Username { get; init; }
|
||||||
|
public string? FullName { get; init; }
|
||||||
|
public string? Email { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client-side model for a document embedded in <see cref="EnvelopeClientDto"/>.
|
||||||
|
/// </summary>
|
||||||
|
public record DocumentClientDto
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public int EnvelopeId { get; init; }
|
||||||
|
public DateTime AddedWhen { get; init; }
|
||||||
|
public IEnumerable<SignatureClientDto>? Elements { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client-side model for a signature/annotation element embedded in <see cref="DocumentClientDto"/>.
|
||||||
|
/// </summary>
|
||||||
|
public record SignatureClientDto
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public int DocumentId { get; init; }
|
||||||
|
public int ReceiverId { get; init; }
|
||||||
|
public int ElementType { get; init; }
|
||||||
|
public double X { get; init; }
|
||||||
|
public double Y { get; init; }
|
||||||
|
public double Width { get; init; }
|
||||||
|
public double Height { get; init; }
|
||||||
|
public int Page { get; init; }
|
||||||
|
public bool Required { get; init; }
|
||||||
|
public string? Tooltip { get; init; }
|
||||||
|
public bool ReadOnly { get; init; }
|
||||||
|
public int AnnotationIndex { get; init; }
|
||||||
|
public DateTime AddedWhen { get; init; }
|
||||||
|
public DateTime? ChangedWhen { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client-side model for the receiver data embedded in <see cref="EnvelopeReceiverDto"/>.
|
||||||
|
/// </summary>
|
||||||
|
public record ReceiverClientDto
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public string? EmailAddress { get; init; }
|
||||||
|
public string? Signature { get; init; }
|
||||||
|
public DateTime AddedWhen { get; init; }
|
||||||
|
public DateTime? TfaRegDeadline { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
namespace EnvelopeGenerator.WebUI.Client.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a captured signature with metadata created by the receiver in the signature popup.
|
||||||
|
/// This model holds the signature image (as base64 data URL) along with signer information
|
||||||
|
/// used for rendering applied signatures on the PDF canvas.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <b>Used in:</b> EnvelopeViewer.razor signature popup workflow
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Creation:</b> User draws/types/uploads signature and fills required fields
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Storage:</b> Session-only (Blazor component state, lost on page refresh)
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Rendering:</b> Applied signatures display: Image + Separator + Name/Position/Place/Date
|
||||||
|
/// </remarks>
|
||||||
|
public sealed record SignatureCaptureDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base64-encoded data URL of the signature image.
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Format:</b> <c>data:image/png;base64,iVBORw0KG...</c>
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Source:</b> Canvas.toDataURL() from signature pad (draw/text/image tabs)
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Usage:</b> Set as <c>img.src</c> in applied signature overlay
|
||||||
|
/// </summary>
|
||||||
|
public required string DataUrl { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Full name of the signer (first and last name).
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Required:</b> Yes (validated in popup)
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Display:</b> Bold text in applied signature block
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Example:</b> "Max Mustermann"
|
||||||
|
/// </summary>
|
||||||
|
public required string FullName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Job title or position of the signer.
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Required:</b> No (optional field)
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Display:</b> Normal weight text between name and place/date
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Example:</b> "Geschäftsführer" or empty string
|
||||||
|
/// </summary>
|
||||||
|
public string Position { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Location/place where the signature was created.
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Required:</b> Yes (validated in popup)
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Display:</b> Shown with current date in German format (dd.MM.yyyy)
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Example:</b> "Berlin" ? rendered as "Berlin, 26.01.2025"
|
||||||
|
/// </summary>
|
||||||
|
public required string Place { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using EnvelopeGenerator.WebUI.Client.Models.Constants;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.WebUI.Client.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a signature position on a PDF page.
|
||||||
|
/// Coordinates stored in INCHES (GdPicture14 native unit).
|
||||||
|
/// Origin: Top-left corner, X increases right, Y increases down.
|
||||||
|
/// </summary>
|
||||||
|
public class SignatureDto
|
||||||
|
{
|
||||||
|
/// <summary>Unique identifier.</summary>
|
||||||
|
public int Id { get; init; }
|
||||||
|
|
||||||
|
private double _x;
|
||||||
|
private double _y;
|
||||||
|
|
||||||
|
/// <summary>Horizontal position in INCHES from left edge.</summary>
|
||||||
|
public double X
|
||||||
|
{
|
||||||
|
get => _x * Factor;
|
||||||
|
init => _x = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Vertical position in INCHES from top edge.</summary>
|
||||||
|
public double Y
|
||||||
|
{
|
||||||
|
get => _y * Factor;
|
||||||
|
init => _y = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>1-based page number.</summary>
|
||||||
|
public int Page { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Sender application type that created this signature.</summary>
|
||||||
|
public SenderAppType SenderAppType { get; init; }
|
||||||
|
|
||||||
|
private UnitOfLength _unitOfLength;
|
||||||
|
|
||||||
|
public SignatureDto Convert(UnitOfLength unitOfLength)
|
||||||
|
{
|
||||||
|
_unitOfLength = unitOfLength;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Factor
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (SenderAppType != SenderAppType.LegacyFormApp)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException(
|
||||||
|
$"SenderAppType '{SenderAppType}' is not yet implemented. " +
|
||||||
|
$"Currently, only '{nameof(SenderAppType.LegacyFormApp)}' is supported. " +
|
||||||
|
$"Future implementations will handle '{nameof(SenderAppType.ReceiverUIBlazorApp)}' and other types.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// LegacyFormApp uses GdPicture14 with INCHES
|
||||||
|
return _unitOfLength switch
|
||||||
|
{
|
||||||
|
UnitOfLength.Inch => 1.0, // No conversion needed: INCHES → INCHES
|
||||||
|
UnitOfLength.Point => 72.0, // INCHES → PDF Points: 1 inch = 72 points (PDF standard, NOT pixels!)
|
||||||
|
_ => throw new InvalidOperationException(
|
||||||
|
$"Unknown UnitOfLength: {_unitOfLength}. Expected '{nameof(UnitOfLength.Inch)}' or '{nameof(UnitOfLength.Point)}'.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SignatureDtoExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts all signatures in the collection to the specified unit of length.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of the collection (IEnumerable, List, etc.)</typeparam>
|
||||||
|
/// <param name="signatures">Collection of SignatureDto objects to convert.</param>
|
||||||
|
/// <param name="unitOfLength">Target unit of measurement (Inch or Point).</param>
|
||||||
|
/// <returns>The same collection with all signatures converted to the specified unit.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when signatures collection is null.</exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// <b>Usage:</b>
|
||||||
|
/// <code>
|
||||||
|
/// var signatures = await SignatureService.GetAsync(envelopeKey);
|
||||||
|
/// var convertedSignatures = signatures.ConvertAll(UnitOfLength.Point);
|
||||||
|
/// </code>
|
||||||
|
/// <b>Note:</b> This method modifies each SignatureDto object in place and returns the same collection.
|
||||||
|
/// </remarks>
|
||||||
|
public static T Convert<T>(this T signatures, UnitOfLength unitOfLength)
|
||||||
|
where T : IEnumerable<SignatureDto>
|
||||||
|
{
|
||||||
|
if (signatures == null)
|
||||||
|
throw new ArgumentNullException(nameof(signatures));
|
||||||
|
|
||||||
|
foreach (var signature in signatures)
|
||||||
|
{
|
||||||
|
signature.Convert(unitOfLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
return signatures;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace EnvelopeGenerator.WebUI.Client.Options;
|
||||||
|
|
||||||
|
public class ApiOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "Api";
|
||||||
|
|
||||||
|
public string BaseUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool UsePredefinedReports { get; set; } = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
namespace EnvelopeGenerator.WebUI.Client.Options;
|
||||||
|
|
||||||
|
public class PdfViewerOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "PdfViewer";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base scale for thumbnail rendering (0.2 - 1.5 recommended)
|
||||||
|
/// Higher values = better quality but slower rendering
|
||||||
|
/// Default: 0.75
|
||||||
|
/// </summary>
|
||||||
|
public double ThumbnailBaseScale { get; set; } = 0.75;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable HiDPI/Retina support for thumbnails
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool ThumbnailEnableHiDPI { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum device pixel ratio multiplier for thumbnails (1.0 - 3.0)
|
||||||
|
/// Caps DPR to avoid excessive memory usage on 4K+ displays
|
||||||
|
/// Default: 2.0
|
||||||
|
/// </summary>
|
||||||
|
public double ThumbnailMaxDPR { get; set; } = 2.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable HiDPI/Retina support for main PDF canvas
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool MainCanvasEnableHiDPI { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum device pixel ratio multiplier for main canvas (1.0 - 3.0)
|
||||||
|
/// Default: 2.0
|
||||||
|
/// </summary>
|
||||||
|
public double MainCanvasMaxDPR { get; set; } = 2.0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable smooth zoom transition (fade effect)
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableSmoothZoom { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zoom transition duration in milliseconds (50 - 500)
|
||||||
|
/// Default: 150
|
||||||
|
/// </summary>
|
||||||
|
public int ZoomTransitionDuration { get; set; } = 150;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opacity during rendering (0.0 - 1.0)
|
||||||
|
/// Lower values = more visible fade effect
|
||||||
|
/// Default: 0.85
|
||||||
|
/// </summary>
|
||||||
|
public double RenderingOpacity { get; set; } = 0.85;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delay between thumbnail renders in milliseconds (10 - 200)
|
||||||
|
/// Higher values = less browser stress, slower initial load
|
||||||
|
/// Default: 50
|
||||||
|
/// </summary>
|
||||||
|
public int ThumbnailRenderDelay { get; set; } = 50;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zoom step percentage (1 - 50)
|
||||||
|
/// Controls how much zoom changes per click or scroll
|
||||||
|
/// Default: 5 (5% per step)
|
||||||
|
/// </summary>
|
||||||
|
public int ZoomStepPercentage { get; set; } = 5;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
@page "/sender"
|
||||||
|
@rendermode InteractiveWebAssembly
|
||||||
|
|
||||||
|
<h3>EnvelopeSender</h3>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
@page "/"
|
||||||
|
@rendermode InteractiveWebAssembly
|
||||||
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
|
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<div class="home-page-wrapper">
|
||||||
|
|
||||||
|
<div class="home-hero-header">
|
||||||
|
<div class="home-hero-header__inner">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="home-hero-header__icon" viewBox="0 0 16 16">
|
||||||
|
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2Zm13 2.383-4.708 2.825L15 11.105V5.383Zm-.034 6.876-5.64-3.471L8 9.583l-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h12a1 1 0 0 0 .966-.741ZM1 11.105l4.708-2.897L1 5.383v5.722Z"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<h1 class="home-hero-header__title">SignFlow</h1>
|
||||||
|
<p class="home-hero-header__subtitle">Willkommen im eSign-Portal</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="home-content">
|
||||||
|
<div class="home-card card shadow border-0">
|
||||||
|
<div class="card-body p-4 p-md-5">
|
||||||
|
|
||||||
|
<p class="text-muted mb-4" style="font-size: 0.92rem; line-height: 1.7; text-align: justify; text-align-last: left; min-height: calc(0.92rem * 1.7 * 9);">
|
||||||
|
<span id="home-description"></span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-4 pt-3 border-top">
|
||||||
|
<div class="d-flex flex-wrap justify-content-center gap-3">
|
||||||
|
<div class="home-feature-badge">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
Sicherer Zugang
|
||||||
|
</div>
|
||||||
|
<div class="home-feature-badge">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
|
||||||
|
</svg>
|
||||||
|
Digitale Unterschrift
|
||||||
|
</div>
|
||||||
|
<div class="home-feature-badge">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
||||||
|
<path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>
|
||||||
|
</svg>
|
||||||
|
PDF-Export
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private const string HomePageDescription =
|
||||||
|
"Das digitale Unterschriftenportal ist eine Plattform, die entwickelt wurde, um Ihre Dokumente sicher zu unterschreiben und zu verwalten. " +
|
||||||
|
"Mit seiner benutzerfreundlichen Oberfläche können Sie Ihre Dokumente schnell hochladen, die Unterschriftsprozesse verfolgen und Ihre digitalen Unterschriftenanwendungen einfach durchführen. " +
|
||||||
|
"Dieses Portal beschleunigt Ihren Arbeitsablauf mit rechtlich gültigen Unterschriften und erhöht gleichzeitig die Sicherheit Ihrer Dokumente.";
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
await JS.InvokeVoidAsync("receiverSignature.startTyped", "home-description", HomePageDescription, 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
@page "/envelope/login/{EnvelopeKey}"
|
||||||
|
@rendermode InteractiveWebAssembly
|
||||||
|
@using EnvelopeGenerator.WebUI.Client.Services
|
||||||
|
@inject AuthService AuthService
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
|
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<div class="login-page-wrapper d-flex align-items-center justify-content-center min-vh-100">
|
||||||
|
<div class="login-card card shadow border-0" style="max-width: 440px; width: 100%;">
|
||||||
|
|
||||||
|
<div class="card-header text-white text-center py-4 border-0" style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border-radius: calc(0.375rem - 1px) calc(0.375rem - 1px) 0 0;">
|
||||||
|
<div class="mb-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h5 class="mb-0 fw-semibold">Dokument öffnen</h5>
|
||||||
|
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">Sicherer Zugang mit Zugangscode</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body p-4">
|
||||||
|
|
||||||
|
<p class="text-muted mb-4" style="font-size: 0.875rem; line-height: 1.5;">
|
||||||
|
Bitte geben Sie den Zugangscode ein, den Sie per E-Mail erhalten haben, um das Dokument sicher zu öffnen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (LoginResult == EnvelopeLoginResult.NotFound) {
|
||||||
|
<div class="alert alert-warning d-flex align-items-start gap-2 py-2" role="alert">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<strong>Dokument nicht gefunden.</strong><br />
|
||||||
|
<span style="font-size:0.85rem;">Der angegebene Zugangscode konnte keinem Dokument zugeordnet werden. Bitte prüfen Sie den Link in Ihrer E-Mail.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else if (LoginResult == EnvelopeLoginResult.InvalidCode) {
|
||||||
|
<div class="alert alert-danger d-flex align-items-start gap-2 py-2" role="alert">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<strong>Ungültiger Zugangscode.</strong><br />
|
||||||
|
<span style="font-size:0.85rem;">Der eingegebene Code ist falsch. Bitte versuchen Sie es erneut.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else if (LoginResult == EnvelopeLoginResult.Error) {
|
||||||
|
<div class="alert alert-secondary d-flex align-items-start gap-2 py-2" role="alert">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
||||||
|
<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>
|
||||||
|
<strong>Serverfehler.</strong><br />
|
||||||
|
<span style="font-size:0.85rem;">Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-medium" for="login-access-code">
|
||||||
|
Zugangscode
|
||||||
|
<span class="text-danger ms-1">*</span>
|
||||||
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text bg-light border-end-0">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#6c757d" viewBox="0 0 16 16">
|
||||||
|
<path d="M3.5 11.5a3.5 3.5 0 1 1 3.163-5H14L15.5 8 14 9.5l-1-1-1 1-1-1-1 1-1-1-1.837 1.337A3.5 3.5 0 0 1 3.5 11.5zm0-1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input id="login-access-code"
|
||||||
|
type="@(ShowCode ? "text" : "password")"
|
||||||
|
class="form-control border-start-0 border-end-0 @(LoginResult == EnvelopeLoginResult.InvalidCode ? "is-invalid" : null)"
|
||||||
|
placeholder="Zugangscode eingeben"
|
||||||
|
@bind="AccessCode"
|
||||||
|
@bind:event="oninput"
|
||||||
|
@onkeydown="OnKeyDownAsync"
|
||||||
|
disabled="@IsLoading"
|
||||||
|
autocomplete="one-time-code" />
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-outline-secondary border-start-0"
|
||||||
|
style="border-left: none;"
|
||||||
|
tabindex="-1"
|
||||||
|
@onclick="() => ShowCode = !ShowCode">
|
||||||
|
@if (ShowCode) {
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/>
|
||||||
|
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z"/>
|
||||||
|
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709z"/>
|
||||||
|
<path fill-rule="evenodd" d="M13.646 14.354l-12-12 .708-.708 12 12-.708.708z"/>
|
||||||
|
</svg>
|
||||||
|
} else {
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
|
||||||
|
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary w-100 py-2 fw-medium"
|
||||||
|
style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border: none;"
|
||||||
|
@onclick="SubmitAsync"
|
||||||
|
disabled="@(IsLoading || string.IsNullOrWhiteSpace(AccessCode))">
|
||||||
|
@if (IsLoading) {
|
||||||
|
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||||
|
<span>Überprüfen …</span>
|
||||||
|
} else {
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
<span>Dokument öffnen</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer text-center text-muted py-3 border-0 bg-transparent" style="font-size: 0.78rem;">
|
||||||
|
Bei Problemen wenden Sie sich bitte an den Absender des Dokuments.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string EnvelopeKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
string AccessCode = string.Empty;
|
||||||
|
bool ShowCode;
|
||||||
|
bool IsLoading;
|
||||||
|
EnvelopeLoginResult? LoginResult;
|
||||||
|
|
||||||
|
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) {
|
||||||
|
if (e.Key == "Enter")
|
||||||
|
await SubmitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task SubmitAsync() {
|
||||||
|
if (string.IsNullOrWhiteSpace(AccessCode) || IsLoading) return;
|
||||||
|
|
||||||
|
IsLoading = true;
|
||||||
|
LoginResult = null;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
var result = await AuthService.LoginEnvelopeReceiverAsync(EnvelopeKey, AccessCode.Trim());
|
||||||
|
|
||||||
|
if (result == EnvelopeLoginResult.Success) {
|
||||||
|
Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginResult = result;
|
||||||
|
IsLoading = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
@page "/sender/login"
|
||||||
|
@rendermode InteractiveWebAssembly
|
||||||
|
@using EnvelopeGenerator.WebUI.Client.Services
|
||||||
|
@inject AuthService AuthService
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
|
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<div class="login-page-wrapper d-flex align-items-center justify-content-center min-vh-100">
|
||||||
|
<div class="login-card card shadow border-0" style="max-width: 440px; width: 100%;">
|
||||||
|
|
||||||
|
<div class="card-header text-white text-center py-4 border-0" style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border-radius: calc(0.375rem - 1px) calc(0.375rem - 1px) 0 0;">
|
||||||
|
<div class="mb-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
|
||||||
|
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h5 class="mb-0 fw-semibold">Sender Anmeldung</h5>
|
||||||
|
<p class="mb-0 mt-1 opacity-75" style="font-size: 0.85rem;">Sicherer Zugang zum Sender-Dashboard</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body p-4">
|
||||||
|
|
||||||
|
<p class="text-muted mb-4" style="font-size: 0.875rem; line-height: 1.5;">
|
||||||
|
Bitte melden Sie sich mit Ihren Zugangsdaten an, um auf das Sender-Dashboard zuzugreifen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (LoginResult == SenderLoginResult.InvalidCredentials) {
|
||||||
|
<div class="alert alert-danger d-flex align-items-start gap-2 py-2" role="alert">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<strong>Ungültige Anmeldedaten.</strong><br />
|
||||||
|
<span style="font-size:0.85rem;">Benutzername oder Passwort ist falsch. Bitte versuchen Sie es erneut.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else if (LoginResult == SenderLoginResult.Error) {
|
||||||
|
<div class="alert alert-secondary d-flex align-items-start gap-2 py-2" role="alert">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" class="flex-shrink-0 mt-1" viewBox="0 0 16 16">
|
||||||
|
<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>
|
||||||
|
<strong>Serverfehler.</strong><br />
|
||||||
|
<span style="font-size:0.85rem;">Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-medium" for="login-username">
|
||||||
|
Benutzername
|
||||||
|
<span class="text-danger ms-1">*</span>
|
||||||
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text bg-light">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#6c757d" viewBox="0 0 16 16">
|
||||||
|
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input id="login-username"
|
||||||
|
type="text"
|
||||||
|
class="form-control @(LoginResult == SenderLoginResult.InvalidCredentials ? "is-invalid" : null)"
|
||||||
|
placeholder="Benutzername eingeben"
|
||||||
|
@bind="Username"
|
||||||
|
@bind:event="oninput"
|
||||||
|
@onkeydown="OnKeyDownAsync"
|
||||||
|
disabled="@IsLoading"
|
||||||
|
autocomplete="username" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-medium" for="login-password">
|
||||||
|
Passwort
|
||||||
|
<span class="text-danger ms-1">*</span>
|
||||||
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text bg-light border-end-0">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#6c757d" viewBox="0 0 16 16">
|
||||||
|
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input id="login-password"
|
||||||
|
type="@(ShowPassword ? "text" : "password")"
|
||||||
|
class="form-control border-start-0 border-end-0 @(LoginResult == SenderLoginResult.InvalidCredentials ? "is-invalid" : null)"
|
||||||
|
placeholder="Passwort eingeben"
|
||||||
|
@bind="Password"
|
||||||
|
@bind:event="oninput"
|
||||||
|
@onkeydown="OnKeyDownAsync"
|
||||||
|
disabled="@IsLoading"
|
||||||
|
autocomplete="current-password" />
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-outline-secondary border-start-0"
|
||||||
|
style="border-left: none;"
|
||||||
|
tabindex="-1"
|
||||||
|
@onclick="() => ShowPassword = !ShowPassword">
|
||||||
|
@if (ShowPassword) {
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/>
|
||||||
|
<path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z"/>
|
||||||
|
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709z"/>
|
||||||
|
<path fill-rule="evenodd" d="M13.646 14.354l-12-12 .708-.708 12 12-.708.708z"/>
|
||||||
|
</svg>
|
||||||
|
} else {
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/>
|
||||||
|
<path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary w-100 py-2 fw-medium"
|
||||||
|
style="background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%); border: none;"
|
||||||
|
@onclick="SubmitAsync"
|
||||||
|
disabled="@(IsLoading || string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password))">
|
||||||
|
@if (IsLoading) {
|
||||||
|
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||||
|
<span>Anmelden …</span>
|
||||||
|
} else {
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-2" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M10 3.5a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-2a.5.5 0 0 1 1 0v2A1.5 1.5 0 0 1 9.5 14h-8A1.5 1.5 0 0 1 0 12.5v-9A1.5 1.5 0 0 1 1.5 2h8A1.5 1.5 0 0 1 11 3.5v2a.5.5 0 0 1-1 0v-2z"/>
|
||||||
|
<path fill-rule="evenodd" d="M4.146 8.354a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L5.707 7.5H14.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3z"/>
|
||||||
|
</svg>
|
||||||
|
<span>Anmelden</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer text-center text-muted py-3 border-0 bg-transparent" style="font-size: 0.78rem;">
|
||||||
|
Bei Problemen wenden Sie sich bitte an den Administrator.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
string Username = string.Empty;
|
||||||
|
string Password = string.Empty;
|
||||||
|
bool ShowPassword;
|
||||||
|
bool IsLoading;
|
||||||
|
SenderLoginResult? LoginResult;
|
||||||
|
|
||||||
|
async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) {
|
||||||
|
if (e.Key == "Enter")
|
||||||
|
await SubmitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task SubmitAsync() {
|
||||||
|
if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password) || IsLoading) return;
|
||||||
|
|
||||||
|
IsLoading = true;
|
||||||
|
LoginResult = null;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
var result = await AuthService.LoginSenderAsync(Username.Trim(), Password.Trim());
|
||||||
|
|
||||||
|
if (result == SenderLoginResult.Success) {
|
||||||
|
Navigation.NavigateTo("/sender", forceLoad: true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginResult = result;
|
||||||
|
IsLoading = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,123 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<metadata name="objectDataSource1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||||
|
<value>17, 17</value>
|
||||||
|
</metadata>
|
||||||
|
</root>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using DevExpress.XtraReports.UI;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.WebUI.Client.PredefinedReports {
|
||||||
|
public static class ReportsFactory
|
||||||
|
{
|
||||||
|
public static readonly Dictionary<string, Func<XtraReport>> Reports = new() {
|
||||||
|
["LargeDatasetReport"] = () => new PredefinedReports.Report()
|
||||||
|
};
|
||||||
|
|
||||||
|
public static XtraReport GetReport(string reportName) {
|
||||||
|
return Reports[reportName]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
|
using EnvelopeGenerator.WebUI.Client.Services;
|
||||||
|
using EnvelopeGenerator.WebUI.Client.Options;
|
||||||
|
using DevExpress.Blazor.Reporting;
|
||||||
|
using DevExpress.XtraReports.Web.Extensions;
|
||||||
|
using DevExpress.DataAccess.Web;
|
||||||
|
using DevExpress.XtraReports.Services;
|
||||||
|
|
||||||
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
|
||||||
|
// HTTP Client (uses WebUI's YARP proxy)
|
||||||
|
builder.Services.AddScoped(sp => new HttpClient {
|
||||||
|
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configuration Options
|
||||||
|
builder.Services.Configure<ApiOptions>(opts =>
|
||||||
|
builder.Configuration.GetSection(ApiOptions.SectionName).Bind(opts));
|
||||||
|
builder.Services.Configure<PdfViewerOptions>(opts =>
|
||||||
|
builder.Configuration.GetSection(PdfViewerOptions.SectionName).Bind(opts));
|
||||||
|
|
||||||
|
// Business Services
|
||||||
|
builder.Services.AddScoped<DocumentService>();
|
||||||
|
builder.Services.AddScoped<AuthService>();
|
||||||
|
builder.Services.AddScoped<AnnotationService>();
|
||||||
|
builder.Services.AddScoped<EnvelopeReceiverService>();
|
||||||
|
builder.Services.AddScoped<SignatureService>();
|
||||||
|
builder.Services.AddScoped<SignatureCacheService>();
|
||||||
|
builder.Services.AddSingleton<AppVersionService>();
|
||||||
|
|
||||||
|
// DevExpress WASM
|
||||||
|
builder.Services.AddDevExpressWebAssemblyBlazorPdfViewer();
|
||||||
|
builder.Services.AddDevExpressWebAssemblyBlazorReportViewer();
|
||||||
|
|
||||||
|
builder.Services.AddDevExpressBlazorReportingWebAssembly(configure => {
|
||||||
|
configure.UseDevelopmentMode();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reporting Services
|
||||||
|
builder.Services.AddScoped<IDataSourceWizardJsonConnectionStorage, CustomDataSourceWizardJsonDataConnectionStorage>();
|
||||||
|
builder.Services.AddScoped<IJsonDataConnectionProviderFactory, CustomJsonDataConnectionProviderFactory>();
|
||||||
|
builder.Services.AddScoped<IObjectDataSourceWizardTypeProvider, ObjectDataSourceWizardCustomTypeProvider>();
|
||||||
|
|
||||||
|
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.WebUI.Client.Data.DataItemList));
|
||||||
|
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(EnvelopeGenerator.WebUI.Client.PredefinedReports.Report));
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<InMemoryReportStorageWebExtension>();
|
||||||
|
builder.Services.AddSingleton<ReportStorageWebExtension>(sp => sp.GetRequiredService<InMemoryReportStorageWebExtension>());
|
||||||
|
builder.Services.AddScoped<IReportProviderAsync, CustomReportProvider>();
|
||||||
|
|
||||||
|
ReportStorageWebExtension.RegisterExtensionGlobal(new InMemoryReportStorageWebExtension());
|
||||||
|
|
||||||
|
var host = builder.Build();
|
||||||
|
await FontLoader.LoadFonts(host.Services.GetRequiredService<HttpClient>(), new List<string> { "opensans.ttf" });
|
||||||
|
await host.RunAsync();
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<Router AppAssembly="typeof(Program).Assembly">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||||
|
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||||
|
</Found>
|
||||||
|
</Router>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using EnvelopeGenerator.WebUI.Client.Models;
|
||||||
|
using EnvelopeGenerator.WebUI.Client.Options;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves annotation positions from the API.
|
||||||
|
/// The URL is composed as <c>{BaseUrl}/api/Annotation/{envelopeKey}</c>.
|
||||||
|
/// During development, <c>BaseUrl</c> is empty so the request resolves to the
|
||||||
|
/// YARP-proxied route on the same origin, which currently serves
|
||||||
|
/// <c>fake-data/annotations.json</c>. To switch to real data, update the
|
||||||
|
/// YARP route in <c>yarp.json</c> — no code change required.
|
||||||
|
/// </summary>
|
||||||
|
[Obsolete("Use SignatureService.")]
|
||||||
|
public class AnnotationService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<AnnotationDto>> GetAnnotationsAsync(string envelopeKey, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var url = $"{apiOptions.Value.BaseUrl}/api/Annotation/{Uri.EscapeDataString(envelopeKey)}";
|
||||||
|
var response = await http.GetAsync(url, cancel);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<List<AnnotationDto>>(_jsonOptions, cancel);
|
||||||
|
return result ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides application version for cache busting static assets.
|
||||||
|
/// Version is automatically incremented on each build via AssemblyVersion.
|
||||||
|
/// </summary>
|
||||||
|
public class AppVersionService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Current application version (e.g., "1.0.0.0")
|
||||||
|
/// </summary>
|
||||||
|
public string Version { get; }
|
||||||
|
|
||||||
|
public AppVersionService()
|
||||||
|
{
|
||||||
|
// Get version from assembly metadata
|
||||||
|
Version = typeof(AppVersionService).Assembly.GetName().Version?.ToString() ?? "1.0.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates versioned URL for static assets (cache busting)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">Asset path (e.g., "css/envelope-viewer.css")</param>
|
||||||
|
/// <returns>Versioned URL (e.g., "css/envelope-viewer.css?v=1.0.0.0")</returns>
|
||||||
|
public string GetVersionedUrl(string path) => $"{path}?v={Version}";
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using EnvelopeGenerator.WebUI.Client.Options;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||||
|
|
||||||
|
public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
|
||||||
|
|
||||||
|
public enum SenderLoginResult { Success, InvalidCredentials, Error }
|
||||||
|
|
||||||
|
public class AuthService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||||
|
{
|
||||||
|
private readonly ApiOptions _api = apiOptions.Value;
|
||||||
|
|
||||||
|
/// <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> CheckEnvelopeAccessAsync(string envelopeKey, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var response = await http.GetAsync($"{_api.BaseUrl}/api/auth/check/envelope/{Uri.EscapeDataString(envelopeKey)}", 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.
|
||||||
|
/// On success the API sets an authentication cookie automatically.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string envelopeKey, string accessCode, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var form = new MultipartFormDataContent();
|
||||||
|
form.Add(new StringContent(accessCode), "AccessCode");
|
||||||
|
|
||||||
|
var response = await http.PostAsync(
|
||||||
|
$"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(envelopeKey)}",
|
||||||
|
form, cancel);
|
||||||
|
|
||||||
|
return response.StatusCode switch
|
||||||
|
{
|
||||||
|
HttpStatusCode.OK => EnvelopeLoginResult.Success,
|
||||||
|
HttpStatusCode.Unauthorized => EnvelopeLoginResult.InvalidCode,
|
||||||
|
HttpStatusCode.NotFound => EnvelopeLoginResult.NotFound,
|
||||||
|
_ => EnvelopeLoginResult.Error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the per-envelope receiver cookie for the given envelope key.
|
||||||
|
/// Calls POST /api/auth/logout/envelope/{envelopeKey}.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> LogoutEnvelopeReceiverAsync(string envelopeKey, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var response = await http.PostAsync(
|
||||||
|
$"{_api.BaseUrl}/api/auth/logout/envelope/{Uri.EscapeDataString(envelopeKey)}",
|
||||||
|
null, cancel);
|
||||||
|
return response.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authenticates a sender user with username and password.
|
||||||
|
/// Calls POST /api/auth?cookie=true with JSON body.
|
||||||
|
/// On success the API sets an authentication cookie automatically.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<SenderLoginResult> LoginSenderAsync(string username, string password, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var requestBody = new { username, password };
|
||||||
|
|
||||||
|
var response = await http.PostAsJsonAsync(
|
||||||
|
$"{_api.BaseUrl}/api/auth?cookie=true",
|
||||||
|
requestBody, cancel);
|
||||||
|
|
||||||
|
return response.StatusCode switch
|
||||||
|
{
|
||||||
|
HttpStatusCode.OK => SenderLoginResult.Success,
|
||||||
|
HttpStatusCode.Unauthorized => SenderLoginResult.InvalidCredentials,
|
||||||
|
_ => SenderLoginResult.Error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using DevExpress.DataAccess.Json;
|
||||||
|
using DevExpress.DataAccess.Web;
|
||||||
|
using DevExpress.DataAccess.Wizard.Services;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||||
|
|
||||||
|
public class CustomDataSourceWizardJsonDataConnectionStorage : IDataSourceWizardJsonConnectionStorage
|
||||||
|
{
|
||||||
|
public static JsonDataConnection GetDefaultConnection() {
|
||||||
|
var uriJsonSource = new UriJsonSource() {
|
||||||
|
Uri = new Uri(@"https://raw.githubusercontent.com/DevExpress-Examples/DataSources/master/JSON/customers.json"),
|
||||||
|
};
|
||||||
|
return new JsonDataConnection(uriJsonSource) { StoreConnectionNameOnly = true, Name = "NWindProductsJson" };
|
||||||
|
}
|
||||||
|
public static List<JsonDataConnection> GetConnections() {
|
||||||
|
var connections = new List<JsonDataConnection> {
|
||||||
|
GetDefaultConnection()
|
||||||
|
};
|
||||||
|
return connections;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IJsonConnectionStorageService.CanSaveConnection => false;
|
||||||
|
bool IJsonConnectionStorageService.ContainsConnection(string connectionName) {
|
||||||
|
return GetConnections().Any(x => x.Name == connectionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerable<JsonDataConnection> IJsonConnectionStorageService.GetConnections() {
|
||||||
|
return GetConnections();
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonDataConnection IJsonDataConnectionProviderService.GetJsonDataConnection(string name) {
|
||||||
|
var connection = GetConnections().FirstOrDefault(x => x.Name == name);
|
||||||
|
if(connection == null)
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IJsonConnectionStorageService.SaveConnection(string connectionName, JsonDataConnection connection, bool saveCredentials) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using DevExpress.DataAccess.Json;
|
||||||
|
using DevExpress.DataAccess.Web;
|
||||||
|
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||||
|
|
||||||
|
public class CustomJsonDataConnectionProviderFactory : IJsonDataConnectionProviderFactory {
|
||||||
|
public IJsonDataConnectionProviderService Create() {
|
||||||
|
return new WebDocumentViewerJsonDataConnectionProvider(CustomDataSourceWizardJsonDataConnectionStorage.GetConnections());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WebDocumentViewerJsonDataConnectionProvider : IJsonDataConnectionProviderService
|
||||||
|
{
|
||||||
|
readonly List<JsonDataConnection> jsonDataConnections;
|
||||||
|
public WebDocumentViewerJsonDataConnectionProvider(List<JsonDataConnection> jsonDataConnections) {
|
||||||
|
this.jsonDataConnections = jsonDataConnections;
|
||||||
|
}
|
||||||
|
public JsonDataConnection GetJsonDataConnection(string name) {
|
||||||
|
var connection = jsonDataConnections.FirstOrDefault(x => x.Name == name);
|
||||||
|
if(connection == null)
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using DevExpress.XtraReports.UI;
|
||||||
|
using DevExpress.XtraReports.Services;
|
||||||
|
using EnvelopeGenerator.WebUI.Client.PredefinedReports;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||||
|
|
||||||
|
public class CustomReportProvider : IReportProviderAsync {
|
||||||
|
private readonly InMemoryReportStorageWebExtension reportStorage;
|
||||||
|
|
||||||
|
public CustomReportProvider(InMemoryReportStorageWebExtension reportStorage) {
|
||||||
|
this.reportStorage = reportStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<XtraReport> GetReportAsync(string id, ReportProviderContext context) {
|
||||||
|
if(reportStorage.TryGetReport(id, out var savedReport))
|
||||||
|
return Task.FromResult(savedReport);
|
||||||
|
|
||||||
|
return Task.FromResult(ReportsFactory.GetReport(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using EnvelopeGenerator.WebUI.Client.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||||
|
|
||||||
|
public class DocumentService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||||
|
{
|
||||||
|
private readonly ApiOptions _api = apiOptions.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches the PDF bytes for the given envelope key from the API.
|
||||||
|
/// Throws HttpRequestException on failure with appropriate status code.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
|
||||||
|
public async Task<byte[]?> GetDocumentAsync(string envelopeKey, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var response = await http.GetAsync($"{_api.BaseUrl}/api/Document/{Uri.EscapeDataString(envelopeKey)}", cancel);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var statusCode = (int)response.StatusCode;
|
||||||
|
var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
|
||||||
|
throw new HttpRequestException(
|
||||||
|
$"Failed to load document. Status: {statusCode} ({reasonPhrase})",
|
||||||
|
null,
|
||||||
|
response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = await response.Content.ReadAsByteArrayAsync(cancel);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using EnvelopeGenerator.WebUI.Client.Models;
|
||||||
|
using EnvelopeGenerator.WebUI.Client.Options;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the <see cref="EnvelopeReceiverDto"/> for the authenticated receiver
|
||||||
|
/// from <c>GET api/EnvelopeReceiver/{envelopeKey}</c>.
|
||||||
|
/// </summary>
|
||||||
|
public class EnvelopeReceiverService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fetches the envelope receiver data for the given envelope key from the API.
|
||||||
|
/// Throws HttpRequestException on failure with appropriate status code.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="HttpRequestException">Thrown when the API request fails.</exception>
|
||||||
|
public async Task<EnvelopeReceiverDto?> GetAsync(string envelopeKey, CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var url = $"{apiOptions.Value.BaseUrl}/api/EnvelopeReceiver/{Uri.EscapeDataString(envelopeKey)}";
|
||||||
|
var response = await http.GetAsync(url, cancel);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var statusCode = (int)response.StatusCode;
|
||||||
|
var reasonPhrase = response.ReasonPhrase ?? "Unknown error";
|
||||||
|
throw new HttpRequestException(
|
||||||
|
$"Failed to load envelope receiver data. Status: {statusCode} ({reasonPhrase})",
|
||||||
|
null,
|
||||||
|
response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.Content.ReadFromJsonAsync<EnvelopeReceiverDto>(_jsonOptions, cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using DevExpress.Drawing;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||||
|
|
||||||
|
public static class FontLoader {
|
||||||
|
public async static Task LoadFonts(HttpClient httpClient, List<string> fontNames) {
|
||||||
|
foreach(var fontName in fontNames) {
|
||||||
|
var fontBytes = await httpClient.GetByteArrayAsync($"fonts/{fontName}");
|
||||||
|
DXFontRepository.Instance.AddFont(fontBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using DevExpress.XtraReports.UI;
|
||||||
|
using DevExpress.XtraReports.Web.Extensions;
|
||||||
|
using EnvelopeGenerator.WebUI.Client.PredefinedReports;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||||
|
|
||||||
|
public class InMemoryReportStorageWebExtension : ReportStorageWebExtension
|
||||||
|
{
|
||||||
|
private const string DefaultReportName = "LargeDatasetReport";
|
||||||
|
private static readonly Dictionary<string, byte[]> Reports = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public override bool CanSetData(string url) => IsValidUrl(url);
|
||||||
|
|
||||||
|
public override byte[] GetData(string url)
|
||||||
|
{
|
||||||
|
url = NormalizeUrl(url);
|
||||||
|
|
||||||
|
if (Reports.TryGetValue(url, out var reportLayout))
|
||||||
|
return reportLayout;
|
||||||
|
|
||||||
|
if (ReportsFactory.Reports.TryGetValue(url, out var reportFactory))
|
||||||
|
return SaveReport(reportFactory());
|
||||||
|
|
||||||
|
throw new DevExpress.XtraReports.Web.ClientControls.FaultException($"Report '{url}' was not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Dictionary<string, string> GetUrls()
|
||||||
|
{
|
||||||
|
var urls = ReportsFactory.Reports.Keys
|
||||||
|
.Concat(Reports.Keys)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(name => name, name => name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool IsValidUrl(string url)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(url)
|
||||||
|
&& url.IndexOfAny(Path.GetInvalidFileNameChars()) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetData(XtraReport report, string url)
|
||||||
|
{
|
||||||
|
url = NormalizeUrl(url);
|
||||||
|
Reports[url] = SaveReport(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string SetNewData(XtraReport report, string defaultUrl)
|
||||||
|
{
|
||||||
|
var url = NormalizeUrl(defaultUrl);
|
||||||
|
Reports[url] = SaveReport(report);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetReport(string url, out XtraReport report)
|
||||||
|
{
|
||||||
|
url = NormalizeUrl(url);
|
||||||
|
|
||||||
|
if (!Reports.ContainsKey(url))
|
||||||
|
{
|
||||||
|
report = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var stream = new MemoryStream(Reports[url]);
|
||||||
|
report = XtraReport.FromXmlStream(stream, true);
|
||||||
|
report.Name = url;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeUrl(string url)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(url) ? DefaultReportName : url;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] SaveReport(XtraReport report)
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
report.SaveLayoutToXml(stream);
|
||||||
|
return stream.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using DevExpress.DataAccess.Web;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||||
|
|
||||||
|
public class ObjectDataSourceWizardCustomTypeProvider : IObjectDataSourceWizardTypeProvider {
|
||||||
|
public IEnumerable<Type> GetAvailableTypes(string context) {
|
||||||
|
return new[] { typeof(Data.DataItemList) };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using EnvelopeGenerator.WebUI.Client.Options;
|
||||||
|
using EnvelopeGenerator.WebUI.Client.Models;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client service for managing cached signatures via API.
|
||||||
|
/// </summary>
|
||||||
|
public class SignatureCacheService(HttpClient http, IOptions<ApiOptions> apiOptions)
|
||||||
|
{
|
||||||
|
private readonly ApiOptions _api = apiOptions.Value;
|
||||||
|
|
||||||
|
public async Task SaveSignatureAsync(
|
||||||
|
string envelopeKey,
|
||||||
|
SignatureCaptureDto signature,
|
||||||
|
CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var response = await http.PostAsJsonAsync(
|
||||||
|
$"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
|
||||||
|
signature,
|
||||||
|
cancel);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync(cancel);
|
||||||
|
throw new HttpRequestException($"Failed to cache signature: {response.StatusCode} - {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SignatureCaptureDto?> GetSignatureAsync(
|
||||||
|
string envelopeKey,
|
||||||
|
CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var response = await http.GetAsync(
|
||||||
|
$"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
|
||||||
|
cancel);
|
||||||
|
|
||||||
|
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync(cancel);
|
||||||
|
throw new HttpRequestException($"Failed to retrieve signature: {response.StatusCode} - {error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.Content.ReadFromJsonAsync<SignatureCaptureDto>(cancellationToken: cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteSignatureAsync(
|
||||||
|
string envelopeKey,
|
||||||
|
CancellationToken cancel = default)
|
||||||
|
{
|
||||||
|
var response = await http.DeleteAsync(
|
||||||
|
$"{_api.BaseUrl}/api/Cache/SignatureCapture/{Uri.EscapeDataString(envelopeKey)}",
|
||||||
|
cancel);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync(cancel);
|
||||||
|
throw new HttpRequestException($"Failed to delete signature: {response.StatusCode} - {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using EnvelopeGenerator.WebUI.Client.Models;
|
||||||
|
using EnvelopeGenerator.WebUI.Client.Options;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace EnvelopeGenerator.WebUI.Client.Services;
|
||||||
|
|
||||||
|
public class SignatureService(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 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,16 @@
|
|||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using EnvelopeGenerator.WebUI.Client
|
||||||
|
@using EnvelopeGenerator.WebUI.Client.Services
|
||||||
|
@using EnvelopeGenerator.WebUI.Client.Models
|
||||||
|
@using EnvelopeGenerator.WebUI.Client.Options
|
||||||
|
@using DevExpress.Blazor
|
||||||
|
@using DevExpress.Blazor.PdfViewer
|
||||||
|
@using DevExpress.Blazor.Reporting
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<base href="/" />
|
||||||
|
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" href="css/app.css" />
|
||||||
|
<link rel="stylesheet" href="css/envelope-viewer.css" />
|
||||||
|
<link rel="stylesheet" href="EnvelopeGenerator.WebUI.styles.css" />
|
||||||
|
<HeadOutlet @rendermode="InteractiveAuto" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<Routes @rendermode="InteractiveAuto" />
|
||||||
|
<script src="_content/DevExpress.Blazor.Resources/js/preload-script.js"></script>
|
||||||
|
<script src="js/typed.umd.js"></script>
|
||||||
|
<script src="js/receiver-signature.js?v=9"></script>
|
||||||
|
<script src="_framework/blazor.web.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,113 @@
|
|||||||
|
@page "/envelope/DxPdfViewer"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
@using System.IO
|
||||||
|
@using DevExpress.Blazor
|
||||||
|
@using System.Reflection
|
||||||
|
|
||||||
|
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.custom-drop-zone {
|
||||||
|
padding: 0 !important;
|
||||||
|
border-style: dashed;
|
||||||
|
border-width: 2px !important;
|
||||||
|
height: 230px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgba(183, 183, 183, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-drop-zone.custom-drop-zone-hover {
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-drop-zone svg {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-drop-zone > *:not(#overviewDemoSelectButton) {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer {
|
||||||
|
height: 800px !important;
|
||||||
|
min-height: 800px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer .dxbrv-surface-wrapper,
|
||||||
|
.pdf-viewer .dxbrv-document-surface {
|
||||||
|
height: 100% !important;
|
||||||
|
min-height: 750px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer .dxbrv-report-preview-content {
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
min-width: 200px !important;
|
||||||
|
min-height: 200px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="overviewDemoDropZone" class="card custom-drop-zone rounded-3 w-100 m-0">
|
||||||
|
<span class="drop-file-icon mb-3"></span>
|
||||||
|
<span class="drop-file-label">Drag and Drop File Here</span><span class="m-1">or</span>
|
||||||
|
<DxButton Id="overviewDemoSelectButton"
|
||||||
|
CssClass="m-1"
|
||||||
|
RenderStyle="ButtonRenderStyle.Primary"
|
||||||
|
Text="Select File" />
|
||||||
|
</div>
|
||||||
|
<DxFileInput @ref="fileInput"
|
||||||
|
AcceptedFileTypes="@ALLOWED_FILE_TYPES"
|
||||||
|
AllowedFileExtensions="@ALLOWED_FILE_TYPES"
|
||||||
|
CssClass="w-100"
|
||||||
|
ExternalDropZoneCssSelector="#overviewDemoDropZone"
|
||||||
|
ExternalDropZoneDragOverCssClass="custom-drop-zone-hover"
|
||||||
|
ExternalSelectButtonCssSelector="#overviewDemoSelectButton"
|
||||||
|
FilesUploading="OnFilesUploading"
|
||||||
|
MaxFileSize="2000000">
|
||||||
|
</DxFileInput>
|
||||||
|
|
||||||
|
@if (DocumentContent != null && DocumentContent.Length > 0)
|
||||||
|
{
|
||||||
|
<div class="alert alert-success mt-3">
|
||||||
|
PDF loaded: @DocumentContent.Length bytes
|
||||||
|
</div>
|
||||||
|
<DxPdfViewer CssClass="w-100 pdf-viewer" DocumentContent="@DocumentContent" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="alert alert-info mt-3">
|
||||||
|
Please upload a PDF file to view it.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
readonly List<string> ALLOWED_FILE_TYPES = new List<string> { ".pdf" };
|
||||||
|
DxFileInput fileInput;
|
||||||
|
byte[] DocumentContent { get; set; }
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||||
|
Stream stream = assembly.GetManifestResourceStream("EnvelopeGenerator.WebUI.Resources.Invoice.pdf");
|
||||||
|
if (stream != null)
|
||||||
|
{
|
||||||
|
using (stream)
|
||||||
|
using (var binaryReader = new BinaryReader(stream))
|
||||||
|
DocumentContent = binaryReader.ReadBytes((int)stream.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected async Task OnFilesUploading(FilesUploadingEventArgs args)
|
||||||
|
{
|
||||||
|
using (MemoryStream stream = new MemoryStream())
|
||||||
|
{
|
||||||
|
IFileInputSelectedFile file = args.Files[0];
|
||||||
|
await file.OpenReadStream(file.Size).CopyToAsync(stream);
|
||||||
|
DocumentContent = stream.ToArray();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
@page "/envelope/{EnvelopeKey}/DxReportViewer"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
@using XtraReport = DevExpress.XtraReports.UI.XtraReport
|
||||||
|
@using DevExpress.Blazor.Reporting
|
||||||
|
@using Microsoft.Extensions.Options
|
||||||
|
@using EnvelopeGenerator.WebUI.Client.Options
|
||||||
|
@using EnvelopeGenerator.WebUI.Client.Services
|
||||||
|
@inject InMemoryReportStorageWebExtension ReportStorage
|
||||||
|
@inject DocumentService DocumentService
|
||||||
|
@inject IOptions<ApiOptions> AppOptions
|
||||||
|
|
||||||
|
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||||
|
<link href="_content/DevExpress.Blazor.Reporting.Viewer/css/dx-blazor-reporting-components.bs5.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
|
||||||
|
@if (_report is not null) {
|
||||||
|
<DxReportViewer Report="_report" RootCssClasses="w-100 h-100" Zoom="1.3" />
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string EnvelopeKey { get; init; } = null!;
|
||||||
|
|
||||||
|
XtraReport? _report = null;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
_report = await CreateReport();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<XtraReport> CreateReport()
|
||||||
|
{
|
||||||
|
if (AppOptions.Value.UsePredefinedReports)
|
||||||
|
{
|
||||||
|
return Client.PredefinedReports.ReportsFactory.GetReport("LargeDatasetReport");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
var pdfBytes = await DocumentService.GetDocumentAsync(EnvelopeKey);
|
||||||
|
if (pdfBytes is null || pdfBytes.Length == 0)
|
||||||
|
throw new InvalidOperationException($"No PDF bytes found for EnvelopeKey: {EnvelopeKey}");
|
||||||
|
|
||||||
|
var report = new XtraReport();
|
||||||
|
var detail = new DevExpress.XtraReports.UI.DetailBand();
|
||||||
|
report.Bands.Add(detail);
|
||||||
|
detail.Controls.Add(new DevExpress.XtraReports.UI.XRPdfContent { Source = pdfBytes, GenerateOwnPages = true });
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
@page "/envelope/Embed"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
@using System.IO
|
||||||
|
@using DevExpress.Blazor
|
||||||
|
@using System.Reflection
|
||||||
|
|
||||||
|
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.custom-drop-zone {
|
||||||
|
padding: 0 !important;
|
||||||
|
border-style: dashed;
|
||||||
|
border-width: 2px !important;
|
||||||
|
height: 230px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgba(183, 183, 183, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-drop-zone.custom-drop-zone-hover {
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-drop-zone svg {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-drop-zone > *:not(#overviewDemoSelectButton) {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer {
|
||||||
|
height: 800px !important;
|
||||||
|
min-height: 800px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer .dxbrv-surface-wrapper,
|
||||||
|
.pdf-viewer .dxbrv-document-surface {
|
||||||
|
height: 100% !important;
|
||||||
|
min-height: 750px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer .dxbrv-report-preview-content {
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
min-width: 200px !important;
|
||||||
|
min-height: 200px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="overviewDemoDropZone" class="card custom-drop-zone rounded-3 w-100 m-0">
|
||||||
|
<span class="drop-file-icon mb-3"></span>
|
||||||
|
<span class="drop-file-label">Drag and Drop File Here</span><span class="m-1">or</span>
|
||||||
|
<DxButton Id="overviewDemoSelectButton"
|
||||||
|
CssClass="m-1"
|
||||||
|
RenderStyle="ButtonRenderStyle.Primary"
|
||||||
|
Text="Select File" />
|
||||||
|
</div>
|
||||||
|
<DxFileInput @ref="fileInput"
|
||||||
|
AcceptedFileTypes="@ALLOWED_FILE_TYPES"
|
||||||
|
AllowedFileExtensions="@ALLOWED_FILE_TYPES"
|
||||||
|
CssClass="w-100"
|
||||||
|
ExternalDropZoneCssSelector="#overviewDemoDropZone"
|
||||||
|
ExternalDropZoneDragOverCssClass="custom-drop-zone-hover"
|
||||||
|
ExternalSelectButtonCssSelector="#overviewDemoSelectButton"
|
||||||
|
FilesUploading="OnFilesUploading"
|
||||||
|
MaxFileSize="2000000">
|
||||||
|
</DxFileInput>
|
||||||
|
|
||||||
|
@if (DocumentContent != null && DocumentContent.Length > 0)
|
||||||
|
{
|
||||||
|
<div class="alert alert-success mt-3">
|
||||||
|
PDF loaded: @DocumentContent.Length bytes
|
||||||
|
</div>
|
||||||
|
<embed src="@GetPdfDataUrl()" type="application/pdf" class="w-100 pdf-viewer" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="alert alert-info mt-3">
|
||||||
|
Please upload a PDF file to view it.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
readonly List<string> ALLOWED_FILE_TYPES = new List<string> { ".pdf" };
|
||||||
|
DxFileInput fileInput;
|
||||||
|
byte[] DocumentContent { get; set; }
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||||
|
Stream stream = assembly.GetManifestResourceStream("EnvelopeGenerator.WebUI.Resources.Invoice.pdf");
|
||||||
|
if (stream != null)
|
||||||
|
{
|
||||||
|
using (stream)
|
||||||
|
using (var binaryReader = new BinaryReader(stream))
|
||||||
|
DocumentContent = binaryReader.ReadBytes((int)stream.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task OnFilesUploading(FilesUploadingEventArgs args)
|
||||||
|
{
|
||||||
|
using (MemoryStream stream = new MemoryStream())
|
||||||
|
{
|
||||||
|
IFileInputSelectedFile file = args.Files[0];
|
||||||
|
await file.OpenReadStream(file.Size).CopyToAsync(stream);
|
||||||
|
DocumentContent = stream.ToArray();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetPdfDataUrl()
|
||||||
|
{
|
||||||
|
if (DocumentContent == null || DocumentContent.Length == 0)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
string base64 = Convert.ToBase64String(DocumentContent);
|
||||||
|
return $"data:application/pdf;base64,{base64}#toolbar=0&navpanes=0&scrollbar=1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user